CatlikeCoding-Unity/Assets/Scripts/MovingSphere.cs

212 lines
5.6 KiB
C#

using UnityEngine;
public class MovingSphere : MonoBehaviour
{
[SerializeField, Range(0f, 100f)]
private float maxSpeed = 10f;
[SerializeField, Range(0f, 100f)]
private float maxAcceleration = 10f, maxAirAcceleration = 1f;
[SerializeField, Range(0f, 10f)]
private float jumpHeight = 2f;
[SerializeField, Range(0, 5)]
private int maxAirJumps = 0;
[SerializeField, Range(0f, 90f)]
private float maxGroundAngle = 25f, maxStairsAngle = 50f;
[SerializeField, Range(0f, 100f)]
float maxSnapSpeed = 100f;
[SerializeField, Min(0f)]
float probeDistance = 1f;
[SerializeField]
LayerMask probeMask = -1, stairsMask = -1;
private Rigidbody body;
private Vector3 desiredVelocity = new Vector3(0f, 0f, 0f);
private bool desiredJump = false;
private int groundContactCount;
private bool onGround => groundContactCount > 0;
private int jumpPhase;
private Vector3 velocity;
private Vector3 contactNormal;
private float minGroundDotProduct, minStairsDotProduct;
private int stepsSinceLastGrounded, stepsSinceLastJump;
private float GetMinDot(int layer)
{
return (stairsMask & (1 << layer)) == 0 ?
minGroundDotProduct : minStairsDotProduct;
}
private void OnValidate()
{
minGroundDotProduct = Mathf.Cos(maxGroundAngle * Mathf.Deg2Rad);
minStairsDotProduct = Mathf.Cos(maxStairsAngle * Mathf.Deg2Rad);
}
private void Awake()
{
body = GetComponent<Rigidbody>();
OnValidate();
}
private 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");
GetComponent<MeshRenderer>().material.SetColor(
"_BaseColor", onGround ? Color.black : Color.white
);
}
private void FixedUpdate()
{
UpdateState();
AdjustVelocity();
if (desiredJump)
{
desiredJump = false;
Jump();
}
body.velocity = velocity;
ClearState();
}
private void UpdateState()
{
stepsSinceLastGrounded++;
stepsSinceLastJump++;
velocity = body.velocity;
if (onGround || SnapToGround())
{
jumpPhase = 0;
stepsSinceLastGrounded = 0;
if (groundContactCount > 1)
{
contactNormal.Normalize();
}
}
else
{
contactNormal = Vector3.up;
}
}
private void OnCollisionEnter(Collision collision)
{
EvaluateCollision(collision);
}
private void OnCollisionStay(Collision collision)
{
EvaluateCollision(collision);
}
private void EvaluateCollision(Collision collision)
{
float minDot = GetMinDot(collision.gameObject.layer);
for (int i = 0; i < collision.contactCount; i++)
{
var normal = collision.GetContact(i).normal;
if (normal.y >= minDot)
{
groundContactCount += 1;
contactNormal += normal;
}
}
}
private void Jump()
{
if (onGround || jumpPhase < maxAirJumps)
{
stepsSinceLastJump = 0;
jumpPhase++;
float jumpSpeed = Mathf.Sqrt(-2f * Physics.gravity.y * jumpHeight);
float alignedSpeed = Vector3.Dot(velocity, contactNormal);
if (alignedSpeed > 0f)
{
jumpSpeed = Mathf.Max(jumpSpeed - alignedSpeed, 0f);
}
velocity += contactNormal * jumpSpeed;
}
}
private 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);
}
private Vector3 ProjectOnContactPlane(Vector3 vector)
{
return vector - contactNormal * Vector3.Dot(vector, contactNormal);
}
private void ClearState()
{
groundContactCount = 0;
contactNormal = Vector3.zero;
}
private bool SnapToGround()
{
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;
}
}