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
if (jumpInputTime > Time.time - jumpBufferWindow && isGrounded)
Jump(); // retroactively register
if (coyoteTimeCounter > 0f && jumpPerformed)
Jump(); // free jump after leaving groundThe 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:
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.
if (jumpHeld && velocity.y < hangTimeThreshold)
rigidbody.gravityScale = normalGravity * 0.4f; // hang
else if (velocity.y < 0)
rigidbody.gravityScale = normalGravity * 5.5f; // fall fastWhy 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
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
[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 physicsGroundDetection: Pure raycastingInputHandler: Input mappingCombat/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:
// 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:
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:
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:
- Record input time
- Check if grounded
- Compare timestamps
- Execute jump
- Consume input
No branching, pure sequential logic. Easy to debug, easy to extend.
Variable Gravity:
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:
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:
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):
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):
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
✮ Links & Resources
- GitHub: [Not available yet]
- Used In: Path to Power
- Live Demo: [Not available yet]
- Documentation: Included in codebase
