Pogo Carrot

Physics Traversal | Pogo Controller | UE5 C++ | Solo Design + Programming

Play The Game →

Summary


Pogo Carrot is a physics traversal prototype built solo in Unreal Engine 5 over two weeks. The core mechanic is a pogo stick controller where bounce and movement are physically coupled — charge intensity directly determines launch trajectory and speed, making every hop a deliberate movement decision rather than a fixed-height jump.

The project served as a focused engine migration challenge: porting a Unity-style event bus framework and a physics-based character controller architecture into Unreal C++ from scratch, adapting familiar patterns to a new engine's physics model and API.


Project Details


Mechanics
Charge-driven pogo controller — hold to charge, release to launch; charge level scales both height and horizontal velocity
Automated camera system decoupled from movement input, solving the two-input constraint without sacrificing feel
Pre- and post-bounce coyote time for both standard and charged bounces, preserving player intent at the edge of a jump

Technical
Fully C++-based — no Blueprint logic in the movement system
Event bus architecture ported from Unity C# to Unreal C++, organized into domain sub-buses (Race, Character) on the Game Instance
Custom movement mode (MOVE_Custom) extending UCharacterMovementComponent — manual velocity, gravity, and collision resolution via CMC's own sweep API


Team and Time


Team Size: 1 (Solo project)
Time: 2 weeks (Prototype Production)
Engine: Unreal Engine 5 (C++)

Goals


The goal of Pogo Carrot was to prototype a physics-driven traversal mechanic that felt expressive and skill-rewarding within a tight two-week window — while simultaneously treating the project as an engine migration exercise.

The core design question was: what does it feel like to move when every variable of your jump is under your control? By coupling charge intensity directly to both height and horizontal velocity, the pogo controller turns traversal into a continuous input problem — not a binary "press jump" decision.

On the technical side, the constraint was to bring proven Unity patterns — specifically an event bus architecture and a custom character controller approach — into Unreal Engine 5 without falling back on Blueprint. The goal was to understand how UE5's delegate system and movement pipeline differ and adapt the architecture accordingly, not just replicate surface-level behavior.

Systems


Charge-Coupled Pogo Controller

Movement runs as a custom mode (MOVE_Custom) inside UCharacterMovementComponent, managing its own velocity variable each tick. Gravity is applied manually when airborne; collision is resolved via CMC's SafeMoveUpdatedComponent and SlideAlongSurface, stripping velocity into a surface on contact. Ground detection is a short line trace from the capsule bottom.

Horizontal movement applies force in camera-relative directions each tick, with a braking multiplier when input opposes the current velocity and an air control multiplier when airborne. Drag decelerates and snaps to zero when no input is held.

On bounce, ApplyBounce decomposes the launch vector into two components relative to the impact surface: along-normal (carrying the vertical force) and along-surface (carrying the horizontal force). This means slopes naturally redirect the player — an angled wall bounces you diagonally without any special-case logic.

Charge produces three distinct outcomes based on timing: Power (released in a coyote window while charged → scales toward MaxBounceForce), Penalty (charging but missed the window → scales toward PenaltyForce), or Default (no charge attempt).

Automated Camera System

A pogo controller creates a two-input problem: charging consumes the same input axis that would normally handle camera rotation, leaving the player unable to aim and charge simultaneously. The camera system resolves this by automating rotation during the charge phase — tracking movement direction and blending smoothly back to player control on release, so feel is preserved without requiring a second analog input.

Coyote Time (Pre- and Post-Bounce)

The coyote system has two distinct windows managed by BounceForceManager, each covering a different player intention:

Pre-coyote (charge then land): the player charges and releases before the pogo tip contacts the ground. On release, a coyote timer starts and the charged state is saved. If the bounce detector fires before the timer expires, the landing is treated as a Power bounce — the player's early commitment is honored.

Post-coyote (land then charge): the player lands on a natural (uncharged) bounce first. On contact, a coyote timer starts and the impact normal is saved in the bounce detector. If the player releases a charge before the timer expires, an OnPostCoyoteRelease event fires and the bounce detector replays the bounce using the saved normal — the player can react to the landing and still get a Power bounce.

The two windows are mutually exclusive: bouncing while in pre-coyote consumes it and blocks post-coyote for that contact, preventing double-rewarding.

Technical Challenge: Porting an Event Bus from Unity to Unreal C++

The pogo controller relies on decoupled communication between systems — the controller, camera, animation layer, and coyote time logic all need to react to bounce events without holding direct references to each other. In Unity (C#), this was handled by a typed event bus built with generics and delegates: systems subscribe to strongly-typed event channels and fire them without knowing who is listening.

Unreal Engine 5 provides its own event and delegate infrastructure, but it is macro-heavy, Blueprint-oriented by default, and does not map cleanly to a generic typed channel pattern. Adapting the Unity framework meant working within UE5's macro system, Unreal's object model (UObject lifecycle, garbage collection), and a templated C++ environment with stricter constraints than C# generics.

The challenge was to reproduce the same decoupled pub-sub pattern in Unreal C++ without losing type safety or requiring Blueprint glue code.

Solution

I implemented the bus as a UObject held directly on UPogoGameInstance and accessed via Cast<UPogoGameInstance>(GetWorld()->GetGameInstance())->EventBus from any component that needs it. This keeps the bus at game-instance scope without a subsystem registration step and gives every UObject a single, consistent access path.

Rather than a flat channel registry, the bus is organized into domain sub-busesEventBus->Race (OnFinishReached, OnKillboxEntered, OnRespawn) and EventBus->Character (OnBounce, OnBounceSound, OnChargeChanged, OnPostCoyoteRelease, OnPostCoyoteBounce). Each event is declared with DECLARE_DYNAMIC_MULTICAST_DELEGATE, so subscriptions use AddDynamic and RemoveAll — compatible with UE5's garbage collector and Blueprint-bindable if needed.

The result is the same decoupled pattern from Unity: BounceDetector fires OnBounce; BounceForceManager, the camera, and the movement component each subscribe independently and react without any direct cross-references.

Outcome

All major systems — bounce detection, coyote time, charge state, camera, sound — communicate exclusively through the event bus with no direct inter-system references.

The domain sub-bus structure also kept subscriptions readable: each component subscribes only to the sub-bus relevant to it (Race events for respawn/finish handling, Character events for gameplay feedback), matching how the Unity version organized channels by context.

Technical Challenge: Custom Character Controller in UE5

UE5's Character Movement Component provides robust swept-capsule collision and built-in movement modes (Walking, Falling, Flying), but those modes run their own velocity integration and gravity — you cannot cleanly inject custom force logic without fighting the component's internal state. CMC's jump implementation is an impulse applied to a managed velocity with built-in apex detection, which produces consistent hops but not a pogo mechanic where bounce height and angle depend on how hard you charged and what surface you hit.

The challenge was to implement movement that felt physically grounded — with manual gravity, drag, air control, and surface-normal-aware bounce impulses — while still using CMC's swept collision infrastructure rather than reimplementing capsule sweeps from scratch.

Solution

I extended CMC using MOVE_Custom mode, which hands full tick control to the subclass while keeping CMC's collision primitives available. The component manages its own Velocity vector: gravity accumulates when airborne, drag decelerates when no input is held, and input force is applied in camera-relative directions each tick.

Collision is resolved using CMC's own SafeMoveUpdatedComponent and SlideAlongSurface — so swept capsule accuracy is preserved — with a manual step to strip velocity going into a blocking surface via dot product against the hit normal. Ground detection is a short downward line trace from the capsule bottom, giving direct access to the surface normal without CMC's floor data.

Bounce impulses are decomposed into two components relative to the impact normal: along-normal (scaled by VerticalBounceForce) and along-surface (scaled by HorizontalBounceForce). The bounce direction blends the impact normal with the actor's up vector via tunable multipliers, so angled surfaces redirect the player naturally without any special-case geometry handling.

Outcome

The controller responds to charge input with physically accurate launch arcs — a partial charge produces a low flat trajectory, a full charge produces a steep high arc — and slopes redirect the bounce direction naturally.

Using MOVE_Custom instead of bypassing CMC entirely meant collision resolution stayed robust on complex geometry, and the coyote and charge systems integrated cleanly against the same manual velocity state rather than fighting CMC's internal floor logic.

Full Gameplay