Sample Project: Creating a Platformer with LITIengine [DAY 2]

Creating a platformer, part two! Now that you’ve created a first test map for your platforming game, we will show you how to load the map into your game, how to configure and spawn a Player entity, and how to implement basic gravity and jumping behaviours.

Chapter overview:

    1. The first Level
    2. Time to put on your hacker gloves! (you are currently here)

      1. Our Main Class
      2. The Ingame Screen
      3. A noble Hero
      4. Where to put my Game Logic?
      5. Organizing Player input
      6. Up we go

DAY 2: Time to put on your hacker gloves!

Our Main Class

Let’s start today by opening up the Eclipse IDE (as it is the IDE of choice for this how-to-series).
At this point, we assume that you’ve already created a Java project and referenced LITIengine in it as described HERE or HERE.
Also, you have created resource folders and added them to your project’s build path.
In our case, there are three resource folders: ‘maps’, ‘sprites’, and ‘audio’, which are located in the root folder of our project.

We proceed with creating a class containing our main-method to actually run our program like in the example shown below:

public class Program {

  public static void main(String[] args) {
    // set meta information about the game
    Game.info().setName("GURK NUKEM");
    Game.info().setSubTitle("");
    Game.info().setVersion("v0.0.1");
    Game.info().setWebsite("https://github.com/gurkenlabs/litiengine-gurk-nukem");
    Game.info().setDescription("An example 2D platformer with shooter elements made in the LITIengine");

    // init the game infrastructure
    Game.init(args);

    // set the icon for the game (this has to be done after initialization because the ScreenManager will not be present otherwise)
    Game.window().setIconImage(Resources.images().get("icon.png"));
    Game.graphics().setBaseRenderScale(4.001f); 

    // load data from the utiLITI game file
    Resources.load("game.litidata");

    // load the first level (resources for the map were implicitly loaded from the game file)
    Game.world().loadEnvironment("level1");

    Game.start();
  }
}
  • First, we set some basic information for our game such as its name, subtitle, version, website, and description by using the respective setters of Game.info()(lines 5-9).
  • Calling Game.init()(line 12) is then needed to initialize the Game infrastructure (see the Javadocs for an in-depth understanding of what this does).
  • Next, we set the window icon from the logo we created in Day 1 by calling Game.window().setIconImage()(line 15). Fancy!
    Of course, the image file ‘icon.png’ has to be present in our project for this to work.
  • Our game world will be scaled by a factor of 4, which we achieve with Game.graphics().setBaseRenderScale()(line 16).
    You’re probably wondering why we’ve chosen 4.001 as a scaling factor; Simply put: There is currently a bug doing some very ugly stuff when scaling sprites with whole numbered factors.
  • Now we load up our game resource file created with utiLITI in Day 1. Just call Resources.load("game.litidata")(line 19) and all our maps, sprites, etc. will be accessible directly from code hereafter.
  • Let’s try loading our ‘level1’ with Game.world().loadEnvironment("level1")(line 22).
  • The last thing needed to get our game running is Game.start() (line 24).

We’ve all been waiting for this moment, let’s go ahead and RUN THE GAME!
But wait, what’s this? Our game window is there, but it doesn’t show anything.

The Ingame Screen

As it would be nice to actually have our game shown in the empty window, we need to render the game world.
For this purpose, we create an IngameScreen that extends GameScreen.

public class IngameScreen extends GameScreen {
  public static final String NAME = "INGAME-SCREEN";

  public IngameScreen() {
    super(NAME);
  }
}

Once we add the IngameScreen to the ScreenManager by calling Game.screens().add(new IngameScreen()) in our main-method, two magic things happen:

  • Since there was previously no Screen present, adding a new one will also call ScreenManager.display(), making the newly added IngameScreen the currently visible screen.
  • Every Screen extending GameScreen contains a call to Game.world().environment().render()in its render()-method. Just remember: Once you override the GameScreen.render()-method, you’ll need to reference the inherited behaviour with super.render() or else the game world won’t be rendered.

Running the game again, we finally see something happening in our window!

