Full LibGDX Game Tutorial – Enemy System
Welcome to part 13 of our Full LibGDX Game Tutorial. This part will focus on adding an enemy. We will add a mechanic to add patrolling enemies to some of our platforms. 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 12, you can continue on.
In the last part, we created some mechanics to allow us to kill our player if the get hit by the ever increasing water zone. This time we will be adding enemies to our world on random platforms.
Creating our components for our enemy system.
The EnemyComponent simply needs to hold a few items relating to our enemy. We will hold the original position of the enemy so we can make them move around this point. We will also add a direction flag so we can tell which direction they’re going and flip them if they get too far and finally, we will add an isDead flag so later we can add the ability for our player to kill the enemies.
So let’s make our EnemyComponent:
1 2 3 4 5 6 7 8 9 |
package blog.gamedevelopment.box2dtutorial.entity.components; import com.badlogic.ashley.core.Component; public class EnemyComponent implements Component{ public boolean isDead = false; public float xPosCenter = -1; public boolean isGoingLeft = false; } |
With the component made we can now add a system to control the enemies. The system will be pretty much the same as other systems we have in place with some bespoke code for our enemies in the processEntity 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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
package blog.gamedevelopment.box2dtutorial.entity.systems; import blog.gamedevelopment.box2dtutorial.entity.components.B2dBodyComponent; import blog.gamedevelopment.box2dtutorial.entity.components.EnemyComponent; 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 EnemySystem extends IteratingSystem{ private ComponentMapper<EnemyComponent> em; private ComponentMapper<B2dBodyComponent> bodm; @SuppressWarnings("unchecked") public EnemySystem(){ super(Family.all(EnemyComponent.class).get()); em = ComponentMapper.getFor(EnemyComponent.class); bodm = ComponentMapper.getFor(B2dBodyComponent.class); } @Override protected void processEntity(Entity entity, float deltaTime) { EnemyComponent enemyCom = em.get(entity); // get EnemyComponent B2dBodyComponent bodyCom = bodm.get(entity); // get B2dBodyComponent // get distance of enemy from its original start position (pad center) float distFromOrig = Math.abs(enemyCom.xPosCenter - bodyCom.body.getPosition().x); // if distance > 1 swap direction enemyCom.isGoingLeft = (distFromOrig > 1)? !enemyCom.isGoingLeft:enemyCom.isGoingLeft; // set speed base on direction float speed = enemyCom.isGoingLeft?-0.01f:0.01f; // apply speed to body bodyCom.body.setTransform(bodyCom.body.getPosition().x + speed, bodyCom.body.getPosition().y, bodyCom.body.getAngle()); } } |
In the code above you can see we have made it so enemies will move left and right until they get 1 unit away from their original position then flip direction.
Adding Enemy units to our level.
Now we have a system for controlling enemies we need to add enemies to our world. In order to do that we will add a method to our levelFactory class call createEnemy:
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 |
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); 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(b2dbody); entity.add(position); entity.add(texture); entity.add(enemy); entity.add(type); engine.addEntity(entity); return entity; } |
This again is pretty standard and similar to the other methods we already have in our levelFactory. The next thing we will do is update our generateLevel method to add enemies randomly based on our simplexNoise values:
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 |
public void generateLevel(int ylevel){ while(ylevel > currentLevel){ // get noise sim.getNoise(xpos,ypos,zpos) 3D noise float noise1 = (float)sim.getNoise(1, currentLevel, 0); // platform 1 should exist? float noise2 = (float)sim.getNoise(1, currentLevel, 100); // if plat 1 exists where on x axis float noise3 = (float)sim.getNoise(1, currentLevel, 200); // platform 2 exists? float noise4 = (float)sim.getNoise(1, currentLevel, 300); // if 2 exists where on x axis ? float noise5 = (float)simRough.getNoise(1, currentLevel ,1400); // should spring exist on p1? float noise6 = (float)simRough.getNoise(1, currentLevel ,2500); // should spring exists on p2? float noise7 = (float)simRough.getNoise(1, currentLevel, 2700); // should enemy exist? float noise8 = (float)simRough.getNoise(1, currentLevel, 3000); // platform 1 or 2? if(noise1 > 0.2f){ createPlatform(noise2 * 25 +2 ,currentLevel * 2); if(noise5 > 0.5f){ // add bouncy platform createBouncyPlatform(noise2 * 25 +2,currentLevel * 2); } if(noise7 > 0.5f){ // add an enemy createEnemy(enemyTex,noise2 * 25 +2,currentLevel * 2 + 1); } } if(noise3 > 0.2f){ createPlatform(noise4 * 25 +2, currentLevel * 2); if(noise6 > 0.4f){ // add bouncy platform createBouncyPlatform(noise4 * 25 +2,currentLevel * 2); } if(noise8 > 0.5f){ // add an enemy createEnemy(enemyTex,noise4 * 25 +2,currentLevel * 2 + 1); } } currentLevel++; } } |
Here you can see we make use of the noise7 and noise8 we added in the last part to decide whether to add an enemy on the pad or not. You will also notice we are now using a second noise called simRough. This is a second simplex noise with a higher roughness level. This allows us to have greater control of our level generation as we can now adjust the platforms and enemies separately instead of all using the same noise.
Previously we were using the same texture for a lot of our objects. Since we added the DFUtils in the last part we can now update our level factory to use the utilities in that class. So let’s update out LevelFactory constructor to use the DFUtils and create a second simplex noise.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public LevelFactory(PooledEngine en, TextureAtlas atlas){ engine = en; this.atlas = atlas; floorTex = DFUtils.makeTextureRegion(40*RenderingSystem.PPM, 0.5f*RenderingSystem.PPM, "111111FF"); enemyTex = DFUtils.makeTextureRegion(1*RenderingSystem.PPM,1*RenderingSystem.PPM, "331111FF"); platformTex = DFUtils.makeTextureRegion(2*RenderingSystem.PPM, 0.1f*RenderingSystem.PPM, "221122FF"); world = new World(new Vector2(0,-10f), true); world.setContactListener(new B2dContactListener()); bodyFactory = BodyFactory.getInstance(world); // create a new SimplexNoise (size,roughness,seed) sim = new SimplexNoise(512, 0.80f, 1); simRough = new SimplexNoise(512, 0.95f, 1); } |
After adding this code to your project you may notice that the makeTextureRegion method is showing an error. This is expected as an extra method has been added to the DFUtils to help keep the code readable. The new method is:
1 2 3 4 5 |
public static TextureRegion makeTextureRegion(float f, float g, String hex) { int fval = (int)f; int gval = (int)g; return makeTextureRegion(fval,gval,hex); } |
This method simply wraps the other makeTextureRegion with ints as parameters to using floats, which it then converts to ints. This saves us converting it in our levelFactory and makes it easier to read.
Finally to add our new enemy system to the game we update our MainScreen class and add the new system to the engine like this:
1 2 3 4 5 6 7 8 9 10 11 |
engine.addSystem(new AnimationSystem()); engine.addSystem(new PhysicsSystem(lvlFactory.world)); engine.addSystem(renderingSystem); engine.addSystem(new PhysicsDebugSystem(lvlFactory.world, renderingSystem.getCamera())); engine.addSystem(new CollisionSystem()); engine.addSystem(new PlayerControlSystem(controller)); engine.addSystem(new EnemySystem()); player = lvlFactory.createPlayer(atlas.findRegion("player"),cam); engine.addSystem(new WallSystem(player)); engine.addSystem(new WaterFloorSystem(player)); engine.addSystem(new LevelGenerationSystem(lvlFactory)); |
In part 14 we will be adding the ability to shoot. Adding functions to check if the player has pressed the mouse button, checking if the player is allowed to shoot, generating a bullet, killing enemies and removing dead objects from our game.
If you see any mistakes in this article or have some suggestions please feel free to leave a comment below.
← Game Mechanics | — Contents — | Shooting → |
Nice tutorials. I have been following it kinly. But i have this issue. from the previous tutorial, you did not show this system: engine.addSystem(new WallSystem(player)); it was suddenly placed with the rest of the code. The implementation was not shown.
Similarly, I discovered adding any new system causes a Null pointer exception and I have tried debugging but couldn’t make a head way. The new additions causing this error are: lvlFactory.createWaterFloor(wFloorRegion);, lvlFactory.createWalls(wallRegion); znc now the new one: engine.addSystem(new EnemySystem()); kindly help out.
Thank you