-
Notifications
You must be signed in to change notification settings - Fork 0
coding_the_player
In this lesson, we'll add player movement, animation, and set it up to detect collisions.
To do this, open the Player
class.
package e.g.dodgethecreeps.game;
...
public class Player extends KinematicBody2D {
...
}
Start by declaring the member variables this object will need:
...
public class Player extends KinematicBody2D {
public static final String MOVE_RIGHT = "move_right";
public static final String MOVE_LEFT = "move_left";
public static final String MOVE_UP = "move_up";
public static final String MOVE_DOWN = "move_down";
private Dodgethecreeps app;
private double speed = 4.0;
private Camera cam;
/**
* Flags used to indicate the direction the player will take depending
* on the key pressed.
*/
private boolean right,
left,
up,
down;
...
}
Initialize the following variables directly in the class constructor:
...
public Player(Dodgethecreeps app) {
this.cam = app.getCamera();
this.app = app;
}
...
The ready()
function is called when a node enters the scene tree.
Now we can use the physicsProcess
function to define what the player will do. physicsProcess
is called every frame, so we'll use it to update elements of our game, which we expect will change often. For the player, we need to do the following:
- Check for input.
- Move in the given direction.
- Play the appropriate animation.
First, we need to check for input - is the player pressing a key? For this game, we have 4 direction inputs to check. Input actions are defined in "InputManager". Here, you can define custom events and assign them different keys, mouse events, or other inputs. For this game, we will assign the arrow keys to the four directions.
Create a new action object for the inputs.
...
private ActionListener _on_Action_Listener = new ActionListener() {
@Override
public void onAction(String name, boolean isPressed, float tpf) {
}
};
...
In method ready()
record the following keyboard entries for the dates the player will control.
...
InputManager inputManager = app.getInputManager();
inputManager.addMapping(Player.MOVE_DOWN, new KeyTrigger(KeyInput.KEY_DOWN));
inputManager.addMapping(Player.MOVE_UP, new KeyTrigger(KeyInput.KEY_UP));
inputManager.addMapping(Player.MOVE_LEFT, new KeyTrigger(KeyInput.KEY_LEFT));
inputManager.addMapping(Player.MOVE_RIGHT, new KeyTrigger(KeyInput.KEY_RIGHT));
inputManager.addListener(_on_Action_Listener, new String[] {
Player.MOVE_DOWN, Player.MOVE_LEFT, Player.MOVE_RIGHT, Player.MOVE_UP
});
...
We only mapped one key to each input action, but you can map multiple keys, joystick buttons, or mouse buttons to the same input action.
You can detect if a key is pressed as follows:
....
private ActionListener _on_Action_Listener = new ActionListener() {
@Override
public void onAction(String name, boolean isPressed, float tpf) {
if ( isEnabled() ) {
if (MOVE_DOWN.equals(name)) {
down = isPressed;
}
if (MOVE_LEFT.equals(name)) {
left = isPressed;
}
if (MOVE_RIGHT.equals(name)) {
right = isPressed;
}
if (MOVE_UP.equals(name)) {
up = isPressed;
}
}
}
};
....
We start by setting the player's speed, each time a key is pressed the character will move in a certain direction.
In method physicsProcess
:
...
Vector2 velocity = new Vector2(0, 0);
if ( right ) {
velocity.x += 1;
}
if ( left ) {
velocity.x -= 1;
}
if ( down ) {
velocity.y -= 1;
}
if ( up ) {
velocity.y += 1;
}
if (velocity.getMagnitude() > 0) {
velocity = velocity.getNormalized().multiply(speed);
spatial.getControl(AnimatedSprite2D.class).setEnabled(true);
} else {
spatial.getControl(AnimatedSprite2D.class).setEnabled(false);
}
....
We also check whether the player is moving so we can call setEnabled(true)
or setEnabled(true)
on the AnimatedSprite2D
. This enables or enables the current animation.
Now that we have a movement direction, we can update the player's position. We can also use Interval.clamp()
to prevent it from leaving the screen. Clamping a value means restricting it to a given range. Add the following to the bottom of the physicsProcess
function
...
Vector2 position = getTransform().getTranslation();
position = position.add(velocity.multiply(delta));
position.x = Interval.clamp(position.x, cam.getFrustumLeft(), cam.getFrustumRight());
position.y = Interval.clamp(position.y, cam.getFrustumBottom(), cam.getFrustumTop());
...
Sets the player's new position.
...
getTransform().setRotation(0);
getTransform().setTranslation(position);
...
Run your game (Compile) and confirm that you can move the player around the screen in all directions.
Now that the player can move, we need to change which animation the AnimatedSprite is playing based on its direction. We have the "walk" animation, which shows the player walking to the right.
This animation should be flipped horizontally using the flipH()
property for left movement. We also have the "up" animation, which should be flipped vertically with flipV()
for downward movement. Let's place this code at the end of the physicsProcess()
function:
...
Sprite sprite = (Sprite) ((Geometry) spatial).getMesh();
if ( velocity.x != 0 ) {
spatial.getControl(AnimatedSprite2D.class).playAnimation("walk", 0.15f);
sprite.flipV(false);
sprite.flipH(velocity.x < 0);
} else if ( velocity.y != 0 ) {
spatial.getControl(AnimatedSprite2D.class).playAnimation("up", 0.15f);
sprite.flipV(velocity.y < 0);
}
...
What you just did is a change in the coordinates of the textures that the model geometry mesh handles. This work has already been done by jMe3GL2 through the
Sprite
mesh it offers.
compile again and verify that the animations are correct in each of the directions.
We want the player to detect when they are hit by an enemy, but we haven't created any enemies yet! That's okay, because we will use the contact listeners that Dyn4j provides to determine the objects in contact with the player's physical body.
...
private final StepListener<PhysicsBody2D> _on_Step_Listener = new StepListenerAdapter<>() {
@Override
public void begin(TimeStep step, PhysicsWorld<PhysicsBody2D, ?> world) {
List<ContactConstraint<PhysicsBody2D>> contacts = world.getContacts(Player.this);
for (final ContactConstraint<PhysicsBody2D> cc : contacts) {
PhysicsBody2D otherBody = cc.getOtherBody(Player.this);
/* collision */
}
}
};
...
When player enters a scene node, it registers the new listener contact.
...
private final SpaceListener<PhysicsBody2D> _on_Space_Listener = new SpaceListener<PhysicsBody2D>() {
@Override
public void spaceAttached(PhysicsSpace<PhysicsBody2D> physicsSpace) {
physicsSpace.addStepListener(_on_Step_Listener);
}
@Override
public void spaceDetached(PhysicsSpace<PhysicsBody2D> physicsSpace) {
physicsSpace.removeStepListener(_on_Step_Listener);
}
};
...
in ready()
method register it safely.
...
addSpaceListener(_on_Space_Listener);
...
Now that almost everything is prepared, create a method that can release resources when the player collides with an enemy.
...
@Override
public void queueFree() {
if (spatial.removeFromParent()) {
physicsSpace.removeStepListener(_on_Step_Listener);
physicsSpace.removeBody(this);
InputManager inputManager = app.getInputManager();
if (inputManager.hasMapping(MOVE_DOWN)) {
inputManager.deleteMapping(MOVE_DOWN);
inputManager.deleteMapping(MOVE_LEFT);
inputManager.deleteMapping(MOVE_RIGHT);
inputManager.deleteMapping(MOVE_UP);
}
inputManager.removeListener(_on_Action_Listener);
app.getStateManager().getState(GameSceneAppState.class).gameOver();
}
}
...
In state GameSceneAppState
, create a function gameOver()
, responsible for a signal to end the game.
package e.g.dodgethecreeps.screen;
...
public class GameSceneAppState extends AbstractScreen {
...
public void gameOver() {
}
...
}
At the moment your code should look similar to the following:
package e.g.dodgethecreeps.game;
...
public final class Player extends KinematicBody2D {
public static final String MOVE_RIGHT = "move_right";
public static final String MOVE_LEFT = "move_left";
public static final String MOVE_UP = "move_up";
public static final String MOVE_DOWN = "move_down";
private Dodgethecreeps app;
private double speed = 4.0;
private Camera cam;
/**
* Flags used to indicate the direction the player will take depending
* on the key pressed.
*/
private boolean right,
left,
up,
down;
public Player(Dodgethecreeps app) {
this.cam = app.getCamera();
this.app = app;
}
private ActionListener _on_Action_Listener = new ActionListener() {
@Override
public void onAction(String name, boolean isPressed, float tpf) {
if ( isEnabled() ) {
if (MOVE_DOWN.equals(name)) {
down = isPressed;
}
if (MOVE_LEFT.equals(name)) {
left = isPressed;
}
if (MOVE_RIGHT.equals(name)) {
right = isPressed;
}
if (MOVE_UP.equals(name)) {
up = isPressed;
}
}
}
};
private final StepListener<PhysicsBody2D> _on_Step_Listener = new StepListenerAdapter<>() {
@Override
public void begin(TimeStep step, PhysicsWorld<PhysicsBody2D, ?> world) {
List<ContactConstraint<PhysicsBody2D>> contacts = world.getContacts(Player.this);
for (final ContactConstraint<PhysicsBody2D> cc : contacts) {
PhysicsBody2D otherBody = cc.getOtherBody(Player.this);
/* collision */
}
}
};
private final SpaceListener<PhysicsBody2D> _on_Space_Listener = new SpaceListener<PhysicsBody2D>() {
@Override
public void spaceAttached(PhysicsSpace<PhysicsBody2D> physicsSpace) {
physicsSpace.addStepListener(_on_Step_Listener);
}
@Override
public void spaceDetached(PhysicsSpace<PhysicsBody2D> physicsSpace) {
physicsSpace.removeStepListener(_on_Step_Listener);
}
};
protected void ready() {
InputManager inputManager = app.getInputManager();
inputManager.addMapping(Player.MOVE_DOWN, new KeyTrigger(KeyInput.KEY_DOWN));
inputManager.addMapping(Player.MOVE_UP, new KeyTrigger(KeyInput.KEY_UP));
inputManager.addMapping(Player.MOVE_LEFT, new KeyTrigger(KeyInput.KEY_LEFT));
inputManager.addMapping(Player.MOVE_RIGHT, new KeyTrigger(KeyInput.KEY_RIGHT));
inputManager.addListener(_on_Action_Listener, new String[] {
Player.MOVE_DOWN, Player.MOVE_LEFT, Player.MOVE_RIGHT, Player.MOVE_UP
});
addSpaceListener(_on_Space_Listener);
}
protected void physicsProcess(float delta) {
Vector2 velocity = new Vector2(0, 0);
if ( right ) {
velocity.x += 1;
}
if ( left ) {
velocity.x -= 1;
}
if ( down ) {
velocity.y -= 1;
}
if ( up ) {
velocity.y += 1;
}
if (velocity.getMagnitude() > 0) {
velocity = velocity.getNormalized().multiply(speed);
spatial.getControl(AnimatedSprite2D.class).setEnabled(true);
} else {
spatial.getControl(AnimatedSprite2D.class).setEnabled(false);
}
Vector2 position = getTransform().getTranslation();
position = position.add(velocity.multiply(delta));
position.x = Interval.clamp(position.x, cam.getFrustumLeft(), cam.getFrustumRight());
position.y = Interval.clamp(position.y, cam.getFrustumBottom(), cam.getFrustumTop());
getTransform().setRotation(0);
getTransform().setTranslation(position);
Sprite sprite = (Sprite) ((Geometry) spatial).getMesh();
if ( velocity.x != 0 ) {
spatial.getControl(AnimatedSprite2D.class).playAnimation("walk", 0.15f);
sprite.flipV(false);
sprite.flipH(velocity.x < 0);
} else if ( velocity.y != 0 ) {
spatial.getControl(AnimatedSprite2D.class).playAnimation("up", 0.15f);
sprite.flipV(velocity.y < 0);
}
}
@Override
public void queueFree() {
if (spatial.removeFromParent()) {
physicsSpace.removeStepListener(_on_Step_Listener);
physicsSpace.removeBody(this);
InputManager inputManager = app.getInputManager();
if (inputManager.hasMapping(MOVE_DOWN)) {
inputManager.deleteMapping(MOVE_DOWN);
inputManager.deleteMapping(MOVE_LEFT);
inputManager.deleteMapping(MOVE_RIGHT);
inputManager.deleteMapping(MOVE_UP);
}
inputManager.removeListener(_on_Action_Listener);
app.getStateManager().getState(GameSceneAppState.class).gameOver();
}
}
public static Player getNewInstancePlayer(Dodgethecreeps app) {
AssetManager assetManager = app.getAssetManager();
Material mat = MaterialUtilities.getUnshadedMaterialFromClassPath(assetManager, "Textures/playerGrey_walk1.png");
mat.setFloat("AlphaDiscardThreshold", 0.0F);
Sprite sprite = new Sprite(1.5F, 1.5F);
Geometry geom = new Geometry("Player", sprite);
geom.setMaterial(mat);
geom.setQueueBucket(RenderQueue.Bucket.Transparent);
// Add the following animations.
AnimatedSprite2D animatedSprite = new AnimatedSprite2D();
animatedSprite.addAnimation("walk", new Texture[] {
TextureUtilities.getTextureFromClassPath(assetManager, "Textures/playerGrey_walk1.png"),
TextureUtilities.getTextureFromClassPath(assetManager, "Textures/playerGrey_walk2.png")
});
animatedSprite.addAnimation("up", new Texture[] {
TextureUtilities.getTextureFromClassPath(assetManager, "Textures/playerGrey_up1.png"),
TextureUtilities.getTextureFromClassPath(assetManager, "Textures/playerGrey_up2.png")
});
animatedSprite.setAnimationSpeed(0.60F);
BodyFixture fixture = new BodyFixture(GeometryUtilities.dyn4jCreateCapsule(0.8, 1.1));
fixture.setFilter(new LayerFilter(0));
Player player = new Player(app);
player.addFixture(fixture);
player.setMass(MassType.NORMAL);
geom.addControl(player);
geom.addControl(animatedSprite);
return player;
}
}