Skip to content

Top-Down Action Shooter Tutorial

In this tutorial, you will build a fast-paced Top-Down Action Shooter inspired by engine patterns in Star Reaperz (litiengine-ldjam52).

flowchart LR
 Input["Mouse & WASD Input"] --> Player["Player Creature
(8-Way Controller)"]
 Player --> Weapon["Projectile Ability
(Spawn Bullet Entity)"]
 Weapon --> Physics["Physics Collision
(Raycast & Hit Detection)"]
 Physics --> Enemy["Enemy Creature
(Chase AI / onHit Event)"]

1. Player Entity with Mouse Aiming

The player moves with WASD and turns to face the mouse crosshair:

import java.awt.geom.Point2D;
import java.awt.event.KeyEvent;
import de.gurkenlabs.litiengine.Game;
import de.gurkenlabs.litiengine.Input;
import de.gurkenlabs.litiengine.entities.Creature;
import de.gurkenlabs.litiengine.entities.EntityInfo;
import de.gurkenlabs.litiengine.entities.MovementInfo;
import de.gurkenlabs.litiengine.entities.CombatInfo;
import de.gurkenlabs.litiengine.entities.CollisionInfo;
import de.gurkenlabs.litiengine.input.KeyboardEntityController;
import de.gurkenlabs.litiengine.util.geom.GeometricUtilities;

@EntityInfo(width = 32, height = 32)
@MovementInfo(velocity = 120)
@CombatInfo(hitpoints = 100)
@CollisionInfo(collision = true, collisionBoxWidth = 24, collisionBoxHeight = 24)
public class Hero extends Creature {

  public Hero() {
    super("hero");
  }

  @Override
  protected KeyboardEntityController<Hero> createMovementController() {
    KeyboardEntityController<Hero> controller = new KeyboardEntityController<>(this);
    controller.addUpKey(KeyEvent.VK_W);
    controller.addDownKey(KeyEvent.VK_S);
    controller.addLeftKey(KeyEvent.VK_A);
    controller.addRightKey(KeyEvent.VK_D);
    return controller;
  }

  @Override
  public void update() {
    super.update();

    // Aim toward mouse cursor in world space
    Point2D mouseScreen = Input.mouse().getLocation();
    Point2D mouseWorld = Game.world().camera().getMapLocation(mouseScreen);
    double angle = GeometricUtilities.calcRotationAngleInDegrees(this.getCenter(), mouseWorld);
    this.setAngle((float) angle);
  }
}

2. Projectile Ability (Shooting)

Create a ranged attack that fires projectiles toward the mouse cursor:

import de.gurkenlabs.litiengine.abilities.Ability;
import de.gurkenlabs.litiengine.abilities.AbilityInfo;
import de.gurkenlabs.litiengine.abilities.effects.Effect;
import de.gurkenlabs.litiengine.abilities.effects.EffectTarget;
import de.gurkenlabs.litiengine.entities.Creature;
import de.gurkenlabs.litiengine.entities.EntityPivotType;

@AbilityInfo(name = "Blaster", cooldown = 200, range = 400, impact = 25, origin = EntityPivotType.DIMENSION_CENTER)
public class BlasterAbility extends Ability {

  public BlasterAbility(Creature executor) {
    super(executor);
    this.addEffect(new ProjectileEffect(this));
  }

  private static class ProjectileEffect extends Effect {
    public ProjectileEffect(Ability ability) {
      super(ability, EffectTarget.ENEMY);
    }

    @Override
    protected void apply(Creature target) {
      super.apply(target);
      target.hit(getAbility().getAttributes().impact().get().intValue());
    }
  }
}

Bind the shooting action to the left mouse button:

BlasterAbility blaster = new BlasterAbility(hero);

Input.mouse().onPressed(e -> {
  if (Input.mouse().isLeftButton(e)) {
    blaster.cast();
  }
});

3. Enemy Chasing AI

Create an enemy that pursues the player using simple spatial vector math:

import de.gurkenlabs.litiengine.Game;
import de.gurkenlabs.litiengine.entities.Creature;
import de.gurkenlabs.litiengine.entities.EntityInfo;
import de.gurkenlabs.litiengine.entities.MovementInfo;
import de.gurkenlabs.litiengine.entities.CombatInfo;
import de.gurkenlabs.litiengine.entities.CollisionInfo;
import de.gurkenlabs.litiengine.util.geom.GeometricUtilities;

@EntityInfo(width = 28, height = 28)
@MovementInfo(velocity = 75)
@CombatInfo(hitpoints = 30)
@CollisionInfo(collision = true, collisionBoxWidth = 20, collisionBoxHeight = 20)
public class Zombie extends Creature {

  public Zombie() {
    super("zombie");
  }

  @Override
  public void update() {
    super.update();
    Creature player = Game.world().environment().get(Creature.class, "hero");
    if (player != null && !player.isDead()) {
      double angle = GeometricUtilities.calcRotationAngleInDegrees(this.getCenter(), player.getCenter());
      Game.physics().move(this, (float) angle, this.getTickVelocity());
    }
  }
}

4. Spawning Waves & Game Loop

Spawn enemy waves dynamically across the map:

public class WaveSpawner implements IUpdateable {
  private int waveNumber = 1;
  private long lastSpawnTime = 0;

  @Override
  public void update() {
    if (Game.time().since(lastSpawnTime) > 5000) {
      spawnWave();
      lastSpawnTime = Game.time().now();
      waveNumber++;
    }
  }

  private void spawnWave() {
    for (int i = 0; i < waveNumber * 3; i++) {
      Zombie zombie = new Zombie();
      zombie.setLocation(Game.random().nextInt(800), Game.random().nextInt(600));
      Game.world().environment().add(zombie);
    }
  }
}