Friday, February 1, 2013

Spaceship Warrior Pt 3 (Level 1)

It's time to add controls to your spaceship, so that we can move it around and shoot bullets.  We'll diverge a little bit from Spaceship Warrior and use the keyboard for motion input, instead of the mouse.  Add the following Components and Systems to our code:
Components
  • Velocity - This will hold how fast we move, in both x and y directions
  • Player - This will mark which ship is controlled by the player, we wouldn't want ALL the ships to respond to our commands
 Systems
  • MovementSystem - This will process all entities with both position and velocity, and update their position accordingly
  • PlayerInputSystem - This will listen for user input.  The demo uses mouse position, but I think arrow controls will be more fun
We will also follow the example of the demo and create an EntityFactory.java class which we will use to actually make our entities.

Let's start with the new components:
package com.gamexyz.components;

import com.artemis.Component;

public class Velocity extends Component {
 public float vx, vy;
 
 public Velocity(float vx, float vy) {
  this.vx = vx;
  this.vy = vy;
 }
 
 public Velocity() {
  this(0,0);
 }
}
package com.gamexyz.components;

import com.artemis.Component;

public class Player extends Component {
}

Notice that Velocity has x and y components, which will control how fast it moves laterally and vertically respectively.  Negative velocities will move it either left or down.

Notice that Player is totally empty.  In this case, we don't actually need to store any data relevant only to Player entities, we just need to tag them with "Player" so that we can grab them specifically to handle user input.

Let's look at the new systems now:
public class MovementSystem extends EntityProcessingSystem {
 @Mapper ComponentMapper<position> pm;
 @Mapper ComponentMapper<velocity> vm;

 @SuppressWarnings("unchecked")
 public MovementSystem() {
  super(Aspect.getAspectForAll(Position.class, Velocity.class));
 }

 @Override
 protected void process(Entity e) {
  Position position = pm.get(e);
  Velocity velocity = vm.get(e);
    
  position.x += velocity.vx*world.delta;
  position.y += velocity.vy*world.delta;
  
 }

}
public class PlayerInputSystem extends EntityProcessingSystem implements InputProcessor {
 @Mapper ComponentMapper<Velocity> vm;
 
 private OrthographicCamera camera;
 private Vector3 mouseVector;
 
 private int ax, ay;
 private int thruster = 400;
 private float drag = 0.4f;
 
 @SuppressWarnings("unchecked")
 public PlayerInputSystem(OrthographicCamera camera) {
  super(Aspect.getAspectForAll(Velocity.class, Player.class));
  this.camera=camera;
 }
 
 @Override
 protected void initialize() {
  Gdx.input.setInputProcessor(this);
 }

 @Override
 protected void process(Entity e) {
  mouseVector = new Vector3(Gdx.input.getX(),Gdx.input.getY(),0);
  camera.unproject(mouseVector);
  
  Velocity vel = vm.get(e);
  
  vel.vx += (ax - drag * vel.vx) * world.getDelta();
  vel.vy += (ay - drag * vel.vy) * world.getDelta();

 }

 @Override
 public boolean keyDown(int keycode) {
  if (keycode == Input.Keys.UP) ay = thruster;
  if (keycode == Input.Keys.DOWN) ay = -thruster;
  if (keycode == Input.Keys.RIGHT) ax = thruster;
  if (keycode == Input.Keys.LEFT) ax = -thruster; 
  return false;
 }

 @Override
 public boolean keyUp(int keycode) {
  if (keycode == Input.Keys.UP) ay = 0;
  if (keycode == Input.Keys.DOWN) ay = 0;
  if (keycode == Input.Keys.RIGHT) ax = 0;
  if (keycode == Input.Keys.LEFT) ax = 0; 
  return false;
 }

}

MovementSystem at this point is pretty basic, it just updates position based on kinematics equations from physics.  PlayerInputSystem is a little more advanced, so let's look at exactly what's happening.

First, I want to note that the mouseVector and camera stuff are all extras, and things that would be really important if you were actually going to use the mouse for input.  We're using the keyboard though, so our commands come in the keyDown() and keyUp() methods.  There are other required input methods I didn't list in my code, but must be present because the class implements InputProcessor: keyTyped(), touchDown(), touchUp(), touchDragged(), mouseMoved(), and scrolled().  You can add System.out.println()'s to see when they are called if you want.

On lines 7 - 9 I declare variables to hold the acceleration rate in the x and y directions, as well as a "thruster" which controls how strongly the ship will accelerate and "drag" coefficient.  You can play with those values to find a combination you like.  These are things that probably should have been included in their own "Thruster" component, so that different ships could have different acceleration abilities, but for now this will work.

The keyDown() method listens for a key to be depressed, but only fires once.  Here, when you press UP, it will set ay to the value of thruster, so that the ship can accelerate upward in the y direction.  The keyUp() methods listens for keys to be released, and will reset accelerations to 0 accordingly.

Then, in process(), I update velocity taking into account some drag.  Drag lets me not worry about any "max_velocity", because drag naturally introduces a terminal velocity.  It will also gradually slow the ship down, and even though we're in space and that shouldn't be a huge issue, I find it more aesthetically pleasing.

Back in GameXYZ.java I add these new processing Systems to my World, and add a Player and Velocity component to my entity.
 public GameXYZ(Game game) {
  
     camera = new OrthographicCamera();
     camera.setToOrtho(false, 1280,900);
     
     this.game = game;
     
     world = new World();
     spriteRenderSystem = world.setSystem(new SpriteRenderSystem(camera),true);
     
     world.setSystem(new PlayerInputSystem(camera));
     world.setSystem(new MovementSystem());
     
     world.initialize();
     
     Entity e = world.createEntity();
     e.addComponent(new Position(150,150));
     e.addComponent(new Sprite());
     e.addComponent(new Player());
     e.addComponent(new Velocity(0,0));
     e.addToWorld();
 }

Running the program lets us now control our ship using the arrow keys.

Before we go on to add the ability to shoot, we should consider using an EntityFactory to manage the creation of our entities.  If you look at the demo code, there won't be very many surprises, but to get us started I've made a simplified EntityFactory that just creates our player, the way we've been doing.
package com.gamexyz;

import com.artemis.Entity;
import com.artemis.World;
import com.gamexyz.components.Player;
import com.gamexyz.components.Position;
import com.gamexyz.components.Sprite;
import com.gamexyz.components.Velocity;

public class EntityFactory {

 public static Entity createPlayer(World world, float x, float y) {
  Entity e = world.createEntity();
  
  e.addComponent(new Position(x, y));
  e.addComponent(new Sprite("textures-original/fighter.png"));
  e.addComponent(new Velocity());
  e.addComponent(new Player());
  
  return e;
 } 
}

To implement it, we just replace our code in GameXYZ which created our entity with
EntityFactory.createPlayer(world, 150, 150).addToWorld();

A quick run of the program reveals that this works just as well.

When we go to shoot, we will need to create new entities for the bullets.  To start with, we'll just give our bullets Position, Velocity, and Sprite.  So in our EntityFactory, add a new method called createBullet()
 public static Entity createBullet(World world, float x, float y) {
  Entity e = world.createEntity();
  
  e.addComponent(new Position(x, y));
  e.addComponent(new Sprite("textures-original/bullet.png"));
  e.addComponent(new Velocity(0,800));
  
  return e;
 }

The velocity is all in the y direction, because our ship is always pointing that way. Back in PlayerInputSystem, we now need a new control that will create bullets for us when users press the space bar.  Note: because the bullet should be shot out from our ship's position, we now need to include Position.class in our "getAspectForAll()" list, and we will also need an @Mapper for position.

We want to put our "space bar" listener in either keyDown() or keyPressed(), but there is a problem.  Neither of these methods can see our Position variable.  So instead, we will create a new private boolean flag called "shoot", and if they press space, shoot gets set to true, and when they release, shoot gets set to false.  So we'll use keyDown() and keyUp(), but not keyPressed().  This is because we don't want to continuously set shoot to true while they're holding space bar, it just needs to be set the first time.  Here's the code we need:
public class PlayerInputSystem extends EntityProcessingSystem implements InputProcessor {
 @Mapper ComponentMapper<Velocity> vm;
 @Mapper ComponentMapper<Position> pm;
 
 private OrthographicCamera camera;
 private Vector3 mouseVector;
 
 private int ax, ay;
 private final int thruster = 400;
 private final float drag = 0.4f;
 
 private boolean shoot = false;

 @SuppressWarnings("unchecked")
 public PlayerInputSystem(OrthographicCamera camera) {
  super(Aspect.getAspectForAll(Velocity.class, Player.class, Position.class));
  this.camera=camera;
 }
 
 @Override
 protected void initialize() {
  Gdx.input.setInputProcessor(this);
 }

 @Override
 protected void process(Entity e) {
  mouseVector = new Vector3(Gdx.input.getX(),Gdx.input.getY(),0);
  camera.unproject(mouseVector);
  
  Velocity vel = vm.get(e);
  Position pos = pm.get(e);
  
  vel.vx += (ax - drag * vel.vx) * world.getDelta();
  vel.vy += (ay - drag * vel.vy) * world.getDelta();
  
  if (shoot) {
   EntityFactory.createBullet(world,pos.x+7,pos.y+40).addToWorld();
   EntityFactory.createBullet(world,pos.x+60,pos.y+40).addToWorld();
  }
 }

 @Override
 public boolean keyDown(int keycode) {
  if (keycode == Input.Keys.UP) ay = thruster;
  if (keycode == Input.Keys.DOWN) ay = -thruster;
  if (keycode == Input.Keys.RIGHT) ax = thruster;
  if (keycode == Input.Keys.LEFT) ax = -thruster;
  if (keycode == Input.Keys.SPACE) shoot = true; 
  return false;
 }

 @Override
 public boolean keyUp(int keycode) {
  if (keycode == Input.Keys.UP) ay = 0;
  if (keycode == Input.Keys.DOWN) ay = 0;
  if (keycode == Input.Keys.RIGHT) ax = 0;
  if (keycode == Input.Keys.LEFT) ax = 0; 
  if (keycode == Input.Keys.SPACE) shoot = false; 
  return false;
 }
}

