So, you’ve learned about LITIENGINE – great!
You’ve created a project in eclipse and imported LITIENGINE. If not, check out our Download Page that comes with detailed installation instructions.
Now you’ve probably come to the point where you’ve asked yourself:
“How am I supposed to actually make a game with this?”
In this How-To-Series, we guide you through the steps necessary for creating a platformer game inspired by the old Duke Nukem games, made in a glorious four-colour gameboy look.
The this project is entirely open sourced (under the MIT license). You can find the code among all the assets in the GitHub repository for Gurk Nukem.
Each blog chapter will represent the work that can be done in one day by following our step-by-step explanations.
For a general idea about what utiLITI is, read the docs page about utiLITI first.
Start the utiLITI editor. Hit "File -> New..." (CTRL+N
)
In the file browser that pops up, navigate to your project directory and hit "Open".
Even if you still have done nothing, hit "File -> Save..." (CTRL+S
) to create a game resource file.
You can name it whatever you want, we go for something like “game.litidata” in this guide.
Let’s assume that you’ve already created assets for your game. In our case, we picked a 4-colour GameBoy style for our project. So what basic Assets do we have at this point?
Player sprites:
For entities that are child classes of Creature, there are naming conventions for walking and idle sprites.
We need sprites named like {SPRITE_PREFIX}-{STATE}-{DIRECTION}.{EXTENSION}
.
Following these naming conventions, we create gurknukem-idle-left.png and gurknukem-walk-left.png.
You only need to provide a left or a right sprite in both cases, the CreatureAnimationController will automatically flip it when necessary.
gurknukem-walk-left.png
Enemy sprites:
We also add two enemy designs to the game. The naming is done in the same way as for our main character, so we end up with four image files: dean-idle-left.png, dean-walk-left.png, jorge-idle-left.png, and jorge-walk-left.png.
dean-walk-left.png
jorge-walk-left.png
Prop sprites:
For now, we will only add two props: a bunker and a destructive barrel.
For props, there are naming conventions, too: prop-{PROPNAME}-{STATE}.{EXTENSION}
This means that we need three image files for our destructible barrel: prop-barrel-intact.png, prop-barrel-damaged.png, and prop-barrel-destroyed.png Non-destructible props such as our bunker only need one animation file, in our case prop-bunker.png.
prop-bunker.png
prop-barrel.png
Our game logo:
We also need:
To import Sprite files into utiLITI, either drag&drop selected files into the Asset panel on the lower end of the screen or hit "Resources -> Import Spritesheets...". The same goes for tilesets ("Resources -> Import Tilesets..."). Maps can be imported by clicking "Map -> Import...". If a map was successfully imported, it will be rendered in the map panel and listed in the map list right to it.
Adjust your grid and snapping settings in the "View" menu.
In the Layer toolbox in the upper right corner, you can Add, Delete, Recolor, Rename, Duplicate, Focus / Hide, and reorder MapObjectLayers. To maintain a comprehensive structure to our map files, we try to create at least one layer for each Entitytype (There is the “Entities” tab to help you navigate through entities by type, as well).
Let us now create your first Entity
!
Select a MapObjectLayer, then right-click somewhere on the map and hit "Add -> Add CollisionBox". Click and drag with your mouse to specify the bounding box of your Collision box as shown in the gif below.
In the same manner, create a Spawnpoint
that will be used to spawn our player entity.
Edit the Spawnpoint’s Attributes in the Map Object Panel on the right as shown below.
You can give the Spawpoint a name for identification and set the spawn direction for our Player.
Before we look into coding now, we will set some basic properties for your map such as its name, description, and static shadow color. Simply hit "Map -> Properties" and you will see a popup for editing Map properties.
Congratulations! You’ve successfully completed the first steps on your way to creating a platforming shooter with LITIENGINE. Learn about setting up the basic functionality of our game in the next chapter.
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.
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. 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().setIcon(Resources.images().get("icon.png"));
Game.graphics().setBaseRenderScale(4f);
// 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();
}
}
Game.info()
.Game.init()
is then needed to initialize the Game
infrastructure (see the [Javadocs][] for an in-depth understanding
of what this does).Game.window().setIcon()
. Fancy! Of course,
the image file 'icon.png' has to be present in our project for this
to work.Game.graphics().setBaseRenderScale()
.Resources.load("game.litidata")
and all our
maps, sprites, etc. will be accessible directly from code hereafter.Game.world().loadEnvironment("level1")
.Game.start()
.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.
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:
Screen
present, adding a new one
will also call ScreenManager.display()
, making the newly added IngameScreen
the currently visible screen.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!
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:
cl_showGameMetrics
to true to see basic info about fps, ups, and networking performance.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");
}
@Override
public void update() {
}
@Override
protected IMovementController createMovementController() {
// setup movement controller
return new PlatformingMovementController<>(this);
}
}
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.MovementInfo
is exclusive to MobileEntities
: It contains
movement attributes such as velocity or acceleration.CollisionInfo
constitutes the size and alignment of a CollisionEntity
's collision box. .Creature
, meaning that it inherits
everything from CombatEntity
, CollisionEntity
, and Entity
,
while also implementing the methods from IMobileEntity
and IUpdateable
. For a clearer unterstanding of the entity
hierarchy, you can have a look at the Javadocs again.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
.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
.
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?).createMovementController()
-method to add a PlatformingMovementController
that allows us
to move entities horizontally with player input. This method will be called upon the initialization of the `Player``.For now, we leave the update()
-method empty and focus on spawning our freshly created player into our game world.
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 behavior 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().onLoaded(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());
}
});
}
}
PositionLockCamera
locked to the current
position of our Player
instance.Game.world().setGravity(120)
, we apply a constant GravityForce
to all MobileEntities
in all levels.Environment
is loaded, we want to spawn our player on the Spawnpoint
with the name 'enter'.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!
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.
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));
}
}
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.
For the jump mechanic, we choose to utilize LITIENGINE's Ability
framework. Go ahead and create the class Jump
:
@AbilityInfo(cooldown = 500, origin = EntityPivotType.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().value().get().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.movement().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 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();
}
}
}
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.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.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
.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.isTouchingCeiling
- condition for cancelling
the ForceEffect
, which determines if the jumping entity's
collision box intersects any static collision box above it.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 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;
}
}
@Override
protected IMovementController createMovementController() {
// setup movement controller
return new PlatformingMovementController<>(this);
}
/**
* Checks whether this instance can currently jump and then performs the Jump ability.
*
* 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.
* Another option is to explicitly specify the Action.name() attribute on the annotation.
*/
@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, Collision.STATIC);
}
}
Jump
instance and the current number of consecutive jumps as fields.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. Adding the jump method at this point is also an
introduction to a feature established in v0.5.0-beta: EntityAction
s. 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
. EntityAction
s 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.isTouchingGround()
-method, which returns
true, if the player's collision box touches a static collision box
or is intersecting with the lower map boundary.update()
-method, we reset the current jump counter to zero
on collision.Sit back and enjoy your awesome player double-jumping around the platforms in your test map, just waiting for some foes to defeat!
There are binaries available on the 'Gurk Nukem' GitHub page. 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.