YAEGER_

Yaeger is Another Education Game Engine Runtime, and a fully functional 2D game-engine that requires only a traditional Object-Oriented style of programming. To create a game, classes should be extended and interfaces should be implemented.

Its API is based on inheritance and composition and can be seen as a layer of abstraction on top of JavaFX. JavaFX is only exposed minimally, so it is not trivially possible to use JavaFX within Yaeger. This is by design.

Required Java version

Although Yaeger only exposes a traditional Object-Oriented API, internally its based on modern Java. To see which specific version is required, please read the readme on the project GitHub.

Howto read this manual

This manual is only meant to give a high-level overview. It will paint a picture of how a game can be created and what kind of objects are available. The details and specifics will not be part of this manual. They can be found in the JavaDoc API.

Creating a game

To create a new Yaeger Game, a Java class should be created that

  • extends YeagerGame
  • contains the following method:
public static void main(String[]args){
    launch(args);
}

Such a game does not have any content yet, but it can be run and will only show the Splash screen. This Splash screen will be shown before each Yaeger Game. It can, however, be disabled by adding a commandline argument when running your game. More information on this can be found here.

Lifecycle methods of YeagerGame

By extending YeagerGame, several methods will need to be implemented. These methods are part of the lifecycle of a Yaeger Game and will be responsible for setting up the content of the Game.

Setting up the game, through setupGame()

The first method to implement is setupGame(), which has the following s ignature:

@Override
protected void setupGame(){
}

This method will be called first and must be used to set up the width/height and title of the Game. A typical implementation can look like:

@Override
protected void setupGame(){
    setGameTitle("Waterworld");
    setSize(new Size(800,600));
}

For more information, check the API of setupGame()

Adding Scenes, through setupScenes()

After setupGame() has been called, Yaeger will call setupScenes(). This is the second step in the life cycle of a Yaeger Game, in which it expects the developer to add Scenes to the Game. Scenes can be added by calling addScene(int, YaegerScene). The first parameter is the id of the YaegerScene, which can be used to select which YaegerScene will be shown. By default, the first scene that is added, will be the first scene that is shown.

A typical implementation can ook like:

@Override
protected void setupScenes(){
    addScene(0, new TitleScene(this));
    addScene(1, new GameLevel(this));
    addScene(2, new GameOverScene(this));
}

Loading a scene

When adding scenes to the Yaeger Game, the first added scene will also be the one that gets loaded and becomes visible. To load a specific scene, YaegerGame provides the method setActiveScene(int). Read the API for the details on how to use this method.

What about the constructor?

For Plain Old Java Objects (POJO's) the constructor is usually used for configuring your Object. Although it is not always the best approach, since the constructor should be only used for constructing an Object, not for configuring it, it will usually still work.

With Yaeger, however, it will usually not work. Configuring a YaegerGame should only be done from the setupGame() method. Furthermore, the constructor of the class that extends YaegerGame should be empty, since Yaeger will otherwise not know how to make an instance of it.

Scenes aka levels

After scenes have been added, Yaeger will load the first one. In this chapter we will dive deeper into them.

What are scenes?

A game is usually comprised of different 'parts', such as levels, menu's or a game-over screen. Some of those are just simple images and other are complex levels, with lots of behaviour.

In Yaeger, all of them implement YaegerScene and will be referred to as a Yaeger scene. For a simple Yaeger scene, without any moving parts, Yaeger provides the abstract class StaticScene. In case of a complex scene, with moving parts and a so-called Game World Update, Yaeger provides a DynamicScene. Both can be added to Yaeger, since they both implement YaegerScene. Their behaviour is, however, quite different.

StaticScene

A StaticScene is a scene that is not aware of time. It can contain anything you like, and those things will listen to user interaction, but their behaviour will not be based on the concept of time. Typical use cases are:

  • A Menu
  • A Game Over scene
  • An inventory selection scene

DynamicScene

A DynamicScene is exactly the same as a StaticScene, but it is also aware of time. A DynamicScene contains a Game World Update, that calls a update(long) method on the scene itself and all dynamic parts of the scene. This way, it is possible to create movement and add time-based behaviour. Typical use cases are:

  • A level for a game
  • A Splash screen
  • A scene with any form of finiteAnimation

ScrollableDynamicScene

A ScrollableDynamicScene is exactly the same as a DynamicScene, but allows a different width/height for its content. This way the scene can be much larger that the viewable area (the viewport). The width and height of the scene can be set from the setupScene() and the part of the scene that is visible can be set by setting the scroll-position.

Creating a scene

To create either a StaticScene or DynamicScene, let your own scene extend one of those. As when extending YaegerGame, again two methods will need to be implemented:

  • setupScene(), which should be used for setting all the properties of the scene
  • setupEntities(), which should be used for adding various entities (Game Objects) to the Scene

Setting up the scene, through setupScene()

The first method to implement is setupScene(), which has the following signature:

@Override
protected void setupScene(){
        }

This method will be called first and must be used to set up the background image, background color and background audio of the scene.

A typical implementation can look like:

@Override
public void setupScene(){
        setBackgroundAudio("audio/ocean.mp3");
        setBackgroundImage("backgrounds/background1.jpg");
        }

For more information, check the API

Adding entities to the scene, through setupEntities()

After setupScene() has been called, Yaeger will call setupEntities(). This is the second step in the life cycle of a YaegerScene, in which it expects the developer to add entities to the scene.

Entities can be added by calling addEntity(YaegerEntity). Since entities can overlap, their placement will follow the order in which they are added to the scene. So newly added entities may overlap entities that had been placed earlier.

More on entities in the following chapter of this documentation.

A typical implementation can look like:

@Override
public void setupEntities(){
        addEntity(new Hanny(new Coordinate2D(1,1),waterworld));
        addEntity(new Swordfish(new Coordinate2D(200,300)));
        addEntity(new Sharky(new Coordinate2D(0,100)));
        }

What about the constructor?

The same rules apply to a YaegerScene, as do to a YaegerGame. Configuring the scene should only be done from the setupScene() method.

It is however possible to use the constructor for basic wiring of objects or other commons object-oriented practices.

Entities aka Game Objects

Entities are what actually make a game. Anything that can be placed on a scene will be an Entity and will have to (indirectly) implement the abstract superclass YaegerEntity.

Static or dynamic entities

Just as with scenes, entities are available in static and dynamic version. The main difference is that a dynamic entity receives a GWU, where a static entity does not.

In general static entities will be typically used for menu-items or non-moving things. Dynamic entities are typically used for anything that should move around the scene, or should have time-based behaviour in general.

Note that the GWU originates at the scene. To make a dynamic entity to receive the GWU, it must be added to a dynamic scene. If added to a static scene, it will be as static as a static entity.

Properties for all entities

Although there a different entities, they all share a basic set of properties. These include the hue, saturation, brightness, rotation and many more. One of the more interesting properties is the viewOrder, which can be used to influence the order in which the entities are rendered on the scene. The lower the value, the closer the entity is placed to the front of the scene.

Movement of dynamic entities

Dynamic entities can move around the scene, by setting their motion. This movement is applied each GWU, which means the entity moves discretely across the scene (as opposed to continuously).

The motion consists of a direction and speed, which can be set individually or together. The direction is a double, where the value 0 denotes downward and 180 logically means upward.

Different types of entities

There are several Entities available, which can be divided into four different types:

Static EntityDynamic EntityType
SpriteEntityDynamicSpriteEntitySprite entity
CircleEntityDynamicCircleEntityShape entity
EllipseEntityDynamicEllipseEntityShape entity
RectangleEntityDynamicRectangleEntityShape entity
TextEntityDynamicTextEntityText entity
CompositeEntityDynamicCompositeEntityComposite entity

The sprite, shape and text-entity are basic entities. The composite entity is of a different type. It should be used whenever the entity should actually consist of several smaller entities.

Applying physics

Yaeger supports a basic form of physics for entities, through the Newtonian interface. Since this form of physics is based on motion, the interface makes only sense when applied to a dynamic entity.

Animating entities

Both the SpriteEntity and the DynamicSpriteEntity can be used to create animated entities. Fot this, the provides image should be a spritesheet, as can be seen in the image below:

A spritesheet

For complex animation, an Animation can be created to encapsulate a part of the spritesheet. For more information, please read the API of the interface Animation and its implementations.

Creating entities

All available entities, except the TextEntity, are abstract classes, meaning they should be extended to create an instance. After creating a class, an instance should be created, which can then be added to a YaegerScene, or a CompositeEntity through the addEntity(YaegerEntity) method.

In general, the setupEntities() is the appropriate place to do such a thing, but it is possible to call addEntity(YaegerEntity) from methods such as setupTimers() (see Timing things) or setupSpawners() (see below).

Spawning entities at a regular time-interval

Sometimes many entities should spawn within a scene. For instance, when enemies spawn to life, or when it snows.

For such cases, Yaeger provides an EntitySpawner, which was designed for specifically this case.

Creating many entities at once, using a TileMap

When many entities populate the scene, a TileMap can be used to easily create them and to let Yaeger calculate the exact location where they should be placed.

Imagine the image below should be used multiple times on the scene:

A tile,

and should be placed in such a way that we get the following scene:

A scene with tiles.

A TileMap facilitates this, by defining a two-dimensional array that represents the scene and stating which entities should be used. The TileMap will then calculate the location and size of each entity, instantiate them and add them to the scene.

To be able to use a TileMap, the scene should implement the interface TileMapContainer.

Entity collisions

Entities can interact with each other by using collision detection. Collision detection is Yaeger is fairly simple. Each GWU checks if all entities that implement Collider intersect with each entity that implements Collided. If an intersection occurs, the method onCollision (List<Collider>) is called on the entity that implements Collided.

Because collision detection is performed on each GWU, which has a discreet value of 60 times/second, if entities move at a high speed, it is possible that collision detection misses their collision. This is currently a limitation of Yaeger.

Entities leaving the scene

Do be notified whenever an entity leaves the scene border, two interfaces are available:

  • SceneBorderCrossingWatcher
  • SceneBorderTouchingWatcher

Their event handler will be called whenever an entity respectively leaves the scene border or merely touches the scene border. As with collision detection, also this checks is performed only on the GWU, meaning it will not be extremely accurate with fast moving entities.

User Input

A Player can interact with both Entities and Scenes by using the keyboard or mouse. To enable this interaction the Scene or Entity should implement the appropriate interface, after which an event handler should be implemented.

Available interactions

The table below gives the full list of interfaces that are available. They can be found in package com.github.hanyaeger.api.userinput and most can be applied to all children of both YaegerEntity and YaegerScene, only the MouseDraggedListener can only be applied to children of YaegerEntity.

In case of a ScrollableDynamicScene, the instances of Coordinate2D that are passed to the event handlers report the coordinates of the entire scene, not only the viewport.

InterfaceEventHandler(s)
KeyListenervoid onPressedKeysChange(Set<KeyCode>)
MouseButtonPressedListenervoid onMouseButtonPressed(MouseButton, Coordinate2D)
MouseButtonReleasedListenervoid onMouseButtonReleased(MouseButton, Coordinate2D)
MouseEnterListenervoid onMouseEntered()
MouseExitListenervoid onMouseExited()
MouseMovedListenervoid onMouseMoved(Coordinate2D)
MouseMovedWhileDraggingListenervoid onMouseMovedWhileDragging(Coordinate2D)
MouseDraggedListenervoid onMouseDragged(Coordinate2D), void onDropped(Coordinate2D)
MouseDragEnterListenervoid onDragEntered(Coordinate2D, MouseDraggedListener)
MouseDragExitListenervoid onDragExited(Coordinate2D, MouseDraggedListener)
MouseDropListenervoid onDrop(Coordinate2D, MouseDraggedListener)

Sounds

Sound are usually an important part of any game. A sound can be either an effect that is part of the game, or an audio-fragment that is being played at the background. Both cases are supported by Yaeger, but both require a different way to make them happen.

Background audio

Background audio can be set on either the YaegerGame or YaegerScene. In both cases the audio will loop indefinitely until respectively the YaegerGame has ended or a different YaegerScene is loaded. This way it is possible to set background that will be used throughout the game, or is specific to a YaegerScene.

Sound effects

When a sound effect should be played, a SoundClip can be used. It will only be played once and as soon as it has finished it becomes eligible for garbage collection. Although the volume of a SoundClip can be set, note that it is only applied to the next time it is played. Thus, the volume has to be set before play() is called.

Timing things

As can be read in Scenes aka Levels, there are two different implementations of YaegerScene available: a StaticScene and a DynamicScene. Their main difference resides in the fact that a DynamicScene contains a Game World Update (GWU) to which all instances of DynamicEntity added to the scene will listen.

This chapter will discuss the different ways in which the GWU can be used within Yaeger. It will start by discussing how the GWU gets delegated to all objects, after which different ways are discussed to use it within your scene or entity.

How the GWU is delegated to all objects

The GWU is initiated by the DynamicScene, and handed down to all dynamic entities, timers, spawners and composite entities that were added to the scene. This is done in the same order as in which they were added to the scene.

From a programming point-of-view, the GWU is a timed method call. The DynamicScene calls its update() method, which then calls all the update() methods of all the dynamic entities, timers, spawners and composite entities. If those objects themself also contain child-objects, their update() method is then also called. And so forth.

Update Delegation

Pausing and resuming the GWU

Since the GWU gets initiated at the level of the DynamicScene, and then passed down, it is possible to pause all objects that are called by this GWU. For this a DynamicScene provides a method to pause and resume the GWU.

Using a Timer to create time-based events

On both dynamic scenes and dynamic entities one or more timers can be used to create time based events. To use such a Timer the scene or entity should implement the interface TimerContainer. After doing so, the method setupTimers() should be implemented and the method addTimer(Timer) can be called to add an instance of Timer.

Exposing the GWU to scenes and entities

The GWU is kept internal on all entities and scenes and gets delegated downwards. It is, however, possible to expose the GWU to an entity or scene by implementing the interface UpdateExposer, which exposes an explicitUpdate (long) method. The value passed to this method represents a timestamp of the current time and can be used to keep track of time.

Command Line Arguments

When run, Yaeger accepts command line arguments. These arguments are primarily meant for debugging your Game.

The following arguments are currently supported:

ArgumentExplanation
--noSplashSkip the Splash screen during start up.
--showBBShow the BoundingBox of all instances of YaegerEntity that implement either Collider or Collided.
--showDebugShow a debug window with information about the Scene. More information on the debug window can be found here.
--showGridShow a coordinate grid as an overlay on each Scene. This can be useful when figuring out where exactly Entities are on the Scene.
--enableScrollEnable the scrolling gesture for all instances of ScrollableDynamicScene. This can help during the development process of such a scene.
--limitGWULimit the Game World Update (GWU) to a max of 60/sec.
--helpShow this help screen with all commandline options.

Using command line arguments from an IDE

Since the command line arguments mainly focus on the development cycle, they will likely only be used during development of a Yaeger Game. In such a case you will be working in an IDE, which will also be responsible for creating a run configuration. Part of such a run configuration will be the (extra) arguments that can be passed to the application. So check the documentation of your IDE to see where these arguments can be entered.

Debugging a game

Yaeger provides a debugging dialog that shows information about the current scene, and the machine it is running on. Since this dialog requires the Game World Update to refresh itself, it gives little information on a scene that extends StaticScene. But on a scene that extends DynamicScene it can give insight.

To enable the debugging dialog, start you Yaeger game with the command line argument as shown in Command line arguments.

Processors and memory

When run, a Java-program allocates memory through the JVM, the Java Virtual Machine, which is part of the Java Runtime Environment (JRE). Depending on the number of Objects that make up the program, it then uses this memory. Since, during the lifecycle of a program, more and more Objects are created, you can see the total used memory increase. When some of these Objects are no longer used by the program, the memory they use gets cleared by the JVM. Hence, the total used memory periodically decreases.

If the total used memory reaches the value of total allocated memory, the program will come to a grinding halt.

Entities, suppliers and garbage

The debugger keeps track of the number of Entities that are present on the scene.

EntityExplanation
Dynamic EntitiesNumber of entities that extend DynamicEntity
Static EntitiesNumber of entities that extend StaticEntity
SuppliersNumber of objects that are able to supply entities to the scene. This means all instances of EntitySpawner, but also the Scene itself.
GarbageNumber of Entities that have been marked as garbage, by calling the remove() method. The will be removed from the Scene during the next Game World Update, which should result in a drop of total used memory
Key listening entitiesNumber of entities that implement KeyListener

Loaded files

Image and audio files will probably make up most of Yaeger's memory usage. To minimize this memory footprint, Yaeger reuses images if possible. Meaning, for entities that use the same image file, this image file gets loaded only once. The same goes for audio files. Because Yaeger tries to reclaim the memory if such a file is no longer used, the background audio does get added to the value in the debugger. As soon as it starts playing, Yaeger loses its reference to the loaded file, and it continues playing.

FAQ

This chapter will sum up known issues and solutions on how to bypass them. The list is likely not complete, so do feel free to share your findings.

Some of the issues noted here can be seen as bugs, but that does not mean they are easily resolved.

My MP3 file is not being played

For some reason, not all MP3 files are supported. If playing an MP3 file does not give any exceptions, but you can not hear it in game, it might be because the MP3 file is not supported. To correct this, use a tool to create an MP3 file that is supported.

An entity behaves strangely when colliding

Sometimes an entity behaves unexpected when colliding. For instance, it is set to change its direction on collision, but when this happens it doesn't immediately do so. First it seems to stutter a few times, after which it finally behaves as expected.

This behaviour likely follows from the fact that the bounding box is used for collision detection, and the bounding box is usually larger that the entity we can see. Especially if the entity is a SpriteEntity containing a round image, the bounding box is still rectangular. When this SpriteEntity is also rotating, it is not unlikely that the angles of the rectangular bounding box will again cause a collision on the Game World Update after the first collision. Because of the second collision the entity will again change its direction and this cycle can repeat itself until the entity no longer collides.

A workaround could be to use a CompositeEntity with a smaller hit box.

I use tile maps, timers and entities on the same scene, why not add everything in the setupEntities() method?

Each YaegerScene exposes the method setupEntities(), which should be used to add instances of YaegerEntity to the scene. When tile maps are required, the scene should implement the interface TileMapContainer, after which the method setupTileMaps() gets exposed and the method addTileMap(TileMap) becomes available. The same goes for timers through the interface TimerContainer.

In a Scene where either tileMaps or timers are required, it is very well possible to ignore the setupTileMaps() or setupTimers() methods and add TileMaps or Timers from the setupEntities() method. It is however preferable to use the appropriate method, to maximize readability. Scenes can grow rather large and to find specific entities, tile maps or timers, it can help to group them within their specific methods.

I have a machine with Apple Silicon (M1, M2, M3, etc.)

If you have a machine with Apple Silicon you might come across an error in the build process like this:

Error building because of libraries that cannot be linked

To solve this you have to change the architecture of your JDK to one that is not specific to an ARM CPU.

Go to Project Structure, through File->Project Structure or enter the keycombo: ⌘+;.

You will see something similar to this:

Project Structure window in IntelliJ

Click on Edit next to the SDK overview and then choose Download JDK.

Download button

Then choose a JDK that does not have the descriptor: aarch64.

JDK without aarch64

To finish click download.

Download the JDK

You should be able to build and run your project now.