Receber por email
← Voltar para o catálogo

character-controller

2D and 3D character controller patterns — coyote time, input buffering, variable jump, wall slide/jump, dash, slopes, stairs, camera-relative movement. Load when implementing player movement.

20estrelas
Atualizado há 6 meses

Ver no GitHub ↗ Licença: MIT

Como adicionar

/plugin marketplace add XeldarAlz/everything-claude-unity

O comando exato pode variar conforme o repositório. Confira o README no GitHub.

Para o autor da skill

Cole no README do seu repo

Mostra que sua skill está catalogada na Skillteca, gera backlink e tráfego rastreável.

[![Skillteca](https://www.skillteca.com.br/api/badge/character-controller/svg)](https://www.skillteca.com.br/skills/character-controller)
#ai

Skills relacionadas

Ver todas de Documentos →

Alerta por categoria

Receba novas skills de Documentos toda segunda

1 email curto, só com as skills novas de Documentos. 4 minutos de leitura, sem spam, cancela com 1 clique.

Você confirma o email no primeiro envio. Sem spam. Cancela com 1 clique.

Ler descrição completa

Character Controller Patterns

Comprehensive reference for building responsive, game-feel-polished character controllers in Unity. Covers both 2D platformer and 3D action game patterns.

2D Character Controller

Ground Detection

Use an overlap circle at the character's feet rather than relying on collision callbacks. This gives frame-accurate ground state.

[Header("Ground Check")]
[SerializeField] private Transform groundCheckPoint;
[SerializeField] private float groundCheckRadius = 0.15f;
[SerializeField] private LayerMask groundLayer;

private bool _isGrounded;

private void CheckGround()
{
    _isGrounded = Physics2D.OverlapCircle(
        groundCheckPoint.position,
        groundCheckRadius,
        groundLayer
    );
}

Place groundCheckPoint as a child transform at the bottom of the character sprite. Keep the radius small to avoid false positives on walls.

Coyote Time

Allow the player to jump for a brief window after walking off a ledge. This forgives slight mistiming and makes platforming feel generous rather than punishing.

[Header("Coyote Time")]
[SerializeField] private float coyoteTimeDuration = 0.1f;

private float _coyoteTimeCounter;

private void Update()
{
    if (_isGrounded)
    {
        _coyoteTimeCounter = coyoteTimeDuration;
    }
    else
    {
        _coyoteTimeCounter -= Time.deltaTime;
    }

    if (_jumpPressed && _coyoteTimeCounter > 0f)
    {
        ExecuteJump();
        _coyoteTimeCounter = 0f; // Consume coyote time
    }
}

A typical value is 0.08 to 0.15 seconds. Higher feels more forgiving; lower feels tighter. Playtest to find the sweet spot for your game's pace.

Input Buffering

Queue a jump input so it fires the moment the player lands, even if they pressed the button a few frames early. Combined with coyote time, this eliminates most "I pressed jump but nothing happened" complaints.

[Header("Input Buffering")]
[SerializeField] private float jumpBufferDuration = 0.12f;

private float _jumpBufferCounter;

private void Update()
{
    // Buffer the input
    if (_jumpPressedThisFrame)
    {
        _jumpBufferCounter = jumpBufferDuration;
    }
    else
    {
        _jumpBufferCounter -= Time.deltaTime;
    }

    // Consume buffer when grounded (or in coyote time)
    if (_jumpBufferCounter > 0f && _coyoteTimeCounter > 0f)
    {
        ExecuteJump();
        _jumpBufferCounter = 0f;
        _coyoteTimeCounter = 0f;
    }
}

Variable Jump Height

Cut the jump short when the player releases the button early. This gives the player fine control over arc height.

[Header("Variable Jump")]
[SerializeField] private float jumpForce = 14f;
[SerializeField] private float jumpCutMultiplier = 0.4f;

private Rigidbody2D _rb;

private void ExecuteJump()
{
    // Reset vertical velocity before applying force for consistent jump height
    _rb.velocity = new Vector2(_rb.velocity.x, 0f);
    _rb.AddForce(Vector2.up * jumpForce, ForceMode2D.Impulse);
}

private void Update()
{
    // When the player releases jump while still moving upward, cut velocity
    if (_jumpReleasedThisFrame && _rb.velocity.y > 0f)
    {
        _rb.velocity = new Vector2(
            _rb.velocity.x,
            _rb.velocity.y * jumpCutMultiplier
        );
    }
}

The jumpCutMultiplier controls how much velocity is retained. A value of 0.4 means releasing early yields roughly 40% of max jump height. Tune this alongside gravity scale.

Wall Slide and Wall Jump

Detect walls with a horizontal raycast or box overlap. Apply reduced gravity while sliding, then launch away from the wall on jump.

[Header("Wall Interaction")]
[SerializeField] private Transform wallCheckPoint;
[SerializeField] private float wallCheckDistance = 0.3f;
[SerializeField] private LayerMask wallLayer;
[SerializeField] private float wallSlideSpeed = 2f;
[SerializeField] private Vector2 wallJumpForce = new Vector2(12f, 16f);
[SerializeField] private float wallJumpLockTime = 0.15f;

private bool _isTouchingWall;
private bool _isWallSliding;
private float _wallJumpLockCounter;
private int _wallDirection; // -1 left, 1 right

private void CheckWall()
{
    _isTouchingWall = Physics2D.Raycast(
        wallCheckPoint.position,
        Vector2.right * transform.localScale.x,
        wallCheckDistance,
        wallLayer
    );

    // Wall slide when airborne, touching wall, and holding toward it
    _isWallSliding = _isTouchingWall && !_isGrounded && _moveInput.x != 0f;
}

private void ApplyWallSlide()
{
    if (!_isWallSliding) return;

    // Clamp downward velocity to slide speed
    if (_rb.velocity.y < -wallSlideSpeed)
    {
        _rb.velocity = new Vector2(_rb.velocity.x, -wallSlideSpeed);
    }

    _wallDirection = transform.localScale.x > 0 ? 1 : -1;
}

private void WallJump()
{
    if (!_isWallSliding) return;

    // Jump away from wall
    _rb.velocity = Vector2.zero;
    _rb.AddForce(new Vector2(-_wallDirection * wallJumpForce.x, wallJumpForce.y),
        ForceMode2D.Impulse);

    // Temporarily lock horizontal input so the player does not immediately
    // steer back into the wall
    _wallJumpLockCounter = wallJumpLockTime;
}

The input lock after a wall jump is critical. Without it, players holding toward the wall will negate the horizontal push and slide back down immediately.

Dash Mechanic

A short burst of speed with optional invincibility frames. Use a cooldown to prevent spamming.

[Header("Dash")]
[SerializeField] private float dashSpeed = 24f;
[SerializeField] private float dashDuration = 0.12f;
[SerializeField] private float dashCooldown = 0.6f;
[SerializeField] private bool dashGrantsInvincibility = true;

private bool _isDashing;
private float _dashCooldownCounter;

private IEnumerator DashCoroutine(Vector2 direction)
{
    _isDashing = true;
    _dashCooldownCounter = dashCooldown;

    if (dashGrantsInvincibility)
        Physics2D.IgnoreLayerCollision(playerLayer, enemyLayer, true);

    _rb.gravityScale = 0f;
    _rb.velocity = direction.normalized * dashSpeed;

    yield return new WaitForSeconds(dashDuration);

    _rb.gravityScale = _defaultGravityScale;
    _isDashing = false;

    if (dashGrantsInvincibility)
        Physics2D.IgnoreLayerCollision(playerLayer, enemyLayer, false);
}

One-Way Platforms

Use Unity's PlatformEffector2D component for pass-through platforms. The player can jump up through them and stand on top.

  • Add PlatformEffector2D to the platform GameObject.
  • Set Surface Arc to 180 (only the top is solid).
  • Enable Use One Way on the effector.
  • On the platform's Collider2D, check Used By Effector.
  • To drop through, temporarily disable the collider or set the effector's rotationalOffset to 180 for a brief period.
private IEnumerator DropThroughPlatform(Collider2D platformCollider)
{
    platformCollider.enabled = false;
    yield return new WaitForSeconds(0.25f);
    platformCollider.enabled = true;
}

State Machine Integration

Organize movement code with explicit states rather than nested booleans. Each state has its own enter/exit/update logic.

Recommended player states for a 2D platformer:

  • Idle - grounded, no input
  • Run - grounded, horizontal input
  • Jump - ascending after jump
  • Fall - airborne, descending
  • WallSlide - against wall, descending slowly
  • Dash - mid-dash, ignoring gravity
  • Hurt - knocked back, input disabled briefly
  • Dead - no input, play death animation

See the state-machine skill for the generic FSM implementation that works with these states.

3D Character Controller

CharacterController vs Rigidbody

CharacterController (built-in):

  • Simpler setup, Move() and SimpleMove() methods
  • Built-in slope and step handling via slopeLimit and stepOffset
  • No physics interactions by default (you push nothing, nothing pushes you)
  • Best for: first/third person games where you want full control

**R

Compartilhar X LinkedIn