Forum begins after the advertisement:
[Part 7] my player can’t die
Home › Forums › Video Game Tutorial Series › Creating a Metroidvania in Unity › [Part 7] my player can’t die
- This topic has 11 replies, 2 voices, and was last updated 3 weeks, 5 days ago by Terence.
-
AuthorPosts
-
November 20, 2024 at 4:30 am #16434::
this is my my controller code
<code>using System.Collections; using System.Collections.Generic; using UnityEngine; 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 float jumpBufferCounter = 0; //stores the jump button input [SerializeField] private float 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 [SerializeField] private float timeBetweenAttack; private float timeSinceAttack; [SerializeField] private float damage; //the damage the player does to an enemy [SerializeField] private GameObject slashEffect; //the effect of the slashs 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; [Space(5)] [HideInInspector] public PlayerStateList pState; private Animator anim; private Rigidbody2D rb; private SpriteRenderer sr; //Input Variables private float xAxis, yAxis; private bool attack = false; private bool canFlash = true; //creates a singleton of the PlayerController public static PlayerController Instance; private void Awake() { if (Instance != null && Instance != this) { Destroy(gameObject); } else { Instance = this; } Health = maxHealth; } // Start is called before the first frame update void Start() { pState = GetComponent<PlayerStateList>(); rb = GetComponent<Rigidbody2D>(); sr = GetComponent<SpriteRenderer>(); anim = GetComponent<Animator>(); gravity = rb.gravityScale; } 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; GetInputs(); UpdateJumpVariables(); RestoreTimeScale(); if (pState.dashing) return; Flip(); Move(); Jump(); StartDash(); Attack(); RestoreTimeScale(); FlashWhileInvincible(); } 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"); } void Flip() { if (xAxis < 0) { transform.localScale = new Vector2(-Mathf.Abs(transform.localScale.x), transform.localScale.y); pState.lookingRight = false; } else if (xAxis > 0) { transform.localScale = new Vector2(Mathf.Abs(transform.localScale.x), 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; rb.velocity = new Vector2(transform.localScale.x * 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 directions 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() { timeSinceAttack += Time.deltaTime; if (attack && timeSinceAttack >= timeBetweenAttack) { timeSinceAttack = 0; anim.SetTrigger("Attacking"); if (yAxis == 0 || yAxis < 0 && Grounded()) { int _recoilLeftOrRight = pState.lookingRight ? 1 : -1; 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); List<Enemy> hitEnemies = new List<Enemy>(); if (objectsToHit.Length > 0) { _recoilBool = true; } for (int i = 0; i < objectsToHit.Length; i++) { if (objectsToHit[i].GetComponent<Enemy>() != null) { objectsToHit[i].GetComponent<Enemy>().EnemyGetsHit(damage, _recoilDir, _recoilStrength); } } } void SlashEffectAtAngle(GameObject _slashEffect, int _effectAngle, Transform _attackTransform) { if (_slashEffect != null && _attackTransform != null) { // Instantiate slash effect GameObject slashInstance = Instantiate(_slashEffect, _attackTransform.position, Quaternion.identity); // Set rotasi dan scale slashInstance.transform.eulerAngles = new Vector3(0, 0, _effectAngle); slashInstance.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) { Health -= Mathf.RoundToInt(_damage); 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 && !pState.cutscene) { if (Time.timeScale > 0.2 && canFlash) { StartCoroutine(Flash()); } } else { sr.enabled = true; } } void RestoreTimeScale() { if (restoreTime) { if (Time.timeScale < 1) { Time.timeScale += Time.unscaledDeltaTime * 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) { 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); } public int Health { get { return health; } set { if (health != value) { health = Mathf.Clamp(value, 0, maxHealth); if (onHealthChangedCallback != null) { onHealthChangedCallback.Invoke(); } } } } 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) { if (jumpBufferCounter > 0 && coyoteTimeCounter > 0) { rb.velocity = new Vector3(rb.velocity.x, jumpForce); pState.jumping = true; } else 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 > 0) { 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 = jumpBufferCounter - Time.deltaTime * 10; } } }</code>
my main enemy script
<code>using System.Collections; using System.Collections.Generic; using Unity.VisualScripting; using UnityEngine; public class Enemy : MonoBehaviour { [SerializeField] protected float health; [SerializeField] protected float recoilLength; [SerializeField] protected float recoilFactor; [SerializeField] protected bool isRecoiling = false; [SerializeField] protected PlayerController player; [SerializeField] protected float speed; [SerializeField] protected float damage; [SerializeField] protected GameObject WhiteBlood; protected float recoilTimer; protected Rigidbody2D rb; protected SpriteRenderer sr; protected Animator anim; protected enum EnemyStates { //Centipede Centipede_idle, Centipede_Flip, //Bat Bat_Idle, Bat_Chase, Bat_Stunned, Bat_Death, } protected EnemyStates currentEnemyState; protected virtual EnemyStates GetCurrentEnemyState { get { return currentEnemyState; } set { if (currentEnemyState != value) { currentEnemyState = value; ChangeCurrentAnimation(); } } } // Start is called before the first frame update protected virtual void Start() { rb = GetComponent<Rigidbody2D>(); sr = GetComponent<SpriteRenderer>(); anim = GetComponent<Animator>(); } protected virtual void Awake() { rb = GetComponent<Rigidbody2D>(); player = PlayerController.Instance; } // Update is called once per frame protected virtual void Update() { if(isRecoiling) { if(recoilTimer < recoilLength) { recoilTimer += Time.deltaTime; } else { isRecoiling = false; recoilTimer = 0; } } else { UpdateEnemyStates(); } } public virtual void EnemyGetsHit(float _damageDone, Vector2 _hitDirection, float _hitForce) { health -= _damageDone; if(!isRecoiling) { GameObject _WhiteBlood = Instantiate(WhiteBlood, transform.position, Quaternion.identity); Destroy(_WhiteBlood, 5.5f); rb.velocity = _hitForce * recoilFactor * _hitDirection; } } protected void OnTriggerStay2D(Collider2D _other) { if(_other.CompareTag("Player") && !PlayerController.Instance.pState.invincible && health > 0) { Attack(); PlayerController.Instance.HitStopTime(0, 5, 0.5f); } } protected virtual void UpdateEnemyStates() { } protected virtual void ChangeCurrentAnimation() { } protected void ChangeState(EnemyStates _newState) { GetCurrentEnemyState = _newState; } protected virtual void Attack() { PlayerController.Instance.TakeDamage(damage); } protected virtual void Death(float _destroyTime) { Destroy(gameObject, _destroyTime); } }</code>
my crawler script
<code>using System.Collections; using System.Collections.Generic; using UnityEngine; public class Centipede : Enemy { [SerializeField] private float flipWaitTime; [SerializeField] private float ledgeCheckX; [SerializeField] private float ledgeCheckY; [SerializeField] private LayerMask whatIsGround; float timer; // Start is called before the first frame update protected override void Start() { base.Start(); rb.gravityScale = 12f; } protected override void Awake() { base.Awake(); } // Update is called once per frame protected override void UpdateEnemyStates() { if(health <= 0) { Death(0.05f); } switch (GetCurrentEnemyState) { case EnemyStates.Centipede_idle: Vector3 _ledgeCheckStart = transform.localScale.x > 0 ? new Vector3(ledgeCheckX, 0) : new Vector3(-ledgeCheckX, 0); Vector2 _wallCheckDir = transform.localScale.x > 0 ? transform.right : -transform.right; if (!Physics2D.Raycast(transform.position + _ledgeCheckStart, Vector2.down, ledgeCheckY, whatIsGround) || Physics2D.Raycast(transform.position, _wallCheckDir, ledgeCheckX, whatIsGround)) { ChangeState(EnemyStates.Centipede_Flip); } if (transform.localScale.x > 0) { rb.velocity = new Vector2(speed, rb.velocity.y); } else { rb.velocity = new Vector2 (-speed, rb.velocity.y); } break; case EnemyStates.Centipede_Flip: timer += Time.deltaTime; if (timer > flipWaitTime) { timer = 0; transform.localScale = new Vector2(transform.localScale.x * -1, transform.localScale.y); ChangeState(EnemyStates.Centipede_idle); } break; } if (health <= 0) { Death(0.05f); } } }</code>
November 20, 2024 at 11:06 am #16445November 20, 2024 at 11:59 am #16451November 20, 2024 at 12:40 pm #16454::You need to update your
TakeDamage()
function as follows:public void TakeDamage(float _damage) {
Health -= Mathf.RoundToInt(_damage); StartCoroutine(StopTakingDamage());if(pState.alive) { Health -= Mathf.RoundToInt(_damage); if(Health <= 0) { Health = 0; StartCoroutine(Death()); } else { StartCoroutine(StopTakingDamage()); } } }November 21, 2024 at 12:49 pm #16471November 21, 2024 at 3:12 pm #16473::Add the following lines to your code and show me your Console when your player is supposed to die:
public void TakeDamage(float _damage) { print("TakeDamage() called"); if(pState.alive) { print("TakeDamage() reduce health"); Health -= Mathf.RoundToInt(_damage); if(Health <= 0) { Health = 0; print("Calling Death()"); StartCoroutine(Death()); } else { StartCoroutine(StopTakingDamage()); } } }
IEnumerator Death() { print("Death coroutine"); 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); }
November 21, 2024 at 4:05 pm #16476November 24, 2024 at 1:10 pm #16511November 24, 2024 at 3:08 pm #16515::You need to check if the messages I suggested above print in the Console. Or I won’t be able to help you.
November 25, 2024 at 3:17 am #16538November 25, 2024 at 8:18 pm #16546::this message from console
TakeDamage() called UnityEngine.MonoBehaviour:print (object) PlayerController:TakeDamage (single) (at Assets/Script/PlayerControl.cs:427) Enemy:Attack () (at Assets/Script/Enemy.cs:123) Enemy:OnCollisionStay2D (UnityEngine.Collision2D) (at Assets/Script/Enemy.cs:106)
and this message error from console
NullReferenceException: Object reference not set to an instance of an object Bat.Start () (at Assets/Script/Bat.cs:15)
November 26, 2024 at 1:09 pm #16548 -
AuthorPosts
- You must be logged in to reply to this topic.
Advertisement below: