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; 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; private void OnValidate() { minGroundDotProduct = Mathf.Cos(maxGroundAngle * 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"); } private void FixedUpdate() { UpdateState(); AdjustVelocity(); if (desiredJump) { desiredJump = false; Jump(); } body.velocity = velocity; ClearState(); } private void UpdateState() { velocity = body.velocity; if (onGround) { jumpPhase = 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) { for (int i = 0; i < collision.contactCount; i++) { var normal = collision.GetContact(i).normal; if (normal.y >= minGroundDotProduct) { groundContactCount += 1; contactNormal += normal; } } } private void Jump() { if (onGround || jumpPhase < maxAirJumps) { 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); } void ClearState() { groundContactCount = 0; contactNormal = Vector3.zero; } }