Postmortem: Our First Game Jam


Lonely Moon is our first game jam game, created for the Godot Wild Jam #48. It was created to fit with the jam's theme of "Moon", with the goal of creating a 2D physics exploration game about finding where you belong.

Things that went great

The physics

Our core idea was to combine simple 2D player movement with realistic(-ish) gravitational effects from nearby objects. The goal was for the player to have a sense of control, but also let them go wild slingshotting themselves around the solar system. From both the programming and gameplay side, this was a huge success.

To implement the mechanic we took inspiration from Kerbal Space Program and used a simplified patched conic approximation. Rather than try to simulate true n-body gravity, which would be a nightmare to balance and confusing for the player, patched conics define "spheres of influence" (SOI) around bodies based on their mass. If a player is inside a body's sphere of influence then it is gravitationally attracted to it until it exits the sphere. SOIs can overlap, as is the case with planets inside the sun's SOI and moons that are inside of their planets' SOIs.

From a programming standpoint, the SOIs were implemented as circular collision shapes whose radius is based on their body's mass. Detecting entry and exit from SOIs was accomplished with Area2D signals. The tricky part was dealing with overlapping SOIs, as is the case with planets and their moons. This was done using a doubly linked list data structure in the main scene. On game start the player defaults to the sun's SOI. Each time they enter a new SOI it's added to the end of the list and set as the currently influencing body, and each time a player leaves an SOI it's popped out of the list. This can get tricky in situations like this: 

SOI hierarchy
* indicates current SOI:
Sun -> Planet -> Moon*
Due to an edge case the player can leave the Planet's SOI without leaving the Moon's:
Sun -> Moon*

This necessitated functions to traverse the SOI hierarchy, remove nodes without breaking the list, and update the current SOI body only when necessary.

The gravitational effect itself was surprisingly simple. Each frame the player's script checks which body it's currently under the influence of and calculates the pull of gravity based on the body's mass and distance:

func calc_grav_accel() -> Vector2:
    var parent_pos := parent_body.global_position
    var to_parent_vec := parent_pos - self.global_position
    var r := to_parent_vec.length()
    var accel_vector := to_parent_vec.normalized()
    var grav_accel: float = Globals.GRAVITATIONAL_CONSTANT * parent_body.body_mass / pow(r, 2)
    accel_vector *= grav_accel
    return accel_vector

This is done using with simple vector math to find the direction and distance (magnitude) the the vector between the two bodies and then using a standard formula to find the acceleration due to gravity.

The final effect is smooth, was relatively easy to tweak for gameplay, and gives players a realistic feel of being whipped around by gravity.

The camera

Something we expected to spend a lot of time to get right, but ended up taking almost none was the camera. We didn't want a camera that simply locked the player in the center, but instead had a drag margin for a more natural feeling and to give the player a sense of movement. It turned out that Godot's Camera2D node has "Drag Margin" properties that can be easily toggled on. This immediately got the camera feeling 99% done.

Another time saving convenience was easily configurable range limits, so the player will fly off screen if they go too far. During development we realized an option to zoom the camera out would be nice to help players orient themselves. This was accomplished by simply creating a Tween on the camera's zoom property:

if Input.is_action_just_pressed("ui_accept"):
    var tween: SceneTreeTween = get_tree().create_tween()
    tween.tween_property($Camera2D, "zoom", max_zoom, 0.5)
if Input.is_action_just_released("ui_accept"):
    var tween: SceneTreeTween = get_tree().create_tween()
    tween.tween_property($Camera2D, "zoom", default_zoom, 0.5)

The final minor touch was adding a slight camera lean effect when a player is inside a body's SOI. We wanted the player to have an idea which direction gravity was pulling them so we implemented a lean effect by simply offsetting the camera's focus a small amount towards whatever body was influencing the player:

func update_camera_focus() -> void:
    if get_node_or_null("Moon"):
    var active_astro_body := moon.parent_body as AstroBody
    if active_astro_body:
        var vector_towards_body: Vector2 = (active_astro_body.global_position - moon.global_position).normalized()
        camera_focus = moon.global_position + (vector_towards_body * camera_lean_amount)
    else:
        camera_focus = moon.global_position

The effect is subtle, and could probably be taken further and combined with a zoom, but it adds a nice effect as is.

The art

All the art in the game was created by Ally using Procreate. This was Ally's first experience with digital art, being accustomed to working with watercolors, and we're thrilled with the result! I typically default to pixel art and originally envisioned the game having that aesthetic, but couldn't be happier with the abstract designs which feel both more natural and also more stylized.

Our workflow was for Ally to create large textures on the iPad, which we then uploaded to the laptop and stamped out circular shapes to apply to the planets. The AstroBody script automatically scaled the sprite to perfectly fit the body's radius on initialization.

It would have been fun to implement dynamic lighting, or tweak the textures a bit for more of a pseudo-3D effect if time allowed, but we're thrilled with the hand painted (almost storybook) aesthetic.

Things that went... not great (and what we learned)

Plan out all the gameplay early and don't forget a win-state

The elephant in the room is that Lonely Moon doesn't actually have a win state. Running into the sun, crashing into too many planets, or getting flung off into space all result in a game over, but there isn't any way to win or even a real objective aside from exploring.

We knew we didn't have a complete end-to-end gameplay sketch when we started. We just loved the idea of a moon searching for its planet and went with it. There was always a vague sense of an endgame where the player manages to achieve an orbit around a particular planet, but around midway through the jam we realized there wouldn't be time to implement it.

For our next jam we'll try to fully sketch out and scope the game's features before diving in.

Over time, all code becomes spaghetti

In my professional and person life I'm a stickler for clean, well documented code. I can now say that all goes out the window under the constraints of a game jam.

What started as well structured code with functionality cleanly separated into separate scenes became a hodgepodge of as features and fixes slowly got added. Should handling the player's death be done in the `World` scene or in the `Moon` scene? Why not both! Global constants, script constant, or just random literals scattered about? Let's do them all!

In the end this wasn't the worst thing. Bugs never took too long to track down. But I learned that I need to take more time to really internalize how to cleanly organize Godot projects into scenes and tool scripts and resist the urge to just hoist everything up to the game's main scene.

Don't reinvent the wheel PhysicsBody2D

Early on I made the regrettable decision to avoid using Godot's built-in physics engine and calculate everything manually using Area2Ds. I did this because I liked having direct control over object's positions and behaviors and didn't want to re-learn how to design and tweak controls and collisions on physics based bodies.

On the one hand, I really enjoyed doing things myself. Figuring out how to calculate the bounce force when the player collides with a planet was a lot of fun. On the other hand this lead to frustrating bugs where players can get stuck inside of the planets and the collisions can feel wonky sometimes.

In the future I'll spend more time familiarizing myself with Godot's physics engine and related nodes rather than charging forward on my own.

Files

LonelyMoon Play in browser
Aug 21, 2022

Leave a comment

Log in with itch.io to leave a comment.