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.

1 comment:

  1. Thanks for the blog, really helpful so far! But I've come into a problem.

    I have the number of explosion particles dependent on ship's health; thus, when the big one 'explodes', about a hundred particles are created and it lags for a little bit (at the moment when they are created; they are rendered at max fps after that).

    I've messed around with spriteBatch fields like size and blending, but the problem still persists. What am I doing wrong?

    Again, thanks for the articles!

    ReplyDelete