Getting some non-Balleg stuff out of the way: I converted Fog Machine and Task Blog from WordPress instances to static HTML pages. They look bad on mobile (or on desktop with a narrow browser window) and will require some more CSS tweaking. I removed the landing page links for both, but they can still be accessed through this blog’s post list page, under Archived Blogs.
Testing multiple hitboxes attached to the player.
Here is some gameplay footage. I’m going to try and capture a video every month or so.
Last devlog, I mentioned wanting to make some more stage content and to revisit actor state management / state machines. Oops, I did neither. They’ll have to wait until June. Instead, I dealt with some collision detection issues that have been bugging me since January. This in turn lead to changes in how opponents respond to the player’s shot, plus some additional collision subshapes, and (the big one) support for multiple hitboxes per actor.
Some background: I planned to make large creatures and devices out of multiple actors. Internally, they are called ‘gacs’ or Grouped Actor Contraptions. In a gac, one actor serves as the parent or root entity. The parent spawns additional child actors during startup, which are “pinned” such that they are continuously positioned relative to the parent actor with arbitrary offsets.
I’ve had difficulties syncing up the individual actors which comprise a gac. I’ve also had trouble dealing with how the player’s shot should react when overlapping two actors belonging to one gac at the same time, when the actors have conflicting attributes. First, I looked into support for multiple hitboxes per actor. I worked on it for a day or so, got spooked by the amount of changes necessary, and backed out my work. I then tried a different approach: adding more subshapes to the hitbox system. While this fixed a few issues with specific gac designs, it didn’t go far enough. I returned to the idea of multi-hitboxes again, this time with a few constraints to help it fit into the existing codebase.
After backing out of multi-hitboxes, I began sketching out some ideas for new subshapes. Most of these didn’t pan out, being too complicated for me to fit into the existing code, but the few that did make it in ought to be useful in a few select cases.
The new subshapes as shown in the diagram below are: half-circles, quarter-circles, rhombuses (rhombi?), and half-rhombuses. The circles can’t be stretched into ellipses, but a half-rhombus can be stretched into an equilateral triangle.
The new subshapes.
To prevent the collision handler from ballooning out of control with shape-to-shape combinations, the partial shapes recycle the code and classifications for the full versions of those shapes. The rhombus handler piggybacks off of the right triangle code. As a side effect, this means that the “cut” sides of shapes can’t be relied upon for collision purposes: any actors colliding against those sides will also intersect with a “phantom” full version of the shape. I’m OK with placing those sides against walls.
Rewriting opponent-shot bounce response
I rewrote the code that handles the player’s shot rebounding off of rectangular enemies. It had some weird behavior, as described in a devlog post last November (under the heading Opponent Hit Response — the second diagram in particular.)
There are two parts to this: rebounding a shot which has just collided, and pushing a shot out if it happens to be stuck within a shape.
Part 1: Rebounds
Before, there was a bias towards horizontal bouncing when the shot hit a corner under these conditions:
- Shot is moving left, hits bottom-right or upper-right corner
- Shot is moving right, hits bottom-left or upper-left corner
The intent was to promote horizontal responses. Playtesting showed that this behavior doesn’t really make sense in-game, so I rewrote it to compare the closest XY coordinate on the shot hitbox against triangular or trapezoidal portions of the hitbox.
Top: Old shot response zones, with an odd special case for the side “facing” the shot. Bottom: New shot response zones.
Part 2: Push-Out
There was no part 2 to this before. A minimum bounce velocity was enforced, but since the shot response only kicked in when an enemy was not in a flinching state, there were opportunities for the shot to hit an enemy, hit something else, and then soar back into the now-flinching enemy before registering another hit. In the worst case, the shot could remain within the enemy, mostly stationary, while the enemy takes hit after hit.
New behavior: opponents monitor how many ticks they’ve been overlapping with the player’s shot. If they are not flinching, then they execute the rebound code. If they are flinching, and the shot has been overlapping them for at least one tick, then they execute the push-out code.
This worked nicely in some cases but not others, so I made it a configurable setting in the opponent behavior state.
Hurt While Flinching
Using the overlap check field, I made it possible to hurt enemies even while they’re flinching, so long as the shot has exited the opponent body for at least one tick. This feels better with stationary or very slow enemies — bouncing a shot between enemy and wall can inflict damage very quickly — and not so good with fast-moving enemies. In particular, jumping or flying actors can continuously bump into the shot so quickly that it’s not clear that they even took multiple hits.
Multiple Hitboxes Per Actor
The new subshapes didn’t really solve my collision issues, so I took another crack at multiple hitboxes, this time with a few constraints to help ease it into the existing codebase:
- Hitboxes belonging to the same actor cannot collide with each other
- Only hitbox #1 is eligible for platforming logic
- For platforming, hitbox #1 is always considered a rectangle, regardless of the shape ID tag
- For platforming, hitbox #1 is always treated as being centered on the actor’s XY position.
This mirrors the old behavior, where an implicit “environment box” was defined by an actor’s width and height fields, and always centered on the actor’s XY position.
Blocks (bodies which behave like solid terrain, pushing actors out of the way) have gone from per-actor to per-hitbox. An actor can have multiple blocking hitboxes now. This should simplify the design of platforming devices, such as elevators with walls and ceilings.
Hitboxes contain their own arbitrary state tables, to hold attributes along the lines of “this hitbox hurts the player on contact” which were originally housed in the actor’s arbitrary data table.
I wrote two features to reduce ambiguity when the player’s shot overlaps multiple hitboxes with conflicting properties:
- A hitbox has four ‘edge’ flags. When false, they cause the colliding hitbox to be clipped on the opposite side during collision tests. For example, if a square hitbox had edge_l set to false, then during a comparison against the player’s shot hitbox, the latter would be halved in size horizontally, with its right side resting where its center would be under normal circumstances.
- Blocking actor hitboxes are not eligible for this because their hit detection code is separate from the normal actor-to-actor collision checks.
- Each hitbox has a ‘group’ ID tag, and only one collision between one actor+group against another actor+group is counted per tick.
- Blocking collisions reposition the colliding actor, but will respect the group tags when it comes to generating a collision event.
An example of how the edge rules work. A box’s clipped edge tags are applied to other boxes during collision checks.
These features should make synchronization easier and remove some ambiguity when parsing collisions.
Multi-hitboxes came at the price of having to rewrite portions of every actor definition, and some pretty heavy rewriting of the platforming and player code. Although it wasn’t as bad as the pseudo fixed-point coordinate system I added in March and April (which also required modifying every actor), it’s still the kind of thing that really ought to be done at the beginning of a fresh project.
This experience very much makes me want to investigate better actor state management, since so many of the project’s enemies could more or less be implemented as a single actor type with different initialization and state logic. In fact, while going through each actor file, I did replace most of the individual bullet actors with a single, general-purpose projectile actor.
I’m also ready, and very willing, to remove the inter-group infighting between different types of enemy. I no longer see a need for enemies to target anything other than the player, and maybe the player’s shot, so the target-hunting code can probably be dropped out of the project as well.
That’s all I have for now. I’ll write another update around the end of June, probably.
No real content additions since last time.
Codebase Issue Tickets: 45
Total LOC: 110500
Core LOC: 38596
Estimated Play-through Time: 4:03.26