Skip to content

❮❮ All Projects

Project Status : Complete & Reusable Framework | 4 weeks

Project Type : Modular Framework | Unity (C#)

Core Focus : Movement feel | Game feel architecture

Forgiving Movement Framework


A production-ready character movement controller for side-scroller games, built on forgiving mechanics principles (jump buffer, coyote time, variable gravity). Designed as a reusable framework that ships in weeks, not months—enabling rapid prototyping and team collaboration through parameter-driven architecture.


𓆩General𓆪

Quick Overview

☑︎ Quick Summary for Recruiters
  • Reusable top-down character movement framework proven in production games
  • Forgiving mechanics built-in: coyote time, jump buffer, variable gravity
  • Slope handling and ground detection using reliable raycasting
  • Modular architecture with parameter-driven design (no magic numbers)
  • Drop into any project, configure in inspector, ship in days
  • Used as foundation for combat systems

Core Philosophy

✮ Core Philosophy

"Movement feel is the foundation of game feel."

This framework demonstrates:

  • Forgiving input windows that reward player intent over precision
  • Physics-based but tunable (gravity, acceleration, friction as parameters)
  • Ground detection that works (reliable raycasts, no edge cases)
  • Performance-conscious (no runtime allocations, pooling-friendly)
  • Team-ready architecture (parameters first, code second)

Every system is built to answer: "How do we make movement feel fair?"

Technical Highlights

✮ Technical Highlights
  • Coyote Time (Jump Grace Window): 0.15s after leaving ground, jump still works
  • Jump Input Buffering: Early inputs register up to 0.1s before landing
  • Variable Gravity: Apex hang (0.4x), descent fall (5.5x) for natural feel
  • Ground Detection & Walking: Raycast-based ground detection handles slopes gracefully
  • Momentum Preservation: Attack/dash systems inherit player velocity
  • Dash Override System: Dash cancels movement, not actions (skill expression)
  • Acceleration Curves: Smooth ramp-up from idle → running (not instant)
  • No Jump Stacking: Gravity check prevents double-jump exploits

- - - --->Features<--- - - -

Highlight Features𓆪

1. Forgiving Input Mechanics

Input: Jump Buffer & Coyote Time

What you're seeing: A player who can't miss a jump due to bad timing—input windows are large enough to be forgiving, small enough to feel responsive.

Jump Buffer
  • Player presses jump 0.15s BEFORE landing
  • System records the input timestamp
  • On landing, if buffer is active, jump executes retroactively
  • Result: No "one-frame miss" frustration, but not exploitable
Coyote Time
  • Player leaves ground (platform edge, fall-through)
  • For 0.15s after leaving, jump is still available
  • Works even if player held jump button while walking off (momentum based)
  • Result: "I jumped too late" moments become successes

Why Both Exist (Not One or the Other):

  • Buffer alone: Punishes fast, reactive players (feels unresponsive)
  • Coyote alone: Punishes predictive players (feels cheap)
  • Together: Both playstyles work, game feels fair
Implementation
csharp
if (jumpInputTime > Time.time - jumpBufferWindow && isGrounded)
    Jump();  // retroactively register

if (coyoteTimeCounter > 0f && jumpPerformed)
    Jump();  // free jump after leaving ground

The Result: Players report "movement feels so good" before understanding why. That's excellent game design.

2. Ground Detection

Physics: Reliable Ground Detection

The Problem: Standard rigidbody collision checks miss ground on slopes, corners, or frame-rate inconsistencies. Result: Jump doesn't work when player is clearly on ground.

The Solution: Capsule Raycasting

Implementation

Multiple raycasts from capsule bottom, fanned to catch edges:

csharp
RaycastHit2D hit = Physics2D.CapsuleCast(
    position: bottomCenter,
    size: capsuleSize,
    direction: Vector2.down,
    distance: groundCheckDistance,
    layerMask: groundLayer
);

Why this works:

  • ✅ Catches ground on slopes (raycasts fan out, so angled surfaces work)
  • ✅ Detects edges early (distance check is bigger than visual size)
  • ✅ Works at any frame rate (raycast is frame-time independent)
  • ✅ No rigidbody collision jitter (runs in FixedUpdate separately)

3. Physics Tuning

Physics: Variable Gravity & Acceleration

Variable Gravity (Hang Time Effect):

Implementation

At jump apex, gravity is reduced to 0.4x. During descent, gravity jumps to 5.5x.

csharp
if (jumpHeld && velocity.y < hangTimeThreshold)
    rigidbody.gravityScale = normalGravity * 0.4f;  // hang
else if (velocity.y < 0)
    rigidbody.gravityScale = normalGravity * 5.5f;  // fall fast

Why it works:

  • Player feels floaty at the apex (more control, longer hang time for planning)
  • Player falls quickly after (commitment to jump, no floaty feel late in jump)
  • Matches real-world intuition (ball thrown up slowly falls back, but we perceive hang)
  • Separates arc feel from trajectory (arc is the game feel)

Acceleration Curves (Smooth Ramp-Up):

Implementation
csharp
targetVelocity = inputDirection * maxSpeed;
velocity.x = Mathf.Lerp(velocity.x, targetVelocity, acceleration * Time.deltaTime);

Why it works:

  • Instant acceleration feels jerky (unrealistic, unintuitive)
  • Smooth lerp feels responsive and weighty (player has momentum)
  • Acceleration value is tunable (0.1 = sluggy, 0.5 = snappy, 0.9 = instant-ish)
  • Asymmetric accel/decel curves possible (faster stop = more control)

Parameter-Driven:

Implementation
csharp
[SerializeField] float maxSpeed = 5f;
[SerializeField] float acceleration = 0.85f;
[SerializeField] float hangTimeGravity = 0.4f;
[SerializeField] float fallGravity = 5.5f;

All tunable in inspector. No code recompile. Change at runtime in play mode.


- - - --->Technical<--- - - -

𓆩Technical Deep Dive𓆪

Architecture Overview

✮ Architecture Overview - Modular Component System
┌──────────────────────────────────────────────┐
│        CharacterMovement.cs (Core)           │
│   Handles velocity, acceleration, gravity    │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│         GroundDetection.cs (Raycast)         │
│     Tells CharacterMovement if grounded      │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│           InputHandler.cs (Input)            │
│       Maps input → movement calls            │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│      Optional: Combat/Dash Systems           │
│    Built on top of movement foundation       │
└──────────────────────────────────────────────┘

Single Responsibility:

  • CharacterMovement: Pure physics
  • GroundDetection: Pure raycasting
  • InputHandler: Input mapping
  • Combat/Dash: Systems that consume movement API

Why This Matters: You can swap any layer without breaking others. Change ground detection logic? CharacterMovement doesn't care. Add new input? Movement doesn't change.

Integration Point:

csharp
// In FixedUpdate
groundDetection.CheckGround();
characterMovement.UpdatePhysics(inputDirection, isJumping);
controller.Move(characterMovement.Velocity);

Linear, clear, testable.

1. Movement Core

✮ System 1: Character Movement

Jump Buffer Implementation:

csharp
private float jumpInputTime;
private const float jumpBufferWindow = 0.1f;

public void HandleJumpInput(bool pressed)
{
    if (pressed)
        jumpInputTime = Time.time;
}

public void Update()
{
    // Jump buffer: even if not grounded now, retroactively register
    if (isGrounded && Time.time - jumpInputTime < jumpBufferWindow)
    {
        Jump();
        jumpInputTime = -999f;  // consume
    }
}

Coyote Time Implementation:

csharp
private float coyoteTimeCounter = 0f;
private const float coyoteTimeDuration = 0.15f;

public void Update()
{
    if (isGrounded)
        coyoteTimeCounter = coyoteTimeDuration;
    else
        coyoteTimeCounter -= Time.deltaTime;
    
    // Jump allowed even in air if coyote time active
    if (coyoteTimeCounter > 0 && jumpPressed)
        Jump();
}

Why This Order Matters:

  1. Record input time
  2. Check if grounded
  3. Compare timestamps
  4. Execute jump
  5. Consume input

No branching, pure sequential logic. Easy to debug, easy to extend.

Variable Gravity:

csharp
private void ApplyGravity()
{
    float gravityMultiplier = velocity.y > 0 ? hangTimeGravity : fallGravity;
    velocity.y += Physics2D.gravity.y * gravityMultiplier * Time.deltaTime;
}

Tiny. Effective. Tunable.

2. Ground Detection

✮ System 2: Ground Detection

Capsule Raycast Pattern:

csharp
public bool CheckGround()
{
    Vector2 bottomCenter = new Vector2(
        transform.position.x,
        transform.position.y - capsuleSize.y / 2f
    );
    
    RaycastHit2D hit = Physics2D.CapsuleCast(
        origin: bottomCenter,
        size: capsuleSize,
        capsuleDirection: CapsuleDirection2D.Vertical,
        angle: 0f,
        direction: Vector2.down,
        distance: groundCheckDistance,
        layerMask: groundLayer
    );
    
    isGrounded = hit.collider != null;
    groundNormal = hit.normal;
    return isGrounded;
}

Why Capsule Over Line Raycast:

  • Line raycast: Only checks center (misses slopes)
  • Capsule raycast: Fans out across width (catches edges)
  • Capsule is consistent with visual size (no magic distance)

Slope Handling:

csharp
public void HandleSlopes(Vector2 velocity)
{
    float slopeAngle = Vector2.Angle(groundNormal, Vector2.up);
    
    if (slopeAngle > maxWalkableSlope)
        return;  // too steep, slide off
    
    // Adjust horizontal velocity to follow slope
    velocity = Vector2.ProjectOnPlane(velocity, groundNormal);
}

Slopes feel natural, don't stick where they shouldn't.

3. Input Handling

✮ System 3: Side-Scroller Controller

Input Mapping (Simple & Clear):

csharp
private void HandleInput()
{
    float moveInput = Input.GetAxisRaw("Horizontal");
    bool jumpInput = Input.GetButtonDown("Jump");
    bool dashInput = Input.GetButtonDown("Dash");
    
    movement.SetDirection(new Vector2(moveInput, 0));
    movement.HandleJumpInput(jumpInput);
    
    if (dashInput)
        Dash();
}

Facing Direction (Decoupled):

csharp
if (moveInput != 0)
    facingDirection = moveInput > 0 ? 1 : -1;

// Facing direction persists even during jump
// Result: Player can face one direction while moving another (skill expression)

Why Decouple Facing:

  • Allows attacking backward mid-jump (high-level play)
  • Feels responsive (visual feedback matches intent)
  • Not automatic (player controls it actively)

The Pattern: Input → Data → Action. No state mutation mid-frame.


- - - --->Extras<--- - - -

𓆩Extra Notes𓆪

When to Use This Framework

✮ When to Use This Framework

Perfect For:

  • ✅ Platformers action games (E.g: Hollow knight)
  • ✅ 2D roguelikes or dungeon crawlers
  • ✅ Rapid prototyping (setup in 30 minutes)
  • ✅ Multiplayer-ready (fully deterministic physics)
  • ✅ Teams (parameter-driven, not hard-coded)

Less Ideal For:

  • ❌ Grid-based movement (use AStar pathfinding instead)
  • ❌ 3D games (code is 2D-only; 3D version available separately)

Framework Implementation

✮ Framework vs. Game-Specific Implementation

This is a Framework, Not a Template

What that means:

  • You own the movement code (it's yours to modify)
  • Parameters are starting points, not dogma (tune to your game feel)
  • It ships in weeks, not months (saves hundreds of hours)
  • It scales to multiple characters (used in Path to Power with custom combat)

What You Get:

  • Battle-tested forgiving mechanics
  • Reliable ground detection (no "jump doesn't work" bugs)
  • Clean, documented code (readable by teams)
  • Parameter-first design (iterate without recompile)

What You Build:

  • Game-specific combat systems
  • Custom abilities (dashes, double-jumps, wall-slides)
  • Unique feel (adjust gravity/acceleration to your game)
  • Level design patterns (slopes, platforms, gaps)

The framework handles the foundation. You handle the flavor.

Technologies & Stack

✮ Technologies & Stack
  • Engine: Unity 2020 LTS+
  • Language: C#
  • Physics: Rigidbody2D with custom movement
  • Input: New Input System (event-driven, configurable)
  • Ground Detection: Raycast & CapsuleCast
  • Performance: Zero allocations per frame (pooling-friendly)
  • Documentation: Inline comments + parameter descriptions
✮ Links & Resources
  • GitHub: [Not available yet]
  • Used In: Path to Power
  • Live Demo: [Not available yet]
  • Documentation: Included in codebase