This game is a university assignment project. It is written in Java, based on a modified version of the game2D engine. The main goal of this project was to practice common game theory problems and come up with practical solutions. I took the opportunity to simultaneously practice Java 8 streams, which you could find overused in the source code of this project.
- https://rvros.itch.io/animated-pixel-hero
- https://0x72.itch.io/dungeontileset-ii
- https://0x72.itch.io/16x16-dungeon-tileset
Check out the releases page
- W A S D or ↑ ← ↓ → - move around
- Space - attack
- Esc - quit game
- F - show/hide collision boxes
- Left Mouse Button - kill enemies and open treasures (cheat)
Object | Type | Property | Description |
---|---|---|---|
Treasure | trigger | non-passable | hit to open |
Cube | non-blocking | pushable | push it around |
You can simply run the provided gradle wrapper with the build command:
./gradlew build
The resulting executable JAR
will be located under: build/libs/game2d-dungeon-SNAPSHOT.jar
A couple of changes were required to the game2D engine, mostly in order to avoid overly hacky workarounds.
- All resources are now embedded and loaded from the
JAR
- Support for displaying tile collision boxes
- Support for scaling tiles by a factor
- Support for flipping sprites horizontally
- Massive performance improvements by keeping the
Clip
loaded in memory. This prevents an issue with the old implementation which randomly delays and cancels sound playback. - Support for filter streams
- Implements
MouseListener
- Exit
JFrame
on window close
Below are the main processes I went through to create this game. They include an introduction to the game engine and explanations of the most important game theory concepts.
The game engine follows a traditional approach of separating Tiles
and Sprites
. Tiles
are defined in a text-based map file, and they do not support animation. Sprites
, on the other hand, are loaded programmatically, and take a single animated tile set. Unfortunately, they cannot be embedded in the map file, which causes some segregation.
After finding some assets online, I imported them to Tiled Map Editor for closer inspection. When I chose what I wanted to use for my game, I used imagemagick to split the tile sets to individual images:
convert -crop 16x16 +repage tileset.png %d.png
From there, I used this Python script to merge any individual animation tiles into animation sets supported by the game engine.
Since the game constantly works with both relative and absolute views, mistaking them becomes very easy. To solve this, I created two stub classes RelativeRectangle
and AbsoluteRectangle
which extend Rectangle
. This allows the compiler to type-check for you if the correct type of coordinates is being used at all times.
The camera following the player was easy to achieve, simply by offsetting everything that is rendered based on the player's absolute position. Customizable bounding was added to the camera as well, so the camera cannot look beyond the left and right borders of the map.
Based on Unity's GameObject
pattern, this game implements an abstract, feature-rich object that can be used to quickly spawn and bring to life various entities. All non-static entities such as the Player
and NPC
s inherit from this object.
All GameObjects
have at least one AnimationState
, such as standing, walking, attacking. Since all animations for an entity have to fit in the same tile, one large animation is enough to stretch the tile for all the others. Consider the screenshots below:
As we can see, the attack animation on the right is many times wider than the standing animation on the left. Yet, the tile rectangle (blue) stays the same. If we were to implement a naive bounding box that covers the whole tile, this blue rectangle is what we would get. Needless to say, that would result in very confusing and unrealistic physics.
To solve this problem, we can opt for customized bounding boxes for each animation. This way, we have individual control over each AnimationState
. In the screenshot above, these collision boxes are the red rectangles.
I used Tiled Map Editor to easily define the collision boxes, and then transferred them over to my game by looking at Tiled's project file, which is an easily readable XML.
Now that we have accurate boundaries, we need a way to find all Tiles
and GameObjects
that are colliding with a given GameObject
. One could loop through every possible combination of objects, but that would burn the CPU instantly. To avoid that, I used the following logic:
- Find all
Tiles
that theGameObject
tile rectangle (blue) covers - From them, filter the colliding
Tiles
- From them, filter the colliding bounding boxes (red)
- I created
GameObjectMap
- a two-way map betweenGameObjects
and theTile
coordinates they cover. It is always up-to-date and allows instant lookups. - Find all
Tiles
that theGameObject
tile rectangle (blue) covers - Through the
GameObjectMap
, retrieve allGameObjects
that cover thoseTiles
- Filter out pairs of colliding bounding boxes (red)
Now that we have the collision coordinates, it is time to resolve them. At first you could be tempted to say that, since each colliding Tile
is static, you could simply push back your Player
so that he no longer overlaps. What you will find is that this works very well for a single collision at a time, but with more than one (e.g, a corner) your Player
will clip right through the walls. This is because some Tiles
will find it closer to push the Player
towards other Tiles
, and the result is chaos.
After a lot of trial and error, I devised the following stable approach:
- For each collision, get the smallest collision rebound direction and save it in a vector. You will end up with a list like this:
Point(10, 0)
,Point(0, -3)
, ... - Add all the vectors together so that you get the biggest collision rebound
- Apply the above rebound to the
Player
- Repeat this process until no more collision is detected
A lot of improvements can be made to this game, given the current state is just a prototype. Some major notes include:
- More realistic gravity
- The current gravity implementation follows simple linear
Velocity
, and it is not very intuitive. More realistic gravity could be applied.
- The current gravity implementation follows simple linear
- Code clean-up
- Although best effort was made to keep the code lean and mean, it could take a big refactor.
- Better event system
- At present, the event system to update and coordinate
GameObjects
is very primitive, and will likely require change for more advanced interactions.
- At present, the event system to update and coordinate
- Behavior trees
- Behavior trees can be used to express all possible states of the entities, avoiding the hard-to-maintain and bug-prone spaghetti of conditional statements that is the
Player
's event handling right now.
- Behavior trees can be used to express all possible states of the entities, avoiding the hard-to-maintain and bug-prone spaghetti of conditional statements that is the
- More levels
- Should be achievable without touching the source code, just by modifying the map files, although the possibilities would be limited
- More entities (tiles, enemies, usables such as weapons)
- Menu
- Local multi-player