一、输入读取,脚本 PlayerInput.cs
MoveX 水平移动输入;JumpPressed 跳跃输入;JumpHeld 蓄力跳跃输入; 公开参数,私有修改。
public float MoveX { get; private set; }
public float JumpPressed { get; private set; }
public float JumpHeld { get; private set; }
void Update(){
MoveX = Input.GetAxisRaw("Horizontal");
JumpPressed = Input.GetButtonDonw("Jump");
JumpHeld = Input.GetButton("Jump");
}
二、玩家控制,脚本 PlayerController.cs
完成了基础的玩家控制:移动,转身,跳跃,土狼和预输入,下落。
1.玩家输入以及本体组件
先从输入脚本(PlayerInput.cs)中获取玩家的输入和其他必须的组件。
private PlayerInput input;
private Rigidbody2D rb; // 玩家物体刚体
private SpriteRenderer sr;// 玩家物体图片渲染
void Awake(){
input = GetComponent<PlayerInput>();
rb = GetComponent<Rigidbody2D>();
sr = GetComponent<SpriteRenderer>();
}
2.移动
[Header("Movement")]
[SerializeField]private float moveSpeed = 4f;
[SerializeField]private float jumpForce = 7.5f; // 这里顺便写上跳跃的速度
void HandleMove(){
rb.velocity = new Vector2(input.MoveX * moveSpeed, rb.velocity.y);
}
3.角色转身
这里单纯是角色的图片进行翻转,不包括任何子物体,后续可能修改。
void HandleFlip(){
if(input.MoveX != 0){
sr.flipX = input.MoveX < 0;
}
}
4.跳跃
跳跃必须检测是否在地面,不能无限跳。
[Header("Ground Check")]
[SerializeField]private Transform groundCheck; // 角色脚下检测地面的子物体中心
[SerializeField]private float groundCheckRadius = 0.2f; // 检测半径
[SerializeField]private LayerMask groundLayer; // 检测的目标图层
private bool isGrounded;
void CheckGround(){
if(Physics2D.OverlapCircle(groundCheck.positon, groundCheckradius, groundLayer)){
isGrounded = true;
}
else{
isGrounded = false;
}
}
void HandleJump(){
if(input.JumpPressed && isGrounded){
rb.velocity = new Vector2(rb.velocity.x, JumpForce);
}
}
5.跳跃的土狼与预输入
很明显上面实现的跳跃极其僵硬,玩家的输入只要稍快或稍慢都会使跳跃失败,出现按下跳跃键却没能成功跳跃的失灵感。 如果输入提前了,设置一定量的预输入,使得落地后自动跳跃。 如果输入延后了,设置一定量的延迟,使得在滞空后一小段时间也能跳跃。
[Header("Better Jump")]
[SerializeField]private float coyoteTime = 0.1f;
[SerializeField]private float jumpBufferTime = 0.1f;
private bool isGrounded;
private float coyoteTimer;
private float jumpBufferTimer;
void Update(){
if(input.JumpPressed){
jumpBufferTimer = jumpBufferTime; // 监听到跳跃输入,记录预输入
}
else if(jumpBufferTimer > 0){
jumpBufferTimer -= Time.deltaTime;
}
}
void CheckGround(){
if(Physics2D.OverlapCircle(groundCheck.positon, groundCheckradius, groundLayer)){
isGrounded = true;
coyoteTimer = coyoteTime; // 接触地面后允许跳跃
}
else{
isGrounded = false;
coyoteTimer -= Time.deltaTime; // 流逝该段时间,结束后不允许跳跃
}
}
void HandleJump(){
// 在 预输入时间内 和 允许跳跃时间内 触发跳跃
if(jumpBufferTimer > 0 && coyoteTimer > 0){
rb.velocity = new Vector2(rb.velocity.x, JumpForce);
jumpBufferTimer = 0;
coyoteTimer = 0;
}
}
6.竖直方向的物理优化
加快角色下落速度;增强轻跳的效果;限制下落的最大速度避免穿模。
[Header("Better Jump")]
[SerializeField]private float fallMultiplier = 2.5f;
[SerializeField]private float lowJumpMultiplier = 2f;
void BetterJump(){
if(rb.velocity.y < 0){
rb.velocity = rb.velocity.up * Physics2D.gravity.y * (fallMultiplier - 1) * Time.deltaTime;
}
else if(rb.velocity.y > 0 && !Input.JumpHeld){
rb.velocity = rb.velocity.up * Physics2D.gravity.y * (lowJumpMultiplier - 1) * Time.deltaTime;
}
if(rb.velocity.y < -20f){
rb.velocity = new Vector2(rb.velocity.x, -20f);
}
}
7.完整代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(Rigidbody2D))]
public class PlayerController : MonoBehaviour
{
[Header("Movement")]
[SerializeField] private float moveSpeed = 5f;
[SerializeField] private float jumpForce = 10f;
[Header("Ground Check")]
[SerializeField] private Transform groundCheck;
[SerializeField] private float groundCheckRadius = 0.2f;
[SerializeField] private LayerMask groundLayer;
[Header("Better Jump")]
[SerializeField] private float fallMultiplier = 2.5f;
[SerializeField] private float lowJumpMultiplier = 2f;
[SerializeField] private float coyoteTime = 0.1f;
[SerializeField] private float jumpBufferTime = 0.1f;
private Rigidbody2D rb;
private PlayerInput input;
private bool isGrounded;
private float coyoteTimer;
private float jumpBufferTimer;
private SpriteRenderer sr;
private void Awake()
{
rb = GetComponent<Rigidbody2D>();
input = GetComponent<PlayerInput>();
sr = GetComponent<SpriteRenderer>();
}
void Update()
{
CheckGround();
if (input.JumpPressed)
{
jumpBufferTimer = jumpBufferTime;
}
else if (jumpBufferTimer > 0)
{
jumpBufferTimer -= Time.deltaTime;
}
HandleJump();
HandleFlip();
}
private void FixedUpdate()
{
HandleMove();
BetterJump();
}
void HandleMove()
{
rb.velocity = new Vector2(input.MoveX * moveSpeed, rb.velocity.y);
}
void HandleJump()
{
if (jumpBufferTimer > 0 && coyoteTimer > 0)
{
rb.velocity = new Vector2(rb.velocity.x, jumpForce);
jumpBufferTimer = 0;
coyoteTimer = 0;
}
}
void BetterJump()
{
if (rb.velocity.y < 0)
{
rb.velocity += Vector2.up * Physics2D.gravity.y * (fallMultiplier - 1) * Time.deltaTime;
}
else if (rb.velocity.y > 0 && !input.JumpHeld)
{
rb.velocity += Vector2.up * Physics2D.gravity.y * (lowJumpMultiplier - 1) * Time.deltaTime;
}
if (rb.velocity.y < -20f)
{
rb.velocity = new Vector2(rb.velocity.x, -20f);
}
}
void CheckGround()
{
if (Physics2D.OverlapCircle(groundCheck.position, groundCheckRadius, groundLayer))
{
isGrounded = true;
coyoteTimer = coyoteTime;
}
else
{
isGrounded = false;
coyoteTimer -= Time.deltaTime;
}
}
void HandleFlip()
{
if (input.MoveX != 0)
{
//transform.localScale = new Vector3(Mathf.Sign(input.MoveX), 1, 1);
sr.flipX = input.MoveX < 0;
}
}
void OnDrawGizmos()
{
if (groundCheck != null)
{
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(groundCheck.position, groundCheckRadius);
}
}
}
Feature: Player Movement System
Design
- Input handling
- Movement logic
- Physics interaction
Implementation
Key scripts and logic explanation.
Challenges
-
Issue 1: Buffer time of jump
-
Issue 2: Coyote time when player is not grounded
Solutions
-
Issue 1: Define a float parameter: jumpBufferTime, set when jump button is pressed. And it decreases through minus Time.deltaTime. Change the logic of jump, from check the button down to if parameter jumpBufferTime is positive or not.
-
Issue 2: Same as jumpBufferTime. A coyoteTime allows player jump in a time area even if player is not grounded.
Future Improvements
Animation connection will be developed in day 2.