using System.Collections; using UnityEngine; using UnityEngine.InputSystem; public class PlayerMovement : MonoBehaviour { //Scriptable object which holds all the player's movement parameters. If you don't want to use it //just paste in all the parameters, though you will need to manuly change all references in this script //HOW TO: to add the scriptable object, right-click in the project window -> create -> Player Data //Next, drag it into the slot in playerMovement on your player public PlayerData Data; #region Variables //Components public Rigidbody2D RB { get; private set; } //Variables control the various actions the player can perform at any time. //These are fields which can are public allowing for other sctipts to read them //but can only be privately written to. public bool IsFacingRight { get; private set; } public bool IsJumping { get; private set; } public bool IsWallJumping { get; private set; } public bool IsSliding { get; private set; } //Timers (also all fields, could be private and a method returning a bool could be used) public float LastOnGroundTime { get; private set; } public float LastOnWallTime { get; private set; } public float LastOnWallRightTime { get; private set; } public float LastOnWallLeftTime { get; private set; } public int trumpet = 0; public bool in_range = false; public GameObject enemy; //Jump private bool _isJumpCut; private bool _isJumpFalling; private bool isRegFalling; //Wall Jump private float _wallJumpStartTime; private int _lastWallJumpDir; private Vector2 _moveInput; public float LastPressedJumpTime { get; private set; } Tutorial_GrapplingRope grapplingRope; bool wasGrappling = false; bool unlockedTrumpet; //clarinet public bool isDashing = false; private float tempGrav; private float tempFall; private bool isFrozen = false; //Set all of these up in the inspector [Header("Checks")] //Size of groundCheck depends on the size of your character generally you want them slightly small than width (for ground) and height (for the wall check) [SerializeField] private Vector2 _groundCheckSize = new Vector2(0.49f, 0.03f); [SerializeField] private float _groundCheckOffset; [Space(5)] [SerializeField] private Vector2 _wallCheckSize = new Vector2(0.5f, 1f); [Header("Layers & Tags")] [SerializeField] private LayerMask _groundLayer; [HideInInspector] private PlayerBehavior playerBehavior; [HideInInspector] private StateController stateController; [HideInInspector] private AudioSource audioSource; [HideInInspector] private bool soundPlaying = false; [HideInInspector] private GameUIController gameUI; #endregion private void Awake() { RB = GetComponent(); playerBehavior = this.gameObject.GetComponent(); grapplingRope = playerBehavior.grapplingRope; stateController = GameObject.FindGameObjectWithTag("StateController").GetComponent(); audioSource = GetComponent(); gameUI = GameObject.FindGameObjectWithTag("GameUICanvas").GetComponent(); } private void Start() { SetGravityScale(Data.gravityScale); IsFacingRight = true; } void OnMove(InputValue value) { if (playerBehavior.playerIsAlive && !isDashing) { this._moveInput = value.Get(); } else { this._moveInput = Vector2.zero; } } void OnJump() { if (playerBehavior.playerIsAlive && !isDashing) { OnJumpInput(); } } private void Update() { isDashing = playerBehavior.isDash; if(isDashing) { FreezeControl(); } else{ if(isFrozen) { Data.gravityStrength = tempGrav; Data.fallGravityMult = tempFall; isFrozen = false; } unlockedTrumpet = stateController.unlockedTrumpet; #region TIMERS LastOnGroundTime -= Time.deltaTime; LastOnWallTime -= Time.deltaTime; LastOnWallRightTime -= Time.deltaTime; LastOnWallLeftTime -= Time.deltaTime; LastPressedJumpTime -= Time.deltaTime; #endregion #region INPUT HANDLER if (_moveInput.x != 0) CheckDirectionToFace(_moveInput.x > 0); #endregion // if (!IsJumping) // { // print("not jumping"); // } // else // { // print("jumping, " + RB.velocity.y); // } #region COLLISION CHECKS if (!IsJumping) { //Ground Check if (IsGrounded()) //checks if set box overlaps with ground { LastOnGroundTime = Data.coyoteTime; //if so sets the lastGrounded to coyoteTime if (unlockedTrumpet) { trumpet = 2; gameUI.ToggleTrumpet(true); } else { trumpet = -1; } wasGrappling = false; isRegFalling = false; } else { // print("not jumping"); if(!_isJumpFalling && !isRegFalling) { if(unlockedTrumpet) { trumpet = 1; gameUI.ToggleTrumpet(true); } else { trumpet = -1; } isRegFalling = true; } } //Right Wall Check if (((Physics2D.OverlapBox(this.transform.position, _wallCheckSize, 0, _groundLayer) && IsFacingRight) || (Physics2D.OverlapBox(this.transform.position, _wallCheckSize, 0, _groundLayer) && !IsFacingRight)) && !IsWallJumping) LastOnWallRightTime = Data.coyoteTime; //Right Wall Check if (((Physics2D.OverlapBox(this.transform.position, _wallCheckSize, 0, _groundLayer) && !IsFacingRight) || (Physics2D.OverlapBox(this.transform.position, _wallCheckSize, 0, _groundLayer) && IsFacingRight)) && !IsWallJumping) LastOnWallLeftTime = Data.coyoteTime; //Two checks needed for both left and right walls since whenever the play turns the wall checkPoints swap sides LastOnWallTime = Mathf.Max(LastOnWallLeftTime, LastOnWallRightTime); } #endregion #region JUMP CHECKS if (IsJumping && RB.velocity.y <= 0) { IsJumping = false; // print("isJumping " + IsJumping); if (!IsWallJumping) _isJumpFalling = true; } if (IsWallJumping && Time.time - _wallJumpStartTime > Data.wallJumpTime) { IsWallJumping = false; } if (LastOnGroundTime > 0 && !IsJumping && !IsWallJumping) { _isJumpCut = false; if (!IsJumping) _isJumpFalling = false; } //Jump if (CanJump() && LastPressedJumpTime > 0) { IsJumping = true; IsWallJumping = false; _isJumpCut = false; _isJumpFalling = false; Jump(); if (!IsGrounded() && in_range && trumpet > 0) { gameObject.transform.Find("Trumpet").GetComponent().Play(); enemy.GetComponent().DefeatEnemy(); enemy = null; in_range = false; } else if (!IsGrounded() && !in_range && trumpet > 0) { trumpet -= 1; } // check if double jump, play sound if (trumpet == 0) { gameObject.transform.Find("Trumpet").GetComponent().Play(); } } // stop sound if needed if (soundPlaying && (isRegFalling || IsJumping || _isJumpFalling)) { print("footsteps stop"); audioSource.Stop(); soundPlaying = false; } //WALL JUMP // else if (CanWallJump() && LastPressedJumpTime > 0) // { // IsWallJumping = true; // IsJumping = false; // _isJumpCut = false; // _isJumpFalling = false; // _wallJumpStartTime = Time.time; // _lastWallJumpDir = (LastOnWallRightTime > 0) ? -1 : 1; // // WallJump(_lastWallJumpDir); // } #endregion #region GRAPPLE CHECKS // set wasGrappling to true if the player starts grappling if (grapplingRope.isGrappling) { wasGrappling = true; } #endregion #region SLIDE CHECKS if (CanSlide() && ((LastOnWallLeftTime > 0 && _moveInput.x < 0) || (LastOnWallRightTime > 0 && _moveInput.x > 0))) IsSliding = true; else IsSliding = false; #endregion #region GRAVITY //Higher gravity if we've released the jump input or are falling if (IsSliding) { SetGravityScale(0); } else if (RB.velocity.y < 0 && _moveInput.y < 0) { //Much higher gravity if holding down SetGravityScale(Data.gravityScale * Data.fastFallGravityMult); //Caps maximum fall speed, so when falling over large distances we don't accelerate to insanely high speeds RB.velocity = new Vector2(RB.velocity.x, Mathf.Max(RB.velocity.y, -Data.maxFastFallSpeed)); } else if (_isJumpCut) { //Higher gravity if jump button released SetGravityScale(Data.gravityScale * Data.jumpCutGravityMult); RB.velocity = new Vector2(RB.velocity.x, Mathf.Max(RB.velocity.y, -Data.maxFallSpeed)); } else if ((IsJumping || IsWallJumping || _isJumpFalling) && Mathf.Abs(RB.velocity.y) < Data.jumpHangTimeThreshold) { SetGravityScale(Data.gravityScale * Data.jumpHangGravityMult); } else if (RB.velocity.y < 0) { //Higher gravity if falling SetGravityScale(Data.gravityScale * Data.fallGravityMult); //Caps maximum fall speed, so when falling over large distances we don't accelerate to insanely high speeds RB.velocity = new Vector2(RB.velocity.x, Mathf.Max(RB.velocity.y, -Data.maxFallSpeed)); } else { //Default gravity if standing on a platform or moving upwards SetGravityScale(Data.gravityScale); } #endregion #region SOUND CHECKS if (!IsJumping && !_isJumpFalling && !isRegFalling && _moveInput.x != 0) { if (!soundPlaying) { // print("footsteps PLAY"); audioSource.Play(); soundPlaying = true; } } else if (soundPlaying && audioSource.clip.name == "footsteps") { // print("footsteps stop"); audioSource.Stop(); soundPlaying = false; } #endregion #region UPDATE UI if (trumpet == 0) { gameUI.ToggleTrumpet(false); } /*if (isDashing) { gameUI.ToggleClarinet(false); print("toggle clarinet false?"); }*/ #endregion } } private void FixedUpdate() { if(!isDashing){ //Handle Run if (IsWallJumping) Run(Data.wallJumpRunLerp); else Run(1); //Handle Slide if (IsSliding) Slide(); } } void FreezeControl() { if(!isFrozen) { tempGrav = Data.gravityStrength; tempFall = Data.fallGravityMult; } Data.gravityStrength = 0; Data.fallGravityMult = 0; isFrozen = true; } #region INPUT CALLBACKS //Methods which whandle input detected in Update() public void OnJumpInput() { LastPressedJumpTime = Data.jumpInputBufferTime; } public void OnJumpUpInput() { if (CanJumpCut() || CanWallJumpCut()) _isJumpCut = true; } #endregion #region GENERAL METHODS public void SetGravityScale(float scale) { RB.gravityScale = scale; } #endregion //MOVEMENT METHODS #region RUN METHODS private void Run(float lerpAmount) { if(!isDashing){ //Calculate the direction we want to move in and our desired velocity float targetSpeed = _moveInput.x * Data.runMaxSpeed; //We can reduce are control using Lerp() this smooths changes to are direction and speed targetSpeed = Mathf.Lerp(RB.velocity.x, targetSpeed, lerpAmount); #region Calculate AccelRate float accelRate; //Gets an acceleration value based on if we are accelerating (includes turning) //or trying to decelerate (stop). As well as applying a multiplier if we're air borne. if (LastOnGroundTime > 0) { accelRate = (Mathf.Abs(targetSpeed) > 0.01f) ? Data.runAccelAmount : Data.runDeccelAmount; } else if (wasGrappling) { accelRate = (Mathf.Abs(targetSpeed) > 0.01f) ? Data.runAccelAmount * Data.accelInAir : Data.runDeccelAmount * (Data.deccelInAir / 5); } else { accelRate = (Mathf.Abs(targetSpeed) > 0.01f) ? Data.runAccelAmount * Data.accelInAir : Data.runDeccelAmount * Data.deccelInAir; } #endregion #region Add Bonus Jump Apex Acceleration //Increase are acceleration and maxSpeed when at the apex of their jump, makes the jump feel a bit more bouncy, responsive and natural if ((IsJumping || IsWallJumping || _isJumpFalling) && Mathf.Abs(RB.velocity.y) < Data.jumpHangTimeThreshold) { accelRate *= Data.jumpHangAccelerationMult; targetSpeed *= Data.jumpHangMaxSpeedMult; } #endregion #region Conserve Momentum //We won't slow the player down if they are moving in their desired direction but at a greater speed than their maxSpeed if ((Data.doConserveMomentum && Mathf.Abs(RB.velocity.x) > Mathf.Abs(targetSpeed) && Mathf.Sign(RB.velocity.x) == Mathf.Sign(targetSpeed) && Mathf.Abs(targetSpeed) > 0.01f && LastOnGroundTime < 0) || grapplingRope.isGrappling) { //Prevent any deceleration from happening, or in other words conserve are current momentum //You could experiment with allowing for the player to slightly increae their speed whilst in this "state" accelRate = 0; } #endregion //Calculate difference between current velocity and desired velocity float speedDif = targetSpeed - RB.velocity.x; //Calculate force along x-axis to apply to thr player float movement = speedDif * accelRate; //Convert this to a vector and apply to rigidbody RB.AddForce(movement * Vector2.right, ForceMode2D.Force); /* * For those interested here is what AddForce() will do * RB.velocity = new Vector2(RB.velocity.x + (Time.fixedDeltaTime * speedDif * accelRate) / RB.mass, RB.velocity.y); * Time.fixedDeltaTime is by default in Unity 0.02 seconds equal to 50 FixedUpdate() calls per second */ } } private void Turn() { //stores scale and flips the player along the x axis, Vector3 scale = transform.localScale; scale.x *= -1; transform.localScale = scale; IsFacingRight = !IsFacingRight; } #endregion #region JUMP METHODS private void Jump() { if(!isDashing){ //Ensures we can't call Jump multiple times from one press LastPressedJumpTime = 0; LastOnGroundTime = 0; #region Perform Jump //We increase the force applied if we are falling //This means we'll always feel like we jump the same amount //(setting the player's Y velocity to 0 beforehand will likely work the same, but I find this more elegant :D) float force = Data.jumpForce; if (RB.velocity.y < 0) force -= RB.velocity.y; RB.AddForce(Vector2.up * force, ForceMode2D.Impulse); #endregion } } private void WallJump(int dir) { //Ensures we can't call Wall Jump multiple times from one press LastPressedJumpTime = 0; LastOnGroundTime = 0; LastOnWallRightTime = 0; LastOnWallLeftTime = 0; #region Perform Wall Jump Vector2 force = new Vector2(Data.wallJumpForce.x, Data.wallJumpForce.y); force.x *= dir; //apply force in opposite direction of wall if (Mathf.Sign(RB.velocity.x) != Mathf.Sign(force.x)) force.x -= RB.velocity.x; if (RB.velocity.y < 0) //checks whether player is falling, if so we subtract the velocity.y (counteracting force of gravity). This ensures the player always reaches our desired jump force or greater force.y -= RB.velocity.y; //Unlike in the run we want to use the Impulse mode. //The default mode will apply are force instantly ignoring masss //RB.AddForce(force, ForceMode2D.Impulse); #endregion } #endregion #region OTHER MOVEMENT METHODS private void Slide() { //Works the same as the Run but only in the y-axis //THis seems to work fine, buit maybe you'll find a better way to implement a slide into this system float speedDif = Data.slideSpeed - RB.velocity.y; float movement = speedDif * Data.slideAccel; //So, we clamp the movement here to prevent any over corrections (these aren't noticeable in the Run) //The force applied can't be greater than the (negative) speedDifference * by how many times a second FixedUpdate() is called. For more info research how force are applied to rigidbodies. movement = Mathf.Clamp(movement, -Mathf.Abs(speedDif) * (1 / Time.fixedDeltaTime), Mathf.Abs(speedDif) * (1 / Time.fixedDeltaTime)); RB.AddForce(movement * Vector2.up); } #endregion #region CHECK METHODS public void CheckDirectionToFace(bool isMovingRight) { // if (isMovingRight != IsFacingRight) // Turn(); } private bool CanJump() { if (!IsGrounded() && trumpet > 0) { return true; } return LastOnGroundTime > 0 && !IsJumping; } private bool CanWallJump() { return LastPressedJumpTime > 0 && LastOnWallTime > 0 && LastOnGroundTime <= 0 && (!IsWallJumping || (LastOnWallRightTime > 0 && _lastWallJumpDir == 1) || (LastOnWallLeftTime > 0 && _lastWallJumpDir == -1)); } private bool CanJumpCut() { return IsJumping && RB.velocity.y > 0; } private bool CanWallJumpCut() { return IsWallJumping && RB.velocity.y > 0; } public bool CanSlide() { if (LastOnWallTime > 0 && !IsJumping && !IsWallJumping && LastOnGroundTime <= 0) return true; else return false; } public bool IsGrounded() { // print(Physics2D.OverlapBox(this.transform.position, _groundCheckSize, 0, _groundLayer) && !IsJumping); return (Physics2D.OverlapBox(new Vector2(this.transform.position.x, this.transform.position.y - _groundCheckOffset), _groundCheckSize, 0, _groundLayer) && !IsJumping); } #endregion #region EDITOR METHODS private void OnDrawGizmosSelected() { Gizmos.color = Color.green; Gizmos.DrawWireCube(new Vector2(this.transform.position.x, this.transform.position.y - _groundCheckOffset), _groundCheckSize); Gizmos.color = Color.blue; Gizmos.DrawWireCube(this.transform.position, _wallCheckSize); Gizmos.DrawWireCube(this.transform.position, _wallCheckSize); } #endregion }