212 lines
5.6 KiB
C#
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;
|
|
}
|
|
}
|