Forum begins after the advertisement:


]Part 7] Save & LoadPlayerData

Home Forums Video Game Tutorial Series Creating a Metroidvania in Unity ]Part 7] Save & LoadPlayerData

Viewing 5 posts - 1 through 5 (of 5 total)
  • Author
    Posts
  • #14783
    MI NI
    Level 14
    Bronze Supporter (Patron)
    Helpful?
    Up
    0
    ::

    This article was translated using Google. There may be some errors in expression. Please forgive me.

    I have some questions about the title My SavePlayerData and LoadPlayerData At the beginning, the current position, health and mana are actually stored. But after restarting, only the position, health and mana remain unchanged.

    Another problem is that after my player is injured, as long as he clicks to heal, he will be stuck in that state. I guess it happened when I was studying part 7. I hope you can help me solve it. Thanks

    If you need more documentation or ideas please let me know

    In addition, my health was made separately, which is slightly different from the video. I used long strips of health, like in Skul: The Hero Slayer. But my mana is the same as in the video

    SaveData

    <code>using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using System.IO;
    using UnityEngine.SceneManagement;
    
    [System.Serializable]
    
    public struct SaveData
    {
        public static SaveData Instance;
    
        //map stuff
        public HashSet<string> sceneName;
    
        //savePoint stuff
        public string savePointSceneName;
        public Vector2 savePointPos;
    
        //player stuff
        public float playerHealth;
        public float playerMana;
        public Vector2 playerPosition;
        public string lastScene;
    
        public void Initialize() //初始化
        {
            //檢查重生點文件是否存在,否則將建立一個新文件
            if (!File.Exists(Application.persistentDataPath + "/save.savePoint.data"))
            {
                BinaryWriter writer = new BinaryWriter(File.Create(Application.persistentDataPath + "/save.savePoint.data"));
            }
            //檢查玩家文件是否存在,否則將建立一個新文件
            if (!File.Exists(Application.persistentDataPath + "/save.player.data"))
            {
                BinaryWriter writer = new BinaryWriter(File.Create(Application.persistentDataPath + "/save.player.data"));
            }
    
            if (sceneName == null) //檢查此場景是否為空 
            {
                //如果是,建立一個新HashSet
                sceneName = new HashSet<string>();
            }
        }
    
        public void SavedSavePoint()
        {
            using (BinaryWriter writer = new BinaryWriter(File.OpenWrite(Application.persistentDataPath + "/save.savePoint.data")))
            {
                writer.Write(savePointSceneName);
                writer.Write(savePointPos.x);
                writer.Write(savePointPos.y);
            }
        }
    
        public void LoadSavePoint()
        {
            //if(File.Exists(Application.persistentDataPath + "/save.savePoint.data"))
            string savePath = Application.persistentDataPath + "/save.savePoint.data";
            if(File.Exists(savePath) && new FileInfo(savePath).Length > 0)
            {
                using (BinaryReader reader = new BinaryReader(File.OpenRead(Application.persistentDataPath + "/save.savePoint.data")))
                {
                    savePointSceneName = reader.ReadString();
                    savePointPos.x = reader.ReadSingle();
                    savePointPos.y = reader.ReadSingle();
                }
            }
            else
            {
                Debug.Log("SavePoint do not exits");
            }
        }
    
        public void SavePlayerData()
        {
            using (BinaryWriter writer = new BinaryWriter(File.OpenWrite(Application.persistentDataPath + "/save.player.data")))
            {
                playerHealth = PlayerController.Instance.Health;
                writer.Write(playerHealth);
                playerMana = PlayerController.Instance.Mana;
                writer.Write(playerMana);
    
                playerPosition = PlayerController.Instance.transform.position;
                writer.Write(playerPosition.x);
                writer.Write(playerPosition.y);
    
                lastScene = SceneManager.GetActiveScene().name;
                writer.Write(lastScene);
    
                Debug.Log(playerHealth + "血量");
                Debug.Log(playerMana +"魔力");
                Debug.Log(playerPosition.x +"x");
                Debug.Log(playerPosition.y + "y");
                Debug.Log(lastScene + "scene");
            }
        }
    
        public void LoadPlayerData()
        {
            string savePath = Application.persistentDataPath + "/save.player.data";
            if (File.Exists(savePath) && new FileInfo(savePath).Length > 0) 
            {
                using (BinaryReader reader = new BinaryReader(File.OpenRead(Application.persistentDataPath + "/save.player.data")))
                {
                    playerHealth = reader.ReadSingle();
                    playerMana = reader.ReadSingle();
                    playerPosition.x = reader.ReadSingle();
                    playerPosition.y = reader.ReadSingle();
                    lastScene = reader.ReadString();
    
                    SceneManager.LoadScene(lastScene);
                    PlayerController.Instance.transform.position = playerPosition;
                    PlayerController.Instance.Health = playerHealth;
                    PlayerController.Instance.Mana = playerMana;
    
                    Debug.Log(playerHealth + "血量");
                    Debug.Log(playerMana + "魔力");
                    Debug.Log(playerPosition.x + "x");
                    Debug.Log(playerPosition.y + "y");
                    Debug.Log(lastScene + "scene");
                }
                Debug.Log("load player data");
            }
            else
            {
                Debug.Log("File doesnt exist");
                PlayerController.Instance.Health = PlayerController.Instance.maxHealth;
                PlayerController.Instance.Mana = 0.5f;
            }
        }
    
    }</code>

    PlayerController

    <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("地面檢查")]
        [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;
    
        [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; //開啟地圖
    
    
        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;
    
            Mana = maxMana;
    
            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);
        }
    
        void Update()
        {
            if (pState.cutscene) return;
            if(pState.alive)
            {
                GetInputs();
                ToggleMap();
            }
    
            UpdateJumpVariables();
            RestoreTimeScale();
    
            if (pState.dashing || pState.healing) return;
            if(pState.alive)
            {
                Flip();
                Move();
                Jump();
                StartDash();
                Attack();
                Heal();
                CastSpell();
    
            }
    
            FlashWhileInvincible();
    
        }
    
        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");
    
            if (Input.GetButton("Cast/Heal"))
            {
                castOrHealtimer += Time.deltaTime;
            }
            else //if(Input.GetButtonDown("Cast/Heal"))
            {
                castOrHealtimer = 0;
            }
        }
    
        void ToggleMap()
        {
            if(openMap)
            {
                UIManager.Instance.mapHandler.SetActive(true);
            }
            else
            {
                UIManager.Instance.mapHandler.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 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;
            //GetComponent().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().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)
            {
                pState.casting = true;
                timeSinceCast = 0;
                StartCoroutine(CastCoroutine());
            }
            else
            {
                timeSinceCast += Time.deltaTime;
            }
        }
    
        IEnumerator CastCoroutine()
        {
            anim.SetBool("Cast", true);
            yield return new WaitForSeconds(0.1f); //此為等待動畫到達正確播放時間 
    
            //橫向技能
            if(yAxis == 0 || (yAxis < 0 && Grounded())) //沒有垂直輸入時播放 目前無需要按住上下才能施放的技能
            {
                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"))
            {
                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--;
            }
    
        }
    
    }
    </code>
    #14788
    Joseph Tang
    Level 13
    Moderator
    Helpful?
    Up
    0
    ::

    Hi there, sorry for the late response.

    For your first issue regarding the SaveData.cs, I’d like to know if the LoadPlayerData() method is returning Debug.Log("load player data"); or Debug.Log("File doesnt exist");. Because if it’s returning the latter, likely your LoadPlayerData() might not be working correctly. I believe it may be due to the code you put at the start of the method to find the savePath. I theorise that due to the parameters, although the savePath exists, it does not meat the FileInfo().Length you are using. I suggest you try simply using this:

    SaveData.cs

        public void LoadPlayerData()
        {
            string savePath = Application.persistentDataPath + "/save.player.data";
            if (File.Exists(savePath) && new FileInfo(savePath).Length > 0) 
            if(File.Exists(Application.persistentDataPath + "/save.player.data"))
            {
                using (BinaryReader reader = new BinaryReader(File.OpenRead(Application.persistentDataPath + "/save.player.data")))
                {

    That is the code that we are using for the series. Unless you have some other reason for creating the savePath, i believe tweaking the if statement should help it. If this does not solve your issue, could you take a video of you saving and restarting in the Unity Editor? Otherwise, if this works, then I suggest you should also change your LoadSavePoint() in a similar fashion.


    As for your second issue, on the player healing. I am certain it is due to your Heal() method being in the wrong position in the Update() of your PlayerController.cs. It is currently under the if statement to return if the pState.healing is true. Thus, if you were to start healing, the player state will remain healing and cannot be set false as the method to do so also cannot be activated unless the state is false, creating a logic issue. Thus, you can simply reorder the Update() like this:

    PlayerController.cs

        void Update()
        {
            if (pState.cutscene) return;
    
            if(pState.alive)
            {
                GetInputs();
                ToggleMap();
            }
            UpdateJumpVariables();
            RestoreTimeScale();
            UpdateCameraYDampForPlayerFall();
    
            if (pState.dashing) return;
            FlashWhileInvincible();
            if (pState.alive) return;
            Move();
            Heal();
            CastSpell();
            if (pState.healing) return;
            Flip();
            Jump();
            StartDash();
            Attack();
        }

    I’ve given a code that has quite a few methods moved around if you do not mind. I’ve worked on this new Update() method to prevent quite a few bugs from occurring, including the one you have now.


    To inform you, I’ve been working on updating the Metroidvania series to fix some of it’s issues and the changelogs for what I’ve done can be found in our forum page here at the top of the posts. You may take a look through them to find other bugs you may have and what can be done to fix them. Although, not all the information is in there.

    #14792
    MI NI
    Level 14
    Bronze Supporter (Patron)
    Helpful?
    Up
    0
    ::

    Hello, I fixed the part about healing, thank you very much, but I think the part about ‘if (pState.alive) return;’ I changed to if (!pState.alive) return; or if (pState. alive){};

    Regarding my Save&LoadPlayerData, because EndOfStreamException: occurred when I executed it for the first time, I found an article in the blog and changed it to the current one. After your reminder, I changed it to if(File.Exists(Application. persistentDataPath + "/save.player.data")) Although it can also work, it also has no effect. I uploaded a video for reference. In the video, I tested Save&LoadPoint (this is my bench) and Save&LoadPlayer. I have printed it out and Save&LoadPoint works. But in LoadPlayerData, the printed content is indeed the last saved content, but it has not changed in the inspector and the screen. This is what I think is strange. I don’t know if it’s because my life bar is different from the teaching one.

    Also, I want to ask another question. In the seventh part of the blog, I tried the code change of “1. Player respawns at 0 health after leaving game from death. + Moving Player corpse.”, but when I hit the circle The player will rotate when forming a circle collioder2D monster. I don’t know if it’s because I didn’t use GetComponent().enabled = false (or true); but when I enter GetComponent().enabled, an error always occurs. I’m not sure which one should Fill in something. I had it recorded.

    #14800
    Joseph Tang
    Level 13
    Moderator
    Helpful?
    Up
    0
    ::

    For your first issue on the respawning, comment out the codes that set your health and mana in PlayerController.cs:

        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;
    
            //Mana = maxMana;
    
            manaImage.fillAmount = Mana;
        }

    Then, for your the GetComponent().enabled, this is an issue because i cant type the angle brackets correctly on the forum. So What you want to do is for each GetComponent, put BoxCollider2D in brackets next to it.

    As for the rotation issue, search for any code that is setting your RigidBodyConstraints2D.FreezeRotation off, if there’s nothing. Then you can add one more line to the Death() method like this:

        IEnumerator Death()
        {
            ...
            anim.SetTrigger("Death");
            rb.constraints = RigidbodyConstraints2D.FreezePosition;
            rb.constraints = RigidbodyConstraints2D.FreezeRotation;
            GetComponent[BoxCollider2D]().enabled = false;
    
            yield return new WaitForSeconds(0.9f);
            ...
        }
    #14801
    MI NI
    Level 14
    Bronze Supporter (Patron)
    Helpful?
    Up
    0
    ::

    It worked successfully, thank you very much

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

Go to Login Page →


Advertisement below: