Saturday, February 9, 2013

Sprite Animation with libgdx and Artemis (Level 2)

After finishing Spaceship warrior, I felt like there were a few more concepts I had to get down.  The first piece I decided to tackle was implementing a SpriteAnimationSystem.  There are a few useful tools in libgdx to accomplish this.

First, when TextuerPacker2 creates a TextureAtlas, it creates a file mapping the file-name to the appropriate region.  If you have a series of images named imageName_0.png, imageName_1.png, ..., imageName_100.png it interprets the _x as an index, and refers to them all by the name imageName.  When there is no number, the atlas just assigns a default index of -1.  But with the numbers, you get indices corresponding to each file.

When you call
atlas.findRegion("imageName");
it returns only the last image of the series.  But if you call
atlas.findRegions("imageName");
it returns an array of all of them, indexed appropriately..  This array can be passed into libgdx's Animation class to create an object that will cycle through all the images in the set.  My idea was to create a new Component called SpriteAnimation which would hold this animation, and SpriteAnimationSystem which would update the Sprite component based on the status of SpriteAnimation.

This way, the SpriteRenderSystem could be used to render everything with just a single static Sprite, and things with SpriteAnimation because all SpriteRenderSystem would ever call was the Sprite class.

First, I did a Google search to find a sprite sheet I could use for the animation, and I came across a game in development called Hero of Allacrost, staring the young knight Claudius.  I found a spritesheet for Claudius and lifted a few frames, naming them warrior_2 through warrior_6 (I started off of 0 just to see what would happen - it worked fine!)


 
Each image is 32x64, and they line up to look like continuous walking.

To understand my implementation, I want to first focus on the TextureRegion class.  TextureRegions have a reference to a Texture, along with rectangle coordinates specifying which part of that Texture to use.  These coordinates are stored in 2 different formats: float u, u2 refer to the x coordinates as a percent of overall texture width.  That is, if a Texture is 100 pixels wide, and the TextureRegion runs horizontally through pixel 35 and pixel 73, u would be 0.35f and u2 would be 0.73f.  float v, v2 do likewise for the vertical component.

On the other hand, int x, y, width, height store the same information in raw pixel counts.  In the example above, x would be 35 and width would be 73-35 = 38. 

The first major change I had to make was to SpriteRenderSystem and Sprite.  In Spaceship Warrior, Sprites only had a name, but SpriteRenderSystem had a bag which assigned a TextureRegion to each entity.  This wouldn't work anymore because the Sprite class itself had to hold its own TextureRegion so the AnimatedSpriteSystem could update it.

Even worse, the Sprites would now only hold a refenece to the actual TextureRegion created from the actual TextureAtlas.  That means when one Sprite updated "its" TextureRegion, it would actually mess with all of them.

To get around this, I used the coordinate information talked about above, and each Sprite had it's own int x, y, width, and height.  Instead of touching the TextureRegion itself, AnimatedSpriteSystem would instead change the Sprite's coordinate information, and the Sprite would update the TextureRegion right before it was drawn.

Here is my implementation:
public class Sprite extends Component {
 
 public Sprite(String name) {
  this.name = name;
 }
 
 
 public TextureRegion region;
 public String name;
 public float r,g,b,a,scaleX,scaleY,rotation;

 public int x, y, width, height;
}

package com.gamexyz.components;
public class SpriteAnimation extends Component {
 
 public Animation animation;
 public float stateTime;
 public float frameDuration;
 public int playMode;
 
 public TextureRegion getFrame() {
  return animation.getKeyFrame(stateTime);
 }
 
}

public class SpriteAnimationSystem extends EntityProcessingSystem {
 @Mapper ComponentMapper<Sprite> sm;
 @Mapper ComponentMapper<SpriteAnimation> sam;
 
 @SuppressWarnings("unchecked")
 public SpriteAnimationSystem() {
  super(Aspect.getAspectForAll(Sprite.class, SpriteAnimation.class));
 }

 @Override
 protected void process(Entity e) {

  Sprite sprite = sm.get(e);
  SpriteAnimation anim = sam.get(e);
  
  anim.stateTime += world.getDelta();

  TextureRegion region = anim.getFrame();
  sprite.x = region.getRegionX();
  sprite.y = region.getRegionY();
  sprite.width = region.getRegionWidth();
  sprite.height = region.getRegionHeight();
 }

 @Override
 protected boolean checkProcessing() {
  return true;
 }
}

public class SpriteRenderSystem extends EntitySystem {
 @Mapper ComponentMapper<position> pm;
 @Mapper ComponentMapper<sprite> sm;
 @Mapper ComponentMapper<spriteanimation> sam;
 
 private OrthographicCamera camera;
 private SpriteBatch batch;
 
 private TextureAtlas atlas;
 
 private List<entity> sortedEntities;
 
 @SuppressWarnings("unchecked")
 public SpriteRenderSystem(OrthographicCamera camera) {
  super(Aspect.getAspectForAll(Position.class, Sprite.class));
  this.camera = camera;
 }
 
 @Override
 protected void initialize() {
  batch = new SpriteBatch();
  
  atlas = new TextureAtlas(Gdx.files.internal("textures/pack.atlas"),Gdx.files.internal("textures"));
  sortedEntities = new ArrayList<entity>();
 }

 @Override
 protected boolean checkProcessing() {
  return true;
 }

 @Override
 protected void processEntities(ImmutableBag<entity> entities) {
  for (Entity e : sortedEntities) {
   process(e);
  }
 }
 
 @Override
 protected void begin() {
  batch.setProjectionMatrix(camera.combined);
  batch.begin();
 }
 
 protected void process(Entity e) {
  if (pm.has(e)) {
   Position position = pm.getSafe(e);
   Sprite sprite = sm.get(e);
   
   TextureRegion spriteRegion = sprite.region;
   batch.setColor(sprite.r, sprite.g, sprite.b, sprite.a);

   int width = spriteRegion.getRegionWidth();
   int height = spriteRegion.getRegionHeight();
   
   sprite.region.setRegion(sprite.x, sprite.y, width, height);
   
   float posX = position.x - (spriteRegion.getRegionWidth() / 2 * sprite.scaleX);
   float posY = position.y - (spriteRegion.getRegionHeight() / 2 * sprite.scaleX);
   batch.draw(spriteRegion, posX, posY, 0, 0, spriteRegion.getRegionWidth(), spriteRegion.getRegionHeight(), sprite.scaleX, sprite.scaleY, sprite.rotation);
  }
 }
 
 @Override
 protected void end() {
  batch.end();
 }
 
 @Override
 protected void inserted(Entity e) {
  Sprite sprite = sm.get(e);
  sortedEntities.add(e);
  TextureRegion reg = atlas.findRegion(sprite.name);
  sprite.region = reg;
  sprite.x = reg.getRegionX();
  sprite.y = reg.getRegionY();
  sprite.width = reg.getRegionWidth();
  sprite.height = reg.getRegionHeight();
  if (sam.has(e)) {
   SpriteAnimation anim = sam.getSafe(e);
   anim.animation = new Animation( anim.frameDuration, atlas.findRegions(sprite.name), anim.playMode);
  }
  
  Collections.sort(sortedEntities, new Comparator<entity>() {
   @Override
   public int compare(Entity e1, Entity e2) {
    Sprite s1 = sm.get(e1);
    Sprite s2 = sm.get(e2);
    return s1.layer.compareTo(s2.layer);
   }
  });
 }
 
 @Override
 protected void removed(Entity e) {
  sortedEntities.remove(e);
 }
}
I completely abandoned the regionsByName bag, and even the regions HashMap which mapped "names" to regions, instead I rely entirely on atlas.getRegion(...).  This is potentially slow, and if I ever need to create a lot of entities at once it may be helpful to recreate the HashMap - especially for the static sprites.  For instance, if I created a particle explosion effect, I may do that down the line.  But for now this seems fine.

My least favorite part comes in lines 79-82.  The SpriteRenderSystem doesn't necessarily require its entities have SpriteAnimation, so I had to carefully check before referencing it.  The reason I didn't include this in a different system, which would have been preferable, is that this is where my TextureAtlas lives.  Without having that to point to, I can't instantiate my Animation.

From that, SpriteAnimationSystem updates the animated sprites x, y, etc, and it gets drawn properly to the screen.  I created an EntityFactory that will make a character with the Claudius animation, and I added a bunch of them.  Copying over my HudRenderingSystem from the previous project let me see that FPS did okay even with thousands of such entities being processed and rendered onscreen.  Furthermore, I made them all start out of step by giving them a random stateTime.  stateTime is the thing that Animation uses to calculate which frame to work with.  For fun, I also added a few static Sprites to verify that they would alongside the animated ones.

int playMode in SpriteAnimation refers to the playMode in libgdx's Animation class.  I like to use Animation.LOOP_PINGPONG for the Claudius animation, which means that once it reaches the last frame, it turns around and loops through them in reverse, back and forth.  This is particularly convenient for a walking animation.  Unfortunately, PINGPONG has a quirk that the endpoint frames last for twice as long.  This ruined our aesthetic in my opinion, and seems extremely silly!  I downloaded the raw code from their github repository here to see what was going on.

Unsatisfied with the way they did things, I created my own Animation class by totally stealing theirs and changing one tiny piece of the code.  I put it in a new package called com.gamexyz.custom, and here is what it looks like:
/*******************************************************************************
 * Copyright 2011 See AUTHORS file.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/

package com.gamexyz.custom;

import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.utils.Array;

/** * An Animation stores a list of {@link TextureRegion}s representing an animated sequence, e.g. for running or jumping. Each
 * region of an Animation is called a key frame, multiple key frames make up the animation.
 * 


 * 
 * @author mzechner */
public class Animation {
 public static final int NORMAL = 0;
 public static final int REVERSED = 1;
 public static final int LOOP = 2;
 public static final int LOOP_REVERSED = 3;
 public static final int LOOP_PINGPONG = 4;
 public static final int LOOP_RANDOM = 5;

 final TextureRegion[] keyFrames;
 public final float frameDuration;
 public final float animationDuration;

 private int playMode = NORMAL;

 /** Constructor, storing the frame duration and key frames.
  * 
  * @param frameDuration the time between frames in seconds.
  * @param keyFrames the {@link TextureRegion}s representing the frames. */
 public Animation (float frameDuration, Array keyFrames) {
  this.frameDuration = frameDuration;
  this.animationDuration = keyFrames.size * frameDuration;
  this.keyFrames = new TextureRegion[keyFrames.size];
  for (int i = 0, n = keyFrames.size; i < n; i++) {
   this.keyFrames[i] = keyFrames.get(i);
  }

  this.playMode = NORMAL;
 }

 /** Constructor, storing the frame duration, key frames and play type.
  * 
  * @param frameDuration the time between frames in seconds.
  * @param keyFrames the {@link TextureRegion}s representing the frames.
  * @param playType the type of animation play (NORMAL, REVERSED, LOOP, LOOP_REVERSED, LOOP_PINGPONG, LOOP_RANDOM) */
 public Animation (float frameDuration, Array keyFrames, int playType) {

  this.frameDuration = frameDuration;
  this.animationDuration = keyFrames.size * frameDuration;
  this.keyFrames = new TextureRegion[keyFrames.size];
  for (int i = 0, n = keyFrames.size; i < n; i++) {
   this.keyFrames[i] = keyFrames.get(i);
  }

  this.playMode = playType;
 }

 /** Constructor, storing the frame duration and key frames.
  * 
  * @param frameDuration the time between frames in seconds.
  * @param keyFrames the {@link TextureRegion}s representing the frames. */
 public Animation (float frameDuration, TextureRegion... keyFrames) {
  this.frameDuration = frameDuration;
  this.animationDuration = keyFrames.length * frameDuration;
  this.keyFrames = keyFrames;
  this.playMode = NORMAL;
 }

 /** Returns a {@link TextureRegion} based on the so called state time. This is the amount of seconds an object has spent in the
  * state this Animation instance represents, e.g. running, jumping and so on. The mode specifies whether the animation is
  * looping or not.
  * 
  * @param stateTime the time spent in the state represented by this animation.
  * @param looping whether the animation is looping or not.
  * @return the TextureRegion representing the frame of animation for the given state time. */
 public TextureRegion getKeyFrame (float stateTime, boolean looping) {
  // we set the play mode by overriding the previous mode based on looping
  // parameter value
  if (looping && (playMode == NORMAL || playMode == REVERSED)) {
   if (playMode == NORMAL)
    playMode = LOOP;
   else
    playMode = LOOP_REVERSED;
  } else if (!looping && !(playMode == NORMAL || playMode == REVERSED)) {
   if (playMode == LOOP_REVERSED)
    playMode = REVERSED;
   else
    playMode = LOOP;
  }

  return getKeyFrame(stateTime);
 }

 /** Returns a {@link TextureRegion} based on the so called state time. This is the amount of seconds an object has spent in the
  * state this Animation instance represents, e.g. running, jumping and so on using the mode specified by
  * {@link #setPlayMode(int)} method.
  * 
  * @param stateTime
  * @return the TextureRegion representing the frame of animation for the given state time. */
 public TextureRegion getKeyFrame (float stateTime) {
  int frameNumber = getKeyFrameIndex (stateTime);
  return keyFrames[frameNumber];
 }
 
 /** Returns the current frame number.
  * @param stateTime
  * @return current frame number */
 public int getKeyFrameIndex (float stateTime) {
  int frameNumber = (int)(stateTime / frameDuration);

  if(keyFrames.length == 1)
         return 0;
  
  switch (playMode) {
  case NORMAL:
   frameNumber = Math.min(keyFrames.length - 1, frameNumber);
   break;
  case LOOP:
   frameNumber = frameNumber % keyFrames.length;
   break;
  case LOOP_PINGPONG:
   frameNumber = frameNumber % ((keyFrames.length * 2) - 2);
   //if (frameNumber >= keyFrames.length)
   frameNumber = keyFrames.length -1 - Math.abs(frameNumber - keyFrames.length + 1);//keyFrames.length - 2 - (frameNumber - keyFrames.length);
   break;
  case LOOP_RANDOM:
   frameNumber = MathUtils.random(keyFrames.length - 1);
   break;
  case REVERSED:
   frameNumber = Math.max(keyFrames.length - frameNumber - 1, 0);
   break;
  case LOOP_REVERSED:
   frameNumber = frameNumber % keyFrames.length;
   frameNumber = keyFrames.length - frameNumber - 1;
   break;

  default:
   // play normal otherwise
   frameNumber = Math.min(keyFrames.length - 1, frameNumber);
   break;
  }
  
  return frameNumber;
 }

 /** Sets the animation play mode.
  * 
  * @param playMode can be one of the following: Animation.NORMAL, Animation.REVERSED, Animation.LOOP, Animation.LOOP_REVERSED,
  *           Animation.LOOP_PINGPONG, Animation.LOOP_RANDOM */
 public void setPlayMode (int playMode) {
  this.playMode = playMode;
 }

 /** Whether the animation would be finished if played without looping (PlayMode Animation#NORMAL), given the state time.
  * @param stateTime
  * @return whether the animation is finished. */
 public boolean isAnimationFinished (float stateTime) {
  if(playMode != NORMAL && playMode != REVERSED) return false;
  int frameNumber = (int)(stateTime / frameDuration);
  return keyFrames.length - 1 < frameNumber;
 }
}

All that is different is that on PINGPONG mode, it doesn't linger extra long at the ends.  I had to update all my import commands to import mine, instead of libgdx's.

So there you go, we created an animation system using entities and components.

To  bring you up to speed, my project currently has the following structure
  • GameXYZ
  • EntityFactory
  • Components
    • Expires
    • Player
    • Position
    • Sprite
    • SpriteAnimation
  • Systems
    • ExpiringSystem
    • HudRenderSystem
    • SpriteAnimationSystem
    • SpriteRenderSystem
  • Custom
    • Animation
  • Utils
    • ImagePacker
Then, of course, in the Desktop project I have a Launcher.  I know I'm not using Expiring or Player yet, but they seem pretty important for down the line, and I probably won't change them a bit

You have gained 100 XP.  Progress to Level 3: 100/600

Thursday, February 7, 2013

Spaceship Warrior Pt 6 (Level 1)

This is the last post we will have to do in Spaceship Warrior - the end is in sight!  And all things considered, nothing coming up is very hard given what we have learned.  There are some surprises though.

First, we're going to add the particle explosion effect.  Notice in the demo that whenever your bullets hit an enemy ship, there's a small explosion with a shower of particles flying out in all directions.  And if you shoot for a long enough time, you'll notice that most of the particles fade away really quickly, but sometimes there is a particle explosion that's extra bright and seems to last longer / travel farther out.  How is all this handled?

Well, that last part is a glitch, albeit a really cool one!  But we'll get to it in due time.  First let's look at where the particles are created.  In CollisionSystem.java, when a collision is detected, we run this code:
for (int i = 0; i < 50; i++) EntityFactory.createParticle(world, bp.x, bp.y).addToWorld();

To run this we have to update our EntityFactory with createParticle()
 public static Entity createParticle(World world, float x, float y) {
  Entity e = world.createEntity();
  
  Position position = new Position();
  position.x = x;
  position.y = y;
  e.addComponent(position);
  
  Sprite sprite = new Sprite();
  sprite.name = "particle";
  sprite.scaleX = sprite.scaleY = MathUtils.random(0.3f, 0.6f);
  sprite.r = 1;
  sprite.g = 216/255f;
  sprite.b = 0;
  sprite.a = 0.5f;
  sprite.layer = Sprite.Layer.PARTICLES;
  e.addComponent(sprite);
  
  Velocity velocity = new Velocity(MathUtils.random(-400,400), MathUtils.random(-400,400));
  e.addComponent(velocity);
  
  Expires expires = new Expires();
  expires.delay = 1;
  e.addComponent(expires);

  return e;
 }

As makes sense, the particles should all have a random velocity.  But it's not perfect.  If you pay close attention to the particle explosions (and it helps to decrease the velocity range from -100 to 100), you'll notice that they don't explode outward in a circle, but rather they explode into a kind of rectangle.  It's not wrong per se... but it certainly violates my sense of explosion aesthetics.  The reason is that the velocity vector can point anywhere in the entire rectangle (-400,-400) to (400,400)  In other words, the maximum velocity achievable if the particle is moving strictly left or right (or up or down) is 400.  But the maximum velocity along the diagonals is sqrt(400^2 + 400^2) = 400sqrt(2) = 566.  That's why particles on the diagonal go farther - they can go faster!

Instead of generating random velocities from -400 to 400 in both dimensions, instead we can generate a random angle from 0 to 2*pi, and a random magnitude from 0 to 400, then use sin() and cos() to grab the components.  It's a little more time consuming of an operation, but not terrible.  If you really need to nickel and dime your performance, maybe go back to the rectangle, but in my code I'm going with this instead.
  
  float radians = MathUtils.random(2*MathUtils.PI);
  float magnitude = MathUtils.random(400f);
  
  Velocity velocity = new Velocity(magnitude * MathUtils.cos(radians),magnitude * MathUtils.sin(radians));
  e.addComponent(velocity);

Note that MathUtils.sin() and MathUtils.cos() take radians for the angle (not degrees), and are precalculated at the beginning so you aren't running a full sin() or cos() method every time.  It's just a lookup table optimized for games using libgdx, but if they drag you down too much, you can download the libgdx source code, decrease the precision of those functions, and recompile it into a .jar your own dang self (which is actually pretty easy to do if you look at their code).

Our particles don't really fade away like the demo's yet, they just disperse until they vanish.  We'll change that by introducing the ColorAnimation component and corresponding ColorAnimationSystem.  The Component is really easy to understand, you can just steal it straight away, but well look at the System:
public class ColorAnimationSystem extends EntityProcessingSystem {
 @Mapper ComponentMapper<ColorAnimation> cam;
 @Mapper ComponentMapper<Sprite> sm;

 public ColorAnimationSystem() {
  super(Aspect.getAspectForAll(ColorAnimation.class, Sprite.class));
 }

 @Override
 protected void process(Entity e) {
  ColorAnimation c = cam.get(e);
  Sprite sprite = sm.get(e);
  
  if(c.alphaAnimate) {
   sprite.a += c.alphaSpeed * world.delta;
   
   if(sprite.a > c.alphaMax || sprite.a < c.alphaMin) {
    if(c.repeat) {
     c.alphaSpeed = -c.alphaSpeed;
    } else {
     c.alphaAnimate = false;
    }
   }
  }
 }
}

I'm starting with the demo's code, which didn't really complete the ColorAnimationSystem, but built the part they needed.  You see they just update the sprites alpha level, or transparency, decreasing it steadily from 1 to 0.  When it reaches the end, if repeat is set to true, it reverses direction and heads back to 1.  Otherwise it sets alphaAnimate to false, which stops it from happening.  I thought about replacing that with e.removeComponent(c), but that would cause problems if the alpha was done, but the others: r, g, and b, were not.

Add this system to your world, and update EntityFactory to add a ColorAnimation to new particles like this:
ColorAnimation colorAnimation = new ColorAnimation();
colorAnimation.alphaAnimate = true;
colorAnimation.alphaSpeed = -1f;
colorAnimation.alphaMin = 0f;
colorAnimation.alphaMax = 1f;
colorAnimation.repeat = false;
e.addComponent(colorAnimation);

If we run it, we see particles fade away as expected.  But note that you will sometimes also see an abnormally large particle explosion with extra bright particles!  This is a neat effect, but where on Earth did it come from?  The problem is in line 15.

This allows the alpha to be decreased to something below zero.  It turns out that if you do that, libgdx folds it back up to be between 0 and 1, and just below 0 maps to just below 1.  That means that near the end of its life, your particles went from transparent to completely opaque!  More opaque than they began!  While I like the effect, it doesn't look so good in other cases.  Imagine your ship obtained a cloaking device, and to represent that you created an alpha animation from 1 to 0 and back, on repeat.  If you let it go below 0, it will fade out gently, suddenly pop into full brightness, then disappear and gradually fade back in.  Then, when it's up to full alpha, there's a good chance it will overshoot alpha=1, and become almost transparent again!  In this case, things aren't so good.  I fixed it by changing it as follows:
 protected void process(Entity e) {
  ColorAnimation c = cam.get(e);
  Sprite sprite = sm.get(e);
  
  if(c.alphaAnimate) {
   sprite.a += c.alphaSpeed * world.delta;
   
   if(sprite.a > c.alphaMax) {
    sprite.a = c.alphaMax;
    if(c.repeat) {
     c.alphaSpeed = -c.alphaSpeed;
    } else {
     c.alphaAnimate = false;
    }
   }
   
   else if(sprite.a < c.alphaMin) {
    sprite.a = c.alphaMin;
    if(c.repeat) {
     c.alphaSpeed = -c.alphaSpeed;
    } else {
     c.alphaAnimate = false;
    }
   }
  }
  
  if(c.redAnimate) {
   sprite.r += c.redSpeed * world.delta;
   
   if(sprite.r > c.redMax) {
    sprite.r = c.redMax;
    if(c.repeat) {
     c.redSpeed = -c.redSpeed;
    } else {
     c.redAnimate = false;
    }
   }
   
   else if(sprite.r < c.redMin) {
    sprite.r = c.redMin;
    if(c.repeat) {
     c.redSpeed = -c.redSpeed;
    } else {
     c.redAnimate = false;
    }
   }
  }
  
  if(c.greenAnimate) {
   sprite.g += c.greenSpeed * world.delta;
   
   if(sprite.g > c.greenMax) {
    sprite.g = c.greenMax;
    if(c.repeat) {
     c.greenSpeed = -c.greenSpeed;
    } else {
     c.greenAnimate = false;
    }
   }
   
   else if(sprite.g < c.greenMin) {
    sprite.g = c.greenMin;
    if(c.repeat) {
     c.greenSpeed = -c.greenSpeed;
    } else {
     c.greenAnimate = false;
    }
   }
  }
  
  if(c.blueAnimate) {
   sprite.b += c.blueSpeed * world.delta;
   
   if(sprite.b > c.blueMax) {
    sprite.b = c.blueMax;
    if(c.repeat) {
     c.blueSpeed = -c.blueSpeed;
    } else {
     c.blueAnimate = false;
    }
   }
   
   else if(sprite.b < c.blueMin) {
    sprite.b = c.blueMin;
    if(c.repeat) {
     c.blueSpeed = -c.blueSpeed;
    } else {
     c.blueAnimate = false;
    }
   }
  }
}

Now we have particles that just gradually fade out.  We lost the big, cool particle explosions, but I guess that's life.

The demo also adds a tiny explosion graphic for every hit, and a big one when the ship dies.  To do this, update CollisionSystem and EntityFactory to add the explosions:
   public void handleCollision(Entity bullet, Entity ship) {
    Health health = hm.get(ship);
    Position bp = pm.get(bullet);
    health.health -= 10;
    
    EntityFactory.createExplosion(world, bp.x, bp.y, 0.1f).addToWorld();
    
    for (int i = 0; i < 50; i++) EntityFactory.createParticle(world, bp.x, bp.y).addToWorld();   
    if (health.health <= 0) {
     ship.deleteFromWorld();
     EntityFactory.createExplosion(world, bp.x, bp.y, 0.5f).addToWorld();
    }
    
    bullet.deleteFromWorld();
   }

Update your EntityFactory like the Demo's, and grab ScaleAnimation and ScaleAnimationSystem from the demo, they work as prescribed, though they do force the x and y components be scaled identically.  Add the ScaleAnimationSystem to your world, and you have yourself some bona fide explosions!

To finish up our special effects, the only piece we're really missing are the background stars.  They again work as prescribed, so they can be stolen straight from the demo.  The only thing to remember is that you have to change the width and height variables manually to 1280 and 900, because our program doesn't reference some universal static field with that information.  It probably should... if you want to update it, make sure you update the camera.setToOrtho, the enemy spawning system, and the Launcher.  When I updated mine, I included two variables called public static int WINDOW_HEIGHT and WINDOW_WIDTH in GameXYZ.java.  That way it was highly visible.  I also updated them in the resize() method in GameXYZ.java.

Anyway, to set the stars up you need to update EntityFactory, create an empty (placeholder) component ParallaxStar, and implement ParallaxStarRepeatingSystem.  The actually create the stars, make a loop in GameXYZ.java which creates a bunch of them.  Notice in EntityFactory that they also have a random velocity, but it's all in the negative y direction, and they have a random scale and random alpha.  Once you've got your stars up and running, you are very near the completed product.

The only bit remaining is to add in the HudRenderingSystem and HealthRenderingSystem.  To make them work, you must copy the resources/fonts folder from the demo into your own project.  Remember, Eclipse won't update the explorer automatically, you have to right click the project and refresh.  These systems again work about as prescribed, with no surprises.  HudRenderingSystem for some reason loads your texture-atlas - it doesn't actually use that information, so I got ride of all those lines in mine.  Remember, rendering systems should be updated manually, so you need fields in GameXYZ.java to hold them, just like you did SpriteRenderSystem, and when you add them to the world you need to pass the "true" argument.  You wouldn't want to inadvertently render in the middle of processing your other systems, and you'd hate to render the HUD or HEALTH systems beneath your main sprites.  So deal with them manually, and under spriteRenderSystem.process(), include both of these.

After that, you are officially pretty much done.  Give it a whirl and be amazed - you have recreated Starship Warrior!  Except for one, tiny little problem.  The demo cruises along and hundreds of FPS... on my medium laptop.  My program at this point only ran at 60.  ... ... WTF?!?!

Hunting this bugger down brought me back to Launcher.java.  In the demo's launcher, they set vsynch to false, something I hadn't done.  Making just that one little change made me skyrocket up into the hundreds right past the demo even!

You may have noticed that I was a lot less specific in this page.  That's partly because I think it was all pretty easy by this point, but mostly because I'm TIRED of Spaceship Warrior.  I want to move on to something COOLER!  If you managed to make it here, congratulations!  You just defeated the Spaceship Warrior and completed your quest.

You have gained 150 XP.  Progress to Level 2: 400/400
DING!  You have advanced to Level 2, congratulations!
As a Level 2 PC, you have mastered
  • The art of setting up Artemis systems and components within the libgdx game loop
  • Basic use of the GroupManager, with a specific application to collision detection
  • The ability to handle multiple rendering systems, along with sprite manipulation animations
You have also started down the path of understanding how to read open source game code, but you are by no means a master.  More of... a Novice's Apprentice.

Sunday, February 3, 2013

Spaceship Warrior Pt 5 (Level 1)

In this article we'll add collision detection and ship health.  To do this, the demo has added 2 new components, and 1 new system:
Components
  • Health - Obviously to track how much health the ship has.
  • Bounds - This component defines the boundary which determines what constitutes a collision or not.  In the demo, all the boundaries are just circles.
Systems
  • CollisionSystem
The components are really simple, so let's start with them.
package com.gamexyz.components;

import com.artemis.Component;

public class Bounds extends Component {
 
 public Bounds(float radius) {
  this.radius = radius;
 }
 
 public Bounds() {
  this(0);
 }
 
 public float radius;
}
package com.gamexyz.components;

import com.artemis.Component;

public class Health extends Component {
 
 public Health(float health, float maxHealth) {
  this.health = health;
  this.maxHealth = maxHealth;
 }
 
 public Health(float health) {
  this(health,health);
 }
 
 public Health() {
  this(0,0);
 }
 
 public float health, maxHealth;

}

The demo includes the maxHealth field so it can display the percent health left.  We have to update our EntityFactory and EntitySpawningSystem to add bounds and health to our ships and bullets (probably no health to bullets though).  Make up your own values or check the demo to see what they give used.

The real magic comes in the CollisionSystem, take a look at mine here:
package com.gamexyz.systems;

import com.artemis.Aspect;
import com.artemis.ComponentMapper;
import com.artemis.Entity;
import com.artemis.EntitySystem;
import com.artemis.annotations.Mapper;
import com.artemis.managers.GroupManager;
import com.artemis.utils.Bag;
import com.artemis.utils.ImmutableBag;
import com.artemis.utils.Utils;
import com.gamexyz.Constants;
import com.gamexyz.components.Bounds;
import com.gamexyz.components.Health;
import com.gamexyz.components.Position;

public class CollisionSystem extends EntitySystem {
 @Mapper ComponentMapper<Position> pm;
 @Mapper ComponentMapper<Bounds> bm;
 @Mapper ComponentMapper<Health> hm;
 
 private Bag<CollisionPair> collisionPairs;

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

 @Override
 public void initialize() {
  collisionPairs = new Bag<CollisionPair>();
  
  collisionPairs.add(new CollisionPair(Constants.Groups.PLAYER_BULLETS, Constants.Groups.ENEMY_SHIPS, new CollisionHandler() {
   @Override
   public void handleCollision(Entity bullet, Entity ship) {
    Health health = hm.get(ship);
    health.health -= 10;
    
    bullet.deleteFromWorld();
    
    if (health.health <= 0) {
     ship.deleteFromWorld();
    }
   }
  }));
 }
 
 @Override
 protected void processEntities(ImmutableBag<Entity> entities) {
  for(int i = 0; collisionPairs.size() > i; i++) {
   collisionPairs.get(i).checkForCollisions();
  }
 }
 
 @Override
 protected boolean checkProcessing() {
  return true;
 }

 private class CollisionPair {
  private ImmutableBag<Entity> groupEntitiesA;
  private ImmutableBag<Entity> groupEntitiesB;
  private CollisionHandler handler;
 
  public CollisionPair(String group1, String group2, CollisionHandler handler) {
   groupEntitiesA = world.getManager(GroupManager.class).getEntities(group1);
   groupEntitiesB = world.getManager(GroupManager.class).getEntities(group2);
   this.handler = handler;
  }
 
  public void checkForCollisions() {
   for(int a = 0; groupEntitiesA.size() > a; a++) {
    for(int b = 0; groupEntitiesB.size() > b; b++) {
     Entity entityA = groupEntitiesA.get(a);
     Entity entityB = groupEntitiesB.get(b);
     if(collisionExists(entityA, entityB)) {
      handler.handleCollision(entityA, entityB);
     }
    }
   }
  }
  
  private boolean collisionExists(Entity e1, Entity e2) {
   Position p1 = pm.get(e1);
   Position p2 = pm.get(e2);
   
   Bounds b1 = bm.get(e1);
   Bounds b2 = bm.get(e2);
   
   return Utils.doCirclesCollide(p1.x, p1.y, b1.radius, p2.x, p2.y, b2.radius);
   //return Utils.distance(p1.x, p1.y, p2.x, p2.y)-b1.radius < b2.radius;
  }
 }
 
 private interface CollisionHandler {
  void handleCollision(Entity a, Entity b);
 }
}
One thing you may notice is that it references the GroupManager that we skipped in the past.  So now we'll have to update our EntityFactory to also include the group manager declarations.  Also, in GameXYZ.java, you must add the GroupManager to the world.
world.setManager(new GroupManager());

But back to the CollisionSystem.  In line 26 it purportedly processes entities with Position and Bounds, but that call ends up being ignored.  Consequently, we need to assign our groups carefully to make sure we don't accidentally process something which does not have those components.  Instead of relying on the System to hand it appropriate entities, it has its own private class called CollisionPair (see lines 60-93).  The idea is that a CollisionPair is created that specifically processes collisions between two different GroupManagers, in our case PLAYER_BULLETS and ENEMY_SHIPS (line 33).  This can be useful because we don't want to worry about enemy ships colliding with one another, nor the possibility of bullets colliding with one another.  The processEntities() method, which comes with an ImmutableBag of entities to be processed, once again ignores the passed Bag and instead processes all the predefined CollisionPairs (lines 49-53).  The CollisionPair consists of Bags containing the entities in each group of the pair, and to find collisions it iterates over each entity combination between the Bags to see if their bounds overlap (lines 71-81).  If there is a collision, it runs the handleCollision() method which is defined for each CollisionPair in the constructor (lines 33-45).  The demo uses line 91 to see if a collision happens, but I found Artemis also comes with a doCirclesCollide() method which does the same thing.  I haven't tested (and probably never will) which method runs faster, but I'm guessing there is no major difference.

So to bring our project up to speed for this, we had to create those Components and add them to our entities, we had to add the GroupManager to the world in GameXYZ.java and add entities to the appropriate GroupManagers in EntityFactory.java.  Then, the CollisionSystem checks all pairs of things that CAN collide, and processes a method which you construct to deal with the collision.  In this, we delete the bullet and damage the ship.  Then, if the ship's health has run out, we delete it as well.  Part of me would rather create a HealthSystem which just checks to see if anyone's health has gone below 0, because maybe in more advanced games there are multiple ways to lose health, and I wouldn't want to have to hardcode checking to see if it kills them every time.  For us, for now, this is fine.

It's still not pretty, we don't have any particle effects or explosions, but it's officially a game!  Next we'll explore adding the special effects animations.

You gained 50 XP.  Progress to level 2: 250/400

Saturday, February 2, 2013

Spaceship Warrior Pt 4 (Level 1)

As of this post, we have a game with a ship that can be controlled, and guns that can fire.  We've even made the bullets disappear after a few seconds, so they don't keep using system resources.  But no enemies.  And somehow, a game with nothing more than the player's ship isn't all that fun.

This post will cover how to add enemy ships, but first I think it's high time we improved our Sprite class.  If you'll remember, the one we made was a lot simpler than the demo's class, mostly because I wanted to get SOMETHING on the screen somehow, so I reverted to my previous skills.  But today we're diving into TexturePacker.

Well, as of more recent versions of libgdx, I should say TexturePacker2, which is apparently faster in Java 1.7.  The fist thing to look at is the ImagePacker.java class that comes with Spaceship Warrior.  It's not very long, but most of the settings included in it aren't all that important.  You can see the full list of settings here, and notice that some of them are just set to the default, and others have been replaced (for instance padding has been replaced with paddingX and paddingY).  This is the modified version that I made in com.gamexyz.utils:
package com.gamexyz.utils;

import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.tools.imagepacker.TexturePacker2;
import com.badlogic.gdx.tools.imagepacker.TexturePacker2.Settings;

public class ImagePacker {

 public static void run() {
  Settings settings = new Settings();  
  settings.filterMin = Texture.TextureFilter.Linear;
  settings.filterMag = Texture.TextureFilter.Linear;
  settings.pot=false;
        TexturePacker2.process(settings, "textures-original", "resources/textures", "pack");
 }

}

Setting pot=false is fine for us because we opted to use OgenGL2.0, so our images don't have to be loaded as powers of two.  "textures-original" is the folder in GameXYZ-desktop which holds all the images, and this file will make a new folder in that project called resources, with a subfolder called textures, and files pack.atlas and pack.png.

