Programmation Orientée Jeux

Jeu de Platforme - Les collisions

Gestion des collisions

Passons maintenant à un point un peu plus délicat: les collisions ! Si l'on récapitule tout ce qui est à présent disponible, nous avons:

Les objectifs vont être les suivants:

1) Mise en place du système de collision

Si l'on regarde notre tilesheet, on voudrait que les 6 premiers tiles soit bloquant. Ajoutez donc un fichier collisions.xml dans le dossier qui contient votre tilesheet avec ce code:

<?xml version="1.0" encoding="UTF-8"?>
<collisions>
    <collision name="GROUND" pattern="0" start="1" end="1"/>
    <collision name="BLOCK" pattern="0" start="2" end="2"/>
    <collision name="TUBE" pattern="0" start="3" end="4"/>
    <collision name="WALL" pattern="0" start="5" end="6"/>
</collisions>

C'est applicable quelque soit le nombre de tilesheet qu'il y a, et il est facile d'ajouter de nouveau types de collisions ! Et il se trouve que ce fichier sera chargé comme par magie dans votre Map. Completez maintenant la classe TileCollision:

/**
 * List of tile collisions.
 *
 * @author Pierre-Alexandre (contact@b3dgs.com)
 */
enum TileCollision
{
    /** Ground collision. */
    GROUND,
    /** Block collision. */
    BLOCK,
    /** Wall collision. */
    WALL,
    /** Tube collision. */
    TUBE,
    /** No collision. */
    NONE;

    /** Vertical collisions list. */
    static final EnumSet<TileCollision> COLLISION_VERTICAL = EnumSet.noneOf(TileCollision.class);
    /** Horizontal collisions list. */
    static final EnumSet<TileCollision> COLLISION_HORIZONTAL = EnumSet.noneOf(TileCollision.class);

    /**
     * Static init.
     */
    static
    {
        TileCollision.COLLISION_VERTICAL.add(TileCollision.GROUND);
        TileCollision.COLLISION_VERTICAL.add(TileCollision.BLOCK);
        TileCollision.COLLISION_VERTICAL.add(TileCollision.TUBE);

        TileCollision.COLLISION_HORIZONTAL.add(TileCollision.GROUND);
        TileCollision.COLLISION_HORIZONTAL.add(TileCollision.BLOCK);
        TileCollision.COLLISION_HORIZONTAL.add(TileCollision.TUBE);
        TileCollision.COLLISION_HORIZONTAL.add(TileCollision.WALL);
    }
}

Cette classe va permettre de lister les différentes collisions que nous géreront. Ajoutez également une nouvelle classe EntityCollisionTileCategory:

/**
 * List of entity collision categories on tile.
 *
 * @author Pierre-Alexandre (contact@b3dgs.com)
 */
enum EntityCollisionTileCategory implements CollisionTileCategory<TileCollision>
{
    /** Default ground center collision. */
    GROUND_CENTER(TileCollision.COLLISION_VERTICAL),
    /** Ground leg left. */
    LEG_LEFT(TileCollision.COLLISION_VERTICAL),
    /** Ground leg right. */
    LEG_RIGHT(TileCollision.COLLISION_VERTICAL),
    /** Horizontal knee left. */
    KNEE_LEFT(TileCollision.COLLISION_HORIZONTAL),
    /** Horizontal knee right. */
    KNEE_RIGHT(TileCollision.COLLISION_HORIZONTAL);

    /** The collisions list. */
    private final EnumSet<TileCollision> collisions;

    /**
     * Constructor.
     *
     * @param collisions The collisions list.
     */
    private EntityCollisionTileCategory(EnumSet<TileCollision> collisions)
    {
        this.collisions = collisions;
    }

    /*
     * CollisionTileCategory
     */

    @Override
    public EnumSet<TileCollision> getCollisions()
    {
        return collisions;
    }
}

Mettez à jour la classe Tile, afin d'implémenter les points de collision:

/**
 * Tile implementation.
 *
 * @author Pierre-Alexandre (contact@b3dgs.com)
 */
final class Tile
        extends TilePlatform<TileCollision>
{
    /**
     * @see TilePlatform#TilePlatform(int, int, Integer, int, Enum)
     */
    Tile(int width, int height, Integer pattern, int number, TileCollision collision)
    {
        super(width, height, pattern, number, collision);
    }

    /*
     * TilePlatform
     */

    @Override
    public Double getCollisionX(Localizable localizable)
    {
        final int top = getTop();

        if (getCollision() == TileCollision.WALL || localizable.getLocationOldY() < top)
        {
            // From left
            if (localizable.getLocationOldX() < localizable.getLocationX())
            {
                final int left = getLeft();
                if (localizable.getLocationX() >= left)
                {
                    return Double.valueOf(left);
                }
            }
            // From right
            if (localizable.getLocationOldX() > localizable.getLocationX())
            {
                final int right = getRight();
                if (localizable.getLocationX() <= right)
                {
                    return Double.valueOf(right);
                }
            }
        }
        return null;
    }

    @Override
    public Double getCollisionY(Localizable localizable)
    {
        // From top
        final int top = getTop();
        final int bottom = getTop() - 2;
        if (localizable.getLocationOldY() >= bottom && localizable.getLocationY() <= top)
        {
            return Double.valueOf(top);
        }
        return null;
    }

    @Override
    public int getTop()
    {
        return super.getTop() - 8;
    }
}

Completez la classe Entity pour définir le traitement de la collision:

/**
 * Abstract entity base implementation.
 *
 * @author Pierre-Alexandre (contact@b3dgs.com)
 */
abstract class Entity
        extends EntityPlatform
{
    /** Map reference. */
    protected final Map map;
    /** Desired fps value. */
    protected final int desiredFps;
    /** Movement force. */
    protected final Movement movement;
    /** Movement jump force. */
    protected final Force jumpForce;
    /** Animations list. */
    private final EnumMap<EntityState, Animation> animations;
    /** Jump force. */
    protected double jumpForceValue;
    /** Movement max speed. */
    protected double movementSpeedValue;
    /** Key right state. */
    protected boolean right;
    /** Key left state. */
    protected boolean left;
    /** Key up state. */
    protected boolean up;
    /** Entity state. */
    protected EntityState state;
    /** Old state. */
    protected EntityState stateOld;
    /** Collision state. */
    protected EntityCollision coll;
    /** Dead flag. */
    protected boolean dead;

    /**
     * Constructor.
     *
     * @param setup The setup reference.
     */
    protected Entity(SetupEntity setup)
    {
        super(setup);
        map = setup.map;
        desiredFps = setup.desiredFps;
        animations = new EnumMap<>(EntityState.class);
        jumpForceValue = getDataDouble("jumpSpeed", "data");
        movementSpeedValue = getDataDouble("movementSpeed", "data");
        movement = new Movement();
        jumpForce = new Force();
        state = EntityState.IDLE;
        setMass(getDataDouble("mass", "data"));
        setFrameOffsets(0, 9);
        setCollision(getDataCollision("default"));
        loadAnimations();
        addCollisionTile(EntityCollisionTileCategory.GROUND_CENTER, 0, 0);
        addCollisionTile(EntityCollisionTileCategory.KNEE_LEFT, -5, 9);
        addCollisionTile(EntityCollisionTileCategory.KNEE_RIGHT, 5, 9);
    }

    /**
     * Called when hit this entity.
     *
     * @param entity The entity hit.
     */
    public abstract void onHitThat(Entity entity);

    /**
     * Called when get hurt.
     *
     * @param entity Entity hitting this.
     */
    protected abstract void onHurtBy(EntityGame entity);

    /**
     * Check if hero can jump.
     *
     * @return <code>true</code> if can jump, <code>false</code> else.
     */
    public boolean canJump()
    {
        return isOnGround();
    }

    /**
     * Check if hero is jumping.
     *
     * @return <code>true</code> if jumping, <code>false</code> else.
     */
    public boolean isJumping()
    {
        return getLocationY() > getLocationOldY();
    }

    /**
     * Check if hero is falling.
     *
     * @return <code>true</code> if falling, <code>false</code> else.
     */
    public boolean isFalling()
    {
        return getLocationY() < getLocationOldY();
    }

    /**
     * Check if entity is on ground.
     *
     * @return <code>true</code> if on ground, <code>false</code> else.
     */
    public boolean isOnGround()
    {
        return coll == EntityCollision.GROUND;
    }

    /**
     * Check if entity is dead.
     *
     * @return <code>true</code> if dead, <code>false</code> else.
     */
    public boolean isDead()
    {
        return dead;
    }

    /**
     * Called when horizontal collision occurred.
     */
    protected void onHorizontalCollision()
    {
        // Nothing by default
    }

    /**
     * Load all existing animations defined in the xml file.
     */
    private void loadAnimations()
    {
        for (final EntityState state : EntityState.values())
        {
            try
            {
                animations.put(state, getDataAnimation(state.getAnimationName()));
            }
            catch (final LionEngineException exception)
            {
                continue;
            }
        }
    }

    /**
     * Check the map limit and apply collision if necessary.
     */
    private void checkMapLimit()
    {
        final int limitLeft = 0;
        if (getLocationX() < limitLeft)
        {
            setLocationX(limitLeft);
            movement.reset();
        }
        final int limitRight = map.getWidthInTile() * map.getTileWidth();
        if (getLocationX() > limitRight)
        {
            setLocationX(limitRight);
            movement.reset();
        }
    }

    /**
     * Update the forces depending of the pressed key.
     */
    private void updateForces()
    {
        movement.setForceToReach(Force.ZERO);
        final double speed;
        if (right && !left)
        {
            speed = movementSpeedValue;
        }
        else if (left && !right)
        {
            speed = -movementSpeedValue;
        }
        else
        {
            speed = 0.0;
        }
        movement.setForceToReach(speed, 0.0);

        if (up && canJump())
        {
            jumpForce.setForce(0.0, jumpForceValue);
            resetGravity();
            coll = EntityCollision.NONE;
        }
    }

    /**
     * Update entity states.
     */
    private void updateStates()
    {
        final double diffHorizontal = getDiffHorizontal();
        stateOld = state;

        if (diffHorizontal != 0.0)
        {
            mirror(diffHorizontal < 0.0);
        }

        final boolean mirror = getMirror();
        if (!isOnGround())
        {
            state = EntityState.JUMP;
        }
        else if (mirror && right && diffHorizontal < 0.0)
        {
            state = EntityState.TURN;
        }
        else if (!mirror && left && diffHorizontal > 0.0)
        {
            state = EntityState.TURN;
        }
        else if (diffHorizontal != 0.0)
        {
            state = EntityState.WALK;
        }
        else
        {
            state = EntityState.IDLE;
        }
        if (dead)
        {
            state = EntityState.DEAD;
        }
    }

    /**
     * Check the horizontal collision.
     *
     * @param category The collision category.
     */
    private void checkHorizontal(EntityCollisionTileCategory category)
    {
        final Tile tile = getCollisionTile(map, category);
        if (tile != null)
        {
            final Double x = tile.getCollisionX(this);
            if (applyHorizontalCollision(x))
            {
                movement.reset();
                onHorizontalCollision();
            }
        }
    }

    /**
     * Check the vertical collision.
     *
     * @param category The collision category.
     */
    protected void checkVertical(EntityCollisionTileCategory category)
    {
        final Tile tile = getCollisionTile(map, category);
        if (tile != null)
        {
            final Double y = tile.getCollisionY(this);
            if (applyVerticalCollision(y))
            {
                jumpForce.setForce(Force.ZERO);
                resetGravity();
                coll = EntityCollision.GROUND;
            }
            else
            {
                coll = EntityCollision.NONE;
            }
        }
    }

    /*
     * EntityPlatform
     */

    @Override
    protected void handleActions(double extrp)
    {
        if (!dead)
        {
            updateForces();
        }
        updateStates();
    }

    @Override
    protected void handleMovements(double extrp)
    {
        movement.update(extrp);
        updateGravity(extrp, desiredFps, jumpForce, movement.getForce());
        updateMirror();
    }

    @Override
    protected void handleCollisions(double extrp)
    {
        checkMapLimit();
        coll = EntityCollision.NONE;

        // Horizontal collision
        if (getDiffHorizontal() < 0)
        {
            checkHorizontal(EntityCollisionTileCategory.KNEE_LEFT);
        }
        else if (getDiffHorizontal() > 0)
        {
            checkHorizontal(EntityCollisionTileCategory.KNEE_RIGHT);
        }

        // Vertical collision
        if (getDiffVertical() < 0 || isOnGround())
        {
            checkVertical(EntityCollisionTileCategory.GROUND_CENTER);
        }
    }

    @Override
    protected void handleAnimations(double extrp)
    {
        // Assign an animation for each state
        if (state == EntityState.WALK)
        {
            setAnimSpeed(Math.abs(movement.getForce().getForceHorizontal()) / 12.0);
        }
        // Play the assigned animation
        if (stateOld != state)
        {
            play(animations.get(state));
        }
        updateAnimation(extrp);
    }
}

Mettez aussi à jour l'implémentation de la classe Mario:

/**
 * Implementation of our controllable entity.
 *
 * @author Pierre-Alexandre (contact@b3dgs.com)
 */
public final class Mario
        extends Entity
{
    /** Dead timer. */
    private final Timing timerDie;
    /** Dead step. */
    private int stepDie;
    /** Die location. */
    private double locationDie;

    /**
     * Constructor.
     *
     * @param setup setup reference.
     */
    public Mario(SetupEntity setup)
    {
        super(setup);
        timerDie = new Timing();
        addCollisionTile(EntityCollisionTileCategory.LEG_LEFT, -5, 0);
        addCollisionTile(EntityCollisionTileCategory.LEG_RIGHT, 5, 0);
    }

    /**
     * Update the mario controls.
     *
     * @param keyboard The keyboard reference.
     */
    public void updateControl(Keyboard keyboard)
    {
        if (!dead)
        {
            right = keyboard.isPressed(Key.RIGHT);
            left = keyboard.isPressed(Key.LEFT);
            up = keyboard.isPressed(Key.UP);
        }
    }

    /**
     * Kill mario.
     */
    public void kill()
    {
        dead = true;
        movement.reset();
        locationDie = getLocationY();
        stepDie = 0;
        timerDie.start();
    }

    /**
     * Respawn mario.
     */
    public void respawn()
    {
        mirror(false);
        teleport(80, 25);
        timerDie.stop();
        stepDie = 0;
        dead = false;
        movement.reset();
        jumpForce.setForce(Force.ZERO);
        resetGravity();
    }

    /*
     * Entity
     */

    @Override
    public void onHurtBy(EntityGame entity)
    {
        if (!dead)
        {
            kill();
        }
    }

    @Override
    public void onHitThat(Entity entity)
    {
        if (!isJumping())
        {
            jumpForce.setForce(0.0, jumpForceValue / 1.5);
            resetGravity();
        }
    }

    @Override
    protected void handleMovements(double extrp)
    {
        if (!dead)
        {
            // Smooth walking speed...
            final double speed;
            final double sensibility;
            if (right || left)
            {
                speed = 0.3;
                sensibility = 0.01;
            }
            // ...but quick stop
            else
            {
                speed = 0.5;
                sensibility = 0.1;
            }
            movement.setVelocity(speed);
            movement.setSensibility(sensibility);
        }

        // Die
        if (dead)
        {
            if (timerDie.elapsed(500))
            {
                // Die effect
                if (stepDie == 0)
                {
                    jumpForce.setForce(0.0, jumpForceValue);
                    stepDie = 1;
                }
                // Respawn
                if (stepDie == 1 && timerDie.elapsed(2000))
                {
                    respawn();
                }
            }
            // Lock mario
            else
            {
                resetGravity();
                setLocationY(locationDie);
            }
        }
        super.handleMovements(extrp);
    }

    @Override
    protected void handleCollisions(double extrp)
    {
        if (!dead)
        {
            super.handleCollisions(extrp);

            // Vertical collision
            if (getDiffVertical() < 0 || isOnGround())
            {
                checkVertical(EntityCollisionTileCategory.LEG_LEFT);
                checkVertical(EntityCollisionTileCategory.LEG_RIGHT);
            }

            // Kill when fall down
            if (getLocationY() < 0)
            {
                kill();
            }
        }
    }
}

En supplément, afin d'avoir des collisions propres, vous pouvez ajouter le code suivant dans la classe Map, et appeler cette methode après le load de la map dans la classe World:

   /**
     * Adjust the collision.
     */
    void adjustCollisions()
    {
        for (int tx = 0; tx < getWidthInTile(); tx++)
        {
            for (int ty = 0; ty < getHeightInTile(); ty++)
            {
                final Tile tile = getTile(tx, ty);
                final Tile top = getTile(tx, ty + 1);
                if (top != null && tile != null && tile.getCollision() != TileCollision.NONE
                        && top.getCollision() == tile.getCollision())
                {
                    tile.setCollision(TileCollision.WALL);
                }
            }
        }
    }

Et voilà, votre personnage peut maintenant sauter et tomber, le tout sans passer au travers des obstacles !

Modifiez la classe World afin d'afficher un background aux couleurs originales, et le tour est joué !

     /** Background color. */
    private static final ColorRgba BACKGROUND_COLOR = new ColorRgba(107, 136, 255);

    @Override
    public void render(Graphic g)
    {
        g.setColor(World.BACKGROUND_COLOR);
        g.drawRect(0, 0, width, height, true);
        map.render(g, camera);
        mario.render(g, camera);
    }
Des collisions !

2) Explications

Vous noterez l'apparition de la classe suivante:

Et des modifications apportées, notamment dans la classe Tile.

Grâce à cet ensemble de fonction, il devient très facile de définir de nouvelles collisions, et surtout d'affiner leur précision (exemple avec la zone du pied gauche et droit de Mario).


Lire la suite: Jeu de Plateforme - Les Monstres

Programmation Orientée Jeux ©2010-2014
Byron 3D Games Studio