Forum begins after the advertisement:


[Part 4] Health positioned in the center of the screen

Home Forums Video Game Tutorial Series Creating a Metroidvania in Unity [Part 4] Health positioned in the center of the screen

Viewing 4 posts - 1 through 4 (of 4 total)
  • Author
    Posts
  • #11937
    Cayden Andrews
    Level 1
    Participant
    Helpful?
    Up
    0
    ::

    Followed code 1:1, health is positioned center of screen and does not visually decrease on hit.

    Heart COntroller

    <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; // += 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();
        }
    }</code>

    Player Controller

    <code>using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class PlayerController : MonoBehaviour
    {
        [Header("Horizontal Movement Settings")]
        private Rigidbody2D rb;
        [SerializeField] private float walkSpeed = 1; // serialize field makes value adjustable in unity
        private float xAxis, yAxis;
        [Space(5)]
    
        [Header("Vertical Movement Variables")]
        [SerializeField] private float jumpForce = 45;
        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("Attack Settings")]
        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, slashEffectDown, slashEffectUp;
        bool restoreTime;
        float restoreTimeSpeed;
        [Space(5)]
    
        [Header("Recoil Settings")]
        [SerializeField] int recoilXSteps = 5, recoilYSteps = 5;
        [SerializeField] float recoilXSpeed = 100, recoilYSpeed = 100;
        int stepsXRecoiled, stepsYRecoiled;
        [Space(5)]
    
        [Header("Health Settings")]
        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;
    
        float healTimer;
        [SerializeField] float timeToHeal;
        [Space(5)]
    
        [Header("Mana Settings")]
    
        [HideInInspector] public PlayerStateList pState;
        private float gravity;
        private Animator anim;
        private bool canDash;
        private bool dashed;
        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<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()
        {
            GetInputs();
            UpdateJumpVariables();
            if (pState.dashing) return;
            Flip();
            Move();
            Jump();
            startDash();
            Attack();
            RestoreTimeScale();
            FlashWhileInvincible();
            Heal();
        }
    
        private void FixedUpdate()
        {
            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(-1, transform.localScale.y);
                pState.lookingRight = false;
            }
            else if(xAxis > 0)
            {
                transform.localScale = new Vector2(1, transform.localScale.y);
                pState.lookingRight = true;
            }
        }
    
        private void Move()
        {
            rb.velocity = new Vector2(walkSpeed * xAxis, rb.velocity.y);
            anim.SetBool("Walking", rb.velocity.x != 0 && Grounded());
        }
    
        void startDash()
        {
            if (Input.GetButtonDown("Dash") && !dashed) //&& canDash
            {
                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);
            Instantiate(dashEffect, transform);
            yield return new WaitForSeconds(dashTime);
            rb.gravityScale = gravity;
            pState.dashing = false;
            yield return new WaitForSeconds(dashCooldown);
            canDash = true;
        }
    
        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);
                    Instantiate(slashEffectUp, upAttackTransform);
                }
                else if(yAxis < 0 && !Grounded())
                {
                    Hit(downAttackTransform, downAttackArea, ref pState.recoilingy, recoilYSpeed);
                    Instantiate(slashEffectDown, 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);
                }
            }
        }
    
        //void SlashEffect(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);
        //}
        // unneeded as i am using 3 different sprites for attacks, seperate for up, side and down
    
        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
            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;
        }
    
        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();
                    }
                }
            }
        }
    
        void Heal()
        {
            if (Input.GetButtonDown("Healing") && Health < maxHealth && !pState.jumping && !pState.dashing)
            {
                pState.healing = true;
                anim.SetBool("Healing", true);
                healTimer += Time.deltaTime;
                if(healTimer >= timeToHeal)
                {
                    Health++;
                    healTimer = 0;
                }
            }
            else
            {
                pState.healing = false;
                anim.SetBool("Healing", false);
                healTimer = 0;
            }
        }
    
        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 (Input.GetButtonUp("Jump") && rb.velocity.y > 0)
            {
                rb.velocity = new Vector2(rb.velocity.x, 0);
    
                pState.jumping = false;
            }
            else if (jumpBufferCounter <= 0 && !Grounded()) // added to fix disabling dj on peak jump NOT IN VID
            {
                pState.jumping = false;
            }
    
            if (!pState.jumping)
            {
                if (jumpBufferCounter > 0 && coyoteTimeCounter > 0) // if we're in the jump window and jump buffer (makes things smoother)
                {
                    rb.velocity = new Vector3(rb.velocity.x, jumpForce);
    
                    pState.jumping = true;
                }
                else if (!Grounded() && airJumpCounter < maxAirJumps && Input.GetButtonDown("Jump") && pState.jumping == false) 
                    // ^ pstate ting used here to prevent dj firing on small presses of jump, works most of time NOT IN VID
                {
                    pState.jumping = true;
    
                    airJumpCounter++;
    
                    rb.velocity = new Vector3(rb.velocity.x, jumpForce);
                }
            }
    
            anim.SetBool("Airborne", !Grounded());
        }
    
        void UpdateJumpVariables()
        {
            if (Grounded())
            {
                pState.jumping = false;
                coyoteTimeCounter = coyoteTime;
                airJumpCounter = 0;
            }
            else
            {
                coyoteTimeCounter -= Time.deltaTime; // deltaTime is amount of time between each frame (decreases counter by 1 every second)
            }
    
            if (Input.GetButtonDown("Jump"))
            {
                jumpBufferCounter = jumpBufferFrames;
            }
            else
            {
                jumpBufferCounter--;
            }
        }
    }
    </code>

    Player State List

    <code>using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class PlayerStateList : MonoBehaviour
    {
        public bool jumping = false;
        public bool dashing = false;
        public bool recoilingx, recoilingy;
        public bool lookingRight;
        public bool invincible;
        public bool healing;
    }</code>
    #11938
    Ben Price
    Level 7
    Participant
    Helpful?
    Up
    0
    ::

    I commented what is likely the fix on youtube:

    Check that this name in the below code snip matches the name you gave the actual full heart component of the container prefab. just double clicky the prefab and check. Make the names match and youre fixed.

    heartFills[i] = temp.transform.Find("<strong>HeartFill</strong>").GetComponent<Image>();

    Then: (copying my youtube comment)

    As for the alignment, check to see the size of your Hearts Parent. NOT the grid sizes, but the actual hearts parent object which should be a child of the canvas. You want the hearts parent to be the same size as the canvas. Depending on how it was created, it will either be the right size, or i believe in your case, smaller and in the middle of the canvas. If it is not the same size as the canvas, the grid component of the hearts parent will only work within the smaller frame of the hearts parent, and your health will be floating in the middle of the screen, or wherever the space of the hearts parent is. To make it the same size as the canvas, click on hearts parent, then find the resize icon in the Rect Transform component of the inspector. That icon is a square lookin thing in the upper left and it says “stretch” horizontally and vertically. Click that dude, then hold Alt and click the lower Right option. Now theyre the same size.

    #11939
    Terence
    Level 30
    Keymaster
    Helpful?
    Up
    0
    ::

    Thanks for making the post here as well Ben. I’ve helped you format the code in your post. Really appreciate your support!

    P.S. If you log in using your Patreon account, you will be labelled a Patron when you post. Not that it matters. Just highlighting!

    #12241
    Witty Comeback
    Level 2
    Participant
    Helpful?
    Up
    0
    ::

    Hi Cayden, the solution to the health not updating is the if statement in the setfillhearts method needs to use health, not maxHealth, like so

    if (i < PlayerController.Instance.health)

    With regards to it not being in the right place i just moved the hearts parent around manually until it was in the position i wanted it to be

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

Go to Login Page →


Advertisement below: