SEEK YE TREASURE IN PROCESSING CASTLE… BUT BEWARE THE SKELETON’S WRATH.
2282 Lines of Pain
Hi, I’m Kee Yong and I was the principal coder for The Lost Treasure of Processing Castle. While I did some of the texture work, the majority of my time was spent implementing our ideas and — for lack of a better term — making stuff work.
Things I did:
- Input handling
- The 3D rendering logic, including the system for displaying 2D sprites in a 3D game world
- A tile-based map system
- A file system for saving and loading map data
- A human-readable level editor, a standalone Processing project, to read and write said map data
- An object-oriented entity handling system for enemies and items
- A physics system with collision detection, momentum, and inertia
- Enemy pathfinding and (rudimentary) AI
- The player’s hand animations
- The damage, health, and scoring systems
- The HUD and YOU’RE WINNER message
- …and pretty much the entire code base
The final code is 2282 lines long (about half of which is comments).
MapBlocks and Entities
In TLTOPC the map is a 64×64 grid of tiles, which are called MapBlocks. Each of these tiles has various pieces of information associated with it, such as whether it’s solid or not, what texture it has, and whether it’s opened by a key.
Everything in the game world that isn’t a MapBlock is an Entity. This includes the player, enemies, items, scenery objects, and so on. Entities have their own properties, such as their coordinates in the game world and what their collision radius is.
The TLTOPC Level Editor was made to keep track of this data and present it in a human-readable format. It allows the player to edit the placement and properties of MapBlocks and Entities. Once the save button is pressed, the map data is stored in an XML file (which, thankfully, Processing has built-in support to read and write). The XML file can then be sent to the game proper, where it’s used to initialize the level as it loads.
When we began this project we thought we would have to write the 3D renderer ourselves. Thankfully, Processing comes pre-packaged with an OpenGL 3D renderer, which we used in TLTOPC. All the images seen in the game are drawn using OpenGL.
That said, OpenGL comes with a bunch of problems:
- The renderer assumes you want to render primitives or 3D models, not 2D sprites.
This assumption might not seem like a big deal (as you can just render geometry with the appropriate texture) but it caused unwanted behavior with transparent textures. You see, OpenGL’s default depth test behavior causes every texture to occlude the geometry behind it, regardless of whether that part of the texture is actually opaque or transparent. This meant that every skeleton and candelabra in the game had an invisible box around it that was blocking vision of things behind it.
To solve this bug, I had to throw out OpenGL’s depth test and make the game manually sort the MapBlocks and Entities in the game by distance from the player, then draw them back-to-front.
- The renderer assumes you want smooth textures, not pixelated ones.
This issue is actually relatively easy to solve in OpenGL, as the fix is to change a single setting in the code (switching the texture smoothing system to be nearest neighbor instead of trilinear). However, Processing’s OpenGL API doesn’t actually expose this setting. To get the pixelated effect we wanted, I had to dig into the back end of Processing and use an undocumented function to interact with the P3D renderer’s rendering settings.
With the OpenGL hacking out of the way, the actual rendering logic used in-game is fairly straightforward.
- We go through all the MapBlocks in a radius around the player (not bothering with the MapBlocks that are too far away for the player to see in the darkness).
- For each MapBlock, we check if any of the walls are facing the player, and render them.
- Then we render any Entities that are inside of that MapBlock. This means we find the Entity’s angle to the player, then create a quad (a 3D square) facing towards the player and draw the Entity’s sprite on that quad.
It wasn’t actually easy to implement because of all the math involved (who even remembers their trigonometry from secondary school?) but the results speak for themselves.
Physics and Collision
The physics implementation in the game is largely inspired by the Marathon series of games, created by Bungie Software (which are nowadays better known as the creators of Halo and Destiny). I played the Marathon games a lot as a kid, and TLTOPC is at its heart a 90s first-person shooter, so I used the way that Marathon‘s physics works as reference for TLTOPC.
In Marathon and many 90s first-person shooters:
- Everything has a momentum and inertia. Things will move in the direction of their momentum until their inertia reduces it to 0.
- All enemies and objects are treated as cylinders for the purposes of collision detection.
- The player’s head and hands bounce up and down when the player moves. (This is vitally important.)
All of these were translated into TLTOPC:
- All Entities have a momentum and inertia in TLTOPC. All movement is in the form of a “push” function which applies a force to an Entity with a given angle and magnitude.
- All Entities have a collision radius in TLTOPC. If two Entities are intersecting, they will each be pushed a small amount away from each other until they stop intersecting. (This implementation ended up being very buggy so it probably wouldn’t fly for future projects.)
- An internal variable called “playerBounceCycle” is advanced when the player moves. This is translated through a sine wave function and used to bob the camera and the player’s hands up and down. The magnitude of the bounce is also a function of the player’s speed, so the faster the player moves, the greater the magnitude of the bounce.
The player’s light attack, created by tapping the attack button, is treated as a giant invisible Entity that moves in front of the player and collides with enemies to damage them. The player’s charged dash attack temporarily turns the player immune to damage, pushes the player forward, and causes the player to deal damage to enemies and knock them away on contact.
Pathfinding and AI
TLTOPC uses the A* search algorithm for enemy pathfinding. This is a logic-based algorithm, commonly used in games, for finding the shortest route from one point in a network to another. How it works is essentially:
- Take a starting point.
- Guess the direction that is the fastest and most likely to lead to the destination.
- Try going in that direction.
- If there’s a path from here you haven’t checked yet, repeat from step 1, using this as the new starting point.
- Otherwise, backtrack and try a different direction instead.
To facilitate A* pathfinding, TLTOPC goes through all the MapBlocks at the start and attaches to each MapBlock a list of adjacent MapBlocks. This creates what I call a pathfinding mesh, an interconnected map of nodes, so that the pathfinding function can refer to it while it’s doing its thing instead of having to manually calculate the adjacent MapBlocks over and over.
The technical term for the “guessing” in step 2 is a heuristic, which is a method by which the algorithm determines which direction is the best. As TLTOPC takes place on a 2D grid of tiles, its heuristic is essentially “try walking in a straight line to the target.” This makes the algorithm take longer in maze-like scenarios but in general be quite efficient.
As for TLTOPC‘s AI, it’s really nothing to write home about. Skeleton enemies have an “aggro radius”; if the player comes within this distance of the enemy, they will activate and start chasing the player. They’ll pathfind to the player’s MapBlock if they aren’t already in it. Otherwise, they’ll move in the direction of the player and just try to mosh the player to death.
Music (click to play!)
I wrote the game’s music one night when I couldn’t sleep. The track (titled “The Lost Treasure of Processing Castle,” because I’m original) was made in LMMS with freely available synth instruments. The track is performed by a lute (medieval guitar), a drum, a tambourine, a flute, and a choir — all rendered in glorious 16-bit MIDI.
The music itself is both melodic and rhythmic, inspired by Baroque compositions as well as the soundtracks of Doom, Wolfenstein 3D, Marathon, Half-Life, and other 90s first-person shooters. It’s meant to be atmospheric and mysterious, with a driving beat pushing the player onward.
- Since TLTOPC is made up of several “windows” that aren’t necessarily displaying the same thing — or indeed the same dimensional space — the 3D view, HUD, and screen flash effect are drawn into their own PGraphics renderer each. The various PGraphics objects are then composited into the main window to form the final composition.
- The lighting was added because it looked cool, and was relatively easy to implement (since the P3D renderer comes with a lighting system). The fact that it hides MapBlocks beyond a certain distance was a happy accident that made it easier to optimize the renderer.
- The treasure chest at the end is coded as an enemy, with the death animation being opening. The treasure chest is also hardcoded to display the YOU’RE WINNER message on death (this was a last minute addition).
TLTOPC was a very fun project to work on that put my programming and software engineering skills to the test. After having worked on TLTOPC, I can say that I’m much more confident in my coding skills, and am better prepared to deal with the (infinitely more complex) code and script systems of commercially produced games.
There are many things I couldn’t implement in the project due to time constraints, including:
- A title screen and ending screen
- Treasure that can be picked up and added to your score
- More types of enemies with different behavior
- A stamina system that adds an element of resource management to attacking and blocking
- A more robust door system that allows us to bind specific doors to specific keys
- A more robust portal system that allows us to teleport the player to different places on the map
- Windows that look out onto a skybox
- …and more.
I look forward to working on more ambitious games and interactive media projects in the future!