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:
- Create a new GameObject: Right-click in Hierarchy → 2D Object → Sprite
- 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)
- 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
- Create an empty C# script: In Project window → Right-click → Create → C# Script → Name it "PlayerMovement" (or similar)
- Attach the script to your character: Drag the script onto your character GameObject
For 3D Character:
- Import a 3D model or create a primitive (GameObject → 3D Object → Capsule)
- 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
- Alternative: Add a Rigidbody and Collider
- Add Component → Physics → Rigidbody
- Add Component → Physics → Capsule Collider
- For character movement: Check "Freeze Rotation" on all axes
- 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:
- Create your input handling: Decide between GetAxis (smooth) or GetAxisRaw (responsive)
- Choose a movement method:
Rigidbody2D.velocity- Good for platformers and physics-based movementRigidbody2D.MovePosition- More precise, less physics interactiontransform.Translate- For non-physics movement (not recommended with Rigidbody)
- Add character flipping: Either by flipping the sprite or rotating the transform
- Add acceleration/deceleration: For more natural movement
- 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:
- Create an empty GameObject as a child of your character
- Position it at the bottom of your character's collider
- In the Inspector, assign this Transform to the groundCheck field
- Create a new Layer called "Ground"
- Assign all your platforms/ground objects to this layer
- 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:
- 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
- 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
- Configure Layers:
- Create a "Ground" layer
- Assign all walkable surfaces to this layer
- Set the groundMask field to the Ground layer
- 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:
- Set up your character with a Character Controller
- 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
- 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
- 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
}
}