day2_addingIngameScreen

Right now, we only see the upper left corner of our map since we haven’t defined any Camera yet. The default camera is locked to the map coordinates (0,0).
You can enable and disable the game metrics shown in the image above, as well as other debug options, by modifying the ‘config.properties‘-file in your project’s main directory:

  • Set the variable cl_showGameMetrics to true to see basic info about fps, ups, and networking performance.

A noble Hero

Finally, we can put our awesomely edgy main protagonist inside the game!
For this, we utilize LITIengine’s Entity framework. Let’s go ahead and create a Player-class:

@EntityInfo(width = 18, height = 18)
@MovementInfo(velocity = 70)
@CollisionInfo(collisionBoxWidth = 8, collisionBoxHeight = 16, collision = true)
public class Player extends Creature implements IUpdateable {

  private static Player instance;

  public static Player instance() { 
    if (instance == null) { 
      instance = new Player();
    } 
    return instance;
  }

  private Player() {
    super("gurknukem");

    // setup movement controller
    this.addController(new PlatformingMovementController<>(this));
  }

  @Override
  public void update() {
    
  }
}
  • Right at the top, you’ll notice the annotations. This is what they do:
    • EntityInfo specifies basic properties that all Entities have, namely their width, height, and RenderType (line 1).
    • MovementInfo is exclusive to MobileEntities: It contains movement attributes such as velocity or acceleration (line 2).
    • CollisionInfoconstitutes the size and alignment of a CollisionEntity‘s collision box. (line 3).
  • Our Player is a child of Creature, meaning that it inherits everything from CombatEntity, CollisionEntity, and Entity, while also implementing the methods from IMobileEntity and IUpdateable (line 4).
    For a clearer unterstanding of the entity hierarchy, you can have a look at the Javadocs again.
  • We adopt a Singleton pattern for the Player-class, meaning that we only allow the existence of one single instance of Player at all times.
    Calling the public static Player.instance() will only invoke the private constructor on its first call. All future calls will return the same instance of Player (lines 8-13).
  • In the Player ‘s private constructor, we invoke the constructor of superclass Creature with super("gurknukem"), where ‘gurknukem’ is the so-called spritePrefix attribute of a Creature (line 16). We need to declare the spritePrefix so that the CreatureAnimationController, which is added in a  Creature‘s constructor, will be able to associate the right Spritesheets with our Creature (remember the naming conventions for Spritesheets?).
  • Furthermore, we add a PlatformingMovementController that allows us to move entities horizontally with player input (line 19).

For now, we leave the update()-method empty and focus on spawning our freshly created player into our game world.

Where to put my Game logic?

To keep our main() method as atomic and light as possible, we go ahead and create a class GurkNukemLogic, where we will initialize global game logic such as spawning behaviour or defining cameras.

public final class GurkNukemLogic {

  private GurkNukemLogic() {
  }

