A 2D game engine built in Java based on ECS architecture. See it in action here!
TEngine (pronounced "ten-gin", short for "Tessa's Engine") developed out of the game engine
supplied by the lecturers of the 159.261 Games Programming course at Massey University. I
fleshed out the original lone little GameEngine.java into what you see here, and used it to
build the games that I submitted for course assignments.
TEngine was built using Java 17 and uses Java Swing for window management (based on requirement
for the course). The structure of game objects (called Actors) is loosely based on the ECS
model. There is support for actors and actor management, level and world management, audio,
composite primitive containers, transforms, text, animated sprites and sprite sequences, and
basic broad-phase collision detection and collision event notifications.
- Minimum Java SE 17+
- IntelliJ, Eclipse, or your favourite Java editor
- Maven (install locally for command-line-only builds)
This project was developed using the Maven build system. You can either build and package the project straight from the command line or using your favourite Java IDE that has support for Maven.
When first opening the project, you'll see the project plugins and dependencies being downloaded.
It may take a minute for the project to process and index all the files. You can use the
pre-existing Maven goals to install the project dependencies and package up the TEngine into a
.jar for use in other projects.
To build from the command-line only, you will need to have Maven installed locally. You can then
use Maven to install the necessary dependencies and package up the TEngine into a .jar for use
in other projects. Enter the following commands in the project root:
# install dependencies
mvn install
# package the game engine
mvn packageA file called TEngine.jar will be created in the out/artifacts/TEngine_jar/ dir. This .jar can
be included in external projects in the usual way you link to external libraries.
TEngine is a 2D game engine that makes use of an ECS model to create and coordinate Actors. It
has a graphics engine, and physics engine,
- Actors & Actor Management: Supported β
- Audio: Supported β
- Graphics Engine: Supported β
- Drawing primitives: circles, rectangles
- Compound primitive containers
- Text
- Sprites
- Animated Sprites & Sprite Sequences
- Transforms
 
- Physics Engine: WIP π οΈ
- Broad-phase collision detection for AABBs β
- Broad-phase collision detection for Circles
- Collision detection events β
- Collision events notifications β
- Collision resolution
 
- World Management: Supported β
For a practical example of working with TEngine, see the snek! game here.
To create your own game, first create a new class that extends GameEngine, for now we'll refer
to it as Game. The GameEngine will take care of creating a window, handling mouse and key
events, and running the core game loop for you. Your Game class will take care of the game logic,
and being fun! :)
Inside Game is where you might decide to coordinate and manage various screens, e.g. a menu,
high score, or game over screen, the state of the game, e.g. playing, paused, game over, etc,
the current level, (called a World), and anything else that relates to your game logic.
There are a variety of methods that you can override so your Game can set itself up, load levels,
and listen for mouse, key, and collision events. As an example, the structure of your Game
class might look like this:
Your Game class is the entrypoint for the program, so you will define a main like so:
public class Game extends GameEngine {
    private static int FRAMERATE = 60;
    public static void main(String[] args) {
        createGame(new Game(), FRAMERATE);
    }
    public void init() { /* ... */ }
    // ...
}Any custom setup for your game prior to starting can be done by overriding the init() method. To
set a custom window dimension and title, call setWindowProperties(Dimension, String) from within
init().
Conceptually, a World is the same as level in a game. You can create multiple different
Worlds to represent different levels of your game. You can also maintain and manipulate the same
World throughout the game by adding and destroying Actors (explained in the next section) and
make changes while the player is playing.
At the beginning of your game, you need to load a World into the GameEngine by calling
loadWorld(World) on your Game class. To swap out one World for another, first call
unloadWorld(World) and then loadWorld(World).
An Actor, a.k.a. Game Object, Game Entity, or Node, is a general purpose object that
interacts with your game, and responds to player input or other Actors in the world. An Actor
in TEngine is modelled on an Entity Component System (ECS).
In an ECS, an Actor has many individual components to represent its data, characteristics, or properties. A game engine using an ECS has systems which operate on these components, e.g. physics, graphics, AI, collisions, etc. The Actor is usually only used to loosely associate the components to one another via pointers, references, or even simply using an ID.
Actor is the base class for an object that can be added to a World. Anything that the player
can control or interact with in the World is considered an Actor. This base class has an
initial set of methods and data that you may find useful. There are a few that you should pay
particular attention to:
- 
velocity,setVelocity()andvelocity(): these do what you expect, without requiring yourActorto have aTPhysicsComponentto work. This is because you usually want to be able to update the position (origin) of yourActorand all of its components usingsetOrigin (TPoint). By default,setOrigin(TPoint)will propagate this change to its graphical and physical components, but you can override this method to customize this behavior.
- 
setWorld(World): Associates thisActorwith a particularWorld. This can be used to pass yourActorbetweenWorlds.
Warning
You do not need to call this method directly. This method is called by
Worldwhen you add anActorto it by callingmyWorld.add(myActor).
- 
destroyWhenOffScreen(bool): set whether thisActorshould be destroyed when it goes offscreen. You can query this setting by callingdestroyWhenOffScreen().
- 
markPendingDestroy(): destroy theActoron the next update.
- 
destroy(): removes the graphic from theWorlds graphical canvas, and theActorfrom the world's list ofActors.
Warning
You only need to mark an
Actorto be destroyed, anddestroy()is called automatically byGameEnginein the update loop.
An Actor comprises two components: a graphical representation (TGraphicObject), and an
optional physical representation (TPhysicsComponent). By default, an Actor does not have a
graphical or physical representation. These components must be created and associated with the
Actor in the extending class constructor before the Actor can be used.
Note
You can choose not to give your
Actora physical component if it doesn't make sense for your game, but it must have a graphical component.
Below is a skeleton class for an Actor that represents the player. You may want to keep
small pieces of data in here, such as the number of lives or the player's score. Alternatively
encapsulate this data in a separate PlayerModel component and associate that object with this
class.
import tengine.graphics.*;
public class Player extends Actor {
    private final PlayerModel model = new PlayerModel();
    private final Dimension dimension;
    private Player(Dimension dimension) {
        // Initialize any private data members here...
        this.dimension = dimension;
        // Initialize graphic
        graphic = initSprite(dimension);
        // Initialize physics (optional)
        // physicsBody = initPhysics(dimension);
        // ...
    }
    /**
     * Initialize the graphic for this Player. You can use any Graphics class
     * that extends from TGraphicObject, e.g. TRect, AnimatedSprite, or
     * even a TGraphicCompound.
     */
    private TSprite initSprite(Dimension dim) {
        // Initialize the Player's graphic here! You can also choose to 
        // do this step in the constructor if this setup is trivial.
        // ...
    }
    /**
     * Call this method from within your World's update method to update
     * this Player every tick. You don't need to worry about updating the
     * graphic or physicsBody from here, that's handled by the GraphicsEngine
     * and PhysicsEngine!
     */
    public void update() {
        // Update the internal state of the Player here. This could be
        // moving to the next GridSquare, checking for game over conditions,
        // updating the Player's score, etc.
        model.update();
    }
    public boolean hasDied() { return model.numLives() == 0; }
    public boolean handleKeyEvent(KeyEvent keyEvent) { /* ... */ }
    // etc...
}Adding Actors to the World can be done by using the public methods on the base World class:
- add(Actor): adds a single- Actorto a- World, and
- add(Actor...): adds a variable number of- Actors to a- World, e.g.- myWorld.add(player, opponent, ball, playerGoal, opponentGoal)
Warning
You do not need to call the
setWorld(World)method on anActor, this is taken care of by theWorlditself:
Removing Actors from the World can be done by marking the actor to be destroyed with the public
method on the base Actor class markPendingDestroy(). From there, the GameEngine will take
care of removing destroying the Actor.
Warning
You do not need to call the
remove(Actor)method on aWorld, this is taken care of by theActoritself:
The GraphicsEngine will take care of drawing anything that you add to a World. You generally
won't need to interact much with the GraphicsEngine or worry about what goes on in here. The
GameEngine will take care of retrieving the Actor list from your World and loading their
graphical components into the GraphicsEngine. Actors are drawn in the order that you add them to
the World, so you can maintain a pseudo-z ordering by taking advantage of this.
TGraphicObject is the base class for all graphical objects that can be displayed in the window.
The basic primitive shapes are TRect and TOval (which is actually a circle). These
shapes can be drawn filled or outlined in any valid java.awt.Color. There is also support for
displaying text using TLabel and images via the GraphicsCtx interface. Using these primitives
and a TGraphicCompound, you can already do quite a lot! You may want to create a custom class
that extends from TShape, and in that case you can specify how the GraphicsCtx draws your
shape. Override the draw() method and make use of the following methods:
- drawRect(Dimension, Color)
- drawFilledRect(Dimension, Color)
- drawCircle(Dimension, Color)
- drawFilledCircle(Dimension, Color)
- drawText(Point, String, Font, Color)
- drawImage(Image)
- drawImage(Image, Dimension)
More than likely though, you will use a Sprite or AnimatedSprite for games. An indepth look
at these classes can be found below.
Below is an overview of the entire Graphics class hierarchy and the methods for each class:
Warning
Currently, there is no support for getting the actual width or height from a
TLabel.
All TGraphicObjects can have translation, rotation, and scale transforms applied to them. To
set the translation of an object, use setOrigin(). To set the scale, use setScale(x, y), and
to set the rotation use setRotation(angleDegrees, rotationOrigin). You can specify the origin
(relative to the object's origin) about which a rotation applies; passing in (0, 0) will rotate
the object around its own origin.
The TGraphicCompound is a composite primitive container that lets you group different
TGraphicObjects and treat them as a single object. Following the GoF Composite Pattern, a
TGraphicCompound can contain anything that is also a TGraphicObject, which includes other
TGraphicCompounds. Each object added to a TGraphicCompound will apply its own transforms
after the transforms for the parent container have been applied, meaning that transforms will
accumulate.
-  TODO: Create and use a Sprite
-  TODO: Create and update an AnimatedSprite
The PhysicsEngine is responsible for collision detection between Actors in your world, creating
CollisionEvents, and notifying the GameEngine when two Actors collide via a callback method.
The GameEngine sets this callback method to the onCollision(CollisionEvent) so your game can
respond to CollisionEvents by overriding this method.
To detect collisions between any two Actors, both Actors need to have physical components
(TPhysicsComponent) with collisions active (hasCollisions) and a CollisionShape.
Warning
Currently, only basic broad phase collision detection between two AABBs is supported. Please ensure you use a
CollisionRectwhen building up aTPhysicsComponent.
To create a TPhysicsComponent for your Actor, use the TPhysicsComponentBuilder class. This
class follows [Bloch's Java Builder pattern](https://blogs.oracle. com/javamagazine/post/exploring-joshua-blochs-builder-design-pattern-in-java),
so works as you would expect:
import tengine.physics.*;
class BouncingBox extends Actor {
    BouncingBox(Dimension dimension) {
        this.dimension = dimension;
        // other initialization...
        physics = initPhysics();
    }
    TPhysicsComponent initPhysics() {
        var boxPhysicsBuilder = new TPhysicsComponentBuilder();
        var collisionRect = new CollisionRect(this.origin, this.dimension);
        boxPhysicsBuilder.actor(this)
                         .isStatic(false)               // Optional: is this Actor stationary? Default = true
                         .collisionShape(collisionRect) // Optional: default = null
                         .hasCollisions(true);          // Optional: default = false
        return boxPhysicsBuilder.build();
    }
    // ...
}For tile-based games or board games, you may find the GridSquare class a more useful
and lightweight concept for collision detection simply by checking if they occupy the position
in a grid.