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

5 comments:

  1. How can I find all entities with the same groupname defined by the GroupManager?

    ReplyDelete
    Replies
    1. You can get an instance of the GroupManager using:

      GroupManager gm = world.getManager(GroupManager.class);

      Then you can get your entities by calling:

      ImmutableBag<Entity> entities = gm.getEntities("groupName");

      You can also just combine it into a single statement (probably for the best):

      ImmutableBag<Entity> entities =
      world.getManager(GroupManager.class).getEntities("groupName");

      Delete
  2. Is there any way to work with collisions in the form of Rectangles?

    ReplyDelete
  3. I seem to have missed something? com.gamexyz.Constants? I tried to back into it, but I can't seem to get it to work?

    ReplyDelete
    Replies
    1. Forgot to check the original source. Back on track

      Delete