Forum begins after the advertisement:


[Part 5] Canvas UI reset and stuck after scene transition

Home Forums Video Game Tutorial Series Creating a Metroidvania in Unity [Part 5] Canvas UI reset and stuck after scene transition

Viewing 3 posts - 1 through 3 (of 3 total)
  • Author
    Posts
  • #13339
    Guillaume Lethug
    Level 1
    Participant
    Helpful?
    Up
    0
    ::

    Hello,

    I’m encountering a problem after following the tutorial. When transitioning from one scene to another, my Canvas UI seems to be resetting; the mana is fully replenished and if I try to heal the UI doesn’t update. Additionally, attempting to use spells, heal, or perform any action involving mana results in numerous errors, and the UI fails to update (e.g., mana depletion, health regeneration). Furthermore, casting a spell after transitioning to a new scene causes my character to become stuck in the casting animation, effectively freezing the player’s state on “casting”.

    Below are some screenshots and code snippets to better illustrate the issue.

    Before transition:

    View post on imgur.com

    After transition:

    View post on imgur.com

    Using a spell after transition:

    View post on imgur.com

    Code:

    Scene Transition:

    <code>
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.SceneManagement;
    
    public class SceneTransition : MonoBehaviour
    {
        [SerializeField] private string transitionTo;
        [SerializeField] private Transform startPoint;
        [SerializeField] private Vector2 exitDirection;
        [SerializeField] private float exitTime;
    
        private void Start() {
            if(transitionTo == GameManager.Instance.transitionFromScene) {
                PlayerController.Instance.transform.position = startPoint.position;
                StartCoroutine(PlayerController.Instance.WalkIntoNewScene(exitDirection, exitTime));
            }
        }
    
        private void OnTriggerEnter2D(Collider2D _other) {
            if(_other.CompareTag("Player")) {
            GameManager.Instance.transitionFromScene = SceneManager.GetActiveScene().name;
    
            PlayerController.Instance.pState.cutscene = true;
    
             SceneManager.LoadScene(transitionTo);
            }
        }
    }
    </code>

    GameManager:

    <code>
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class GameManager : MonoBehaviour
    {
        public string transitionFromScene;
    
        public static GameManager Instance { get; private set; }
    
        private void Awake() {
           if (Instance != null && Instance != this) {
               Destroy(gameObject);
           } else {
               Instance = this;
           }
           DontDestroyOnLoad(gameObject);
        }
    }
    </code>

    PlayerController:

    <code>
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.UI;
    
    public class PlayerController : MonoBehaviour
    {
        [Header ("Horizontal Movement Settings:")]
        [SerializeField] private float walkSpeed = 2f;
    
        [Header ("Vertical Movement Settings:")]
        [SerializeField] private float jumpForce = 45f;
        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 ("Ground Check Settings:")]
        [SerializeField] private Transform groundCheckPoint;
        [SerializeField] private float groundCheckY = 0.2f;
        [SerializeField] private float groundCheckX = 0.5f;
        [SerializeField] private LayerMask whatIsGround;
        [Space(5)]
    
        [Header ("Dash Settings:")]
    
        [SerializeField] private float dashSpeed;
        [SerializeField] private float dashTime;
        [SerializeField] private float dashCooldown;
        [SerializeField] GameObject dashEffect;
        [Space(5)]
    
        [Header ("Attacking :")]
        bool attack = false;
        float timeBetweenAttack, timeSinceAttack;
        [SerializeField] Transform SideAttackTransform, UpAttackTransform, DownAttackTransform;
        [SerializeField] Vector2 SideAttackArea, UpAttackArea, DownAttackArea;
        [SerializeField] LayerMask attackableLayer;
        [SerializeField] float damage;
        [SerializeField] GameObject slashEffect;
    
        bool restoreTime;
        float restoreTimeSpeed;
        [Space(5)]
    
        [Header ("Recoil :")]
        [SerializeField] int recoilXSteps = 5;
        [SerializeField] int recoilYSteps = 5;
        [SerializeField] float recoilXSpeed = 100;
        [SerializeField] float recoilYSpeed = 100;
        int stepsXRecoiled, stepsYRecoiled;
        [Space(5)]
    
        [Header ("Health Settings :")]
        [SerializeField] UnityEngine.UI.Image manaStorage;
        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] float mana;
        [SerializeField] float manaDrainSpeed;
        [SerializeField] float manaGain;
        [Space(5)]
    
        [Header ("Spell Settings :")]
        [SerializeField] float manaSpellCost = 0.3f;
        [SerializeField] float timeBetweenCast = 0.5F;
        float timeSinceCast;
        [SerializeField] float spellDamage; // 2 other spells
        [SerializeField] float downSpellForce; // desolate dive only
        //spell cast objects
        [SerializeField] GameObject sideSpellFireball;
        [SerializeField] GameObject upSpellExplosion;
        [SerializeField] GameObject downSpellFireball;
        [Space(5)]
    
        [HideInInspector] public PlayerStateLists pState;
        private Rigidbody2D rb;
        private SpriteRenderer sr;
    
        private float xAxis, yAxis;
        private float gravity;
        Animator anim;
        private bool canDash = true;
        private bool dashed;
    
    
    
        public static PlayerController Instance;
    
        private void Awake()
        {
            if(Instance != null && Instance != this) {
                Destroy(gameObject);
            } 
            else 
            {
                Instance = this;
            }
           DontDestroyOnLoad(gameObject);
           Debug.Log("PlayerController Awake");
           Debug.Log("PlayerController Instance: " + Instance);
           Debug.Log("PlayerController this: " + this);
           Debug.Log("PlayerController gameObject: " + gameObject);
        }
    
        // Start is called before the first frame update
        void Start()
        {
            pState = GetComponent<PlayerStateLists>();
    
            rb = GetComponent<Rigidbody2D>();
            sr = GetComponent<SpriteRenderer>();
    
            anim = GetComponent<Animator>();
    
            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);
    
        }
    
        void Update()
        {
            if (pState.cutscene) return;
    
            GetInput();
            UpdateJumpVariables();
            if (pState.dashing) return;
            Flip();
            Move();  
            Jump();
            StartDash();
            Attack();
            RestoreTimeScale();
            FlashWhileInvincible();
            Heal();
            CastSpell();
        }
    
        private void OnTriggerEnter2D(Collider2D _other) {
            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 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;
            }
        }
    
        void GetInput()
        {
            xAxis = Input.GetAxisRaw("Horizontal");
            yAxis = Input.GetAxisRaw("Vertical");
            attack = Input.GetButtonDown("Attack");
        }
    
        private void Move()
        {
            rb.velocity = new Vector2(xAxis * walkSpeed, 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 upward
            if (_exitDir.y > 0) {
                rb.velocity = jumpForce * _exitDir;
            }
    
            // If exit direction require horizontal movement
            if(_exitDir.x != 0) {
                xAxis = _exitDir.x > 0 ? 1 : -1;
    
                Move();
            }
    
            Flip();
            yield return new WaitForSeconds(_delay);
            pState.cutscene = false;
    
        }
    
        void Attack() {
            timeSinceAttack += Time.deltaTime;
            if(attack && timeSinceAttack >= timeBetweenAttack) {
                timeSinceAttack = 0;
                anim.SetTrigger("Attacking");
    
                if(yAxis == 0 || yAxis < 0 && Grounded()){
                    Hit(SideAttackTransform, SideAttackArea, ref pState.recoilingX, recoilXSpeed);
                    Instantiate(slashEffect, SideAttackTransform);
                } else if (yAxis > 0) {
                    Hit(UpAttackTransform, UpAttackArea, ref pState.recoilingY, recoilYSpeed);
                    SlashEffectAtAngle(slashEffect, 80, UpAttackTransform);
                } else if (yAxis < 0 && !Grounded()) {
                    Hit(DownAttackTransform, DownAttackArea, ref pState.recoilingY, recoilYSpeed);
                    SlashEffectAtAngle(slashEffect, -90, DownAttackTransform);
                } 
            }
        }
    
        void Hit(Transform _attackTransform, Vector2 _attackArea, ref bool _recoilDir, float _recoilStrength) {
    
            Collider2D[] objectsToHit = Physics2D.OverlapBoxAll(_attackTransform.position, _attackArea, 0, attackableLayer);
    
            if (objectsToHit.Length > 0) {
               _recoilDir = true;  
            }
            for(int i = 0; i < objectsToHit.Length; i++)
            {
                if (objectsToHit[i].GetComponent<Enemy>() != null) {
                    objectsToHit[i].GetComponent<Enemy>().EnemyHit(damage, (transform.position - objectsToHit[i].transform.position).normalized, _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 Vector3(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;
            }
    
            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) {
    
            Health -= Mathf.RoundToInt(_damage);
            StartCoroutine(StopTakingDamage());
        }
    
        IEnumerator StopTakingDamage() {
            pState.invicible = true;
            GameObject _bloodSpurtParticles = Instantiate(bloodSpurt, UpAttackTransform.position, Quaternion.identity);
            Destroy(_bloodSpurtParticles, 1.5f);
            anim.SetTrigger("TakeDamage");
            yield return new WaitForSeconds(1f);
            pState.invicible = false;
            Debug.Log("Healt Player" + health);
    
        }
    
        void FlashWhileInvincible() {
            sr.material.color = pState.invicible ? Color.Lerp(Color.white, Color.black, Mathf.PingPong(Time.time * hitFlashSpeed, 1.0f)) : Color.white;
        }
    
         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);
        }
    
      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("Healing") && 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;
            }
    
        }
    
        float Mana {
            get {return mana;}
            set {
                if(mana != value) {
                    mana = Mathf.Clamp(value, 0, 1);
                    manaStorage.fillAmount = Mana;
                }
            }
        }
    
        void CastSpell() {
            if(Input.GetButtonDown("CastSpell") && timeSinceCast >= timeBetweenCast && Mana > manaSpellCost) {
                pState.casting = true;
                timeSinceCast = 0;
                StartCoroutine(CastCoroutine());
            } else {
                timeSinceCast += Time.deltaTime;
            }
    
            if (Grounded()) {
                //disable down spell
                downSpellFireball.SetActive(false);
            }
    
            if(downSpellFireball.activeInHierarchy) {
                rb.velocity += downSpellForce * Vector2.down;
            }
        }
    
        IEnumerator CastCoroutine() {
            anim.SetBool("Casting", true);
            yield return new WaitForSeconds(0.15f);
    
            // fireball 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;
                } else {
                    _fireBall.transform.eulerAngles = new Vector2(_fireBall.transform.eulerAngles.x, 180);
                }
                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);
            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 (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("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--;
            }
        }
     }
    </code>

    HeartController:

    <code>
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.UI;
    
    public class HeartController : MonoBehaviour
    {
        PlayerController player;
    
        private GameObject[] heartContainers;
        private Image[] heartFills;
        public Transform heartsParent;
        public GameObject heartContainerPrefab;
        // Start is called before the first frame update
        void Start()
        {
            player = PlayerController.Instance;
            heartContainers = new GameObject[PlayerController.Instance.maxHealth];
            heartFills = new Image[PlayerController.Instance.maxHealth];
    
            PlayerController.Instance.onHealthChangedCallback += UpdateHeartsHUD;
            InstantiateHeartContainers();
            UpdateHeartsHUD();
        }
    
        // Update is called once per frame
        void Update()
        {
    
        }
    
        void SetHeartContainers(){
            for (int i = 0; i < heartContainers.Length; i++)
            {
              if (i < PlayerController.Instance.maxHealth)
              {
                heartContainers[i].SetActive(true);
              }
              else
              {
                heartContainers[i].SetActive(false);
              }
            }
        } 
         void SetFilledHearts(){
            for (int i = 0; i < heartFills.Length; i++)
            {
              if (i < PlayerController.Instance.health)
              {
                heartFills[i].fillAmount = 1;
              }
              else
              {
                heartFills[i].fillAmount = 0;
              }
            }
        } 
    
        void InstantiateHeartContainers(){
            for (int i = 0; i < PlayerController.Instance.maxHealth; i++)
            {
                GameObject temp = Instantiate(heartContainerPrefab);
                temp.transform.SetParent(heartsParent, false);
                heartContainers[i] = temp;
                heartFills[i] = temp.transform.Find("HeartFill").GetComponent<Image>();
            }
        }
    
        void UpdateHeartsHUD(){
            SetHeartContainers();
            SetFilledHearts();
        }
    }
    </code>

    Errors:

    After trying to heal:

    <code>
    MissingReferenceException: The object of type 'Image' has been destroyed but you are still trying to access it.
    Your script should either check if it is null or you should not destroy the object.
    UnityEngine.EventSystems.UIBehaviour.IsActive () (at ./Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/UIBehaviour.cs:28)
    UnityEngine.UI.Graphic.SetVerticesDirty () (at ./Library/PackageCache/com.unity.ugui@1.0.0/Runtime/UI/Core/Graphic.cs:295)
    UnityEngine.UI.Image.set_fillAmount (System.Single value) (at ./Library/PackageCache/com.unity.ugui@1.0.0/Runtime/UI/Core/Image.cs:505)
    PlayerController.set_Mana (System.Single value) (at Assets/Scripts/PlayerController.cs:444)
    PlayerController.Heal () (at Assets/Scripts/PlayerController.cs:430)
    PlayerController.Update () (at Assets/Scripts/PlayerController.cs:163)
    </code>

    After casting a spell:

    <code>
    MissingReferenceException: The object of type 'Image' has been destroyed but you are still trying to access it.
    Your script should either check if it is null or you should not destroy the object.
    UnityEngine.EventSystems.UIBehaviour.IsActive () (at ./Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/UIBehaviour.cs:28)
    UnityEngine.UI.Graphic.SetVerticesDirty () (at ./Library/PackageCache/com.unity.ugui@1.0.0/Runtime/UI/Core/Graphic.cs:295)
    UnityEngine.UI.Image.set_fillAmount (System.Single value) (at ./Library/PackageCache/com.unity.ugui@1.0.0/Runtime/UI/Core/Image.cs:505)
    PlayerController.set_Mana (System.Single value) (at Assets/Scripts/PlayerController.cs:444)
    PlayerController+<CastCoroutine>d__98.MoveNext () (at Assets/Scripts/PlayerController.cs:496)
    UnityEngine.SetupCoroutine.InvokeMoveNext (System.Collections.IEnumerator enumerator, System.IntPtr returnValueAddress) (at /Users/bokken/build/output/unity/unity/Runtime/Export/Scripting/Coroutines.cs:17)
    </code>
    #13343
    Terence
    Level 30
    Keymaster
    Helpful?
    Up
    0
    ::

    Hi Guillaume, your issues are in your set mana function in your PlayerController script. The manaStorage variable is empty after you change levels, causing the line to try and access null.fillAmount, causing a MissingReferenceException.

    float Mana {
        get {return mana;}
        set {
            if(mana != value) {
                mana = Mathf.Clamp(value, 0, 1);
                manaStorage.fillAmount = Mana;
            }
        }
    }

    This is occurring because when you switch levels, the UI elements from the old level (one of which manaStorage was pointing to) were deleted (and replaced with new ones) instead of being transferred over to the new level. I haven’t followed the Metroidvania tutorial entirely, so I’m not sure if the tutorial has you bring over the Canvas from the first level to another. Where does the video put the Canvas in?

    #13344
    Guillaume Lethug
    Level 1
    Participant
    Helpful?
    Up
    0
    ::

    Hello, Thanks you for your response. It appears that for the transition part later in the tutorial, there is a black fading feature, by adding this with UIManager, the canvas is keep between each scenes and the problem is resolved :)

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

Go to Login Page →


Advertisement below: