Easy Learn C#

Character Movement in Unity

Introduction to Character Movement

Character movement is one of the most fundamental aspects of game development. Well-designed movement controls can make or break the player experience. In Unity, you can implement character movement using a combination of built-in components and custom C# scripts.

This guide covers the following movement types:

  • 2D character movement (side-scrolling and top-down)
  • 3D character movement (first-person and third-person)
  • Physics-based vs. transform-based movement
  • Character controller-based movement

Setting Up Your Character - Step by Step

Before coding movement, you need to properly set up your character in Unity:

For 2D Character:

  1. Create a new GameObject: Right-click in Hierarchy → 2D Object → Sprite
  2. Add a Rigidbody2D: Select your character → Add Component → Physics 2D → Rigidbody 2D
    • For platformers: Set Gravity Scale to 3-5
    • For top-down games: Set Gravity Scale to 0
    • Set Freeze Rotation on Z (check the Z box under Constraints → Freeze Rotation)
  3. Add a Collider: Add Component → Physics 2D → Box Collider 2D (or Capsule Collider 2D)
    • Adjust the collider size to fit your sprite
    • For platformers, make it slightly narrower than the visual sprite for better gameplay
  4. Create an empty C# script: In Project window → Right-click → Create → C# Script → Name it "PlayerMovement" (or similar)
  5. Attach the script to your character: Drag the script onto your character GameObject

For 3D Character:

  1. Import a 3D model or create a primitive (GameObject → 3D Object → Capsule)
  2. Add a Character Controller (recommended): Add Component → Physics → Character Controller
    • Adjust the Height and Radius to match your character model
    • Set Center Y position to half your character's height
  3. Alternative: Add a Rigidbody and Collider
    • Add Component → Physics → Rigidbody
    • Add Component → Physics → Capsule Collider
    • For character movement: Check "Freeze Rotation" on all axes
  4. Create and attach a C# script as described above

Basic 2D Movement

For 2D games, you typically use Vector2 for movement calculations. Here's a simple example of a 2D side-scrolling character controller:

Basic 2D Side-Scrolling Movement:


using UnityEngine;

public class SimpleCharacter2D : MonoBehaviour
{
    // Movement speed in units per second
    public float moveSpeed = 5f;
    
    // Component references
    private Rigidbody2D rb;        // Reference to the physics component
    private SpriteRenderer spriteRenderer;  // Reference to control sprite appearance
    
    // Direction tracking
    private float horizontalInput;  // Stores left/right input value (-1 to 1)
    private bool isFacingRight = true;  // Tracks which way character is facing
    
    void Start()
    {
        // Get component references when the game starts
        rb = GetComponent();       // Get physics component
        spriteRenderer = GetComponent(); // Get sprite renderer
    }
    
    void Update()
    {
        // Get horizontal input (-1 for left, 0 for none, 1 for right)
        // GetAxisRaw provides immediate response without smoothing
        horizontalInput = Input.GetAxisRaw("Horizontal");
        
        // Flip the sprite based on movement direction
        if (horizontalInput > 0 && !isFacingRight)  // Moving right but facing left
        {
            FlipCharacter();  // Flip to face right
        }
        else if (horizontalInput < 0 && isFacingRight)  // Moving left but facing right
        {
            FlipCharacter();  // Flip to face left
        }
    }
    
    void FixedUpdate()
    {
        // Move the character using physics (in FixedUpdate for consistent physics)
        // We keep the current vertical velocity (y) and only change horizontal (x)
        rb.velocity = new Vector2(horizontalInput * moveSpeed, rb.velocity.y);
    }
    
    // Flip the character's facing direction
    void FlipCharacter()
    {
        isFacingRight = !isFacingRight;  // Toggle facing direction flag
        spriteRenderer.flipX = !spriteRenderer.flipX;  // Flip the sprite visually
    }
}
                            

Key concepts in this script:

  • Input.GetAxisRaw("Horizontal") - Returns -1 (left), 0 (no input), or 1 (right)
  • Rigidbody2D.velocity - Controls the physics-based movement
  • FixedUpdate() - Used for physics calculations at fixed time intervals
  • FlipCharacter() - Changes the character's facing direction based on movement

Tip: Smoothing Movement

To create smoother movement, you can use Input.GetAxis() instead of Input.GetAxisRaw(). GetAxis provides built-in smoothing.

For custom smoothing, you can use Mathf.Lerp or Mathf.SmoothDamp:


// Smooth horizontal movement with Lerp
float targetVelocityX = horizontalInput * moveSpeed;  // Calculate the desired velocity
float smoothVelocityX = Mathf.Lerp(rb.velocity.x, targetVelocityX, smoothFactor * Time.fixedDeltaTime);  // Gradually change to target velocity
rb.velocity = new Vector2(smoothVelocityX, rb.velocity.y);  // Apply the smoothed velocity
                            

Top-Down 2D Movement:


using UnityEngine;

public class TopDown2DController : MonoBehaviour
{
    public float moveSpeed = 5f;  // Character movement speed
    private Rigidbody2D rb;       // Reference to physics component
    private Vector2 movement;     // Stores both x and y movement input
    
    void Start()
    {
        // Get the Rigidbody2D component attached to this GameObject
        rb = GetComponent();
    }
    
    void Update()
    {
        // Get input from both horizontal (left/right) and vertical (up/down) axes
        movement.x = Input.GetAxisRaw("Horizontal");  // Left/right movement
        movement.y = Input.GetAxisRaw("Vertical");    // Up/down movement
        
        // Normalize for consistent speed in all directions
        // This prevents diagonal movement from being faster (Pythagorean theorem)
        if (movement.magnitude > 1)
        {
            movement.Normalize();  // Makes the vector have a magnitude of 1
        }
    }
    
    void FixedUpdate()
    {
        // Move the character using MovePosition for more precise control
        // rb.position is current position + movement direction * speed * time
        rb.MovePosition(rb.position + movement * moveSpeed * Time.fixedDeltaTime);
    }
}
                            

Implementation Steps for 2D Movement:

  1. Create your input handling: Decide between GetAxis (smooth) or GetAxisRaw (responsive)
  2. Choose a movement method:
    • Rigidbody2D.velocity - Good for platformers and physics-based movement
    • Rigidbody2D.MovePosition - More precise, less physics interaction
    • transform.Translate - For non-physics movement (not recommended with Rigidbody)
  3. Add character flipping: Either by flipping the sprite or rotating the transform
  4. Add acceleration/deceleration: For more natural movement
  5. Test and adjust values: Tune moveSpeed and other parameters for your game's feel

Advanced 2D Platform Movement

A more comprehensive platformer controller with jumping, better ground detection, and variable jump height:

Advanced Platformer Controller:


using UnityEngine;

public class PlatformerController : MonoBehaviour
{
    [Header("Movement Settings")]
    public float moveSpeed = 7f;       // Base horizontal movement speed
    public float acceleration = 60f;    // How quickly character reaches max speed
    public float deceleration = 60f;    // How quickly character slows down
    public float airControl = 0.5f;     // Reduced control multiplier when in air (0-1)
    
    [Header("Jump Settings")]
    public float jumpForce = 16f;       // Initial upward force when jumping
    public float fallMultiplier = 2.5f; // Gravity multiplier when falling (faster falls)
    public float lowJumpMultiplier = 2f; // Gravity multiplier for short jumps
    public float jumpBufferTime = 0.1f;  // Time window to queue a jump before landing
    public float coyoteTime = 0.15f;     // Time window to jump after leaving a platform
    
    [Header("Ground Detection")]
    public Transform groundCheck;        // Position to check for ground beneath player
    public float groundCheckRadius = 0.1f; // Size of ground detection circle
    public LayerMask groundLayer;        // Which layers count as ground
    
    // Private variables
    private Rigidbody2D rb;              // Physics component reference
    private SpriteRenderer spriteRenderer; // Visual component reference
    private float horizontalInput;         // Left/right input value
    private float currentSpeed = 0f;       // Current horizontal speed (with acceleration applied)
    private bool isGrounded;               // Is the player touching ground?
    private bool isFacingRight = true;     // Which way is player facing
    private float lastGroundedTime;        // Time tracking for coyote time
    private float jumpBufferCounter;       // Time tracking for jump buffer
    
    void Start()
    {
        // Get component references
        rb = GetComponent();
        spriteRenderer = GetComponent();
    }
    
    void Update()
    {
        // Get input
        horizontalInput = Input.GetAxisRaw("Horizontal");
        
        // Ground check using physics overlap circle at groundCheck position
        isGrounded = Physics2D.OverlapCircle(groundCheck.position, groundCheckRadius, groundLayer);
        
        // Update coyote time - track when we last touched ground
        if (isGrounded)
        {
            lastGroundedTime = Time.time;  // Record current time when grounded
        }
        
        // Jump buffer timing - gives player a grace period to press jump before landing
        if (Input.GetButtonDown("Jump"))
        {
            jumpBufferCounter = jumpBufferTime;  // Start the jump buffer timer
        }
        else
        {
            jumpBufferCounter -= Time.deltaTime;  // Count down jump buffer timer
        }
        
        // Jump logic - works if jump pressed recently AND touched ground recently
        if (jumpBufferCounter > 0 && Time.time - lastGroundedTime <= coyoteTime)
        {
            Jump();  // Execute jump
            jumpBufferCounter = 0;  // Reset jump buffer to prevent double jumps
        }
        
        // Variable jump height logic
        if (rb.velocity.y < 0)
        {
            // Falling - apply higher gravity for faster falling
            rb.velocity += Vector2.up * Physics2D.gravity.y * (fallMultiplier - 1) * Time.deltaTime;
        }
        else if (rb.velocity.y > 0 && !Input.GetButton("Jump"))
        {
            // Rising but jump button released - apply higher gravity for shorter jump
            rb.velocity += Vector2.up * Physics2D.gravity.y * (lowJumpMultiplier - 1) * Time.deltaTime;
        }
        
        // Flip character to face movement direction
        if (horizontalInput > 0 && !isFacingRight)
        {
            FlipCharacter();
        }
        else if (horizontalInput < 0 && isFacingRight)
        {
            FlipCharacter();
        }
    }
    
    void FixedUpdate()
    {
        // Calculate target speed based on input
        float targetSpeed = horizontalInput * moveSpeed;
        
        // Calculate acceleration rate based on if we're speeding up or slowing down
        float accelRate = (Mathf.Abs(targetSpeed) > 0.01f) ? acceleration : deceleration;
        
        // Reduce acceleration in air for less control while jumping
        if (!isGrounded)
        {
            accelRate *= airControl;  // Multiply by air control factor (usually less than 1)
        }
        
        // Apply acceleration to current speed
        float speedDif = targetSpeed - currentSpeed;  // Difference between current and target
        float movement = speedDif * accelRate * Time.fixedDeltaTime;  // Calculate change amount
        currentSpeed += movement;  // Apply the change
        
        // Apply the calculated movement to the rigidbody
        rb.velocity = new Vector2(currentSpeed, rb.velocity.y);
    }
    
    // Apply upward force for jumping
    void Jump()
    {
        rb.velocity = new Vector2(rb.velocity.x, jumpForce);  // Set upward velocity directly
    }
    
    // Flip the character's facing direction
    void FlipCharacter()
    {
        isFacingRight = !isFacingRight;  // Toggle direction flag
        spriteRenderer.flipX = !spriteRenderer.flipX;  // Flip sprite visually
    }
    
    // Visualize the ground check radius in the editor (for debugging)
    void OnDrawGizmosSelected()
    {
        if (groundCheck == null) return;
        
        // Draw green sphere if grounded, red if not
        Gizmos.color = isGrounded ? Color.green : Color.red;
        Gizmos.DrawWireSphere(groundCheck.position, groundCheckRadius);
    }
}
                            

Advanced features in this controller:

  • Coyote Time - Allows the player to jump shortly after leaving a platform
  • Jump Buffering - Queues a jump if the button is pressed slightly before landing
  • Variable Jump Height - Shorter jumps when the button is released early
  • Acceleration/Deceleration - Smooth start and stop movement
  • Air Control - Configurable control while in the air
  • Precise Ground Detection - Using Physics2D.OverlapCircle for better accuracy

Setting Up Ground Detection:

  1. Create an empty GameObject as a child of your character
  2. Position it at the bottom of your character's collider
  3. In the Inspector, assign this Transform to the groundCheck field
  4. Create a new Layer called "Ground"
  5. Assign all your platforms/ground objects to this layer
  6. In the inspector, set the groundLayer to the Ground layer

3D Character Movement

For 3D games, Unity offers several options for character movement, including the Character Controller component, Rigidbody physics, or direct Transform manipulation.

First-Person Movement with Character Controller:


using UnityEngine;

public class FirstPersonController : MonoBehaviour
{
    [Header("Movement Settings")]
    public float moveSpeed = 5f;     // Normal walking speed
    public float sprintSpeed = 8f;    // Running speed when holding sprint key
    public float crouchSpeed = 2.5f;  // Slower speed when crouching
    public float jumpHeight = 2f;     // How high the character jumps
    
    [Header("Ground Settings")]
    public Transform groundCheck;     // Position to check for ground
    public float groundDistance = 0.4f; // Distance to check for ground
    public LayerMask groundMask;      // Which layers count as ground
    
    [Header("Camera Settings")]
    public Transform cameraTransform;  // Reference to the player's camera
    public float mouseSensitivity = 100f; // Mouse look sensitivity
    public bool lockCursor = true;     // Whether to lock mouse to game window
    
    // Private variables
    private CharacterController controller; // Unity's built-in character controller
    private Vector3 velocity;             // Current movement velocity
    private bool isGrounded;              // Is player touching ground?
    private float xRotation = 0f;         // Camera up/down rotation
    private float currentSpeed;           // Current movement speed
    private float gravity = -9.81f;       // Gravity strength
    
    void Start()
    {
        // Get the character controller component
        controller = GetComponent();
        currentSpeed = moveSpeed;  // Start with normal move speed
        
        // Lock and hide cursor for first-person look
        if (lockCursor)
        {
            Cursor.lockState = CursorLockMode.Locked;
            Cursor.visible = false;
        }
    }
    
    void Update()
    {
        // Check if grounded using a sphere cast
        isGrounded = Physics.CheckSphere(groundCheck.position, groundDistance, groundMask);
        
        // Reset downward velocity when grounded to prevent accumulation
        if (isGrounded && velocity.y < 0)
        {
            velocity.y = -2f; // Small negative value instead of zero for better grounding
        }
        
        // Mouse look handling
        float mouseX = Input.GetAxis("Mouse X") * mouseSensitivity * Time.deltaTime;
        float mouseY = Input.GetAxis("Mouse Y") * mouseSensitivity * Time.deltaTime;
        
        // Calculate camera rotation - vertical rotation
        xRotation -= mouseY;  // Subtract to invert the mouse Y axis
        xRotation = Mathf.Clamp(xRotation, -90f, 90f); // Prevent looking too far up/down
        
        // Apply rotations - vertical to camera, horizontal to player
        cameraTransform.localRotation = Quaternion.Euler(xRotation, 0f, 0f);
        transform.Rotate(Vector3.up * mouseX);  // Rotate player horizontally
        
        // Movement inputs from keyboard
        float x = Input.GetAxis("Horizontal");  // A/D or left/right arrows
        float z = Input.GetAxis("Vertical");    // W/S or up/down arrows
        
        // Speed control - sprint, crouch, or normal movement
        if (Input.GetKey(KeyCode.LeftShift))
        {
            currentSpeed = sprintSpeed;  // Sprint when shift is held
        }
        else if (Input.GetKey(KeyCode.LeftControl))
        {
            currentSpeed = crouchSpeed;  // Crouch when ctrl is held
        }
        else
        {
            currentSpeed = moveSpeed;    // Normal speed otherwise
        }
        
        // Calculate movement direction relative to where the player is facing
        Vector3 move = transform.right * x + transform.forward * z;
        
        // Apply movement using character controller
        controller.Move(move * currentSpeed * Time.deltaTime);
        
        // Jumping when grounded and jump button pressed
        if (Input.GetButtonDown("Jump") && isGrounded)
        {
            // Jump formula: v = sqrt(h * -2 * g) - physics formula for initial velocity
            velocity.y = Mathf.Sqrt(jumpHeight * -2f * gravity);
        }
        
        // Apply gravity to vertical velocity
        velocity.y += gravity * Time.deltaTime;
        
        // Apply vertical movement (jumping/falling)
        controller.Move(velocity * Time.deltaTime);
    }
}
                            

Setting Up First-Person Movement:

  1. Create a Camera Setup:
    • Add a Camera as a child of your character
    • Position it at eye level (typically around Y=1.6 for human-sized characters)
    • Assign this camera to the cameraTransform field
  2. Set Up Ground Check:
    • Create an empty GameObject as a child of your character
    • Position it at the bottom of your character
    • Assign to the groundCheck field
  3. Configure Layers:
    • Create a "Ground" layer
    • Assign all walkable surfaces to this layer
    • Set the groundMask field to the Ground layer
  4. Adjust Character Controller Settings:
    • Set Skin Width to 0.01-0.05
    • Set Step Offset to 0.3-0.5 for stair climbing

Third-Person Character Movement

Third-person movement typically requires camera-relative movement and more complex character controls:

Third-Person Controller:


using UnityEngine;

public class ThirdPersonController : MonoBehaviour
{
    [Header("Movement Settings")]
    public float moveSpeed = 5f;     // Normal walking speed
    public float sprintSpeed = 8f;    // Running speed when sprinting
    public float rotationSpeed = 10f; // How fast character turns to face movement direction
    public float jumpForce = 8f;      // How high character jumps
    
    [Header("Ground Detection")]
    public float groundCheckDistance = 0.3f; // Distance to check for ground
    public LayerMask groundMask;            // Which layers count as ground
    
    [Header("Camera")]
    public Transform cameraTransform;        // Reference to the camera
    
    // Private variables
    private CharacterController controller;  // Unity's character controller
    private Transform playerTransform;       // Reference to player's transform
    private Vector3 moveDirection;           // Current movement direction
    private float currentSpeed;              // Current movement speed
    private bool isGrounded;                 // Is the character on the ground?
    private float verticalVelocity;          // Vertical speed (for jumping/falling)
    private float gravity = -9.81f;          // Gravity strength
    
    void Start()
    {
        // Get component references
        controller = GetComponent();
        playerTransform = transform;
        currentSpeed = moveSpeed;  // Start with normal speed
    }
    
    void Update()
    {
        // Ground check using a sphere cast at player's feet
        isGrounded = Physics.CheckSphere(
            transform.position + Vector3.down * controller.height/2, 
            groundCheckDistance, 
            groundMask
        );
        
        // Reset vertical velocity when grounded to prevent accumulation
        if (isGrounded && verticalVelocity < 0)
        {
            verticalVelocity = -2f;  // Small negative value for better grounding
        }
        
        // Get keyboard movement input
        float horizontal = Input.GetAxis("Horizontal");  // A/D keys or left/right arrows
        float vertical = Input.GetAxis("Vertical");      // W/S keys or up/down arrows
        
        // Set speed based on sprint input (shift key)
        currentSpeed = Input.GetKey(KeyCode.LeftShift) ? sprintSpeed : moveSpeed;
        
        // Calculate movement direction relative to camera
        Vector3 forward = cameraTransform.forward;  // Camera's forward direction
        Vector3 right = cameraTransform.right;      // Camera's right direction
        
        // Project forward and right vectors on the horizontal plane (remove y component)
        forward.y = 0;
        right.y = 0;
        forward.Normalize();  // Ensure vector length is 1
        right.Normalize();    // Ensure vector length is 1
        
        // Calculate the move direction in world space based on camera orientation
        // This creates camera-relative movement (forward is camera forward, not world forward)
        moveDirection = forward * vertical + right * horizontal;
        
        // Normalize movement vector to prevent faster diagonal movement
        if (moveDirection.magnitude > 1f)
        {
            moveDirection.Normalize();  // Make vector length 1
        }
        
        // Apply movement using character controller
        controller.Move(moveDirection * currentSpeed * Time.deltaTime);
        
        // Rotate player to face movement direction
        if (moveDirection != Vector3.zero)
        {
            // Create rotation that looks in movement direction
            Quaternion targetRotation = Quaternion.LookRotation(moveDirection);
            
            // Smoothly rotate from current to target rotation
            playerTransform.rotation = Quaternion.Slerp(
                playerTransform.rotation, 
                targetRotation, 
                rotationSpeed * Time.deltaTime
            );
        }
        
        // Jump when space is pressed and grounded
        if (Input.GetButtonDown("Jump") && isGrounded)
        {
            verticalVelocity = jumpForce;  // Set upward velocity
        }
        
        // Apply gravity to vertical velocity
        verticalVelocity += gravity * Time.deltaTime;
        
        // Apply vertical movement (jumping/falling)
        controller.Move(Vector3.up * verticalVelocity * Time.deltaTime);
    }
    
    void OnDrawGizmosSelected()
    {
        // Draw ground check sphere in Scene view for debugging
        Gizmos.color = isGrounded ? Color.green : Color.red;
        Vector3 groundCheckPos = transform.position + Vector3.down * controller.height/2;
        Gizmos.DrawWireSphere(groundCheckPos, groundCheckDistance);
    }
}
                            

Implementing a Camera Follow System:

The following script creates a third-person camera that follows the player:


using UnityEngine;

public class ThirdPersonCamera : MonoBehaviour
{
    public Transform target;               // Player character to follow
    public float distance = 5f;            // Distance from camera to player
    public float height = 2f;              // Height offset of camera
    public float smoothSpeed = 10f;        // How smoothly camera follows player
    public float rotationSmoothSpeed = 5f; // How smoothly camera rotates
    
    // Mouse input settings
    public float mouseSensitivity = 100f;  // Mouse sensitivity for camera rotation
    public bool invertY = false;           // Whether to invert vertical mouse
    
    // Collision settings
    public float minDistance = 1f;         // Minimum camera distance (for collision)
    public LayerMask collisionMask;        // Which layers block camera
    
    // Private variables
    private float currentRotationX = 0f;   // Current horizontal rotation
    private float currentRotationY = 0f;   // Current vertical rotation
    private Vector3 currentRotation;       // Combined rotation vector
    private Vector3 smoothVelocity = Vector3.zero; // For smooth damping
    
    void Start()
    {
        // Lock cursor
        Cursor.lockState = CursorLockMode.Locked;
        Cursor.visible = false;
        
        // Initial rotation based on target
        Vector3 angles = transform.eulerAngles;
        currentRotationX = angles.y;
        currentRotationY = angles.x;
    }
    
    void LateUpdate()
    {
        if (target == null) return;
        
        // Mouse input
        float mouseX = Input.GetAxis("Mouse X") * mouseSensitivity * Time.deltaTime;
        float mouseY = Input.GetAxis("Mouse Y") * mouseSensitivity * Time.deltaTime;
        
        // Adjust rotation based on mouse input
        currentRotationX += mouseX;
        currentRotationY += (invertY ? 1 : -1) * mouseY;
        currentRotationY = Mathf.Clamp(currentRotationY, -60f, 60f);
        
        // Calculate desired rotation
        currentRotation = Vector3.SmoothDamp(
            currentRotation, 
            new Vector3(currentRotationY, currentRotationX, 0), 
            ref smoothVelocity, 
            1f / rotationSmoothSpeed
        );
        
        // Convert to quaternion
        Quaternion rotation = Quaternion.Euler(currentRotation);
        
        // Calculate camera position
        Vector3 targetPosition = target.position + Vector3.up * height;
        Vector3 direction = rotation * Vector3.back;
        float targetDistance = distance;
        
        // Camera collision detection
        RaycastHit hit;
        if (Physics.Raycast(targetPosition, direction, out hit, distance, collisionMask))
        {
            targetDistance = Mathf.Clamp(hit.distance, minDistance, distance);
        }
        
        // Set camera position and rotation
        transform.position = targetPosition + direction * targetDistance;
        transform.rotation = rotation;
    }
}
                        

Setting Up a Third-Person System:

  1. Set up your character with a Character Controller
  2. Create a custom camera rig:
    • Create an empty GameObject for the camera
    • Attach the ThirdPersonCamera script to it
    • Assign your player character to the target field
  3. Create animation parameters (if using Animator):
    • Speed (float) - For controlling walk/run animations
    • IsGrounded (bool) - For jump/fall animations
    • JumpTrigger (trigger) - For initiating jump animations
  4. Refine the movement feel:
    • Adjust moveSpeed and rotationSpeed
    • Fine-tune ground detection settings
    • Adjust camera follow parameters

Improving Movement Feel

Creating "game feel" is crucial for making movement enjoyable. Here are some techniques to enhance your character movement:

1. Add Acceleration and Deceleration


// Instead of directly setting velocity
float targetSpeed = input * maxSpeed;  // Calculate target speed from input
float speedDif = targetSpeed - currentSpeed;  // How far we are from target speed
float accelRate = (Mathf.Abs(targetSpeed) > 0.01f) ? acceleration : deceleration;  // Use acceleration or deceleration
float movement = Mathf.Pow(Mathf.Abs(speedDif) * accelRate, 0.9f) * Mathf.Sign(speedDif);  // Non-linear acceleration
currentSpeed += movement * Time.deltaTime;  // Apply to current speed
                        

2. Add Different Movement States

  • Walking, Running, Sprinting - Different speed modifiers
  • Crouching - Reduced speed, lower collision height
  • Air Control - Limited movement while jumping/falling

3. Add Motion Variety


// Add subtle head bobbing for first-person
void HeadBob()
{
    if (!isGrounded) return;  // Don't bob in air
    
    if (moveDirection.magnitude > 0.1f)  // Only bob when moving
    {
        // Calculate bob amount based on walking speed and time
        // Sin wave creates up/down motion based on time
        float bobAmount = Mathf.Sin(Time.time * bobFrequency) * bobMagnitude;
        
        // Apply bobbing to camera position
        cameraTransform.localPosition = new Vector3(
            cameraTransform.localPosition.x,  // Keep x the same
            defaultCameraY + bobAmount,       // Add bobbing to default height
            cameraTransform.localPosition.z   // Keep z the same
        );
    }
    else
    {
        // Smoothly return to default position when not moving
        cameraTransform.localPosition = new Vector3(
            cameraTransform.localPosition.x,  // Keep x the same
            Mathf.Lerp(cameraTransform.localPosition.y, defaultCameraY, Time.deltaTime * 5f),  // Smooth return
            cameraTransform.localPosition.z   // Keep z the same
        );
    }
}
                        

4. Add Movement Feedback

  • Audio - Footstep sounds, jump/land sounds
  • Visual Effects - Dust particles when running/landing
  • Camera Shake - Small shake on landing or impacts
  • Animations - Blend between different animation states

5. Variable Jump Height


// For more responsive jumping
if (rb.velocity.y < 0)
{
    // Apply higher gravity when falling for faster descent
    rb.velocity += Vector2.up * Physics2D.gravity.y * (fallMultiplier - 1) * Time.deltaTime;
}
else if (rb.velocity.y > 0 && !Input.GetButton("Jump"))
{
    // Apply higher gravity when rising but jump button released
    // This creates shorter jumps when button is tapped vs held
    rb.velocity += Vector2.up * Physics2D.gravity.y * (lowJumpMultiplier - 1) * Time.deltaTime;
}
                        

Common Character Movement Problems and Solutions

Problem: Character slides down slopes

Solution: For Character Controller, set Slope Limit appropriately. For Rigidbody, apply counterforce:


// Calculate the ground normal (direction perpendicular to the surface)
RaycastHit hit;
if (Physics.Raycast(transform.position, Vector3.down, out hit, groundCheckDistance))
{
    // If on a slope
    if (hit.normal != Vector3.up)
    {
        // Calculate direction along the slope (projection of down vector onto the surface)
        Vector3 slopeDirection = Vector3.ProjectOnPlane(Vector3.down, hit.normal);
        
        // Apply force in the opposite direction to prevent sliding
        rigidbody.AddForce(-slopeDirection * slopeSlideForce);
    }
}
                            

Problem: Character gets stuck on edges

Solution: For Character Controller, increase the Step Offset. For colliders, use a slightly narrower collider than your visual model:


// For a capsule character in 2D platformers
CapsuleCollider2D collider = GetComponent();
collider.size = new Vector2(0.8f, 1.8f); // Narrower than the sprite
                            

Problem: Jittery movement

Solution: Use FixedUpdate for physics operations and interpolation for smoother visuals:


// Use Vector3.Lerp or SmoothDamp for visual representations
void Update()
{
    // Visual update for smooth movement
    if (visualModel != null && rb != null)
    {
        // Interpolate visual position to physics position for smoother appearance
        visualModel.position = Vector3.Lerp(
            visualModel.position,  // Current visual position
            rb.position,           // Target physics position
            visualSmoothFactor * Time.deltaTime  // Smoothing factor
        );
    }
}
                            

Problem: Bunny hopping (jump spam)

Solution: Add a jump cooldown or use a jump buffer system:


// Jump cooldown
float jumpCooldown = 0.2f;  // Time required between jumps
float lastJumpTime = -jumpCooldown;  // Last time we jumped (negative for immediate first jump)

void Update()
{
    // Only allow jump if:
    // 1. Jump button pressed
    // 2. Player is grounded
    // 3. Enough time has passed since last jump
    if (Input.GetButtonDown("Jump") && isGrounded && (Time.time - lastJumpTime) >= jumpCooldown)
    {
        Jump();  // Perform jump
        lastJumpTime = Time.time;  // Record jump time
    }
}