Forum begins after the advertisement:


[Part8] wallJump will jump again after

Home Forums Video Game Tutorial Series Creating a Metroidvania in Unity [Part8] wallJump will jump again after

Viewing 13 posts - 1 through 13 (of 13 total)
  • Author
    Posts
  • #14896
    MI NI
    Participant

    Hello, today I learned to play part 8

    I have some questions to ask.
    My wallJump will automatically perform a jump when I hold down the jump. What can I do to avoid it?

    second question
    When I heal, if I have any mana left, I will automatically fire a fireball.

    The third question
    How do I clear the save file and keep it in its original state?

    I don’t know if it’s because I only made one spell, but my unlockSideSpell doesn’t work in CastCoroutine.
    , it cannot be used correctly until I change unlockSideSpell to if(Input.GetButtonUp("Cast/Heal") && castOrHealtimer <= 0.05f && timeSinceCast >= timeBetweenCast && Mana > manaSpellCost && unlockSideSpell) in CastSpell()

    Similarly, this is an article I translated using Google. There may be some errors in wording. Please forgive me.

    #14901
    Joseph Tang
    Moderator

    1. & 3. Automatic jump & UnlockedSideSpell issue.

    For these 2 issues, can you send your PlayerController.cs code?
    For the automatic jump, i believe you should check your code if it has a GetButtonUp("Jump") to activate a jump.

    As for the UnlockedSideSpell, I do not see nor understand the problem. Could you send a video demonstrating how you came about to the issue?


    2. Spellcasting after healing

    This was discussed in the Part 6 Article:

    In our PlayerController Script, let’s make a change to reset the castOrHealTimer in the CastSpell() method instead of GetInputs(). This relocation ensures that the timer doesn’t reset when you release the Cast/Heal button before CastSpell() is called.

    PlayerController.cs

        void GetInputs()
        {
            xAxis = Input.GetAxisRaw("Horizontal");
            yAxis = Input.GetAxisRaw("Vertical");
            attack = Input.GetButtonDown("Attack");
    
            if (Input.GetButton("Cast/Heal"))
            {
                castOrHealTimer += Time.deltaTime;
            }
            else
            {
                castOrHealTimer = 0;
            }
        }
    
        void CastSpell()
        {
            if (Input.GetButtonUp("Cast/Heal") && castOrHealTimer <= 0.05f && timeSinceCast >= timeBetweenCast && Mana >= manaSpellCost)
            {
                pState.casting = true;
                timeSinceCast = 0;
                StartCoroutine(CastCoroutine());
            }
            else
            {
                timeSinceCast += Time.deltaTime;
            }
    
            if (!Input.GetButton("Cast/Heal"))
            {
                castOrHealTimer = 0;
            }
    
            if(Grounded())
            {
                //disable downspell if on the ground
                downSpellFireball.SetActive(false);
            }
            ...
        }

    In the case that this doesn’t work, i can provide a different solution.

    #14934
    MI NI
    Participant

    1. My PlayerController.cs does have GetButtonUp("Jump") but I don’t know how to modify it
    2.unlockSideSpell

    I recorded a video showing the effects of my changes in the code

    3.Spellcasting after healing
    After modification, my Spellcasting after healing will no longer cast spells, but I cannot cast spells normally.

    This is my Playcontroller.cs

    
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.UIElements;
    using UnityEngine.UI;
    
    public class PlayerController : MonoBehaviour
    {
        [Header("水平移動")]
        [SerializeField] private float walkSpeed = 1; //走路速度
        [Space(5)]
    
        [Header("垂直移動")]
        [SerializeField] private float jumpForce = 45; //跳躍速度
        private int jumpBufferCounter = 0; //按下按鈕儲存指令
        [SerializeField] private int jumpBufferFrames; //按下按鈕儲存指令時間/偵
        private float coyoteTimeCounter = 0; //土狼時間
        [SerializeField] private float coyoteTime; //土狼時間/偵
        private int airJumpCounter = 0;
        [SerializeField] private int maxAirJumps;
        [Space(5)]
    
        [Header("WallJump")]
        [SerializeField] private float wallSlidingSpeed; //滑行速度
        [SerializeField] private Transform wallCheck; //牆壁檢查點
        [SerializeField] private LayerMask wallLayer;
        [SerializeField] private float wallJumpingDuration; //蹬牆跳持續時間
        [SerializeField] private Vector2 wallJumpPower;
        float wallJumpingDirection;
        bool isWallSliding;
        bool isWallJumping;
        [Space(5)]
    
        [Header("地面檢查")]
        [SerializeField] private Transform groundCheckPoint; //地面檢查點
        [SerializeField] private float groundCheckY = 0.2f; //
        [SerializeField] private float groundCheckX = 0.5f; //
        [SerializeField] private LayerMask whatIsGround; //檢查是否在地面
        [Space(5)]
    
        [Header("衝刺")]
        [SerializeField] private float dashSpeed; //衝刺速度
        [SerializeField] private float dashTime;  //衝刺時間
        [SerializeField] private float dashCooldown; //衝刺冷卻
        [Space(5)]
    
        [Header("攻擊")]
    
        [SerializeField] private float timeBetweenAttack; //攻擊時間
        [SerializeField] Transform SideAttackTransform, UpAttackTransform, DownAttackTransform; //不同方向攻擊位置
        [SerializeField] Vector2 SideAttackArea, UpAttackArea, DownAttackArea; //不同方向攻擊範圍
        [SerializeField] LayerMask attackableLayer; //可攻擊的對象圖層
        [SerializeField] float damage; //傷害
        [SerializeField] GameObject slashEffect; //劍氣效果
        private bool attack = false;
        private float timeSinceAttack;
    
        bool restoreTime;
        public float restoreTimeSpeed;
    
        [Space(5)]
    
        [Header("後座力")]
        [SerializeField] int recilXSteps = 5;
        [SerializeField] int recilYSteps = 5;
    
        [SerializeField] float recilXSpeed = 5;
        [SerializeField] float recilYSpeed = 5;
    
        private int stepsXRecoiled, stepsYRecoiled;
        [Space(5)]
    
        [Header("血量")]
        public float health;  //當前血量
        public float maxHealth; //最大血量
        [SerializeField] GameObject hitEffect; //被擊效果
        [SerializeField] float hitFlashSpeed; //受傷角色閃爍
        float healTimer;
        [SerializeField] float timeToheal;
        [Space(5)]
    
        [Header("血量圖片")]
        public Text healthText; //血量文字
        public UnityEngine.UI.Image healthImage; //血量圖片
        public UnityEngine.UI.Image healthEffect; //血量緩衝特效
        public float effectTime = 0.5f; //緩衝持續時間
        private Coroutine updateCoroutine;
        [Space(5)]
    
        [Header("魔力")]
        [SerializeField] float mana; //魔力量
        [SerializeField] float maxMana; //最大魔力量
        [SerializeField] float manaDrainSpeed; //魔力消耗速度
        [SerializeField] float manaGain; //獲得魔力
        [SerializeField] UnityEngine.UI.Image manaImage; //魔力圖片
    
        [Header("技能")]
        [SerializeField] float manaSpellCost = 0.3f; //法術消耗
        [SerializeField] float timeBetweenCast = 0.5f; //攻擊間隔速度
        [SerializeField] GameObject sideSpellFireball; //火球物件Prefab
        float timeSinceCast;
        float castOrHealtimer;
    
        [Header("相機")]
        [SerializeField] private float playerFallSpeedTheshold = -10;
    
        [HideInInspector] public PlayerStateList pState; //取得緩衝輸入腳本
        [HideInInspector] public Rigidbody2D rb;
        private Animator anim;
        private SpriteRenderer sr;
    
        private float gravity; //重力
        private float xAxis, yAxis; //取得XY軸輸入
        private bool canDash = true;
        private bool dashed;
        private bool canFlash = true;
        bool openMap; //開啟地圖
        bool openInventory; //開啟背包
    
        //解鎖能力
        public bool unlockWallJump;
        public bool unlockDash;
        public bool unlockAirJump;
        public bool unlockSideSpell = false;
        
    
        public static PlayerController Instance; //防止多個重複的玩家腳本出現
        private void Awake()
        {
            if(Instance != null && Instance != this)
            {
                Destroy(gameObject);
            }
            else
            {
                Instance = this;
            }
    
            DontDestroyOnLoad(gameObject);
    
            
        }
    
        void Start()
        {
            pState = GetComponent<PlayerStateList>();
    
            rb = GetComponent<Rigidbody2D>();
    
            sr = GetComponent<SpriteRenderer>();
    
            anim = GetComponent<Animator>();
    
            SaveData.Instance.LoadPlayerData();
    
            gravity = rb.gravityScale;
    
            if(Health == 0)
            {
                pState.alive = false;
                GameManager.Instance.RespawnPlayer();
            }
    
            //Health = maxHealth; Save之後不再需要
            //Mana = maxMana;
    
            healthImage.fillAmount = health / maxHealth;
            healthEffect.fillAmount = health / maxHealth;
            healthText.text = health.ToString() + "/" + maxHealth.ToString();
            manaImage.fillAmount = Mana;
        }
    
        private void OnDrawGizmos()
        {
            Gizmos.color = Color.red;
            Gizmos.DrawWireCube(SideAttackTransform.position, SideAttackArea);
            Gizmos.DrawWireCube(UpAttackTransform.position, UpAttackArea);
            Gizmos.DrawWireCube(DownAttackTransform.position, DownAttackArea);
            //Gizmos.DrawWireCube(wallCheck.position, )
        }
    
        void Update()
        {
            if (pState.cutscene) return;
            if (pState.alive)
            {
                GetInputs();
                ToggleMap();
                ToggleInventory();
            }
            UpdateJumpVariables();
            RestoreTimeScale();
            UpdateCameraYDampForPlayerFall();
    
            if(pState.alive)
            {
                Heal();
            }
    
            if (pState.dashing || pState.healing) return;
    
            if(pState.alive)
            {
                if(!isWallJumping)
                {
                    Flip();
                    Move();
                    Jump();
                }
                if(unlockWallJump)
                {
                    WallSlide();
                    WallJump();
                }
                if(unlockDash)
                {
                    StartDash();
                }
                
    
                
                Attack();
                CastSpell();
            }
            FlashWhileInvincible();
    
            /*
            if (pState.dashing) return;
            FlashWhileInvincible();
            if (!pState.alive) return;
            Move();
            Heal();
            CastSpell();
            if (pState.healing) return;
            Flip();
            Jump();
            StartDash();
            Attack();
            */
    
        }
    
        private void FixedUpdate()
        {
            if (pState.cutscene) return;
    
            if (pState.dashing || pState.healing) return;
            Recoil();
        }
    
        void GetInputs() //獲取輸入
        {
            xAxis = Input.GetAxisRaw("Horizontal");
            yAxis = Input.GetAxisRaw("Vertical");
            attack = Input.GetButtonDown("Attack");
            openMap = Input.GetButton("Map");
            openInventory = Input.GetButton("Inventory");
    
            if (Input.GetButton("Cast/Heal"))
            {
                castOrHealtimer += Time.deltaTime;
            }
            /*
            else
            {
                castOrHealtimer = 0;
            }
            */
        }
    
        void ToggleMap()
        {
            if(openMap)
            {
                UIManager.Instance.mapHandler.SetActive(true);
            }
            else
            {
                UIManager.Instance.mapHandler.SetActive(false);
            }
        }
    
        void ToggleInventory()
        {
            if (openInventory)
            {
                UIManager.Instance.inventory.SetActive(true);
            }
            else
            {
                UIManager.Instance.inventory.SetActive(false);
            }
        }
    
        void Flip() //翻轉
        {
            if(xAxis < 0)
            {
                transform.localScale = new Vector2(-1, transform.localScale.y);
                pState.lookingRight = false;
            }
            else if(xAxis >0)
            {
                transform.localScale = new Vector2(1, transform.localScale.y);
                pState.lookingRight = true;
            }
        }
    
        private void Move() //移動
        {
            if (pState.healing) rb.velocity = new Vector2(0, 0); //如果玩家正在治療則無法移動
            rb.velocity = new Vector2(walkSpeed * xAxis ,rb.velocity.y);
            anim.SetBool("Walk", rb.velocity.x !=0 && Grounded());
        }
    
        void UpdateCameraYDampForPlayerFall()
        {
            //if falling past a certain speed threshold
            if (rb.velocity.y < playerFallSpeedTheshold && !CameraManager.Instance.isLerpingYDamp && !CameraManager.Instance.hasLerpingYDamping)
            {
                StartCoroutine(CameraManager.Instance.LerpYDamping(true));
            }
            //if standing stil or moveing up
            if(rb.velocity.y >= 0 && !CameraManager.Instance.isLerpingYDamp && CameraManager.Instance.hasLerpingYDamping)
            {
                //reset camera funtion
                CameraManager.Instance.hasLerpingYDamping = false;
                StartCoroutine(CameraManager.Instance.LerpYDamping(false));
            }
        }
    
        void StartDash() //衝刺
        {
            if(Input.GetButtonDown("Dash") && canDash && !dashed)
            {
                StartCoroutine(Dash());
                dashed = true;
            }
    
            if(Grounded())
            {
                dashed = false;
            }
        }
    
        IEnumerator Dash()
        {
            canDash = false; 
            pState.dashing = true;
            anim.SetTrigger("Dash");
            rb.gravityScale = 0;
            int _dir = pState.lookingRight ? 1 : -1;
            rb.velocity = new Vector2(_dir * dashSpeed, 0);
            yield return new WaitForSeconds(dashTime);
            rb.gravityScale = gravity;
            pState.dashing = false;
            yield return new WaitForSeconds(dashCooldown);
            canDash = true;
    
        }
    
        public IEnumerator WalkIntoNewScene(Vector2 _exitDir, float _delay) //過場動畫
        {
            pState.invincible = true;
    
            if(_exitDir.y > 0) //如果退出位置為向上
            {
                rb.velocity = jumpForce * _exitDir;
            }
    
            //如果退出位置為水平移動
            if(_exitDir.x != 0)
            {
                xAxis = _exitDir.x > 0 ? 1 : -1;
    
                Move();
            }
    
            Flip();
            yield return new WaitForSeconds(_delay);
            pState.invincible = false;
            pState.cutscene = false;
    
        }
    
        void Attack() //攻擊
        {
            timeSinceAttack += Time.deltaTime;
            if(attack && timeSinceAttack >= timeBetweenAttack)
            {
                timeSinceAttack = 0;
                anim.SetTrigger("Attack");
    
                if(yAxis == 0 || yAxis < 0 && Grounded()) //如果玩家在地面上且沒有按住"上",正常攻擊
                {
                    int _recoilLeftOrRight = pState.lookingRight ? 1 : -1; 
    
                    Hit(SideAttackTransform, SideAttackArea, ref pState.recoilingX, Vector2.right * _recoilLeftOrRight, recilXSpeed);
                    Instantiate(slashEffect, SideAttackTransform);
                }
                else if(yAxis > 0) //如果按住"上",上攻擊
                {
                    Hit(UpAttackTransform, UpAttackArea, ref pState.recoilingY, Vector2.up, recilYSpeed);
                    SlashEffectAtAngle(slashEffect, 90, UpAttackTransform);
                }
                else if (yAxis < 0 && !Grounded()) //如果不在地面且按住"下",下攻擊
                {
                    Hit(DownAttackTransform, DownAttackArea, ref pState.recoilingY, Vector2.down, recilYSpeed);
                    SlashEffectAtAngle(slashEffect, -90, DownAttackTransform);
                }
    
            }
        }
    
        private void Hit(Transform _attackTransfrom, Vector2 _attackArea, ref bool _recoilBool,Vector2 _recoilDir , float _recoilStrength) //給予傷害
        {
            Collider2D[] objectsToHit = Physics2D.OverlapBoxAll(_attackTransfrom.position, _attackArea, 0, attackableLayer);
            List<Enemy> hitEnemise = new List<Enemy>();
    
            if(objectsToHit.Length > 0)
            {
                //Debug.Log("Hit");
                _recoilBool = true;
    
            }
            for(int i = 0; i < objectsToHit.Length; i++)
            {
                if(objectsToHit[i].GetComponent<Enemy>() != null)
                {
                    objectsToHit[i].GetComponent<Enemy>().EnemyHit(damage, _recoilDir , _recoilStrength)  ;
                    /*
                    Enemy e = objectsToHit[i].GetComponent<Enemy>();
                    if(e && !hitEnemise.Contains(e))
                    {
                        e.EnemyHit(damage, (transform.position - objectsToHit[i].transform.position).normalized, _recoilStrength);
                        hitEnemise.Add(e);
                    }
                    */
                    if (objectsToHit[i].CompareTag("Monster")) //打到怪物增加魔力
                    {
                        Mana += manaGain;
                    }
                }
            }
        }
        
        void SlashEffectAtAngle(GameObject _slashEffect, int _effectAngle, Transform _attackTransform)
        {
            _slashEffect = Instantiate(_slashEffect, _attackTransform);
            _slashEffect.transform.eulerAngles = new Vector3(0, 0, _effectAngle);
            _slashEffect.transform.localScale = new Vector2(transform.localScale.x, transform.localScale.y);
        }
    
        void Recoil() //反衝
        {
            if(pState.recoilingX)
            {
                if (pState.lookingRight)
                {
                    rb.velocity = new Vector2(-recilXSpeed, 0);
                }
                else
                {
                    rb.velocity = new Vector2(recilXSpeed, 0);
                }
            }
    
            if(pState.recoilingY)
            {
                rb.gravityScale = 0;
                if (yAxis < 0)
                {
                    rb.velocity = new Vector2(rb.velocity.x, recilYSpeed);
                }
                else
                {
                    rb.velocity = new Vector2(rb.velocity.x, -recilYSpeed);
                }
    
                airJumpCounter = 0;
    
            }
            else
            {
                rb.gravityScale = gravity;
            }
            
            //Stop recoil 停止反衝
            if(pState.recoilingX && stepsXRecoiled < recilXSteps)
            {
                stepsXRecoiled++;
            }
            else
            {
                StopRecoilX();
            }
    
            if (pState.recoilingY && stepsYRecoiled < recilYSteps)
            {
                stepsYRecoiled++;
            }
            else
            {
                StopRecoilY();
            }
    
            if(Grounded())
            {
                StopRecoilY();
            }
    
        }
    
        void StopRecoilX()
        {
            stepsXRecoiled = 0;
            pState.recoilingX = false;
        }
        void StopRecoilY()
        {
            stepsYRecoiled = 0;
            pState.recoilingY = false;
        }
    
        public void TakeDamage(float _damage) //玩家受傷
        {
            if(pState.alive)
            {
                Health -= Mathf.RoundToInt(_damage);
                if(Health <= 0)
                {
                    Health = 0;
                    StartCoroutine(Death());
                }
                else
                {
                    StartCoroutine(StopTakingDamage());
    
                    healthImage.fillAmount = health / maxHealth; //血量圖片
                    healthText.text = health.ToString() + "/" + maxHealth.ToString(); //血量文字
    
                    if (updateCoroutine != null)
                    {
                        StopCoroutine(updateCoroutine);
                    }
                    updateCoroutine = StartCoroutine(UpdateHealthEffect());
                } 
            }
        }
    
        private IEnumerator UpdateHealthEffect() //血量緩降協程
        {
            float effectLength = healthEffect.fillAmount - healthImage.fillAmount;
            float elapsedTime = 0f;
    
            while (elapsedTime < effectTime && effectLength != 0)
            {
                elapsedTime += Time.deltaTime;
                healthEffect.fillAmount = Mathf.Lerp
                (healthImage.fillAmount + effectLength, healthImage.fillAmount, elapsedTime / effectTime);
                yield return null;
            }
            healthEffect.fillAmount = healthImage.fillAmount;
        }
    
        IEnumerator StopTakingDamage() //無敵偵
        {
            pState.invincible = true;
            GameObject _hitEffect = Instantiate(hitEffect, transform.position, Quaternion.identity);
            Destroy(_hitEffect, 1.5f);
            anim.SetTrigger("Hurt");
            yield return new WaitForSeconds(1f);
            pState.invincible = false;
        }
    
        IEnumerator Flash()
        {
            sr.enabled = !sr.enabled;
            canFlash = false;
            yield return new WaitForSeconds(0.2f);
            canFlash = true;
        }    
    
        void FlashWhileInvincible()
        {
            //將渲染改為"PingPongLerp" 如果玩家是無敵狀態則將其從白色改為黑色 否則將玩家設定為白色
            //sr.material.color = pState.invincible ? Color.Lerp(Color.white, Color.black, Mathf.PingPong(Time.time * hitFlashSpeed, 1.0f)) : Color.white;
            if(pState.invincible && !pState.cutscene)
            {
                if (Time.timeScale > 0.2f && canFlash) 
                {
                    StartCoroutine(Flash()); 
                }
            }
            else
            {
                sr.enabled = true;
            }
        }
    
        void RestoreTimeScale() //受傷暫停時間
        {
            if(restoreTime) //檢查是否為真
            {
                if(Time.timeScale < 1) //如果小於1 時間速度加快到達1
                {
                    Time.timeScale += Time.unscaledDeltaTime * restoreTimeSpeed;
                }
                else //如果超過1 時間為1
                {
                    Time.timeScale = 1;
                    restoreTime = false;
                }
            }
        }
    
        public void HitStopTime(float _newTimeScale, int _restoreSpeed, float _delay)
        {
            restoreTimeSpeed = _restoreSpeed;
            Time.timeScale = _newTimeScale;
    
            if(_delay > 0)
            {
                StopCoroutine(StartTimeAgain(_delay));
                StartCoroutine(StartTimeAgain(_delay));
            }
            else
            {
                restoreTime = true;
            }
        }
    
        IEnumerator StartTimeAgain(float _delay)
        {
            yield return new WaitForSecondsRealtime(_delay);
            restoreTime = true;
        }
    
        IEnumerator Death()
        {
            pState.alive = false;
            Time.timeScale = 1f; //避免受傷暫停時間導致出錯
            //可在此呼叫粒子效果
            anim.SetTrigger("Death");
    
            rb.constraints = RigidbodyConstraints2D.FreezePosition;
            rb.constraints = RigidbodyConstraints2D.FreezeRotation;
            GetComponent<BoxCollider2D>().enabled = false;
    
            yield return new WaitForSeconds(0.9f);
            StartCoroutine(UIManager.Instance.ActiveDeathScreen());
        }
    
        public void Respawned()
        {
            if(!pState.alive)
            {
                rb.constraints = RigidbodyConstraints2D.None;
                rb.constraints = RigidbodyConstraints2D.FreezeRotation;
                GetComponent<BoxCollider2D>().enabled = true;
    
                pState.alive = true;
                Health = maxHealth;
                anim.Play("PlayerIdle");
            }
        }
    
        public float Health //血量
        {
            get { return health; }
            set
            {
                if(health != value)
                {
                    health = Mathf.Clamp(value, 0, maxHealth);
    
                }
            }
        }
    
        void Heal()
        {
            if(Input.GetButton("Cast/Heal") && castOrHealtimer > 0.05f && health < maxHealth && Mana > 0 && Grounded() && !pState.dashing)
            {
                pState.healing = true;
                anim.SetBool("Heal", true);
    
                //補血
                healTimer += Time.deltaTime;
                if(healTimer >= timeToheal)
                { 
                    health++; //恢復血量
                    healTimer = 0; //血量計時器
                    healthImage.fillAmount = health / maxHealth; //血量圖片
                    healthEffect.fillAmount = health / maxHealth; //血量效果圖片
                    healthText.text = health.ToString() + "/" + maxHealth.ToString(); //血量文字
                }
                Mana -= Time.deltaTime * manaDrainSpeed;//魔力消耗
            }
            else
            {
                pState.healing = false;
                anim.SetBool("Heal", false);
                healTimer = 0;
            }
    
            
        }
    
        public float Mana
        {
            get { return mana; }
            set
            {
                if(mana != value)
                {
                    mana = Mathf.Clamp(value, 0, maxMana);
                    manaImage.fillAmount = Mana;
                }
    
            }
        }
    
        void CastSpell()
        {
            if(Input.GetButtonUp("Cast/Heal") && castOrHealtimer <= 0.05f && timeSinceCast >= timeBetweenCast && Mana > manaSpellCost && unlockSideSpell)
            {
                pState.casting = true;
                timeSinceCast = 0;
                StartCoroutine(CastCoroutine());
            }
            else
            {
                timeSinceCast += Time.deltaTime;
            }
            
            if(!Input.GetButton("Cast/Heal"))
            {
                castOrHealtimer = 0;
            }
            
        }
    
        IEnumerator CastCoroutine()
        {
            //橫向技能
            if(yAxis == 0 || (yAxis < 0 && Grounded()) ) //沒有垂直輸入時播放 目前無需要按住上下才能施放的技能
            {
                anim.SetBool("Cast", true);
                yield return new WaitForSeconds(0.1f); //此為等待動畫到達正確播放時間 
    
                GameObject _fireBall = Instantiate(sideSpellFireball, SideAttackTransform.position, Quaternion.identity); //實例化該物件
    
                //Flip火球
                if(pState.lookingRight)
                {
                    _fireBall.transform.eulerAngles = Vector3.zero;
                }
                else
                {
                    _fireBall.transform.eulerAngles = new Vector2(_fireBall.transform.eulerAngles.x, 180); //如果不是面向右邊翻轉180度
                }
                pState.recoilingX = true; //給予玩家反衝
    
                Mana -= manaSpellCost; //魔力減少
                yield return new WaitForSeconds(0.3f); //此為動畫撥放完整時間
            }
    
            anim.SetBool("Cast", false);
            pState.casting = false;
    
        }
    
        public bool Grounded() //地面檢測
        {
            if (Physics2D.Raycast(groundCheckPoint.position, Vector2.down, groundCheckY, whatIsGround) 
                || Physics2D.Raycast(groundCheckPoint.position + new Vector3(groundCheckX , 0, 0), Vector2.down, groundCheckY, whatIsGround)
                || Physics2D.Raycast(groundCheckPoint.position + new Vector3(-groundCheckX , 0, 0), Vector2.down, groundCheckY, whatIsGround))
            {
                return true;
            }
            else
            {
                return false;
            }
        }
    
        void Jump() //跳躍
        {
            if (jumpBufferCounter > 0 && coyoteTimeCounter > 0 &&!pState.jumping)
            {
                rb.velocity = new Vector3(rb.velocity.x, jumpForce);
    
                pState.jumping = true;
            }
    
             if (!Grounded() && airJumpCounter < maxAirJumps && Input.GetButtonDown("Jump") && unlockAirJump)
            {
                pState.jumping = true;
    
                airJumpCounter++;
    
                rb.velocity = new Vector3(rb.velocity.x, jumpForce);
            }
    
            if (Input.GetButtonUp("Jump") && rb.velocity.y > 3)
            {
                rb.velocity = new Vector2(rb.velocity.x, 0);
    
                pState.jumping = false;
            }
    
            anim.SetBool("Jump", !Grounded());
        }
    
        void UpdateJumpVariables()
        {
            if(Grounded())
            {
                pState.jumping = false;
                coyoteTimeCounter = coyoteTime;
                airJumpCounter = 0;  
            }
            else
            {
                coyoteTimeCounter -= Time.deltaTime;
            }
    
            if(Input.GetButtonDown("Jump"))
            {
                jumpBufferCounter = jumpBufferFrames;
            }
            else
            {
                jumpBufferCounter--;
            }
    
        }
    
        private bool Walled()
        {
            return Physics2D.OverlapCircle(wallCheck.position, 0.2f, wallLayer);
        }
    
        void WallSlide()
        {
            if(Walled() && !Grounded() && xAxis !=0)
            {
                isWallSliding = true;
                rb.velocity = new Vector2(rb.velocity.x, Mathf.Clamp(rb.velocity.y, -wallSlidingSpeed, float.MaxValue));
            }
            else
            {
                isWallSliding = false;
            }
        }
    
        void WallJump()
        {
            if(isWallSliding)
            {
                isWallJumping = false;
                wallJumpingDirection = !pState.lookingRight ? 1 : -1;
    
                CancelInvoke(nameof(StopWallJumping));
            }
    
            if(Input.GetButtonDown("Jump") && isWallSliding)
            {
                isWallJumping = true;
                rb.velocity = new Vector2(wallJumpingDirection * wallJumpPower.x, wallJumpPower.y);
    
                dashed = false;
                airJumpCounter = 0;
    
                pState.lookingRight = !pState.lookingRight;
                transform.eulerAngles = new Vector2(transform.eulerAngles.x, 180);
    
                Invoke(nameof(StopWallJumping), wallJumpingDuration);
            }
        }
    
        void StopWallJumping()
        {
            isWallJumping = false;
            transform.eulerAngles = new Vector2(transform.eulerAngles.x, 0);
        }
    
    }
    
    #14935
    MI NI
    Participant

    1. My PlayerController.cs does have GetButtonUp("Jump") but I don’t know how to modify it
    2.unlockSideSpell

    I recorded a video showing the effects of my changes in the code

    3.Spellcasting after healing
    After modification, my Spellcasting after healing will no longer cast spells, but I cannot cast spells normally.

    This is my Playcontroller.cs

    
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.UIElements;
    using UnityEngine.UI;
    
    public class PlayerController : MonoBehaviour
    {
        [Header("水平移動")]
        [SerializeField] private float walkSpeed = 1; //走路速度
        [Space(5)]
    
        [Header("垂直移動")]
        [SerializeField] private float jumpForce = 45; //跳躍速度
        private int jumpBufferCounter = 0; //按下按鈕儲存指令
        [SerializeField] private int jumpBufferFrames; //按下按鈕儲存指令時間/偵
        private float coyoteTimeCounter = 0; //土狼時間
        [SerializeField] private float coyoteTime; //土狼時間/偵
        private int airJumpCounter = 0;
        [SerializeField] private int maxAirJumps;
        [Space(5)]
    
        [Header("WallJump")]
        [SerializeField] private float wallSlidingSpeed; //滑行速度
        [SerializeField] private Transform wallCheck; //牆壁檢查點
        [SerializeField] private LayerMask wallLayer;
        [SerializeField] private float wallJumpingDuration; //蹬牆跳持續時間
        [SerializeField] private Vector2 wallJumpPower;
        float wallJumpingDirection;
        bool isWallSliding;
        bool isWallJumping;
        [Space(5)]
    
        [Header("地面檢查")]
        [SerializeField] private Transform groundCheckPoint; //地面檢查點
        [SerializeField] private float groundCheckY = 0.2f; //
        [SerializeField] private float groundCheckX = 0.5f; //
        [SerializeField] private LayerMask whatIsGround; //檢查是否在地面
        [Space(5)]
    
        [Header("衝刺")]
        [SerializeField] private float dashSpeed; //衝刺速度
        [SerializeField] private float dashTime;  //衝刺時間
        [SerializeField] private float dashCooldown; //衝刺冷卻
        [Space(5)]
    
        [Header("攻擊")]
    
        [SerializeField] private float timeBetweenAttack; //攻擊時間
        [SerializeField] Transform SideAttackTransform, UpAttackTransform, DownAttackTransform; //不同方向攻擊位置
        [SerializeField] Vector2 SideAttackArea, UpAttackArea, DownAttackArea; //不同方向攻擊範圍
        [SerializeField] LayerMask attackableLayer; //可攻擊的對象圖層
        [SerializeField] float damage; //傷害
        [SerializeField] GameObject slashEffect; //劍氣效果
        private bool attack = false;
        private float timeSinceAttack;
    
        bool restoreTime;
        public float restoreTimeSpeed;
    
        [Space(5)]
    
        [Header("後座力")]
        [SerializeField] int recilXSteps = 5;
        [SerializeField] int recilYSteps = 5;
    
        [SerializeField] float recilXSpeed = 5;
        [SerializeField] float recilYSpeed = 5;
    
        private int stepsXRecoiled, stepsYRecoiled;
        [Space(5)]
    
        [Header("血量")]
        public float health;  //當前血量
        public float maxHealth; //最大血量
        [SerializeField] GameObject hitEffect; //被擊效果
        [SerializeField] float hitFlashSpeed; //受傷角色閃爍
        float healTimer;
        [SerializeField] float timeToheal;
        [Space(5)]
    
        [Header("血量圖片")]
        public Text healthText; //血量文字
        public UnityEngine.UI.Image healthImage; //血量圖片
        public UnityEngine.UI.Image healthEffect; //血量緩衝特效
        public float effectTime = 0.5f; //緩衝持續時間
        private Coroutine updateCoroutine;
        [Space(5)]
    
        [Header("魔力")]
        [SerializeField] float mana; //魔力量
        [SerializeField] float maxMana; //最大魔力量
        [SerializeField] float manaDrainSpeed; //魔力消耗速度
        [SerializeField] float manaGain; //獲得魔力
        [SerializeField] UnityEngine.UI.Image manaImage; //魔力圖片
    
        [Header("技能")]
        [SerializeField] float manaSpellCost = 0.3f; //法術消耗
        [SerializeField] float timeBetweenCast = 0.5f; //攻擊間隔速度
        [SerializeField] GameObject sideSpellFireball; //火球物件Prefab
        float timeSinceCast;
        float castOrHealtimer;
    
        [Header("相機")]
        [SerializeField] private float playerFallSpeedTheshold = -10;
    
        [HideInInspector] public PlayerStateList pState; //取得緩衝輸入腳本
        [HideInInspector] public Rigidbody2D rb;
        private Animator anim;
        private SpriteRenderer sr;
    
        private float gravity; //重力
        private float xAxis, yAxis; //取得XY軸輸入
        private bool canDash = true;
        private bool dashed;
        private bool canFlash = true;
        bool openMap; //開啟地圖
        bool openInventory; //開啟背包
    
        //解鎖能力
        public bool unlockWallJump;
        public bool unlockDash;
        public bool unlockAirJump;
        public bool unlockSideSpell = false;
        
    
        public static PlayerController Instance; //防止多個重複的玩家腳本出現
        private void Awake()
        {
            if(Instance != null && Instance != this)
            {
                Destroy(gameObject);
            }
            else
            {
                Instance = this;
            }
    
            DontDestroyOnLoad(gameObject);
    
            
        }
    
        void Start()
        {
            pState = GetComponent<PlayerStateList>();
    
            rb = GetComponent<Rigidbody2D>();
    
            sr = GetComponent<SpriteRenderer>();
    
            anim = GetComponent<Animator>();
    
            SaveData.Instance.LoadPlayerData();
    
            gravity = rb.gravityScale;
    
            if(Health == 0)
            {
                pState.alive = false;
                GameManager.Instance.RespawnPlayer();
            }
    
            //Health = maxHealth; Save之後不再需要
            //Mana = maxMana;
    
            healthImage.fillAmount = health / maxHealth;
            healthEffect.fillAmount = health / maxHealth;
            healthText.text = health.ToString() + "/" + maxHealth.ToString();
            manaImage.fillAmount = Mana;
        }
    
        private void OnDrawGizmos()
        {
            Gizmos.color = Color.red;
            Gizmos.DrawWireCube(SideAttackTransform.position, SideAttackArea);
            Gizmos.DrawWireCube(UpAttackTransform.position, UpAttackArea);
            Gizmos.DrawWireCube(DownAttackTransform.position, DownAttackArea);
            //Gizmos.DrawWireCube(wallCheck.position, )
        }
    
        void Update()
        {
            if (pState.cutscene) return;
            if (pState.alive)
            {
                GetInputs();
                ToggleMap();
                ToggleInventory();
            }
            UpdateJumpVariables();
            RestoreTimeScale();
            UpdateCameraYDampForPlayerFall();
    
            if(pState.alive)
            {
                Heal();
            }
    
            if (pState.dashing || pState.healing) return;
    
            if(pState.alive)
            {
                if(!isWallJumping)
                {
                    Flip();
                    Move();
                    Jump();
                }
                if(unlockWallJump)
                {
                    WallSlide();
                    WallJump();
                }
                if(unlockDash)
                {
                    StartDash();
                }
                
    
                
                Attack();
                CastSpell();
            }
            FlashWhileInvincible();
    
            /*
            if (pState.dashing) return;
            FlashWhileInvincible();
            if (!pState.alive) return;
            Move();
            Heal();
            CastSpell();
            if (pState.healing) return;
            Flip();
            Jump();
            StartDash();
            Attack();
            */
    
        }
    
        private void FixedUpdate()
        {
            if (pState.cutscene) return;
    
            if (pState.dashing || pState.healing) return;
            Recoil();
        }
    
        void GetInputs() //獲取輸入
        {
            xAxis = Input.GetAxisRaw("Horizontal");
            yAxis = Input.GetAxisRaw("Vertical");
            attack = Input.GetButtonDown("Attack");
            openMap = Input.GetButton("Map");
            openInventory = Input.GetButton("Inventory");
    
            if (Input.GetButton("Cast/Heal"))
            {
                castOrHealtimer += Time.deltaTime;
            }
            /*
            else
            {
                castOrHealtimer = 0;
            }
            */
        }
    
        void ToggleMap()
        {
            if(openMap)
            {
                UIManager.Instance.mapHandler.SetActive(true);
            }
            else
            {
                UIManager.Instance.mapHandler.SetActive(false);
            }
        }
    
        void ToggleInventory()
        {
            if (openInventory)
            {
                UIManager.Instance.inventory.SetActive(true);
            }
            else
            {
                UIManager.Instance.inventory.SetActive(false);
            }
        }
    
        void Flip() //翻轉
        {
            if(xAxis < 0)
            {
                transform.localScale = new Vector2(-1, transform.localScale.y);
                pState.lookingRight = false;
            }
            else if(xAxis >0)
            {
                transform.localScale = new Vector2(1, transform.localScale.y);
                pState.lookingRight = true;
            }
        }
    
        private void Move() //移動
        {
            if (pState.healing) rb.velocity = new Vector2(0, 0); //如果玩家正在治療則無法移動
            rb.velocity = new Vector2(walkSpeed * xAxis ,rb.velocity.y);
            anim.SetBool("Walk", rb.velocity.x !=0 && Grounded());
        }
    
        void UpdateCameraYDampForPlayerFall()
        {
            //if falling past a certain speed threshold
            if (rb.velocity.y < playerFallSpeedTheshold && !CameraManager.Instance.isLerpingYDamp && !CameraManager.Instance.hasLerpingYDamping)
            {
                StartCoroutine(CameraManager.Instance.LerpYDamping(true));
            }
            //if standing stil or moveing up
            if(rb.velocity.y >= 0 && !CameraManager.Instance.isLerpingYDamp && CameraManager.Instance.hasLerpingYDamping)
            {
                //reset camera funtion
                CameraManager.Instance.hasLerpingYDamping = false;
                StartCoroutine(CameraManager.Instance.LerpYDamping(false));
            }
        }
    
        void StartDash() //衝刺
        {
            if(Input.GetButtonDown("Dash") && canDash && !dashed)
            {
                StartCoroutine(Dash());
                dashed = true;
            }
    
            if(Grounded())
            {
                dashed = false;
            }
        }
    
        IEnumerator Dash()
        {
            canDash = false; 
            pState.dashing = true;
            anim.SetTrigger("Dash");
            rb.gravityScale = 0;
            int _dir = pState.lookingRight ? 1 : -1;
            rb.velocity = new Vector2(_dir * dashSpeed, 0);
            yield return new WaitForSeconds(dashTime);
            rb.gravityScale = gravity;
            pState.dashing = false;
            yield return new WaitForSeconds(dashCooldown);
            canDash = true;
    
        }
    
        public IEnumerator WalkIntoNewScene(Vector2 _exitDir, float _delay) //過場動畫
        {
            pState.invincible = true;
    
            if(_exitDir.y > 0) //如果退出位置為向上
            {
                rb.velocity = jumpForce * _exitDir;
            }
    
            //如果退出位置為水平移動
            if(_exitDir.x != 0)
            {
                xAxis = _exitDir.x > 0 ? 1 : -1;
    
                Move();
            }
    
            Flip();
            yield return new WaitForSeconds(_delay);
            pState.invincible = false;
            pState.cutscene = false;
    
        }
    
        void Attack() //攻擊
        {
            timeSinceAttack += Time.deltaTime;
            if(attack && timeSinceAttack >= timeBetweenAttack)
            {
                timeSinceAttack = 0;
                anim.SetTrigger("Attack");
    
                if(yAxis == 0 || yAxis < 0 && Grounded()) //如果玩家在地面上且沒有按住"上",正常攻擊
                {
                    int _recoilLeftOrRight = pState.lookingRight ? 1 : -1; 
    
                    Hit(SideAttackTransform, SideAttackArea, ref pState.recoilingX, Vector2.right * _recoilLeftOrRight, recilXSpeed);
                    Instantiate(slashEffect, SideAttackTransform);
                }
                else if(yAxis > 0) //如果按住"上",上攻擊
                {
                    Hit(UpAttackTransform, UpAttackArea, ref pState.recoilingY, Vector2.up, recilYSpeed);
                    SlashEffectAtAngle(slashEffect, 90, UpAttackTransform);
                }
                else if (yAxis < 0 && !Grounded()) //如果不在地面且按住"下",下攻擊
                {
                    Hit(DownAttackTransform, DownAttackArea, ref pState.recoilingY, Vector2.down, recilYSpeed);
                    SlashEffectAtAngle(slashEffect, -90, DownAttackTransform);
                }
    
            }
        }
    
        private void Hit(Transform _attackTransfrom, Vector2 _attackArea, ref bool _recoilBool,Vector2 _recoilDir , float _recoilStrength) //給予傷害
        {
            Collider2D[] objectsToHit = Physics2D.OverlapBoxAll(_attackTransfrom.position, _attackArea, 0, attackableLayer);
            List<Enemy> hitEnemise = new List<Enemy>();
    
            if(objectsToHit.Length > 0)
            {
                //Debug.Log("Hit");
                _recoilBool = true;
    
            }
            for(int i = 0; i < objectsToHit.Length; i++)
            {
                if(objectsToHit[i].GetComponent<Enemy>() != null)
                {
                    objectsToHit[i].GetComponent<Enemy>().EnemyHit(damage, _recoilDir , _recoilStrength)  ;
                    /*
                    Enemy e = objectsToHit[i].GetComponent<Enemy>();
                    if(e && !hitEnemise.Contains(e))
                    {
                        e.EnemyHit(damage, (transform.position - objectsToHit[i].transform.position).normalized, _recoilStrength);
                        hitEnemise.Add(e);
                    }
                    */
                    if (objectsToHit[i].CompareTag("Monster")) //打到怪物增加魔力
                    {
                        Mana += manaGain;
                    }
                }
            }
        }
        
        void SlashEffectAtAngle(GameObject _slashEffect, int _effectAngle, Transform _attackTransform)
        {
            _slashEffect = Instantiate(_slashEffect, _attackTransform);
            _slashEffect.transform.eulerAngles = new Vector3(0, 0, _effectAngle);
            _slashEffect.transform.localScale = new Vector2(transform.localScale.x, transform.localScale.y);
        }
    
        void Recoil() //反衝
        {
            if(pState.recoilingX)
            {
                if (pState.lookingRight)
                {
                    rb.velocity = new Vector2(-recilXSpeed, 0);
                }
                else
                {
                    rb.velocity = new Vector2(recilXSpeed, 0);
                }
            }
    
            if(pState.recoilingY)
            {
                rb.gravityScale = 0;
                if (yAxis < 0)
                {
                    rb.velocity = new Vector2(rb.velocity.x, recilYSpeed);
                }
                else
                {
                    rb.velocity = new Vector2(rb.velocity.x, -recilYSpeed);
                }
    
                airJumpCounter = 0;
    
            }
            else
            {
                rb.gravityScale = gravity;
            }
            
            //Stop recoil 停止反衝
            if(pState.recoilingX && stepsXRecoiled < recilXSteps)
            {
                stepsXRecoiled++;
            }
            else
            {
                StopRecoilX();
            }
    
            if (pState.recoilingY && stepsYRecoiled < recilYSteps)
            {
                stepsYRecoiled++;
            }
            else
            {
                StopRecoilY();
            }
    
            if(Grounded())
            {
                StopRecoilY();
            }
    
        }
    
        void StopRecoilX()
        {
            stepsXRecoiled = 0;
            pState.recoilingX = false;
        }
        void StopRecoilY()
        {
            stepsYRecoiled = 0;
            pState.recoilingY = false;
        }
    
        public void TakeDamage(float _damage) //玩家受傷
        {
            if(pState.alive)
            {
                Health -= Mathf.RoundToInt(_damage);
                if(Health <= 0)
                {
                    Health = 0;
                    StartCoroutine(Death());
                }
                else
                {
                    StartCoroutine(StopTakingDamage());
    
                    healthImage.fillAmount = health / maxHealth; //血量圖片
                    healthText.text = health.ToString() + "/" + maxHealth.ToString(); //血量文字
    
                    if (updateCoroutine != null)
                    {
                        StopCoroutine(updateCoroutine);
                    }
                    updateCoroutine = StartCoroutine(UpdateHealthEffect());
                } 
            }
        }
    
        private IEnumerator UpdateHealthEffect() //血量緩降協程
        {
            float effectLength = healthEffect.fillAmount - healthImage.fillAmount;
            float elapsedTime = 0f;
    
            while (elapsedTime < effectTime && effectLength != 0)
            {
                elapsedTime += Time.deltaTime;
                healthEffect.fillAmount = Mathf.Lerp
                (healthImage.fillAmount + effectLength, healthImage.fillAmount, elapsedTime / effectTime);
                yield return null;
            }
            healthEffect.fillAmount = healthImage.fillAmount;
        }
    
        IEnumerator StopTakingDamage() //無敵偵
        {
            pState.invincible = true;
            GameObject _hitEffect = Instantiate(hitEffect, transform.position, Quaternion.identity);
            Destroy(_hitEffect, 1.5f);
            anim.SetTrigger("Hurt");
            yield return new WaitForSeconds(1f);
            pState.invincible = false;
        }
    
        IEnumerator Flash()
        {
            sr.enabled = !sr.enabled;
            canFlash = false;
            yield return new WaitForSeconds(0.2f);
            canFlash = true;
        }    
    
        void FlashWhileInvincible()
        {
            //將渲染改為"PingPongLerp" 如果玩家是無敵狀態則將其從白色改為黑色 否則將玩家設定為白色
            //sr.material.color = pState.invincible ? Color.Lerp(Color.white, Color.black, Mathf.PingPong(Time.time * hitFlashSpeed, 1.0f)) : Color.white;
            if(pState.invincible && !pState.cutscene)
            {
                if (Time.timeScale > 0.2f && canFlash) 
                {
                    StartCoroutine(Flash()); 
                }
            }
            else
            {
                sr.enabled = true;
            }
        }
    
        void RestoreTimeScale() //受傷暫停時間
        {
            if(restoreTime) //檢查是否為真
            {
                if(Time.timeScale < 1) //如果小於1 時間速度加快到達1
                {
                    Time.timeScale += Time.unscaledDeltaTime * restoreTimeSpeed;
                }
                else //如果超過1 時間為1
                {
                    Time.timeScale = 1;
                    restoreTime = false;
                }
            }
        }
    
        public void HitStopTime(float _newTimeScale, int _restoreSpeed, float _delay)
        {
            restoreTimeSpeed = _restoreSpeed;
            Time.timeScale = _newTimeScale;
    
            if(_delay > 0)
            {
                StopCoroutine(StartTimeAgain(_delay));
                StartCoroutine(StartTimeAgain(_delay));
            }
            else
            {
                restoreTime = true;
            }
        }
    
        IEnumerator StartTimeAgain(float _delay)
        {
            yield return new WaitForSecondsRealtime(_delay);
            restoreTime = true;
        }
    
        IEnumerator Death()
        {
            pState.alive = false;
            Time.timeScale = 1f; //避免受傷暫停時間導致出錯
            //可在此呼叫粒子效果
            anim.SetTrigger("Death");
    
            rb.constraints = RigidbodyConstraints2D.FreezePosition;
            rb.constraints = RigidbodyConstraints2D.FreezeRotation;
            GetComponent<BoxCollider2D>().enabled = false;
    
            yield return new WaitForSeconds(0.9f);
            StartCoroutine(UIManager.Instance.ActiveDeathScreen());
        }
    
        public void Respawned()
        {
            if(!pState.alive)
            {
                rb.constraints = RigidbodyConstraints2D.None;
                rb.constraints = RigidbodyConstraints2D.FreezeRotation;
                GetComponent<BoxCollider2D>().enabled = true;
    
                pState.alive = true;
                Health = maxHealth;
                anim.Play("PlayerIdle");
            }
        }
    
        public float Health //血量
        {
            get { return health; }
            set
            {
                if(health != value)
                {
                    health = Mathf.Clamp(value, 0, maxHealth);
    
                }
            }
        }
    
        void Heal()
        {
            if(Input.GetButton("Cast/Heal") && castOrHealtimer > 0.05f && health < maxHealth && Mana > 0 && Grounded() && !pState.dashing)
            {
                pState.healing = true;
                anim.SetBool("Heal", true);
    
                //補血
                healTimer += Time.deltaTime;
                if(healTimer >= timeToheal)
                { 
                    health++; //恢復血量
                    healTimer = 0; //血量計時器
                    healthImage.fillAmount = health / maxHealth; //血量圖片
                    healthEffect.fillAmount = health / maxHealth; //血量效果圖片
                    healthText.text = health.ToString() + "/" + maxHealth.ToString(); //血量文字
                }
                Mana -= Time.deltaTime * manaDrainSpeed;//魔力消耗
            }
            else
            {
                pState.healing = false;
                anim.SetBool("Heal", false);
                healTimer = 0;
            }
    
            
        }
    
        public float Mana
        {
            get { return mana; }
            set
            {
                if(mana != value)
                {
                    mana = Mathf.Clamp(value, 0, maxMana);
                    manaImage.fillAmount = Mana;
                }
    
            }
        }
    
        void CastSpell()
        {
            if(Input.GetButtonUp("Cast/Heal") && castOrHealtimer <= 0.05f && timeSinceCast >= timeBetweenCast && Mana > manaSpellCost && unlockSideSpell)
            {
                pState.casting = true;
                timeSinceCast = 0;
                StartCoroutine(CastCoroutine());
            }
            else
            {
                timeSinceCast += Time.deltaTime;
            }
            
            if(!Input.GetButton("Cast/Heal"))
            {
                castOrHealtimer = 0;
            }
            
        }
    
        IEnumerator CastCoroutine()
        {
            //橫向技能
            if(yAxis == 0 || (yAxis < 0 && Grounded()) ) //沒有垂直輸入時播放 目前無需要按住上下才能施放的技能
            {
                anim.SetBool("Cast", true);
                yield return new WaitForSeconds(0.1f); //此為等待動畫到達正確播放時間 
    
                GameObject _fireBall = Instantiate(sideSpellFireball, SideAttackTransform.position, Quaternion.identity); //實例化該物件
    
                //Flip火球
                if(pState.lookingRight)
                {
                    _fireBall.transform.eulerAngles = Vector3.zero;
                }
                else
                {
                    _fireBall.transform.eulerAngles = new Vector2(_fireBall.transform.eulerAngles.x, 180); //如果不是面向右邊翻轉180度
                }
                pState.recoilingX = true; //給予玩家反衝
    
                Mana -= manaSpellCost; //魔力減少
                yield return new WaitForSeconds(0.3f); //此為動畫撥放完整時間
            }
    
            anim.SetBool("Cast", false);
            pState.casting = false;
    
        }
    
        public bool Grounded() //地面檢測
        {
            if (Physics2D.Raycast(groundCheckPoint.position, Vector2.down, groundCheckY, whatIsGround) 
                || Physics2D.Raycast(groundCheckPoint.position + new Vector3(groundCheckX , 0, 0), Vector2.down, groundCheckY, whatIsGround)
                || Physics2D.Raycast(groundCheckPoint.position + new Vector3(-groundCheckX , 0, 0), Vector2.down, groundCheckY, whatIsGround))
            {
                return true;
            }
            else
            {
                return false;
            }
        }
    
        void Jump() //跳躍
        {
            if (jumpBufferCounter > 0 && coyoteTimeCounter > 0 &&!pState.jumping)
            {
                rb.velocity = new Vector3(rb.velocity.x, jumpForce);
    
                pState.jumping = true;
            }
    
             if (!Grounded() && airJumpCounter < maxAirJumps && Input.GetButtonDown("Jump") && unlockAirJump)
            {
                pState.jumping = true;
    
                airJumpCounter++;
    
                rb.velocity = new Vector3(rb.velocity.x, jumpForce);
            }
    
            if (Input.GetButtonUp("Jump") && rb.velocity.y > 3)
            {
                rb.velocity = new Vector2(rb.velocity.x, 0);
    
                pState.jumping = false;
            }
    
            anim.SetBool("Jump", !Grounded());
        }
    
        void UpdateJumpVariables()
        {
            if(Grounded())
            {
                pState.jumping = false;
                coyoteTimeCounter = coyoteTime;
                airJumpCounter = 0;  
            }
            else
            {
                coyoteTimeCounter -= Time.deltaTime;
            }
    
            if(Input.GetButtonDown("Jump"))
            {
                jumpBufferCounter = jumpBufferFrames;
            }
            else
            {
                jumpBufferCounter--;
            }
    
        }
    
        private bool Walled()
        {
            return Physics2D.OverlapCircle(wallCheck.position, 0.2f, wallLayer);
        }
    
        void WallSlide()
        {
            if(Walled() && !Grounded() && xAxis !=0)
            {
                isWallSliding = true;
                rb.velocity = new Vector2(rb.velocity.x, Mathf.Clamp(rb.velocity.y, -wallSlidingSpeed, float.MaxValue));
            }
            else
            {
                isWallSliding = false;
            }
        }
    
        void WallJump()
        {
            if(isWallSliding)
            {
                isWallJumping = false;
                wallJumpingDirection = !pState.lookingRight ? 1 : -1;
    
                CancelInvoke(nameof(StopWallJumping));
            }
    
            if(Input.GetButtonDown("Jump") && isWallSliding)
            {
                isWallJumping = true;
                rb.velocity = new Vector2(wallJumpingDirection * wallJumpPower.x, wallJumpPower.y);
    
                dashed = false;
                airJumpCounter = 0;
    
                pState.lookingRight = !pState.lookingRight;
                transform.eulerAngles = new Vector2(transform.eulerAngles.x, 180);
    
                Invoke(nameof(StopWallJumping), wallJumpingDuration);
            }
        }
    
        void StopWallJumping()
        {
            isWallJumping = false;
            transform.eulerAngles = new Vector2(transform.eulerAngles.x, 0);
        }
    
    }
    
    #14951
    Joseph Tang
    Moderator

    1. Jump issue

    I cannot find any issue with the jump mechanic, i also do not see a reason as to why holding the jump button and not releasing it would cause another jump.

    A theory i have is that the player is being Grounded() when they touch the wall, and after they jump, they’re jumpBufferCounter & coyoteTimeCounter does not decrease fast enough to prevent a jump from automatically occuring.
    Let’s try stopping the function of jumping entirely by disabling the setting of jumpBufferCounter during a wall jump.

        void UpdateJumpVariables()
        {
            if(Grounded())
            {
                pState.jumping = false;
                coyoteTimeCounter = coyoteTime;
                airJumpCounter = 0;  
            }
            else
            {
                coyoteTimeCounter -= Time.deltaTime;
            }
    
            if(Input.GetButtonDown("Jump")  && isWallJumping)
            {
                jumpBufferCounter = jumpBufferFrames;
            }
            else
            {
                jumpBufferCounter--;
            }
    
        }

    This should prevent the jumpBufferCounter from being set to your jumpBufferFrames above the if statement’s prerequisite in Jump() to activate a jump, while you are in a walljump.


    2. UnlockedSideSpell issue

    There doesn’t appear to be anything particularly outstanding in the code to cause an issue like this, but i suppose we should try to keep it as similar to the original code as an attempt to correct it. In the case that even in the original style, the code does not function properly, i will take a deepdive into figuring out the issue.

    For now, the original code uses the unlockSideSpell boolean in the CastCoroutine() method instead of the CastSpell().

        void CastSpell()
        {
            if(Input.GetButtonUp("Cast/Heal") && castOrHealtimer <= 0.05f && timeSinceCast >= timeBetweenCast && Mana > manaSpellCost && unlockSideSpell)
            {
                pState.casting = true;
                timeSinceCast = 0;
                StartCoroutine(CastCoroutine());
            }
            else
            {
                timeSinceCast += Time.deltaTime;
            }
            
            if(!Input.GetButton("Cast/Heal"))
            {
                castOrHealtimer = 0;
            }
            
        }
    
        IEnumerator CastCoroutine()
        {
            //橫向技能
            if((yAxis == 0 || (yAxis < 0 && Grounded())) && unlockSideSpell) //沒有垂直輸入時播放 目前無需要按住上下才能施放的技能
            {
                anim.SetBool("Cast", true);
                yield return new WaitForSeconds(0.1f); //此為等待動畫到達正確播放時間 
    
                GameObject _fireBall = Instantiate(sideSpellFireball, SideAttackTransform.position, Quaternion.identity); //實例化該物件
    
                //Flip火球
                if(pState.lookingRight)
                {
                    _fireBall.transform.eulerAngles = Vector3.zero;
                }
                else
                {
                    _fireBall.transform.eulerAngles = new Vector2(_fireBall.transform.eulerAngles.x, 180); //如果不是面向右邊翻轉180度
                }
                pState.recoilingX = true; //給予玩家反衝
    
                Mana -= manaSpellCost; //魔力減少
                yield return new WaitForSeconds(0.3f); //此為動畫撥放完整時間
            }
    
            anim.SetBool("Cast", false);
            pState.casting = false;
    
        }

    3. Spells aren’t casting

    As per another post asking on this, I believe your issue is solved by this note in the Part 5 Article,

    Change the if parameter of both Heal() and CastSpell() to a higher float value if needed as 0.05f is a very limited time for a player to tap and fire a spell.

    You can test this by setting the value in both Heal() and CastSpell()‘s if statement, castOrHealtimer <= 0.05f, to a higher value like 1f. [Ensure both have the same value to avoid healing before the time limit of casting


    4. Deleting save files to return to original state

    Take a look at these 2 posts for my suggestions on deleting save files

    [Part 7] My Maps cannot update when I quit the game

    and setting up a new game option

    [Part 7] How can I let the game have “new game”

    #14953
    MI NI
    Participant

    The problems of Heal&Spell and unlocksideSpell have been solved
    I can’t jump after adding && isWallJumping

    #14954
    Joseph Tang
    Moderator

    Ah sorry, try !isWallJumping instead

    #14955
    MI NI
    Participant

    Still the same, after I added !isWallJumping,
    If you continue to hold down Jump during WallJump, it will jump.

    
        void UpdateJumpVariables()
        {
            if(Grounded())
            {
                pState.jumping = false;
                coyoteTimeCounter = coyoteTime;
                airJumpCounter = 0;  
            }
            else
            {
                coyoteTimeCounter -= Time.deltaTime;
            }
    
            if(Input.GetButtonDown("Jump")  && !isWallJumping )
            {
                jumpBufferCounter = jumpBufferFrames;
            }
            else
            {
                jumpBufferCounter--;
            }
    
        }
    
    #14956
    Joseph Tang
    Moderator

    Alright, let’s try to check and confirm some things.

        void Jump() //跳躍
        {
            if (jumpBufferCounter > 0 && coyoteTimeCounter > 0 &&!pState.jumping)
            {
                print("regular jump");
                rb.velocity = new Vector3(rb.velocity.x, jumpForce);
    
                pState.jumping = true;
            }
    
             if (!Grounded() && airJumpCounter < maxAirJumps && Input.GetButtonDown("Jump") && unlockAirJump)
            {
                print("air jump");
                pState.jumping = true;
    
                airJumpCounter++;
    
                rb.velocity = new Vector3(rb.velocity.x, jumpForce);
            }
    
            if (Input.GetButtonUp("Jump") && rb.velocity.y > 3)
            {
                rb.velocity = new Vector2(rb.velocity.x, 0);
    
                pState.jumping = false;
            }
    
            anim.SetBool("Jump", !Grounded());
        }

    You can remove the is wall jumping check, tell me which of these prints is being returned by the issue.

    #14958
    MI NI
    Participant

    I recorded a video for you to check. In addition, I found a problem, that is, when I tap to jump, he will continue to jump to the highest point, and there is a chance of triggering. I don’t know the reason. In the follow-up of the video, I All jumps are done by just tapping the jump button and then releasing the button. Around 1:00 in the video, I successfully made a small jump.

    If needed here is my updated code

    
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.UIElements;
    using UnityEngine.UI;
    
    public class PlayerController : MonoBehaviour
    {
        [Header("水平移動")]
        [SerializeField] private float walkSpeed = 1; //走路速度
        [Space(5)]
    
        [Header("垂直移動")]
        [SerializeField] private float jumpForce = 45; //跳躍速度
        private int jumpBufferCounter = 0; //按下按鈕儲存指令
        [SerializeField] private int jumpBufferFrames; //按下按鈕儲存指令時間/偵
        private float coyoteTimeCounter = 0; //土狼時間
        [SerializeField] private float coyoteTime; //土狼時間/偵
        private int airJumpCounter = 0;
        [SerializeField] private int maxAirJumps;
        [Space(5)]
    
        [Header("WallJump")]
        [SerializeField] private float wallSlidingSpeed; //滑行速度
        [SerializeField] private Transform wallCheck; //牆壁檢查點
        [SerializeField] private LayerMask wallLayer;
        [SerializeField] private float wallJumpingDuration; //蹬牆跳持續時間
        [SerializeField] private Vector2 wallJumpPower;
        float wallJumpingDirection;
        bool isWallSliding;
        bool isWallJumping;
        [Space(5)]
    
        [Header("地面檢查")]
        [SerializeField] private Transform groundCheckPoint; //地面檢查點
        [SerializeField] private float groundCheckY = 0.2f; //
        [SerializeField] private float groundCheckX = 0.5f; //
        [SerializeField] private LayerMask whatIsGround; //檢查是否在地面
        [Space(5)]
    
        [Header("衝刺")]
        [SerializeField] private float dashSpeed; //衝刺速度
        [SerializeField] private float dashTime;  //衝刺時間
        [SerializeField] private float dashCooldown; //衝刺冷卻
        [Space(5)]
    
        [Header("攻擊")]
    
        [SerializeField] private float timeBetweenAttack; //攻擊時間
        [SerializeField] Transform SideAttackTransform, UpAttackTransform, DownAttackTransform; //不同方向攻擊位置
        [SerializeField] Vector2 SideAttackArea, UpAttackArea, DownAttackArea; //不同方向攻擊範圍
        [SerializeField] LayerMask attackableLayer; //可攻擊的對象圖層
        [SerializeField] float damage; //傷害
        [SerializeField] GameObject slashEffect; //劍氣效果
        private bool attack = false;
        private float timeSinceAttack;
    
        bool restoreTime;
        public float restoreTimeSpeed;
    
        [Space(5)]
    
        [Header("後座力")]
        [SerializeField] int recilXSteps = 5;
        [SerializeField] int recilYSteps = 5;
    
        [SerializeField] float recilXSpeed = 5;
        [SerializeField] float recilYSpeed = 5;
    
        private int stepsXRecoiled, stepsYRecoiled;
        [Space(5)]
    
        [Header("血量")]
        public float health;  //當前血量
        public float maxHealth; //最大血量
        [SerializeField] GameObject hitEffect; //被擊效果
        [SerializeField] float hitFlashSpeed; //受傷角色閃爍
        float healTimer;
        [SerializeField] float timeToheal;
        [Space(5)]
    
        [Header("血量圖片")]
        public Text healthText; //血量文字
        public UnityEngine.UI.Image healthImage; //血量圖片
        public UnityEngine.UI.Image healthEffect; //血量緩衝特效
        public float effectTime = 0.5f; //緩衝持續時間
        private Coroutine updateCoroutine;
        [Space(5)]
    
        [Header("魔力")]
        [SerializeField] float mana; //魔力量
        [SerializeField] float maxMana; //最大魔力量
        [SerializeField] float manaDrainSpeed; //魔力消耗速度
        [SerializeField] float manaGain; //獲得魔力
        [SerializeField] UnityEngine.UI.Image manaImage; //魔力圖片
    
        [Header("技能")]
        [SerializeField] float manaSpellCost = 0.3f; //法術消耗
        [SerializeField] float timeBetweenCast = 0.5f; //攻擊間隔速度
        [SerializeField] GameObject sideSpellFireball; //火球物件Prefab
        float timeSinceCast;
        float castOrHealtimer;
    
        [Header("相機")]
        [SerializeField] private float playerFallSpeedTheshold = -10;
    
        [HideInInspector] public PlayerStateList pState; //取得緩衝輸入腳本
        [HideInInspector] public Rigidbody2D rb;
        private Animator anim;
        private SpriteRenderer sr;
    
        private float gravity; //重力
        private float xAxis, yAxis; //取得XY軸輸入
        private bool canDash = true;
        private bool dashed;
        private bool canFlash = true;
        bool openMap; //開啟地圖
        bool openInventory; //開啟背包
    
        //解鎖能力
        public bool unlockWallJump;
        public bool unlockDash;
        public bool unlockAirJump;
        public bool unlockSideSpell = false;
        
    
        public static PlayerController Instance; //防止多個重複的玩家腳本出現
        private void Awake()
        {
            if(Instance != null && Instance != this)
            {
                Destroy(gameObject);
            }
            else
            {
                Instance = this;
            }
    
            DontDestroyOnLoad(gameObject);
    
            
        }
    
        void Start()
        {
            pState = GetComponent<PlayerStateList>();
    
            rb = GetComponent<Rigidbody2D>();
    
            sr = GetComponent<SpriteRenderer>();
    
            anim = GetComponent<Animator>();
    
            gravity = rb.gravityScale;
    
            if(Health == 0)
            {
                pState.alive = false;
                GameManager.Instance.RespawnPlayer();
            }
    
            //Health = maxHealth; Save之後不再需要
            //Mana = maxMana;
    
            healthImage.fillAmount = health / maxHealth;
            healthEffect.fillAmount = health / maxHealth;
            healthText.text = health.ToString() + "/" + maxHealth.ToString();
            manaImage.fillAmount = Mana;
    
            SaveData.Instance.LoadPlayerData();
        }
    
        private void OnDrawGizmos()
        {
            Gizmos.color = Color.red;
            Gizmos.DrawWireCube(SideAttackTransform.position, SideAttackArea);
            Gizmos.DrawWireCube(UpAttackTransform.position, UpAttackArea);
            Gizmos.DrawWireCube(DownAttackTransform.position, DownAttackArea);
            //Gizmos.DrawWireCube(wallCheck.position, )
        }
    
        void Update()
        {
            if (GameManager.Instance.gameIsPaused) return;
    
            if (pState.cutscene) return;
            if (pState.alive)
            {
                GetInputs();
                ToggleMap();
                ToggleInventory();
            }
            UpdateJumpVariables();
            RestoreTimeScale();
            UpdateCameraYDampForPlayerFall();
    
            if(pState.alive)
            {
                Heal();
            }
    
            if (pState.dashing || pState.healing) return;
    
            if(pState.alive)
            {
                if(!isWallJumping)
                {
                    Flip();
                    Move();
                    Jump();
                }
                if(unlockWallJump)
                {
                    WallSlide();
                    WallJump();
                }
                if(unlockDash)
                {
                    StartDash();
                }
                
    
                
                Attack();
                CastSpell();
            }
            FlashWhileInvincible();
    
            /*
            if (pState.dashing) return;
            FlashWhileInvincible();
            if (!pState.alive) return;
            Move();
            Heal();
            CastSpell();
            if (pState.healing) return;
            Flip();
            Jump();
            StartDash();
            Attack();
            */
    
        }
    
        private void FixedUpdate()
        {
            if (pState.cutscene) return;
    
            if (pState.dashing || pState.healing) return;
            Recoil();
        }
    
        void GetInputs() //獲取輸入
        {
            xAxis = Input.GetAxisRaw("Horizontal");
            yAxis = Input.GetAxisRaw("Vertical");
            attack = Input.GetButtonDown("Attack");
            openMap = Input.GetButton("Map");
            openInventory = Input.GetButton("Inventory");
    
            if (Input.GetButton("Cast/Heal"))
            {
                castOrHealtimer += Time.deltaTime;
            }
            /*
            else
            {
                castOrHealtimer = 0;
            }
            */
        }
    
        void ToggleMap()
        {
            if(openMap)
            {
                UIManager.Instance.mapHandler.SetActive(true);
            }
            else
            {
                UIManager.Instance.mapHandler.SetActive(false);
            }
        }
    
        void ToggleInventory()
        {
            if (openInventory)
            {
                UIManager.Instance.inventory.SetActive(true);
            }
            else
            {
                UIManager.Instance.inventory.SetActive(false);
            }
        }
    
        void Flip() //翻轉
        {
            if(xAxis < 0)
            {
                transform.localScale = new Vector2(-1, transform.localScale.y);
                pState.lookingRight = false;
            }
            else if(xAxis >0)
            {
                transform.localScale = new Vector2(1, transform.localScale.y);
                pState.lookingRight = true;
            }
        }
    
        private void Move() //移動
        {
            if (pState.healing) rb.velocity = new Vector2(0, 0); //如果玩家正在治療則無法移動
            rb.velocity = new Vector2(walkSpeed * xAxis ,rb.velocity.y);
            anim.SetBool("Walk", rb.velocity.x !=0 && Grounded());
        }
    
        void UpdateCameraYDampForPlayerFall()
        {
            //if falling past a certain speed threshold
            if (rb.velocity.y < playerFallSpeedTheshold && !CameraManager.Instance.isLerpingYDamp && !CameraManager.Instance.hasLerpingYDamping)
            {
                StartCoroutine(CameraManager.Instance.LerpYDamping(true));
            }
            //if standing stil or moveing up
            if(rb.velocity.y >= 0 && !CameraManager.Instance.isLerpingYDamp && CameraManager.Instance.hasLerpingYDamping)
            {
                //reset camera funtion
                CameraManager.Instance.hasLerpingYDamping = false;
                StartCoroutine(CameraManager.Instance.LerpYDamping(false));
            }
        }
    
        void StartDash() //衝刺
        {
            if(Input.GetButtonDown("Dash") && canDash && !dashed)
            {
                StartCoroutine(Dash());
                dashed = true;
            }
    
            if(Grounded())
            {
                dashed = false;
            }
        }
    
        IEnumerator Dash()
        {
            canDash = false; 
            pState.dashing = true;
            anim.SetTrigger("Dash");
            rb.gravityScale = 0;
            int _dir = pState.lookingRight ? 1 : -1;
            rb.velocity = new Vector2(_dir * dashSpeed, 0);
            yield return new WaitForSeconds(dashTime);
            rb.gravityScale = gravity;
            pState.dashing = false;
            yield return new WaitForSeconds(dashCooldown);
            canDash = true;
    
        }
    
        public IEnumerator WalkIntoNewScene(Vector2 _exitDir, float _delay) //過場動畫
        {
            pState.invincible = true;
    
            if(_exitDir.y > 0) //如果退出位置為向上
            {
                rb.velocity = jumpForce * _exitDir;
            }
    
            //如果退出位置為水平移動
            if(_exitDir.x != 0)
            {
                xAxis = _exitDir.x > 0 ? 1 : -1;
    
                Move();
            }
    
            Flip();
            yield return new WaitForSeconds(_delay);
            pState.invincible = false;
            pState.cutscene = false;
    
        }
    
        void Attack() //攻擊
        {
            timeSinceAttack += Time.deltaTime;
            if(attack && timeSinceAttack >= timeBetweenAttack)
            {
                timeSinceAttack = 0;
                anim.SetTrigger("Attack");
    
                if(yAxis == 0 || yAxis < 0 && Grounded()) //如果玩家在地面上且沒有按住"上",正常攻擊
                {
                    int _recoilLeftOrRight = pState.lookingRight ? 1 : -1; 
    
                    Hit(SideAttackTransform, SideAttackArea, ref pState.recoilingX, Vector2.right * _recoilLeftOrRight, recilXSpeed);
                    Instantiate(slashEffect, SideAttackTransform);
                }
                else if(yAxis > 0) //如果按住"上",上攻擊
                {
                    Hit(UpAttackTransform, UpAttackArea, ref pState.recoilingY, Vector2.up, recilYSpeed);
                    SlashEffectAtAngle(slashEffect, 90, UpAttackTransform);
                }
                else if (yAxis < 0 && !Grounded()) //如果不在地面且按住"下",下攻擊
                {
                    Hit(DownAttackTransform, DownAttackArea, ref pState.recoilingY, Vector2.down, recilYSpeed);
                    SlashEffectAtAngle(slashEffect, -90, DownAttackTransform);
                }
    
            }
        }
    
        private void Hit(Transform _attackTransfrom, Vector2 _attackArea, ref bool _recoilBool,Vector2 _recoilDir , float _recoilStrength) //給予傷害
        {
            Collider2D[] objectsToHit = Physics2D.OverlapBoxAll(_attackTransfrom.position, _attackArea, 0, attackableLayer);
            List<Enemy> hitEnemise = new List<Enemy>();
    
            if(objectsToHit.Length > 0)
            {
                //Debug.Log("Hit");
                _recoilBool = true;
    
            }
            for(int i = 0; i < objectsToHit.Length; i++)
            {
                if(objectsToHit[i].GetComponent<Enemy>() != null)
                {
                    objectsToHit[i].GetComponent<Enemy>().EnemyHit(damage, _recoilDir , _recoilStrength)  ;
                    /*
                    Enemy e = objectsToHit[i].GetComponent<Enemy>();
                    if(e && !hitEnemise.Contains(e))
                    {
                        e.EnemyHit(damage, (transform.position - objectsToHit[i].transform.position).normalized, _recoilStrength);
                        hitEnemise.Add(e);
                    }
                    */
                    if (objectsToHit[i].CompareTag("Monster")) //打到怪物增加魔力
                    {
                        Mana += manaGain;
                    }
                }
            }
        }
        
        void SlashEffectAtAngle(GameObject _slashEffect, int _effectAngle, Transform _attackTransform)
        {
            _slashEffect = Instantiate(_slashEffect, _attackTransform);
            _slashEffect.transform.eulerAngles = new Vector3(0, 0, _effectAngle);
            _slashEffect.transform.localScale = new Vector2(transform.localScale.x, transform.localScale.y);
        }
    
        void Recoil() //反衝
        {
            if(pState.recoilingX)
            {
                if (pState.lookingRight)
                {
                    rb.velocity = new Vector2(-recilXSpeed, 0);
                }
                else
                {
                    rb.velocity = new Vector2(recilXSpeed, 0);
                }
            }
    
            if(pState.recoilingY)
            {
                rb.gravityScale = 0;
                if (yAxis < 0)
                {
                    rb.velocity = new Vector2(rb.velocity.x, recilYSpeed);
                }
                else
                {
                    rb.velocity = new Vector2(rb.velocity.x, -recilYSpeed);
                }
    
                airJumpCounter = 0;
    
            }
            else
            {
                rb.gravityScale = gravity;
            }
            
            //Stop recoil 停止反衝
            if(pState.recoilingX && stepsXRecoiled < recilXSteps)
            {
                stepsXRecoiled++;
            }
            else
            {
                StopRecoilX();
            }
    
            if (pState.recoilingY && stepsYRecoiled < recilYSteps)
            {
                stepsYRecoiled++;
            }
            else
            {
                StopRecoilY();
            }
    
            if(Grounded())
            {
                StopRecoilY();
            }
    
        }
    
        void StopRecoilX()
        {
            stepsXRecoiled = 0;
            pState.recoilingX = false;
        }
        void StopRecoilY()
        {
            stepsYRecoiled = 0;
            pState.recoilingY = false;
        }
    
        public void TakeDamage(float _damage) //玩家受傷
        {
            if(pState.alive)
            {
                Health -= Mathf.RoundToInt(_damage);
                if(Health <= 0)
                {
                    Health = 0;
                    StartCoroutine(Death());
                }
                else
                {
                    StartCoroutine(StopTakingDamage());
    
                    healthImage.fillAmount = health / maxHealth; //血量圖片
                    healthText.text = health.ToString() + "/" + maxHealth.ToString(); //血量文字
    
                    if (updateCoroutine != null)
                    {
                        StopCoroutine(updateCoroutine);
                    }
                    updateCoroutine = StartCoroutine(UpdateHealthEffect());
                } 
            }
        }
    
        private IEnumerator UpdateHealthEffect() //血量緩降協程
        {
            float effectLength = healthEffect.fillAmount - healthImage.fillAmount;
            float elapsedTime = 0f;
    
            while (elapsedTime < effectTime && effectLength != 0)
            {
                elapsedTime += Time.deltaTime;
                healthEffect.fillAmount = Mathf.Lerp
                (healthImage.fillAmount + effectLength, healthImage.fillAmount, elapsedTime / effectTime);
                yield return null;
            }
            healthEffect.fillAmount = healthImage.fillAmount;
        }
    
        IEnumerator StopTakingDamage() //無敵偵
        {
            pState.invincible = true;
            GameObject _hitEffect = Instantiate(hitEffect, transform.position, Quaternion.identity);
            Destroy(_hitEffect, 1.5f);
            anim.SetTrigger("Hurt");
            yield return new WaitForSeconds(1f);
            pState.invincible = false;
        }
    
        IEnumerator Flash()
        {
            sr.enabled = !sr.enabled;
            canFlash = false;
            yield return new WaitForSeconds(0.2f);
            canFlash = true;
        }    
    
        void FlashWhileInvincible()
        {
            //將渲染改為"PingPongLerp" 如果玩家是無敵狀態則將其從白色改為黑色 否則將玩家設定為白色
            //sr.material.color = pState.invincible ? Color.Lerp(Color.white, Color.black, Mathf.PingPong(Time.time * hitFlashSpeed, 1.0f)) : Color.white;
            if(pState.invincible && !pState.cutscene)
            {
                if (Time.timeScale > 0.2f && canFlash) 
                {
                    StartCoroutine(Flash()); 
                }
            }
            else
            {
                sr.enabled = true;
            }
        }
    
        void RestoreTimeScale() //受傷暫停時間
        {
            if(restoreTime) //檢查是否為真
            {
                if(Time.timeScale < 1) //如果小於1 時間速度加快到達1
                {
                    Time.timeScale += Time.unscaledDeltaTime * restoreTimeSpeed;
                }
                else //如果超過1 時間為1
                {
                    Time.timeScale = 1;
                    restoreTime = false;
                }
            }
        }
    
        public void HitStopTime(float _newTimeScale, int _restoreSpeed, float _delay)
        {
            restoreTimeSpeed = _restoreSpeed;
            Time.timeScale = _newTimeScale;
    
            if(_delay > 0)
            {
                StopCoroutine(StartTimeAgain(_delay));
                StartCoroutine(StartTimeAgain(_delay));
            }
            else
            {
                restoreTime = true;
            }
        }
    
        IEnumerator StartTimeAgain(float _delay)
        {
            yield return new WaitForSecondsRealtime(_delay);
            restoreTime = true;
        }
    
        IEnumerator Death()
        {
            pState.alive = false;
            Time.timeScale = 1f; //避免受傷暫停時間導致出錯
            //可在此呼叫粒子效果
            anim.SetTrigger("Death");
    
            rb.constraints = RigidbodyConstraints2D.FreezePosition;
            rb.constraints = RigidbodyConstraints2D.FreezeRotation;
            GetComponent<BoxCollider2D>().enabled = false;
    
            yield return new WaitForSeconds(0.9f);
            StartCoroutine(UIManager.Instance.ActiveDeathScreen());
        }
    
        public void Respawned()
        {
            if(!pState.alive)
            {
                rb.constraints = RigidbodyConstraints2D.None;
                rb.constraints = RigidbodyConstraints2D.FreezeRotation;
                GetComponent<BoxCollider2D>().enabled = true;
    
                pState.alive = true;
                Health = maxHealth;
                anim.Play("PlayerIdle");
            }
        }
    
        public float Health //血量
        {
            get { return health; }
            set
            {
                if(health != value)
                {
                    health = Mathf.Clamp(value, 0, maxHealth);
    
                }
            }
        }
    
        void Heal()
        {
            if(Input.GetButton("Cast/Heal") && castOrHealtimer > 0.5f && health < maxHealth && Mana > 0 && Grounded() && !pState.dashing)
            {
                pState.healing = true;
                anim.SetBool("Heal", true);
    
                //補血
                healTimer += Time.deltaTime;
                if(healTimer >= timeToheal)
                { 
                    health++; //恢復血量
                    healTimer = 0; //血量計時器
                    healthImage.fillAmount = health / maxHealth; //血量圖片
                    healthEffect.fillAmount = health / maxHealth; //血量效果圖片
                    healthText.text = health.ToString() + "/" + maxHealth.ToString(); //血量文字
                }
                Mana -= Time.deltaTime * manaDrainSpeed;//魔力消耗
            }
            else
            {
                pState.healing = false;
                anim.SetBool("Heal", false);
                healTimer = 0;
            }
    
            
        }
    
        public float Mana
        {
            get { return mana; }
            set
            {
                if(mana != value)
                {
                    mana = Mathf.Clamp(value, 0, maxMana);
                    manaImage.fillAmount = Mana;
                }
    
            }
        }
    
        void CastSpell()
        {
            if(Input.GetButtonUp("Cast/Heal") && castOrHealtimer <= 0.5f && timeSinceCast >= timeBetweenCast && Mana > manaSpellCost)
            {
                pState.casting = true;
                timeSinceCast = 0;
                StartCoroutine(CastCoroutine());
            }
            else
            {
                timeSinceCast += Time.deltaTime;
            }
            
            if(!Input.GetButton("Cast/Heal"))
            {
                castOrHealtimer = 0;
            }
            
        }
    
        IEnumerator CastCoroutine()
        {
            //橫向技能
            if((yAxis == 0 || (yAxis < 0 && Grounded())) && unlockSideSpell ) //沒有垂直輸入時播放 目前無需要按住上下才能施放的技能
            {
                anim.SetBool("Cast", true);
                yield return new WaitForSeconds(0.1f); //此為等待動畫到達正確播放時間 
    
                GameObject _fireBall = Instantiate(sideSpellFireball, SideAttackTransform.position, Quaternion.identity); //實例化該物件
    
                //Flip火球
                if(pState.lookingRight)
                {
                    _fireBall.transform.eulerAngles = Vector3.zero;
                }
                else
                {
                    _fireBall.transform.eulerAngles = new Vector2(_fireBall.transform.eulerAngles.x, 180); //如果不是面向右邊翻轉180度
                }
                pState.recoilingX = true; //給予玩家反衝
    
                Mana -= manaSpellCost; //魔力減少
                yield return new WaitForSeconds(0.3f); //此為動畫撥放完整時間
            }
    
            anim.SetBool("Cast", false);
            pState.casting = false;
    
        }
    
        public bool Grounded() //地面檢測
        {
            if (Physics2D.Raycast(groundCheckPoint.position, Vector2.down, groundCheckY, whatIsGround) 
                || Physics2D.Raycast(groundCheckPoint.position + new Vector3(groundCheckX , 0, 0), Vector2.down, groundCheckY, whatIsGround)
                || Physics2D.Raycast(groundCheckPoint.position + new Vector3(-groundCheckX , 0, 0), Vector2.down, groundCheckY, whatIsGround))
            {
                return true;
            }
            else
            {
                return false;
            }
        }
        /*
        void Jump() //跳躍
        {
            if (jumpBufferCounter > 0 && coyoteTimeCounter > 0 &&!pState.jumping)
            {
                rb.velocity = new Vector3(rb.velocity.x, jumpForce);
    
                pState.jumping = true;
            }
    
             if (!Grounded() && airJumpCounter < maxAirJumps && Input.GetButtonDown("Jump") && unlockAirJump)
            {
                pState.jumping = true;
    
                airJumpCounter++;
    
                rb.velocity = new Vector3(rb.velocity.x, jumpForce);
            }
    
            if (Input.GetButtonUp("Jump") && rb.velocity.y > 3)
            {
                rb.velocity = new Vector2(rb.velocity.x, 0);
    
                pState.jumping = false;
            }
    
            anim.SetBool("Jump", !Grounded());
        }
        */
        
        void Jump() //跳躍
        {
            if (jumpBufferCounter > 0 && coyoteTimeCounter > 0 && !pState.jumping)
            {
                print("regular jump");
                rb.velocity = new Vector3(rb.velocity.x, jumpForce);
    
                pState.jumping = true;
            }
    
            if (!Grounded() && airJumpCounter < maxAirJumps && Input.GetButtonDown("Jump") && unlockAirJump)
            {
                print("air jump");
                pState.jumping = true;
    
                airJumpCounter++;
    
                rb.velocity = new Vector3(rb.velocity.x, jumpForce);
            }
    
            if (Input.GetButtonUp("Jump") && rb.velocity.y > 3)
            {
                rb.velocity = new Vector2(rb.velocity.x, 0);
    
                pState.jumping = false;
            }
    
            anim.SetBool("Jump", !Grounded());
        }
        
    
        void UpdateJumpVariables()
        {
            if(Grounded())
            {
                pState.jumping = false;
                coyoteTimeCounter = coyoteTime;
                airJumpCounter = 0;  
            }
            else
            {
                coyoteTimeCounter -= Time.deltaTime;
            }
    
            if(Input.GetButtonDown("Jump") )
            {
                jumpBufferCounter = jumpBufferFrames;
            }
            else
            {
                jumpBufferCounter--;
            }
    
        }
    
        private bool Walled()
        {
            return Physics2D.OverlapCircle(wallCheck.position, 0.2f, wallLayer);
        }
    
        void WallSlide()
        {
            if(Walled() && !Grounded() && xAxis !=0)
            {
                isWallSliding = true;
                rb.velocity = new Vector2(rb.velocity.x, Mathf.Clamp(rb.velocity.y, -wallSlidingSpeed, float.MaxValue));
            }
            else
            {
                isWallSliding = false;
            }
        }
    
        void WallJump()
        {
            if(isWallSliding)
            {
                isWallJumping = false;
                wallJumpingDirection = !pState.lookingRight ? 1 : -1;
    
                CancelInvoke(nameof(StopWallJumping));
            }
    
            if(Input.GetButtonDown("Jump") && isWallSliding)
            {
                isWallJumping = true;
                rb.velocity = new Vector2(wallJumpingDirection * wallJumpPower.x, wallJumpPower.y);
    
                dashed = false;
                airJumpCounter = 0;
    
                pState.lookingRight = !pState.lookingRight;
                transform.eulerAngles = new Vector2(transform.eulerAngles.x, 180);
    
                Invoke(nameof(StopWallJumping), wallJumpingDuration);
            }
        }
    
        void StopWallJumping()
        {
            isWallJumping = false;
            transform.eulerAngles = new Vector2(transform.eulerAngles.x, 0);
        }
    
    }
    
    #14980
    Joseph Tang
    Moderator

    Looking at the console, and watching the video slowly, we can see that both your air jump and your wall jump is occuring simultaneously.

    This may be the reason why you fly upwards as the air jump velocity change is overtaking the velocity of your wall jump based on the fixed update rate of your unity editor. or at least that’s a theory.

    But, this should be fixed by either two methods.

    1. You can change the if statement of the air jump to stop when wall sliding/jumping

        void Jump() //跳躍
        {
            if (jumpBufferCounter > 0 && coyoteTimeCounter > 0 &&!pState.jumping)
            {
                print("regular jump");
                rb.velocity = new Vector3(rb.velocity.x, jumpForce);
    
                pState.jumping = true;
            }
    
             if (!Grounded() && airJumpCounter < maxAirJumps && Input.GetButtonDown("Jump") && unlockAirJump !isWallSliding)
            {
                print("air jump");
                pState.jumping = true;
    
                airJumpCounter++;
    
                rb.velocity = new Vector3(rb.velocity.x, jumpForce);
            }
    
            if (Input.GetButtonUp("Jump") && rb.velocity.y > 3)
            {
                rb.velocity = new Vector2(rb.velocity.x, 0);
    
                pState.jumping = false;
            }
    
            anim.SetBool("Jump", !Grounded());
        }

    2. Preventing Jump() from updating while wall sliding.

        void Update()
        {
            ...
    
            if (pState.dashing || pState.healing) return;
    
            if(pState.alive)
            {
                if(!isWallJumping)
                {
                    Flip();
                    Move();
    
                    if (!isWallSliding)
                    {
                        Jump();
                    }
                }
                if(unlockWallJump)
                {
                    WallSlide();
                    WallJump();
                }
                ...
        }
    #15014
    MI NI
    Participant

    I tested the above two methods, but it didn’t work. Although I only recorded one of them in the video, I’m not sure if I am the only one experiencing this problem. I posted a picture of my Unity settings. , if many people have encountered this problem. I have an idea that I can write a coroutine and give “for example 0.5f” time during WallJump to be unable to control the character. This may solve the problem.

    Also I downloaded the keyboard monitor and you can see that when I lightly tap the jump key, there is a chance that the jump is uncontrolled and it jumps to the highest point instead of falling immediately after I release the key. I don’t quite Can you determine the reason for this?

    View post on imgur.com

    #15022
    Joseph Tang
    Moderator

    Okay, so after finally being able to test it on my end, the reason for the issue isn’t the air jump nor normal jump. it’s simply because the wall jump is too powerful. The vertical boost is just the leftover velocity from the walljump.

    to stop it, just add a code to change the y velocity of your player to StopWallJumping()

        void StopWallJumping()
        {
            isWallJumping = false;
            rb.velocity = new Vector2(rb.velocity.x, 0);
            transform.eulerAngles = new Vector2(transform.eulerAngles.x, 0);
        }

    As for you being to accidentally jump so high. That’s most likely due to the fixed timestep of unity causing issues on receiving your Input.GetButtonUp("Jump"). You can improve the code by adding this:

        void Jump()
        }
            ...
    
            if (Input.GetButtonUp("Jump") || (Input.GetButton("Jump") && rb.velocity.y > 3)
            {
                rb.velocity = new Vector2(rb.velocity.x, 0);
                pState.jumping = false;
            }
    
            ...
        }

    I would NOT recommend this as this may cause some issues like being knocked up by other elements [Jump pads if / recoiling upwards for nail jumps], since this code will ensure that so long as you are not touching the jump button the player will fall back down. And I would most likely suggest a different method of setting up a boolean check and timer just like how we do the jumpbufferframes & jumpbuffercounter instead.

    You can find more about Unity’s frame rate/timestep here:

    Unity Rigidbody’s Interpolate property

    and here


    I will look into adding the walljump fix into the article sometime soon.

Viewing 13 posts - 1 through 13 (of 13 total)
  • You must be logged in to reply to this topic.

Go to Login Page →


Advertisement below: