Testing actor rotation, which probably won’t be used much, but LÖVE has good support for it so I might as well add it to the engine.
Work Since Last Devlog
Audio
Fixed an intermittent issue with sounds being cut off. I didn’t really notice the problem until I added a loud explosion sound to the game which sometimes wouldn’t play. I thought it was likely a problem with my doubled sound prevention, but it turned out that the code to turn off the channel’s previously-associated pooled source was stopping sources that no longer belonged to it:
-- Old line if old_name ~= source_name or old_index ~= pool_index then -- New line if channel.playing and old_name ~= source_name or pool_index ~= source_index then
Graphics
Did some cleanup of the graphics module, and set the origin for quads correctly so that sprites can now rotate around their center point instead of their upper-left corners. This turned out to be pretty easy, and I think I just misunderstood the parameters for love.graphics.draw() earlier on. For this game, I’d only be interested in 90-degree sprite rotations that don’t mangle the pixel art, but it’s good to set it up correctly for future work.
Added a couple of camera shake effects: one “heavy” shake that offsets the screen vertically a few times, and one “buzzy” rumble that sifts the screen horizontally at a faster rate. I linked the former to explosions, and the latter would probably be good for heavy mechanical objects moving around. Both shaking effects are deterministic and don’t use calls to random().
Performance
I was able to speed up tilemap drawing by making some changes to the nested loop:
- Convert as many table lookups to local values as possible
- Use an alternate, less safe version of the map.getCell() function
- If the current tile ID is 1 (“nothing”), then skip the rest of that loop iteration with goto
- I left a love.graphics.setColor() call in the inner nested loop by mistake. Pulled it out of both loops as it’s not necessary to run it on every single tile.
In some cases (partly depending on how many “nothing” tiles are present), the average draw time for the tilemap was cut in half.
I made some similar adjustments to the actor drawing function, though I’m not sure how much of a difference it made. Earlier this week, I copied the default love.run into the engine codebase so that I could tweak how the main loop works, and tried replacing all of the function calls within the main loop with locals. These are just micro-optimizations and I should stop.
Incidentally, it looks like this is where the 1000 FPS cap is defined:
if love.timer then love.timer.sleep(0.001) end
Input
Working on improving input reliability under low framerate conditions. While playing around with higher love.timer.sleep() values, I noticed that input gets dropped when a press and release happens within the span of one frame. This is because the keypressed() and keyreleased() callbacks are both processed before the game logic is run.
This isn’t a problem under normal circumstances because one frame is extremely fast, like 16.7 milliseconds when synced to a 60Hz monitor. But the lower the framerate, the more potential there is for dropped input. Maybe the game messed up and spawned a thousand objects, or your computer decided it’s a good time to do some disk-intensive housecleaning in the background.
My first thought was to defer all key-release processing to the end of the frame. This allowed key-press events to be picked up when both a press and a release happened in the same frame. However, this also caused all key-release events to lag by one frame.
Next attempt: selectively defer only the key-release events which were preceded by key-presses within the same frame. So if only a key-up is received, process it right away, and if a key-down followed by a key-up happens, defer the key-up. This worked better, but broke in situations where the user presses, releases, and then presses again, all in the span of one frame. The deferred release would overwrite the second press.
To fix that: When processing a keypress, if any deferred releases are logged in a given frame (which should be super, super rare), then iterate through the entire defer list and erase any instances of this key. This resolves the ‘press->release->press == release’ issue.
This has an unintended interaction with directional movement controls: in the span of one frame, pressing in one direction and then correcting in the opposite direction causes both directions to be triggered. Higher up in the game logic, pressing opposing directions at the same time causes them to cancel each other out. For example, when holding both right and left, the player doesn’t move. So there could be situations where a user needs to change direction quickly, and some kind of frame stutter combined with these deferred releases causes a delay in the player character actually turning around. I think this shouldn’t be a huge issue for this game, but it’s something that I’ll have to come back to and reevaluate, and fully resolving it will require more substantial changes to the input system. For now, I’ve made it an option that can be disabled.
Other Bugs
Fixed some session-related issues:
- By design, the game briefly slows down when the player or a boss is defeated. If the user paused and quit back to the title screen during this moment, the game would be stuck in slow motion until another player or boss actor is defeated.
- When at zero lives, if the boss defeats the player, but the player’s shot also posthumously defeats the boss in turn, then both the game over state would trigger and the next level map would load, causing a the game over menu to be active while the player is also able to move.
Thoughts
I made a Windows build and tried it out on a more powerful PC than the one I normally use for testing. It seemed to run pretty well. I haven’t built for Windows on this branch of the engine since probably April.
Additionally, I upgraded my LÖVE installation from 11.2 to 11.3 so that I can check out some new features:
- love.window.getSafeArea: I was hoping this would detect anchored OS widgets like the upper menu bar and the shortcut dock, but it seems to just return the window dimensions. I guess this might be intended for mobile / touchscreen devices, not desktop. I’d really like a way to determine the unobstructed area on the display so that I can set the initial window size to an integral scale that fits, without overlapping OS UI elements (which appear in front of application windows.) Based on some forum posts, I learned that it’s possible to do this indirectly by first setting the window size to resizable, maximizing it, grabbing the current window dimensions, and then using that as the unobstructed screen size. Strangely this works in Windows 10, but in Fedora 31, it just gives the full display dimensions. Though if I wait a few frames between maximizing the window and getting the window dimensions, I do get the right numbers. I dunno.
- love.window.setVSync: Lets you change the VSync setting without having to recreate the window.
Spent some time trying to clean up the engine core directory, adding subfolders to keep things like graphics modules together.
Plans For Next Post
- I keep writing that I need to add more game content but I seem to keep getting pulled into bug hunts. Oh well. Same as before: fill out levels, add more creatures.