Full LibGDX Game Tutorial – Shooting
Welcome to part 14 of our Full LibGDX Game Tutorial. This part will focus on allowing the player to start shooting enemies. If you haven’t seen the earlier parts of this tutorial I advise you to start at Full LibGDX Game Tutorial – Project setup as this tutorial continues from these earlier parts. For those of you who have come from part 13, you can continue on.
Now our game has some enemies, we can think about adding a player shooting action that will allow the player to shoot bullets that will instantly kill and enemy. The bullet and enemy will then de-spawn. Since we want to be able to tell when a bullet has hit an enemy we will need to add a collision component to out enemy so they are included in the collision system.
Shooting – Create Enemy Update
update the createEnemy method in your levelFactory so enemy now has collision component:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
public Entity createEnemy(TextureRegion tex, float x, float y){ Entity entity = engine.createEntity(); B2dBodyComponent b2dbody = engine.createComponent(B2dBodyComponent.class); TransformComponent position = engine.createComponent(TransformComponent.class); TextureComponent texture = engine.createComponent(TextureComponent.class); EnemyComponent enemy = engine.createComponent(EnemyComponent.class); TypeComponent type = engine.createComponent(TypeComponent.class); CollisionComponent colComp = engine.createComponent(CollisionComponent.class); b2dbody.body = bodyFactory.makeCirclePolyBody(x,y,1, BodyFactory.STONE, BodyType.KinematicBody,true); position.position.set(x,y,0); texture.region = tex; enemy.xPosCenter = x; type.type = TypeComponent.ENEMY; b2dbody.body.setUserData(entity); entity.add(colComp); entity.add(b2dbody); entity.add(position); entity.add(texture); entity.add(enemy); entity.add(type); engine.addEntity(entity); return entity; } |
Shooting – Collision System Update
Now the enemy has a collision component we need to update the collisionSystem so it is able to process enemies. Previously we only cared about collisions with the player so the collisionSystem used to take the player as an argument. This is no longer the case so we have to remove the player from the constructor and only process entities with collision components. Update your collisionSystem to match this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
package blog.gamedevelopment.box2dtutorial.entity.systems; import blog.gamedevelopment.box2dtutorial.entity.components.CollisionComponent; import blog.gamedevelopment.box2dtutorial.entity.components.EnemyComponent; import blog.gamedevelopment.box2dtutorial.entity.components.Mapper; import blog.gamedevelopment.box2dtutorial.entity.components.PlayerComponent; import blog.gamedevelopment.box2dtutorial.entity.components.TypeComponent; import com.badlogic.ashley.core.ComponentMapper; import com.badlogic.ashley.core.Entity; import com.badlogic.ashley.core.Family; import com.badlogic.ashley.systems.IteratingSystem; public class CollisionSystem extends IteratingSystem { ComponentMapper<CollisionComponent> cm; ComponentMapper<PlayerComponent> pm; @SuppressWarnings("unchecked") public CollisionSystem() { super(Family.all(CollisionComponent.class).get()); cm = ComponentMapper.getFor(CollisionComponent.class); pm = ComponentMapper.getFor(PlayerComponent.class); } @Override protected void processEntity(Entity entity, float deltaTime) { // get collision for this entity CollisionComponent cc = cm.get(entity); //get collided entity Entity collidedEntity = cc.collisionEntity; TypeComponent thisType = entity.getComponent(TypeComponent.class); // Do Player Collisions if(thisType.type == TypeComponent.PLAYER){ if(collidedEntity != null){ TypeComponent type = collidedEntity.getComponent(TypeComponent.class); if(type != null){ switch(type.type){ case TypeComponent.ENEMY: //do player hit enemy thing System.out.println("player hit enemy"); PlayerComponent pl = pm.get(entity); pl.isDead = true; int score = (int) pl.cam.position.y; System.out.println("Score = "+ score); break; case TypeComponent.SCENERY: //do player hit scenery thing pm.get(entity).onPlatform = true; System.out.println("player hit scenery"); break; case TypeComponent.SPRING: //do player hit other thing pm.get(entity).onSpring = true; System.out.println("player hit spring: bounce up"); break; case TypeComponent.OTHER: //do player hit other thing System.out.println("player hit other"); break; case TypeComponent.BULLET: System.out.println("Player just shot. bullet in player atm"); break; default: System.out.println("No matching type found"); } cc.collisionEntity = null; // collision handled reset component }else{ System.out.println("type == null"); } } }else if(thisType.type == TypeComponent.ENEMY){ // Do enemy collisions if(collidedEntity != null){ TypeComponent type = collidedEntity.getComponent(TypeComponent.class); if(type != null){ switch(type.type){ case TypeComponent.PLAYER: System.out.println("enemy hit player"); break; case TypeComponent.ENEMY: System.out.println("enemy hit enemy"); break; case TypeComponent.SCENERY: System.out.println("enemy hit scenery"); break; case TypeComponent.SPRING: System.out.println("enemy hit spring"); break; case TypeComponent.OTHER: System.out.println("enemy hit other"); break; case TypeComponent.BULLET: EnemyComponent enemy = Mapper.enemyCom.get(entity); enemy.isDead = true; System.out.println("enemy got shot"); default: System.out.println("No matching type found"); } cc.collisionEntity = null; // collision handled reset component }else{ System.out.println("type == null"); } } } } } |
Here we have removed the player from the constructor and then split the processEntity into a player and enemy checking version. Currently, this is the only two objects we need to check collisions on.
Shooting – Player Component Update
Now we are checking for collisions we need the player to be able to produce bullets or we won’t have anything to make the collisions. The first thing we will do is to add a shootDelay and timeSinceLastShot to our player component. These values will be used to limit the amount of shots a player is able to shoot per second. So let’s update our player component:
1 2 3 4 5 6 7 8 |
public class PlayerComponent implements Component{ public OrthographicCamera cam = null; public boolean onPlatform = false; public boolean onSpring = false; public boolean isDead = false; public float shootDelay = 0.5f; public float timeSinceLastShot = 0f; } |
Shooting – Player Control System Update
In our game, we will allow the player to shoot with the mouse button. This will allow us to see where the player wants to shoot towards by taking the mouse position when the mouse button is pressed. Luckily the mouse position and mouse click action is already taken care of in our controller. All wee need to do is check for them in our player control system. So let’s update our playerControlSystem to include this code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
if(controller.isMouse1Down){ // if mouse button is pressed // user wants to fire if(player.timeSinceLastShot <=0){ // check the player hasn't just shot //player can shoot so do player shoot Vector3 mousePos = new Vector3(controller.mouseLocation.x,controller.mouseLocation.y,0); // get mouse position player.cam.unproject(mousePos); // convert position from screen to box2d world position float speed = 10f; // set the speed of the bullet float shooterX = b2body.body.getPosition().x; // get player location float shooterY = b2body.body.getPosition().y; // get player location float velx = mousePos.x - shooterX; // get distance from shooter to target on x plain float vely = mousePos.y - shooterY; // get distance from shooter to target on y plain float length = (float) Math.sqrt(velx * velx + vely * vely); // get distance to target direct if (length != 0) { velx = velx / length; // get required x velocity to aim at target vely = vely / length; // get required y velocity to aim at target } // create a bullet lvlFactory.createBullet(shooterX, shooterY, velx*speed, vely*speed); //reset timeSinceLastShot player.timeSinceLastShot = player.shootDelay; } } |
Shooting – Create Bullets Method
The player can now shoot, but, we don’t have the method createBullet yet to create bullets… ooops! Let’s fix that by adding the method to create bullets:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
public Entity createBullet(float x, float y, float xVel, float yVel){ Entity entity = engine.createEntity(); B2dBodyComponent b2dbody = engine.createComponent(B2dBodyComponent.class); TransformComponent position = engine.createComponent(TransformComponent.class); TextureComponent texture = engine.createComponent(TextureComponent.class); TypeComponent type = engine.createComponent(TypeComponent.class); CollisionComponent colComp = engine.createComponent(CollisionComponent.class); BulletComponent bul = engine.createComponent(BulletComponent.class); b2dbody.body = bodyFactory.makeCirclePolyBody(x,y,0.5f, BodyFactory.STONE, BodyType.DynamicBody,true); b2dbody.body.setBullet(true); // increase physics computation to limit body travelling through other objects bodyFactory.makeAllFixturesSensors(b2dbody.body); // make bullets sensors so they don't move player position.position.set(x,y,0); texture.region = bulletTex; type.type = TypeComponent.BULLET; b2dbody.body.setUserData(entity); bul.xVel = xVel; bul.yVel = yVel; entity.add(bul); entity.add(colComp); entity.add(b2dbody); entity.add(position); entity.add(texture); entity.add(type); engine.addEntity(entity); return entity; } |
Most of this you have seen before. The extra items are b2dbody.body.setBullet(true); which changes the body to a bullet type and bodyFactory.makeAllFixturesSensors(b2dbody.body); which changes it to a sensor. By changing the body type to a bullet, we’re telling box2d that this is a small fast moving object and it should be processed as such. This will stop the object from travelling through objects which can happen when not set as a bullet. We changed the body to a sensor too, so that it won’t push the player when it spawns inside the player and it will still respond to contacts with other bodies.
Shooting – Bullets System
Great, we have bullets and the ability to shoot them, but they won’t move unless we tell them to and we are going to tell them too with a new system called the bulletSystem. This system will control the bullets by setting their velocity and it will also update them when they die so they can be removed later.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
package blog.gamedevelopment.box2dtutorial.entity.systems; import blog.gamedevelopment.box2dtutorial.entity.components.B2dBodyComponent; import blog.gamedevelopment.box2dtutorial.entity.components.BulletComponent; import blog.gamedevelopment.box2dtutorial.entity.components.Mapper; import com.badlogic.ashley.core.Entity; import com.badlogic.ashley.core.Family; import com.badlogic.ashley.systems.IteratingSystem; public class BulletSystem extends IteratingSystem{ @SuppressWarnings("unchecked") public BulletSystem(){ super(Family.all(BulletComponent.class).get()); } @Override protected void processEntity(Entity entity, float deltaTime) { //get box 2d body and bullet components B2dBodyComponent b2body = Mapper.b2dCom.get(entity); BulletComponent bullet = Mapper.bulletCom.get(entity); // apply bullet velocity to bullet body b2body.body.setLinearVelocity(bullet.xVel, bullet.yVel); //check if bullet is dead if(bullet.isDead){ b2body.isDead = true; } } } |
All this system does is cycles through all the bullets and sets their velocity and updates the body to isDead when it dies.
Shooting – Main Class Update
For this system to work we need to add it to our engine in the MainClass. Remember the systems are called in the order they are added to the engine so add them in this order or you may end up with unexpected results:
1 2 3 4 5 6 7 8 9 10 11 12 |
engine.addSystem(new AnimationSystem()); engine.addSystem(new PhysicsSystem(lvlFactory.world, engine)); engine.addSystem(renderingSystem); engine.addSystem(new PhysicsDebugSystem(lvlFactory.world, renderingSystem.getCamera())); engine.addSystem(new CollisionSystem()); engine.addSystem(new PlayerControlSystem(controller,lvlFactory)); engine.addSystem(new EnemySystem()); player = lvlFactory.createPlayer(atlas.findRegion("player"),cam); engine.addSystem(new WallSystem(player)); engine.addSystem(new WaterFloorSystem(player)); engine.addSystem(new BulletSystem()); engine.addSystem(new LevelGenerationSystem(lvlFactory)); |
Shooting – Body Component Update
In some of the previous code snippets, you may have noticed this b2body.isDead = true; but our bodyComponent doesn’t have this property. That’s because you’re going to add it now.
1 2 3 4 5 6 7 8 9 10 |
package blog.gamedevelopment.box2dtutorial.entity.components; import com.badlogic.ashley.core.Component; import com.badlogic.gdx.physics.box2d.Body; public class B2dBodyComponent implements Component{ public Body body; public boolean isDead = false; } |
This has been added so we can check if a body is dead and needs to be removed from our world.
Shooting – Physics System Update
We will also want to remove the entity from our engine since its dead and no longer has a body. To do that we will update our physicsSystem’s update method.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
@Override public void update(float deltaTime) { super.update(deltaTime); float frameTime = Math.min(deltaTime, 0.25f); accumulator += frameTime; if(accumulator >= MAX_STEP_TIME) { world.step(MAX_STEP_TIME, 6, 2); accumulator -= MAX_STEP_TIME; //Entity Queue for (Entity entity : bodiesQueue) { TransformComponent tfm = tm.get(entity); B2dBodyComponent bodyComp = bm.get(entity); Vector2 position = bodyComp.body.getPosition(); tfm.position.x = position.x; tfm.position.y = position.y; tfm.rotation = bodyComp.body.getAngle() * MathUtils.radiansToDegrees; if(bodyComp.isDead){ System.out.println("Removing a body and entity"); world.destroyBody(bodyComp.body); engine.removeEntity(entity); } } } bodiesQueue.clear(); } |
Shooting – Testing
If you run the game now, you will be able to shot a single enemy and that enemy will die. If you, however, try to shoot again you will find that no Bullet is fired and this is due to how we have been making components. We are currently using a PooledEngine (Want to know more about pooling? Check out this LibGDX Object Pooling guide) which keeps objects we create after they are removed so they can be used again to save time recreating them. This is good as it can speed up your game and stops your memory becoming too fragmented. A problem with this is if we don’t reset our components for the re-used entity the data stored in it will still be present. In our case, we have set the bullet to dead. So every time we reuse a dead bullet it’s still dead and gets removed instantly. To stop this from happening we have to add the poolable interface to our components and override the reset method to reset all our variable in our components.
We will start by updating the BulletComponent to use the poolable interface:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package blog.gamedevelopment.box2dtutorial.entity.components; import com.badlogic.ashley.core.Component; import com.badlogic.gdx.utils.Pool.Poolable; public class BulletComponent implements Component, Poolable{ public float xVel = 0; public float yVel = 0; public boolean isDead = false; @Override public void reset() { xVel = 0; yVel = 0; isDead = false; } } |
As you can see we have imported the poolable interface and then overridden the reset method, resetting all the values to the original values defined above. We need to repeat this for all our components. Let’s do the CollisionComponent next:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
package blog.gamedevelopment.box2dtutorial.entity.components; import com.badlogic.ashley.core.Component; import com.badlogic.ashley.core.Entity; import com.badlogic.gdx.utils.Pool.Poolable; /* * Stores collision data such as entity that this entity has collided with */ public class CollisionComponent implements Component, Poolable { public Entity collisionEntity; @Override public void reset() { collisionEntity = null; } } |
Again we implement the poolable interface and then rest the collisionEntity. Since we didn’t define a value for the collisionEntity we are resetting it to null as it would have been if the component was brand new. Do the same for all your other components that have variables in. Some components like the WaterFloorComponent don’t have variables in and don’t need the poolable interface. Some components have final variables in and these final variables don’t need reset.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
package blog.gamedevelopment.box2dtutorial.entity.components; import com.badlogic.ashley.core.Component; import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.math.Vector3; import com.badlogic.gdx.utils.Pool.Poolable; public class TransformComponent implements Component, Poolable { public final Vector3 position = new Vector3(); public final Vector2 scale = new Vector2(1.0f, 1.0f); public float rotation = 0.0f; public boolean isHidden = false; @Override public void reset() { rotation = 0.0f; isHidden = false; } } |
Shooting – Final Task
Our final task in the part of the tutorial is to remove bullets that travel too far from the player. This is a performance-related change to help reduce the amount of objects in the game. If we don’t remove these objects then they will be forever floating about even when not on the screen or ever going to hit an enemy. To do this we’re going to check the distance the bullet is from the player and if it’s over a certain amount we will set the bullet to dead and it will be automatically removed.
Update your bulletSystem to:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
package blog.gamedevelopment.box2dtutorial.entity.systems; import blog.gamedevelopment.box2dtutorial.entity.components.B2dBodyComponent; import blog.gamedevelopment.box2dtutorial.entity.components.BulletComponent; import blog.gamedevelopment.box2dtutorial.entity.components.Mapper; import com.badlogic.ashley.core.Entity; import com.badlogic.ashley.core.Family; import com.badlogic.ashley.systems.IteratingSystem; public class BulletSystem extends IteratingSystem{ private Entity player; @SuppressWarnings("unchecked") public BulletSystem(Entity player){ super(Family.all(BulletComponent.class).get()); this.player = player; } @Override protected void processEntity(Entity entity, float deltaTime) { //get box 2d body and bullet components B2dBodyComponent b2body = Mapper.b2dCom.get(entity); BulletComponent bullet = Mapper.bulletCom.get(entity); // apply bullet velocity to bullet body b2body.body.setLinearVelocity(bullet.xVel, bullet.yVel); // get player pos B2dBodyComponent playerBodyComp = Mapper.b2dCom.get(player); float px = playerBodyComp.body.getPosition().x; float py = playerBodyComp.body.getPosition().y; //get bullet pos float bx = b2body.body.getPosition().x; float by = b2body.body.getPosition().y; // if bullet is 20 units away from player on any axis then it is probably off screen if(bx - px > 20 || by - py > 20){ bullet.isDead = true; } //check if bullet is dead if(bullet.isDead){ System.out.println("Bullet died"); b2body.isDead = true; } } } |
Notice we added the player to the constructor so this will have to be added in the main class. What we have done is checked if the bullet is more than 20 units away from the player on any axis. If the bullet is that far then it is probably off the screen and can be deleted. We have also added a System.out line so we can see in our console if a bullet was deleted.
If you test your program now you should be able to shoot the enemies and they will die. You will also be able to shoot off the screen and then after the bullets have gotten far enough away they will die and add a message to the console.
As usual here is a link to download the completed tutorial project from stormyvids.
In the next part( part 15 ), we will be updating our code to make some improvements to the level generation we will also add some stuff to the end screen so that when the player dies they are presented by the end screen and then back to the main menu ready for the next attempt to beat their high score.
This video shows some of the updates we will be making:
[embedyt] https://www.youtube.com/watch?v=Zo63u1Gql-k[/embedyt]
AS usual, if you notice any mistakes or have some suggestions. Please feel free to leave a comment below.
← Ashley Enemy System | — Contents — | General Improvements → |
Hey is the series still ongoing?
Yes, I add new entries when I have the spare time to write them. I aim for at least 1 per month.
Great tutorial, not really suitable for total beginners, but a lot can be learned from it anyway. It would be cool to see more of it. Keep it up!
Thank you, I will.
I would like to make it as easy as possible for beginners in this tutorial, so any suggestions on how to improve this series to make it easier for beginners is welcome.
The camera isn’t following my player character. I can’t find anywhere in the code where it updates the camera position to amtch with how high the player has jumped, like in your video example. Where should this code be?
Nevermind, figured it out
Where did you find it? I’ve got the same problem and I’ve looked everywhere.
The bullet isn’t being shot directly at my mouse. I think it’s because the angles aren’t correct, but i can’t seem to figure out what is wrong.
I have tried copy-pasting your code directly, but it did not work either so i don’t think is a typo.
Google couldn’t help me, do you perhaps have an idea of what could be wrong?
Thanks in advance.
I love this tutorial
Please contact me.
I need help with a project.