Archive

Game

The Team DogPit 2023 Jam kicked off this month! It remains my favorite annual jam, though this year I had to scale back the planned game because I was out on a trip for the first half. I ended up submitted a game called “Odd Soul” which you can play here: https://xoana.itch.io/odd-soul

The theme was, “The Odds are Good,” and while I had initially planned a game where you have to DM for a bunch of players who keep rolling terribly, the game seemed too far removed and complicated to execute.

I pivoted to something of a visual novel, planning to make a game with multiple endings whose story junctions depended not just on text but also games of Rock, Paper, Scissors. The mechanics for Rock, Paper, Scissors I stole from someone I saw on Twitter:

The gameplay turned out to be too hard to manage. There’s too much visual noise for the player to really figure out what they’re doing and the action can either be too fast-paced or too boring. There wasn’t really an in-between I found.

Nonetheless, I kept with the visual novel system and, after struggling with a story that was ENTIRELY too complicated and serious for a fun game, I stumbled upon the idea of playing Death for your eternal soul. Mild spoiler: Death plays perfectly for most rounds of the game, but every so often there’s a small chance that it will make a mistake. One-shot, your odds of winning a game are in the tens of thousands, but play often enough and you’re basically guaranteed to win.

When I got tired of working on minigames I went back to writing story and bumbling through possible endings. It’s far from the finest bit of writing, but it works well enough for the game and expected playtime.

I am slightly pleased with how robust the visual novel format is. A user can create an arbitrary story as a JSON file and the application will detect and run that, so new stories can be made (with new visual assets) with no code.

The story JSON is fairly simple: { "scenes": { ... all scenes ... }, "assets": { ... all assets ...}

With the average scene looking like this:

"goto_apartment": {
	"background": "apartment",
	"actors": ["dog", "cat", "death"],
	"text": "[death:eyes_closed]One moment.  [death]There we are.\n[dog]Dog: *bark*\n[cat]Cat: *meow*\n[death]Oh!\nOh wow. <truncated>\n",
	"prompt": "Yeah.",
	"options": {
		"They're hungry.": "hungry",
		"They're my reason for living.  Or they were.": "reason_for_living"
	},
	"next": "",
	"on_enter": "fade_in",
	"minigame": "",
	"minigame_outcome": {

	}
},

And the average asset looking something like this:

"assets": {
	"characters": {
		"reaper": {
			"default": "png:<a base64 encoded PNG>",
			"angry": "file:path/to/a/png/alternative.png",
		}, // More characters
	},
	"backgrounds": { ... like above ... }
}

Let’s break down a scene:

The “actors” array determines the order of the portraits to be shown. In the above example, we have a scene like this:

Is it a good idea to show Death your apartment? Maybe!

The ‘text’ area then can/will highlight actors. When an actor is not speaking, they’re faded slightly and made transparent. An actor can have different emotes inline. [death:happy] will, for example, look for an image in the Death asset named “happy” and will display that where Death is. All actors need a “default” image. Convention over configuration. One additional key about the text is that switching expressions will not pause the dialog for user input. User input is only required when a newline appears in the text, so in theory we could chain several emotes together for an animation (this happens once in the story) or several scenes together to trigger actions (this also happens in the story).

The last piece to consider is how to branch. The branching will trigger after all the text is finished displaying. “options” specifies an associative array (dictionary) with the text of the choice mapping to the name of the scene. In the above, the user will see two buttons, “They’re hungry.” and “They’re my reason for living…”. Clicking “They’re hungry” will bring us to the scene named “hungry”. Another possible way to branch is to trigger a minigame by setting the “minigame” field. I have four implemented (“tictactoe”, “pong”, “random”, and “rockpaperscissors”), but only two appear in the story. (I’d meant to make more, but timing is hard.) Each minigame has to emit a signal on completion with the status: GameOutcome.WIN, GameOutcome.LOSE, or GameOutcome.DRAW. And the “minigame_outcome” has to map “win”, “lose”, and “draw” to the scenes.

I am slightly proud of the “random” game, since that starts up a minigame which immediately returns one of the outcomes and is a way to make a story branch randomly. If there’s no “text”, the scene starts up, finishes, and branches in a single frame.

If there’s no “prompt” and no “minigame”, then “next” is the scene to which we jump when the dialog is complete.

Finally, “on_enter”. This is the part I’m least fond of. “on_enter” is the name of a function to be called as soon as the scene is done loading. I’ve used it to fade_in and trigger returns to the menu, but I think these uses could be better served by inlining them. That way we don’t need separate scenes for returning to the menu or jumping to endings, and opens the possibility of setting multiple flags in the same scene.

All things told, I’m not proud of the gameplay or the art or any of it, but it’s done and I find relief in this and satisfaction in having completed another jam. The entire project is open source and available for review here: https://github.com/JosephCatrambone/DogpitJam2023

tldr: Pitch: A physics based (soft body) sim where you drag and fling a floppy cat, Angry Birds style, through a series of levels like Sonic.

Ideation Phase: I was thinking of something roguelike, but I’m worried I’d spend too much time endlessly tweaking the procedural generation stuff and not enough time making a shippable game. I would also like to be doing this in Rust, but since it’s too hard to throw things online and deploy to mobile, I’m thinking that Godot makes the most sense. Something simple and physics based with one-finger inputs would be satisfying and approachable by my friends and family. Perhaps Pikuniku meets JellyCar.

Development Stream:

Day 3 (Workday 1):

I spent a little too much time fiddling with verlet integration for the soft-body physics stuff. The simulation is unstable for reasons that aren’t clear and it also doesn’t really add to gameplay. It’s not a goal or even a mechanic — it’s a distraction.

Backing out, I think I’d like to use SmartShape2D with RigidBody physics, but it’s not available for Godot 4 yet, so I’ll probably do a tileset with some clever shapes. I think it might be better to have a simplified set of angles, too. It lets people predict their shots more easily.

Possible assistive features: slow down time and/or project the angles? Hmm.

Design Question: when a player drags on the cat, should the impulse applied always be central, or does it make sense to apply a torque when they grab off center? Or should there be a sweet spot where it’s “central torque” and everywhere else applies spin?

Workday 2:

First enemy in the game was “Psychophant”, the psychotic elephant. I added a component called “rb_damage” which listens for contacts between RigidBodies. I had originally used a CharacterBody because it made sense for something that was kinematic, but there wasn’t a good way to register impacts (with velocity) on the body. After a lot of futzing there was just no convincing way to get the impacts. The get_slide_collision on CharacterBody only returns the physics objects encountered along the path of movement, so if the player bonks an enemy from behind we don’t register a hit. I gave up and said that all enemies have to be RigidBodies, then I added a meme explosion sprite on death.

Workday 3:

I only had time to make a camera component which followed the player and the player’s direction of movement. At this point, I’m really avoiding making level geometry and more enemies because I neither want to use tilemap nor have to deal with converting Polygon2D maps to collision geometry. SmartShape2D is sadly not available for Godot 4 yet.

Workday 4:

I finally broke down and created levels. I spent a lot of time automatically generating collision shapes from polygons so that I can remove the debug rendering. Offsets were ruining what was otherwise a fairly straightforward creation of one collision polygon per polygon2d. Ultimately, I gave up and threw a warning when the polygon had nonzero offset.

Workday 5:

It looks like a game, at least. I added time slowing when the player taps, and it feels pretty good! However, a new problem has shown itself: Dragging the player adds an impulse, rather than explicitly setting linear velocity. This seemed like a nicer and more physics based way to do things. The problem is that when the player is in free-fall, applying a big force just brings the player to a standstill, rather than setting the linear velocity. This doesn’t feel really great, so I’m wondering if there are better choices. I could set linear velocity and let the player pivot in the air, but it’s mostly a matter of experimentation now.

Next Week:

I ended up setting the linear velocity if the angle was outside of some threshold. If you are travelling forward and slingshot floppy cat in the same direction, the velocities add. If you apply a linear velocity in the opposite direction, the velocity gets set. This feels pretty good, anecdotally.

I spent a while working on the title screen and level select. I’m starting to feel some hacks coming in from the UI because of how ‘main game’ is a standalone scene which does level loading. After the MainGame screen gets loaded, I don’t have a way of signaling from the level select screen that “Level 1” should be loaded, so instead I set a global variable “map to load” and then main game checks if it’s set, loads the level provided, and clears the value. Gross. Hacky. Functional.

And that leads us most of the way to a complete game. I have kill boxes to return the player to the last checkpoint, a ‘level complete’ marker at the end which will trigger the end of level sequence, and now all that remains is adding three more levels and doing a bunch of playtesting.

Final Week:

After hastily throwing together a bunch of levels and wiring up menus as best I could, the game is ready to ship, or at least “done sufficiently for the time provided”. It’s up and running on itch.io at https://xoana.itch.io/floppy-cat-saves-the-world . Making the last levels feel fun was a challenge, both because of the way that they were structured (using polygons) and because of viewport limitations in Godot itself. I found that I couldn’t scroll past +4000 in either direction, which, when your viewport is 1k by 1k, makes for a bit of a tricky cap. I think if I had to do it again I’d shrink the viewport significantly and do scaling. This would help with the texturing, too, as all of them ended up looking really tiny in the final shipped product. I also didn’t finish the ending cinematic or hidden ending in time, but I’m glad I got something out of the door.

Mhess is a stupid messy chess game about giving your pieces superpowers and fighting robot masters. It’s been the center of my attention for the past month or so, and it seemed fitting to share a few of the things I’m learning about the good, the bad, and the ugly of chess engine programming.

As a caveat, I deliberately did not look at any existing implementations. I was hoping that avoiding them would prevent having any pre-conceived notions about the best approach and would lead to a more novel, interesting implementation. If you’re interested in writing your own chess game, I would recommend looking at The Chess Programming Wiki.

Implementing a chess game and a chess engine are very different problems, though a chess game needs a chess engine, the design rules for a fully functional game are slightly different than those of a pure chess engine. For starters, something I had not thought about when building out the engine component was how difficult keeping the internal board representation and external board representation in sync would be. I spent a significant amount of time debugging why things in the game world would disappear or refuse to move. This challenge was compounded, or perhaps made more subtle by what I think are, in hindsight, bad decisions.

Bad Decision 1: Game State representation.

I use the following to represent a game state. It’s designed to be light weight and quick to clone so that when we start looking at trees of depth 14 we don’t blow out our compute time.

#[derive(Clone, Eq, Hash)]
pub struct GameState {
	pub board_width: u8,
	pub board_height: u8,
	pub board_holes: vec![],
	pub board_state: Vec<(u8, u8, Piece)>,
	pub en_passant_space: Option<(u8, u8)>,
	pub black_can_queenside_castle: bool,
	pub white_can_queenside_castle: bool,
	pub black_can_kingside_castle: bool,
	pub white_can_kingside_castle: bool,
	pub halfmove_count: u8,
	pub fullmove_count: u8,
	pub current_player: PlayerColor,
}

The board_state is worth highlighting. I had started with a vector of tuples of u16,Piece. That proved to be an unnecessary optimization since we always immediately converted the idx into x,y. Note that there’s a deliberate choice to have x,y in the board state and NOT inside the piece. This is done to make cloning the board state a shallow operation and really fast. Generating 11 million moves takes less than 700ms in debug mode.

A piece does not know its position in the world. Instead, a move generator takes a game state and produces a bunch of “gamemove” items. A gamemove item takes a gamestate and produces from it a new state. If this whole thing seems roundabout, that’s because it is. Complexity of defining moves is generally localized to the gamemove.rs code, but there’s a LOT of complexity to be localized. Implementing castling has been a mess because checking whether pieces are being attacked involves generating all of the possible moves for the next state. Similarly, checking for checkmate (or check) involves basically first generating the next moves and then determining which result in the king being able to be captured. This is not a great solution.

The next experiment will be a lengthy one and will probably involve trying to switch from the current gamestate + move generator to a gamestate + heavy piece system. If that works out, or if it doesn’t, I’ll be sure to post an update here.