😮
Some brief footage. No new content to show this month. I took some time to reflect on the state of the project, and ended up making a lot of cuts to the engine and changes to the drawing code.
November Summary:
- Removed multiple rooms per scene context (as in, more than one room operating simultaneously)
- Hard-coded tile dimensions to 16×16
- Changed hitboxes to store half-width and half-height instead of full dimensions
- Added support for background matte changes when transferring rooms, including a ‘wipe’ effect implemented with scissors
- Changed the renderer to support drawing pixel art “off-grid”, but still floored to the nearest pixel/texel when the canvas is scaled greater than 1x
- Reimplemented canvas scaling to (eventually) allow more control over interpolation and cropping
- Removed “pixel integral” deferred motion for actors
- Removed “wobble” and “mirage” vertex shader effects
Removal of multi-rooms
Recently, I cut down on the number of rooms that are active in a level, and I added a managed transfer sequence to travel between collections of rooms. After some consideration, I’ve gone one step further and completely removed support for multiple active rooms / tilemaps per scene context. I added this back in May 2020, hoping that it would allow for large, dynamic levels with seamless setup and teardown of map chunks. Since then, it’s become apparent that smaller, tighter areas with managed transitions are a better fit for this project.
I still saw multiple overlapping rooms as a good means of implementing stage gimmicks such as collapsing ceilings and very large vehicles. These kinds of stage gimmicks may still be possible using blocking actors, which are basically movable objects that behave like solid terrain. The potential benefits of multiple rooms were outweighed by the complexity they added to working with maps, map metadata, collision and physics. It’s so, so much easier to deal with one tilemap anchored in one spot than it is multiple tilemaps that could be anywhere in world-space.
It’s been a couple of weeks since I made this change, and so far I don’t regret it.
Splitting room data and visual representation
To help support room transfers, I split the graphical state of rooms into a separate structure called a visroom. While the engine no longer supports multiple rooms per scene, it does have two visrooms: one representing the current room, and an auxiliary visroom to represent the previous room for the duration of the room transfer.
Map and Room format changes
Before this month, the first step to creating a room was to fully deep-copy a map structure. This worked, but it did create many copies of tables which are almost always treated as read-only. I’ve reworked it so that you can create a room from scratch in-engine, and I wrote a function to apply a map to a room which copies references for tables where that makes sense.
Collision
Hard-coded 16×16 tiles
I hope I don’t end up regretting this, but lately I haven’t encountered any scenarios where tile sizes other than 16×16 pixel-units make sense for this game. Previously, I thought that 32×32 tiles might be good for reducing overhead when drawing background layers, but that was before I switched tilemap rendering to use LÖVE SpriteBatches. Additionally, the player code assumes 16×16 tiles as part of collision detection for hangbars and some other terrain interactions, so it isn’t like I could throw in some 32×32 stage tiles without causing issues anyways.
Halved Hitbox Dimensions
Reviewing the codebase, hitbox width and height were almost always written in conjunction with a ‘*0.5’ to cut them in half, like ‘left = center_x – width*0.5’. Might as well skip that step and just store them as halved in the first place.
This does cause some awkwardness in situations where the full width and/or height is actually desired, in which case it has to be ‘width*2’. It’s also confusing when hitbox dimensions are mixed in with other rectangles which are not halved, such as room geometry or the viewport.
Room Transition Improvements
I rearranged the background matte maps so that they are all rooms belonging to one world structure. This allows switching background maps (as rooms) without having to load a new world (and resetting the scene state) each time. And because they’re rooms, and the background scene has two of its own visrooms, that means it’s now possible to transition between backgrounds while also switching between rooms in the main action scene.
I used scissors to implement a wipe effect between the old and new background visrooms. I also added scissor cropping to the action scene visrooms. As a result, I no longer have to use special invisible terrain to terminate room exits, and it’s a little less jarring if the edge tiles of two adjoining rooms don’t fully match up.
I’m having difficulty demonstrating this in-engine, so here is a mockup GIF of the effect. In practice, there would be some kind of visual separator between the matte split (like a large pillar or something) so that it doesn’t look freaky.
“Off-The-Grid” Pixel Rendering Support
Several overlapping sprites rendered “out of alignment.”
I added an integral scale variable for when creating the main display canvas. This allows the canvas texture to be 2x, 3x, 4x, etc., larger than the base resolution of 480×270. Pixel art rendered to the canvas is scaled up by this factor, and allowed to be drawn such that the artwork’s pixels don’t have to align to a pixel grid of the same size (or rather, they are aligned to the texture’s internal pixel grid.)
Retro gaming purists tend to despise this, and I agree that it’s inauthentic and can look downright hideous when several sprites are overlapping each other very closely, or when two dithering patterns overlap. The reason for trying this at all was that I noticed a slight stutter to the scrolling when the player walks at full speed through a room. Investigating this revealed that due to the walk speed I chose for the player, he usually crosses one pixel-column boundary per tick, but occasionally it’s two. This happens many times per second, and so everything kind of jerks as it scrolls.
An exaggerated example.
(You may be wondering why I didn’t notice this sooner. I kind of just assumed there was something up with OS + GPU combo that I could eventually remedy by getting some new hardware or whatever. Watching the player move in slow motion made it clear that this is an issue caused by my previous design choices.)
I could change the max walk speed to only cross a fixed number of pixel columns per tick, but other mechanisms could cause similar issues, like standing on a platform that moves at a slightly slower speed. The issue is exacerbated further by the game’s internal simulation tick rate (256 Hz) not being a multiple of common monitor fixed refresh rates. It could be further impacted if a user sets a different time-scale to make the game run faster or slower: a timescale of 0.75 leads to a tick rate of 320 Hz. By increasing the number of positions that a sprite or background can be rendered at, the stuttering, though still present, can be masked a bit by the higher granularity of positions. 2x (960×540) is not very helpful, but 3x (1440×810) looks a lot smoother to my eyes.
What other downsides are there? The code I wrote to shift platform cargo by per-pixel amounts had to be scrapped, because what is “per-pixel” now depends on the canvas scale, and implementing different physics behavior as a result of display settings is just asking for trouble.
Base Resolution Woes
My previous game used a base resolution of 384×216. For this game, I upped it to 480×270 because it fit into 1920×1080 at 4x and 1440×900 with letterboxing at 3x. I didn’t consider that it scales poorly for 1366×768 or 1280×720. It may help slightly to scale the canvas by an integral amount and then upscale that to best-fit with interpolation — surely that would be better than just upscaling 480×270 directly.
Many retro games use a base vertical resolution of 240 so that they can at least scale cleanly to 720p. If I were starting from scratch, I would probably consider that. As it currently stands, too much existing gameplay behavior is designed for a base resolution of 480×270. Oh well.
Pixel Art Antialiasing
I looked into pixel antialiasing shaders, and started integrating this one by RNavega which only interpolates at borders between texels. I hit a wall when it came to rendering text, though, as the shader needs the texture resolution passed in as a uniform, and I wasn’t sure if that’s something you can get from a LÖVE font object. At some point, I had read that LÖVE fonts use an internal texture atlas which rearranges the glyphs, so I didn’t think the ImageFont texture’s dimensions would be relevant info at the time of drawing.
After some experimentation, I did find that it’s possible to get the texture dimensions of an ImageFont within the shader using textureSize(). GLSL 3 is required to access this, so I had to add #pragma language glsl3 to the top of the shader. I could not get this working on LÖVE’s default TrueType font, Vera Sans, so I assume this only works with ImageFonts.
Ultimately, I reverted the changes and went back to floored positioning on a canvas with integral scaling. The drawing code at this point uses scissor boxes for cropping sprites, and scissor coordinates work on the basis of screen pixels, disregarding the current state of the coordinate system. The smooth interpolated borders of sprites would clash against the scissor edges. That said, this shader would be of tremendous help in a game that zooms and rotates pixel art sprites.
Wobble and Mirage
For lack of better terms, these are what I called the vertex shader effects which provided scanline interrupt-like wavy background deformations. I removed them because they looked a little weird at higher resolutions. I don’t think I implemented them properly anyways, because seams and weird pixel discrepancies (likely due to rounding error) were sometimes present.
Bug of the Month
Flagging a map’s background layer to be removed during the build process would cause the player and other actors to fall through the ground at run-time.
Map layers can have a _remove_on_build property set so that they don’t feature in the final build. This is helpful because of how I have background mattes implemented: the matte is a totally different room, which just happens to be rendered behind the active room. Tiled has no knowledge of this relationship, and therefore is not able to display it that way (which is fine.) So a simple stand-in for the matte can give a better idea of how the map is going to look in-game while editing.
Anyways, I was totally surprised when I created a new map for doing sprite tests, and the player just fell through the floor. Warping to one of the main prototype levels, everything worked OK. Warping to another test level, it broke. This new map was a copy of an existing test file, which had ‘_remove_on_build’ checked for the unused background. Luckily, one of the first things I did was uncheck this and rebuild the game. Collision worked! If I hadn’t done that, I likely would have spent a lot more time feeling around in the dark.
Investigating further, I found that the room’s tilemap layers were out of order. Everything looked OK to the naked eye, but in reality, the stage tilemap had been shuffled to the slot reserved for the background. That’s why nothing collided. Backtracking through the map init code, I found that this mix-up happened before the game boots up. Even the Lua map file for this room had the layers out of order. But in the build system, the map conversion function had the correct layer order from start to finish. What?
The problem was the step in-between, when a converted map table is written out as a string and saved to disk. I had written a sorting function for hash tables which arranges keys in a specific order: booleans, numbers, strings. This function should only apply to hash tables. It was also being applied to arrays, and somehow operating on values instead of keys!
This wouldn’t be noticed if an array had only one data type, like if every value was a table. Map layer tables can have mixed tables and boolean false, though: the latter indicates that the layer isn’t populated. 1 is the stage, 2 is the background, and 3 is the foreground, so this test map’s layer table should have looked like this:
t.layers = { [1] = table: 0x4103f840 [2] = false [3] = table: 0x4103f868 }
It was being sorted into this:
t.layers = { [1] = false [2] = table: 0x4103f840 [3] = table: 0x4103f868 }
The fix was to check if the table contains only array entries (1..n with no gaps), and if so, just return the current order instead of attempting to sort them.
Project Outlook
So I had a weird PC issue this month where all USB ports stopped working, even after rebooting. My motherboard had PS/2 ports for mouse and keyboard, so I was still able to interact with the system, but no backup drives would connect. (Oh boy.) This went from bad to worse when I attempted to upgrade my OS. It got stuck at 92% completion for an hour, and (oh boy) I hit the reset button. Every subsequent boot attempt failed with this ambiguous error message: Oh no! Something has gone wrong.
I reset the BIOS by pulling the battery out of the motherboard for a few minutes (couldn’t remember the BIOS password (OH BOY)), and then I did a fresh install of Fedora 35 using an ISO burned to a DVD-R. Miraculously, it seems to be working now, and I haven’t lost any progress since I keep most of my work on a separate drive from the OS. It took a while to get my applications set up again, and whatever browser bookmarks and tabs I had accumulated are gone.
I have no idea what caused this to happen, and there isn’t much I can do except wait and see if it reoccurs. I’ve had occasional issues with my mouse and keyboard not responding (through USB), but it would always clear up after a reboot. This happened near the beginning of the month, and I haven’t had any issues since then. Fingers crossed.
Saved by PS/2 ports and a DVD-R drive in 2021… hopefully I’ll post again near the end of December. I schedule “dead man’s switch” posts every six months on this blog. If you see such a message in January 1st 2022, it’s probably just that my computer screwed up again.
Stats
Codebase Issue Tickets: 45 (+1)
Total LOC: 121777 (-1489)
Core LOC: 35914 (-183)
Estimated Play-through Time: 11:09.46 (+0:00)*
*: Virtually nothing content-wise has changed, so I just copied the time from October.
(e 31/Dec/2021: Punctuation)