Forum begins after the advertisement:


[Part 10] Article Changes, Common Issues & Bugfixes

Home Forums Video Game Tutorial Series Creating a Metroidvania in Unity [Part 10] Article Changes, Common Issues & Bugfixes

Viewing 1 post (of 1 total)
  • Author
    Posts
  • #14716
    Joseph Tang
    Moderator

    This is a supplementary post written for Part 10 of our Metroidvania series, and it aims to address 2 things:

    1. Missing information in the video, and;
    2. Address common issues / questions that readers run into, and possible solutions for these questions.

    For convenience, below are the links to the article and the video:


    Table of Contents

    Missing Information in the Video

    1. Dive and Barrage not functioning while nearby boss
    2. Boss gets stuck on Walls
    3. Player does not enter invincibility nor stop time when hit
    4. Boss can be hit during death animation and animation is reset
    5. Player can activate SpawnBoss without entering and is locked out
    6. Boss remains after player dies and respawns (+ Saving boss defeated progression)

    Common Issues

    1. Boss is not taking damage

    [Part 10] Boss Fight

    Article Changes

    1. Dive and Barrage not functioning while nearby boss

    [If your boss is not falling down correctly, this is caused by missing code in Boss_Idle.cs to reset the falling velocity of the boss.]

    • Add an if statement to the Boss_Idle.cs OnStateUpdate() to check for TheHollowKnight.cs Grounded().
    • If false, add a line to set the rb.velocity to a new Vector2() with a negative y value of [-25].
       override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {
            rb.velocity = Vector2.zero;
            RunToPlayer(animator);
    
            if (TheHollowKnight.Instance.attackCountdown <= 0)
            {
                TheHollowKnight.Instance.AttackHandler();
                TheHollowKnight.Instance.attackCountdown = Random.Range(TheHollowKnight.Instance.attackTimer - 1, TheHollowKnight.Instance.attackTimer + 1);
            }
    
            if (!TheHollowKnight.Instance.Grounded())
            {
                rb.velocity = new Vector2(rb.velocity.x, -25); //if knight is not grounded, fall to ground
            }
        }

    2. Boss gets stuck on Walls

    [This is caused by inefficient code, the boss wants to travel to it’s target moveToPosition but cannot reach it due to the wall.]

    • Add a new Transform variable to TheHollowKnight.cs called wallCheckPoint like the groundCheckPoint.
    • Then, duplicate the public bool Grounded() method and rename it TouchedWall().
    • Replace all groundCheckPoint mentioned in TouchedWall() to wallCheckPoint and remove the grounded variables.
    • Now, in three scripts: Boss_BendDown, Boss_Jump, Boss_Bounce1, add a new if statement to their OnStateUpdate() or primary attack methods that will check for TheHollowKnight.Instance.TouchedWall()
    • In the new if statement, set the moveToPosition.x value to the current rb.position.x value, before resetting the _newPos. This will allow the boss to continue the attack by flying upwards when contacting a wall.

    TheHollowKnight.cs

    public Transform wallCheckPoint; //point at which wall check happens
    
        public bool TouchedWall()
        {
            if (Physics2D.Raycast(wallCheckPoint.position, Vector2.down, groundCheckY, whatIsGround)
        || Physics2D.Raycast(wallCheckPoint.position + new Vector3(groundCheckX, 0, 0), Vector2.down, groundCheckY, whatIsGround)
        || Physics2D.Raycast(wallCheckPoint.position + new Vector3(-groundCheckX, 0, 0), Vector2.down, groundCheckY, whatIsGround))
            {
                return true;
            }
            else
            {
                return false;
            }
        }

    Boss_Bounce1.cs , Boss_Jump.cs , Boss_BendDown.cs

                Vector2 _newPos = Vector2.MoveTowards(rb.position, TheHollowKnight.Instance.moveToPosition,
                    TheHollowKnight.Instance.speed * 1.5f * Time.fixedDeltaTime);
                rb.MovePosition(_newPos);
    
                if (TheHollowKnight.Instance.TouchedWall())
                {
                    TheHollowKnight.Instance.moveToPosition.x = rb.position.x;
                    _newPos = Vector2.MoveTowards(rb.position, TheHollowKnight.Instance.moveToPosition,
                        TheHollowKnight.Instance.speed * 1.5f * Time.fixedDeltaTime);
                }
    
                float _distance = Vector2.Distance(rb.position, _newPos);

    3. Player does not enter invincibility nor stop time when hit

    [This is caused by missing code. All attacks are calling TakeDamage() to deal damage directly to the player, and the Enemy.cs OnCollisionStay2D is overridden without being inherited. Thus, nothing is calling the HitStopTime() function.

    • For the four damaging scripts: THKEvents, Boss_Lunge, DivingPillar, BarrageFireball, add a parameter to check if the player is invincible to their if statement containing the TakeDamage() line.
    • Then add another if statement in the right below the TakeDamage() line to check if the player is alive before calling the PlayerController’s HitStopTime().

    THKEvents.cs

        void Hit(Transform _attackTransform, Vector2 _attackArea)
        {
            Collider2D _objectsToHit = Physics2D.OverlapBox(_attackTransform.position, _attackArea, 0);
    
            if (_objectsToHit.GetComponent() != null && !PlayerController.Instance.pState.invincible)
            {
                _objectsToHit.GetComponent().TakeDamage(TheHollowKnight.Instance.damage);
                if (PlayerController.Instance.pState.alive)
                {
                    PlayerController.Instance.HitStopTime(0, 5, 0.5f);
                }
            }
        }

    The other three scripts follow the similar steps above


    4. Boss can be hit during death animation and animation is reset

    [This is caused by inefficient code. The EnemyGetsHit()/EnemyHit() method is still calling the Death() method everytime the boss is hit below a health value of [0].]

    • Add an additional parameter to the EnemyGetsHit() method’s if statement for calling Death() to check if the boss is alive. This will stop further hits off the boss from calling Death() again as the alive bool would have been set to false on the first call.
        public override void EnemyGetsHit(float _damageDone, Vector2 _hitDirection, float _hitForce)
        {
            ...
            if (health < 5)
            {
                ChangeState(EnemyStates.THK_Stage4);
            }
            if(health <= 0 && alive)
            {
                Death(0);
            }
    
            #endregion
    
        }

    5. Player can activate SpawnBoss without entering and is locked out

    [This is caused by inefficient code. The player can still influence the player movement and negate the velocity change in the WalkIntoRoom() method.]

    • ADd a line in the SpawnBoss.cs WalkIntoRoom() method to set the player’s cutscene state to true. this will prevent the player from doing anything while walking into the room and will be reset to false after the 1 second delay.
        IEnumerator WalkIntoRoom()
        {
            StartCoroutine(PlayerController.Instance.WalkIntoNewScene(exitDirection, 1));
            PlayerController.Instance.GetComponent().cutscene = true;
            yield return new WaitForSeconds(1f);
            col.isTrigger = false;
            Instantiate(boss, spawnPoint.position, Quaternion.identity);
        }

    6. Boss remains after player dies and respawns (+ Saving boss defeated progression).

    [This is caused by inefficient code and lack of a feature (Full explanation is in the article)]

    • Add a Save and Load boss data method in SaveData.cs and a new bool to track the clear/defeat status of the boss.
    • Add a bool in GameManager.cs to track the boss status as well, before calling to load the data from SaveData.cs and set the current defeat status of the boss.
    • Set the THKEvents.cs DestroyAfterDeath() to save the defeat status to GameManager.cs and call the save data of both boss data and player data from SaveData.cs
    • Set new code in the SpawnBoss.cs under Awake() method to remove the boss if it is present in the scene, and another if statement to check if the GameManager.cs boss defeated bool is true before setting callOnce to true.
    • Additionally, add a new parameter to the if statement of the OnTriggerEnter2D() method of SpawnBoss.cs to check if the boss status is defeated.

    SaveData.cs

        //TheHollowKnight
        public bool THKDefeated;
    
        public void SaveBossData()
        {
            using (BinaryWriter writer = new BinaryWriter(File.OpenWrite(Application.persistentDataPath + "/save.boss.data")))
            {
                THKDefeated = GameManager.Instance.THKDefeated;
    
                writer.Write(THKDefeated);
            }
        }
    
        public void LoadBossData()
        {
            if (File.Exists(Application.persistentDataPath + "/save.Boss.data"))
            {
                using (BinaryReader reader = new BinaryReader(File.OpenRead(Application.persistentDataPath + "/save.boss.data")))
                {
                    THKDefeated = reader.ReadBoolean();
    
                    GameManager.Instance.THKDefeated = THKDefeated;
                }
            }
            else
            {
                Debug.Log("Boss doesnt exist");
            }
        }

    GameManager.cs

    public class GameManager : MonoBehaviour
    {
        ...
    
        public GameObject shade;
    
        public bool THKDefeated = false;
    
        [SerializeField] FadeUI pauseMenu;
        [SerializeField] float fadeTime;
        public bool gameIsPaused;
    
        public static GameManager Instance { get; private set; }
        private void Awake()
        {
            SaveData.Instance.Initialize();
            
            if(Instance != null && Instance != this)
            ...
    
            if(PlayerController.Instance != null)
            {
                ...
            }
            
            SaveScene();
            DontDestroyOnLoad(gameObject);
            bench = FindObjectOfType();
    
            SaveData.Instance.LoadBossData();
        }

    THKEvents.cs

        void DestroyAfterDeath()
        {
            SpawnBoss.Instance.IsNotTrigger();
            TheHollowKnight.Instance.DestroyAfterDeath();
            GameManager.Instance.THKDefeated = true;
            SaveData.Instance.SaveBossData();
            SaveData.Instance.SavePlayerData();
        }

    SpawnBoss.cs

        private void Awake()
        {
            if (TheHollowKnight.Instance != null)
            {
                Destroy(TheHollowKnight.Instance);
                callOnce = false;
                col.isTrigger = true;
            }
    
            if (GameManager.Instance.THKDefeated)
            {
                callOnce = true;
            }
    
            if (Instance != null && Instance != this)
            {
                Destroy(gameObject);
            }
            else
            {
                Instance = this;
            }
        }
        // Start is called before the first frame update
        void Start()
        {
            col = GetComponent();
        }
    
        // Update is called once per frame
        void Update()
        {
            
        }
        private void OnTriggerEnter2D(Collider2D _other)
        {
            if(_other.CompareTag("Player") && !callOnce && !GameManager.Instance.THKDefeated)
            {
                StartCoroutine(WalkIntoRoom());
                callOnce = true;
            }
        }

    Common Issues

    7. Boss is not taking damage

    [This is most probably caused by a missing code]

    • Ensure that your TheHollowKnight.cs EnemyGetsHit()/EnemyHit() method has a statement to set parrying to false.
        public override void EnemyGetsHit(float _damageDone, Vector2 _hitDirection, float _hitForce)
        {
            if (!stunned)
            {
                if (!parrying)
                {
                    if(canStun)
                    {
                        hitCounter++;
                        if(hitCounter >= 3)
                        {
                            ResetAllAttacks();
                            StartCoroutine(Stunned());
                        }
                    }
                    ResetAllAttacks();
                    base.EnemyGetsHit(_damageDone, _hitDirection, _hitForce);
    
                    if (currentEnemyState != EnemyStates.THK_Stage4)
                    {
                        ResetAllAttacks(); //cancel any current attack to avoid bugs 
                        StartCoroutine(Parry());
                    }
    
                }
                else
                {
                    StopCoroutine(Parry());
                    parrying = false;
                    ResetAllAttacks();
                    StartCoroutine(Slash());  //riposte
                }
            }
            else
            {
                StopCoroutine(Stunned());
                anim.SetBool("Stunned", false);
                stunned = false;
            }
    
            #region health to state
            ...
            #endregion
        }


    That will be all for Part 10.
    Hopefully this can help you on any issues you may have. However, if you find that your issues weren’t addressed or is a unique circumstance, you can submit a forum post to go into detail on your problem for further assistance.

Viewing 1 post (of 1 total)
  • You must be logged in to reply to this topic.

Go to Login Page →


Advertisement below: