Forum begins after the advertisement:


[Part 10] Boss doesn’t flip

Home Forums Video Game Tutorial Series Creating a Metroidvania in Unity [Part 10] Boss doesn’t flip

Viewing 15 posts - 1 through 15 (of 20 total)
  • Author
    Posts
  • #13494
    Allan Valin
    Level 5
    Participant
    Helpful?
    Up
    0
    ::

    Lunge doesn’t do damage. Player stands over the boss sword’s collider while it uses Triple Slash without taking damage (in front of it works). Attacking the boss makes it climb the player sword’s collider. Sometimes the boss stops taking damage. Some animation transitions don’t seem to work. Barrage and Flame Pillar aren’t activating apparently. When bouncing, the boss might clip through the ground.

    I added the player prefab to another scene and it wouldn’t move for some reason. There’s also this bug where the Animator window won’t show the states unless I restart the engine, wtf.

    BossController

    <code>
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class BossController : Enemy
    {
        public static BossController Instance;
        [SerializeField] GameObject slashEffect;
    
        [Header("Ground Check Settings:")]
        [SerializeField] public 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("Attack Settings:")]
        [SerializeField] public Transform SideAttackTransform; //the middle of the side attack area
        [SerializeField] public Vector2 SideAttackArea; //how large the area of side attack is
        [SerializeField] public Transform UpAttackTransform; //the middle of the up attack area
        [SerializeField] public Vector2 UpAttackArea; //how large the area of side attack is
        [SerializeField] public Transform DownAttackTransform; //the middle of the down attack area
        [SerializeField] public Vector2 DownAttackArea; //how large the area of down attack is
    
        public float attackRange;
        public float attackTimer;
    
        [Space(5)]
    
        [HideInInspector] public bool facingRight;
    
        int hitCounter;
        bool stunned, canStun;
        bool alive;
    
        [HideInInspector] public float runSpeed;
    
        public GameObject impactParticle;
    
    
        private void Awake()
        {
            if (Instance != null && Instance != this)
            {
                Destroy(gameObject);
            }
            else
            {
                Instance = this;
            }
        }
    
        protected override void Start()
        {
            base.Start();
            sr = GetComponentInChildren<SpriteRenderer>();
            anim = GetComponentInChildren<Animator>();
            ChangeState(EnemyStates.Boss_Stage1);
            alive = true;
        }
    
        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;
            }
        }
    
        private void OnDrawGizmos()
        {
            Gizmos.color = Color.red;
            Gizmos.DrawWireCube(SideAttackTransform.position, SideAttackArea);
            Gizmos.DrawWireCube(UpAttackTransform.position, UpAttackArea);
            Gizmos.DrawWireCube(DownAttackTransform.position, DownAttackArea);
        }
    
        float bloodCountDown;
        float bloodTimer;
    
        protected override void Update()
        {
            base.Update();
    
            if(health <= 0 && alive)
            {
                Death(0);
            }
    
            if(!attacking)
            {
                attackCountdown -= Time.deltaTime;
            }
    
            if(stunned)
            {
                rb.velocity = Vector2.zero;
            }
    
            bloodCountDown -= Time.deltaTime;
            if(bloodCountDown <= 0 && (currentEnemyState != EnemyStates.Boss_Stage1 && 
                currentEnemyState != EnemyStates.Boss_Stage2))
            {
                GameObject _orangeBlood = Instantiate(orangeBlood, groundCheckPoint.position, Quaternion.identity);
                Destroy(_orangeBlood, 4f);
                bloodCountDown = bloodTimer;
            }
        }
    
        public void Flip()
        {
            if(PlayerController.Instance.transform.position.x < transform.position.x && transform.position.x > 0)//.localScale.x > 0)
            {
                transform.eulerAngles = new Vector2(transform.eulerAngles.x, 180);
                //transform.localScale = new Vector2(-1, transform.localScale.y);
                facingRight = false;
            }
            else
            {
                transform.eulerAngles = new Vector2(transform.eulerAngles.x, 0);
                //transform.localScale = new Vector2(1, transform.localScale.y);
                facingRight = true;
            }
        }
    
        protected override void UpdateEnemyStates()
        {
            if(PlayerController.Instance != null)
            {
                switch(GetCurrentEnemyState)
                {
                    case EnemyStates.Boss_Stage1:
                    canStun = true;
                    //attackTimer = 6; // higher numbers = slower attack speed
                    runSpeed = speed;
                    break;
    
                    case EnemyStates.Boss_Stage2:
                    canStun = true;
                    //attackTimer = 5;
                    break;
    
                    case EnemyStates.Boss_Stage3:
                    canStun = false;
                    //attackTimer = 8;
                    bloodTimer = 5f;
                    break;
    
                    case EnemyStates.Boss_Stage4:
                    canStun = false;
                    //attackTimer = 10;
                    runSpeed = speed / 2;
                    bloodTimer = 1.5f;
                    break;
                }
            }
        }
    
        protected override void OnCollisionStay2D(Collision2D _other)
        {
            //base.OnCollisionStay2D(_other);
        }
    
        #region attacking
        #region variables
        [HideInInspector] public bool attacking;
        [HideInInspector] public float attackCountdown;
        [HideInInspector] public bool damagedPlayer = false;
        [HideInInspector] public bool parrying;
    
        [HideInInspector] public Vector2 moveToPosition;
        [HideInInspector] public bool diveAttack;
        public GameObject divingCollider;
        public GameObject pillar;
    
        [HideInInspector] public bool barrageAttack;
        public GameObject barrageFireball;
        [HideInInspector] public bool outbreakAttack;
    
        [HideInInspector] public bool bounceAttack;
        [HideInInspector] public float rotationDirectionToTarget;
        [HideInInspector] public int bounceCount;
    
        #endregion
    
        #region Control
    
        public void AttackHandler()
        {
            if(currentEnemyState == EnemyStates.Boss_Stage1)
            {
                if(Vector2.Distance(PlayerController.Instance.transform.position, rb.position) <= attackRange)
                {
                    StartCoroutine(TripleSlash());
                }
                else
                {
                    StartCoroutine(Lunge());
                }
            }
    
            if(currentEnemyState == EnemyStates.Boss_Stage2)
            {
                if(Vector2.Distance(PlayerController.Instance.transform.position, rb.position) <= attackRange)
                {
                    StartCoroutine(TripleSlash());
                }
                else
                {
                    int _attackChosen = Random.Range(1, 3);
                    switch(_attackChosen)
                    {
                        case 1:
                            StartCoroutine(Lunge());
                            break;
    
                        case 2:
                            DiveAttackJump();
                            break;
    
                        case 3:
                            BarrageBendDown();
                            break;
                    }
                }
            }
    
            if(currentEnemyState == EnemyStates.Boss_Stage3)
            {
                int _attackChosen = Random.Range(1, 4);
    
                if(_attackChosen == 1)
                {
                    OutbreakBendDown();
                }
    
                if (_attackChosen == 2)
                {
                    DiveAttackJump();
                }
    
                if (_attackChosen == 3)
                {
                    BarrageBendDown();
                }
    
                if (_attackChosen == 4)
                {
                    BounceAttack();
                }
                /*switch(_attackChosen)
                {
                    case 1:
                        OutbreakBendDown();
                        break;
    
                    case 2:
                        DiveAttackJump();
                        break;
    
                    case 3:
                        BarrageBendDown();
                        break;
    
                    case 4:
                        BounceAttack();
                        break;
                }*/
    
            }
    
            if(currentEnemyState == EnemyStates.Boss_Stage4)
            {
                if(Vector2.Distance(PlayerController.Instance.transform.position, rb.position) <= attackRange)
                {
                    StartCoroutine(Slash());
                }
                else
                {
                    BounceAttack();
                }
            }
        }
    
        public void ResetAllAttacks()
        {
            attacking = false;
    
            StopCoroutine(TripleSlash());
            StopCoroutine(Lunge());
            StopCoroutine(Parry());
            StopCoroutine(Slash());
    
            diveAttack = false;
            barrageAttack = false;
            outbreakAttack = false;
            bounceAttack = false;
        }
    
        #endregion
    
        #region Stage 1
    
        IEnumerator TripleSlash()
        {
            attacking = true;
            rb.velocity = Vector2.zero;
    
            anim.SetTrigger("Slash");
            SlashAngle();
            yield return new WaitForSecondsRealtime(0.3f);
            yield return new WaitForSecondsRealtime(0.3f);
            anim.ResetTrigger("Slash");
    
            anim.SetTrigger("Slash");
            SlashAngle();
            yield return new WaitForSecondsRealtime(0.5f);
            anim.ResetTrigger("Slash");
    
            anim.SetTrigger("Slash");
            SlashAngle();
            yield return new WaitForSecondsRealtime(0.2f);
            anim.ResetTrigger("Slash");
    
            ResetAllAttacks();
        }
    
        void SlashAngle()
        {
            // side attack
            if(PlayerController.Instance.transform.position.x > transform.position.x ||
                PlayerController.Instance.transform.position.x < transform.position.x)
            {
                Instantiate(slashEffect, SideAttackTransform);
            }
            // up attack
            else if(PlayerController.Instance.transform.position.y > transform.position.y)
            {
                Instantiate(slashEffect, UpAttackTransform);
            }
    
            // down attack
            else if(PlayerController.Instance.transform.position.y < transform.position.y)
            {
                Instantiate(slashEffect, DownAttackTransform);
            }
        }
    
        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);
        }
    
        IEnumerator Lunge()
        {
            Flip();
            attacking = true;
    
            anim.SetBool("Lunge", true);
            yield return new WaitForSecondsRealtime(1f);
            anim.SetBool("Lunge", false);
            damagedPlayer = false;
    
            ResetAllAttacks();
        }
    
        IEnumerator Parry()
        {
            attacking = true;
            rb.velocity = Vector2.zero;
    
            anim.SetBool("Parry", true);
            yield return new WaitForSecondsRealtime(0.8f);
            anim.SetBool("Parry", false);
    
            parrying = false;
            ResetAllAttacks();
        }
    
        IEnumerator Slash()
        {
            attacking = true;
            rb.velocity = Vector2.zero;
    
            anim.SetTrigger("Slash");
            SlashAngle();
            yield return new WaitForSecondsRealtime(0.3f);
            anim.ResetTrigger("Slash");
    
            ResetAllAttacks();
        }
    
        #endregion
        #region Stage 2
    
        void DiveAttackJump()
        {
            attacking = true;
            moveToPosition = new Vector2(PlayerController.Instance.transform.position.x, rb.position.y + 10);
            diveAttack = true;
            anim.SetBool("Jump", true);
        }
    
        public void Dive()
        {
            anim.SetBool("Dive", true);
            anim.SetBool("Dive", false);
        }
    
        private void OnTriggerEnter2D(Collider2D _other)
        {
            if(_other.GetComponent<PlayerController>() != null && (diveAttack || bounceAttack))
            {
                _other.GetComponent<PlayerController>().TakeDamage(damage * 2);
                PlayerController.Instance.pState.recoilingX = true;
            }
        }
    
        public void DivingPillars()
        {
            Vector2 _impactPoint = groundCheckPoint.position;
            float _spawnDistance = 5;
    
            for(int i = 0; i < 10; i++)
            {
                Vector2 _pillarSpawnPointRight = _impactPoint + new Vector2(_spawnDistance, 0);
                Vector2 _pillarSpawnPointLeft = _impactPoint - new Vector2(_spawnDistance, 0);
                Instantiate(pillar, _pillarSpawnPointRight, Quaternion.Euler(0, 0, -90));
                Instantiate(pillar, _pillarSpawnPointLeft, Quaternion.Euler(0, 0, -90));
    
                _spawnDistance += 5;
            }
            ResetAllAttacks();
        }
    
        void BarrageBendDown()
        {
            attacking = true;
            rb.velocity = Vector2.zero;
            barrageAttack = true;
            anim.SetTrigger("BendDown");
        }
    
        public IEnumerator Barrage()
        {
            rb.velocity = Vector2.zero;
    
            float _currentAngle = 30f;
            for(int i = 0; i < 10; i++)
            {
                GameObject _projectile = Instantiate(barrageFireball, transform.position, Quaternion.Euler(0, 0, _currentAngle));
    
                if(facingRight)
                {
                    _projectile.transform.eulerAngles = new Vector3(_projectile.transform.eulerAngles.x, 0, _currentAngle);
                }
                else
                {
                    _projectile.transform.eulerAngles = new Vector3(_projectile.transform.eulerAngles.x, 180, _currentAngle);
                }
    
                _currentAngle += 5f;
    
                yield return new WaitForSecondsRealtime(0.4f);
            }
            yield return new WaitForSecondsRealtime(0.1f);
            anim.SetBool("Cast", false);
            ResetAllAttacks();
        }
    
        #endregion
        #region Stage 3
    
        void OutbreakBendDown()
        {
            attacking = true;
            rb.velocity = Vector2.zero;
            moveToPosition = new Vector2(transform.position.x, rb.position.y + 5);
            outbreakAttack = true;
            anim.SetTrigger("BendDown");
        }
    
        public IEnumerator Outbreak()
        {
            yield return new WaitForSecondsRealtime(1f);
            anim.SetBool("Cast", true);
    
            rb.velocity = Vector2.zero;
            for(int i = 0; i < 30; i++)
            {
                Instantiate(barrageFireball, transform.position, Quaternion.Euler(0, 0, Random.Range(110, 130))); // downwards
                Instantiate(barrageFireball, transform.position, Quaternion.Euler(0, 0, Random.Range(50, 70))); // diagonally right
                Instantiate(barrageFireball, transform.position, Quaternion.Euler(0, 0, Random.Range(260, 280))); // diagonally left
    
                yield return new WaitForSecondsRealtime(0.2f);
            }
            yield return new WaitForSecondsRealtime(0.1f);
    
            rb.constraints = RigidbodyConstraints2D.None;
            rb.constraints = RigidbodyConstraints2D.FreezeRotation;
            rb.velocity = new Vector2(rb.velocity.x, -10);
            yield return new WaitForSecondsRealtime(0.1f);
            anim.SetBool("Cast", false);
            ResetAllAttacks();
        }
    
        public void BounceAttack()
        {
            attacking = true;
            bounceCount = Random.Range(2, 5);
            BounceBendDown();
        }
        int _bounces = 0;
    
        public void CheckBounce()
        {
            if(_bounces < bounceCount - 1)
            {
                _bounces++;
                BounceBendDown();
            }
            else
            {
                _bounces = 0;
                anim.Play("Boss_Run");
            }
        }
    
        public void BounceBendDown()
        {
            rb.velocity = Vector2.zero;
            moveToPosition = new Vector2(PlayerController.Instance.transform.position.x, rb.position.y + 10);
            bounceAttack = true;
            anim.SetTrigger("BendDown");
        }
    
        public void CalculateTargetAngle()
        {
            Vector3 _directionToTarget = (PlayerController.Instance.transform.position - transform.position).normalized;
            float _angleOfTarget = Mathf.Atan2(_directionToTarget.y, _directionToTarget.x) * Mathf.Rad2Deg;
            rotationDirectionToTarget = _angleOfTarget;
        }
    
        #endregion
    
        #endregion
    
        public override void EnemyHit(float _damageDone, Vector2 _hitDirection, float _hitForce)
        {
            if(!stunned)
            {
                if(!parrying)
                {
                    if(canStun)
                    {
                        hitCounter++;
                        if(hitCounter >= 3) // number of hits to stun
                        {
                            ResetAllAttacks();
                            StartCoroutine(Stunned());
                        }
                    }
                    base.EnemyHit(_damageDone, _hitDirection, _hitForce);
    
                    if(currentEnemyState != EnemyStates.Boss_Stage4)
                    {
                        ResetAllAttacks(); // cancel attacks to avoid bugs
                        StartCoroutine(Parry());
                    }
    
                }
                else
                {
                    StopCoroutine(Parry());
                    ResetAllAttacks();
                    StartCoroutine(Slash()); // riposte
                }
            }
            else
            {
                StopCoroutine(Stunned());
                anim.SetBool("Stunned", false);
                stunned = false;
            }
    
            #region health to state
            if(health > 20)
            {
                ChangeState(EnemyStates.Boss_Stage1);
            }
            if(health <= 15 && health < 10)
            {
                ChangeState(EnemyStates.Boss_Stage2);
            }
            if(health <= 10 && health < 5)
            {
                ChangeState(EnemyStates.Boss_Stage3);
            }
            if(health < 10)
            {
                ChangeState(EnemyStates.Boss_Stage4);
            }
            if(health <= 0)
            {
                Death(0);
            }
            #endregion
    
        }
    
        public IEnumerator Stunned()
        {
            stunned = true;
            hitCounter = 0;
            anim.SetBool("Stunned", true);
    
            yield return new WaitForSecondsRealtime(6f);
    
            anim.SetBool("Stunned", false);
            stunned = false;
        }
    
        protected override void Death(float _destroyTime)
        {
            ResetAllAttacks();
            alive = false;
            rb.velocity = new Vector2(rb.velocity.x, -25);
            anim.SetTrigger("Die");
            bloodTimer = 0.8f;
        }
    
        public void DestroyAfterDeath()
        {
            Destroy(gameObject);
        }
    }
    </code>

    BossEvents

    <code>
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class BossEvents : MonoBehaviour
    {
        void SlashDamagePlayer()
        {
            // side attack
            if (PlayerController.Instance.transform.position.x > transform.position.x ||
                PlayerController.Instance.transform.position.x < transform.position.x)
            {
                Hit(BossController.Instance.SideAttackTransform, BossController.Instance.SideAttackArea);
            }
            // up attack
            else if (PlayerController.Instance.transform.position.y > transform.position.y)
            {
                Hit(BossController.Instance.UpAttackTransform, BossController.Instance.UpAttackArea);
            }
    
            // down attack
            else if (PlayerController.Instance.transform.position.y < transform.position.y)
            {
                Hit(BossController.Instance.DownAttackTransform, BossController.Instance.DownAttackArea);
            }
        }
    
        void Hit(Transform _attackTransform, Vector2 _attackArea)
        {
            Collider2D[] _objectsToHit = Physics2D.OverlapBoxAll(_attackTransform.position, _attackArea, 0);
            for (int i = 0; i < _objectsToHit.Length; i++)
            {
                if (_objectsToHit[i].GetComponent<PlayerController>() != null)
                {
                    _objectsToHit[i].GetComponent<PlayerController>().TakeDamage(BossController.Instance.damage);
                }
            }
        }
    
        void Parrying()
        {
            BossController.Instance.parrying = true;
        }
    
        void BendDownCheck()
        {
            if (BossController.Instance.barrageAttack)
            {
                StartCoroutine(BarrageAttackTransition());
            }
    
            if (BossController.Instance.outbreakAttack)
            {
                StartCoroutine(OutbreakAttackTransition());
            }
    
            if (BossController.Instance.bounceAttack)
            {
                BossController.Instance.anim.SetTrigger("Bounce1");
            }
        }
    
        void BarrageOrOutbreak()
        {
            if (BossController.Instance.barrageAttack)
            {
                BossController.Instance.StartCoroutine(BossController.Instance.Barrage());
            }
    
            if (BossController.Instance.outbreakAttack)
            {
                BossController.Instance.StartCoroutine(BossController.Instance.Outbreak());
            }
        }
    
        IEnumerator BarrageAttackTransition()
        {
            yield return new WaitForSecondsRealtime(1f);
            BossController.Instance.anim.SetBool("Cast", true);
        }
    
        IEnumerator OutbreakAttackTransition()
        {
            yield return new WaitForSecondsRealtime(1f);
            BossController.Instance.anim.SetBool("Cast", true);
        }
    
        void DestroyAfterDeath()
        {
            SpawnBoss.Instance.IsNotTrigger();
            BossController.Instance.DestroyAfterDeath();
        }
    }</code>

    PlayerController

    <code>using System.Collections;
    using System.Collections.Generic;
    using System.Net.Sockets;
    using UnityEngine;
    using UnityEngine.UIElements;
    using UnityEngine.UI;
    
    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 int jumpBufferCounter = 0; //stores the jump button input
        [SerializeField] private int 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("Wall Jump Settings")]
        [SerializeField] private float wallSlidingSpeed = 2f;
        [SerializeField] private Transform wallCheck;
        [SerializeField] private LayerMask wallLayer;
        [SerializeField] private float wallJumpingDuration;
        [SerializeField] private Vector2 wallJumpingPower;
        float wallJumpingDirection;
        bool isWallSliding;
        bool isWallJumping;
        [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
        private float timeBetweenAttack, timeSinceAttack;
        [SerializeField] private float damage; //the damage the player does to an enemy
        [SerializeField] private GameObject slashEffect; //the effect of the slashs
        [Space(5)]
    
        // time restoration
        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;
        public int maxTotalHealth = 10;
        public int heartPieces = 4; // how many pieces to get a heart
        [SerializeField] GameObject bloodSpurt;
        [SerializeField] float hitFlashSpeed;
        public delegate void OnHealthChangedDelegate();
        [HideInInspector] public OnHealthChangedDelegate onHealthChangedCallback;
        float healTimer;
        [SerializeField] float timeToHeal;
        [Space(5)]
    
        [Header("Mana Settings")]
        [SerializeField] UnityEngine.UI.Image manaStorage;
        [SerializeField] float mana;
        [SerializeField] float manaDrainSpeed;
        [SerializeField] float manaGain;
        [Space(5)]
    
        [Header("Spell Settings")]
        //spell stats
        [SerializeField] float manaSpellCost = 0.3f;
        [SerializeField] float timeBetweenCast = 0.5f;
        [SerializeField] float spellDamage; //upspellexplosion and downspellfireball
        [SerializeField] float downSpellForce; // desolate dive only
        //spell cast objects
        [SerializeField] GameObject sideSpellFireball;
        [SerializeField] GameObject upSpellExplosion;
        [SerializeField] GameObject downSpellFireball;
        float timeSinceCast;
        float castOrHealTimer;
        [Space(5)]
    
        [Header("Audio")]
        [SerializeField] AudioClip landingSound;
        [SerializeField] AudioClip jumpSound;
        [SerializeField] AudioClip dashAndAttackSound;
        [SerializeField] AudioClip spellcastSound;
        [SerializeField] AudioClip hurtSound;
        private bool landingSoundPlayed;
        [Space(5)]
    
        [HideInInspector] public PlayerStateList pState;
        private Animator anim;
        public Rigidbody2D rb;
        private SpriteRenderer sr;
        private AudioSource audioSource;
    
        //Input Variables
        private float xAxis, yAxis;
        private bool attack = false;
        private bool canFlash = true;
        bool openInventory;
    
        public static PlayerController Instance;
    
        [Header("Unlocks")]
        public bool unlockedWallJump;
        public bool unlockedDash;
        public bool unlockedVarJump;
        public bool unlockedSideCast;
        public bool unlockedUpCast;
        public bool unlockedDownCast;
    
        private void Awake()
        {
            if (Instance != null && Instance != this)
            {
                Destroy(gameObject);
            }
            else
            {
                Instance = this;
            }
            DontDestroyOnLoad(gameObject);
    
        }
    
        // Start is called before the first frame update
        void Start()
        {
            pState = GetComponent<PlayerStateList>();
            rb = GetComponent<Rigidbody2D>();
            sr = GetComponent<SpriteRenderer>();
            anim = GetComponent<Animator>();
            audioSource = GetComponent<AudioSource>();
    
            //SaveData.Instance.LoadPlayerData();
            gravity = rb.gravityScale;
    
            Mana = mana;
            manaStorage.fillAmount = Mana;
            Health = maxHealth;
        }
    
        private void OnDrawGizmos()
        {
            Gizmos.color = Color.red;
            Gizmos.DrawWireCube(SideAttackTransform.position, SideAttackArea);
            Gizmos.DrawWireCube(UpAttackTransform.position, UpAttackArea);
            Gizmos.DrawWireCube(DownAttackTransform.position, DownAttackArea);
        }
    
        // Update is called once per frame
        void Update()
        {
            if (GameManager.Instance.gameIsPaused) return;
    
            if (pState.cutscene) return;
            if(pState.alive)
            {
                GetInputs();
                ToggleInventory();
            }
    
            UpdateJumpVariables();
            RestoreTimeScale();
    
            if (pState.dashing) return;
            if (pState.alive)
            {
                if(!isWallJumping)
                {
                    Flip();
                    Move();
                    Jump();
                }
    
                if (unlockedWallJump)
                {
                    WallSlide();
                    WallJump();
                }
    
                if(unlockedDash)
                {
                    StartDash();
                }
    
                Attack();
                Heal();
                CastSpell();
            }
            FlashWhileInvincible();
    
        }
        private void OnTriggerEnter2D(Collider2D _other) //for up and down cast spell
        {
            if (_other.GetComponent<Enemy>() != null && pState.casting)
            {
                _other.GetComponent<Enemy>().EnemyHit(spellDamage, (_other.transform.position - transform.position).normalized, -recoilYSpeed);
            }
        }
    
        private void FixedUpdate()
        {
            if (pState.cutscene) return;
    
            if (pState.dashing) return;
            Recoil();
        }
    
        void GetInputs()
        {
            xAxis = Input.GetAxisRaw("Horizontal");
            yAxis = Input.GetAxisRaw("Vertical");
            attack = Input.GetButtonDown("Attack");
            //openInventory = Input.GetButton("Inventory");
            if (Input.GetButtonDown("Inventory")) openInventory = !openInventory;
    
            if (Input.GetButton("Cast/Heal"))
            {
                castOrHealTimer += Time.deltaTime;
            }
            else
            {
                castOrHealTimer = 0;
            }
        }
    
    
        void ToggleInventory()
        {
            if (openInventory)
            {
                UIManager.Instance.inventory.SetActive(true);
            }
            else
            {
                UIManager.Instance.inventory.SetActive(false) ;
            }
        }
        // movement methods
        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") && canDash && !dashed)
            {
                StartCoroutine(Dash());
                dashed = true;
            }
    
            if (Grounded())
            {
                dashed = false;
            }
        }
    
        IEnumerator Dash()
        {
            canDash = false;
            pState.dashing = true;
            anim.SetTrigger("Dashing");
            audioSource.PlayOneShot(dashAndAttackSound);
            rb.gravityScale = 0;
            int _dir = pState.lookingRight ? 1 : -1;
            rb.velocity = new Vector2(_dir * dashSpeed, 0);
            if (Grounded()) Instantiate(dashEffect, transform);
            yield return new WaitForSeconds(dashTime);
            rb.gravityScale = gravity;
            pState.dashing = false;
            yield return new WaitForSeconds(dashCooldown);
            canDash = true;
        }
        public IEnumerator WalkIntoNewScene(Vector2 _exitDir, float _delay)
        {
            //If exit direction is upwards
            if (_exitDir.y > 0)
            {
                rb.velocity = jumpForce * _exitDir;
            }
    
            //If exit direction requires horizontal movement
            if (_exitDir.x != 0)
            {
                xAxis = _exitDir.x > 0 ? 1 : -1;
    
                Move();
            }
    
            Flip();
            yield return new WaitForSeconds(_delay);
            pState.cutscene = false;
        }
    
        // attacking methods
        void Attack()
        {
            timeSinceAttack += Time.deltaTime;
            if (attack && timeSinceAttack >= timeBetweenAttack)
            {
                int _recoilLeftOrRight = pState.lookingRight ? 1 : -1;
                timeSinceAttack = 0;
                anim.SetTrigger("Attacking");
                audioSource.PlayOneShot(dashAndAttackSound);
    
                if (yAxis == 0 || yAxis < 0 && Grounded())
                {
                    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);
    
            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;
                    }
                }
            }
        }
        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);
        }
        // life and death methods
        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)
        {
            audioSource.PlayOneShot(hurtSound);
    
            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.1f);
            canFlash = true;
        }
        void FlashWhileInvincible()
        {
            if (pState.invincible)
            {
                if (Time.timeScale > 0.2 && canFlash)
                {
                    StartCoroutine(Flash());
                }
            }
            else
            {
                sr.enabled = true;
            }
        }
    
        void RestoreTimeScale()
        {
            if (restoreTime)
            {
                if (Time.timeScale < 1)
                {
                    Time.timeScale += Time.deltaTime * restoreTimeSpeed;
                }
                else
                {
                    Time.timeScale = 1;
                    restoreTime = false;
                }
            }
        }
    
        public void HitStopTime(float _newTimeScale, int _restoreSpeed, float _delay)
        {
            restoreTimeSpeed = _restoreSpeed;
            if (_delay > 0)
            {
                StopCoroutine(StartTimeAgain(_delay));
                StartCoroutine(StartTimeAgain(_delay));
            }
            else
            {
                restoreTime = true;
            }
            Time.timeScale = _newTimeScale;
        }
    
        IEnumerator StartTimeAgain(float _delay)
        {
            restoreTime = true;
            yield return new WaitForSeconds(_delay);
        }
    
        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 WaitForSecondsRealtime(0.9f);
    
            StartCoroutine(UIManager.Instance.ActivateDeathScreen());
    
            //Respawned();
        }
    
        public void Respawned()
        {
            if(!pState.alive)
            {
                pState.alive = true;
                Health = maxHealth;
                anim.Play("Player_Idle");
                mana = 0;
            }
        }
    
        public int Health
        {
            get { return health; }
            set
            {
                if (health != value)
                {
                    health = Mathf.Clamp(value, 0, maxHealth);
    
                    if (onHealthChangedCallback != null)
                    {
                        onHealthChangedCallback.Invoke();
                    }
                }
            }
        }
    
        // magic methods
        void Heal()
        {
            if (Input.GetButton("Cast/Heal") && castOrHealTimer > 0.05f && Health < maxHealth && Mana > 0 && !pState.jumping && !pState.dashing)
            {
                pState.healing = true;
                anim.SetBool("Healing", true);
    
                //healing
                healTimer += Time.deltaTime;
                if (healTimer >= timeToHeal)
                {
                    Health++;
                    healTimer = 0;
                }
    
                //drain mana
                Mana -= Time.deltaTime * manaDrainSpeed;
            }
            else
            {
                pState.healing = false;
                anim.SetBool("Healing", false);
                healTimer = 0;
            }
        }
    
        public float Mana
        {
            get { return mana; }
            set
            {
                //if mana stats change
                if (mana != value)
                {
                    mana = Mathf.Clamp(value, 0, 1);
                    manaStorage.fillAmount = Mana;
                }
            }
        }
    
        void CastSpell()
        {
            if (Input.GetButtonUp("Cast/Heal") && castOrHealTimer <= 0.05f && timeSinceCast >= timeBetweenCast && Mana >= manaSpellCost)
            {
                pState.casting = true;
                timeSinceCast = 0;
                StartCoroutine(CastCoroutine());
    
            }
            else
            {
                timeSinceCast += Time.deltaTime;
            }
    
            if (Grounded())
            {
                //disable downspell if on the ground
                downSpellFireball.SetActive(false);
            }
            //if down spell is active, force player down until grounded
            if (downSpellFireball.activeInHierarchy)
            {
                rb.velocity += downSpellForce * Vector2.down;
            }
        }
    
        IEnumerator CastCoroutine()
        {
            audioSource.PlayOneShot(spellcastSound);
    
            anim.SetBool("Casting", true);
            yield return new WaitForSeconds(0.15f); // check on Animator if it matches
    
            //side cast
            if ((yAxis == 0 || (yAxis < 0 && Grounded())) && unlockedSideCast)
            {
                anim.SetBool("Casting", true);
                yield return new WaitForSeconds(0.15f); // check on Animator if it matches
                GameObject _fireBall = Instantiate(sideSpellFireball, SideAttackTransform.position, Quaternion.identity);
    
                //flip fireball
                if (pState.lookingRight)
                {
                    _fireBall.transform.eulerAngles = Vector3.zero; // if facing right, fireball continues as per normal
                }
                else
                {
                    _fireBall.transform.eulerAngles = new Vector2(_fireBall.transform.eulerAngles.x, 180);
                    //if not facing right, rotate the fireball 180 deg
                }
                pState.recoilingX = true;
    
                Mana -= manaSpellCost;
                yield return new WaitForSeconds(0.35f); // check Animator
            }
    
            //up cast
            else if (yAxis > 0 && unlockedUpCast)
            {
                anim.SetBool("Casting", true);
                yield return new WaitForSeconds(0.15f); // check on Animator if it matches
                Instantiate(upSpellExplosion, transform);
                rb.velocity = Vector2.zero;
    
                Mana -= manaSpellCost;
                yield return new WaitForSeconds(0.35f); // check Animator
            }
    
            //down cast
            else if ((yAxis < 0 && !Grounded()) && unlockedDownCast)
            {
    
                anim.SetBool("Casting", true);
                yield return new WaitForSeconds(0.15f); // check on Animator if it matches
                downSpellFireball.SetActive(true);
    
                Mana -= manaSpellCost;
                yield return new WaitForSeconds(0.35f); // check Animator
            }
    
    
            anim.SetBool("Casting", false);
            pState.casting = false;
        }
        // jumping methods
        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 && jumpBufferCounter > 0 && coyoteTimeCounter > 0)
            {
                audioSource.PlayOneShot(jumpSound);
    
                rb.velocity = new Vector3(rb.velocity.x, jumpForce);
    
                pState.jumping = true;
            }
    
            if (!Grounded() && airJumpCounter < maxAirJumps && Input.GetButtonDown("Jump") && unlockedVarJump)
            {
                audioSource.PlayOneShot(jumpSound);
    
                pState.jumping = true;
    
                airJumpCounter++;
    
                rb.velocity = new Vector3(rb.velocity.x, jumpForce);
            }
    
            if (Input.GetButtonUp("Jump") && rb.velocity.y > 3)
            {
                rb.velocity = new Vector2(rb.velocity.x, 0);
    
                pState.jumping = false;
            }
    
            anim.SetBool("Jumping", !Grounded());
        }
    
        void UpdateJumpVariables()
        {
            if (Grounded())
            {
                if (!landingSoundPlayed)
                {
                    audioSource.PlayOneShot(landingSound);
                    landingSoundPlayed = true;
                }
                pState.jumping = false;
                coyoteTimeCounter = coyoteTime;
                airJumpCounter = 0;
            }
            else
            {
                coyoteTimeCounter -= Time.deltaTime;
                landingSoundPlayed = false;
            }
    
            if (Input.GetButtonDown("Jump"))
            {
                jumpBufferCounter = jumpBufferFrames;
            }
            else
            {
                jumpBufferCounter--;
            }
        }
        // wall jumping methods
        private bool Walled()
        {
            return Physics2D.OverlapCircle(wallCheck.position, 0.2f, wallLayer);
        }
    
        void WallSlide()
        {
            if(Walled() && !Grounded() && xAxis != 0)
            {
                isWallSliding = true;
    
                // set player speed to wallsliding speed
                rb.velocity = new Vector2(rb.velocity.x, Mathf.Clamp(rb.velocity.y, -wallSlidingSpeed, float.MaxValue));
            }
            else
            {
                isWallSliding = false;
            }
        }
    
        void WallJump()
        {
            if (isWallSliding)
            {
                isWallJumping = false;
                wallJumpingDirection = !pState.lookingRight ? 1 : -1;
    
                CancelInvoke(nameof(StopWallJumping));
            }
    
            if (Input.GetButtonDown("Jump") && isWallSliding)
            {
                isWallJumping = true;
                rb.velocity = new Vector2(wallJumpingDirection * wallJumpingPower.x, wallJumpingPower.y);
    
                dashed = false;
                airJumpCounter = 0;
    
                if ((pState.lookingRight && transform.eulerAngles.y == 0) || (!pState.lookingRight && transform.eulerAngles.y != 0))
                {
                    pState.lookingRight = !pState.lookingRight;
                    int _yRotation = pState.lookingRight ? 0 : 180;
    
                    transform.eulerAngles = new Vector2(transform.eulerAngles.x, _yRotation);
                }
    
                Invoke(nameof(StopWallJumping), wallJumpingDuration);
            }
        }
    
        void StopWallJumping()
        {
            isWallJumping = false;
        }
    }</code>

    Enemy

    <code>using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class Enemy : MonoBehaviour
    {
        [SerializeField] protected float health;
        [SerializeField] protected float recoilLength;
        [SerializeField] protected float recoilFactor;
        [SerializeField] protected bool isRecoiling = false;
    
        [SerializeField] public float speed;
    
        [SerializeField] public float damage;
        [SerializeField] protected GameObject orangeBlood;
    
        [SerializeField] AudioClip hurtSound;
    
        protected float recoilTimer;
        [HideInInspector] public Rigidbody2D rb;
        protected SpriteRenderer sr;
        public Animator anim;
        protected AudioSource audioSource;
    
        protected bool hasTakenDamage = false;
        protected EnemyStates currentEnemyState;
        protected enum EnemyStates
        {
            // Crawler
            Crawler_Idle,
            Crawler_Flip,
    
            // Bat
            Bat_Idle,
            Bat_Chase,
            Bat_Stunned,
            Bat_Death,
    
            // Charger
            Charger_Idle,
            Charger_Surprised,
            Charger_Charge,
    
            // Boss
            Boss_Stage1,
            Boss_Stage2,
            Boss_Stage3,
            Boss_Stage4,
        }
    
        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>();
            audioSource = GetComponent<AudioSource>();
        }
        // Update is called once per frame
        protected virtual void Update()
        {
            if (GameManager.Instance.gameIsPaused) return;
    
            hasTakenDamage = false;
    
    
            if (isRecoiling)
            {
                if (recoilTimer < recoilLength)
                {
                    recoilTimer += Time.deltaTime;
                }
                else
                {
                    isRecoiling = false;
                    recoilTimer = 0;
                }
            }
            else
            {
                UpdateEnemyStates();
            }
        }
    
        public virtual void EnemyHit(float _damageDone, Vector2 _hitDirection, float _hitForce)
        {
            if (hasTakenDamage) return;
    
            health -= _damageDone;
            if (!isRecoiling)
            {
                audioSource.PlayOneShot(hurtSound);
                GameObject _orangeBlood = Instantiate(orangeBlood, transform.position, Quaternion.identity);
                Destroy(_orangeBlood, 5.5f);
                rb.velocity = _hitForce * recoilFactor * _hitDirection;
            }
            hasTakenDamage = true;
    
        }
        protected virtual void OnCollisionStay2D(Collision2D _other)
        {
            if (_other.gameObject.CompareTag("Player") && !PlayerController.Instance.pState.invincible && health > 0)
            {
                Attack();
    
                if(PlayerController.Instance.pState.alive)
                {
                    PlayerController.Instance.HitStopTime(0, 5, 0.5f);
                }
    
    
            }
    
            if (_other.gameObject.CompareTag("Enemy"))
            {
                transform.localScale = new Vector2(transform.localScale.x * -1, transform.localScale.y);
            }
    
        }
    
        protected virtual void Death(float _destroyTime)
        {
            Destroy(gameObject, _destroyTime);
        }
    
        protected virtual void UpdateEnemyStates() {}
    
        protected virtual void ChangeCurrentAnimation() { }
        protected void ChangeState(EnemyStates _newState)
        {
            GetCurrentEnemyState = _newState;
        }
        protected virtual void Attack()
        {
            PlayerController.Instance.TakeDamage(damage);
            PlayerController.Instance.HitStopTime(0, 5, 0.5f);
        }
    
    }
    </code>

    SpawnBoss

    <code>using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class SpawnBoss : MonoBehaviour
    {
        public static SpawnBoss Instance;
        [SerializeField] Transform spawnPoint;
        [SerializeField] GameObject boss;
        [SerializeField] Vector2 exitDirection;
        bool callOnce;
        BoxCollider2D col;
    
        private void Awake()
        {
            if (Instance != null && Instance != this)
            {
                Destroy(gameObject);
            }
            else
            {
                Instance = this;
            }
        }
    
        void Start()
        {
            col = GetComponent<BoxCollider2D>();
        }
    
        private void OnTriggerEnter2D(Collider2D _other)
        {
            if (_other.CompareTag("Player"))
            {
                if (!callOnce)
                {
                    StartCoroutine(WalkIntoRoom());
                    callOnce = true;
                }
            }
        }
    
        IEnumerator WalkIntoRoom()
        {
            StartCoroutine(PlayerController.Instance.WalkIntoNewScene(exitDirection, 1));
            yield return new WaitForSecondsRealtime(1f);
            col.isTrigger = false;
            Instantiate(boss, spawnPoint.position, Quaternion.identity);
        }
    
        public void IsNotTrigger()
        {
            col.isTrigger = true;
        }
    }
    </code>

    Boss_Idle

    <code>using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class Boss_Idle : StateMachineBehaviour
    {
        public Rigidbody2D rb;
    
        // OnStateEnter is called when a transition starts and the state machine starts to evaluate this state
        override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {
            rb = animator.GetComponentInParent<Rigidbody2D>();
        }
    
        // OnStateUpdate is called on each Update frame between OnStateEnter and OnStateExit callbacks
        override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {
            rb.velocity = Vector2.zero;
            RunToPlayer(animator);
    
            if (BossController.Instance.attackCountdown <= 0)
            {
                BossController.Instance.AttackHandler();
                BossController.Instance.attackCountdown = Random.Range(BossController.Instance.attackTimer - 1, BossController.Instance.attackTimer + 1);
            }
        }
    
        void RunToPlayer(Animator animator)
        {
            if (Vector2.Distance(PlayerController.Instance.transform.position, rb.position) >= BossController.Instance.attackRange)
            {
                animator.SetBool("Run", true);
            }
            else
            {
                return;
            }
        }
    
        // OnStateExit is called when a transition ends and the state machine finishes evaluating this state
        override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {
    
        }
    }</code>

    Boss_Run

    <code>using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class Boss_Run : StateMachineBehaviour
    {
        Rigidbody2D rb;
    
        // OnStateEnter is called when a transition starts and the state machine starts to evaluate this state
        override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {
            rb = animator.GetComponentInParent<Rigidbody2D>();
        }
    
        // OnStateUpdate is called on each Update frame between OnStateEnter and OnStateExit callbacks
        override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {
            TargetPlayerPosition(animator);
    
            if (BossController.Instance.attackCountdown <= 0)
            {
                BossController.Instance.AttackHandler();
                BossController.Instance.attackCountdown = Random.Range(BossController.Instance.attackTimer - 1, BossController.Instance.attackTimer + 1);
            }
        }
    
        void TargetPlayerPosition(Animator animator)
        {
            if (BossController.Instance.Grounded())
            {
                BossController.Instance.Flip();
                Vector2 _target = new Vector2(PlayerController.Instance.transform.position.x, rb.position.y);
                Vector2 _newPos = Vector2.MoveTowards(rb.position, _target, BossController.Instance.runSpeed * Time.fixedDeltaTime);
                //BossController.Instance.runSpeed = BossController.Instance.speed;
                rb.MovePosition(_newPos);
            }
            else
            {
                rb.velocity = new Vector2(rb.velocity.x, -25);
            }
    
            if (Vector2.Distance(PlayerController.Instance.transform.position, rb.position) <= BossController.Instance.attackRange)
            {
                animator.SetBool("Run", false);
            }
            else
            {
                return;
            }
        }
    
        // OnStateExit is called when a transition ends and the state machine finishes evaluating this state
        override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {
            animator.SetBool("Run", false);
        }
    }</code>

    Boss_Lunge

    <code>using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class Boss_Lunge : StateMachineBehaviour
    {
        Rigidbody2D rb;
    
        // OnStateEnter is called when a transition starts and the state machine starts to evaluate this state
        override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {
            rb = animator.GetComponentInParent<Rigidbody2D>();
        }
    
        // OnStateUpdate is called on each Update frame between OnStateEnter and OnStateExit callbacks
        override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {
            rb.gravityScale = 0;
            int _dir = BossController.Instance.facingRight ? 1 : -1;
            rb.velocity = new Vector2(_dir * (BossController.Instance.speed * 5), 0f);
    
            if (Vector2.Distance(PlayerController.Instance.transform.position, rb.position) <=
                BossController.Instance.attackRange && !BossController.Instance.damagedPlayer)
            {
                PlayerController.Instance.TakeDamage(BossController.Instance.damage);
                BossController.Instance.damagedPlayer = true;
            }
        }
    
        void TargetPlayerPosition(Animator animator)
        {
    
        }
    
        // OnStateExit is called when a transition ends and the state machine finishes evaluating this state
        override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {
    
        }
    }
    </code>

    Boss_Jump

    <code>using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class Boss_Jump : StateMachineBehaviour
    {
        Rigidbody2D rb;
    
        // OnStateEnter is called when a transition starts and the state machine starts to evaluate this state
        override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {
            rb = animator.GetComponentInParent<Rigidbody2D>();
        }
    
        // OnStateUpdate is called on each Update frame between OnStateEnter and OnStateExit callbacks
        override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {
            DiveAttack();
        }
    
        void DiveAttack()
        {
            if (BossController.Instance.diveAttack)
            {
                BossController.Instance.Flip();
    
                Vector2 _newPos = Vector2.MoveTowards(rb.position, BossController.Instance.moveToPosition,
                    BossController.Instance.speed * 3 * Time.fixedDeltaTime);
                rb.MovePosition(_newPos);
    
                float _distance = Vector2.Distance(rb.position, _newPos);
                if (_distance < 0.1f)
                {
                    BossController.Instance.Dive();
                }
            }
        }
    
        // OnStateExit is called when a transition ends and the state machine finishes evaluating this state
        override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {
    
        }
    }</code>

    DivingPillar

    <code>using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class DivingPillar : MonoBehaviour
    {
        private void OnTriggerEnter2D(Collider2D _other)
        {
            if (_other.CompareTag("Player"))
            {
                _other.GetComponent<PlayerController>().TakeDamage(BossController.Instance.damage);
            }
        }
    }</code>

    Boss_Dive

    <code>using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class Boss_Dive : StateMachineBehaviour
    {
        Rigidbody2D rb;
        bool callOnce;
    
        // OnStateEnter is called when a transition starts and the state machine starts to evaluate this state
        override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {
            rb = animator.GetComponentInParent<Rigidbody2D>();
        }
    
        // OnStateUpdate is called on each Update frame between OnStateEnter and OnStateExit callbacks
        override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {
            BossController.Instance.divingCollider.SetActive(true);
    
            if (BossController.Instance.Grounded())
            {
                BossController.Instance.divingCollider.SetActive(false);
    
                if (!callOnce)
                {
                    GameObject _impactParticle = Instantiate(BossController.Instance.impactParticle,
                        BossController.Instance.groundCheckPoint.position, Quaternion.identity);
                    Destroy(_impactParticle, 4f);
                    BossController.Instance.DivingPillars();
                    animator.SetBool("Dive", false);
                    BossController.Instance.ResetAllAttacks();
                    callOnce = true;
                }
    
            }
        }
    
        // OnStateExit is called when a transition ends and the state machine finishes evaluating this state
        override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {
            callOnce = false;
        }
    }</code>

    Boss_BendDown

    <code>using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class Boss_BendDown : StateMachineBehaviour
    {
        Rigidbody2D rb;
    
        // OnStateEnter is called when a transition starts and the state machine starts to evaluate this state
        override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {
            rb = animator.GetComponentInParent<Rigidbody2D>();
        }
    
        // OnStateUpdate is called on each Update frame between OnStateEnter and OnStateExit callbacks
        override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {
    
        }
    
        void OutbreakAttack()
        {
            if (BossController.Instance.outbreakAttack)
            {
                Vector2 _newPos = Vector2.MoveTowards(rb.position, BossController.Instance.moveToPosition,
                    BossController.Instance.speed * 1.5f * Time.fixedDeltaTime);
                rb.MovePosition(_newPos);
    
                float _distance = Vector2.Distance(rb.position, _newPos);
                if (_distance < 0.1f)
                {
                    BossController.Instance.rb.constraints = RigidbodyConstraints2D.FreezePosition;
                }
            }
        }
    
        // OnStateExit is called when a transition ends and the state machine finishes evaluating this state
        override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {
            animator.ResetTrigger("BendDown");
        }
    }</code>

    Boss_Bounce1

    <code>using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class Boss_Bounce1 : StateMachineBehaviour
    {
        Rigidbody2D rb;
    
        // OnStateEnter is called when a transition starts and the state machine starts to evaluate this state
        override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {
            rb = animator.GetComponentInParent<Rigidbody2D>();
        }
    
        // OnStateUpdate is called on each Update frame between OnStateEnter and OnStateExit callbacks
        override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {
            if (BossController.Instance.bounceAttack)
            {
                Vector2 _newPos = Vector2.MoveTowards(rb.position, BossController.Instance.moveToPosition,
                    BossController.Instance.speed * Random.Range(2, 4) * Time.fixedDeltaTime);
                rb.MovePosition(_newPos);
    
                float _distance = Vector2.Distance(rb.position, _newPos);
                if (_distance < 0.1f)
                {
                    BossController.Instance.CalculateTargetAngle();
                    animator.SetTrigger("Bounce2");
                }
            }
        }
    
        // OnStateExit is called when a transition ends and the state machine finishes evaluating this state
        override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {
            animator.ResetTrigger("Bounce1");
        }
    }</code>

    Boss_Bounce2

    <code>using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class Boss_Bounce2 : StateMachineBehaviour
    {
        Rigidbody2D rb;
        bool callOnce;
    
        // OnStateEnter is called when a transition starts and the state machine starts to evaluate this state
        override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {
            rb = animator.GetComponentInParent<Rigidbody2D>();
        }
    
        // OnStateUpdate is called on each Update frame between OnStateEnter and OnStateExit callbacks
        override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {
            Vector2 _forceDirection = new Vector2(Mathf.Cos(Mathf.Deg2Rad * BossController.Instance.rotationDirectionToTarget),
                Mathf.Sin(Mathf.Deg2Rad * BossController.Instance.rotationDirectionToTarget));
            rb.AddForce(_forceDirection * 3, ForceMode2D.Impulse);
    
            BossController.Instance.divingCollider.SetActive(true);
    
            if (BossController.Instance.Grounded())
            {
                BossController.Instance.divingCollider.SetActive(false);
                if (!callOnce)
                {
                    GameObject _impactParticle = Instantiate(BossController.Instance.impactParticle,
                        BossController.Instance.groundCheckPoint.position, Quaternion.identity);
                    Destroy(_impactParticle, 4f);
    
                    BossController.Instance.ResetAllAttacks();
                    BossController.Instance.CheckBounce();
                    callOnce = true;
                }
    
                animator.SetTrigger("Grounded");
            }
    
        }
    
        // OnStateExit is called when a transition ends and the state machine finishes evaluating this state
        override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {
            animator.ResetTrigger("Bounce2");
            animator.ResetTrigger("Grounded");
            callOnce = false;
        }
    }</code>

    BarrageFireball

    <code>using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class BarrageFireball : MonoBehaviour
    {
        [SerializeField] Vector2 startForceMinMax;
        [SerializeField] float turnSpeed = 0.5f;
    
        Rigidbody2D rb;
    
        void Start()
        {
            rb = GetComponent<Rigidbody2D>();
            Destroy(gameObject, 4f);
            rb.AddForce(transform.right * Random.Range(startForceMinMax.x, startForceMinMax.y), ForceMode2D.Impulse);
        }
    
        void Update()
        {
            var _dir = rb.velocity;
    
            if (_dir != Vector2.zero)
            {
                Vector3 _frontVector = Vector3.right;
    
                Quaternion _targetRotation = Quaternion.FromToRotation(_frontVector, _dir - (Vector2)transform.position);
                if (_dir.x > 0)
                {
                    transform.rotation = Quaternion.Lerp(transform.rotation, _targetRotation, turnSpeed);
                    transform.eulerAngles = new Vector3(transform.eulerAngles.x, 180, transform.eulerAngles.z);
                }
                else
                {
                    transform.rotation = Quaternion.Lerp(transform.rotation, _targetRotation, turnSpeed);
                }
            }
        }
    
        private void OnTriggerEnter2D(Collider2D _other)
        {
            if (_other.tag == "Player")
            {
                _other.GetComponent<PlayerController>().TakeDamage(BossController.Instance.damage);
                Destroy(gameObject);
            }
        }
    }</code>
    #13495
    Allan Valin
    Level 5
    Participant
    Helpful?
    Up
    0
    ::

    Apparently there’s a time limit to edit posts. I forgot to add a greeting and talk about not having compile errors with the boss’ codes (that is, until I did what is mentioned in the post).

    Oh yeah, as you can see in the videos, the Fireball seems to stop after a while.

    <code>using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class FireBall : MonoBehaviour
    {
        [SerializeField] float damage;
        [SerializeField] float hitForce;
        [SerializeField] int speed;
        [SerializeField] float lifetime = 1;
        // Start is called before the first frame update
        void Start()
        {
            Destroy(gameObject, lifetime);
        }
    
        private void FixedUpdate()
        {
            transform.position += speed * transform.right;
        }
        //detect hit
        private void OnTriggerEnter2D(Collider2D _other)
        {
            if (_other.tag == "Enemy")
            {
                _other.GetComponent<Enemy>().EnemyHit(damage, (_other.transform.position - transform.position).normalized, -hitForce);
            }
        }
    }</code>
    #13499
    Terence
    Level 30
    Keymaster
    Helpful?
    Up
    0
    ::

    Let’s tackle the easier ones first:

    Attacking the boss makes it climb the player sword’s collider.
    Make sure that your player’s sword collider is a trigger (has Is Trigger marked).
    Some animation transitions don’t seem to work. Barrage and Flame Pillar aren’t activating apparently.
    You can put your Animator beside your Game view, and see how the animation transitions are firing. For those animations that aren’t playing, check the transitions and see whether the conditions are correct. Check whether your Exit Time settings are correct as well.
    When bouncing, the boss might clip through the ground.
    Make the boss’s Rigidbody have a Collision Detection mode of Continuous.

    To read more about this: https://blog.terresquall.com/2019/12/collision-detection-modes-in-unitys-rigidbody-component/

    #13502
    Allan Valin
    Level 5
    Participant
    Helpful?
    Up
    0
    ::

    I’m responding to this, but a bit thinking on the answer for part 7 or 9 you just wrote (about Hit() ).

    The Collision Detection was indeed Discrete. So I assume it wasn’t mentioned on the first and last videos of the series lol (on the first one I might have messed up, but I don’t think it was the case on the last).

    About the sword colliders, they don’t really exist! The code generates them with ray cast or something, I’m assuming.

    From all the children objects under the Player, GroundCheck, Wall Check, Side Attack Transform, Up Attack Transform, Down Attack Transform, and Down Spell Fireball, only the last one has any component attached to it (Sprite Renderer, Circle Colider 2D and Animator), all the others only have a Transform. So there’s literally no collider to speak of where I could check IsTrigger.

    About the Animator on the boss, I need to check while it runs, but all transitions appear to be correct tbh. I used the exact values shown on the video for the transitions that should have an Exit Time. The only one I’m not sure is Boss_Death1. I use Die() to get to it, but there’s no conditions on the transitions to Boss_Death2, and from 2 to 3, but it only player until the 2. I might do a video on that later to show it, need to go to the gym before I go crazy first XD

    #13503
    Allan Valin
    Level 5
    Participant
    Helpful?
    Up
    0
    ::

    btw, what was the variable that defines the attack speed of the player again? I can’t seem to find it. Because of the recoil I can make the player fly of dig through a ground collider (discrete detection, gotta test with continuous).

    #13511
    Terence
    Level 30
    Keymaster
    Helpful?
    Up
    0
    ::
    btw, what was the variable that defines the attack speed of the player again? I can’t seem to find it. Because of the recoil I can make the player fly of dig through a ground collider (discrete detection, gotta test with continuous).

    I think the variable you are looking for is timeSinceAttack.

    #13512
    Terence
    Level 30
    Keymaster
    Helpful?
    Up
    0
    ::
    About the sword colliders, they don’t really exist! The code generates them with ray cast or something, I’m assuming.

    From all the children objects under the Player, GroundCheck, Wall Check, Side Attack Transform, Up Attack Transform, Down Attack Transform, and Down Spell Fireball, only the last one has any component attached to it (Sprite Renderer, Circle Colider 2D and Animator), all the others only have a Transform. So there’s literally no collider to speak of where I could check IsTrigger.

    I just checked. You are right. If we are able to rule out colliders, then another possibility is that the displacement might be caused by your animation instead of a collider.

    About the Animator on the boss, I need to check while it runs, but all transitions appear to be correct tbh. I used the exact values shown on the video for the transitions that should have an Exit Time. The only one I’m not sure is Boss_Death1. I use Die() to get to it, but there’s no conditions on the transitions to Boss_Death2, and from 2 to 3, but it only player until the 2. I might do a video on that later to show it, need to go to the gym before I go crazy first XD
    It may also be that the Parameters are not being fired / toggled as expected, so if the transitions are not firing, observe what your Parameters as the boss fight unfolds.

    Enjoy your gym sesh!

    #13513
    Allan Valin
    Level 5
    Participant
    Helpful?
    Up
    0
    ::

    Hey, ChatGPT fixed the Flipfunction for me, turns out you need to declare a Vector3 scale = transform.localScale; so that so that localScale works properly.

    <code>public void Flip()
        {
            // Get the local scale of the boss object
            Vector3 scale = transform.localScale;
    
            // If the player is to the left of the boss and the boss is facing right
            if (PlayerController.Instance.transform.position.x < transform.position.x && scale.x > 0)
            {
                // Flip the boss by negating the x scale
                scale.x = -scale.x;
                // Update the local scale
                transform.localScale = scale;
                facingRight = false;
            }
            // If the player is to the right of the boss and the boss is facing left
            else if (PlayerController.Instance.transform.position.x > transform.position.x && scale.x < 0)
            {
                // Flip the boss by negating the x scale
                scale.x = -scale.x;
                // Update the local scale
                transform.localScale = scale;
                facingRight = true;
            }
        }
    </code>

    On another matter, on the Lunge animation, should I alter the x value so that the Boss lunges forward or the code should to that? Because my boss is just thrusting forward without moving.

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

    ChatGPT is amazing isn’t it?

    #13515
    Terence
    Level 30
    Keymaster
    Helpful?
    Up
    0
    ::
    On another matter, on the Lunge animation, should I alter the x value so that the Boss lunges forward or the code should to that? Because my boss is just thrusting forward without moving.
    If you are asking whether you should do it in the animation or in the code, I suggest doing it in the code, unless the tutorial has guided you to set up root motion for your animations (I assume our Metroidvania series doesn’t do that).
    #13516
    Allan Valin
    Level 5
    Participant
    Helpful?
    Up
    0
    ::

    You can look in the BossController code above, the method should do that tbh (it doesn’t deal damage as well, as mentioned before). I tried moving it on the animation, but then the collider doesn’t seem to follow the sprite and the boss teleports back to the initial position after the animation ends.

    #13518
    Allan Valin
    Level 5
    Participant
    Helpful?
    Up
    0
    ::

    Damn, chatGPT is saving the day here, sharing some updates.

    The video shows this code, which basically makes the boss not deal damage properly when touching the player.

    <code>protected override void OnCollisionStay2D(Collision2D _other)
        {
            //base.OnCollisionStay2D(_other);
        }
    </code>

    Updated Flip

    <code>public void Flip()
        {
            // Get the local scale of the boss object
            Vector3 scale = transform.localScale;
    
            // If the player is to the left of the boss and the boss is facing right
            if (PlayerController.Instance.transform.position.x < transform.position.x && scale.x > 0)
            {
                // Flip the boss by negating the x scale
                scale.x = -scale.x;
                // Update the local scale
                transform.localScale = scale;
                facingRight = false; // Update facingRight
            }
            // If the player is to the right of the boss and the boss is facing left
            else if (PlayerController.Instance.transform.position.x > transform.position.x && scale.x < 0)
            {
                // Flip the boss by negating the x scale
                scale.x = -scale.x;
                // Update the local scale
                transform.localScale = scale;
                facingRight = true; // Update facingRight
            }
        }
    </code>

    Updated Lunge (with new method to deal damage, not sure if needed or working tbh)

    <code>IEnumerator Lunge()
    {
        Flip(); // Flip the boss if necessary
        attacking = true;
    
        // Set the "Lunge" animation trigger
        anim.SetBool("Lunge", true);
    
        // Get the starting position of the boss
        Vector2 startPosition = rb.position;
    
        // Calculate the direction to the player
        Vector2 directionToPlayer = (PlayerController.Instance.transform.position - transform.position).normalized;
    
        // Calculate the target position by moving 15 units towards the player on the x-axis
        Vector2 targetPosition = new Vector2(startPosition.x + (facingRight ? 15f : -15f), startPosition.y);
    
        // Get the duration of the animation
        float animationDuration = anim.GetCurrentAnimatorStateInfo(0).length;
    
        // Define the time elapsed
        float elapsedTime = 0f;
    
        // Define if the damage has been dealt
        bool damageDealt = false;
    
        // Move the boss gradually towards the target position
        while (elapsedTime < animationDuration)
        {
            // Calculate the interpolation factor
            float t = elapsedTime / animationDuration;
    
            // Interpolate the boss position between the start and target positions
            rb.MovePosition(Vector2.Lerp(startPosition, targetPosition, t));
    
            // Check if it's time to deal damage
            if (!damageDealt && t >= 0.5f) // Adjust the timing as needed
            {
                DealDamageToPlayer();
                damageDealt = true;
            }
    
            // Update the elapsed time
            elapsedTime += Time.deltaTime;
    
            // Wait for the next frame
            yield return null;
        }
    
        // Reset the "Lunge" animation trigger
        anim.SetBool("Lunge", false);
    
        // Reset attacking state
        attacking = false;
    
        // Freeze the boss's position until the coroutine completes
        while (true)
        {
            // Ensure the boss stays at the target position
            rb.MovePosition(targetPosition);
    
            // Wait for the next frame
            yield return null;
        }
    }
    
    void DealDamageToPlayer()
    {
        // Check if the player is in range
        if (Vector2.Distance(transform.position, PlayerController.Instance.transform.position) <= attackRange)
        {
            // Deal damage to the player
            PlayerController.Instance.TakeDamage(damage);
        }
    }
    </code>

    Updated OnTriggerEnted2D

    <code>private void OnTriggerEnter2D(Collider2D _other)
        {
            if (_other.CompareTag("Player") && ((_other.transform.position.x < transform.position.x && facingRight) ||
                (_other.transform.position.x > transform.position.x && !facingRight)))
            {
                PlayerController.Instance.TakeDamage(damage);
            }
        }
    </code>

    I’m checking the player as well to see if I solve the being hit by himself when attacking problem. Will post if I manage to fix it.

    I found out a bug with wall jumping btw. If you land on top of a wall after climbing it, you’d end up flipped on the wrong side.

    <code>void WallSlide()
        {
            if (Walled() && !Grounded() && xAxis != 0)
            {
                isWallSliding = true;
    
                // Set player speed to wall sliding speed
                rb.velocity = new Vector2(rb.velocity.x, Mathf.Clamp(rb.velocity.y, -wallSlidingSpeed, float.MaxValue));
    
                // Update player orientation based on the wall side
                if (xAxis < 0 && pState.lookingRight)
                {
                    Flip();
                }
                else if (xAxis > 0 && !pState.lookingRight)
                {
                    Flip();
                }
            }
            else
            {
                isWallSliding = false;
            }
        }
    
    void WallJump()
        {
            if (isWallSliding)
            {
                isWallJumping = false;
                wallJumpingDirection = !pState.lookingRight ? 1 : -1;
    
                // Update player orientation based on the wall side
                if ((wallJumpingDirection > 0 && !pState.lookingRight) || (wallJumpingDirection < 0 && pState.lookingRight))
                {
                    Flip();
                }
    
                CancelInvoke(nameof(StopWallJumping));
            }
    
            if (Input.GetButtonDown("Jump") && isWallSliding)
            {
                isWallJumping = true;
                rb.velocity = new Vector2(wallJumpingDirection * wallJumpingPower.x, wallJumpingPower.y);
    
                dashed = false;
                airJumpCounter = 0;
    
                // Update player orientation based on the wall side
                if ((wallJumpingDirection > 0 && !pState.lookingRight) || (wallJumpingDirection < 0 && pState.lookingRight))
                {
                    Flip();
                }
    
                Invoke(nameof(StopWallJumping), wallJumpingDuration);
            }
        }</code>
    #13519
    Terence
    Level 30
    Keymaster
    Helpful?
    Up
    0
    ::

    Thanks for sharing your findings Allan. I’m glad ChatGPT managed to help you with it.

    #13530
    Allan Valin
    Level 5
    Participant
    Helpful?
    Up
    0
    ::

    I managed to solve the Hit problem by starting from scratch using a real trigger collider, I used code from other tutorials, so it would take way too long to show all the changes to make the player stop taking damage.

    Two other things I noticed when messing up with that:

    • timeBetweenAttack is declared as private without [SerializeField] and has never a value assigned to it, that made the attacks not be spaced out.
    • Player animation: add transition from Jump attack to Idle if jumping is false (I think the video didn’t show that and it sometimes makes the player stuck on the jump attack animation).

    btw I started using github Copilot, you can assign which files it can access for cross-reference and it sometimes helps a lot. But often, as it is with AI, if you don’t know where the problem is, it can’t find it that easily lol

    #13537
    Terence
    Level 30
    Keymaster
    Helpful?
    Up
    0
    ::
    btw I started using github Copilot, you can assign which files it can access for cross-reference and it sometimes helps a lot. But often, as it is with AI, if you don’t know where the problem is, it can’t find it that easily lol
    That’s amazing. Perhaps it is something I should try as well.
Viewing 15 posts - 1 through 15 (of 20 total)
  • You must be logged in to reply to this topic.

Go to Login Page →


Advertisement below: