Hey everyone. This is my first post here. If there is a more properly location to be posting questions and advice let me know.
Basically I’ve been following a tutorial for a while now and while I’ve learned a lot, something about the code isn’t compatible with the addition of a jump buffer and coyote timer. I’ve tried a few different methods and even references to codes that seem similar in implementation are working. I’m not sure if it has something to do with ground snapping or how I have jump phases defined but I’ve been stumped for a few days now. Anything to help steer me in the right direction would be greatly appreciated. The person who made the tutorial has some life stuff going on and hasn’t gotten back to me, so I’m posting here.
The code is below. I’ve attempted to purge some lines of code that I don’t think would pertain to the issue. Cheers.
using UnityEngine;
public class SphereGame : MonoBehaviour {
[SerializeField]
Transform playerInputSpace = default, ball = default;
[SerializeField, Range(0f, 100f)]
float maxSpeed = 15f, maxClimbSpeed = 4f, maxSwimSpeed = 5f;
[SerializeField, Range(0f, 100f)]
float
maxAcceleration = 67.5f,
maxAirAcceleration = 20f,
maxClimbAcceleration = 40f,
maxSwimAcceleration = 5f;
[SerializeField, Range(0f, 10f)]
float jumpHeight = 2.35f;
[SerializeField, Range(0, 5)]
int maxAirJumps = 1;
[SerializeField, Range(0, 90)]
float maxGroundAngle = 15f, maxStairsAngle = 50f;
[SerializeField, Range(90, 170)]
float maxClimbAngle = 140f;
[SerializeField, Range(0f, 100f)]
float maxSnapSpeed = 100f;
[SerializeField, Min(0f)]
float probeDistance = 1f;
[SerializeField]
float submergenceOffset = 0.5f;
[SerializeField, Min(0.1f)]
float submergenceRange = 1f;
[SerializeField, Min(0f)]
float buoyancy = 1f;
[SerializeField, Range(0f, 10f)]
float waterDrag = 1f;
[SerializeField, Range(0.01f, 1f)]
float swimThreshold = 0.5f;
[SerializeField]
LayerMask
probeMask = -1,
stairsMask = -1,
climbMask = -1,
waterMask = 0;
[SerializeField]
Material
normalMaterial = default,
climbingMaterial = default,
swimmingMaterial = default;
[SerializeField, Min(0.1f)]
float ballRadius = 0.5f;
[SerializeField, Min(0f)]
float ballAlignSpeed = 180f;
[SerializeField, Min(0f)]
float ballAirRotation = 0.5f, ballSwimRotation = 2f;
//[SerializeField, Range(0f, 0.3f)] float jumpBuffer = 0.15f;
Rigidbody body, connectedBody, previousConnectedBody;
Vector3 playerInput;
Vector3 velocity, connectionVelocity;
Vector3 connectionWorldPosition, connectionLocalPosition;
Vector3 upAxis, rightAxis, forwardAxis;
bool desiredJump, desiresClimbing;
Vector3 contactNormal, steepNormal, climbNormal, lastClimbNormal;
Vector3 lastContactNormal, lastSteepNormal, lastConnectionVelocity;
int groundContactCount, steepContactCount, climbContactCount;
bool OnGround => groundContactCount > 0;
bool OnSteep => steepContactCount > 0;
bool Climbing => climbContactCount > 0 && stepsSinceLastJump > 2;
bool InWater => submergence > 0f;
bool Swimming => submergence >= swimThreshold;
float submergence;
int jumpPhase;
float minGroundDotProduct, minStairsDotProduct, minClimbDotProduct;
int stepsSinceLastGrounded, stepsSinceLastJump;
MeshRenderer meshRenderer;
// This prevents the ground snapping from affecting your jump height.
public void PreventSnapToGround () {
stepsSinceLastJump = -1;
}
void OnValidate () {
minGroundDotProduct = Mathf.Cos(maxGroundAngle * Mathf.Deg2Rad);
minStairsDotProduct = Mathf.Cos(maxStairsAngle * Mathf.Deg2Rad);
minClimbDotProduct = Mathf.Cos(maxClimbAngle * Mathf.Deg2Rad);
}
void Awake () {
body = GetComponent<Rigidbody>();
body.useGravity = false;
meshRenderer = ball.GetComponent<MeshRenderer>();
OnValidate();
}
void Update () {
// This is for player controls and accounts for being in water or alterations to gravity as well!
playerInput.x = Input.GetAxis("Horizontal");
playerInput.z = Input.GetAxis("Vertical");
playerInput.y = Swimming ? Input.GetAxis("UpDown") : 0f;
playerInput = Vector3.ClampMagnitude(playerInput, 1f);
if (playerInputSpace) {
rightAxis = ProjectDirectionOnPlane(playerInputSpace.right, upAxis);
forwardAxis =
ProjectDirectionOnPlane(playerInputSpace.forward, upAxis);
}
else {
rightAxis = ProjectDirectionOnPlane(Vector3.right, upAxis);
forwardAxis = ProjectDirectionOnPlane(Vector3.forward, upAxis);
}
if (Swimming) {
desiresClimbing = false;
}
else {
desiredJump |= Input.GetButtonDown("Jump");
desiresClimbing = Input.GetButton("Climb");
}
UpdateBall();
}
void FixedUpdate () {
Vector3 gravity = CustomGravity.GetGravity(body.position, out upAxis);
UpdateState();
if (InWater) {
velocity *= 1f - waterDrag * submergence * Time.deltaTime;
}
AdjustVelocity();
if (desiredJump) {
desiredJump = false;
Jump(gravity);
}
if (Climbing) {
velocity -=
contactNormal * (maxClimbAcceleration * 0.9f * Time.deltaTime);
}
else if (InWater) {
velocity +=
gravity * ((1f - buoyancy * submergence) * Time.deltaTime);
}
else if (OnGround && velocity.sqrMagnitude < 0.01f) {
velocity +=
contactNormal *
(Vector3.Dot(gravity, contactNormal) * Time.deltaTime);
}
else if (desiresClimbing && OnGround) {
velocity +=
(gravity - contactNormal * (maxClimbAcceleration * 0.9f)) *
Time.deltaTime;
}
else {
velocity += gravity * Time.deltaTime;
}
body.velocity = velocity;
ClearState();
}
void ClearState () {
lastContactNormal = contactNormal;
lastSteepNormal = steepNormal;
lastConnectionVelocity = connectionVelocity;
groundContactCount = steepContactCount = climbContactCount = 0;
contactNormal = steepNormal = climbNormal = Vector3.zero;
connectionVelocity = Vector3.zero;
previousConnectedBody = connectedBody;
connectedBody = null;
submergence = 0f;
}
void UpdateState () {
stepsSinceLastGrounded += 1;
stepsSinceLastJump += 1;
velocity = body.velocity;
if (
CheckClimbing() || CheckSwimming() ||
OnGround || SnapToGround() || CheckSteepContacts()
) {
stepsSinceLastGrounded = 0;
if (stepsSinceLastJump > 1) {
jumpPhase = 0;
}
if (groundContactCount > 1) {
contactNormal.Normalize();
}
}
else {
contactNormal = upAxis;
}
if (connectedBody) {
if (connectedBody.isKinematic || connectedBody.mass >= body.mass) {
UpdateConnectionState();
}
}
}
void UpdateConnectionState () {
if (connectedBody == previousConnectedBody) {
Vector3 connectionMovement =
connectedBody.transform.TransformPoint(connectionLocalPosition) -
connectionWorldPosition;
connectionVelocity = connectionMovement / Time.deltaTime;
}
connectionWorldPosition = body.position;
connectionLocalPosition = connectedBody.transform.InverseTransformPoint(
connectionWorldPosition
);
}
bool SnapToGround () {
if (stepsSinceLastGrounded > 1 || stepsSinceLastJump <= 2 || InWater) {
return false;
}
float speed = velocity.magnitude;
if (speed > maxSnapSpeed) {
return false;
}
if (!Physics.Raycast(
body.position, -upAxis, out RaycastHit hit,
probeDistance, probeMask, QueryTriggerInteraction.Ignore
)) {
return false;
}
float upDot = Vector3.Dot(upAxis, hit.normal);
if (upDot < 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;
}
connectedBody = hit.rigidbody;
return true;
}
bool CheckSteepContacts () {
if (steepContactCount > 1) {
steepNormal.Normalize();
float upDot = Vector3.Dot(upAxis, steepNormal);
if (upDot >= minGroundDotProduct) {
steepContactCount = 0;
groundContactCount = 1;
contactNormal = steepNormal;
return true;
}
}
return false;
}
void AdjustVelocity () {
float acceleration, speed;
Vector3 xAxis, zAxis;
if (Climbing) {
acceleration = maxClimbAcceleration;
speed = maxClimbSpeed;
xAxis = Vector3.Cross(contactNormal, upAxis);
zAxis = upAxis;
}
else if (InWater) {
float swimFactor = Mathf.Min(1f, submergence / swimThreshold);
acceleration = Mathf.LerpUnclamped(
OnGround ? maxAcceleration : maxAirAcceleration,
maxSwimAcceleration, swimFactor
);
speed = Mathf.LerpUnclamped(maxSpeed, maxSwimSpeed, swimFactor);
xAxis = rightAxis;
zAxis = forwardAxis;
}
else {
acceleration = OnGround ? maxAcceleration : maxAirAcceleration;
speed = OnGround && desiresClimbing ? maxClimbSpeed : maxSpeed;
xAxis = rightAxis;
zAxis = forwardAxis;
}
xAxis = ProjectDirectionOnPlane(xAxis, contactNormal);
zAxis = ProjectDirectionOnPlane(zAxis, contactNormal);
Vector3 relativeVelocity = velocity - connectionVelocity;
Vector3 adjustment;
adjustment.x =
playerInput.x * speed - Vector3.Dot(relativeVelocity, xAxis);
adjustment.z =
playerInput.z * speed - Vector3.Dot(relativeVelocity, zAxis);
adjustment.y = Swimming ?
playerInput.y * speed - Vector3.Dot(relativeVelocity, upAxis) : 0f;
adjustment =
Vector3.ClampMagnitude(adjustment, acceleration * Time.deltaTime);
velocity += xAxis * adjustment.x + zAxis * adjustment.z;
if (Swimming) {
velocity += upAxis * adjustment.y;
}
}
void Jump (Vector3 gravity) {
Vector3 jumpDirection;
if (OnGround) {
jumpDirection = contactNormal;
}
else if (OnSteep) {
jumpDirection = steepNormal;
// This controls air jump recovery from wall jumps
jumpPhase = 0;
}
else if (maxAirJumps > 0 && jumpPhase <= maxAirJumps) {
// This controls how many jumps you get when leaving a surface without jumping.
if (jumpPhase == 0) {
jumpPhase = 1;
}
jumpDirection = contactNormal;
}
else {
return;
}
stepsSinceLastJump = 0;
jumpPhase += 1;
// This determines desired jump height while accounting for force needed to overcome gravity(??)
float jumpSpeed = Mathf.Sqrt(2f * gravity.magnitude * jumpHeight);
if (InWater) {
jumpSpeed *= Mathf.Max(0f, 1f - submergence / swimThreshold);
}
jumpDirection = (jumpDirection + upAxis).normalized;
float alignedSpeed = Vector3.Dot(velocity, jumpDirection);
// This prevents your jump from losing momentum if an upward force exceeds the jump force.
if (alignedSpeed > 0f) {
jumpSpeed = Mathf.Max(jumpSpeed - alignedSpeed, 0f);
}
// This prevents the double jump from not giving much height if you jump late into a fall.
else if (alignedSpeed < 0f) {
jumpSpeed -= alignedSpeed;
}
velocity += jumpDirection * jumpSpeed;
}
void OnCollisionEnter (Collision collision) {
EvaluateCollision(collision);
}
void OnCollisionStay (Collision collision) {
EvaluateCollision(collision);
}
void EvaluateCollision (Collision collision) {
if (Swimming) {
return;
}
int layer = collision.gameObject.layer;
float minDot = GetMinDot(layer);
for (int i = 0; i < collision.contactCount; i++) {
Vector3 normal = collision.GetContact(i).normal;
float upDot = Vector3.Dot(upAxis, normal);
if (upDot >= minDot) {
groundContactCount += 1;
contactNormal += normal;
connectedBody = collision.rigidbody;
}
else {
if (upDot > -0.01f) {
steepContactCount += 1;
steepNormal += normal;
if (groundContactCount == 0) {
connectedBody = collision.rigidbody;
}
}
if (
desiresClimbing && upDot >= minClimbDotProduct &&
(climbMask & (1 << layer)) != 0
) {
climbContactCount += 1;
climbNormal += normal;
lastClimbNormal = normal;
connectedBody = collision.rigidbody;
}
}
}
}
Vector3 ProjectDirectionOnPlane (Vector3 direction, Vector3 normal) {
return (direction - normal * Vector3.Dot(direction, normal)).normalized;
}
float GetMinDot (int layer) {
return (stairsMask & (1 << layer)) == 0 ?
minGroundDotProduct : minStairsDotProduct;
}
}