Forum begins after the advertisement:


[Part 7] Save system issues

Home Forums Video Game Tutorial Series Creating a Metroidvania in Unity [Part 7] Save system issues

Viewing 15 posts - 1 through 15 (of 16 total)
  • Author
    Posts
  • #13441
    Allan Valin
    Participant

    Hey there, I finished watching the part 7 and got a couple issues with the save system (maybe it’d be more, but I decided not to bother implementing the mana cap, shade spawn and map since my uni project just needs to be playable for 10 minutes lol).

    So, the first is the timing for the death screen, even though I followed the same values and tried changing them around, there still this off-sync and teleport situation (see the video below).

    The second problem is that the interact key isn’t working (I changed in the input manager as shown), at 0:27 you can see I stop by the flag after having jumped on the respawn point I set on the platform, but it doesn’t work (the link to the bench file was leading to a site with malware, so I used something random, I tried making it so the animation changes when interacted, but with little success since I can’t interact lol). In the video I end up jumping on the platform again, but I tested without and it’s the same result.

    After I pause the game you can jump to 1:00 because the screen capture didn’t get the IDE window. What I did was remove the comment of //SaveData.Instance.LoadPlayerData(); in PlayerController.cs‘s Start() method. For whatever reason, calling this method destroys the player’s ability to move.

    My player health is bugged as well, but the hearts are filled after the death… any ideas?

    Another problem is that the respawn button only works once (I forgot to record on the first video, check below another one). After I stop the testing you can skip to 00:41, dying while the SaveData.Instance.LoadPlayerData(); is active in the code gets the other behavior going.

    Using P to save manually in another scene works when the code is uncommented.

    Instead of Bench I used “Savepoint”

    Savepoint.cs

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.SceneManagement;
    
    public class Savepoint : MonoBehaviour
    {
        public bool interacted;
        private Animator anim;
    
        // Start is called before the first frame update
        void Start()
        {
            anim = GetComponent<Animator>();
            //anim.Play("Flag_Down");
        }
    
        // Update is called once per frame
        void Update()
        {
            
        }
    
        private void OnTriggerStay2D(Collider2D collision)
        {
            if(collision.CompareTag("Player") && Input.GetButtonDown("Interact"))
            {
                interacted = true;
                //anim.Play("Flag_Up");
    
                SaveData.Instance.savepointSceneName = SceneManager.GetActiveScene().name;
                SaveData.Instance.savepointPos = new Vector2(gameObject.transform.position.x, gameObject.transform.position.y);
                SaveData.Instance.StoreSavepoint();
                SaveData.Instance.SavePlayerData();
            }
        }
    
        private void OnTriggerExit2D(Collider2D collision)
        {
            if (collision.CompareTag("Player") && Input.GetButtonDown("Interact"))
            {
                interacted = false;
            }
        }
    }

    SaveData.cs

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using System.IO;
    using UnityEngine.SceneManagement;
    
    [System.Serializable]
    public struct SaveData
    {
        public static SaveData Instance;
    
        // save current scene name
        public HashSet<string> sceneNames;
    
        // save point
        public string savepointSceneName;
        public Vector2 savepointPos;
    
        // player data
        public int playerHealth;
        public float playerMana;
        public Vector2 playerPosition;
        public string lastScene;
    
        public void Initialize()
        {
            if(!File.Exists(Application.persistentDataPath + "/save.data"))
            {
                BinaryWriter writer = new BinaryWriter(File.Create(Application.persistentDataPath + "/save.data"));
            }
            if (!File.Exists(Application.persistentDataPath + "/save.player.data"))
            {
                BinaryWriter writer = new BinaryWriter(File.Create(Application.persistentDataPath + "/save.player.data"));
            }
    
            if (sceneNames == null) sceneNames = new HashSet<string>();
        }
    
        public void StoreSavepoint()
        {
            using(BinaryWriter writer = new BinaryWriter(File.OpenWrite(Application.persistentDataPath + "/save.data")))
            {
                writer.Write(savepointSceneName);
                //writer.Write((int)savepointPos.x);
                writer.Write(savepointPos.x);
                writer.Write(savepointPos.y);
    
            }
        }
    
        public void LoadSavepoint()
        {
            if (File.Exists(Application.persistentDataPath + "/save.data"))
            {
                using (BinaryReader reader = new BinaryReader(File.OpenRead(Application.persistentDataPath + "/save.data")))
                {
                    savepointSceneName = reader.ReadString();
                    savepointPos.x = reader.ReadSingle();
                    savepointPos.y = reader.ReadSingle();
                }
            }
        }
    
        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);
            }
        }
    
        public void LoadPlayerData()
        {
            if (File.Exists(Application.persistentDataPath + "/save.player.data"))
            {
                using (BinaryReader reader = new BinaryReader(File.OpenRead(Application.persistentDataPath + "/save.player.data")))
                {
                    playerHealth = reader.ReadInt32();
                    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;
                    
                }
            }
            else
            {
                Debug.Log("File doesn't exist");
                PlayerController.Instance.Health = PlayerController.Instance.maxHealth;
                PlayerController.Instance.Mana = 0.5f;
            }
        }
    }

    GameManager.cs

    
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.SceneManagement;
    
    public class GameManager : MonoBehaviour
    {
        public string transitionedFromScene;
    
        public Vector2 platformingRespawnPoint;
        public Vector2 respawnPoint;
        [SerializeField] Savepoint savepoint;
    
        public static GameManager Instance { get; private set; }
        private void Awake()
        {
            SaveData.Instance.Initialize();
    
            if (Instance != null && Instance != this)
            {
                Destroy(gameObject);
            }
            else
            {
                Instance = this;
            }
    
            SaveScene();
    
            DontDestroyOnLoad(gameObject);
            savepoint = FindObjectOfType<Savepoint>();
        }
    
        public void RespawnPlayer()
        {
            SaveData.Instance.LoadSavepoint();
    
            if(SaveData.Instance.savepointSceneName != null)
            {
                SceneManager.LoadScene(SaveData.Instance.savepointSceneName);
            }
            
            if(SaveData.Instance.savepointPos != null)
            {
                respawnPoint = SaveData.Instance.savepointPos;
            }
            else
            {
                respawnPoint = platformingRespawnPoint;
            }
            /*
            if (savepoint != null)
            {
                if (savepoint.interacted)
                {
                    respawnPoint = savepoint.transform.position;
                }
                else
                {
                    respawnPoint = platformingRespawnPoint;
                }
            }
            else
            {
                respawnPoint = platformingRespawnPoint;
            }
            */
    
            PlayerController.Instance.transform.position = respawnPoint;
            StartCoroutine(UIManager.Instance.DeactivateDeathScreen());
            PlayerController.Instance.Respawned();
        }
    
        private void Update() 
        {
            if(Input.GetKeyDown(KeyCode.P)) // for testing 
            {
                SaveData.Instance.SavePlayerData();
            }
        }
    
        public void SaveScene()
        {
            string currentSceneName = SceneManager.GetActiveScene().name;
            SaveData.Instance.sceneNames.Add(currentSceneName);
        }
    }

    UIManager.cs

    using System.Collections;
    using System.Collections.Generic;
    using Unity.VisualScripting;
    using UnityEngine;
    
    public class UIManager : MonoBehaviour
    {
        public static UIManager Instance;
        public SceneFader sceneFader;
    
        [SerializeField] GameObject deathScreen;
        private void Awake()
        {
            if (Instance != null && Instance != this)
            {
                Destroy(gameObject);
            }
            else
            {
                Instance = this;
            }
            DontDestroyOnLoad(gameObject);
    
            
        }
    
        private void Start()
        {
            sceneFader = GetComponentInChildren<SceneFader>();
        }
    
        public IEnumerator ActivateDeathScreen()
        {
            yield return new WaitForSecondsRealtime(0.8f);
            StartCoroutine(sceneFader.Fade(SceneFader.FadeDirection.In));
            yield return new WaitForSecondsRealtime(0.8f);
            deathScreen.SetActive(true);
    
        }
    
        public IEnumerator DeactivateDeathScreen()
        {
            yield return new WaitForSecondsRealtime(0.5f);
            deathScreen.SetActive(false);
            StartCoroutine(sceneFader.Fade(SceneFader.FadeDirection.Out));
        }
    
    }

    PlayerController.cs

    
    using System.Collections;
    using System.Collections.Generic;
    using System.Net.Sockets;
    using UnityEngine;
    using UnityEngine.UIElements;
    using UnityEngine.UI;
    
    public class PlayerController : MonoBehaviour
    {
        [Header("Horizontal Movement Settings:")]
        [SerializeField] private float walkSpeed = 1; //sets the players movement speed on the ground
        [Space(5)]
    
        [Header("Vertical Movement Settings")]
        [SerializeField] private float jumpForce = 45f; //sets how hight the player can jump
    
        private int jumpBufferCounter = 0; //stores the jump button input
        [SerializeField] private int jumpBufferFrames; //sets the max amount of frames the jump buffer input is stored
    
        private float coyoteTimeCounter = 0; //stores the Grounded() bool
        [SerializeField] private float coyoteTime; ////sets the max amount of frames the Grounded() bool is stored
    
        private int airJumpCounter = 0; //keeps track of how many times the player has jumped in the air
        [SerializeField] private int maxAirJumps; //the max no. of air jumps
    
        private float gravity; //stores the gravity scale at start
        [Space(5)]
    
        [Header("Ground Check Settings:")]
        [SerializeField] private Transform groundCheckPoint; //point at which ground check happens
        [SerializeField] private float groundCheckY = 0.2f; //how far down from ground chekc point is Grounded() checked
        [SerializeField] private float groundCheckX = 0.5f; //how far horizontally from ground chekc point to the edge of the player is
        [SerializeField] private LayerMask whatIsGround; //sets the ground layer
        [Space(5)]
    
        [Header("Dash Settings")]
        [SerializeField] private float dashSpeed; //speed of the dash
        [SerializeField] private float dashTime; //amount of time spent dashing
        [SerializeField] private float dashCooldown; //amount of time between dashes
        [SerializeField] GameObject dashEffect;
        private bool canDash = true, dashed;
        [Space(5)]
    
        [Header("Attack Settings:")]
        [SerializeField] private Transform SideAttackTransform; //the middle of the side attack area
        [SerializeField] private Vector2 SideAttackArea; //how large the area of side attack is
    
        [SerializeField] private Transform UpAttackTransform; //the middle of the up attack area
        [SerializeField] private Vector2 UpAttackArea; //how large the area of side attack is
    
        [SerializeField] private Transform DownAttackTransform; //the middle of the down attack area
        [SerializeField] private Vector2 DownAttackArea; //how large the area of down attack is
    
        [SerializeField] private LayerMask attackableLayer; //the layer the player can attack and recoil off of
    
        private float timeBetweenAttack, timeSinceAttck;
    
        [SerializeField] private float damage; //the damage the player does to an enemy
    
        [SerializeField] private GameObject slashEffect; //the effect of the slashs
        [Space(5)]
    
        bool restoreTime;
        float restoreTimeSpeed;
        [Space(5)]
    
        [Header("Recoil Settings:")]
        [SerializeField] private int recoilXSteps = 5; //how many FixedUpdates() the player recoils horizontally for
        [SerializeField] private int recoilYSteps = 5; //how many FixedUpdates() the player recoils vertically for
    
        [SerializeField] private float recoilXSpeed = 100; //the speed of horizontal recoil
        [SerializeField] private float recoilYSpeed = 100; //the speed of vertical recoil
    
        private int stepsXRecoiled, stepsYRecoiled; //the no. of steps recoiled horizontally and verticall
        [Space(5)]
    
        [Header("Health Settings")]
        public int health;
        public int maxHealth;
        [SerializeField] GameObject bloodSpurt;
        [SerializeField] float hitFlashSpeed;
        public delegate void OnHealthChangedDelegate();
        [HideInInspector] public OnHealthChangedDelegate onHealthChangedCallback;
        float healTimer;
        [SerializeField] float timeToHeal;
        [Space(5)]
    
        [Header("Mana Settings")]
        [SerializeField] UnityEngine.UI.Image manaStorage;
        [SerializeField] float mana;
        [SerializeField] float manaDrainSpeed;
        [SerializeField] float manaGain;
        [Space(5)]
    
        [Header("Spell Settings")]
        //spell stats
        [SerializeField] float manaSpellCost = 0.3f;
        [SerializeField] float timeBetweenCast = 0.5f;
        [SerializeField] float spellDamage; //upspellexplosion and downspellfireball
        [SerializeField] float downSpellForce; // desolate dive only
        //spell cast objects
        [SerializeField] GameObject sideSpellFireball;
        [SerializeField] GameObject upSpellExplosion;
        [SerializeField] GameObject downSpellFireball;
        float timeSinceCast;
        float castOrHealTimer;
        [Space(5)]
    
        [HideInInspector] public PlayerStateList pState;
        private Animator anim;
        public Rigidbody2D rb;
        private SpriteRenderer sr;
    
        //Input Variables
        private float xAxis, yAxis;
        private bool attack = false;
        private bool canFlash = true;
    
        public static PlayerController Instance;
    
        private void Awake()
        {
            if (Instance != null && Instance != this)
            {
                Destroy(gameObject);
            }
            else
            {
                Instance = this;
            }
            DontDestroyOnLoad(gameObject);
            
        }
    
        // Start is called before the first frame update
        void Start()
        {
            pState = GetComponent<PlayerStateList>();
            rb = GetComponent<Rigidbody2D>();
            sr = GetComponent<SpriteRenderer>();
            anim = GetComponent<Animator>();
    
            //SaveData.Instance.LoadPlayerData();
            gravity = rb.gravityScale;
    
            Mana = mana;
            manaStorage.fillAmount = Mana;
            Health = maxHealth;
        }
    
        private void OnDrawGizmos()
        {
            Gizmos.color = Color.red;
            Gizmos.DrawWireCube(SideAttackTransform.position, SideAttackArea);
            Gizmos.DrawWireCube(UpAttackTransform.position, UpAttackArea);
            Gizmos.DrawWireCube(DownAttackTransform.position, DownAttackArea);
        }
    
        // Update is called once per frame
        void Update()
        {
            if (pState.cutscene) return;
            if(pState.alive)
            {
                GetInputs();
            }
            
            UpdateJumpVariables();
            RestoreTimeScale();
    
            if (pState.dashing) return;
            if (pState.alive)
            {
                Flip();
                Move();
                Jump();
                StartDash();
                Attack();
                Heal();
                CastSpell();
            }
            FlashWhileInvincible();
    
        }
        private void OnTriggerEnter2D(Collider2D _other) //for up and down cast spell
        {
            if (_other.GetComponent<Enemy>() != null && pState.casting)
            {
                _other.GetComponent<Enemy>().EnemyHit(spellDamage, (_other.transform.position - transform.position).normalized, -recoilYSpeed);
            }
        }
    
        private void FixedUpdate()
        {
            if (pState.cutscene) return;
    
            if (pState.dashing) return;
            Recoil();
        }
    
        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 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()
        {
            rb.velocity = new Vector2(walkSpeed * xAxis, rb.velocity.y);
            anim.SetBool("Walking", 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("Dashing");
            rb.gravityScale = 0;
            int _dir = pState.lookingRight ? 1 : -1;
            rb.velocity = new Vector2(_dir * dashSpeed, 0);
            if (Grounded()) Instantiate(dashEffect, transform);
            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)
        {
            //If exit direction is upwards
            if (_exitDir.y > 0)
            {
                rb.velocity = jumpForce * _exitDir;
            }
    
            //If exit direction requires horizontal movement
            if (_exitDir.x != 0)
            {
                xAxis = _exitDir.x > 0 ? 1 : -1;
    
                Move();
            }
    
            Flip();
            yield return new WaitForSeconds(_delay);
            pState.cutscene = false;
        }
        void Attack()
        {
            timeSinceAttck += Time.deltaTime;
            if (attack && timeSinceAttck >= timeBetweenAttack)
            {
                int _recoilLeftOrRight = pState.lookingRight ? 1 : -1;
                timeSinceAttck = 0;
                anim.SetTrigger("Attacking");
    
                if (yAxis == 0 || yAxis < 0 && Grounded())
                {
                    Hit(SideAttackTransform, SideAttackArea, ref pState.recoilingX, Vector2.right * _recoilLeftOrRight, recoilXSpeed);
                    Instantiate(slashEffect, SideAttackTransform);
                }
                else if (yAxis > 0)
                {
                    Hit(UpAttackTransform, UpAttackArea, ref pState.recoilingY, Vector2.up, recoilYSpeed);
                    SlashEffectAtAngle(slashEffect, 80, UpAttackTransform);
                }
                else if (yAxis < 0 && !Grounded())
                {
                    Hit(DownAttackTransform, DownAttackArea, ref pState.recoilingY, Vector2.down, recoilYSpeed);
                    SlashEffectAtAngle(slashEffect, -90, DownAttackTransform);
                }
            }
    
        }
        void Hit(Transform _attackTransform, Vector2 _attackArea, ref bool _recoilBool, Vector2 _recoilDir, float _recoilStrength)
        {
            Collider2D[] objectsToHit = Physics2D.OverlapBoxAll(_attackTransform.position, _attackArea, 0, attackableLayer);
    
            if (objectsToHit.Length > 0)
            {
                _recoilBool = true;
            }
            for (int i = 0; i < objectsToHit.Length; i++)
            {
                if (objectsToHit[i].GetComponent<Enemy>() != null)
                {
                    objectsToHit[i].GetComponent<Enemy>().EnemyHit(damage, _recoilDir, _recoilStrength);
    
                    if (objectsToHit[i].CompareTag("Enemy"))
                    {
                        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(-recoilXSpeed, 0);
                }
                else
                {
                    rb.velocity = new Vector2(recoilXSpeed, 0);
                }
            }
    
            if (pState.recoilingY)
            {
                rb.gravityScale = 0;
                if (yAxis < 0)
                {
                    rb.velocity = new Vector2(rb.velocity.x, recoilYSpeed);
                }
                else
                {
                    rb.velocity = new Vector2(rb.velocity.x, -recoilYSpeed);
                }
                airJumpCounter = 0;
            }
            else
            {
                rb.gravityScale = gravity;
            }
    
            //stop recoil
            if (pState.recoilingX && stepsXRecoiled < recoilXSteps)
            {
                stepsXRecoiled++;
            }
            else
            {
                StopRecoilX();
            }
            if (pState.recoilingY && stepsYRecoiled < recoilYSteps)
            {
                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());
                }
            }
            
            
        }
        IEnumerator StopTakingDamage()
        {
            pState.invincible = true;
            GameObject _bloodSpurtParticles = Instantiate(bloodSpurt, transform.position, Quaternion.identity);
            Destroy(_bloodSpurtParticles, 1.5f);
            anim.SetTrigger("TakeDamage");
            yield return new WaitForSeconds(1f);
            pState.invincible = false;
        }
    
        IEnumerator Flash()
        {
            sr.enabled = !sr.enabled;
            canFlash = false;
            yield return new WaitForSeconds(0.1f);
            canFlash = true;
        }
        void FlashWhileInvincible()
        {
            if (pState.invincible)
            {
                if (Time.timeScale > 0.2 && canFlash)
                {
                    StartCoroutine(Flash());
                }
            }
            else
            {
                sr.enabled = true;
            }
        }
    
        void RestoreTimeScale()
        {
            if (restoreTime)
            {
                if (Time.timeScale < 1)
                {
                    Time.timeScale += Time.deltaTime * restoreTimeSpeed;
                }
                else
                {
                    Time.timeScale = 1;
                    restoreTime = false;
                }
            }
        }
    
        public void HitStopTime(float _newTimeScale, int _restoreSpeed, float _delay)
        {
            restoreTimeSpeed = _restoreSpeed;
            if (_delay > 0)
            {
                StopCoroutine(StartTimeAgain(_delay));
                StartCoroutine(StartTimeAgain(_delay));
            }
            else
            {
                restoreTime = true;
            }
            Time.timeScale = _newTimeScale;
        }
    
        IEnumerator StartTimeAgain(float _delay)
        {
            restoreTime = true;
            yield return new WaitForSeconds(_delay);
        }
    
        IEnumerator Death()
        {
            pState.alive = false;
            Time.timeScale = 1f;
            GameObject _bloodSpurtParticles = Instantiate(bloodSpurt, transform.position, Quaternion.identity);
            Destroy(_bloodSpurtParticles, 1.5f);
            anim.SetTrigger("Death");
            yield return new WaitForSecondsRealtime(0.9f);
    
            StartCoroutine(UIManager.Instance.ActivateDeathScreen());
    
            //Respawned();
        }
    
        public void Respawned()
        {
            if(!pState.alive)
            {
                pState.alive = true;
                Health = maxHealth;
                anim.Play("Player_Idle");
                mana = 0;
            }
        }
    
        public int Health
        {
            get { return health; }
            set
            {
                if (health != value)
                {
                    health = Mathf.Clamp(value, 0, maxHealth);
    
                    if (onHealthChangedCallback != null)
                    {
                        onHealthChangedCallback.Invoke();
                    }
                }
            }
        }
    
        void Heal()
        {
            if (Input.GetButton("Cast/Heal") && castOrHealTimer > 0.05f && Health < maxHealth && Mana > 0 && !pState.jumping && !pState.dashing)
            {
                pState.healing = true;
                anim.SetBool("Healing", true);
    
                //healing
                healTimer += Time.deltaTime;
                if (healTimer >= timeToHeal)
                {
                    Health++;
                    healTimer = 0;
                }
    
                //drain mana
                Mana -= Time.deltaTime * manaDrainSpeed;
            }
            else
            {
                pState.healing = false;
                anim.SetBool("Healing", false);
                healTimer = 0;
            }
        }
    
        public float Mana
        {
            get { return mana; }
            set
            {
                //if mana stats change
                if (mana != value)
                {
                    mana = Mathf.Clamp(value, 0, 1);
                    manaStorage.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;
            }
    
            if (Grounded())
            {
                //disable downspell if on the ground
                downSpellFireball.SetActive(false);
            }
            //if down spell is active, force player down until grounded
            if (downSpellFireball.activeInHierarchy)
            {
                rb.velocity += downSpellForce * Vector2.down;
            }
        }
    
        IEnumerator CastCoroutine()
        {
            anim.SetBool("Casting", true);
            yield return new WaitForSeconds(0.15f); // check on Animator if it matches
    
            //side cast
            if (yAxis == 0 || (yAxis < 0 && Grounded()))
            {
                GameObject _fireBall = Instantiate(sideSpellFireball, SideAttackTransform.position, Quaternion.identity);
    
                //flip fireball
                if (pState.lookingRight)
                {
                    _fireBall.transform.eulerAngles = Vector3.zero; // if facing right, fireball continues as per normal
                }
                else
                {
                    _fireBall.transform.eulerAngles = new Vector2(_fireBall.transform.eulerAngles.x, 180);
                    //if not facing right, rotate the fireball 180 deg
                }
                pState.recoilingX = true;
            }
    
            //up cast
            else if (yAxis > 0)
            {
                Instantiate(upSpellExplosion, transform);
                rb.velocity = Vector2.zero;
            }
    
            //down cast
            else if (yAxis < 0 && !Grounded())
            {
                downSpellFireball.SetActive(true);
            }
    
            Mana -= manaSpellCost;
            yield return new WaitForSeconds(0.35f); // check Animator
            anim.SetBool("Casting", 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 (!pState.jumping && jumpBufferCounter > 0 && coyoteTimeCounter > 0)
            {
                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("Jumping", !Grounded());
        }
    
        void UpdateJumpVariables()
        {
            if (Grounded())
            {
                pState.jumping = false;
                coyoteTimeCounter = coyoteTime;
                airJumpCounter = 0;
            }
            else
            {
                coyoteTimeCounter -= Time.deltaTime;
            }
    
            if (Input.GetButtonDown("Jump"))
            {
                jumpBufferCounter = jumpBufferFrames;
            }
            else
            {
                jumpBufferCounter--;
            }
        }
    }
    #13442
    Allan Valin
    Participant

    Btw, for the interaction I tried changing in Savepoint.cs (Bench.cs) from OnTriggerStay2D to OnTriggerEnter2D but couldn’t see much of a difference.
    What I could note is that using the previous respawn points make it so you don’t fall into the abyss when dying a second time.
    Basically, since I can’t interact with the save point, there seems to not be a standard respawn point and I get teleported the the origin 0,0,0. That could be partially solved with a respawn point where the player spawns so it’s immediately triggered, but that sounds like a band-aid solution lol

    #13443
    Terence
    Keymaster

    Allan, you might find this helpful: https://blog.terresquall.com/community/topic/part-7-scenefader-nullreferenceexception-and-savedata-endofstreamexception/#post-13222

    It addresses most of the issues, except for the Respawn button not working without a benched saved (you need to sit on the bench once, then the respawn will work).

    We’ll fix the respawn issue later this month: https://www.patreon.com/posts/99646882

    #13445
    Allan Valin
    Participant

    Nice, I made the changes mentioned on the post and movement is working again. Thought I can’t say that the bench interaction is working properly.

    I made an animation to make a flag appear on the pole I used instead of the bench to see if the interaction was working (previously I renamed the animation and the change didn’t reflect on the Animator panel, so it wasn’t working before), I see that even though the animation plays, the rest of the code isn’t saving the bench position properly and I get teleported to 0,0,0 after death.

    #13446
    Terence
    Keymaster

    I can have a look at the code for you if you don’t mind sharing.

    #13447
    Allan Valin
    Participant

    All the relevant code is on the top post already XD
    Unless I forgot something?

    #13448
    Allan Valin
    Participant

    Just noticed another problem, the player is taking damage when hitting enemies, even though only the sword hits them.

    #13449
    Allan Valin
    Participant

    btw, just started part 8 and the bug with the wall jump unlock is still there because the file doesn’t exist when the game starts. As mentioned, saving manually, closing the game and opening it again “””solves””” it, but not really lol
    Wouldn’t just having a file existing on the project settings solve that? (idk how though, maybe create it when the game launches, if it doesn’t exist)

    #13451
    Terence
    Keymaster

    I made an animation to make a flag appear on the pole I used instead of the bench to see if the interaction was working (previously I renamed the animation and the change didn’t reflect on the Animator panel, so it wasn’t working before), I see that even though the animation plays, the rest of the code isn’t saving the bench position properly and I get teleported to 0,0,0 after death.

    One possible reason this is not working may be that you have multiple instances of SaveData on the scene. You can check this by typing “t:SaveData” into the Hierarchy when the game is playing and seeing how many GameObjects appear. The reason I say this is because your code clearly sets the variables. Yet it seems the value is not saved.

    Also, this is a mistake that Kiefer and Matias made, but we shouldn’t read input in OnTriggerXXX2D(), because they are not synchronised with the Unity’s framerate. All input should be read in Update() only. Otherwise, this will cause input to be dropped sometimes.

    Just noticed another problem, the player is taking damage when hitting enemies, even though only the sword hits them.

    If I’m not mistaken, enemies deal damage if they register that a collider is tagged as “Player”. You may have accidentally tagged your attack hitboxes as “Player”.

    #13452
    Terence
    Keymaster

    btw, just started part 8 and the bug with the wall jump unlock is still there because the file doesn’t exist when the game starts. As mentioned, saving manually, closing the game and opening it again “””solves””” it, but not really lol
    Wouldn’t just having a file existing on the project settings solve that? (idk how though, maybe create it when the game launches, if it doesn’t exist)

    Do you happen to be able to grab a screenshot of this? I’m compiling a list of fixes we have to make to the series.

    #13453
    Allan Valin
    Participant

    Yet it seems the value is not saved.

    t:SaveData shows nothing, even when I used P to save and it clearly saved.

    All input should be read in Update() only.

    How one goes to compare the Player tag then, when there’s no collision argument?

    void Update()
    {
        
    }
    
    private void OnTriggerStay2D(Collider2D collision)
    {
        if(collision.CompareTag("Player") && Input.GetButtonDown("Interact"))
        {
            ...
        }
    }

    If I’m not mistaken, enemies deal damage if they register that a collider is tagged as “Player”. You may have accidentally tagged your attack hitboxes as “Player”.

    That seems indeed to be what is happening, they where in the Attackable Layer and Untagged tag. I moved them Side/Up/Down Attack Transforms to the Default layer, but the problem still persists. The code below should be the one related to attacking and tags, if needed.

    void Attack()
    {
        timeSinceAttack += Time.deltaTime;
        if (attack && timeSinceAttack >= timeBetweenAttack)
        {
            int _recoilLeftOrRight = pState.lookingRight ? 1 : -1;
            timeSinceAttack = 0;
            anim.SetTrigger("Attacking");
    
            if (yAxis == 0 || yAxis < 0 && Grounded())
            {
                Hit(SideAttackTransform, SideAttackArea, ref pState.recoilingX, Vector2.right * _recoilLeftOrRight, recoilXSpeed);
                Instantiate(slashEffect, SideAttackTransform);
            }
            else if (yAxis > 0)
            {
                Hit(UpAttackTransform, UpAttackArea, ref pState.recoilingY, Vector2.up, recoilYSpeed);
                SlashEffectAtAngle(slashEffect, 80, UpAttackTransform);
            }
            else if (yAxis < 0 && !Grounded())
            {
                Hit(DownAttackTransform, DownAttackArea, ref pState.recoilingY, Vector2.down, recoilYSpeed);
                SlashEffectAtAngle(slashEffect, -90, DownAttackTransform);
            }
        }
    
    }
    void Hit(Transform _attackTransform, Vector2 _attackArea, ref bool _recoilBool, Vector2 _recoilDir, float _recoilStrength)
    {
        Collider2D[] objectsToHit = Physics2D.OverlapBoxAll(_attackTransform.position, _attackArea, 0, attackableLayer);
    
        if (objectsToHit.Length > 0)
        {
            _recoilBool = true;
        }
        for (int i = 0; i < objectsToHit.Length; i++)
        {
            if (objectsToHit[i].GetComponent<Enemy>() != null)
            {
                objectsToHit[i].GetComponent<Enemy>().EnemyHit(damage, _recoilDir, _recoilStrength);
    
                if (objectsToHit[i].CompareTag("Enemy"))
                {
                    Mana += manaGain;
                }
            }
        }
    }
    private void OnTriggerEnter2D(Collider2D _other) //for up and down cast spell
    {
        if (_other.GetComponent<Enemy>() != null && pState.casting)
        {
            _other.GetComponent<Enemy>().EnemyHit(spellDamage, (_other.transform.position - transform.position).normalized, -recoilYSpeed);
        }
    }
    

    Do you happen to be able to grab a screenshot of this? I’m compiling a list of fixes we have to make to the series.

    This error is shown on the video part 8, around 8:49 timestamp.

    #13454
    Terence
    Keymaster

    How one goes to compare the Player tag then, when there’s no collision argument?

    Record the interaction in a boolean.

    void Update()
    {
        if(myInteraction)
        {
            ...
        }
    }
    
    private void OnTriggerStay2D(Collider2D collision)
    {
        if(collision.CompareTag("Player") && Input.GetButtonDown("Interact"))
        {
            myInteraction = true;
        }
        else
        {
            myInteraction = false;
        }
    }

    Update 6 March 2024: My code above is wrong. It should be like this instead:

    void Update()
    {
        if(Input.GetButtonDown("Interact"))
        {
            myInteraction = true;
        }
        else
        {
            myInteraction = true;
        }
    }
    
    private void OnTriggerStay2D(Collider2D collision)
    {
        if(collision.CompareTag("Player") && myInteraction)
        {
            ...
        }
        else
        {
            ...
        }
    }
    #13455
    Allan Valin
    Participant

    I changed it like below, but I’m not sure where to place interacted = false; from OnTriggerExit2D.
    I had the problem from using anim.Play since it’d only work after pressing F multiple times, but adding a trigger and using anim.SetTrigger(“Active”); works better.

    Do you happen to have a screenshot of the values used on the PlayerController? I followed the videos but keep being launched violently backwards from recoil, or freezing too long in time-stop after damage etc.

    On another note, are enemies supposed to respawn like in MegaMan?
    That means, if you kill them, move forward enough to not see they respawn point, then immediately come back, they appear again.

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.SceneManagement;
    
    public class Savepoint : MonoBehaviour
    {
        public bool interacted;
        private Animator anim;
        private bool interactionCheck;
    
        // Start is called before the first frame update
        void Start()
        {
            anim = GetComponent<Animator>();
            anim.Play("Flag_Down");
        }
    
        // Update is called once per frame
        void Update()
        {
            if(interactionCheck)
            {
                interacted = true;
                //anim.Play("Flag");
                anim.SetTrigger("Active");
    
                SaveData.Instance.savepointSceneName = SceneManager.GetActiveScene().name;
                SaveData.Instance.savepointPos = new Vector2(gameObject.transform.position.x, gameObject.transform.position.y);
                SaveData.Instance.StoreSavepoint();
                SaveData.Instance.SavePlayerData();
            }
            else
            {
                interacted = false;
            }
        }
    
        private void OnTriggerStay2D(Collider2D collision)
        {
            if(collision.CompareTag("Player") && Input.GetButtonDown("Interact"))
            {
                interactionCheck = true;
            }
            else
            {
                interactionCheck = false;
            }
        }
    
        /*private void OnTriggerExit2D(Collider2D collision)
        {
            if (collision.CompareTag("Player"))
            {
                interacted = false;
            }
        }*/
    }
    
    #13467
    Terence
    Keymaster

    Hi Allan, sorry, I got the code wrong. I’ve modified your code. See below:

        // Update is called once per frame
        void Update()
        {
            if(interactionCheckInput.GetButtonDown("Interact"))
            {
                interactionCheck = true;
                interacted = true;
                //anim.Play("Flag");
                anim.SetTrigger("Active");
    
                SaveData.Instance.savepointSceneName = SceneManager.GetActiveScene().name;
                SaveData.Instance.savepointPos = new Vector2(gameObject.transform.position.x, gameObject.transform.position.y);
                SaveData.Instance.StoreSavepoint();
                SaveData.Instance.SavePlayerData();
            }
            else
            {
                interacted = false;interactionCheck = false;
            }
        }
    
        private void OnTriggerStay2D(Collider2D collision)
        {
            if(collision.CompareTag("Player") && Input.GetButtonDown("Interact")interactionCheck)
            {
                interactionCheck = true;
                interacted = true;
                //anim.Play("Flag");
                anim.SetTrigger("Active");
    
                SaveData.Instance.savepointSceneName = SceneManager.GetActiveScene().name;
                SaveData.Instance.savepointPos = new Vector2(gameObject.transform.position.x, gameObject.transform.position.y);
                SaveData.Instance.StoreSavepoint();
                SaveData.Instance.SavePlayerData();
            }
            else
            {
                interactionCheck = false;
            }
        }
    #13493
    Allan Valin
    Participant

    I tried your code and it doesn’t seem much different. I added a Debug.Log("saved") after the save and your code (and previous iterations of mine) took a long time to save.

    My current version plays the animation and shows the debug immediately, not sure if I’ve shown it previously.

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.SceneManagement;
    
    public class Savepoint : MonoBehaviour
    {
        public bool interacted;
        private Animator anim;
        private bool interactionCheck;
    
        // Start is called before the first frame update
        void Start()
        {
            anim = GetComponent<Animator>();
            anim.Play("Flag_Down");
        }
    
        // Update is called once per frame
        void Update()
        {
            if(interactionCheck)
            {
                interacted = true;
                //anim.Play("Flag");
                anim.SetTrigger("Active");
    
                SaveData.Instance.savepointSceneName = SceneManager.GetActiveScene().name;
                SaveData.Instance.savepointPos = new Vector2(gameObject.transform.position.x, gameObject.transform.position.y);
                SaveData.Instance.StoreSavepoint();
                SaveData.Instance.SavePlayerData();
                Debug.Log("saved");
            }
            else
            {
                interacted = false;
            }
        }
    
        private void OnTriggerStay2D(Collider2D collision)
        {
            if(collision.CompareTag("Player") && Input.GetButtonDown("Interact"))
            {
                interactionCheck = true;
            }
            else
            {
                interactionCheck = false;
            }
        }

    I think the most elegant way would be to create a new method and only have it being called on Update, but alas, it seems to work lol

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

Go to Login Page →


Advertisement below: