Forum begins after the advertisement:
[Part 7] Issues after changing Scene
Home › Forums › Video Game Tutorial Series › Creating a Metroidvania in Unity › [Part 7] Issues after changing Scene
- This topic has 6 replies, 3 voices, and was last updated 5 days, 23 hours ago by
Ser Apustaja.
-
AuthorPosts
-
May 4, 2025 at 12:27 am #18066::
When i go to other scene, and i take damage im always invincible and i cant take more damage, and when i press L key to die, after i go to other scene, the player stop and i cant move
MY PlayerController:
using System.Collections; using System.Collections.Generic; using Unity.VisualScripting; using UnityEngine; using UnityEngine.UIElements; using static TMPro.SpriteAssetUtilities.TexturePacker_JsonArray; using UnityEngine.UI; using UnityEngine.UIElements.Experimental; public class PlayerController : MonoBehaviour { [Header("Horizontal Movement Settings")] //header serve para criar categorias, no unity no inspector do player [SerializeField] private float walkSpeed = 1; [Space(5)] [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; //coyoteTime é pra qnd o player da miss no saldo ele ainda tem um pouco de tempo para saltar em vez de cair private int airJumpCounter = 0; [SerializeField] private int maxAirJumps; //airjump é o duble jumpe private float gravity; [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; private bool canDash = true; private bool dashed; [Space(5)] [Header("Attack")] [SerializeField] private Transform SideAttacktransform; [SerializeField] private Transform UpAttacktransform; [SerializeField] private Transform DownAttacktransform; [SerializeField] private Vector2 SideAttackArea, UpAttackArea, DownAttackArea; [SerializeField] private LayerMask attackableLayer; private bool attack = false; [SerializeField] private float timeBetwenAttack; private float timeSinceAttack; [SerializeField] private float damage; [SerializeField] private GameObject slashEffect; bool restoreTime; float restoreTimeSpeed; [Space(5)] [Header("Recoil")] [SerializeField] private int recoilXSteps = 5; [SerializeField] private int recoilYSteps = 5; [SerializeField] private float recoilXSpeed = 100; [SerializeField] private float recoilYSpeed = 100; private int stepsXRecoiled, stepsYRecoiled; [Space(5)] [Header("Health")] public int health; public int maxHealth; [SerializeField] GameObject bloodSpurt; [SerializeField] float hitFlashSpeed; public delegate void OnHealthChangedDelegate(); //delagete permite executares divesas funções de uma só vez [HideInInspector] public OnHealthChangedDelegate OnHealthChangedCallback; float healTimer; [SerializeField] float timeToHeal; [Space(5)] [Header("Mana")] [SerializeField] UnityEngine.UI.Image manaStorage; [SerializeField] float mana; [SerializeField] float manaDrainSpeed; [SerializeField] float manaGain; bool halfMana; [Space(5)] [Header("Spell")] //spell status [SerializeField] float manaSpellCost = 0.3f; [SerializeField] float timeBetweenCast = 0.5f; [SerializeField] float spellDamage; //so para o spell pra baixo e para cima [SerializeField] float downSpellForce; //spell cast objects [SerializeField] GameObject sideSpellFireball; [SerializeField] GameObject upSpellExplosion; [SerializeField] GameObject downSpellFireball; float timeSinceCast; float castOrHealtimer; [Space(5)] [Header("Camera")] [SerializeField] private float playerFallSpeedThreshold = -10; [HideInInspector]public PlayerStateList pState; public Rigidbody2D rb; private float xAxis, yAxis; private Animator anim; private SpriteRenderer sr; private bool canFlash = true; [SerializeField] private float maxFallSpeed = 10f; bool openMap; public static PlayerController Instance; private void Awake() { if (Instance != null && Instance != this) { Destroy (gameObject); } else { Instance = this; } DontDestroyOnLoad(gameObject); Health = maxHealth; } // Isto começa antes do primeiro frame update void Start() { pState = GetComponent<PlayerStateList>(); rb = GetComponent<Rigidbody2D>(); sr = GetComponent<SpriteRenderer>(); anim = GetComponent<Animator>(); gravity = rb.gravityScale; Mana = mana; manaStorage.fillAmount = Mana; } private void OnDrawGizmos() { Gizmos.color = Color.red; Gizmos.DrawWireCube(SideAttacktransform.position, SideAttackArea); Gizmos.DrawWireCube(UpAttacktransform.position, UpAttackArea); Gizmos.DrawWireCube(DownAttacktransform.position, DownAttackArea); } // Update é chamado 1 vez por segundo void Update() { //if (!canMove) return; //{ // rb.linearVelocityX = Input.GetAxisRaw("Horizontal"); // rb.linearVelocityY = Input.GetAxisRaw("Vertical"); //} if (pState.cutscene) return; if (pState.alive) { GetInputs(); ToggleMap(); } RestoreTimeScale(); UpdateJumpVariables(); UpdateCameraYDampForPlayerFall(); if (pState.Healing || pState.dashing) return; if (pState.alive) { Flip(); Move(); Jump(); StartDash(); Attack(); Heal(); CastSpell(); } FlashWhileInvincible(); if (Input.GetKeyDown(KeyCode.L)) { StartCoroutine(Death()); } } 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(); if (rb.linearVelocity.y < maxFallSpeed) { rb.linearVelocity = new Vector2(rb.linearVelocity.x, maxFallSpeed); } } 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; } } 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() { rb.linearVelocity = new Vector2(walkSpeed * xAxis, rb.linearVelocity.y); anim.SetBool("Walking", rb.linearVelocity.x != 0 && Grounded()); } void UpdateCameraYDampForPlayerFall() { if (rb.linearVelocityY < playerFallSpeedThreshold && !CameraManager.Instance.isLerpingYDamping && !CameraManager.Instance.hasLerpedYDamping) { StartCoroutine(CameraManager.Instance.LerpedYDamping(true)); } if (rb.linearVelocityY >= 0 && !CameraManager.Instance.isLerpingYDamping && CameraManager.Instance.isLerpingYDamping) { CameraManager.Instance.hasLerpedYDamping = false; StartCoroutine(CameraManager.Instance.LerpedYDamping(false)); } } void StartDash() { if (Input.GetButtonDown("Dash") && canDash && !dashed) { StartCoroutine(Dash()); dashed = true; } if (Grounded()) { dashed = false; } } IEnumerator Dash() { canDash = false; pState.dashing = true; //pState.invincible = true;//fica invincible anim.SetTrigger("Dashing"); rb.gravityScale = 0; //isto é para o player dar dash sem ser afetado pela gravidade duranto o dash int _dir = pState.lookingRight ? 1 : -1; rb.linearVelocity = new Vector2(_dir * dashSpeed, 0); //dar dash para o lado que o player está virado if (Grounded()) Instantiate(dashEffect, transform); //isto é pra qnd o player dar dash no chão faz o dasheffect yield return new WaitForSeconds(dashTime); //duração do dash rb.gravityScale = gravity; pState.dashing = false; //pState.invincible = false; // para de estar invincible yield return new WaitForSeconds(dashCooldown); //cooldown do dash canDash = true; } public IEnumerator WalkIntoNewScene(Vector2 _exitDir, float _delay) { pState.invincible = true; //se a saida é pra cima if (_exitDir.y > 0) { rb.linearVelocity = jumpForce * _exitDir; } //se a saida é na horizontal 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 >= timeBetwenAttack) { timeSinceAttack = 0; anim.SetTrigger("Attacking"); if (yAxis == 0 || yAxis < 0 && Grounded()) //isto é para ele dar o Side atk se n tiver a pressionar no "S" ou no "W" ou então se ele tiver a pressionar "S" mas tiver no chão { int _recoilLeftOrRight = pState.lookingRight ? 1 : -1; Hit(SideAttacktransform, SideAttackArea, ref pState.recoilingX, Vector2.right * _recoilLeftOrRight, recoilXSpeed); Instantiate(slashEffect, SideAttacktransform); } else if (yAxis > 0)//up atk { Hit(UpAttacktransform, UpAttackArea, ref pState.recoilingY, Vector2.up, recoilYSpeed); SlashEffectAtAngle(slashEffect, 80, UpAttacktransform); // os "90" representa os graus de rotação da figura } else if (yAxis < 0 && !Grounded())//down atk { Hit(DownAttacktransform, DownAttackArea, ref pState.recoilingY,Vector2.down, recoilYSpeed); SlashEffectAtAngle(slashEffect, -90, DownAttacktransform); } } } //alterei com o blackbox 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; // Recupera mana ao atacar um inimigo } } } } 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.linearVelocity = new Vector2(-recoilXSpeed, 0); } else { rb.linearVelocity = new Vector2 (recoilXSpeed, 0); } } if(pState.recoilingY) { rb.gravityScale = 0; //para a gravidade n afetar o recoil qnd damos um atk para baixo if(yAxis < 0)// so é utilizado esse recoil qnd o eixo do Y for maior q 0 ou seja, no ar { rb.linearVelocity = new Vector2(rb.linearVelocity.x, recoilYSpeed); } else { rb.linearVelocity = new Vector2(rb.linearVelocity.x, -recoilYSpeed); //qnd atacamos para cima temos o recoil para baixo } 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) { 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.2f); canFlash = true; } void FlashWhileInvincible() { //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.2 && canFlash) { StartCoroutine(Flash()); } } else { sr.enabled = true; } } void RestoreTimeScale() { if(restoreTime) { if (Time.timeScale < 1) //se o timeScale é menor q 1, sobe ate ao 1 { Time.timeScale += Time.unscaledDeltaTime * restoreTimeSpeed; } else { Time.timeScale = 1; restoreTime = false; } } } public void HitStopTime(float _newTimeScale, int _restoreSpeed, float _delay) //quando levo um hit o boneco para por um tempo { restoreTimeSpeed = _restoreSpeed; if (_delay > 0) { StopCoroutine(StartTimeAgain(_delay)); StartCoroutine(StartTimeAgain(_delay)); } else { restoreTime = true; } Time.timeScale = _newTimeScale; } IEnumerator StartTimeAgain(float _delay) { yield return new WaitForSecondsRealtime(_delay); restoreTime = true; } 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 WaitForSeconds(0.9f); StartCoroutine(UIManager.Instance.ActivateDeathScreen()); yield return new WaitForSeconds(0.9f); Instantiate(GameManager.Instance.shade, transform.position, Quaternion.identity); } public void Respawned() { if (!pState.alive) { pState.alive = true; halfMana = true; UIManager.Instance.SwitchMana(UIManager.ManaState.HalfMana); Mana = 0; Health = maxHealth; anim.Play("Player_Idle"); } } public void RestoreMana() { halfMana = false; UIManager.Instance.SwitchMana(UIManager.ManaState.FullMana); } 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.1f && Health < maxHealth && Mana > 0 && Grounded() && !pState.dashing) { pState.Healing = true; rb.linearVelocity = new Vector2(0, rb.linearVelocity.y); if (pState.Healing) { anim.SetBool("Healing", true); } //heal healTimer += Time.deltaTime; if(healTimer >= timeToHeal) { Health++; healTimer = 0; } Mana -= Time.deltaTime * manaDrainSpeed; } else { pState.Healing = false; anim.SetBool("Healing", false); healTimer = 0; } } float Mana { get { return mana; } set { if(mana != value) { if (!halfMana) { mana = Mathf.Clamp(value, 0, 1); } else { mana = Mathf.Clamp(value, 0, 0.5f); } manaStorage.fillAmount = Mana; } } } void CastSpell() { if (Input.GetButtonUp("Cast/Heal")) { castOrHealtimer = 0; } if (Input.GetButtonUp("Cast/Heal") && castOrHealtimer <= 0.1f && timeSinceCast >= timeBetweenCast && Mana >= manaSpellCost) { pState.casting = true; timeSinceCast = 0; StartCoroutine(CastCoroutine()); } else { timeSinceCast += Time.deltaTime; } if (Grounded()) { //desativa o downcast se ta grounded downSpellFireball.SetActive(false); } //se o down spell for ativo ele so para quando chegar ao cão if (downSpellFireball.activeInHierarchy) { rb.linearVelocity += downSpellForce * Vector2.down; } } IEnumerator CastCoroutine() { anim.SetBool("Casting", true); yield return new WaitForSeconds(0.09f); //side cast 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); } pState.recoilingX = true; } //up cast else if (yAxis > 0) { Instantiate(upSpellExplosion, transform); rb.linearVelocity = Vector2.zero; } //down cast else if (yAxis < 0 && !Grounded()) { downSpellFireball.SetActive(true); } Mana -= manaSpellCost; yield return new WaitForSeconds(0.20f); anim.SetBool("Casting", false); maxFallSpeed = -100f; } 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.linearVelocity = new Vector3(rb.linearVelocity.x, jumpForce); pState.jumping = true; } if (!Grounded() && airJumpCounter < maxAirJumps && Input.GetButtonDown("Jump")) { pState.jumping = true; airJumpCounter++; rb.linearVelocity = new Vector3(rb.linearVelocity.x, jumpForce); } //isto é para quanto mais carregas no espaço mal alto saltas if (Input.GetButtonUp("Jump") && rb.linearVelocity.y > 3) { pState.jumping = false; rb.linearVelocity = new Vector2(rb.linearVelocity.x, 0); } anim.SetBool("Jumping", !Grounded()); } //mete o jumpBufferCounter ao max do jumpBufferFrames e reduz em 1 cada frame ou seja tornar o salto mais smooth void UpdateJumpVariables() { if (Grounded()) { pState.jumping = false; coyoteTimeCounter = coyoteTime; airJumpCounter = 0; } else { //object time.deltaTime é a quantidade de tempo entre cada frame coyoteTimeCounter -= Time.deltaTime; //isto faz com que o coyoteTimeCounter -1 a cada segundo } if (Input.GetButtonDown("Jump")) { jumpBufferCounter = jumpBufferFrames; } else { jumpBufferCounter--; } } }using System.Collections; using System.Collections.Generic; using Unity.VisualScripting; using UnityEngine; using UnityEngine.UIElements; using static TMPro.SpriteAssetUtilities.TexturePacker_JsonArray; using UnityEngine.UI; using UnityEngine.UIElements.Experimental; public class PlayerController : MonoBehaviour { [Header("Horizontal Movement Settings")] //header serve para criar categorias, no unity no inspector do player [SerializeField] private float walkSpeed = 1; [Space(5)] [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; //coyoteTime é pra qnd o player da miss no saldo ele ainda tem um pouco de tempo para saltar em vez de cair private int airJumpCounter = 0; [SerializeField] private int maxAirJumps; //airjump é o duble jumpe private float gravity; [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; private bool canDash = true; private bool dashed; [Space(5)] [Header("Attack")] [SerializeField] private Transform SideAttacktransform; [SerializeField] private Transform UpAttacktransform; [SerializeField] private Transform DownAttacktransform; [SerializeField] private Vector2 SideAttackArea, UpAttackArea, DownAttackArea; [SerializeField] private LayerMask attackableLayer; private bool attack = false; [SerializeField] private float timeBetwenAttack; private float timeSinceAttack; [SerializeField] private float damage; [SerializeField] private GameObject slashEffect; bool restoreTime; float restoreTimeSpeed; [Space(5)] [Header("Recoil")] [SerializeField] private int recoilXSteps = 5; [SerializeField] private int recoilYSteps = 5; [SerializeField] private float recoilXSpeed = 100; [SerializeField] private float recoilYSpeed = 100; private int stepsXRecoiled, stepsYRecoiled; [Space(5)] [Header("Health")] public int health; public int maxHealth; [SerializeField] GameObject bloodSpurt; [SerializeField] float hitFlashSpeed; public delegate void OnHealthChangedDelegate(); //delagete permite executares divesas funções de uma só vez [HideInInspector] public OnHealthChangedDelegate OnHealthChangedCallback; float healTimer; [SerializeField] float timeToHeal; [Space(5)] [Header("Mana")] [SerializeField] UnityEngine.UI.Image manaStorage; [SerializeField] float mana; [SerializeField] float manaDrainSpeed; [SerializeField] float manaGain; bool halfMana; [Space(5)] [Header("Spell")] //spell status [SerializeField] float manaSpellCost = 0.3f; [SerializeField] float timeBetweenCast = 0.5f; [SerializeField] float spellDamage; //so para o spell pra baixo e para cima [SerializeField] float downSpellForce; //spell cast objects [SerializeField] GameObject sideSpellFireball; [SerializeField] GameObject upSpellExplosion; [SerializeField] GameObject downSpellFireball; float timeSinceCast; float castOrHealtimer; [Space(5)] [Header("Camera")] [SerializeField] private float playerFallSpeedThreshold = -10; [HideInInspector]public PlayerStateList pState; public Rigidbody2D rb; private float xAxis, yAxis; private Animator anim; private SpriteRenderer sr; private bool canFlash = true; [SerializeField] private float maxFallSpeed = 10f; bool openMap; public static PlayerController Instance; private void Awake() { if (Instance != null && Instance != this) { Destroy (gameObject); } else { Instance = this; } DontDestroyOnLoad(gameObject); Health = maxHealth; } // Isto começa antes do primeiro frame update void Start() { pState = GetComponent<PlayerStateList>(); rb = GetComponent<Rigidbody2D>(); sr = GetComponent<SpriteRenderer>(); anim = GetComponent<Animator>(); gravity = rb.gravityScale; Mana = mana; manaStorage.fillAmount = Mana; } private void OnDrawGizmos() { Gizmos.color = Color.red; Gizmos.DrawWireCube(SideAttacktransform.position, SideAttackArea); Gizmos.DrawWireCube(UpAttacktransform.position, UpAttackArea); Gizmos.DrawWireCube(DownAttacktransform.position, DownAttackArea); } // Update é chamado 1 vez por segundo void Update() { //if (!canMove) return; //{ // rb.linearVelocityX = Input.GetAxisRaw("Horizontal"); // rb.linearVelocityY = Input.GetAxisRaw("Vertical"); //} if (pState.cutscene) return; if (pState.alive) { GetInputs(); ToggleMap(); } RestoreTimeScale(); UpdateJumpVariables(); UpdateCameraYDampForPlayerFall(); if (pState.Healing || pState.dashing) return; if (pState.alive) { Flip(); Move(); Jump(); StartDash(); Attack(); Heal(); CastSpell(); } FlashWhileInvincible(); if (Input.GetKeyDown(KeyCode.L)) { StartCoroutine(Death()); } } 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(); if (rb.linearVelocity.y < maxFallSpeed) { rb.linearVelocity = new Vector2(rb.linearVelocity.x, maxFallSpeed); } } 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; } } 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() { rb.linearVelocity = new Vector2(walkSpeed * xAxis, rb.linearVelocity.y); anim.SetBool("Walking", rb.linearVelocity.x != 0 && Grounded()); } void UpdateCameraYDampForPlayerFall() { if (rb.linearVelocityY < playerFallSpeedThreshold && !CameraManager.Instance.isLerpingYDamping && !CameraManager.Instance.hasLerpedYDamping) { StartCoroutine(CameraManager.Instance.LerpedYDamping(true)); } if (rb.linearVelocityY >= 0 && !CameraManager.Instance.isLerpingYDamping && CameraManager.Instance.isLerpingYDamping) { CameraManager.Instance.hasLerpedYDamping = false; StartCoroutine(CameraManager.Instance.LerpedYDamping(false)); } } void StartDash() { if (Input.GetButtonDown("Dash") && canDash && !dashed) { StartCoroutine(Dash()); dashed = true; } if (Grounded()) { dashed = false; } } IEnumerator Dash() { canDash = false; pState.dashing = true; //pState.invincible = true;//fica invincible anim.SetTrigger("Dashing"); rb.gravityScale = 0; //isto é para o player dar dash sem ser afetado pela gravidade duranto o dash int _dir = pState.lookingRight ? 1 : -1; rb.linearVelocity = new Vector2(_dir * dashSpeed, 0); //dar dash para o lado que o player está virado if (Grounded()) Instantiate(dashEffect, transform); //isto é pra qnd o player dar dash no chão faz o dasheffect yield return new WaitForSeconds(dashTime); //duração do dash rb.gravityScale = gravity; pState.dashing = false; //pState.invincible = false; // para de estar invincible yield return new WaitForSeconds(dashCooldown); //cooldown do dash canDash = true; } public IEnumerator WalkIntoNewScene(Vector2 _exitDir, float _delay) { pState.invincible = true; //se a saida é pra cima if (_exitDir.y > 0) { rb.linearVelocity = jumpForce * _exitDir; } //se a saida é na horizontal 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 >= timeBetwenAttack) { timeSinceAttack = 0; anim.SetTrigger("Attacking"); if (yAxis == 0 || yAxis < 0 && Grounded()) //isto é para ele dar o Side atk se n tiver a pressionar no "S" ou no "W" ou então se ele tiver a pressionar "S" mas tiver no chão { int _recoilLeftOrRight = pState.lookingRight ? 1 : -1; Hit(SideAttacktransform, SideAttackArea, ref pState.recoilingX, Vector2.right * _recoilLeftOrRight, recoilXSpeed); Instantiate(slashEffect, SideAttacktransform); } else if (yAxis > 0)//up atk { Hit(UpAttacktransform, UpAttackArea, ref pState.recoilingY, Vector2.up, recoilYSpeed); SlashEffectAtAngle(slashEffect, 80, UpAttacktransform); // os "90" representa os graus de rotação da figura } else if (yAxis < 0 && !Grounded())//down atk { Hit(DownAttacktransform, DownAttackArea, ref pState.recoilingY,Vector2.down, recoilYSpeed); SlashEffectAtAngle(slashEffect, -90, DownAttacktransform); } } } //alterei com o blackbox 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; // Recupera mana ao atacar um inimigo } } } } 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.linearVelocity = new Vector2(-recoilXSpeed, 0); } else { rb.linearVelocity = new Vector2 (recoilXSpeed, 0); } } if(pState.recoilingY) { rb.gravityScale = 0; //para a gravidade n afetar o recoil qnd damos um atk para baixo if(yAxis < 0)// so é utilizado esse recoil qnd o eixo do Y for maior q 0 ou seja, no ar { rb.linearVelocity = new Vector2(rb.linearVelocity.x, recoilYSpeed); } else { rb.linearVelocity = new Vector2(rb.linearVelocity.x, -recoilYSpeed); //qnd atacamos para cima temos o recoil para baixo } 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) { 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.2f); canFlash = true; } void FlashWhileInvincible() { //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.2 && canFlash) { StartCoroutine(Flash()); } } else { sr.enabled = true; } } void RestoreTimeScale() { if(restoreTime) { if (Time.timeScale < 1) //se o timeScale é menor q 1, sobe ate ao 1 { Time.timeScale += Time.unscaledDeltaTime * restoreTimeSpeed; } else { Time.timeScale = 1; restoreTime = false; } } } public void HitStopTime(float _newTimeScale, int _restoreSpeed, float _delay) //quando levo um hit o boneco para por um tempo { restoreTimeSpeed = _restoreSpeed; if (_delay > 0) { StopCoroutine(StartTimeAgain(_delay)); StartCoroutine(StartTimeAgain(_delay)); } else { restoreTime = true; } Time.timeScale = _newTimeScale; } IEnumerator StartTimeAgain(float _delay) { yield return new WaitForSecondsRealtime(_delay); restoreTime = true; } 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 WaitForSeconds(0.9f); StartCoroutine(UIManager.Instance.ActivateDeathScreen()); yield return new WaitForSeconds(0.9f); Instantiate(GameManager.Instance.shade, transform.position, Quaternion.identity); } public void Respawned() { if (!pState.alive) { pState.alive = true; halfMana = true; UIManager.Instance.SwitchMana(UIManager.ManaState.HalfMana); Mana = 0; Health = maxHealth; anim.Play("Player_Idle"); } } public void RestoreMana() { halfMana = false; UIManager.Instance.SwitchMana(UIManager.ManaState.FullMana); } 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.1f && Health < maxHealth && Mana > 0 && Grounded() && !pState.dashing) { pState.Healing = true; rb.linearVelocity = new Vector2(0, rb.linearVelocity.y); if (pState.Healing) { anim.SetBool("Healing", true); } //heal healTimer += Time.deltaTime; if(healTimer >= timeToHeal) { Health++; healTimer = 0; } Mana -= Time.deltaTime * manaDrainSpeed; } else { pState.Healing = false; anim.SetBool("Healing", false); healTimer = 0; } } float Mana { get { return mana; } set { if(mana != value) { if (!halfMana) { mana = Mathf.Clamp(value, 0, 1); } else { mana = Mathf.Clamp(value, 0, 0.5f); } manaStorage.fillAmount = Mana; } } } void CastSpell() { if (Input.GetButtonUp("Cast/Heal")) { castOrHealtimer = 0; } if (Input.GetButtonUp("Cast/Heal") && castOrHealtimer <= 0.1f && timeSinceCast >= timeBetweenCast && Mana >= manaSpellCost) { pState.casting = true; timeSinceCast = 0; StartCoroutine(CastCoroutine()); } else { timeSinceCast += Time.deltaTime; } if (Grounded()) { //desativa o downcast se ta grounded downSpellFireball.SetActive(false); } //se o down spell for ativo ele so para quando chegar ao cão if (downSpellFireball.activeInHierarchy) { rb.linearVelocity += downSpellForce * Vector2.down; } } IEnumerator CastCoroutine() { anim.SetBool("Casting", true); yield return new WaitForSeconds(0.09f); //side cast 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); } pState.recoilingX = true; } //up cast else if (yAxis > 0) { Instantiate(upSpellExplosion, transform); rb.linearVelocity = Vector2.zero; } //down cast else if (yAxis < 0 && !Grounded()) { downSpellFireball.SetActive(true); } Mana -= manaSpellCost; yield return new WaitForSeconds(0.20f); anim.SetBool("Casting", false); maxFallSpeed = -100f; } 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.linearVelocity = new Vector3(rb.linearVelocity.x, jumpForce); pState.jumping = true; } if (!Grounded() && airJumpCounter < maxAirJumps && Input.GetButtonDown("Jump")) { pState.jumping = true; airJumpCounter++; rb.linearVelocity = new Vector3(rb.linearVelocity.x, jumpForce); } //isto é para quanto mais carregas no espaço mal alto saltas if (Input.GetButtonUp("Jump") && rb.linearVelocity.y > 3) { pState.jumping = false; rb.linearVelocity = new Vector2(rb.linearVelocity.x, 0); } anim.SetBool("Jumping", !Grounded()); } //mete o jumpBufferCounter ao max do jumpBufferFrames e reduz em 1 cada frame ou seja tornar o salto mais smooth void UpdateJumpVariables() { if (Grounded()) { pState.jumping = false; coyoteTimeCounter = coyoteTime; airJumpCounter = 0; } else { //object time.deltaTime é a quantidade de tempo entre cada frame coyoteTimeCounter -= Time.deltaTime; //isto faz com que o coyoteTimeCounter -1 a cada segundo } if (Input.GetButtonDown("Jump")) { jumpBufferCounter = jumpBufferFrames; } else { jumpBufferCounter--; } } }
May 4, 2025 at 12:37 am #18067::It seems like you’re encountering two issues with your
PlayerController
script:- Player remains invincible after taking damage: This indicates that the
pState.invincible
flag is not being reset correctly after the player takes damage. - Player stops moving after respawning: This could be due to the player state not being properly initialized or set when the player respawns.
Here are a few suggestions to troubleshoot and fix these issues:
1. Handling Invincibility
Make sure that the invincibility state is reset when the player respawns or after a certain duration. Check the
StopTakingDamage
coroutine where you setpState.invincible
tofalse
. Ensure that there’s no other part of your code that inadvertently setspState.invincible
totrue
again.You could add debug logs to track when
pState.invincible
is being set to true and false:IEnumerator StopTakingDamage() { pState.invincible = true; Debug.Log("Player is now invincible"); GameObject _bloodSpurtParticles = Instantiate(bloodSpurt, transform.position, Quaternion.identity); Destroy(_bloodSpurtParticles, 1.5f); anim.SetTrigger("TakeDamage"); yield return new WaitForSeconds(1f); pState.invincible = false; Debug.Log("Player is no longer invincible"); }
2. Movement After Respawning
When you respawn, ensure that all necessary player states are properly initialized. Check the
Respawned
method and ensure that you are resetting the player’s state correctly:public void Respawned() { if (!pState.alive) { pState.alive = true; halfMana = true; UIManager.Instance.SwitchMana(UIManager.ManaState.HalfMana); Mana = 0; Health = maxHealth; // Ensure health is set to max rb.velocity = Vector2.zero; // Reset velocity to prevent sticking anim.Play("Player_Idle"); } }
Make sure to reset the player’s velocity when respawning, as shown above. This will prevent the player from being stuck in place if they were moving before dying.
Additional Considerations
- Debugging Movement: Add logs in the
Update
method to check if the input is being received after respawning:if (pState.alive) { Debug.Log("Player is alive and receiving input"); GetInputs(); //...rest of your code }
- Cutscene Handling: If the player is in a cutscene (
pState.cutscene
), ensure that the cutscene logic is correctly allowing player movement after the cutscene ends.
Summary
By adding debug logs and ensuring states are reset correctly, you should be able to identify why the player remains invincible and why they cannot move after respawning. Make sure to check for any conditions that might inadvertently keep
pState.invincible
true and ensure the player’s state is fully reset upon respawn.May 5, 2025 at 1:21 am #18068::Hi Rafael, for the player not moving, can you add the following line and let me know if it shows up on the Console when you take damage?
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); print("Turning player's invincibility off"); pState.invincible = false; }
As for the player not moving when in a new Scene, after transitioning to your new Scene, in your Hierarchy, type
t:PlayerController
and see if anything shows up on the Hierarchy. I suspect there may not be a PlayerController in your other Scenes, causing this issue.May 6, 2025 at 6:11 pm #18110::it shows on the console “Missing ReferenceException: The object of type ‘UnityEngine.GameObject’ 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. Parameter name: data”, why its bieng destroyed after going to other scene
May 6, 2025 at 6:15 pm #18111::The “Missing ReferenceException” you’re encountering typically occurs when you try to access a GameObject that has been destroyed or is no longer available in the current scene. This can happen if you have a reference to a GameObject that is destroyed when you load a new scene.
Here are a few things to check:
-
Check for Destroyed Objects: Ensure that you are not trying to access any GameObjects that are destroyed when transitioning between scenes. You can add null checks before accessing any GameObject references.
For example:
if (myGameObject != null) { // Access myGameObject }
-
DontDestroyOnLoad: If you want certain GameObjects to persist across scenes, make sure you are using
DontDestroyOnLoad(gameObject)
correctly in your scripts. This will prevent them from being destroyed when loading a new scene. -
Scene Management: Review your scene transition logic. If you are destroying objects that are still referenced in your scripts, you will encounter this error. Make sure to manage your references properly when changing scenes.
-
Debugging: Add debug logs to track when and where the GameObject is being destroyed. This can help you identify if the destruction is happening earlier than expected.
Could you provide more details about which GameObject is being referenced when the error occurs? This will help in diagnosing the issue more accurately.
May 6, 2025 at 6:44 pm #18112::Which is the script that is triggering a MissingReferenceException? Can you pinpoint the script and the line that is causing the error for me?
If we fix that, we may very well fix your issue.
May 6, 2025 at 6:46 pm #18113::Its on Update
void Update() { if (pState.cutscene) return; if (pState.alive) { GetInputs(); ToggleMap(); } RestoreTimeScale(); UpdateJumpVariables(); UpdateCameraYDampForPlayerFall(); if (pState.Healing || pState.dashing) return; if (pState.alive) { Flip(); Move(); Jump(); StartDash(); Attack(); Heal(); CastSpell(); } FlashWhileInvincible(); if (Input.GetKeyDown(KeyCode.L)) { StartCoroutine(Death()); //here its the error } } i think that maybe on the IEnumerator death is missing something to not destroy, 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 WaitForSeconds(0.9f); StartCoroutine(UIManager.Instance.ActivateDeathScreen()); yield return new WaitForSeconds(0.9f); Instantiate(GameManager.Instance.shade, transform.position, Quaternion.identity); }
the same error shows when i take damage (on
StartCoroutine(StopTakingDamage());
), or press L key to die (OnStartCoroutine(Death());
)May 6, 2025 at 6:51 pm #18114::The error you’re encountering, “Missing ReferenceException: The object of type ‘UnityEngine.GameObject’ has been destroyed but you are still trying to access it,” likely stems from the
Death
coroutine or theUpdate
method trying to access a GameObject that has already been destroyed.Here are a few points to consider:
-
Check
bloodSpurt
Reference: Ensure that thebloodSpurt
GameObject is properly assigned in the inspector and that it exists when theDeath
coroutine is called. IfbloodSpurt
is destroyed or not assigned, trying to instantiate it will lead to issues. -
Check
anim
Reference: Make sure that theanim
(Animator) reference is valid and not destroyed when you callanim.SetTrigger("Death");
. If the Animator component is on a GameObject that gets destroyed, this will cause the error. -
Coroutine Execution: If the
Death
coroutine is called while the player is already in the process of dying (e.g., if theDeath
coroutine is still running), it may lead to multiple calls that can cause references to be lost. You might want to add a check to prevent the coroutine from being called again if the player is already dead. -
Null Checks: Before accessing any GameObject or component, you can add null checks to ensure they are still valid. For example:
if (bloodSpurt != null) { GameObject _bloodSpurtParticles = Instantiate(bloodSpurt, transform.position, Quaternion.identity); Destroy(_bloodSpurtParticles, 1.5f); }
-
Debugging: Add debug logs to track the state of your GameObjects and components before they are accessed. This can help identify what is null or destroyed at the time of the error.
Here’s a modified version of your
Death
coroutine with added null checks:IEnumerator Death () { pState.alive = false; Time.timeScale = 1f; if (bloodSpurt != null) { GameObject _bloodSpurtParticles = Instantiate(bloodSpurt, transform.position, Quaternion.identity); Destroy(_bloodSpurtParticles, 1.5f); } else { Debug.LogWarning("bloodSpurt is null!"); } if (anim != null) { anim.SetTrigger("Death"); } else { Debug.LogWarning("Animator is null!"); } yield return new WaitForSeconds(0.9f); StartCoroutine(UIManager.Instance.ActivateDeathScreen()); yield return new WaitForSeconds(0.9f); if (GameManager.Instance.shade != null) { Instantiate(GameManager.Instance.shade, transform.position, Quaternion.identity); } else { Debug.LogWarning("shade is null!"); } }
This should help you identify the source of the error. If the issue persists, could you provide more details about the
bloodSpurt
,anim
, andGameManager.Instance.shade
references?has upvoted this post. May 6, 2025 at 7:01 pm #18117May 6, 2025 at 7:05 pm #18118::Yes, applying the same null checks and safety measures in your
TakeDamage
method and any other coroutines is a good practice. This will help prevent the “Missing ReferenceException” from occurring when you try to access destroyed GameObjects or components.Here’s how you can implement similar checks in your
TakeDamage
method: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; if (bloodSpurt != null) { GameObject _bloodSpurtParticles = Instantiate(bloodSpurt, transform.position, Quaternion.identity); Destroy(_bloodSpurtParticles, 1.5f); } else { Debug.LogWarning("bloodSpurt is null!"); } if (anim != null) { anim.SetTrigger("TakeDamage"); } else { Debug.LogWarning("Animator is null!"); } yield return new WaitForSeconds(1f); pState.invincible = false; }
Additional Recommendations:
-
Centralized Error Handling: If you find that many coroutines are causing similar issues, consider creating a centralized method for handling instantiation and destruction of GameObjects. This method can include null checks and logging.
-
State Management: Ensure that your player state (like
pState.alive
) is managed correctly throughout the game. If the player is dead, you should prevent any further actions that could lead to accessing destroyed objects. -
Coroutines Management: If you have multiple coroutines that can be called in quick succession, consider using a boolean flag to prevent them from being called again while one is already running. For example:
private bool isTakingDamage = false; IEnumerator StopTakingDamage() { if (isTakingDamage) yield break; // Prevents re-entry isTakingDamage = true; // Your existing code... isTakingDamage = false; // Reset flag after execution }
Clarifying Question:
Are there any specific coroutines or methods other than
TakeDamage
that you are concerned about, or would you like to review how to implement these checks in other parts of your code?has upvoted this post. May 6, 2025 at 7:11 pm #18119May 6, 2025 at 7:15 pm #18120::It’s great to hear that you were able to identify the issue with the blood spurt and that implementing the null checks resolved the problem!
Using the same approach of checking for null references before instantiating or accessing GameObjects is a solid strategy to prevent runtime errors. This practice will help ensure that your game runs smoothly without unexpected crashes or exceptions.
If you have any other specific areas of your code that you’re concerned about or if there are additional features you want to implement, feel free to share! Is there anything else you would like assistance with regarding your game development?
has upvoted this post. - Player remains invincible after taking damage: This indicates that the
-
AuthorPosts
- You must be logged in to reply to this topic.
Advertisement below: