一、准备工作
1.创建项目
2.创建示例场景
a.创建3d ——Plane(地面)(0,0,0)和Sphere(游戏对象)(0,0.5,0),Sphere添加拖尾组件TrailRenderer
b.创建脚本 MovingSphere ——该脚本挂载到Sphere上并添加Rigidboy组件
二、需求如下
- 控制刚体球体的速度。
2.支持通过跳跃进行垂直移动。
3.检测地面及其角度。
4.沿斜坡移动。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MovingSphere : MonoBehaviour
{
// 最大速度,通过Inspector面板调整,范围0到100
[SerializeField, Range(0f, 100f), Tooltip("最大速度")]
private float maxSpeed = 10f;
// 最大加速度,通过Inspector面板调整,范围0到100
[SerializeField, Range(0f, 100f), Tooltip("最大加速度")]
private float maxAcceleration = 10f;
// 最大空中加速度,通过Inspector面板调整,范围0到100
[SerializeField, Range(0f, 100f), Tooltip("最大空气加速度")]
private float maxAirAcceleration = 1f;
// 跳跃高度,通过Inspector面板调整,范围0到10
[SerializeField, Range(0f, 10f), Tooltip("跳跃高度")]
private float jumpHeight = 2f;
// 最大空中跳跃次数,通过Inspector面板调整,范围0到5
[SerializeField, Range(0, 5), Tooltip("最大空中跳跃次数")]
private int maxAirJumps = 0;
// 最大地面角度,通过Inspector面板调整,范围0到90度
[SerializeField, Range(0, 90), Tooltip("最大地面角度")]
private float maxGroundAngle = 25;
// 当前速度
Vector3 velocity;
// 期望速度
Vector3 desiredVelocity;
// Rigidbody组件,用于物理模拟
Rigidbody body;
// 是否需要跳起
bool desiredJump;
// 地面接触点计数
int groundContactCount;
// 是否在地面上
bool OnGround
{
get { return groundContactCount > 0; }
}
// 跳跃阶段计数器
int jumpPhase;
// 地面法线与球体移动方向的最小点积,用于判断接触角度
float minGroundDotProduct;
// 接触点法线
Vector3 contactNormal;
// 当Inspector面板中的值改变时调用
private void OnValidate()
{
// 计算最大地面角度的余弦值
minGroundDotProduct = Mathf.Cos(maxGroundAngle * Mathf.Deg2Rad);
}
// Awake在对象实例化时调用一次
private void Awake()
{
// 获取Rigidbody组件
body = GetComponent<Rigidbody>();
// 调用OnValidate以确保minGroundDotProduct被正确初始化
OnValidate();
}
// 每帧调用
void Update()
{
// 获取玩家输入
Vector2 playerInput;
playerInput.x = Input.GetAxis("Horizontal");
playerInput.y = Input.GetAxis("Vertical");
// 限制输入向量的长度为1
playerInput = Vector2.ClampMagnitude(playerInput, 1f);
// 根据输入和最大速度计算期望速度
desiredVelocity = new Vector3(playerInput.x, 0f, playerInput.y) * maxSpeed;
// 如果按下跳跃键,则设置desiredJump为true
desiredJump |= Input.GetButtonDown("Jump");
// 根据地面接触点数调整球体颜色(仅作为示例)
GetComponent<Renderer>().material.SetColor("_Color", Color.white * (groundContactCount * 0.25f));
}
// FixedUpdate用于物理更新,每固定时间间隔调用一次
private void FixedUpdate()
{
// 更新状态信息
UpdateState();
// 根据当前状态调整速度
AdjustVelocity();
// 如果需要跳跃,则执行跳跃动作
if (desiredJump)
{
desiredJump = false;
Jump();
}
// 应用新速度
body.velocity = velocity;
// 清除状态信息,为下一帧准备
ClearState();
}
// 碰撞开始和持续时调用
private void OnCollisionEnter(Collision collision)
{
EvaluateCollision(collision);
}
private void OnCollisionStay(Collision collision)
{
EvaluateCollision(collision);
}
// 清除状态信息
void ClearState()
{
groundContactCount = 0;
contactNormal = Vector3.zero;
}
// 根据当前状态和期望状态调整速度
void AdjustVelocity()
{
// 计算接触平面的X轴和Z轴方向
Vector3 xAxis = ProjectOnContactPlane(Vector3.right).normalized;
Vector3 zAxis = ProjectOnContactPlane(Vector3.forward).normalized;
// 计算当前速度在X轴和Z轴上的分量
float currentX = Vector3.Dot(velocity, xAxis);
float currentZ = Vector3.Dot(velocity, zAxis);
// 根据是否在地面上选择加速度
float acceleration = OnGround ? maxAcceleration : maxAirAcceleration;
// 计算每帧最大速度变化量
float maxSpeedChange = acceleration * Time.deltaTime;
// 计算新的X轴和Z轴速度分量
float newX = Mathf.MoveTowards(currentX, desiredVelocity.x, maxSpeedChange);
float newZ = Mathf.MoveTowards(currentZ, desiredVelocity.z, maxSpeedChange);
// 更新速度
velocity += xAxis * (newX - currentX) + zAxis * (newZ - currentZ);
}
// 将向量投影到接触平面上
Vector3 ProjectOnContactPlane(Vector3 vector)
{
return vector - contactNormal * Vector3.Dot(vector, contactNormal);
}
// 更新状态信息,包括速度和接触信息
void UpdateState()
{
// 获取当前速度
velocity = body.velocity;
// 如果在地面上,重置跳跃阶段和更新接触法线
if (OnGround)
{
jumpPhase = 0;
if (groundContactCount > 0)
{
contactNormal.Normalize();
}
}
else
{
// 如果不在地面上,将接触法线设置为向上方向
contactNormal = Vector3.up;
}
}
// 根据碰撞信息更新地面接触点和法线
void EvaluateCollision(Collision collision)
{
for (int i = 0; i < collision.contactCount; i++)
{
Vector3 normal = collision.GetContact(i).normal;
// 如果法线与Y轴的夹角小于最大地面角度,则认为是地面接触
if (normal.y >= minGroundDotProduct)
{
groundContactCount += 1;
contactNormal += normal;
}
}
}
// 执行跳跃动作
void Jump()
{
// 如果在地面上或者跳跃次数未超过限制,则执行跳跃
if (OnGround || jumpPhase < maxAirJumps)
{
jumpPhase += 1;
// 计算跳跃速度
float jumpSpeed = Mathf.Sqrt(-2f * Physics.gravity.y * jumpHeight);
float alignedSpeed = Vector3.Dot(velocity, contactNormal);
// 如果球体已经向上移动,则减少跳跃速度
if (alignedSpeed > 0f)
{
jumpSpeed = Mathf.Max(jumpSpeed - velocity.y, 0f);
}
// 添加跳跃速度到当前速度上
velocity += contactNormal * jumpSpeed;
}
}
}