Testing actor-based obstacles with slope support.
I started the month on track, but then went on a long detour to implement more platforming and collision subsystems and make improvements to the build system. On the 27th, I started tearing the collision detection and platforming systems apart in an attempt to resolve some issues with it.
I noticed some collision issues where tilemapped obstacles and actor-based obstacles meet. This mainly shows up in actor-based doors that slide into tilemapped columns: if you kick a shot at the point where a closed door and the wall connect, the shot reacts to the tilemapped ceiling, and ricochets downward. As a workaround, I made a special terrain property that prevents any neighbouring obstacle tiles from being assigned solid edges in the navigation mask.
Here is an example door — the same kind as the one pictured above. It slides seamlessly into the tilemapped column above it:
This is our collision information. The door is configured to be solid on the bottom, left and right sides, but not the top. However, the tilemapped column ends with a hard edge / ceiling where it joins with the door:
(Sorry for the crappy diagrams.) In the next pic, pretend the circle is a player shot, which has just been created as the player jumps up and against the spot where the door and the tilemapped column meet. Our player has aimed the shot at the column, diagonally and upwards. We expect the shot to bounce back horizontally, and continue arcing up through the air. Instead, the shot (which is very briefly inside of the wall, due to how it spawns from the thinner player) detects the tilemapped ceiling and the door / tilemapped wall, and deflects horizontally and vertically:
Placing cancel-terrain along the ceiling eliminates it from the navmask, leaving nothing to conflict with shots created in this scenario.
Another case where this might be helpful is to bridge actor-based floors with tilemap-based floors. By cancelling the wall part of the tilemap, players won’t get caught on the tilemapped sides as they step from actor-surface to tilemap-surface.
Sloped Hazardous Surfaces
The circle of doom Various sloped surfaces with a damage attribute set.
These go with the square hazard blocks that I made back in November. Not sure how much I’ll actually use them, but it “completes the set.” They may be helpful in some specific cases where square damage tiles don’t quite fit in with the overall stage flow.
In October, I added support for assigning non-rectangular regions within hitboxes (arbitrarily called subshapes.) At the time, I wrote that they would be for special cases only, and that they “won’t have widespread support across the platforming module.” I’ve since changed my stance on this because I really, really want solid and damageable components to at least support angled surfaces, and the easiest way to accomplish that is with triangle subshapes.
For the most part, the code to handle shot responses against these shapes already existed in the form of sloped tilemap handling. Most of the trouble was creating new collision modes for opponents (including whether each side of a triangle should be considered; you wouldn’t want a triangle resting on the ground to consider the side hugging the ground as part of the shot rebound, for example), and then filling out a much-too-large if/elseif/else block to handle every case with the adapted code. And then testing every combo.
Here are some examples of various shot responses against opponents with subshapes:
I’ve been working on enemies that have protected areas and weak points. I figured I could just spawn child actors to represent different parts of an enemy’s body, like this:
The problem I’ve run into is that when the player’s shot collides with two actors belonging to the same enemy simultaneously, there can be conflicts in how the shot responds. A successful impact which also plays the “deflected with no damage” audiovisual cue is confusing. I looked over my enemy design ideas, and noticed that even when multiple actors are used, their overall shape is still a plain rectangle. In such cases, instead of splitting functionality between multiple actors, it might be simpler to just allow hitboxes to be partitioned into chunks, and then assign responses on a per-chunk basis. Here is a diagram of every splitting mode I put together:
All split-lines can be moved to arbitrary positions, but they must be axis-aligned. To break a tie between two hitbox chunks so that only one collision response is selected, we can compare the player’s center coordinate against the split-line position. The outer rows or columns can be effectively disabled by assigning split-lines that are far outside of the hitbox dimensions.
The “arbitrary split” in the bottom-right is for special cases where the other split modes don’t cut it: the partitions are stored as an array of quads, and the player’s shot is tested against each quad in sequence. The player shot’s center coord needs to be within one of the arbitrary hitboxes to register a hit, hence the boxes extending out beyond the actual hitbox.
Hitbox splitting is an opponent-level construct that is independent of hitbox subshapes, so it can be applied on top of circle, triangle and line subshapes as well:
This solves a lot of my problems, though it doesn’t fully resolve conflicts with opponents that need more complex bodies, and by extension, multiple actors. Maybe there isn’t an easy solution. Maybe I should avoid overly complicated enemy designs.
Again with the subshapes. One thing I’ve found lacking in the project so far is actor-based solid ground with angled surfaces. “Blocking” actors behave like solid walls on any of their four hitbox sides, but the way I wrote them didn’t make it easy to assign any kind of sloped edges to them. This is because while they can be configured to block selectively only on certain sides, they are still designed to shove actors out of their boundaries on both axes, in up to four directions. Here is every blocking actor configuration:
Top left: no blocking; Bottom-right: block on all sides.
After some failed attempts to make it work, I gave up, backed out my changes, and then worked on a separate entity that only blocks in one general direction. I’m calling them barricades in the source code, just because I haven’t used that word for other things and it’s easy to search for. It’s admittedly not a good descriptor.
This is every barricade configuration:
Top-to-bottom: floors, left-facing walls, right-facing walls, and ceilings.
Realistically, you shouldn’t ever see triangular wall barricades like the ones pictured above: pretend they’re two or three times taller. This more or less solves the shortcoming I saw in blocking actors, though I haven’t quite gotten the floor barricades to behave 100% correctly.
Build System and Preprocessor Stuff
I refactored the build system to move more heavy lifting to Lua scripts. The main script is still Python, and it still syncs directories, deletes stale files, and probes the timestamps of files to determine which need updating. However, the majority of the work is passed on to Lua scripts. Python collects info that is difficult to get from Lua’s basic runtime, compiles it into some text files, and then passes the text file paths as command line arguments to Lua, which reconstructs the lines into arrays of table structures.
I only look at Python stuff occasionally, and I keep forgetting how to do basic things in it. It’s probably for the best that I move as much as possible over to Lua. Anyways, this change came about as a result of various preprocessor experiments described below.
Global Constant Preprocessor Pass
Progress on barricade entities was slowed by a bad habit of cramming mutually exclusive state information into single fields as number values. I’ve mostly kept this habit isolated to platforming and collision detection code. For some things, I find it easier to check for the absence of several states with if self.some_mode <= 0, rather than check a bunch of booleans (or wrap those checks into a function, which may or may not be reachable from other modules that need to do similar checks, etc.) Using <=, I can also toggle a state on and off by negating the number.
As long as I kept it to about 2 or 3 states, everything was good. For whatever reason, I made the identifiers for actor hitbox subshapes numbers instead of strings. There are eight subshapes, of which I can only accurately recall the first two:
- AABB Rectangle
- Line, bottom-left to top-right
- Line, top-left to bottom-right
- Right triangle with hypotenuse facing upper-left
- Right triangle with hypotenuse facing upper-right
- Right triangle with hypotenuse facing bottom-left
- Right triangle with hypotenuse facing bottom-right
Numbers 5 to 8 are the problem here. The barricade code is a copy-pasted mishmash of other platforming logic, and there’s just no way for me to tell at a glance that these numbers are correct when they’re swimming in a big pool of other symbols.
Rather than do the sensible thing (pack the numbers into a table of soft-constants and reference the table; use strings for identifiers), I looked around for a preprocessor, and then tried rolling my own. The main keywords are !CONST to declare a constant identifier and !MACRO to create a C-style #define macro.
Short Operator Macros
After getting !CONST, !MACRO and a few other directives working, I looked into support for shortened operators. Plain Lua doesn’t support expressions like var += 1. You always have to write out var = var + 1. I know this sounds petty, but I have experienced moments where the short form would have prevented issues like this:
self.x = self.x + self.xvel
self.y = self.x + self.yvel
I got it working, then disabled it in the build script until I can figure out a way to ensure that it’s only applied to certain source files.
Working on the preprocessor and build script led me to investigate an old idea of converting TMX/TSX files to Lua source from a Lua script, instead of depending on Tiled to do the export. I have experienced some oddities with Tiled’s Lua exporter, like (silently?) assigning different tile IDs if Tiled wasn’t able to query the associated tileset’s image at time of export. For legacy reasons (if I recall correctly), it also always embeds tileset data into the map file, regardless of what you specify in the application preferences. That’s fine for a small game with only a few tilesets, but it doesn’t scale well. My workaround has been to use Tiled to export TMX to Lua, load them in a Lua script (as plain Lua tables, via require), remove the things I don’t want, and save it back out with Serpent.
I looked at some XML parser libraries for Lua, but had a hard time understanding the source, so I made the ill-advised choice of trying to roll my own. TMX files only use a small subset of XML features, so it wouldn’t be necessary to write a full parser to get what I want. I looked up the XML 1.0 specification and … couldn’t read it top-to-bottom. I referenced the Wikipedia article on XML for the most part instead. This in turn led to reading up on Unicode and UTF-8 (Wiki, RFC 3629), and making a small module to identify and convert UTF-8 code points between strings and numeric representations.
Eventually, I got it to load one of my TMX map files, parse it into a Lua nested table, and print it back out in XML form with no differences. The next step was to take that intermediate Lua table and create a version suitable for the game engine. Tiled’s Lua exporter also makes various changes from TMX that I had to mirror (since that was what the engine expected at that point in time).
After much trial and error, comparing TMX files against the exported Lua versions, reading the TMX format documentation, and hunting down each startup error, I finally got the game booting with the new export script. Reaching this point allowed me to remove Tiled from the build process, and merge various after-the-fact map format changes into the new conversion script.
The last DIWhy thing I did this month was write a table serializer for outputting the finalized maps and tilesets to disk. I was using Serpent for this purpose, and it works, but I wanted to make some changes to the formatting, and I had difficulty following Serpent’s source code. (In hindsight, the format function may have covered what I wanted.)
The serializer writes non-table fields first, and then all table fields afterwards. Within each group, the default order for keys is false, true, <numbers ascending>, <strings 0-9 a-z A-Z>. You can attach a special formatting table to anchor certain fields to the top of the list (though tables will still be written below all non-table fields.) This way, I can ensure that the map entity’s name always appears at the top of the list, and that commonly paired things like width and height are always written close together.
Experimenting with spark particles.
Looked into LÖVE’s particle system, and found that it’s capable of applying sprite-sheet animations to particles by sending a list of quads to the object. This means it will work with a texture atlas as well. It’s also much faster than my “general-purpose sprite” actors doing comparable things. My only concern is there is no way to save and restore the state of a ParticleSystem’s particles, which could lead to some weird-looking scenes when loading a game from a save file. I considered modifying the source to experiment with that, and also to set quad ranges for the emitter (to be copied to particles as they’re made), but I would have to get a lot more comfortable with C++ and there are other things I should be focusing on right now. But maybe sometime in the future?
On the 27th, I started (yet another) refactor of the platforming toolkit, and a bunch of stuff is malfunctioning again. Hooray. The addition of barricade code has made things too complicated and difficult to follow. I need to try and unify some of the behavior between tilemapped obstacles and actor-based obstacles. I’m also considering a fixed-point coordinate system to get around floating point error accumulation, but I don’t know if I’ll follow through on that threat, as it would be a huge change that touches everything that deals with object coordinates.
Hopefully I turn this around and get back to doing real work by the end of March.
Codebase Issue Tickets: 52
Total LOC: 105491
Core LOC: 35881
Estimated Play-through Time: N/A (Platforming engine is busted while I rewrite it)