Vous voilà déjà pas mal avancé ! Maintenant qu'il est possible d'importer/sauver/charger un niveau, je vous propose de rajouter un personnage permettant de se déplacer. Nous verrons ensuite comment établir un scrolling en fonction de la position du joueur.
Voici la marche à suivre:
entities/
, dans un fichier mario.xml
:
Pour mieux comprendre comment ça marche:<entity name="mario" surface="mario.png">
<frames horizontal="7" vertical="1"/>
<size width="16" height="16"/>
<data movementSpeed="3.0" jumpSpeed="8.0" mass="2.5"/>
<collision name="default" offsetX="0" offsetY="0" width="12" height="15" mirror="false"/>
<animation name="idle" start="1" end="1" step="1" speed="0.125" reversed="false" repeat="false"/>
<animation name="walk" start="4" end="6" step="1" speed="0.25" reversed="false" repeat="true"/>
<animation name="turn" start="7" end="7" step="1" speed="0.125" reversed="false" repeat="false"/>
<animation name="jump" start="3" end="3" step="1" speed="0.25" reversed="false" repeat="false"/>
<animation name="dead" start="2" end="2" step="1" speed="0.125" reversed="false" repeat="false"/>
</entity>
frames
indique le nombre de frames horizontales et verticales (sert pour le découpage automatique).size
représente la taille de l'entité (servira pour la collision).collision
servira à délimiter la zone de collision entres entités.animation
permettent de déclarer une animation et ses données, récupérable par la suite.EntityState
(servira pour connaitre l'état du joueur, et jouer l'animation correspondante):
/**
* List of entity states.
*
* @author Pierre-Alexandre (contact@b3dgs.com)
*/
enum EntityState
{
/** Idle state. */
IDLE,
/** Walk state. */
WALK,
/** turn state. */
TURN,
/** Jump state. */
JUMP,
/** Dead state. */
DEAD;
/** Animation name. */
private final String animationName;
/**
* Constructor.
*/
private EntityState()
{
animationName = name().toLowerCase(Locale.ENGLISH);
}
/**
* Get the animation name.
*
* @return The animation name.
*/
public String getAnimationName()
{
return animationName;
}
}
SetupEntity
, héritant de SetupSurfacePlatform
, qui permettra de gérer les instances d'entité:
/**
* Setup entity implementation.
*
* @author Pierre-Alexandre (contact@b3dgs.com)
*/
final class SetupEntity
extends SetupSurfaceGame
{
/** Map. */
final Map map;
/** Desired fps. */
final int desiredFps;
/**
* Constructor.
*
* @param config The media config.
* @param map The map reference.
* @param desiredFps The desired fps.
*/
SetupEntity(Media config, Map map, int desiredFps)
{
super(config);
this.map = map;
this.desiredFps = desiredFps;
}
}
EntityCollision
(servira pour connaitre l'état des collisions):
/**
* List of entity collisions.
*
* @author Pierre-Alexandre (contact@b3dgs.com)
*/
enum EntityCollision
{
/** No collision. */
NONE,
/** Ground collision. */
GROUND;
}
Entity
, héritant de EntityPlatform
, qui décrira le gameplay de base de nos entités:
/**
* 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;
/**
* 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);
loadAnimations();
}
/**
* 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;
}
/**
* 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;
}
}
/*
* EntityPlatform
*/
@Override
protected void handleActions(double extrp)
{
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;
if (getLocationY() < 23)
{
teleportY(23);
coll = EntityCollision.GROUND;
}
}
@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);
}
}
Mario
héritant de Entity
:
/**
* Implementation of our controllable entity.
*
* @author Pierre-Alexandre (contact@b3dgs.com)
*/
public final class Mario
extends Entity
{
/**
* Constructor.
*
* @param setup setup reference.
*/
public Mario(SetupEntity setup)
{
super(setup);
}
/**
* Update the mario controls.
*
* @param keyboard The keyboard reference.
*/
public void updateControl(Keyboard keyboard)
{
right = keyboard.isPressed(Key.RIGHT);
left = keyboard.isPressed(Key.LEFT);
up = keyboard.isPressed(Key.UP);
}
/**
* Respawn mario.
*/
public void respawn()
{
mirror(false);
teleport(80, 25);
movement.reset();
jumpForce.setForce(Force.ZERO);
resetGravity();
}
/*
* Entity
*/
@Override
protected void handleMovements(double extrp)
{
// 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);
super.handleMovements(extrp);
}
}
EntityType
, qui permettra de lier le type à une classe à instancier avec la factory:
/**
* List of entity types.
*
* @author Pierre-Alexandre (contact@b3dgs.com)
*/
enum EntityType implements ObjectType
{
/** Mario. */
MARIO(Mario.class);
/** Class target. */
private final Class<?> target;
/** Path name. */
private final String pathName;
/**
* Constructor.
*
* @param target The target class.
*/
private EntityType(Class<?> target)
{
this.target = target;
pathName = ObjectTypeUtility.getPathName(this);
}
/*
* ObjectType
*/
@Override
public Class<?> getTargetClass()
{
return target;
}
@Override
public String getPathName()
{
return pathName;
}
}
FactoryEntity
, héritant de FactoryEntityGame
, qui permettra de créer les instances d'entités:
/**
* Factory entity implementation. Any entity instantiation has to be made using a factory instance.
*
* @author Pierre-Alexandre (contact@b3dgs.com)
*/
final class FactoryEntity
extends FactoryObjectGame<EntityType, SetupEntity, Entity>
{
/** Main entity directory name. */
private static final String ENTITY_DIR = "entities";
/** Map. */
private final Map map;
/** Desired fps. */
private final int desiredFps;
/**
* Constructor.
*
* @param map The map reference.
* @param desiredFps The desired fps.
*/
FactoryEntity(Map map, int desiredFps)
{
super(EntityType.class, FactoryEntity.ENTITY_DIR);
this.map = map;
this.desiredFps = desiredFps;
load();
}
/*
* FactoryObjectGame
*/
@Override
protected SetupEntity createSetup(EntityType key, Media config)
{
return new SetupEntity(config, map, desiredFps);
}
}
World
:
/**
* World implementation.
*
* @author Pierre-Alexandre (contact@b3dgs.com)
*/
final class World
extends WorldGame
{
/** Camera reference. */
private final CameraPlatform camera;
/** Map reference. */
private final Map map;
/** Factory reference. */
private final FactoryEntity factory;
/** Mario reference. */
private final Mario mario;
/**
* @see WorldGame#WorldGame(Sequence)
*/
World(Sequence sequence)
{
super(sequence);
camera = new CameraPlatform(width, height);
map = new Map();
factory = new FactoryEntity(map, source.getRate());
mario = factory.create(EntityType.MARIO);
}
/*
* WorldGame
*/
@Override
public void update(double extrp)
{
mario.updateControl(keyboard);
mario.update(extrp);
camera.follow(mario);
}
@Override
public void render(Graphic g)
{
g.clear(source);
map.render(g, camera);
mario.render(g, camera);
}
@Override
protected void saving(FileWriting file) throws IOException
{
map.save(file);
}
@Override
protected void loading(FileReading file) throws IOException
{
map.load(file);
camera.setLimits(map);
camera.setIntervals(16, 0);
mario.respawn();
}
}
Après tous ces efforts, vous devriez (si tout s'est bien passé) pouvoir déplacer le petit Mario dans le niveau, et avoir un scrolling lorsque ce dernier est au centre de l'écran:
A ce niveau là, nous avons utilisé plusieurs éléments importants:
ObjectType
SetupSurfaceGame
FactoryEntityGame
Ces 3 éléments sont totalement reliés. En effet, ils permettent de gérer et de créer des instances d'objet, tout en partageant des ressources (fichier XML et sprite PNG), et ce à partir d'un simple enum.
Lors de l'appel à Mario mario = factory.create(EntityType.MARIO);
, la factory récupère la classe du type (getTargetClass
), et l'instancie avec son setup correspondant,
chargé au préalable par la factory lors de l'appel à load()
.
Ainsi, pour rajouter un nouveau type, il suffira de créer une nouvelle classe héritant de Entity
, d'ajouter son type dans EntityType
, en liant sa classe correspondante,
d'ajouter son fichier XML et son image dans le dossier géré par la factory (entities/
), et d'appeler la méthode create(type)
.
Les cast sont réalisés automatiquement, ce qui évite d'alourdir le code inutilement.
L'intérêt d'avoir défini notre propre SetupEntity
est de pouvoir rajouter facilement des liens vers d'autres composants.
Actuellement seul un lien vers Map
et un entier sont réalisés; mais si demain il faut une nouvelle dépendance, il suffira de la lier à la factory, qui la fournira à notre setup lors de sa création.
Ainsi ce dernier sera accessible dans notre entité.
Lire la suite: Jeu de Plateforme - Les Collisions