Forum begins after the advertisement:
[part 4] hearts don’t decrease on hit
Home › Forums › Video Game Tutorial Series › Creating a Metroidvania in Unity › [part 4] hearts don’t decrease on hit
- This topic has 6 replies, 2 voices, and was last updated 1 year, 1 month ago by Terence.
-
AuthorPosts
-
November 16, 2023 at 2:26 am #12213::
my hearts appear on the HUD in the top left corner. i have 10 visible hearts now(how do i change this?) my main issue is that my hearts dont decrease when i take damage.
using System.Collections; using System.Collections.Generic; using Unity.VisualScripting; using UnityEngine; public class PlayerController : MonoBehaviour { [Header("Horizontal Movement Settings")] [SerializeField] private float walkSpeed = 1; [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("Vertical Movement Settings")] [SerializeField] private float jumpForce = 45; private int jumpBufferCounter = 0; [SerializeField] private int jumpBufferFrames; private float coyoteTimeCounter = 0; [SerializeField] private float coyoteTime; [SerializeField] private float airJumpsCounter = 0; [SerializeField] private int maxAirJumps; [Space(5)] [Header("recoil")] [SerializeField] int recoilXSteps = 5; [SerializeField] int recoilYSteps = 5; [SerializeField] float recoilXSpeed = 100; [SerializeField] float recoilYSpeed = 100; private int stepsXRecoiled, stepsYRecoiled; [Space(5)] [Header("Attack Settings")] private bool attack = false; private float timeBetweenAttack, timeSinceAttack; [SerializeField] Transform SideAttackTransform, BehindAttackTransform, UpAttackTransform, DownAttackTransform; [SerializeField] Vector2 SideAttackArea, BehindAttackArea, UpAttackArea, DownAttackArea; [SerializeField] LayerMask attackableLayer; [SerializeField] float damage; [SerializeField] GameObject slashEffect; bool restoreTime; float restoreTimeSpeed; [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("Health")] public int health; public int maxHealth; [SerializeField] GameObject bloodSpurt; [SerializeField] float hitFlashSpeed; public delegate void OnHealthChangedDelegate(); // delegate voids can be used on multiple methods [HideInInspector] public OnHealthChangedDelegate onHealthChangedCallBack; [Space(5)] [HideInInspector] public playerStatesList pState; private float xAxis, yAxis; private Rigidbody2D rb; private float gravity; private Animator anim; private SpriteRenderer sr; 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<playerStatesList>(); rb = GetComponent<Rigidbody2D>(); anim = GetComponent<Animator>(); sr = GetComponent<SpriteRenderer>(); gravity = rb.gravityScale; } //draw out hitboxes for basic attacks private void OnDrawGizmos() { Gizmos.color = Color.red; Gizmos.DrawWireCube(SideAttackTransform.position, SideAttackArea); Gizmos.DrawWireCube(BehindAttackTransform.position, BehindAttackArea); Gizmos.DrawWireCube(UpAttackTransform.position, UpAttackArea); Gizmos.DrawWireCube(DownAttackTransform.position, DownAttackArea); } // Update is called once per frame void Update() { GetInputs(); updateJumpVariable(); if (pState.dashing) return; Flip(); Move(); Jump(); StartDash(); Attack(); RestoreTimeScale(); FlashWhileInvincible(); } private void FixedUpdate() { if(pState.dashing) return; Recoil(); } //basic inputs for movement void GetInputs() { xAxis = Input.GetAxisRaw("Horizontal"); yAxis = Input.GetAxisRaw("Vertical"); attack = Input.GetButtonDown("Attack"); } // flip player on x axis when left or right is picked 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; } } //move left and right private void Move() { rb.velocity = new Vector2(walkSpeed * xAxis, rb.velocity.y); anim.SetBool("walking", rb.velocity.x != 0 && Grounded()); } //allows for dashing and air dashing void StartDash() { if(Input.GetButtonDown("Dash") && canDash && !dashed) { StartCoroutine(Dash()); dashed = true; } if (Grounded()) { dashed = false; } } //stops middair infinite dashing 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; } //create hitboxes for attacks void Attack() { timeSinceAttack += Time.deltaTime; if(attack && timeSinceAttack >= timeBetweenAttack) { timeSinceAttack = 0; //side attack if (yAxis == 0 || yAxis < 0 && Grounded()) { Hit(SideAttackTransform, SideAttackArea, ref pState.recoilingx, recoilXSpeed); Instantiate(slashEffect, SideAttackTransform); anim.SetTrigger("Attacking"); } //up air attack else if (yAxis > 0 && !Grounded()) { Hit(UpAttackTransform, UpAttackArea, ref pState.recoilingy, recoilYSpeed); SlashEffectAtAngle(slashEffect, 80, UpAttackTransform); anim.SetTrigger("Attacking"); } //down air attak else if (yAxis < 0 && !Grounded()) { Hit(DownAttackTransform, DownAttackArea, ref pState.recoilingy, recoilYSpeed); SlashEffectAtAngle(slashEffect, -80, DownAttackTransform); anim.SetTrigger("Attacking"); } if (yAxis > 0 && Grounded()) { Hit(UpAttackTransform, UpAttackArea, ref pState.recoilingy, recoilYSpeed); SlashEffectAtAngle(slashEffect, 95, UpAttackTransform); anim.SetTrigger("upAttack"); } } } //hitting the enemy and adding recoil to attacks 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); } } } //changes the direction of attack based on aerial positions 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); } //directional player recoil based on main slash attacks, x and y axis movement void Recoil() { //recoiling in the x axis if (pState.recoilingx) { if (pState.lookingRight) { rb.velocity = new Vector2(-recoilXSpeed, 0); } else { rb.velocity = new Vector2(recoilXSpeed, 0); } } //recoil in the y axis 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); } airJumpsCounter = 0; } else { rb.gravityScale = gravity; } //stop recoiling if (pState.recoilingx && stepsXRecoiled < recoilXSteps) { stepsXRecoiled++; } else { StopRecoilX(); } if (pState.recoilingy && stepsYRecoiled < recoilYSteps) { stepsYRecoiled++; } else { StopRecoilY(); } if (Grounded()) { StopRecoilY(); } } //stop recoiling forever void StopRecoilX() { stepsXRecoiled = 0; pState.recoilingx = false; } void StopRecoilY() { stepsYRecoiled = 0; pState.recoilingy = false; } //take damage, set health and gives some invinicibility frames 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; } void FlashWhileInvincible() { sr.material.color = pState.invincible ? 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; Time.timeScale = _newTimeScale; if (_delay > 0) { StopCoroutine(StartTimeAgain(_delay)); StartCoroutine(StartTimeAgain(_delay)); } else { restoreTime = true; } } 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(); } } } } //checks if you are grounded 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; } } //makes you jump when pressing spacebar void Jump() { if(Input.GetButtonUp("Jump") && rb.velocity.y > 0) { rb.velocity = new Vector2(rb.velocity.x, 0); pState.jumping = false; } if(!pState.jumping) { if (jumpBufferCounter > 0 && coyoteTimeCounter > 0) { rb.velocity = new Vector3(rb.velocity.x, jumpForce); pState.jumping = true; } else if(!Grounded() && airJumpsCounter < maxAirJumps && Input.GetButtonDown("Jump")) { pState.jumping = true; airJumpsCounter++; rb.velocity = new Vector3(rb.velocity.x, jumpForce); } } //animate jumping double jump and falling animation anim.SetBool("jumping", !Grounded() && rb.velocity.y > -20 && airJumpsCounter == 0); anim.SetBool("doublejump", !Grounded() && airJumpsCounter == 1); anim.SetBool("falling", !Grounded() && rb.velocity.y < -10); } //coyote time and jump buffering void updateJumpVariable() { //coyote time if (Grounded()) { pState.jumping = false; coyoteTimeCounter = coyoteTime; airJumpsCounter = 0; } else { coyoteTimeCounter -= Time.deltaTime; } //jump buffering if (Input.GetButtonDown("Jump")) { jumpBufferCounter = jumpBufferFrames; } else { jumpBufferCounter--; } } }
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; // += as multicasting delegate - many func at same time 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.maxHealth) { 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(); } }
November 16, 2023 at 9:37 pm #12214::Hi Niyam,
The hearts should automatically update according to how much health you have. I suspect you probably forgot to assign something in the Inspector, or you are missing a line or 2 somewhere.
Can you play the game, select your player character and go into Debug mode on the Inspector? Here’s how you activate debug mode: https://blog.theknightsofunity.com/can-switch-inspector-debug-mode/
Then, let your player take damage and see if his / her health value in the Inspector decreases.
November 17, 2023 at 2:41 am #12223November 17, 2023 at 2:48 am #12224November 17, 2023 at 12:24 pm #12225November 18, 2023 at 2:45 am #12227::So everything was correct in the inspector and I couldn’t see any issues in my code? Was rlly strange… I copied and pasted the health deplete code from ur blog and then it suddenly started working?
November 18, 2023 at 12:17 pm #12231::Either there was a difference between your code and the code that was in the blog, or Unity did not compile your code after you made changes to it. Very rarely, Unity can compile the older version of your code even after you’ve made changes and saved.
To see if your code is different from what’s on the blog, you can compare them on this site: https://www.diffchecker.com/
-
AuthorPosts
- You must be logged in to reply to this topic.
Advertisement below: