tower jet (dino game)

Created: Jan 23, 2023Published: Mar 28, 2023Last modified: Apr 25, 2023
Word count: 3288Backlinks: 4

A Dino game.

---

My entry for the godot wild jam #52

postmortem - godot wild jam 52

> state machines

State Machines via Beehive's Machine and State nodes are working pretty well. Perhaps there could be some clever debugging integration. Like a toggle for showing the state labels on the player.

>> beehive machines could own a debug-togglable state label

Right now I add this by hand to most of my state machines, just to be sure things are behaving as expected.

It could be added by the machine itself at 'ready'. It could also be toggled on via an interface/keybinding.

maybe a debug interface for dino?

>> Too Lax on state machine semantics?

the states are only ever supposed to move from one to the next - you're not _really_ supposed to externally kick it into a state.

I've been letting input state be handled in one place (the higher level player) and i've been machine.transit(new_state)-ing the player based on player input and on projectile collisions.

Arguably, this code should live in the states themselves, but those states end up with duplication. Ex: idle and run states both support 'fire' to fire a missle, and both should be hit by an enemy missle.

I think the duplication is inevitable in state machines, and the answer generally is to write the functions on the player (actor) itself, and provide that api to the states, so really they're just invoking existing functions and moving between states (rather than executing specific game logic). This takes a balance tho - part of the state machine benefit is shrinking the player class - if they can really offload the logic to the player class, the states will stay thin, and maybe be reusable. Thicker states (more logic defined in Run and Fall) are a nice way to split off some responsibility, however. That way, we don't care how to implement run or fall in the player class, that's handled specifically in the state code.

It's a balance that I've got a few examples of now.

I did hit a confusing bug this jam that was caused by the fall state vs the external state competing over state transitions. That's why you're not supposed to deal with those transitions outside of the states, really.

>> Confusing state machine function names

I was stuck for a few hours (tho, while also watching a movie) trying to write a new beehive machine from scratch. Ultimately, all the states were running at the same time b/c I was overwriting the wrong parent function. I'd impled the states to execute process, not _process. This was super annoying, and I wonder if there's a nice way to catch this sooner.

I could warn when the built-in _process functions are used... but I could also have made a mistake naming those methods - it was definitely a strong opinion to match the built-in but drop the underscore.

Would the built-in run every time, b/c that's how nodes just-work? Maybe a new name is forced?

I'm debating something like state_process instead.

> shaders

shaders I created and hacked on more shaders this jam. Some fails but mostly successes.

>> Shockwave Shader fail!

I'd watched a video about this way back, and I knew alot more about shaders now, so I thought this would go smoothly. However, I ended up having to drop it completely, in one of my only thats-it-i'm-taking-the-rest-of-the-night-off evenings of the jam.

Getting the wrapped shader to sort-of work was not bad - I ran into trouble trying to get it in the right location. Ultimately I think my problem was related to the camera, in-editor testing, and godot resizing in my window manager whenever I try to run it.

I ran into some similar camera trouble (that I eventually solved!) implementing the 'offscreen-indicators' a few days later. I'm hopeful that the solutions/helpers I wrote for this will play back into the shockwave shader to make a solution reasonable it simple.

>> Color reassign 1 and 2 - gray-scale art assets

I found/edited/implemented two different color reassignment shaders - one reassigns based on luminance (more than 50%? use color a. less? use color b).

The second is a better implementation I found - it takes in 5 colors to match on, and 5 colors to reassign to, so you can just pick the base colors and then adjust the assigned colors as needed in code or in the editor.

These were a big step, because it unlocked a better way to work with the art - I made a gray-scale version of my player sprite, and was able to change the colors as the player moved between different backgrounds. The jam had a 4 color limit as a wildcard, and the player became hard to see based on whatever the background was - this style shader plus some tile detection allowed me to change the player's colors (and the jetpack behavior) in the code, and without creating more art assets. Pretty cool!

>> color reassign 'window'?

I saw a screenshot of someone's game - they'd impled it all gray scale, and it looked like they had a floating window assigning the colors in the editor/at game run time. WUT? HOW? I want to follow up on this - I think it'll use similar concepts to the shockwave shader. I tried for a bit to get one going, but couldn't figure out how to get the rendered nodes affected by a color rect/texture rect. I think it needs to interact with the viewport or camera somehow?