To run this, in our Launcher.java file, just include (somewhere in main(), before the new LwjglApplication() line.
ImagePacker.run();

And make sure to ctrl+shift+o to update your imports list.  Now running the program will include some console lines about making your files, but over in the Eclipse explorer, they're nowhere to be seen!  When "Eclipse" doesn't create files for your project, even if it's your program IN Eclipse, those files won't be automatically added to the project.  To make them show, right click on the GameXYZ-desktop project and click "Refresh".  They should show up now.  Open up pack.png to see what it looks like.

NOTE:  If you ever release your game, you will not want to include this line of code, but rather just include the already created Texture atlas.  For large games, it will add unnecessary launch time to recreate the atlas every time.

Of course, we still aren't loading using them to get our images, we're still referencing the old folder (which is still there, thankfully).  To access images from within a TextureAtlas, we just need these lines of code
TextureAtlas atlas = new TextureAtlas(Gdx.files.internal("textures/pack.atlas"),Gdx.files.internal("textures"));
AtlasRegion region = atlas.findRegion("imagename")

"imagename" should not include the ".png", but to access fighter.png we would just say findRegion("fighter").  Unfortunately, findRegion is slow, so we don't want to do it EVERY single time we want to load that image (which is quite often - for instance, every time we shoot a bullet we will need to point to that image).  The demo's workaround is to create a HashMap which maps String->AtlasRegion.  The HashMap is quicker to look up in, and so right after we load the TextureAtlas the first time, we'll make a HashMap to associate the imagename with with correct image.  Even then, the HashMap isn't as quick as we'd like, and Artemis includes a much faster container called a Bag.

So when we first load the program, we will read the Textures from the atlas and store them in a HashMap associating the name to the image.  From there, whenever we create a new entity we will ask the HashMap which image it should get.  Then we will store that image in a Bag, referenced by the entity's ID.  That way, every time we draw entities to the screen we only have to look the images up in the quickest container.

All of this will go into our SpriteRenderSystem, and to get started we can add the following code
public class SpriteRenderSystem extends EntitySystem {
(...)
 private TextureAtlas atlas;
 private HashMap<String, AtlasRegion> regions;
 private Bag<Atlasregion> regionsByEntity;
 
 @Override
 protected void initialize() {
  batch = new SpriteBatch();
  
  atlas = new TextureAtlas(Gdx.files.internal("textures/pack.atlas"),Gdx.files.internal("textures"));
  regions = new HashMap<String, AtlasRegion>();
  for (AtlasRegion r : atlas.getRegions()) {
   regions.put(r.name, r);
  }
  regionsByEntity = new Bag<atlasregion>();
 }
(...)
 @Override
 protected void inserted(Entity e) {
  Sprite sprite = sm.get(e);
  regionsByEntity.set(e.getId(), regions.get(sprite.name));
 }
 
 @Override
 protected void removed(Entity e) {
  regionsByEntity.set(e.getId(), null);
 }
(...)
}
Notice that we have included in the inserted() and removed() methods code which will keep our Bag up to date, but we need to update our Sprite component to have a String called name, which stores something like "fighter" or "bullet".  For now, I'll let you add it however you want, but later I'll post my full Sprite class so we can be on the same page.

Now that we have a Bag which maps entities to the correct AtlasRegion, we can update our process(Entity) method to draw from this.
 protected void process(Entity e) {
  if (pm.has(e)) {
   Position position = pm.getSafe(e);
   Sprite sprite = sm.get(e);
   
   AtlasRegion spriteRegion = regionsByEntity.get(e.getId());
   batch.setColor(sprite.r, sprite.g, sprite.b, sprite.a);

   float posX = position.x - (spriteRegion.getRegionWidth() / 2 * sprite.scaleX);
   float posY = position.y - (spriteRegion.getRegionHeight() / 2 * sprite.scaleX);
   batch.draw(spriteRegion, posX, posY, 0, 0, spriteRegion.getRegionWidth(), spriteRegion.getRegionHeight(), sprite.scaleX, sprite.scaleY, sprite.rotation);

  }
 }
Here lines 9-10 allow us to draw the image CENTERED at the position coordinates, whereas before the sprite was always drawn from the bottom left corner.

Running this should basically give you the same result as before, except now our bullets are off center because we changed where the ship is drawn relative to it's position.  Back in PlayerInputSystem we can change where we create the bullets, I like them in the x direction at pos.x-27 and pos.x+27, and in the y direction to both be pos.y+7.

There is one more thing we need to do to bring our SpriteRenderSystem and Sprite classes up to snuff.  In the Spaceship Warrior demo, the particle effects are drawn on top of everything, the little ships are always above you, you are always above the bigger ships, and everything is above the starfield.  This was accomplished by introducing Layers, and drawing the bottom layer first and so on.  You can see this really easily by looking at a 2D platformer, like this Indie game Hyperion, which has a starfield in the back, a layer of trees in front of that, another layer of trees, and then the game world.  They scroll these layers at different speeds to get an effect of depth, but it would be ruined if the stars got drawn last, because they would become the front layer!

To manage layers, the Sprite component gets an enumeration of the different possible layers, and each sprite gets assigned to one.  Then the SpriteRenderSystem maintains a SORTED list of entities, sorted based on which layer they are in.  Thus the back layers get drawn first, and so on.  The demo stored this sorted list as a List<Entity> called sortedEntities.  Remember the processEntities() method which passed us our bag of entities?  Well, now we'll ignore that passed Bag because the Bag class doesn't maintain any order, and thus can't be sorted.

This is what my final Sprite and SpriteRenderSystem look like to take all this into account:
package com.gamexyz.components;

import com.artemis.Component;

public class Sprite extends Component {
 
 public enum Layer {
  DEFAULT,
  BACKGROUND,
  ACTORS_1,
  ACTORS_2,
  ACTORS_3,
  PARTICLES;
  
  public int getLayerId() {
   return ordinal();
  }
 }
 
 public Sprite(String name, Layer layer) {
  this.name = name;
  this.layer = layer;
 }
 
 public Sprite(String name) {
  this(name, Layer.DEFAULT);
 }
 
 public Sprite() {
  this("default",Layer.DEFAULT);
 }
 
 public String name;
 public float r = 1;
 public float g = 1;
 public float b = 1;
 public float a = 1;
 public float scaleX = 1;
 public float scaleY = 1;
 public float rotation;
 public Layer layer = Layer.DEFAULT;

}
package com.gamexyz.systems;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;

import com.artemis.Aspect;
import com.artemis.ComponentMapper;
import com.artemis.Entity;
import com.artemis.EntitySystem;
import com.artemis.annotations.Mapper;
import com.artemis.utils.Bag;
import com.artemis.utils.ImmutableBag;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
import com.gamexyz.components.Position;
import com.gamexyz.components.Sprite;

public class SpriteRenderSystem extends EntitySystem {
 @Mapper ComponentMapper<Position> pm;
 @Mapper ComponentMapper<Sprite> sm;
 
 private OrthographicCamera camera;
 private SpriteBatch batch;
 
 private TextureAtlas atlas;
 private HashMap<String, AtlasRegion> regions;
 private Bag<AtlasRegion> regionsByEntity;
 
 private List<Entity> sortedEntities;
 
 @SuppressWarnings("unchecked")
 public SpriteRenderSystem(OrthographicCamera camera) {
  super(Aspect.getAspectForAll(Position.class, Sprite.class));
  this.camera = camera;
 }
 
 @Override
 protected void initialize() {
  batch = new SpriteBatch();
  
  atlas = new TextureAtlas(Gdx.files.internal("textures/pack.atlas"),Gdx.files.internal("textures"));
  regions = new HashMap<String, AtlasRegion>();
  for (AtlasRegion r : atlas.getRegions()) {
   regions.put(r.name, r);
  }
  regionsByEntity = new Bag<AtlasRegion>();
  
  sortedEntities = new ArrayList<Entity>();
 }

 @Override
 protected boolean checkProcessing() {
  return true;
 }

 @Override
 protected void processEntities(ImmutableBag<Entity> entities) {
  for (Entity e : sortedEntities) {
   process(e);
  }
 }
 
 @Override
 protected void begin() {
  batch.setProjectionMatrix(camera.combined);
  batch.begin();
 }
 
 protected void process(Entity e) {
  if (pm.has(e)) {
   Position position = pm.getSafe(e);
   Sprite sprite = sm.get(e);
   
   AtlasRegion spriteRegion = regionsByEntity.get(e.getId());
   batch.setColor(sprite.r, sprite.g, sprite.b, sprite.a);

   float posX = position.x - (spriteRegion.getRegionWidth() / 2 * sprite.scaleX);
   float posY = position.y - (spriteRegion.getRegionHeight() / 2 * sprite.scaleX);
   batch.draw(spriteRegion, posX, posY, 0, 0, spriteRegion.getRegionWidth(), spriteRegion.getRegionHeight(), sprite.scaleX, sprite.scaleY, sprite.rotation);

  }
 }
 
 @Override
 protected void end() {
  batch.end();
 }
 
 @Override
 protected void inserted(Entity e) {
  Sprite sprite = sm.get(e);
  regionsByEntity.set(e.getId(), regions.get(sprite.name));
  
  sortedEntities.add(e);
  
  Collections.sort(sortedEntities, new Comparator<Entity>() {
   @Override
   public int compare(Entity e1, Entity e2) {
    Sprite s1 = sm.get(e1);
    Sprite s2 = sm.get(e2);
    return s1.layer.compareTo(s2.layer);
   }
  });
  
 }
 
 @Override
 protected void removed(Entity e) {
  regionsByEntity.set(e.getId(), null);
  sortedEntities.remove(e);
 }
}
The Sprite class doesn't have too many changes.  It has the enum, and a new constructor which takes Sprite(String name, Layer layer).  If those options aren't passed, I refer to a "default" in both cases.  For a default sprite, I googled to find a small exclamation mark png
and stuck it in the textures-original folder.

In SpriteRenderSystem, we added a List called sortedEntities and initialized it in the initialize() method.  In the inserted() and removed() methods we added the entity to sortedEntities, and in inserted() also added a sorter which compared them based on layer.  Entities in the same layer will be stacked in the order of their creation, from first on bottom to last on top.

In processEntities(), as promised we ignore the ImmutableBag we are given, and stick with our sortedEntities List to make sure we draw in the correct order.  I admit this was a lot of work to achieve no discernible change in our program, but I'm going to trust that it is stronger for it now, more expandable.  Because of course that's the ultimate goal, borrow the code and methods learned here and apply them to an original game.

Before we're done for the day, let's add some enemies, it turns out to not be too tough.  First, I edited my EntityFactory to look a bit more like the Demo's (though not completely) and added a createEnemyShip() method:
public class EntityFactory {

 public static Entity createPlayer(World world, float x, float y) {
  Entity e = world.createEntity();
  
  Position position = new Position();
  position.x = x;
  position.y = y;
  e.addComponent(position);
  
  Sprite sprite = new Sprite("fighter",Sprite.Layer.ACTORS_3);
  sprite.r = 93/255f;
  sprite.g = 255/255f;
  sprite.b = 129/255f;
  e.addComponent(sprite);
  
  Velocity velocity = new Velocity(0,0);
  e.addComponent(velocity);
  e.addComponent(new Player());
  
  return e;
 }
 
 public static Entity createBullet(World world, float x, float y) {
  Entity e = world.createEntity();
  
  Position position = new Position();
  position.x = x;
  position.y = y;
  e.addComponent(position);
  
  Sprite sprite = new Sprite();
  sprite.name = "bullet";
  sprite.layer = Sprite.Layer.PARTICLES;
  e.addComponent(sprite);
  
  Velocity velocity = new Velocity(0, 800);
  e.addComponent(velocity);
  
  Expires expires = new Expires(2f);
  e.addComponent(expires);
  
  return e;
 }
 
 public static Entity createEnemyShip(World world, String name, Sprite.Layer layer, float x, float y, float vx, float vy) {
  Entity e = world.createEntity();
  
  Position position = new Position();
  position.x = x;
  position.y = y;
  e.addComponent(position);
  
  Sprite sprite = new Sprite();
  sprite.name = name;
  sprite.r = 255/255f;
  sprite.g = 0/255f;
  sprite.b = 142/255f;
  sprite.layer = layer;
  e.addComponent(sprite);
  
  Velocity velocity = new Velocity(vx, vy);
  e.addComponent(velocity);
  
  return e;
 }
}

The lines setting Sprite.r, etc, provide a color filter.  If you look, the textures have white borders, where in the demo game the ships are colored.  That magic happens by setting these red, green, blue, and alpha filters.  The enemy ship method has a lot of arguments used to decide which kind of ship to create.  To actually create them, we'll make a new System called EntitySpawningTimerSystem, taken almost straight from the demo:
package com.gamexyz.systems;

import com.artemis.systems.VoidEntitySystem;
import com.artemis.utils.Timer;
import com.badlogic.gdx.math.MathUtils;
import com.gamexyz.EntityFactory;
import com.gamexyz.components.Sprite;

public class EntitySpawningTimerSystem extends VoidEntitySystem {

 private Timer timer1;
 private Timer timer2;
 private Timer timer3;

 public EntitySpawningTimerSystem() {
  timer1 = new Timer(2, true) {
   @Override
   public void execute() {
    EntityFactory.createEnemyShip(world, "enemy1", Sprite.Layer.ACTORS_3, MathUtils.random(0, 1280), 900 + 50, 0, -40).addToWorld();
   }
  };

  timer2 = new Timer(6, true) {
   @Override
   public void execute() {
    EntityFactory.createEnemyShip(world, "enemy2", Sprite.Layer.ACTORS_2, MathUtils.random(0, 1280), 900 + 100, 0, -30).addToWorld();
   }
  };

  timer3 = new Timer(12, true) {
   @Override
   public void execute() {
    EntityFactory.createEnemyShip(world, "enemy3", Sprite.Layer.ACTORS_1, MathUtils.random(0, 1280), 900 + 200, 0, -20).addToWorld();
   }
  };
 }

 @Override
 protected void processSystem() {
  timer1.update(world.delta);
  timer2.update(world.delta);
  timer3.update(world.delta);
 }

}
There are a couple of differences I noticed.  First, because we have separated ourselves into 2 projects, files in this project cannot see the variables we set in Launcher.java for width and height, so they had to be hardcoded.  In general, that's a pretty awful idea.  Perhaps they should be stored as static fields in GameXYZ.java?  Or perhaps a constants file.  Also, the demo's world seemed to range from [-width/2, +width/2] and likewise for height, so that (0,0) is right in the middle of the screen.  I suspect that the difference lies in the camera, but I couldn't just trivially see what was different.  As it stands, I actually like having (0,0) be the bottom left corner, and (width,height) be the top right, it just meant we had to tweak the code somewhat.

Other than that, I changed the createEnemyShip arguments to match those in my EntityFactory (since we haven't implemented all the components yet, certain things didn't need to be there - someday they probably will).  One neat thing is that the class extends VoidEntitySystem, which is a system that runs without processing any actual entities (i.e. there is no getAspectForAll(...)).  Other than that I think the Timer class is pretty awesome, and hopefully I'll be able to play with it more another time.

In summary, we revamped our SpriteRenderSystem to use a TextureAtlas instead of loading individual .PNGs, and implemented a set of Layers to control which entities are drawn on top of which other ones.  We also added a new system which spawns lots of entities.  Overall, I feel like at this point most of the quirks are out of the way.  Coming into the project, the one that scared me was their rendering system.  Of course we haven't touched the collision detection yet, I'm eager to see how that works out.  We also haven't touched on the Groups - the demo had them but I don't really think it ever used them.  It may be that we can totally ignore that facet, we'll see.  I'm also excited to get around to the ScaleAnimationSystem, and add particles and stars.  Fun times ahead!

You have gained 50 XP.  Progress to Level 2: 200/400

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