Developing a 2D side-scrolling game in Unity requires a well-functioning player movement system to provide a smooth and enjoyable gaming experience. This comprehensive guide will walk you through creating a robust player movement script using C#, covering horizontal movement, jumping mechanics, and ground detection. By the end of this tutorial, you'll have a solid foundation upon which you can build and expand your game's movement features.
Start by launching Unity and creating a new project:
To visualize the player, add a sprite to your scene:
Player.Sprite Renderer component (you can use the default square or import a custom sprite).To enable physics-based movement, add necessary components to the player:
Player GameObject.Rigidbody2D component.Body Type is set to Dynamic.BoxCollider2D or CircleCollider2D for collision detection.Now, create the script that will handle player movement:
Assets folder.PlayerMovement.Replace the default code with the following comprehensive script:
// PlayerMovement.cs
using UnityEngine;
public class PlayerMovement : MonoBehaviour
{
[Header("Movement Settings")]
public float moveSpeed = 5f; // Horizontal movement speed
public float jumpForce = 12f; // Upward jump force
[Header("Ground Detection Settings")]
public Transform groundCheck; // Empty GameObject to check ground
public float groundCheckRadius = 0.2f; // Radius for ground detection
public LayerMask groundLayer; // Layer for ground objects
private Rigidbody2D rb; // Rigidbody2D component
private bool isGrounded; // Whether the player is on the ground
private float moveInput; // Horizontal input
private void Start()
{
// Initialize Rigidbody2D component
rb = GetComponent<Rigidbody2D>();
}
private void Update()
{
// Get horizontal input
moveInput = Input.GetAxisRaw("Horizontal");
// Check for jump input
if (Input.GetButtonDown("Jump") && isGrounded)
{
Jump();
}
}
private void FixedUpdate()
{
// Handle horizontal movement
rb.velocity = new Vector2(moveInput * moveSpeed, rb.velocity.y);
// Check if the player is grounded
isGrounded = Physics2D.OverlapCircle(groundCheck.position, groundCheckRadius, groundLayer);
}
private void Jump()
{
// Apply jump force
rb.velocity = new Vector2(rb.velocity.x, jumpForce);
}
// Optional: Visualize ground check in the editor
private void OnDrawGizmosSelected()
{
if (groundCheck != null)
{
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(groundCheck.position, groundCheckRadius);
}
}
}
The script includes:
Physics2D.OverlapCircle to detect if the player is on the ground.Update and FixedUpdate methods for smooth physics interactions.OnDrawGizmosSelected helps visualize the ground check area in the Unity Editor.Player GameObject in the Hierarchy.PlayerMovement script onto the Inspector panel.Ground detection ensures that the player can only jump when touching the ground:
Player:
Player in the Hierarchy and select Create Empty.GroundCheck.GroundCheck to the groundCheck field in the PlayerMovement script.Ground.Ground layer to all ground-related GameObjects (e.g., platforms, terrain).PlayerMovement script, set the Ground Layer to the Ground layer.Ensure the Rigidbody2D is set up correctly:
Player GameObject.Rigidbody2D component.Gravity Scale to a value that feels natural (e.g., 3).Freeze Rotation Z is enabled to prevent the player from rotating.For more realistic movement, implement acceleration and deceleration:
// Enhanced PlayerMovement.cs
using UnityEngine;
public class PlayerMovement : MonoBehaviour
{
[Header("Movement Settings")]
public float moveSpeed = 5f;
public float acceleration = 10f;
public float deceleration = 10f;
public float jumpForce = 12f;
[Header("Ground Detection Settings")]
public Transform groundCheck;
public float groundCheckRadius = 0.2f;
public LayerMask groundLayer;
private Rigidbody2D rb;
private bool isGrounded;
private float moveInput;
private void Start()
{
rb = GetComponent<Rigidbody2D>();
}
private void Update()
{
moveInput = Input.GetAxisRaw("Horizontal");
if (Input.GetButtonDown("Jump") && isGrounded)
{
Jump();
}
}
private void FixedUpdate()
{
isGrounded = Physics2D.OverlapCircle(groundCheck.position, groundCheckRadius, groundLayer);
float targetSpeed = moveInput * moveSpeed;
float speedDifference = targetSpeed - rb.velocity.x;
float accelRate = (Mathf.Abs(targetSpeed) > 0.01f) ? acceleration : deceleration;
float movement = Mathf.Pow(Mathf.Abs(speedDifference) * accelRate, 0.9f) * Mathf.Sign(speedDifference);
rb.AddForce(movement * Vector2.right);
// Limit the maximum speed
if (Mathf.Abs(rb.velocity.x) > moveSpeed)
{
rb.velocity = new Vector2(Mathf.Sign(rb.velocity.x) * moveSpeed, rb.velocity.y);
}
}
private void Jump()
{
rb.velocity = new Vector2(rb.velocity.x, jumpForce);
}
private void OnDrawGizmosSelected()
{
if (groundCheck != null)
{
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(groundCheck.position, groundCheckRadius);
}
}
}
This enhancement introduces:
moveSpeed.To make jumping more responsive and forgiving, add a jump buffer and coyote time:
// Enhanced PlayerMovement with Jump Buffer and Coyote Time
using UnityEngine;
public class PlayerMovement : MonoBehaviour
{
[Header("Movement Settings")]
public float moveSpeed = 5f;
public float acceleration = 10f;
public float deceleration = 10f;
public float jumpForce = 12f;
[Header("Ground Detection Settings")]
public Transform groundCheck;
public float groundCheckRadius = 0.2f;
public LayerMask groundLayer;
[Header("Jump Settings")]
public float jumpBufferTime = 0.2f;
public float coyoteTime = 0.2f;
private Rigidbody2D rb;
private bool isGrounded;
private float moveInput;
private float jumpBufferCounter;
private float coyoteCounter;
private void Start()
{
rb = GetComponent<Rigidbody2D>();
}
private void Update()
{
moveInput = Input.GetAxisRaw("Horizontal");
if (Input.GetButtonDown("Jump"))
{
jumpBufferCounter = jumpBufferTime;
}
else
{
jumpBufferCounter -= Time.deltaTime;
}
}
private void FixedUpdate()
{
isGrounded = Physics2D.OverlapCircle(groundCheck.position, groundCheckRadius, groundLayer);
if (isGrounded)
{
coyoteCounter = coyoteTime;
}
else
{
coyoteCounter -= Time.fixedDeltaTime;
}
// Jump logic with buffer and coyote time
if (jumpBufferCounter > 0f && coyoteCounter > 0f)
{
Jump();
jumpBufferCounter = 0f;
coyoteCounter = 0f;
}
// Movement
float targetSpeed = moveInput * moveSpeed;
float speedDifference = targetSpeed - rb.velocity.x;
float accelRate = (Mathf.Abs(targetSpeed) > 0.01f) ? acceleration : deceleration;
float movement = Mathf.Pow(Mathf.Abs(speedDifference) * accelRate, 0.9f) * Mathf.Sign(speedDifference);
rb.AddForce(movement * Vector2.right);
// Limit the maximum speed
if (Mathf.Abs(rb.velocity.x) > moveSpeed)
{
rb.velocity = new Vector2(Mathf.Sign(rb.velocity.x) * moveSpeed, rb.velocity.y);
}
}
private void Jump()
{
rb.velocity = new Vector2(rb.velocity.x, jumpForce);
}
private void OnDrawGizmosSelected()
{
if (groundCheck != null)
{
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(groundCheck.position, groundCheckRadius);
}
}
}
This addition provides:
To make the player face the direction of movement, implement character flipping:
// Enhanced PlayerMovement with Character Flipping
using UnityEngine;
public class PlayerMovement : MonoBehaviour
{
[Header("Movement Settings")]
public float moveSpeed = 5f;
public float acceleration = 10f;
public float deceleration = 10f;
public float jumpForce = 12f;
[Header("Ground Detection Settings")]
public Transform groundCheck;
public float groundCheckRadius = 0.2f;
public LayerMask groundLayer;
[Header("Jump Settings")]
public float jumpBufferTime = 0.2f;
public float coyoteTime = 0.2f;
private Rigidbody2D rb;
private bool isGrounded;
private float moveInput;
private float jumpBufferCounter;
private float coyoteCounter;
private bool isFacingRight = true;
private void Start()
{
rb = GetComponent<Rigidbody2D>();
}
private void Update()
{
moveInput = Input.GetAxisRaw("Horizontal");
if (Input.GetButtonDown("Jump"))
{
jumpBufferCounter = jumpBufferTime;
}
else
{
jumpBufferCounter -= Time.deltaTime;
}
// Handle character flipping
if (moveInput > 0 && !isFacingRight)
{
Flip();
}
else if (moveInput < 0 && isFacingRight)
{
Flip();
}
}
private void FixedUpdate()
{
isGrounded = Physics2D.OverlapCircle(groundCheck.position, groundCheckRadius, groundLayer);
if (isGrounded)
{
coyoteCounter = coyoteTime;
}
else
{
coyoteCounter -= Time.fixedDeltaTime;
}
// Jump logic with buffer and coyote time
if (jumpBufferCounter > 0f && coyoteCounter > 0f)
{
Jump();
jumpBufferCounter = 0f;
coyoteCounter = 0f;
}
// Movement
float targetSpeed = moveInput * moveSpeed;
float speedDifference = targetSpeed - rb.velocity.x;
float accelRate = (Mathf.Abs(targetSpeed) > 0.01f) ? acceleration : deceleration;
float movement = Mathf.Pow(Mathf.Abs(speedDifference) * accelRate, 0.9f) * Mathf.Sign(speedDifference);
rb.AddForce(movement * Vector2.right);
// Limit the maximum speed
if (Mathf.Abs(rb.velocity.x) > moveSpeed)
{
rb.velocity = new Vector2(Mathf.Sign(rb.velocity.x) * moveSpeed, rb.velocity.y);
}
}
private void Jump()
{
rb.velocity = new Vector2(rb.velocity.x, jumpForce);
}
private void Flip()
{
isFacingRight = !isFacingRight;
Vector3 scaler = transform.localScale;
scaler.x *= -1;
transform.localScale = scaler;
}
private void OnDrawGizmosSelected()
{
if (groundCheck != null)
{
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(groundCheck.position, groundCheckRadius);
}
}
}
Features added:
isFacingRight boolean.Integrate animations to enhance visual feedback:
Animator Controller by right-clicking in the Project window and selecting Create > Animator Controller.PlayerAnimator.Animator component on the Player.Idle, Run, and Jump.Animator component.// Enhanced PlayerMovement with Animation
using UnityEngine;
public class PlayerMovement : MonoBehaviour
{
[Header("Movement Settings")]
public float moveSpeed = 5f;
public float acceleration = 10f;
public float deceleration = 10f;
public float jumpForce = 12f;
[Header("Ground Detection Settings")]
public Transform groundCheck;
public float groundCheckRadius = 0.2f;
public LayerMask groundLayer;
[Header("Jump Settings")]
public float jumpBufferTime = 0.2f;
public float coyoteTime = 0.2f;
private Rigidbody2D rb;
private Animator anim;
private bool isGrounded;
private float moveInput;
private float jumpBufferCounter;
private float coyoteCounter;
private bool isFacingRight = true;
private void Start()
{
rb = GetComponent<Rigidbody2D>();
anim = GetComponent<Animator>();
}
private void Update()
{
moveInput = Input.GetAxisRaw("Horizontal");
if (Input.GetButtonDown("Jump"))
{
jumpBufferCounter = jumpBufferTime;
}
else
{
jumpBufferCounter -= Time.deltaTime;
}
// Handle character flipping
if (moveInput > 0 && !isFacingRight)
{
Flip();
}
else if (moveInput < 0 && isFacingRight)
{
Flip();
}
// Update Animator parameters
anim.SetFloat("Speed", Mathf.Abs(rb.velocity.x));
anim.SetBool("isGrounded", isGrounded);
}
private void FixedUpdate()
{
isGrounded = Physics2D.OverlapCircle(groundCheck.position, groundCheckRadius, groundLayer);
if (isGrounded)
{
coyoteCounter = coyoteTime;
}
else
{
coyoteCounter -= Time.fixedDeltaTime;
}
// Jump logic with buffer and coyote time
if (jumpBufferCounter > 0f && coyoteCounter > 0f)
{
Jump();
jumpBufferCounter = 0f;
coyoteCounter = 0f;
}
// Movement
float targetSpeed = moveInput * moveSpeed;
float speedDifference = targetSpeed - rb.velocity.x;
float accelRate = (Mathf.Abs(targetSpeed) > 0.01f) ? acceleration : deceleration;
float movement = Mathf.Pow(Mathf.Abs(speedDifference) * accelRate, 0.9f) * Mathf.Sign(speedDifference);
rb.AddForce(movement * Vector2.right);
// Limit the maximum speed
if (Mathf.Abs(rb.velocity.x) > moveSpeed)
{
rb.velocity = new Vector2(Mathf.Sign(rb.velocity.x) * moveSpeed, rb.velocity.y);
}
}
private void Jump()
{
rb.velocity = new Vector2(rb.velocity.x, jumpForce);
anim.SetTrigger("Jump");
}
private void Flip()
{
isFacingRight = !isFacingRight;
Vector3 scaler = transform.localScale;
scaler.x *= -1;
transform.localScale = scaler;
}
private void OnDrawGizmosSelected()
{
if (groundCheck != null)
{
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(groundCheck.position, groundCheckRadius);
}
}
}
Additional features:
Animator component to control animations.Speed and isGrounded parameters to switch between animations.SetTrigger("Jump") to initiate the jump animation.Ensure your Animator Controller is set up with the necessary parameters and transitions:
PlayerAnimator in the Animator window.Idle, Run, and Jump animation states.Speed (float) and isGrounded (bool) as parameters in the Animator.Idle to Run when Speed > 0.1.Run to Idle when Speed < 0.1.Jump when Jump trigger is set.Jump back to Idle or Run based on isGrounded.Refactor the code to enhance readability and maintainability:
// Optimized PlayerMovement.cs
using UnityEngine;
[RequireComponent(typeof(Rigidbody2D), typeof(Animator))]
public class PlayerMovement : MonoBehaviour
{
[Header("Movement Settings")]
[SerializeField] private float moveSpeed = 5f;
[SerializeField] private float acceleration = 10f;
[SerializeField] private float deceleration = 10f;
[SerializeField] private float jumpForce = 12f;
[Header("Ground Detection Settings")]
[SerializeField] private Transform groundCheck;
[SerializeField] private float groundCheckRadius = 0.2f;
[SerializeField] private LayerMask groundLayer;
[Header("Jump Settings")]
[SerializeField] private float jumpBufferTime = 0.2f;
[SerializeField] private float coyoteTime = 0.2f;
private Rigidbody2D rb;
private Animator anim;
private bool isGrounded;
private float moveInput;
private float jumpBufferCounter;
private float coyoteCounter;
private bool isFacingRight = true;
private void Awake()
{
rb = GetComponent<Rigidbody2D>();
anim = GetComponent<Animator>();
}
private void Update()
{
HandleInput();
UpdateAnimations();
}
private void FixedUpdate()
{
CheckGroundStatus();
HandleMovement();
HandleJump();
}
private void HandleInput()
{
moveInput = Input.GetAxisRaw("Horizontal");
if (Input.GetButtonDown("Jump"))
{
jumpBufferCounter = jumpBufferTime;
}
else
{
jumpBufferCounter -= Time.deltaTime;
}
HandleCharacterFlip();
}
private void HandleCharacterFlip()
{
if (moveInput > 0 && !isFacingRight)
{
Flip();
}
else if (moveInput < 0 && isFacingRight)
{
Flip();
}
}
private void CheckGroundStatus()
{
isGrounded = Physics2D.OverlapCircle(groundCheck.position, groundCheckRadius, groundLayer);
if (isGrounded)
{
coyoteCounter = coyoteTime;
}
else
{
coyoteCounter -= Time.fixedDeltaTime;
}
}
private void HandleMovement()
{
float targetSpeed = moveInput * moveSpeed;
float speedDifference = targetSpeed - rb.velocity.x;
float accelRate = (Mathf.Abs(targetSpeed) > 0.01f) ? acceleration : deceleration;
float movement = Mathf.Pow(Mathf.Abs(speedDifference) * accelRate, 0.9f) * Mathf.Sign(speedDifference);
rb.AddForce(movement * Vector2.right);
// Clamp the velocity
rb.velocity = new Vector2(Mathf.Clamp(rb.velocity.x, -moveSpeed, moveSpeed), rb.velocity.y);
}
private void HandleJump()
{
if (jumpBufferCounter > 0f && coyoteCounter > 0f)
{
Jump();
jumpBufferCounter = 0f;
coyoteCounter = 0f;
}
}
private void Jump()
{
rb.velocity = new Vector2(rb.velocity.x, jumpForce);
anim.SetTrigger("Jump");
}
private void Flip()
{
isFacingRight = !isFacingRight;
Vector3 scaler = transform.localScale;
scaler.x *= -1;
transform.localScale = scaler;
}
private void UpdateAnimations()
{
anim.SetFloat("Speed", Mathf.Abs(rb.velocity.x));
anim.SetBool("isGrounded", isGrounded);
}
private void OnDrawGizmosSelected()
{
if (groundCheck != null)
{
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(groundCheck.position, groundCheckRadius);
}
}
}
Improvements made:
Rigidbody2D and Animator are present.Introduce a dash ability to add depth to player movement:
// PlayerMovement with Dashing
using UnityEngine;
[RequireComponent(typeof(Rigidbody2D), typeof(Animator))]
public class PlayerMovement : MonoBehaviour
{
[Header("Movement Settings")]
[SerializeField] private float moveSpeed = 5f;
[SerializeField] private float acceleration = 10f;
[SerializeField] private float deceleration = 10f;
[SerializeField] private float jumpForce = 12f;
[Header("Ground Detection Settings")]
[SerializeField] private Transform groundCheck;
[SerializeField] private float groundCheckRadius = 0.2f;
[SerializeField] private LayerMask groundLayer;
[Header("Jump Settings")]
[SerializeField] private float jumpBufferTime = 0.2f;
[SerializeField] private float coyoteTime = 0.2f;
[Header("Dash Settings")]
[SerializeField] private float dashSpeed = 20f;
[SerializeField] private float dashDuration = 0.2f;
[SerializeField] private float dashCooldown = 1f;
private Rigidbody2D rb;
private Animator anim;
private bool isGrounded;
private float moveInput;
private float jumpBufferCounter;
private float coyoteCounter;
private bool isFacingRight = true;
private bool isDashing = false;
private float dashTimer;
private float dashCooldownTimer;
private void Awake()
{
rb = GetComponent<Rigidbody2D>();
anim = GetComponent<Animator>();
}
private void Update()
{
HandleInput();
UpdateAnimations();
if (isDashing)
{
return;
}
if (dashCooldownTimer > 0)
{
dashCooldownTimer -= Time.deltaTime;
}
}
private void FixedUpdate()
{
if (isDashing)
{
rb.velocity = new Vector2(transform.localScale.x * dashSpeed, 0);
dashTimer -= Time.fixedDeltaTime;
if (dashTimer <= 0)
{
isDashing = false;
}
return;
}
CheckGroundStatus();
HandleMovement();
HandleJump();
}
private void HandleInput()
{
moveInput = Input.GetAxisRaw("Horizontal");
if (Input.GetButtonDown("Jump"))
{
jumpBufferCounter = jumpBufferTime;
}
else
{
jumpBufferCounter -= Time.deltaTime;
}
if (Input.GetKeyDown(KeyCode.LeftShift) && dashCooldownTimer <= 0f)
{
Dash();
}
HandleCharacterFlip();
}
private void HandleCharacterFlip()
{
if (moveInput > 0 && !isFacingRight)
{
Flip();
}
else if (moveInput < 0 && isFacingRight)
{
Flip();
}
}
private void CheckGroundStatus()
{
isGrounded = Physics2D.OverlapCircle(groundCheck.position, groundCheckRadius, groundLayer);
if (isGrounded)
{
coyoteCounter = coyoteTime;
}
else
{
coyoteCounter -= Time.fixedDeltaTime;
}
}
private void HandleMovement()
{
float targetSpeed = moveInput * moveSpeed;
float speedDifference = targetSpeed - rb.velocity.x;
float accelRate = (Mathf.Abs(targetSpeed) > 0.01f) ? acceleration : deceleration;
float movement = Mathf.Pow(Mathf.Abs(speedDifference) * accelRate, 0.9f) * Mathf.Sign(speedDifference);
rb.AddForce(movement * Vector2.right);
// Clamp the velocity
rb.velocity = new Vector2(Mathf.Clamp(rb.velocity.x, -moveSpeed, moveSpeed), rb.velocity.y);
}
private void HandleJump()
{
if (jumpBufferCounter > 0f && coyoteCounter > 0f)
{
Jump();
jumpBufferCounter = 0f;
coyoteCounter = 0f;
}
}
private void Jump()
{
rb.velocity = new Vector2(rb.velocity.x, jumpForce);
anim.SetTrigger("Jump");
}
private void Dash()
{
isDashing = true;
dashTimer = dashDuration;
dashCooldownTimer = dashCooldown;
anim.SetTrigger("Dash");
}
private void Flip()
{
isFacingRight = !isFacingRight;
Vector3 scaler = transform.localScale;
scaler.x *= -1;
transform.localScale = scaler;
}
private void UpdateAnimations()
{
anim.SetFloat("Speed", Mathf.Abs(rb.velocity.x));
anim.SetBool("isGrounded", isGrounded);
anim.SetBool("isDashing", isDashing);
}
private void OnDrawGizmosSelected()
{
if (groundCheck != null)
{
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(groundCheck.position, groundCheckRadius);
}
}
}
New additions:
Ensure your scene has ground objects for the player to interact with:
Ground.BoxCollider2D to enable collision.Ground layer to this GameObject.Configure the player's movement parameters in the Inspector:
Move Speed to 5.Jump Force to 12.Ground Check Radius to 0.2.Run the game and test the player controls:
If the player isn't behaving as expected, check the following:
GroundCheck is correctly positioned at the player's feet.Ground layer.moveSpeed, jumpForce, and groundCheckRadius.Add the ability for the player to perform a second jump while airborne:
// PlayerMovement with Double Jump
using UnityEngine;
[RequireComponent(typeof(Rigidbody2D), typeof(Animator))]
public class PlayerMovement : MonoBehaviour
{
[Header("Movement Settings")]
[SerializeField] private float moveSpeed = 5f;
[SerializeField] private float acceleration = 10f;
[SerializeField] private float deceleration = 10f;
[SerializeField] private float jumpForce = 12f;
[SerializeField] private int maxJumpCount = 2;
[Header("Ground Detection Settings")]
[SerializeField] private Transform groundCheck;
[SerializeField] private float groundCheckRadius = 0.2f;
[SerializeField] private LayerMask groundLayer;
[Header("Jump Settings")]
[SerializeField] private float jumpBufferTime = 0.2f;
[SerializeField] private float coyoteTime = 0.2f;
[Header("Dash Settings")]
[SerializeField] private float dashSpeed = 20f;
[SerializeField] private float dashDuration = 0.2f;
[SerializeField] private float dashCooldown = 1f;
private Rigidbody2D rb;
private Animator anim;
private bool isGrounded;
private float moveInput;
private float jumpBufferCounter;
private float coyoteCounter;
private bool isFacingRight = true;
private bool isDashing = false;
private float dashTimer;
private float dashCooldownTimer;
private int jumpCount;
private void Awake()
{
rb = GetComponent<Rigidbody2D>();
anim = GetComponent<Animator>();
}
private void Update()
{
HandleInput();
UpdateAnimations();
if (isDashing)
{
return;
}
if (dashCooldownTimer > 0)
{
dashCooldownTimer -= Time.deltaTime;
}
}
private void FixedUpdate()
{
if (isDashing)
{
rb.velocity = new Vector2(transform.localScale.x * dashSpeed, 0);
dashTimer -= Time.fixedDeltaTime;
if (dashTimer <= 0)
{
isDashing = false;
}
return;
}
CheckGroundStatus();
HandleMovement();
HandleJump();
}
private void HandleInput()
{
moveInput = Input.GetAxisRaw("Horizontal");
if (Input.GetButtonDown("Jump"))
{
jumpBufferCounter = jumpBufferTime;
}
else
{
jumpBufferCounter -= Time.deltaTime;
}
if (Input.GetKeyDown(KeyCode.LeftShift) && dashCooldownTimer <= 0f)
{
Dash();
}
HandleCharacterFlip();
}
private void HandleCharacterFlip()
{
if (moveInput > 0 && !isFacingRight)
{
Flip();
}
else if (moveInput < 0 && isFacingRight)
{
Flip();
}
}
private void CheckGroundStatus()
{
isGrounded = Physics2D.OverlapCircle(groundCheck.position, groundCheckRadius, groundLayer);
if (isGrounded)
{
coyoteCounter = coyoteTime;
jumpCount = 0;
}
else
{
coyoteCounter -= Time.fixedDeltaTime;
}
}
private void HandleMovement()
{
float targetSpeed = moveInput * moveSpeed;
float speedDifference = targetSpeed - rb.velocity.x;
float accelRate = (Mathf.Abs(targetSpeed) > 0.01f) ? acceleration : deceleration;
float movement = Mathf.Pow(Mathf.Abs(speedDifference) * accelRate, 0.9f) * Mathf.Sign(speedDifference);
rb.AddForce(movement * Vector2.right);
// Clamp the velocity
rb.velocity = new Vector2(Mathf.Clamp(rb.velocity.x, -moveSpeed, moveSpeed), rb.velocity.y);
}
private void HandleJump()
{
if (jumpBufferCounter > 0f && (coyoteCounter > 0f || jumpCount < maxJumpCount))
{
Jump();
jumpBufferCounter = 0f;
if (!isGrounded)
{
jumpCount++;
}
}
}
private void Jump()
{
rb.velocity = new Vector2(rb.velocity.x, jumpForce);
anim.SetTrigger("Jump");
}
private void Dash()
{
isDashing = true;
dashTimer = dashDuration;
dashCooldownTimer = dashCooldown;
anim.SetTrigger("Dash");
}
private void Flip()
{
isFacingRight = !isFacingRight;
Vector3 scaler = transform.localScale;
scaler.x *= -1;
transform.localScale = scaler;
}
private void UpdateAnimations()
{
anim.SetFloat("Speed", Mathf.Abs(rb.velocity.x));
anim.SetBool("isGrounded", isGrounded);
anim.SetBool("isDashing", isDashing);
}
private void OnDrawGizmosSelected()
{
if (groundCheck != null)
{
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(groundCheck.position, groundCheckRadius);
}
}
}
Enhancements include:
Allow the player to control jump height based on how long the jump button is held:
// PlayerMovement with Variable Jump Heights
using UnityEngine;
[RequireComponent(typeof(Rigidbody2D), typeof(Animator))]
public class PlayerMovement : MonoBehaviour
{
[Header("Movement Settings")]
[SerializeField] private float moveSpeed = 5f;
[SerializeField] private float acceleration = 10f;
[SerializeField] private float deceleration = 10f;
[SerializeField] private float jumpForce = 12f;
[SerializeField] private float lowJumpMultiplier = 2f;
[SerializeField] private float fallMultiplier = 2.5f;
[Header("Ground Detection Settings")]
[SerializeField] private Transform groundCheck;
[SerializeField] private float groundCheckRadius = 0.2f;
[SerializeField] private LayerMask groundLayer;
[Header("Jump Settings")]
[SerializeField] private float jumpBufferTime = 0.2f;
[SerializeField] private float coyoteTime = 0.2f;
[Header("Dash Settings")]
[SerializeField] private float dashSpeed = 20f;
[SerializeField] private float dashDuration = 0.2f;
[SerializeField] private float dashCooldown = 1f;
private Rigidbody2D rb;
private Animator anim;
private bool isGrounded;
private float moveInput;
private float jumpBufferCounter;
private float coyoteCounter;
private bool isFacingRight = true;
private bool isDashing = false;
private float dashTimer;
private float dashCooldownTimer;
private int jumpCount;
[SerializeField] private int maxJumpCount = 2;
private void Awake()
{
rb = GetComponent<Rigidbody2D>();
anim = GetComponent<Animator>();
}
private void Update()
{
HandleInput();
UpdateAnimations();
if (isDashing)
{
return;
}
if (dashCooldownTimer > 0)
{
dashCooldownTimer -= Time.deltaTime;
}
HandleVariableJump();
}
private void FixedUpdate()
{
if (isDashing)
{
rb.velocity = new Vector2(transform.localScale.x * dashSpeed, 0);
dashTimer -= Time.fixedDeltaTime;
if (dashTimer <= 0)
{
isDashing = false;
}
return;
}
CheckGroundStatus();
HandleMovement();
HandleJump();
}
private void HandleInput()
{
moveInput = Input.GetAxisRaw("Horizontal");
if (Input.GetButtonDown("Jump"))
{
jumpBufferCounter = jumpBufferTime;
}
else
{
jumpBufferCounter -= Time.deltaTime;
}
if (Input.GetKeyDown(KeyCode.LeftShift) && dashCooldownTimer <= 0f)
{
Dash();
}
HandleCharacterFlip();
}
private void HandleCharacterFlip()
{
if (moveInput > 0 && !isFacingRight)
{
Flip();
}
else if (moveInput < 0 && isFacingRight)
{
Flip();
}
}
private void CheckGroundStatus()
{
isGrounded = Physics2D.OverlapCircle(groundCheck.position, groundCheckRadius, groundLayer);
if (isGrounded)
{
coyoteCounter = coyoteTime;
jumpCount = 0;
}
else
{
coyoteCounter -= Time.fixedDeltaTime;
}
}
private void HandleMovement()
{
float targetSpeed = moveInput * moveSpeed;
float speedDifference = targetSpeed - rb.velocity.x;
float accelRate = (Mathf.Abs(targetSpeed) > 0.01f) ? acceleration : deceleration;
float movement = Mathf.Pow(Mathf.Abs(speedDifference) * accelRate, 0.9f) * Mathf.Sign(speedDifference);
rb.AddForce(movement * Vector2.right);
// Clamp the velocity
rb.velocity = new Vector2(Mathf.Clamp(rb.velocity.x, -moveSpeed, moveSpeed), rb.velocity.y);
}
private void HandleJump()
{
if (jumpBufferCounter > 0f && (coyoteCounter > 0f || jumpCount < maxJumpCount))
{
Jump();
jumpBufferCounter = 0f;
if (!isGrounded)
{
jumpCount++;
}
}
}
private void Jump()
{
rb.velocity = new Vector2(rb.velocity.x, jumpForce);
anim.SetTrigger("Jump");
}
private void Dash()
{
isDashing = true;
dashTimer = dashDuration;
dashCooldownTimer = dashCooldown;
anim.SetTrigger("Dash");
}
private void Flip()
{
isFacingRight = !isFacingRight;
Vector3 scaler = transform.localScale;
scaler.x *= -1;
transform.localScale = scaler;
}
private void UpdateAnimations()
{
anim.SetFloat("Speed", Mathf.Abs(rb.velocity.x));
anim.SetBool("isGrounded", isGrounded);
anim.SetBool("isDashing", isDashing);
}
private void HandleVariableJump()
{
if (rb.velocity.y > 0 && !Input.GetButton("Jump"))
{
rb.velocity += Vector2.up * Physics2D.gravity.y * (lowJumpMultiplier - 1) * Time.deltaTime;
}
else if (rb.velocity.y < 0)
{
rb.velocity += Vector2.up * Physics2D.gravity.y * (fallMultiplier - 1) * Time.deltaTime;
}
}
private void OnDrawGizmosSelected()
{
if (groundCheck != null)
{
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(groundCheck.position, groundCheckRadius);
}
}
}
Enhancements include:
A smooth camera that follows the player enhances the gaming experience:
// CameraFollow.cs
using UnityEngine;
public class CameraFollow : MonoBehaviour
{
[Header("Target Settings")]
public Transform target;
public float smoothSpeed = 0.125f;
public Vector3 offset;
private void LateUpdate()
{
if (target == null)
return;
Vector3 desiredPosition = target.position + offset;
Vector3 smoothedPosition = Vector3.Lerp(transform.position, desiredPosition, smoothSpeed);
transform.position = smoothedPosition;
}
}
Usage:
CameraFollow.Main Camera.Player as the Target.Offset and Smooth Speed in the Inspector for desired camera behavior.Ensure your movement script is optimized for better performance:
Design scripts to be modular for easier maintenance and scalability:
Thoroughly test all movement mechanics to ensure they work seamlessly:
Identify and fix any bugs or inconsistencies:
Finalize your game for deployment:
Creating a player movement script for a 2D side-scroller in Unity involves understanding physics interactions, handling user input, and implementing responsive controls. This comprehensive guide provided step-by-step instructions, from setting up the Unity project to writing and optimizing the movement script. By following these guidelines and continuously testing and refining your script, you can develop a fluid and engaging player movement system that enhances your game's overall experience. Remember to explore and integrate additional features like dashing, double jumping, and animations to further enrich your game's mechanics.