> juice in general

Between the ludum dare and this jam, I dove into documenting Juice in Games. I'd seen and followed (and praised) these concepts before (you know, game feel ), but I hadn't quite zeroed in on it. It ended up being quite inspiring! I collected resources in the above linked note, including a youtube playlist of some of my favorites. I made a huge list of effect ideas to implement (mostly based on 'the art of screenshake'), and I went to town trying to execute as many as I could in 'gunner' before the game started.

There's another talk to find and link, one of those gdc talks with 4 different speakers, where polish is discussed. Specifically, renaming polish to 'pizzazz' to try to break away from the trends toward putting it off and thinking of it as 'extra'. Polish (juice, chunkiness, w/e) is not 'extra'! It's the fun part of the game, and in fact, the game is far less important than the juice.

Can you tell I'm getting pumped for the juice jam next week?

>> what juice is in tower?

I talk this up, but whenever I try to capture the list it feels small. Makes me feel like I should be building a feature list as I build these things. Like, what features are event supposed to be in this game?

  • screenshake on fire, on hit, on heavy landings
  • hit stop on missile hits
  • slow mo when the 2nd to last and last targets are destroyed
  • sounds for lots of things (via gdfxr) any sounds > no sounds
  • gun knockback
  • missile exploding animation
  • text effects

> camera shake/focus juicing

One of the juicing talks that sent me down this rabbit hole was focused on

camera juicing. (Math for game developers by Squirrel Eiserloh (SMU Guildhall) )

He also introduced me to Perlin Noise, which led me directly into the proc gen stuff I'll get into next.

He covers some 3d camera stuff as well, which I don't get into here.

I had to watch this video several times over a few days, but then I was able to get up and implement this in a dino addon one morning. I hope to clean that up and share the current state as a juicy-camera addon soon.

>> screenshake

Squirrel breaks down screenshake into a few components.

>>> Trauma

Trauma is a measure how much shake we want to see. It's a value between 0 and 1, and is always decreasing linearly. When you want a shake, you just add some amount to trauma (.2, .5, etc). The shake starts hard, but fades smoothly as trauma decreases.

>>> 2d shake

Translational shake - moving the camera offset on the x/y plane. (moves camera

up, down, left, right)

Rotational shake - rotating/tilting the camera itself

The direction/rotation of the shake is random, but they eventually return to zero as the trauma falls off.

>>> Noise functions

Once you have trauma and the offsets/rotations to push the camera around, a noise function gives you a better-than-raw-randomness curve.

Perlin noise and open simplex noise are examples of 'smoother' randomness functions - these are useful for creating a randomness that smoothly moves between values. A true randomness will be very jerky, but a smooth randomness is going to feel more like a person holding a camera - the 'shake' has to move between values, rather than just be reassigned to the next x/y combination.

>> points of focus and interest

Squirrel then talks about points of focus and points of interest.

>>> POFs stay in focus

Usually equally shared among the members

>>> POIs are a function of proximity and importance

You'll need some threshold of proximity * importance to determine whether we care enough to see this POI right now.

Proximity, i.e. distance. Importance, probably between 0 and 1.

>>> After that, some smoothing, and you're golden

>>> I still need to impl this properly

Right now my camera code only checks for pofs once, and sticks with them forever

(even if they die! :/)

Also, pois are ignored, but need to be factored into the centering/zooming calculations.

> camera slowmo and hitstop

I ended up implementing slow mo and hitstop/freezeframe registries the same morning as the screenshake juicing. Part of the reason is that it makes it really easy to debug screenshake implementations! Definitely recommend using that if you're trying to figure out what's going on.

The registries are kind of cool - they use the same underlying helpers, and they can be stacked. If you're in a sort-of-slowmo, then want a proper hitstop, you need to move to the hit-stop's slowmo, then back to whatever slowmo was running before. This wasn't too crazy of an implementation, but could use some testing.

> player visibility improvement

Visibility became a problem in my game (it probably often is).

>> scroll zoom

I wasn't sure what the 'normal' zoom level would be for players. Playing on the web has a fixed size, but then there's full-screen mode. Also, playing via the editor kicks the game into my window manager, which immediately tiles it, which is a weird proportion to play at.

I was able to implement a zoom in/out in several of the camera modes, which is a nice escape hatch to have going forward. It's not perfect, and for some reason scrolling on trackpads don't work (more to dig into later).

This was a nice win!

Note this also helped impl the screenshake - it's alot easier to see what's going on a different zoom levels.

>> offscreen indicators

Player visibility again - off-camera things shooting at you is not quite fair. One way to help with this is a little arrow on the edge of the screen, pointing at the targets or enemies that are over there.

This ended up being a pain - even just getting the camera's current rectangle in the coordinates of the current scene was tricky. Fortunately, I found some helpful transform code, and I can now fetch the camera's global coord rectangle and use it to place things on the edge of the screen, between the player and the offscreen entity.

The solution here might play backwards into the shockwave shader - i think the same coordinate systems might be at play.

These are a bit static/too subtle for my taste at the moment - I'd love to give them more juice, if possible.

> ensure_camera()

One more camera detail - a camera autoload that supports ensure_camera() was a really nice step, because now whenever I write a Gym or run a scene, the player fo-sho has a camera attached. No more adding the player AND the camera to scenes.

Maybe I could add an interface for adding nodes to scenes! Then I don't have to add the player at all in the editor, just do it after it's started up, if necessary. hmmmmmm debug interface?

> procedural generation

This was the heart of the jam for me, and the biggest time-sink. I got some nice abstractions out of it - Reptile (a dino addon) started to grow.

>> Reptile autoload

pure helpers! generating noise images based on inputs, and helpers for working with tilemaps Reptile.all_coords(t).

>>> isn't this just t.get_used_cells() ?

>> ReptileRoom and ReptileGroup

I don't like the 'group' naming yet.

ReptileRooms generate a set of tilemaps based on open simplex noise inputs and relative luminance values from the image. One or more groups can be defined and given a tilemap, which is drawn based on the image. The groups have upper and lower bounds for which percentile of luminance they want to cover.

The generated images can be thought of as elevations, so these groups are able to grab a mountain, a valley, or an in-between - i.e. a band of elevation.

I wanted the generated rooms to remain open and editable in the interface - it should be reasonable to visit and manually edit a groups bounds, and reassing a tilemap in a group. This was achieved, but not in a pure way - the generation code now expects the room to already be added to the world to be created.

I had hoped to end up in a place where I could serve endless levels to folks - go-until-you-die, see-how-far-you-get style. However, I couldn't work out a way to get my navigation to generate the levels properly (without a strange hiccup where the player gets reset after every level starts). I dropped this in favor of just-creating a few levels using the editor (generating with the tool i had built) and shipping those fixed levels as the game for now.

I'm excited to get back to this to create something more flexible, hopefully without sacrificing the editor-toying for random, in-game generation.

>> TowerRoom

Subclassing the ReptileRoom, the TowerRoom was the beginning of a probably never ending procedural generation journey. This was where I finally wrote functions that randomly selected parts of the room to add player-start points, enemy-spawn points, and targets.

These are brute-force for-x-and-y-in-grid style right now, but I'm excited to build these up into some reasonable, constraint-based apis for enriching generated rooms with enemies, npcs, and environment details (trees, leaves, grass, flowers, birds, all the things!).

> sounds via gdfxr, DJ sound-map handling

Finally got to creating sounds directly in godot, and it's definitely a win. The sounds are not incredible, but they're reasonable, and any amount of feedback to give the player seems worth it.

I went with sfxr - several of the juice talks mention sound and sfxr in the same breath. it's chippy but fun, and now there's little excuse to integrate a sound into something.

I wrote more sound helpers into DJ, my autoload for sound and music handling. DJ will hopefully evolve into a stronger interface. For now it's a couple helpers to process a string-based sound map, and supports an api like play_sound("punch") - this pulls a random sound from list, and randomly pushes the pitch up or down to provide some built-in variety.

The leap for DJ will be an interface where we can configure the sounds against string names/enums, and support playing the sounds on click in the UI. Maybe this could work from an in-game console as well.

> Hood, Hood.ensure_hud() and Hood.notif()

Dev logs becoming player logs! Share that experience.

These helpers work together to provide on screen notifications to help the dev and player keep track of everything happening.

It's also a win to not need to create a HUD every time, just to get notifications working.

I ended up copy-pasting alot to get tower wrapped up - the HUD from gunner, the menu and pause screens from elsewhere. I think there's more room to clean these things up, but I'm not totally sure what makes sense. Ultimately I think every game gets it's own menus and HUD design, so there's not some central thing to share.

However, it does seem reasonable to wrap subcomponents of the HUD into reusable things - a notifications component could own that setup on its own, and similarly for player health/item components. There's gotta be a cheap core to build on that takes on most of the effort somehow.

Which reminds me - I've an idea to create a SuperPlayer or Hero or some such addon to own player interactions. If there were a Player autoload, a number of things could easily get at it. Which might help? Not sure.

A related idea I like is including several versions of the player in the SuperPlayer addon. Add/remove jetpack, jumping, sword, etc. Players from gunner, from runner, from ghost. Can they be swapped in and out? Could/should they otherwise be combined?

They could probably all interact with the same HUD, at least.

> text effects

Hotline Miami comes to mind as an example of excellent text effects.

I found a nice godot text effects addon with a bunch of samples - those will serve as a decent base and a guide to implementing better ones going forward.

These are for player.notif() events (showing above the player for things like 'level up' or 'one target remaining').

> screen-wrapping woes

Screen wrapping needs more work - i wrote a quick something, but there are cases with problems to be handled better.

Ex: if there's a wall/floor/ceiling on the other side, the player starts to flicker between the positions. It's ugly!

probably needs some stricter level design to work nicely.

> difficulty ramp up and a missing tutorial

The game is too hard, mostly because you get thrust into the deep end on level 2.

Level 1 would have been a fine level 5. The first few should have been just the basics.

  • Level 1, just a target.
  • Level 2, a few targets.
  • Level 3, an enemy.
  • Level 4, pickups spawn an enemy.
  • Level 5, the actual level 1.
  • Levels 6, 7, 8 - probably as impled.
  • Level 8 or 9 or whatever - crazy boss last level.

It wouldn't have taken much time at all to do this - I was all setup to gen and tweak a few small levels without much trouble.

In retrospect, I completely skipped the level design.

I was distracted by the proc-gen.

Probably this would be called a tutorial, and it seems important now. At the time, I was like, I don't have time for that.

But I already had progression, just needed to clone and dupe a few scenes :/

> addons extended and added

>> reptile

>>> Reptile autoload with noise gen and tile helpers

>>> ReptileRoom and ReptileGroup objects for gen + regening tilemaps

>> camera

>>> screenshake with trauma and open simplex noise

>>> slowmo registries

>>> freezeframe/hitstop

>>> POF centering/handling

>> hood

>>> Hood.ensure_hud() for strapping on at least the notifications

>>> Hood.notif() for dev/player logs

>> dj

>>> sound map play/interrupt helpers

future ideas

> improve onboarding/tutorial for ramping up to the crazier levels

> extending reptile room gens into larger maps

> reptile room 'add neighbor' concept

> add pof/poi handling to targets and enemies

> regen the rooms with changing parameters

> grow the ice/heat areas dynamically

the area shrinks as you jet in it

it's a limited resource

> enemy robot needs a 'tell'

am i about to be shot at?

> enemy robot - only attack when on camera


Backlinks

I love rooms (game design element) as a unit of game design and iteration. A rooms api should support that.

Dino has seen a few different rooms api approaches already. The Runner Room code managed cycling rooms ahead of the player so they could be revisited. In Tower Jet, the Reptile Room was built up to generate (and regenerate) the tiles based on noise images. Then, Mvania / Hatbot featured a fuller room and area management, including room data lifecycle via hotel and pausing/unpausing the rooms based on the player location.

A Godot monorepo full of games, addons, and scripts.

A monthly, godot -required game jam.

Always 9 days (two weekends).

A Dino addon.

---

Dino's camera code grew especially while working on tower jet (dino game).

See also: