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(); 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().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; } }