Making a LibGDX Game – Part 14 – Firing Bullets #gamedev

To those who still check back for or are tracking the Github repo thank you for returning and checking out the tutorial! Thanks to @UmzGames for prompting me to add more! I would like to explore different bullet types and movement patterns but lets start with something simple.

pewpew

Check out the commit: Git commit for this tutorial

To fire our weapon we will use the spacebar, the control class requires a new boolean variable which we shall call “spacePressed”.

core/src/uk/co/carelesslabs/Control.java

  public boolean spacePressed;

The keydown function case statement will set the new variable to true:
Github code link

public boolean keyDown(int keyCode) {
...
...
    case Keys.SPACE:
        spacePressed = true;
        break;

The Enum class will need a new EntityType, we wont use this right now but it is usefull setting an entity type for checks later on or debuging.

core/src/uk/co/carelesslabs/Enums.java

public enum EntityType {
     HERO,
     TREE,
     BIRD,
     BULLET
 }

GameClass Updates
There are some changes to be made to the class that has the main game loop. When the hero update method is called we will now pass in our box2D instance, this will allow us access to the functions that allow new sensors to be created when a new bullet is added. Previously the call was hero.update(control).

Also a new method that checks and removes bullets is called, this will check which bullets are no longer active and should be removed from the game.

core/src/uk/co/carelesslabs/GameClass.java

  hero.update(control, box2D);

  // Clear travelled bullets
  hero.clearAmmo(box2D);

There was already a gun added to the Hero class, but now we will want to set an amount of ammo that the gun has. The Gun class should be refactored to allow the amount of ammo it has to be passed when its initiated or it could have a default number. Also
this could read “gun.addAmmo(10)”, not sure why I look it up in the ArrayList when its available after the gun variable is initiated. Its always good to read through your code and explain / document especially as a solo dev to find these issues, working in a team you have the luxury of code review with Pull Requests.

core/src/uk/co/carelesslabs/entity/Hero.java

public Hero(Vector3 pos, Box2DWorld box2d){
  ...

    // Weapon
    Gun gun = new Gun(1, -1, 7);
    weapons = new ArrayList();
    weapons.add(gun);

    // Add some ammo to the gun
    weapons.get(0).addAmmo(10);

On drawing the gun we can also call the tick method, we only draw and tick weapons that are active, a later tutorial will look at multiple weapons and weapon switching.

@Override
    public void draw(SpriteBatch batch){
        if(shadow != null) batch.draw(shadow, pos.x, pos.y, width, height);
        if(texture != null) batch.draw(texture, pos.x, pos.y, width, height);
        for(Gun g : weapons){
			if(g.active){
				g.tick(Gdx.graphics.getDeltaTime()); // New line
				g.drawRotated(batch);
			}
		}
    }

The update function of the Hero as mentioned previously now requires an instance of Box2DWorld to be passed, when looping the weapons and checking which are active an additional check will add a new bullet to the guns active bullet list if space was pressed, spacePressed is reset to false.

When space is pressed and the gun is active we generate a new SimpleBullet, the current gun and box2D is passed into this class, the instance of the bullet just created is then added to the guns active or fired ammo list.

* The guns ammo count should have been checked before generating a new SimpleBullet: if (control.spacePressed && g.ammoCount > 0We are generating a new bullet and not actually using it with the current version of the code.

Another new method “clearAmmo” will loop through all guns and call a method for that class “clearTravelledAmmo”; this is used to remove bullets.

view on Github

public void update(Control control, Box2DWorld box2D) {
...
    // Update weapons
        for(Gun g : weapons){
            if(g.active){
                g.updatePos(pos.x, pos.y);
            	g.angle = control.angle - 90;

            	if(control.spacePressed){
                    SimpleBullet bullet = new SimpleBullet(g, box2D);
            	    g.addActiveAmmo(bullet);
            	    control.spacePressed = false;
            	 }
            }
        }

    // New Method to loop all weapons and check its ammo.
    public void clearAmmo(Box2DWorld box2D) {
        for(Gun g : weapons){
            g.clearTravelledAmmo(box2D);
        }
    }

The new ammo class extends Entity and introduces some new variables:

  • range – how far the bullet can travel
  • damage – the damage this bullet inflicts
  • vector – the heading of the bullet, used to move it each frame
  • distMoved – the ditance the bullet has moved thus far

core/src/uk/co/carelesslabs/entity/ammo/Ammo.java

package uk.co.carelesslabs.entity.ammo;

import com.badlogic.gdx.math.Vector2;
import uk.co.carelesslabs.entity.Entity;

public class Ammo extends Entity {
	public float range;
	public float damage;
	public Vector2 vector;
	public float distMoved;

	public Ammo(){
		super();
		vector = new Vector2();
	}

	public void tick(float delta){

	}

}

SimpleBullet
The ammo class will be used to extend other bullet classes, starting with the most basic SimpleBullet. This bullet will travel along a vector for a given distance at a given speed. For this tutorial an existing image will be used to show the bullet place on the screen,
in another tutorial we can look at adding some graphics and rendering the bullet at the correct angle etc.

core/src/uk/co/carelesslabs/entity/ammo/SimpleBullet.java

package uk.co.carelesslabs.entity.ammo;

import uk.co.carelesslabs.Media;
import uk.co.carelesslabs.Enums.EntityType;
import uk.co.carelesslabs.box2d.Box2DHelper;
import uk.co.carelesslabs.box2d.Box2DWorld;
import uk.co.carelesslabs.weapons.Gun;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.physics.box2d.BodyDef;

public class SimpleBullet extends Ammo {
    Gun gun;

    // A new bullet has some defaults set
    public SimpleBullet(Gun gun, Box2DWorld box2d) {
        super();
        this.gun = gun;
        type = EntityType.BULLET;
        texture = Media.close_menu;
        range = 50;
        damage = 1;
        width = 8;
        height = 8;
        speed = 60;
        active = true;
        setupBullet(box2d);
    }

    public void tick(float delta) {
        // only process if active
        if (active) {
            float dx = (delta * vector.x) * speed; // Calculate the x amount (+-)
            float dy = (delta * vector.y) * speed; // Calculate the y amount (+-)
            float dx2 = pos.x + dx; // Calculate the new X position
            float dy2 = pos.y + dy; // Calulate the new Y position

            // Compare the current postion and the new position
            // get difference in distance and add that onto distMoved
            distMoved += Vector2.dst(pos.x, pos.y, dx2, dy2);

            // Set the new postion of the bullet
            pos.set(dx2, dy2, 0);

            // Update the postion of the sensor
            sensor.setTransform(pos.x + width / 2, pos.y + height / 2, 0);

            // Check if bullet has travelled its range
            // We do not want bullets moving for ever
            if (distMoved > range) {
                // Remove Bullet
                remove = true;
                active = false;
            }
        }
    }

    // The bullet vector is along the line at which the gun is pointing at the moment the space bar was pressed.
    // A quick fix to move the bullet toward the end of the gun is setting the bullet x to match the gun and move it
    // along the heading by 10, we apply the same to the y position and it will move along its heading.
    //
    // We create a sensor which will be used for collision checking with Box2D.
    public void setupBullet(Box2DWorld box2d) {
        // Position
        float angleRadians = MathUtils.degreesToRadians * gun.angle;
        vector.x = MathUtils.cos(angleRadians);
        vector.y = MathUtils.sin(angleRadians);

        // Move bullet toward end of gun
        pos.x = gun.pos.x + (vector.x * 10);
        pos.y = gun.pos.y + (vector.y * 10);

        // Physics
        sensor = Box2DHelper.createSensor(box2d.world, width, height * .85 f, width / 2, height / 3, pos, BodyDef.BodyType.DynamicBody);
        hashcode = sensor.getFixtureList().get(0).hashCode();
    }
}

The only change left is to amend the Gun class; first we add an ammoCount, this will be used to check if the gun has ammo to fire, on firing we will need to decrease this value.
The new ArrayList activeAmmo will be used to track ammo that needs to tick each frame.

core/src/uk/co/carelesslabs/weapons/Gun.java

The ArrayList is initiated in the Public Gun method:

activeAmmo = new ArrayList();

Two new methods tick and addActiveAmmo; tick allows the gun to loop through its activeAmmo and call tick for each ammo item. addActiveAmmo will decrease a guns current ammo count and add a new bullet to the active list, this is the bullets that have been fired, if the variable name doesnt made sense feel free to rename it to firedAmmo for example. As already mentioned the ammoCount check should have occured before this function.

When the gun is empty of ammo at the moment we will just print line, later this can draw some graphic to the screen or play a sound.

public void tick(float delta) {
    for (Ammo a: activeAmmo) {
        a.tick(delta);
    }
}

public void addActiveAmmo(Ammo a) {
    if (ammoCount > 0) {
        activeAmmo.add(a);
        ammoCount--;
    } else {
        System.out.println("Clink");
    }
}

Drawing the bullet, the guns draw method simply loops through active ammo and as it extendings Entity can use its draw method.

public void drawRotated(SpriteBatch batch){
    ...
    for(Ammo a : activeAmmo){
        a.draw(batch);
    }
}

Simple method to add bullets, this can be used later on when we have collectables.

    public void addAmmo(int count) {
        ammoCount += count;
    }

This method "clearTravelledAmmo" is used to remove bullets that have travelled over their range and been marked for removal. The ammo must be removed from the activeAmmo array, any bodies/sensors removed from the Box2D World. We use an iterator as they allow the caller to remove elements from the underlying collection during the iteration, other loops will crash if you remove items while looping through.

public void clearTravelledAmmo(Box2DWorld box2D) {
    Iterator it = activeAmmo.iterator();
    while(it.hasNext()) {
        Ammo a = it.next();
            if(a.remove){
                a.removeBodies(box2D);
                box2D.removeEntityToMap(a);
                it.remove();
        }
    }
}

Lots of small changes and some new classes but we now have the ability to fire bullets, the next tutorial will look at the collision of these bullets with other entities. If you enjoy or find these tutorials useful feel free to let me know at Twitter: @CarelessLabs

There are improvements this code requires and you should carry these out yourselves and please let me know of any bugs!

Advertisements