modular 2d platformer

building a modular 2d platformer in godot 4

The Simple Shape Platformer started as a minimal visual experiment and gradually turned into a focused prototype about movement feel, level flow, and reusable systems. The goal was to see how far a clean geometric style could go when paired with responsive controls and editor-friendly tooling.

role & project details

  • Role: Solo technical designer and programmer
  • Engine: Godot 4
  • Language: GDScript
  • Focus: Movement, room layout, pickups, and editor tooling

the problem

i wanted to test responsive platforming movement while keeping room setup and ability pickups easy to change. The controller needed to handle forgiving input timing without becoming difficult to extend.

what i built

i built a room system that updates camera limits and collision bounds in the editor, a player controller with coyote time and jump buffering, and configurable pickups that delegate abilities to the player. Save data, audio settings, and small canvas drawn effects complete the prototype.

tools

  • Godot 4 and GDScript
  • @tool editor scripts
  • Canvas drawing and tweens
  • Mermaid diagrams for system documentation
Gameplay showing modular 2D platformer room and movement mechanics

room system

One of the first problems to solve was room setup. In a screen-based platformer, manually adjusting camera limits and collision bounds for every space gets tedious fast, so the project uses a small @tool workflow in room.gd to handle that work in the editor.

Designers can set room width and height as screen-sized units in the inspector, and the collision shape updates automatically. That keeps layout iteration quick and makes it easier to build connected rooms without constantly fixing camera boundaries by hand.

When the player moves from one room to the next, the camera limits shift and the view slides into place. The pause is brief, but it gives transitions a deliberate, classic feel that fits the project well.

@tool 
extends Node2D
var screen_x = 1152
var screen_y = 640

@onready var area = $RoomArea

@export var width : int = 1:
	set(value):
		width = value
		$RoomArea/RoomCollision.shape = $RoomArea/RoomCollision.shape.duplicate()
		$RoomArea/RoomCollision.shape.size.x = value * screen_x
	get:
		return $RoomArea/RoomCollision.shape.size.x / screen_x

movement and physics

Most of the work went into movement. For a platformer like this, the difference between something that feels good and something that feels forgettable usually comes down to small timing details.

The player controller includes variable jump height, coyote time, wall interaction, and jump buffering so inputs feel reliable even when timing is slightly early or late. There is also a charged power jump that adds a stronger vertical option and gives the movement system a bit more personality.

Wall slides reduce gravity, wall jumps apply a controlled push-off, and external forces such as bouncepads can feed into the movement state without making the controller feel messy. The result is a system that stays readable while still leaving room for expressive movement.

graph TD
    A[Player Input] --> B{Action Pressed?}
    B -->|Jump| C{Ground Check / Coyote?}
    C -->|Yes| D[Standard Jump]
    C -->|No| E{Wall Contact?}
    E -->|Yes| F[Wall Jump & Pushback]
    E -->|No| G{Double Jump Unlocked?}
    G -->|Yes & Available| H[Double Jump & Spin Anim]
    B -->|Grab + Jump| I[Charge Power Jump]
    I -->|Release| J[Amplified High Jump + Screenshake]
            

pickup design

To keep the player script from turning into a pile of item-specific logic, pickups use a simple delegation pattern in pickup.gd. Each pickup decides what kind of action it represents, then calls the matching method on the player.

That makes ability unlocks like wall_jump, double_jump, and power_jump easy to configure in the inspector without hardwiring every interaction into one giant controller. It also keeps the code easier to extend when new pickup types are added later.

The pickups themselves are drawn with lightweight canvas code and animated with tweens instead of relying on heavier texture work. That fits the visual style and keeps the whole project consistent.

func _on_area_2d_body_entered(body):
	var func_name = "_" + TYPE
	if body is Player:
		var func_ref = Callable(body, func_name)
		if TYPE == "UNLOCK":
			func_ref.call(unlocking)
		else:
			func_ref.call()
		if one_time:
			queue_free()

supporting systems

A few smaller systems helped round the project out. The options menu uses proper linear-to-decibel conversion for volume sliders, and settings plus basic run statistics are serialized into a save file for persistence.

Those features are not flashy, but they matter. They make the prototype feel more complete and show the same attention to structure as the movement and level systems.

visual direction

What I like most about the project is that the presentation comes directly from the technical constraints instead of hiding them. Simple shapes, procedural grids, and shader-driven transitions give it a distinct look without needing heavy art production.

That approach kept the scope manageable while still giving the game a clear identity. In the end, the project became less about minimalism for its own sake and more about using a restricted visual language to support sharp movement and clean design.

result

The result is a playable prototype with reusable room setup, responsive movement, configurable pickups, save data, and a clear visual identity.