Full LibGDX Game Tutorial – Ashley & Steering Behaviors
Welcome to part 17 of our Full LibGDX Game Tutorial. This part will focus on linking Ashley, Box2D and Gdx-Ai together to allow entities to use steering behaviors. 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 16, you can continue on.
In this part we will add the Gdx-Ai extension using gradle so it is included in our project. We will then add a few components for a new enemy type that will move towards the player using the Gdx-Ai steering behaviors.
Adding Gdx-Ai to your project
We have to add the Gdx-Ai extension to our project as we never added it when we created our project. We can do that by using gradle. Since we already have the repository “https://oss.sonatype.org/content/repositories/snapshots/” defined in our project all we need to do is add the Gdx-Ai dependency in our core project. I believe we also need it in our android dependency as well. Simply open your build.gradle file in the box2dtut project and add “com.badlogicgames.gdx:gdx-ai:1.8.0” to the file and save.
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 |
project(":android") { apply plugin: "android" configurations { natives } dependencies { compile project(":core") compile "com.badlogicgames.gdx:gdx-backend-android:$gdxVersion" natives "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-armeabi" natives "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-armeabi-v7a" natives "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-arm64-v8a" natives "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-x86" natives "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-x86_64" compile "com.badlogicgames.gdx:gdx-box2d:$gdxVersion" natives "com.badlogicgames.gdx:gdx-box2d-platform:$gdxVersion:natives-armeabi" natives "com.badlogicgames.gdx:gdx-box2d-platform:$gdxVersion:natives-armeabi-v7a" natives "com.badlogicgames.gdx:gdx-box2d-platform:$gdxVersion:natives-arm64-v8a" natives "com.badlogicgames.gdx:gdx-box2d-platform:$gdxVersion:natives-x86" natives "com.badlogicgames.gdx:gdx-box2d-platform:$gdxVersion:natives-x86_64" compile "com.badlogicgames.box2dlights:box2dlights:$box2DLightsVersion" compile "com.badlogicgames.ashley:ashley:1.7.0" compile "com.badlogicgames.gdx:gdx-ai:1.8.0" } } project(":core") { apply plugin: "java" dependencies { compile "com.badlogicgames.gdx:gdx:$gdxVersion" compile "com.badlogicgames.gdx:gdx-box2d:$gdxVersion" compile "com.badlogicgames.box2dlights:box2dlights:$box2DLightsVersion" compile "com.badlogicgames.ashley:ashley:1.7.0" compile "com.badlogicgames.gdx:gdx-ai:1.8.0"; } } |
We then have to refresh our dependencies. We can do that in eclipse by right clicking on each project and going to Gradle(STS) > Refresh All. This will refresh the project and you will be prompted with a build message once completed and should look something similar to this:
1 2 3 4 5 6 7 |
BUILD SUCCESSFUL Total time: 0.324 secs [sts] ----------------------------------------------------- [sts] Build finished succesfully! [sts] Time taken: 0 min, 0 sec [sts] ----------------------------------------------------- |
Plugging our steering behaviors into Ashley
We can now add a new component for our steering behavior, let’s call it SteeringComponent so it’s job easily understood. The SteeringComponent will contain all the data for steering an entity. It will also have some methods that it gets from the Steerable interface so doesn’t fall into the Ashley ECS system very well as a component should only contain data. The benefits of being able to use prebuilt AI behaviors is more important to me than keeping the ECS system pure. So our component looks like 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 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 |
package blog.gamedevelopment.box2dtutorial.entity.components; import blog.gamedevelopment.box2dtutorial.DFUtils; import com.badlogic.ashley.core.Component; import com.badlogic.gdx.ai.steer.Steerable; import com.badlogic.gdx.ai.steer.SteeringAcceleration; import com.badlogic.gdx.ai.steer.SteeringBehavior; import com.badlogic.gdx.ai.utils.Location; import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.physics.box2d.Body; import com.badlogic.gdx.utils.Pool.Poolable; public class SteeringComponent implements Steerable<Vector2>, Component, Poolable{ public static enum SteeringState {WANDER,SEEK,FLEE,ARRIVE,NONE} // a list of possible behaviours public SteeringState currentMode = SteeringState.WANDER; // stores which state the entity is currently in public Body body; // stores a reference to our Box2D body // Steering data float maxLinearSpeed = 2f; // stores the max speed the entity can go float maxLinearAcceleration = 5f; // stores the max acceleration float maxAngularSpeed =50f; // the max turning speed float maxAngularAcceleration = 5f;// the max turning acceleration float zeroThreshold = 0.1f; // how accurate should checks be (0.0000001f will mean the entity must get within 0.0000001f of // target location. This will cause problems as our entities travel pretty fast and can easily over or undershoot this.) public SteeringBehavior<Vector2> steeringBehavior; // stors the action behaviour private static final SteeringAcceleration<Vector2> steeringOutput = new SteeringAcceleration<Vector2>(new Vector2()); // this is the actual steering vactor for our unit private float boundingRadius = 1f; // the minimum radius size for a circle required to cover whole object private boolean tagged = true; // This is a generic flag utilized in a variety of ways. (never used this myself) private boolean independentFacing = false; // defines if the entity can move in a direction other than the way it faces) @Override public void reset() { currentMode = SteeringState.NONE; body = null; steeringBehavior = null; } public boolean isIndependentFacing () { return independentFacing; } public void setIndependentFacing (boolean independentFacing) { this.independentFacing = independentFacing; } /** Call this to update the steering behaviour (per frame) * @param delta delta time between frames */ public void update (float delta) { if (steeringBehavior != null) { steeringBehavior.calculateSteering(steeringOutput); applySteering(steeringOutput, delta); } } /** apply steering to the Box2d body * @param steering the steering vector * @param deltaTime teh delta time */ protected void applySteering (SteeringAcceleration<Vector2> steering, float deltaTime) { boolean anyAccelerations = false; // Update position and linear velocity. if (!steeringOutput.linear.isZero()) { // this method internally scales the force by deltaTime body.applyForceToCenter(steeringOutput.linear, true); anyAccelerations = true; } // Update orientation and angular velocity if (isIndependentFacing()) { if (steeringOutput.angular != 0) { // this method internally scales the torque by deltaTime body.applyTorque(steeringOutput.angular, true); anyAccelerations = true; } } else { // If we haven't got any velocity, then we can do nothing. Vector2 linVel = getLinearVelocity(); if (!linVel.isZero(getZeroLinearSpeedThreshold())) { float newOrientation = vectorToAngle(linVel); body.setAngularVelocity((newOrientation - getAngularVelocity()) * deltaTime); // this is superfluous if independentFacing is always true body.setTransform(body.getPosition(), newOrientation); } } if (anyAccelerations) { // Cap the linear speed Vector2 velocity = body.getLinearVelocity(); float currentSpeedSquare = velocity.len2(); float maxLinearSpeed = getMaxLinearSpeed(); if (currentSpeedSquare > (maxLinearSpeed * maxLinearSpeed)) { body.setLinearVelocity(velocity.scl(maxLinearSpeed / (float)Math.sqrt(currentSpeedSquare))); } // Cap the angular speed float maxAngVelocity = getMaxAngularSpeed(); if (body.getAngularVelocity() > maxAngVelocity) { body.setAngularVelocity(maxAngVelocity); } } } @Override public Vector2 getPosition() { return body.getPosition(); } @Override public float getOrientation() { return body.getAngle(); } @Override public void setOrientation(float orientation) { body.setTransform(getPosition(), orientation); } @Override public float vectorToAngle(Vector2 vector) { return DFUtils.vectorToAngle(vector); } @Override public Vector2 angleToVector(Vector2 outVector, float angle) { return DFUtils.angleToVector(outVector, angle); } @Override public Location<Vector2> newLocation() { return new Box2dLocation(); } @Override public float getZeroLinearSpeedThreshold() { return zeroThreshold; } @Override public void setZeroLinearSpeedThreshold(float value) { zeroThreshold = value; } @Override public float getMaxLinearSpeed() { return this.maxLinearSpeed; } @Override public void setMaxLinearSpeed(float maxLinearSpeed) { this.maxLinearSpeed = maxLinearSpeed; } @Override public float getMaxLinearAcceleration() { return this.maxLinearAcceleration; } @Override public void setMaxLinearAcceleration(float maxLinearAcceleration) { this.maxLinearAcceleration = maxLinearAcceleration; } @Override public float getMaxAngularSpeed() { return this.maxAngularSpeed; } @Override public void setMaxAngularSpeed(float maxAngularSpeed) { this.maxAngularSpeed = maxAngularSpeed; } @Override public float getMaxAngularAcceleration() { return this.maxAngularAcceleration; } @Override public void setMaxAngularAcceleration(float maxAngularAcceleration) { this.maxAngularAcceleration = maxAngularAcceleration; } @Override public Vector2 getLinearVelocity() { return body.getLinearVelocity(); } @Override public float getAngularVelocity() { return body.getAngularVelocity(); } @Override public float getBoundingRadius() { return this.boundingRadius; } @Override public boolean isTagged() { return this.tagged; } @Override public void setTagged(boolean tagged) { this.tagged = tagged; } } |
Here we have basically just implemented the steerable interface, added some behavior enums and linked the steering to our Box2D body. The update method is what we need to call in our system to update the steering. Since we added a new component we need to add a new component mapper to our Mapper class. We simply add:
1 |
public static final ComponentMapper<SteeringComponent> sCom = ComponentMapper.getFor(SteeringComponent.class); |
And we’re done so let’s move into our system.
Building our Steering Behavior system
Our steering system is very simple as all it needs to do is update each entities SteeringComponent and update the Gdx-Ai timepiece. The timepiece is essentially a timer solely for the AI which allows pausing AI by not updating the timepiece. So our SteeringSystem looks like 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 |
package blog.gamedevelopment.box2dtutorial.entity.systems; import blog.gamedevelopment.box2dtutorial.entity.components.Mapper; import blog.gamedevelopment.box2dtutorial.entity.components.SteeringComponent; import com.badlogic.ashley.core.Entity; import com.badlogic.ashley.core.Family; import com.badlogic.ashley.systems.IteratingSystem; import com.badlogic.gdx.ai.GdxAI; public class SteeringSystem extends IteratingSystem { @SuppressWarnings("unchecked") public SteeringSystem() { super(Family.all(SteeringComponent.class).get()); } @Override public void update(float deltaTime) { super.update(deltaTime); GdxAI.getTimepiece().update(deltaTime); } @Override protected void processEntity(Entity entity, float deltaTime) { SteeringComponent steer = Mapper.sCom.get(entity); steer.update(deltaTime); } } |
Steering Behaviors
Gdx-Ai comes with a range of different behaviors and each one has its own set of properties that can be changed. Usually, this is done for each different type of enemy e.g. one type of enemy will have a big wander area whereas a different enemy will have a small area. Since we only have 1 enemy that will wander we will reuse the same wander settings again and again. To save us from adding code in different places for the same steering behavior we will add a static class that defines the different behaviors we will need.
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 |
package blog.gamedevelopment.box2dtutorial.ai; import blog.gamedevelopment.box2dtutorial.entity.components.SteeringComponent; import com.badlogic.gdx.ai.steer.behaviors.Arrive; import com.badlogic.gdx.ai.steer.behaviors.Flee; import com.badlogic.gdx.ai.steer.behaviors.Seek; import com.badlogic.gdx.ai.steer.behaviors.Wander; import com.badlogic.gdx.math.MathUtils; import com.badlogic.gdx.math.Vector2; public class SteeringPresets { public static Wander<Vector2> getWander(SteeringComponent scom){ Wander<Vector2> wander = new Wander<Vector2>(scom) .setFaceEnabled(false) // let wander behaviour manage facing .setWanderOffset(20f) // distance away from entity to set target .setWanderOrientation(0f) // the initial orientation .setWanderRadius(10f) // size of target .setWanderRate(MathUtils.PI2 * 2); // higher values = more spinning return wander; } public static Seek<Vector2> getSeek(SteeringComponent seeker, SteeringComponent target){ Seek<Vector2> seek = new Seek<Vector2>(seeker,target); return seek; } public static Flee<Vector2> getFlee(SteeringComponent runner, SteeringComponent fleeingFrom){ Flee<Vector2> seek = new Flee<Vector2>(runner,fleeingFrom); return seek; } public static Arrive<Vector2> getArrive(SteeringComponent runner, SteeringComponent target){ Arrive<Vector2> arrive = new Arrive<Vector2>(runner, target) .setTimeToTarget(0.1f) // default 0.1f .setArrivalTolerance(7f) // .setDecelerationRadius(10f); return arrive; } } |
Here we have added a wander behavior so we can have our enemies wander when there is no player close. We also have a seek and flee behavior that allows us to make an enemy run to or run away from another steering entity. Finally, we have the arrive behavior, which allows us to seek out a target and slow down when we get close.
A new Enemy for our new Steering Behaviors
We need to make a new enemy so we have something to attach our new steering behaviors to. We will create a new enemy that will float around randomly and then when it is close enough, move towards the player and if the player gets too close then the enemy will flee. So let’s make the enemy:
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 |
public Entity createSeeker(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); CollisionComponent colComp = engine.createComponent(CollisionComponent.class); TypeComponent type = engine.createComponent(TypeComponent.class); StateComponent stateCom = engine.createComponent(StateComponent.class); EnemyComponent enemy = engine.createComponent(EnemyComponent.class); SteeringComponent scom = engine.createComponent(SteeringComponent.class); b2dbody.body = bodyFactory.makeCirclePolyBody(x,y,1, BodyFactory.STONE, BodyType.DynamicBody,true); b2dbody.body.setGravityScale(0f); // no gravity for our floating enemy b2dbody.body.setLinearDamping(0.3f); // setting linear dampening so the enemy slows down in our box2d world(or it can float on forever) position.position.set(x,y,0); texture.region = atlas.findRegion("player"); type.type = TypeComponent.ENEMY; stateCom.set(StateComponent.STATE_NORMAL); b2dbody.body.setUserData(entity); bodyFactory.makeAllFixturesSensors(b2dbody.body); // seeker should fly about not fall scom.body = b2dbody.body; //enemy.enemyType = EnemyComponent.Type.CLOUD; // used later in tutorial // set out steering behaviour scom.steeringBehavior = SteeringPresets.getWander(scom); scom.currentMode = SteeringComponent.SteeringState.WANDER; entity.add(b2dbody); entity.add(position); entity.add(texture); entity.add(colComp); entity.add(type); entity.add(enemy); entity.add(stateCom); entity.add(scom); engine.addEntity(entity); return entity; } |
We have our method for creating a new enemy. We need to add it now to our level generation so it is generated as the level is generated and added to our world. While testing this out I noticed the player would sometimes die at the start of the level due to the enemies spawning in the same place or very close to the player. This isn’t good so the level generation has been updated to account for this as well as a few changes to generally improve the level generation. So, still in the levelFactory update the level generation code 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 |
/** Creates a pair of platforms per level up to yLevel * @param ylevel */ public void generateLevel(int ylevel){ while(ylevel > currentLevel){ for(int i = 1; i < 5; i ++){ generateSingleColumn(i); } currentLevel++; } } // generate noise for level private float genNForL(int level, int height){ return (float)openSim.eval(height, level); } private void generateSingleColumn(int i){ int offset = 10 * i; int range = 15; if(genNForL(i,currentLevel) > -0.5f){ createPlatform(genNForL(i * 100,currentLevel) * range + offset ,currentLevel * 2); if(genNForL(i * 200,currentLevel) > 0.3f){ // add bouncy platform createBouncyPlatform(genNForL(i * 100,currentLevel) * range + offset,currentLevel * 2); } // only make enemies above level 7 (stops insta deaths) if(currentLevel > 7){ if(genNForL(i * 300,currentLevel) > 0.2f){ // add an enemy createEnemy(enemyTex,genNForL(i * 100,currentLevel) * range + offset,currentLevel * 2 + 1); } } //only make cloud enemies above level 10 (stops insta deaths) if(currentLevel > 10){ if(genNForL(i * 400,currentLevel) > 0.3f){ // add a cloud enemy createSeeker(genNForL(i * 100,currentLevel) * range + offset,currentLevel * 2 + 1); } } } } |
I believe you can now test out a level and there will be enemies floating about for you to avoid. They will only wander about at the moment because we haven’t updated our enemy system to change their behaviors based on the distance to the player.
Switching Steering Behaviors based on player proximity
Our enemySystem should now be updated to include the code to change the behavior of our cloud enemies only. To do that we need to have to update our enemyComponent to:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class EnemyComponent implements Component, Poolable{ public static enum Type { DROPLET, CLOUD }; public boolean isDead = false; public float xPosCenter = -1; public boolean isGoingLeft = false; public float shootDelay = 1.5f; public float timeSinceLastShot = 0f; public Type enemyType = Type.DROPLET; @Override public void reset() { shootDelay = 0.5f; timeSinceLastShot = 0f; enemyType = Type.DROPLET; isDead = false; xPosCenter = -1; isGoingLeft = false; } } |
This will allow us to check which enemy type we are processing and allow us to apply steering behaviors to only the cloud enemies. It will also allow us later to enable our enemies to shoot. We now have our enemyComponent updated we need to update our enemySystem 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 |
@Override protected void processEntity(Entity entity, float deltaTime) { EnemyComponent enemyCom = em.get(entity); // get EnemyComponent B2dBodyComponent bodyCom = bodm.get(entity); // get B2dBodyComponent if(enemyCom.enemyType == Type.DROPLET){ // 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()); }else if(enemyCom.enemyType == Type.CLOUD){ B2dBodyComponent b2Player = Mapper.b2dCom.get(player); B2dBodyComponent b2Enemy = Mapper.b2dCom.get(entity); float distance = b2Player.body.getPosition().dst(b2Enemy.body.getPosition()); //System.out.println(distance); SteeringComponent scom = Mapper.sCom.get(entity); if(distance < 3 && scom.currentMode != SteeringComponent.SteeringState.FLEE){ scom.steeringBehavior = SteeringPresets.getFlee(Mapper.sCom.get(entity),Mapper.sCom.get(player)); scom.currentMode = SteeringComponent.SteeringState.FLEE; }else if(distance > 3 && distance < 10 && scom.currentMode != SteeringComponent.SteeringState.ARRIVE){ scom.steeringBehavior = SteeringPresets.getArrive(Mapper.sCom.get(entity),Mapper.sCom.get(player)); scom.currentMode = SteeringComponent.SteeringState.ARRIVE; }else if(distance > 15 && scom.currentMode != SteeringComponent.SteeringState.WANDER){ scom.steeringBehavior = SteeringPresets.getWander(Mapper.sCom.get(entity)); scom.currentMode = SteeringComponent.SteeringState.WANDER; } } // check for dead enemies if(enemyCom.isDead){ bodyCom.isDead =true; } } |
Here we have added some checks to check the distance of the player from the current entity. We have a set of if statements which we use to change the behavior of the enemy for each distance. We also have a check in those if statements to check that the state isn’t already in that state as we don’t want to overwrite a wander state with a wander state or the enemy would only ever do the first step of the wander state and would just move in a single direction.
Allowing Our Enemies to shoot
We already added some variables for the enemy component to allows us to shoot, Now all we need is a way for them to shoot. To allow them to do this we will update our enemySystem again:
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 |
@Override protected void processEntity(Entity entity, float deltaTime) { EnemyComponent enemyCom = em.get(entity); // get EnemyComponent B2dBodyComponent bodyCom = bodm.get(entity); // get B2dBodyComponent if(enemyCom.enemyType == Type.DROPLET){ //same code, no changes }else if(enemyCom.enemyType == Type.CLOUD){ //same code, no changes // should enemy shoot if(scom.currentMode == SteeringComponent.SteeringState.ARRIVE){ // enemy is following if(enemyCom.timeSinceLastShot >= enemyCom.shootDelay){ //do shoot Vector2 aim = DFUtils.aimTo(bodyCom.body.getPosition(), b2Player.body.getPosition()); aim.scl(10); levelFactory.createBullet(bodyCom.body.getPosition().x, bodyCom.body.getPosition().y, aim.x, aim.y, BulletComponent.Owner.ENEMY); //reset timer enemyCom.timeSinceLastShot = 0; } } } // do shoot timer enemyCom.timeSinceLastShot += deltaTime; // check for dead enemies if(enemyCom.isDead){ bodyCom.isDead =true; } } |
Here we check that the enemy is in the arrive state (a state where we know the enemy has the player in their sights) Then we check if the timer has elapsed enough time to allow the enemy to shoot. Similar to the player shooting code. Next, we use a new DFUtil called aimTo. I added this as its a common set of code in many games and the code is:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public static Vector2 aimTo(Vector2 shooter, Vector2 target){ Vector2 aim = new Vector2(); float velx = target.x - shooter.x; // get distance from shooter to target on x plain float vely = target.y - shooter.y; // 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) { aim.x = velx / length; // get required x velocity to aim at target aim.y = vely / length; // get required y velocity to aim at target } return aim; } /** Takes Vector 3 as argument here for mouse location(unproject etc) * @param shooter Vector 2 for shooter position * @param target Vector 3 for target location * @return */ public static Vector2 aimTo(Vector2 shooter, Vector3 target){ return aimTo(shooter, new Vector2(target.x,target.y)); } |
Now when the enemy shoots, it kills itself which, let’s face it, isn’t very good. We need a way for the bullets to know which entity type they’re from so that we can ignore collisions from its own side. We need to update our bullet component to this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public class BulletComponent implements Component, Poolable{ public static enum Owner { ENEMY,PLAYER,SCENERY,NONE } public Entity particleEffect; public float xVel = 0; public float yVel = 0; public boolean isDead = false; public Owner owner = Owner.NONE; @Override public void reset() { owner = Owner.NONE; xVel = 0; yVel = 0; isDead = false; particleEffect = null; } } |
We have added an enum that will identify what type of entity shot this bullet. Now we need to update the collisionSystem so it can change what happens depending on the type of bullet:
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 |
@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){ PlayerComponent pl = pm.get(entity); 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"); 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: // TODO add mask so player can't hit themselves BulletComponent bullet = Mapper.bulletCom.get(collidedEntity); if(bullet.owner != BulletComponent.Owner.PLAYER){ // can't shoot own team pl.isDead = true; } 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("Player: collidedEntity.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); BulletComponent bullet = Mapper.bulletCom.get(collidedEntity); if(bullet.owner != BulletComponent.Owner.ENEMY){ // can't shoot own team bullet.isDead = true; enemy.isDead = true; System.out.println("enemy got shot"); } break; default: System.out.println("No matching type found"); } cc.collisionEntity = null; // collision handled reset component }else{ System.out.println("Enemy: collidedEntity.type == null"); } } }else{ cc.collisionEntity = null; } } |
What we have done is made a check to see who fired the bullet. if it’s from a friendly team member or from themselves it is ignored. If it isn’t then it sets the entity to dead as usual.
[embedyt] https://www.youtube.com/watch?v=iaFIVhAMMvs[/embedyt]
In this part we have added 2 steering behaviors; wander and seek. We also have a new enemy which can shoot at the user. From this part onwards I will be adding the code to github with a branch for each part. This part is available from https://github.com/dfour/box2dtut/tree/part17 .
← Particle Effects | — Contents — | Coming Soon → |
Very good game development covering all aspects of creating a game using Box2D in libGDX for people new to programming. Worth reading who wants to make their first game.
Learn Level Design with Blender and Unity 3D
Hello!
You are doing a magnificent job!
It really helped me understand many core concepts.
Could you please create a tutorial on Behavior Trees?
I looked all over the net there aren’t any step by step tutorials.
I have been looking at the tests created in the official documentation but its hard to piece together.
Thanks!
hola
colo me salen las texturas de las llamas
gracias
Years later, but thanks for all the content. i found this all very helpful.