We can run it and shoot, which is pretty cool, but things still aren't perfect.  For one thing, the continuous stream of bullets seems a little unreasonable.  We can introduce a "timeToFire" variable and "fireRate" variable to control this as follows:

Now we can run around blasting our imagined enemies to oblivion.  Unfortunately, every bullet we have ever fired will keep going and going and going.  Sooner or later, the computer will be trying to process so many bullets that it just can't keep looking smooth anymore.  We will create a timer which will destroy old bullets.  Following the Demo, we'll create a Component called Expires, and a System called ExpiringSystem.
package com.gamexyz.components;

import com.artemis.Component;

public class Expires extends Component {
 public float delay;
 
 public Expires(float delay) {
  this.delay = delay;
 }
 
 public Expires() {
  this(0);
 }
}


package com.gamexyz.systems;

import com.artemis.Aspect;
import com.artemis.ComponentMapper;
import com.artemis.Entity;
import com.artemis.annotations.Mapper;
import com.artemis.systems.DelayedEntityProcessingSystem;
import com.gamexyz.components.Expires;

public class ExpiringSystem extends DelayedEntityProcessingSystem {
 @Mapper
 ComponentMapper em;

 public ExpiringSystem() {
  super(Aspect.getAspectForAll(Expires.class));
 }
 
 @Override
 protected void processDelta(Entity e, float accumulatedDelta) {
  Expires expires = em.get(e);
  expires.delay -= accumulatedDelta;
 }
 
 @Override
 protected void processExpired(Entity e) {
  e.deleteFromWorld();
 }
 
 @Override
 protected float getRemainingDelay(Entity e) {
  Expires expires = em.get(e);
  return expires.delay;
 }
}
The System is kind of cool, notice it extends DelayedEntityProcessingSystem, which is a whole EntitySystem designed to handle stuff like this, with processDelta(), and processExpired().  Adding this to our world will kill anything with the Exires component, so let's go back into the EntityFactory and give that to bullets.
 public static Entity createBullet(World world, float x, float y) {
  Entity e = world.createEntity();
  
  e.addComponent(new Position(x, y));
  e.addComponent(new Sprite("textures-original/bullet.png"));
  e.addComponent(new Velocity(0,800));
  e.addComponent(new Expires(1f));
  
  return e;
 }

Uh-oh, we have a problem.  The argument (1f) tells Expires that it should wait 1 second before expiring.  That seems to work well, except it expires ALL the bullets, not just the ones that have existed for 1 second.  Damn... it's never easy, huh?

To make sure we're not insane, check the demo program.  It's set to expire bullets after 5 seconds, watch and see every 5 seconds your whole bullet stream dies.  When I do this, I see that sometimes they die, and sometimes they don't.  The difference seems to be if any bullets of that 5 seconds have hit an enemy ship.  To verify this to myself I went into their EntityFactory and under createEnemyShip() I set the bounds.radius to 0 (this is used in their collision detection) so that no bullets would hit ships.  And sure enough, every 5 seconds all the bullets died like clockwork.

Huh... a mystery for another day.
UPDATE: I read on the Artemis forum that the whole Delay system isn't very good, so I just ignored the demo's class and made my very own ExpiringSystem
public class ExpiringSystem extends EntityProcessingSystem {
 
 @Mapper ComponentMapper<Expires> em;
 
 public ExpiringSystem() {
  super(Aspect.getAspectForAll(Expires.class));
 }

 @Override
 protected boolean checkProcessing() {
  return true;
 }
 
 @Override
 protected void process(Entity e) {
  Expires exp = em.get(e);
  exp.delay -= world.getDelta();
  if (exp.delay <= 0) {
   e.deleteFromWorld();
  }
 }
}
This System seems to work well, though it doesn't really have many frills.

One other comment I'd like to make: I'm trying to roughly reproduce the demo program, with a little variation here and there.  But I feel that this PlayerInputSystem is becoming too clogged with things that should be separated.  For instance, I think we should have separate components for Thruster or Accelerator,  and Gun.  It seems that too much data has creeped into our PlayerInputSystem.  In future posts I may explore separating it out more.  I worry, though, that it would end up required a bunch of different InputSystems to handle the different types of weapons (guns vs grenades vs swords vs whatever...), but maybe that's not such a bad thing?  Or maybe it won't be important.  We'll see!

You gain 50 XP.  Progress to level 2: 150/400

4 comments:

  1. Excellent tutorial. I wish someone would explain this 'Mapper' voodoo. I guess I'll figure it out at some point.

    For some reason, in MovementSystem above, position and velocity are not capitalized. So you get e.g. "velocity cannot be resolved to a type" errors.

    @Mapper ComponentMapper pm;
    @Mapper ComponentMapper vm;

    ReplyDelete
  2. Really great tutorial series. Thanks!

    ReplyDelete
  3. Great tuts, thank you for creating them!

    There is small mistake in the second code snippet: 2nd and 3rd line should read:
    @Mapper ComponentMapper pm;
    @Mapper ComponentMapper vm;

    Cheers!

    ReplyDelete