public static NewPlayerMovement Instance { get; private set; }
[SerializeField, Range(0f, 100f)]
float maxWalkSpeed = 10f, maxRunSpeed = 15f;
float walkOrRunSpeed;
[SerializeField, Range(0f, 100f)]
float maxAcceleration = 10f, maxAirAcceleration = 1f;
[SerializeField, Range(0f, 10f)]
float jumpHeight = 2f, dashForce = 5f;
[SerializeField, Range(0, 5)]
int maxAirJumps = 0, maxDashes = 1;
[SerializeField, Range(0f, 90f)]
float maxGroundAngle = 25f, maxStairsAngle = 50f;
[SerializeField, Range(0f, 100f)]
float maxSnapSpeed = 100f;
[SerializeField, Min(0f)]
float probeDistance = 1f;
[SerializeField]
LayerMask probeMask = -1, stairsMask = -1, waterBubble = -1, bounceMask;
//probeMask should be everything but IgnoreRaycast and Agent --"Agent" is Player-layer.
[SerializeField]
Transform playerInputSpace = default; //make player input relative to a gameobject, we want camera to be put in there
int jumpPhase, dashPhase;
bool desiredJump, desiredDash;
public bool DesiredJump { get { return desiredJump; } }
Vector2 playerInput;
Vector3 velocity, connectionVelocity;
Vector3 upAxis, rightAxis, forwardAxis;
Rigidbody body, connectedBody, previousConnectedBody;
Vector3 connectionWorldPosition, connectionLocalPosition;
float minGroundDotProduct, minStairsDotProduct; //min dot products (threshholds)
Vector3 contactNormal, steepNormal;
int groundContactCount, steepContactCount;
public bool OnGround => groundContactCount > 0; //shorthand for bool OnGround { get {return groundContactCount > 0;} } ... shorthand to define a single-statement readonly property.
bool OnSteep => steepContactCount > 0;
int stepsSinceLastGrounded, stepsSinceLastJump;
Vector3 gravity;
public Vector3 UpAxis { get { return upAxis; } }
Vector3 xAxis, zAxis;
void OnValidate()
{
//Computes the min dot product for different kinds of thresh holds
minGroundDotProduct = Mathf.Cos(maxGroundAngle * Mathf.Deg2Rad);
minStairsDotProduct = Mathf.Cos(maxStairsAngle * Mathf.Deg2Rad);
}
void Awake()
{
if (Instance == null)
{
Instance = this;
}
else
{
Destroy(gameObject);
}
body = GetComponent<Rigidbody>();
body.useGravity = false;
OnValidate();
}
private void Start()
{
MovementUpgrades.SetStats(maxAirJumps, maxDashes, jumpHeight, dashForce);
MovementUpgrades.OnUpgrade += _OnUpgrade;
}
//when this is called, update the stats
private void _OnUpgrade(object sender, EventArgs e)
{
jumpHeight = MovementUpgrades.jumpHeight;
dashForce = MovementUpgrades.dashForce;
maxAirJumps = MovementUpgrades.maxJumps;
maxDashes = MovementUpgrades.maxDashes;
}
public Vector2 GetSpeedAndInputs(out float speed, out float maxSpeed)
{
maxSpeed = walkOrRunSpeed;
speed = velocity.magnitude;
return playerInput;
}
public Vector3 GetForwardAxis()
{
// Debug.Log("forwardaxis: " + forwardAxis + " velocity direction???: " + velocity.normalized);
return forwardAxis;
}
public Vector3 GetRightAxis()
{
return xAxis;
}
void Update()
{
playerInput.x = Input.GetAxis("Horizontal");
playerInput.y = Input.GetAxis("Vertical");
playerInput = Vector2.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);
}
desiredJump |= Input.GetButtonDown("Jump");
desiredDash |= Input.GetKeyDown(KeyCode.F);
}
void FixedUpdate()
{
EvaluateGravitySource();
UpdateState();
AdjustVelocity();
if (desiredJump)
{
desiredJump = false;
Jump(gravity);
}
if (desiredDash)
{
desiredDash = false;
Dash();
}
if (OnGround && velocity.sqrMagnitude < 0.01f)
{
velocity += contactNormal * (Vector3.Dot(gravity, contactNormal) * Time.deltaTime);
}
else
{
velocity += gravity * Time.deltaTime;
}
body.velocity = velocity; //body is the rigidbody
transform.up = upAxis;
ClearState();
}
void AdjustVelocity() //changed
{
float acceleration;
//Vector3 xAxis, zAxis;
walkOrRunSpeed = (Input.GetKey(KeyCode.LeftShift)) ? maxRunSpeed : maxWalkSpeed;
acceleration = OnGround ? maxAcceleration : maxAirAcceleration;
xAxis = rightAxis;
zAxis = forwardAxis;
xAxis = ProjectDirectionOnPlane(xAxis, contactNormal);
zAxis = ProjectDirectionOnPlane(zAxis, contactNormal);
//Debug.Log("xAxis: " + xAxis + "zAxis: " + zAxis);
Vector3 relativeVelocity = velocity - connectionVelocity;
float currentX = Vector3.Dot(relativeVelocity, xAxis);
float currentZ = Vector3.Dot(relativeVelocity, zAxis);
//float acceleration = OnGround ? maxAcceleration : maxAirAcceleration;
float maxSpeedChange = acceleration * Time.deltaTime;
float newX = Mathf.MoveTowards(currentX, playerInput.x * walkOrRunSpeed, maxSpeedChange);
float newZ = Mathf.MoveTowards(currentZ, playerInput.y * walkOrRunSpeed, maxSpeedChange);
velocity += xAxis * (newX - currentX) + zAxis * (newZ - currentZ);
}
Vector3 ProjectDirectionOnPlane(Vector3 direction, Vector3 normal) //new, replaced the above function with this one. This method works with an arbitrary normal and also performs the normalization at the end
{
return (direction - normal * Vector3.Dot(direction, normal)).normalized;
}
private void EvaluateGravitySource()
{
gravity = CustomGravity.GetGravity(body.position, out upAxis);
//Debug.Log("Current gravity source: " + CustomGravity.GetGravitySource);
}
void UpdateState() //changed
{
//Now we also update stepsSinceLastGrounded to 0 before we do proceed to do more calculations
//If stepsSinceLastJump, a different float, is greater than 1, then we reset he jumpPhase
//stepsSinceLastGrounded += 1;
//stepsSinceLastJump += 1;
if (stepsSinceLastJump > 1)
{
jumpPhase = 0;
}
velocity = body.velocity;
if (OnGround || SnapToGround() || CheckSteepContacts()) //if we are grounded, snapped to ground, or on a steepContact then we reset steps and jumpPhase
{
stepsSinceLastGrounded = 0;
jumpPhase = 0;
dashPhase = 0;
if (groundContactCount > 1) //We also normalize contactNormal (essentially the jumping direction) if we are in a concave mesh - standing at a point where we are touching several points
{
contactNormal.Normalize();
}
}
else
{
contactNormal = upAxis; //Changed Vector.up with upAxis , so that we can implement custom gravity
}
if (connectedBody) //New, from moving platform
{
if (connectedBody.isKinematic || connectedBody.mass >= body.mass)
{
UpdateConnectionState();
}
}
}
void UpdateConnectionState()//New, from moving platform
{
if (connectedBody == previousConnectedBody)
{
Vector3 connectionMovement = connectedBody.transform.TransformPoint(connectionLocalPosition) - connectionWorldPosition;
connectionVelocity = connectionMovement / Time.deltaTime;
}
connectionWorldPosition = body.position;
connectionLocalPosition = connectedBody.transform.InverseTransformPoint(connectionWorldPosition);
}
void ClearState()
{
groundContactCount = steepContactCount = 0;
contactNormal = steepNormal = connectionVelocity = Vector3.zero;
previousConnectedBody = connectedBody;
connectedBody = null;
}
void Jump(Vector3 gravity, float overrideJumpSpeed = 1f) //changed, it now has a parameter
{
//The changes are that now we check whether PC is on ground, OnSteep or still airborn in which case the jumpDirection remains the contactNormal
Vector3 jumpDirection; //we create a jumpDirection vector, which changes based on what player stands on/or if in air
if (OnGround)
{
jumpDirection = contactNormal;
}
else if (OnSteep)
{
jumpDirection = steepNormal;
jumpPhase = 0;
}
else if (maxAirJumps > 0 && jumpPhase <= maxAirJumps)
{
if (jumpPhase == 0)
{
jumpPhase = 1;
}
jumpDirection = contactNormal;
}
else
{
return;
}
stepsSinceLastJump = 0;
jumpPhase += 1;
float jumpSpeed = Mathf.Sqrt(2f * gravity.magnitude * jumpHeight); //changed, instead of Physics.gravity.magnitude, we use our custom gravity which is passed in as a parameter
jumpDirection = (jumpDirection + upAxis).normalized; //Also changed jumpDirection + Vector3.up with jumpDirection + upAxis, as upAxis will now change, as in the direction off the gravity will change
float alignedSpeed = Vector3.Dot(velocity, jumpDirection);
if (alignedSpeed > 0f)
{
jumpSpeed = Mathf.Max(jumpSpeed - alignedSpeed, 0f);
}
velocity += jumpDirection * jumpSpeed * overrideJumpSpeed;
}
private void Dash()
{
Vector3 dashDirection;
if(dashPhase <= maxDashes && !OnGround)
{
dashDirection = playerInputSpace.forward;
dashDirection.Normalize();
}
else
{
return;
}
dashPhase += 1;
velocity += dashDirection * dashForce;
}
void OnCollisionEnter(Collision collision)
{
EvaluateCollision(collision);
}
void OnCollisionStay(Collision collision)
{
EvaluateCollision(collision);
}
void EvaluateCollision(Collision collision)
{
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); //new, checks what kind of contact we have
//Debug.Log("normal and updot: " + normal + "" + upDot + "upaxis: " + upAxis);
if (upDot >= minDot)
{
groundContactCount += 1;
contactNormal += normal;
connectedBody = collision.rigidbody;
//Debug.Log("When does this call?"); //gets called when standing on flat ground
if((bounceMask & (1 << layer)) != 0)
{
BouncePlatform bouncePlatform = collision.gameObject.GetComponent<BouncePlatform>();
Vector3 jumpDirection = contactNormal;
float jumpSpeed = Mathf.Sqrt(2f * gravity.magnitude * jumpHeight);
jumpDirection = (jumpDirection + upAxis).normalized;
body.AddForce(jumpDirection * jumpSpeed * bouncePlatform.GetBounceForce(), ForceMode.Impulse);
}
}
//else if (upDot > -0.01f) {
else
{
if (upDot > -0.01f)
{
steepContactCount += 1;
steepNormal += normal;
//Debug.Log("and When does this call?"); //gets called when not on flat ground, but when walls/slopes etc.
if (groundContactCount == 0)
{
connectedBody = collision.rigidbody;
}
}
}
}
}
public void ResetDashAndJump()
{
dashPhase = 0;
jumpPhase = 0;
}
bool SnapToGround()
{
//This returns true if we want to snap PC to the ground, if we are running quickly over a small stump we can prevent PC from flying, and just snap to ground if velocity is high enough
if (stepsSinceLastGrounded > 1 || stepsSinceLastJump <= 2)
{
return false;
}
float speed = velocity.magnitude; //we calculate the speed earlier then we used to, so we can return early if we haven't reached a desired speed
if (speed > maxSnapSpeed) //if we do not move fast enough, then snap
{
return false;
}
if (!Physics.Raycast(body.position, -upAxis, out RaycastHit hit, probeDistance, probeMask)) //We changed the second parameter (which was Vector3.down) to -upAxis, so down relative to where the player is
{
return false;
}
float upDot = Vector3.Dot(upAxis, hit.normal); //new -- we also removed hit.normal.y to upDot, as we no longer could use that
if (upDot < GetMinDot(hit.collider.gameObject.layer))
{
return false;
}
groundContactCount = 1;
contactNormal = hit.normal;
//float speed = velocity.magnitude;
float dot = Vector3.Dot(velocity, hit.normal);
if (dot > 0f)
{
velocity = (velocity - hit.normal * dot).normalized * speed; //the actual snapping code, it changes the velocity to align with the ground?? if I'm correct..
}
connectedBody = hit.rigidbody;
return true;
}
float GetMinDot(int layer)
{
return (stairsMask & (1 << layer)) == 0 ? minGroundDotProduct : minStairsDotProduct;
}
bool CheckSteepContacts()
{
if (steepContactCount > 1)
{
steepNormal.Normalize();
float upDot = Vector3.Dot(upAxis, steepNormal); //new, used in statement below
if (upDot >= minGroundDotProduct) //replaced steepNormal.y with upDot
{
groundContactCount = 1;
contactNormal = steepNormal;
return true;
}
}
return false;
}