  /**
   * Initializes the game logic for the GURK NUKEM game.
   */
  public static void init() {
    
    // we'll use a camera in our game that is locked to the location of the player
    Camera camera = new PositionLockCamera(Player.instance());
    camera.setClampToMap(true);
    Game.world().setCamera(camera);
    
    // set a basic gravity for all levels.
    Game.world().setGravity(120);

    // add default game logic for when a level was loaded
    Game.world().addLoadedListener(e -> {

      // spawn the player instance on the spawn point with the name "enter"
      Spawnpoint enter = e.getSpawnpoint("enter");
      if (enter != null) {
        enter.spawn(Player.instance());
      }
    });
  }
  • LITIengine comes with a few basic pre-implemented cameras. Momentarily, we’ll use a PositionLockCamera locked to the current position of our Player instance (lines 12-14).
  • With Game.world().setGravity(120), we apply a constant GravityForce to all MobileEntities in all levels (line 17).
  • Once an Environment is loaded, we want to spawn our player on the Spawnpoint with the name ‘enter‘ (lines 20-27).

Note that Entity names are considered unique per level in LITIengine by design. If you want to retrieve a collection of  Spawnpoints from the environment to pick a random one from, you could add Tags to them in utiLITI and call  Game.world().environment().getByTag("tag1","tag2",...).

Once we call GurkNukemLogic.init() from our main()-method, we can now walk around the ground in our map and we’ll even fall down into pits!

day2_walkOnGround

Of course, there’s one crucial thing missing here: The ability to jump!

But first, let’s briefly create a class to manage all input (except player movement, which is already handled by the Player‘s PlatformingMovementController.

Organizing Player Input

An odd thing you might have noticed by now is that you’re not yet able to exit the game with any keypress.
For initializing our key bindings, we create the class PlayerInput:

public final class PlayerInput {
  private PlayerInput() {
  }

  public static void init() {
    // make the game exit upon pressing ESCAPE (by default there is no such key binding and the window needs to be shutdown otherwise, e.g. ALT-F4 on Windows)
    Input.keyboard().onKeyPressed(KeyEvent.VK_ESCAPE, e -> System.exit(0));

  }
}
  • For now, we bind ESCAPE to kill our program. Of course this will change later on, but it will save some time in development.

Again, we need to call this code from our main()-method by adding PlayerInput.init() to it.

Up we go

Leap of Faith:

For the jump mechanic, we choose to utilize LITIengine’s Ability framework. Go ahead and create the class Jump:

@AbilityInfo(cooldown = 500, origin = AbilityOrigin.COLLISIONBOX_CENTER, duration = 300, value = 240)
public class Jump extends Ability {

  public Jump(Creature executor) {
    super(executor);

    this.addEffect(new JumpEffect(this));
  }

  private class JumpEffect extends ForceEffect {

    protected JumpEffect(Ability ability) {
      super(ability, ability.getAttributes().getValue().getCurrentValue().intValue(), EffectTarget.EXECUTINGENTITY);
    }

    @Override
    protected Force applyForce(IMobileEntity affectedEntity) {
      // create a new force and apply it to the player
      GravityForce force = new GravityForce(affectedEntity, this.getStrength(), Direction.UP);
      affectedEntity.getMovementController().apply(force);
      return force;
    }

    @Override
    protected boolean hasEnded(final EffectApplication appliance) {
      return super.hasEnded(appliance) || this.isTouchingCeiling();
    }

    /**
     * Make sure that the jump is cancelled when the entity touches a static collision box above it.
     * 
     * @return True if the entity touches a static collision box above it.
     */
    private boolean isTouchingCeiling() {

      Optional<CollisionBox> opt = Game.world().environment().getCollisionBoxes().stream().filter(x -> x.getBoundingBox().intersects(this.getAbility().getExecutor().getBoundingBox())).findFirst();
      if (!opt.isPresent()) {
        return false;
      }

      CollisionBox box = opt.get();
      return box.getCollisionBox().getMaxY() <= this.getAbility().getExecutor().getCollisionBox().getMinY();
    }
  }
}
  • As mentioned before, the class Jump is a child of LITIengine’s Ability. As such, we can annotate the class with the @AbilityInfo-annotation to determine its cooldown, origin location, duration, and value, which is an abstract numeral used in different ways, depending on what your ability does (line 1).
  • In an Ability‘s constructor, the Creature which casts the Ability is always passed as a parameter. In our case, we also add a JumpEffect to the Jump‘s list of effects (lines 4-8).
  • The inner class JumpEffect here is a ForceEffect, i.e. it will apply a given force to its affected entities. In its constructor, we establish its strength and EffectTarget (lines 12-14).
  • In the applyForce-method, we create a GravityForce directed upward which adopts the ForceEffect‘s strength. The force will then be applied to ability executor for the duration of the ability (lines 17-22).
  • We also provide the isTouchingCeiling– condition for cancelling the ForceEffect, which determines if the jumping entity’s collision box intersects any static collision box above it (lines 25-44).
Where it all comes together:

Now, back to our Player-class, where we have to perform a few more adjustments, ending up with the following code:

@EntityInfo(width = 18, height = 18)
@MovementInfo(velocity = 70)
@CollisionInfo(collisionBoxWidth = 8, collisionBoxHeight = 16, collision = true)
public class Player extends Creature implements IUpdateable {
  public static final int MAX_ADDITIONAL_JUMPS = 1;

  private static Player instance;

  private final Jump jump;

  private int consecutiveJumps;

  private Player() {
    super("gurknukem");

    // setup movement controller
    this.addController(new PlatformingMovementController<>(this));

    // setup the player's abilities
    this.jump = new Jump(this);
  }

  public static Player instance() {
    if (instance == null) {
      instance = new Player();
    }

    return instance;
  }

  @Override
  public void update() {
    // reset the number of consecutive jumps when touching the ground
    if (this.isTouchingGround()) {
      this.consecutiveJumps = 0;
    }
  }

  /**
   * Checks whether this instance can currently jump and then performs the <code>Jump</code> ability.
   * <p>
   * <i>Note that the name of this methods needs to be equal to {@link PlatformingMovementController#JUMP}} in order for the controller to be able to
   * use this method. <br>
   * Another option is to explicitly specify the <code>Action.name()</code> attribute on the annotation.</i>
   * </p>
   */
  @Action(description = "This performs the jump ability for the player's entity.")
  public void jump() {
    if (this.consecutiveJumps >= MAX_ADDITIONAL_JUMPS || !this.jump.canCast()) {
      return;
    }

    this.jump.cast();
    this.consecutiveJumps++;
  }

  private boolean isTouchingGround() {
    // the idea of this ground check is to extend the current collision box by
    // one pixel and see if
    // a) it collides with any static collision box
    Rectangle2D groundCheck = new Rectangle2D.Double(this.getCollisionBox().getX(), this.getCollisionBox().getY(), this.getCollisionBoxWidth(), this.getCollisionBoxHeight() + 1);

    // b) it collides with the map's boundaries
    if (groundCheck.getMaxY() > Game.physics().getBounds().getMaxY()) {
      return true;
    }

    return Game.physics().collides(groundCheck, CollisionType.STATIC);
  }
}
  • First, we declare the maximum number of mid-air-jumps, a Jump instance and the current number of consecutive jumps as fields (lines 5-11).
  • Then, we write a jump()-method. It casts the Jump ability and raises the consecutive jump counter by one if the jump limit hasn’t been reached and the Jump ability’s canCast()-detection returns true (lines 48-55).
    Adding the jump method at this point is also an introduction to a feature established in v0.5.0-beta: EntityActions.
    For an intuition about their workings, let’s have a look at the PlatformingMovementController: it can very well call all the methods that are declared by the IMobileEntity it is registered for.
    But what if we want the PlatformingMovementController to call logic that is exclusive to one single Entity implementing the IMobileEntity interface?For this purpose, there is a reflection-based system in LITIengine to call methods from IEntities that are otherwise inaccessible in certain places: Any  IEntity can declare an @Action-annotation on its methods. During runtime, these methods will be registered automatically by their method name and callable with IEntity.perform(String actionName). Once the SPACEBAR (or any other declared jump key) is pressed, the PlatformingMovementController will call Player.perform("jump") to run the  jump()-method annotated with @Action.
    EntityActions can also be registered explicitly without the annotation by using IEntity.register(String name, Runnable action). However, you should try avoiding this approach whenever possible as using reflection can come with severe security risks (among other drawbacks)! In most cases, you should stick to the inheritance-based Entity framework that LITIengine provides.
  • Hereafter, we declare the isTouchingGround()-method, which returns true, if the player’s collision box touches a static collision box or is intersecting with the lower map boundary (lines 57-69).
  • In the update()-method, we reset the current jump counter to zero on collision (lines 33-36).

day2_jumpAround

And that’s it for today!

Sit back and enjoy your awesome player double-jumping around the platforms in your test map, just waiting for some foes to defeat!

From now on, we will put together a release on the ‘Gurk Nukem’ GitHub page for each day. Head over there and compare your work to our reference project!
We can’t wait to share more about the journey of creating games with LITIengine, so stay tuned for part three of this how-to-series.