Bolero Devlog (September 2020)

Experimenting with a rolling state for the player’s shot.

I spent the first part of this month working on sprite display features. Then I replaced a lot of hard-coded “opponent behavior” stuff (taking damage, what audiovisual effects to execute when defeated, etc.) with modular equivalents. I cranked through some prototype object designs, and then I had a crisis about whether the player’s sole offensive capability is actually good enough to carry a whole game. This lead to me tearing apart my platformer physics code and trying to make it work better, and then splitting the player’s shot behavior into separate bouncing and rolling states. I spent the last bit of the month attempting to reconcile these two states, and determine how to transition between them in a way that’s intuitive to the end user.

Sprite Stuff

9-Slice Sprites

9-slice sprite test.

I experimented with 9-slice scaling for actors. 9-slicing divides a texture into a 3×3 grid. The corners of the texture remain at 1.0 scale, and the other sections stretch to fill out the remaining space. While this is most often used for UI elements, like informational windows with variable dimensions, it could also be helpful for making platforms and barriers of varying sizes without having to draw each variation.

This is currently implemented as nine sprites attached to one actor, with each sprite positioned and scaled according to which part of the 3×3 grid they represent. The middle cell should contain a solid color, and the non-corner edge cells (‘2’, ‘4’, ‘6’ and ‘8’ in the screenshot above) should contain very simple patterns that look the same at any scale. I also made 3×1, 1×3, 2×1, and 1×2 variations.

It’d probably be better to implement this at a lower level, maybe directly within the sprite instance table, but I don’t imagine it will be used for more than 1% of all in-game entities. Handling it at a higher level also means that all nine sprites can be managed and manipulated independently.

Animation-to-direction mapping

Top: A chain of interconnected rotating sprites. Bottom: The spritesheet.

I’m trying to avoid non-90 degree sprite rotations as a stylistic choice, but I do have plans for entities that rotate by paging through several sprite frames. Up to this point, I haven’t had an easy way of mapping specific animations to directions (since it’s a platformer, almost everything just flips their sprite horizontally when turning around), so I wrote a module to deal with that. The two use cases for this are metallic rods that connect machine parts together, and homing missile projectiles.

The module lets you create mapping tables and assign them an arbitrary number of directions (power of 2 numbers recommended.) Each direction entry stores an animation name and horizontal + vertical flipping parameters. The bigger the object, the more directions you need to make the rotation look reasonably smooth.

I ended up writing helper functions to take one quadrant worth of definitions, and apply them to the whole range with flipping. Later, I needed to do this with two quadrants, so that sprites with a consistent upper light source don’t look weird when flipped vertically. I also experimented with a 64-direction map table… that’s way too much, even if duplicating one quadrant to produce all mappings.

Definitely don’t bother with this if you can get away with just rotating the sprite in the renderer coordinate system.

Opponent Damage Handler

I made a generic handler for enemies which manages collisions against the player’s shot. Up to this point, I had been copy-pasting the logic for this within each actor tick() callback. That wasn’t going to scale. Even though most of the response code is wrapped in other functions (‘receive damage’, ‘make guts’, etc.), it really sucks to try and make broad changes to how player-to-opponent damage works when you have to inspect copy-pasted code across dozens of actor types.

The handler deals with things like:

  • Should the opponent take damage, and how much?
  • Should the opponent ‘flinch’? (flash and have temporary invulnerability)
  • Should the opponent destroy the colliding shot?
  • When the actor is defeated:
    • What sprite should we show, and what sound should we play, if any?
    • Is there a custom callback function for additional defeat logic?

 

Defeat Tables

Defeat tables fulfil the last bit about managing what happens when an enemy bites the dust. They’re only referenced when a defeat event occurs, which is to say that an opponent has been decisively removed from the scene for higher-level gameplay reasons. The player shot them, they fell on some spikes, stuff like that. This is different from just removing an actor from a scene. We may remove several actors as part of a room being unloaded due to being too far away from the player, and we don’t want all of these actors to play the ‘ouch, you got me!’  sound effects every time that happens.

When I started looking at this, I was stuck on the notion of assigning a rank or insignia value to opponents based on their strength and importance (something like weak_enemy, strong_enemy, mini_boss, boss, final_boss.) The rank would be used as the basis for the effects used when defeating the enemy. In the end, I decided to make it more free-form, as I imagine end-of-stage bosses may make reappearances as mini-bosses later on. Every actor can have a per-kind defeat table, and an override defeat table assigned at run-time, and if neither are defined, then they fall back to a generic defeat table with an itty-bitty explosion graphic instead.

I regret not getting this under control before designing a ton of prototype enemies.

 

Platformer Movement Work

Overlap Issue with Blocking Actors

So I ran into some issues with the blocking actor code I wrote last month. I wanted to make a ‘falling stalactite’ object with the following properties:

  • It’s an obstacle that blocks the player’s movement
  • When hit by the player’s shot, it disconnects from the ceiling and falls to the ground, creating a step that can be used to reach other surfaces
  • When it’s falling, it’s hazardous to the player

I set up the actor file and created three states: 1: idle, 2: falling, 3: grounded. But when I fired a shot at the stalactite (which is really just a rectangle right now), it wouldn’t react. The shot just rebounded as if it hit a wall.

This ended up being an ordering problem:

  • START OF TICK
    • (a) Clear last frame’s overlap info
    • (b) Update actor overlap info in a loop
    • (c) Process actor logic in the order that they were added to the scene.
      • (c.2) If the actor has the platformer service installed:
        • Move horizontal
        • Correct H position against blocking actors
        • Correct H position against tilemap terrain
        • Move vertical
        • Correct V position against blocking actors
        • Correct V position against tilemap terrain
    • (… Rest of scene processing …)
  • END OF TICK

 

The issue is that (c.2) moves actors, detects overlapping against blocking actors, and corrects positions while (b) never gets a chance to detect them. I can’t add new collision events at (c.2) because one of the actors involved may have already been processed by the scene run(), and the event will be discarded on the start of the next tick.

To fix this, I had to change the order in which tasks are carried out:

  • START OF TICK
    • (a) Clear last frame’s overlap info
    • (b) In a loop, handle movement for all actors with the platformer service
      • Move horizontal
      • Correct H position against blocking actors + make overlap events
      • Correct H position against tilemap terrain
      • Move vertical
      • Correct V position against blocking actors + make overlap events
      • Correct V position against tilemap terrain
    • (c) Update actor overlap info, appending to event lists created during (b)
    • (d) Process actor logic in the order that they were added to the scene.
    • (… Rest of scene processing …)
  • END OF TICK

 

This way, all instances of two actors overlapping get events. I have to make improvements to the scene structure / class to better accommodate things like this, as I’m sure it won’t be the last such issue.

 

Angled Bounce Reactions

The ‘player shot’ actor contained some slapped-together code for bouncing off of angled surfaces. I’ve wanted other actors to be able to bounce off slopes in the same way for a while, so I pulled it out of this actor and worked it into the platformer toolkit, spending some time ironing out inconsistencies in how flat and angled surfaces are handled.

 

Slope Anchoring Rewrite

I broke something, somewhere, and ended up with some pretty bad slope placement bugs. (Actors teleporting one tile into the ground, that kind of thing.) The code to handle slopes was over-engineered and difficult to follow, so it seemed worth doing a refactor.

During the slope detection pass, it basically collected tile metadata at three coordinates relative to the actor: ABC (“above bottom-center”), BC (“bottom-center”) and BBC (“Below bottom-center”). Then it ran code for any coordinate which returned a tile with an ‘is_slope’ attribute. The reason for doing it this way was that I had been tearing my hair out trying to fix placement bugs, and this was a way to catch very specific scenarios where an actor would disconnect or fall through. The code to handle each of the three points didn’t use the same logic, and I think there was a possibility for BC and BBC to point to the same tile, resulting in different logic getting executed against the same tile, one after another.

I ended up tearing the whole section apart, and the code now calls a function to pick one tile from one of those three coordinates and return its metadata. It prioritizes BC, then BBC, then ABC, and it will only return one of those, or none if there weren’t any slope tiles among them. The logic to handle slopes is now the same for tiles pulled from any of those coordinates.

Code for angled ceilings and fall-through platforms had similar issues, so I cleaned those up as well.

“Rolling” / “Bouncing” State Transitions

I haven’t been satisfied with how the player’s shot behaves when it’s only barely skipping across the floor. It tap-tap-taps and quickly disappears. This doesn’t happen most of the time, but when it does, it looks weird and feels like the game bugged out. I gutted the shot’s tick() code and rewrote it with two states: one for being mid-air, bouncing against surfaces, and a separate state for rolling across floors. I’m experimenting with making shots launched while crouching always begin in the rolling state.

The two states on their own more or less work now, but choosing the rules for how to transition back and forth has been tricky. This is what I have right now:

  • BOUNCING
    • When colliding with the ground:
      • Is shot velocity adjacent to surface normal less than min_vel? (a)
        • Yes: Switch to rolling
        • No: Bounce off surface and increment damage counter
  • ROLLING
    • No longer on ground?
      • Yes: Switch to bouncing
    • Collided with wall?
      • Yes: Rebound horizontally and increment damage counter

 

(I’m pretty bad with math, so I’m going to leave some comments on (a) here. This is calculated by treating the shot’s movement vector and the surface normal as parts of a right triangle, and finding the length of the adjacent side. The shot vector magnitude is the length of the hypotenuse, and the acute angle is the angle between the shot vector and the surface normal. Multiplying hypotenuse by math.cos(acute_angle) gives the adjacent length. So if adjacent length is less than min_vel, then switch to rolling.)

I was hoping to get this all sorted out by the time I published this devlog. I want to avoid the tap-tap-tap of the shot running out of vertical momentum, but I can’t set the bounce-to-roll threshold too high, or else it will interfere with the player’s typical attack scenarios. How the rolling action fits into the game also isn’t fully clear in my mind. The player can only have one shot at a time. Bouncing shots, in general, are removed after four wall impacts. If the sphere can roll to a stop, then it has to eventually be removed somehow so that new shots can be made. What if a rolling shot gets stuck at the bottom of a V-shaped indentation, and it just keeps rolling back and forth? Lots of scenarios to think through.

 

Miscellaneous

I restructured my test maps to host content roughly by topic, and not just whatever I tossed in at the time that I made them. I need to improve the quality of my prototype designs, and getting the existing content organized was a step towards that goal.

Tiled Lua Export Woes

I had a weird issue with tilesets. Tiled’s Lua exporter can assign incorrect ’tilecount’ and ‘firstgid’ values if, for whatever reason, it’s not able to read the associated tileset image files at time of export. Exporting through the GUI would give me the correct values, while exporting through CLI would give wrong values in certain cases.

When this happens, Tiled seems to fall back to the number of tiles with ‘properties’ records, instead of (rows*columns). For example, a 32×32 tileset with 240 custom tile properties would have a tilecount of 240, not 1024. My map import function would then fail due to trying to convert tile IDs with invalid offsets.

The root cause of all this trouble was that I moved my tileset files (.tsx) to another directory to separate them from the map files (.tmx), for organizational purposes. Tiled automatically detects broken paths, and will prompt you to fix them through the GUI, so I went through every tileset and tilemap file and selected new paths. In one tileset, I somehow ended up with a huge string of “../../../../../” at the beginning of the image path. (Maybe a Linux thing I’m ignorant of? Dunno.) I cleaned up the path manually in a text editor, and all of my problems vanished.

At some point, I want to write a standalone tmx/tsx-to-Lua export tool. For now though, I just want to focus on project development.

Project Outlook

September was pretty good for code improvements. I wrote in August that I hoped I would be working on prototype level fragments by October, but that’s going to have to wait a bit longer unfortunately. The project depends on a solid attack system, so that’s where I need to put my focus. Next post will be around the start of November.

 

Stats

Codebase issue tickets:

 

Project lines of code:

 

(e 16/Oct/2020: typo)
(e 29/Oct/2020: another typo)

Leave a Comment