一、准备工作
1.创建项目
2.创建示例场景
a.创建3d ——Plane(地面)(0,0,0)和Sphere(游戏对象)(0,0.5,0),Sphere添加拖尾组件TrailRenderer
b.创建脚本 MovingSphere ——该脚本挂载到Sphere上并添加Rigidboy组件
c.Sphere的Layer==Agent;
二、需求如下
坚持着地,而不是从坡道上发射。
执行光线投射。
配置多个层及其交互。
导航楼梯。
利用陡峭的触点。
using UnityEngine;
public class MovingSphere : MonoBehaviour {
[SerializeField, Range(0f, 100f)]
float maxSpeed = 10f; // 最大移动速度
[SerializeField, Range(0f, 100f)]
float maxAcceleration = 10f, maxAirAcceleration = 1f; // 地面和空中的最大加速度
[SerializeField, Range(0f, 10f)]
float jumpHeight = 2f; // 跳跃高度
[SerializeField, Range(0, 5)]
int maxAirJumps = 0; // 最大空中跳跃次数
[SerializeField, Range(0, 90)]
float maxGroundAngle = 25f, maxStairsAngle = 50f; // 最大地面角度和楼梯角度
[SerializeField, Range(0f, 100f)]
float maxSnapSpeed = 100f; // 最大吸附速度
[SerializeField, Min(0f)]
float probeDistance = 1f; // 探测距离
[SerializeField]
LayerMask probeMask = -1, stairsMask = -1; // 探测掩码和楼梯掩码
Rigidbody body; // 刚体组件
Vector3 velocity, desiredVelocity; // 当前速度和目标速度
bool desiredJump; // 是否请求跳跃
Vector3 contactNormal, steepNormal; // 接触法线和陡坡法线
int groundContactCount, steepContactCount; // 地面接触次数和陡坡接触次数
bool OnGround => groundContactCount > 0; // 是否在地面上
bool OnSteep => steepContactCount > 0; // 是否在陡坡上
int jumpPhase; // 跳跃阶段
float minGroundDotProduct, minStairsDotProduct; // 最小地面点积和最小楼梯点积
int stepsSinceLastGrounded, stepsSinceLastJump; // 上次着地后的步数和上次跳跃后的步数
void OnValidate () {
// 计算最小地面点积和最小楼梯点积
minGroundDotProduct = Mathf.Cos(maxGroundAngle * Mathf.Deg2Rad);
minStairsDotProduct = Mathf.Cos(maxStairsAngle * Mathf.Deg2Rad);
}
void Awake () {
// 获取刚体组件并验证参数
body = GetComponent<Rigidbody>();
OnValidate();
}
void Update () {
// 获取玩家输入
Vector2 playerInput;
playerInput.x = Input.GetAxis("Horizontal");
playerInput.y = Input.GetAxis("Vertical");
playerInput = Vector2.ClampMagnitude(playerInput, 1f);
// 计算目标速度
desiredVelocity =
new Vector3(playerInput.x, 0f, playerInput.y) * maxSpeed;
// 检测跳跃输入
desiredJump |= Input.GetButtonDown("Jump");
}
void FixedUpdate () {
// 更新状态并调整速度
UpdateState();
AdjustVelocity();
// 处理跳跃
if (desiredJump) {
desiredJump = false;
Jump();
}
// 应用速度并清除状态
body.velocity = velocity;
ClearState();
}
void ClearState () {
// 清除接触状态
groundContactCount = steepContactCount = 0;
contactNormal = steepNormal = Vector3.zero;
}
void UpdateState () {
// 更新步数计数器
stepsSinceLastGrounded += 1;
stepsSinceLastJump += 1;
velocity = body.velocity;
// 如果在地面、吸附到地面或检测到陡坡接触,则重置步数计数器
if (OnGround || SnapToGround() || CheckSteepContacts()) {
stepsSinceLastGrounded = 0;
if (stepsSinceLastJump > 1) {
jumpPhase = 0;
}
if (groundContactCount > 1) {
contactNormal.Normalize();
}
}
else {
contactNormal = Vector3.up;
}
}
bool SnapToGround () {
// 如果不符合吸附条件,则返回false
if (stepsSinceLastGrounded > 1 || stepsSinceLastJump <= 2) {
return false;
}
float speed = velocity.magnitude;
if (speed > maxSnapSpeed) {
return false;
}
// 发射射线检测地面
if (!Physics.Raycast(
body.position, Vector3.down, out RaycastHit hit,
probeDistance, probeMask
)) {
return false;
}
if (hit.normal.y < GetMinDot(hit.collider.gameObject.layer)) {
return false;
}
// 更新接触状态
groundContactCount = 1;
contactNormal = hit.normal;
float dot = Vector3.Dot(velocity, hit.normal);
if (dot > 0f) {
velocity = (velocity - hit.normal * dot).normalized * speed;
}
return true;
}
bool CheckSteepContacts () {
// 检测陡坡接触
if (steepContactCount > 1) {
steepNormal.Normalize();
if (steepNormal.y >= minGroundDotProduct) {
steepContactCount = 0;
groundContactCount = 1;
contactNormal = steepNormal;
return true;
}
}
return false;
}
void AdjustVelocity () {
// 调整速度以适应接触平面
Vector3 xAxis = ProjectOnContactPlane(Vector3.right).normalized;
Vector3 zAxis = ProjectOnContactPlane(Vector3.forward).normalized;
float currentX = Vector3.Dot(velocity, xAxis);
float currentZ = Vector3.Dot(velocity, zAxis);
float acceleration = OnGround ? maxAcceleration : maxAirAcceleration;
float maxSpeedChange = acceleration * Time.deltaTime;
float newX =
Mathf.MoveTowards(currentX, desiredVelocity.x, maxSpeedChange);
float newZ =
Mathf.MoveTowards(currentZ, desiredVelocity.z, maxSpeedChange);
velocity += xAxis * (newX - currentX) + zAxis * (newZ - currentZ);
}
void Jump () {
// 处理跳跃逻辑
Vector3 jumpDirection;
if (OnGround) {
jumpDirection = contactNormal;
}
else if (OnSteep) {
jumpDirection = steepNormal;
jumpPhase = 0;
}
else if (maxAirJumps > 0 && jumpPhase <= maxAirJumps) {
if (jumpPhase == 0) {
jumpPhase = 1;
}
jumpDirection = contactNormal;
}
else {
return;
}
stepsSinceLastJump = 0;
jumpPhase += 1;
float jumpSpeed = Mathf.Sqrt(-2f * Physics.gravity.y * jumpHeight);
jumpDirection = (jumpDirection + Vector3.up).normalized;
float alignedSpeed = Vector3.Dot(velocity, jumpDirection);
if (alignedSpeed > 0f) {
jumpSpeed = Mathf.Max(jumpSpeed - alignedSpeed, 0f);
}
velocity += jumpDirection * jumpSpeed;
}
void OnCollisionEnter (Collision collision) {
// 碰撞进入时评估碰撞
EvaluateCollision(collision);
}
void OnCollisionStay (Collision collision) {
// 碰撞持续时评估碰撞
EvaluateCollision(collision);
}
void EvaluateCollision (Collision collision) {
// 评估碰撞并更新接触状态
float minDot = GetMinDot(collision.gameObject.layer);
for (int i = 0; i < collision.contactCount; i++) {
Vector3 normal = collision.GetContact(i).normal;
if (normal.y >= minDot) {
groundContactCount += 1;
contactNormal += normal;
}
else if (normal.y > -0.01f) {
steepContactCount += 1;
steepNormal += normal;
}
}
}
Vector3 ProjectOnContactPlane (Vector3 vector) {
// 将向量投影到接触平面上
return vector - contactNormal * Vector3.Dot(vector, contactNormal);
}
float GetMinDot (int layer) {
// 根据层级获取最小点积
return (stairsMask & (1 << layer)) == 0 ?
minGroundDotProduct : minStairsDotProduct;
}
}