Forum begins after the advertisement:
[Part 10 Bug Fixes] Boss Issues
Home › Forums › Video Game Tutorial Series › Creating a Metroidvania in Unity › [Part 10 Bug Fixes] Boss Issues
- This topic has 0 replies, 1 voice, and was last updated 6 months, 3 weeks ago by Joseph Tang.
-
AuthorPosts
-
May 19, 2024 at 5:52 pm #14716::
This is a supplementary post written for Part 10 of our Metroidvania series, and it aims to address 2 things:
- Missing information in the video, and;
- 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:
- Article Link: https://blog.terresquall.com/2023/11/creating-a-metroidvania-like-hollow-knight-part-10/
- Video Link: https://youtu.be/rl_IlwBKqc8
Table of Contents
Missing Information in the Video
- Dive and Barrage not functioning while nearby boss
- Boss gets stuck on Walls
- Player does not enter invincibility nor stop time when hit
- Boss can be hit during death animation and animation is reset
- Player can activate SpawnBoss without entering and is locked out
- Boss remains after player dies and respawns (+ Saving boss defeated progression)
Common Issues
Missing Information in the Video
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.csGrounded()
. - If false, add a line to set the
rb.velocity
to anew 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 calledwallCheckPoint
like thegroundCheckPoint
. - Then, duplicate the
public bool Grounded()
method and rename itTouchedWall()
. - Replace all
groundCheckPoint
mentioned inTouchedWall()
towallCheckPoint
and remove thegrounded
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 forTheHollowKnight.Instance.TouchedWall()
- In the new if statement, set the
moveToPosition.x
value to the currentrb.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.csOnCollisionStay2D
is overridden without being inherited. Thus, nothing is calling theHitStopTime()
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’sHitStopTime()
.
THKEvents.cs
void Hit(Transform _attackTransform, Vector2 _attackArea) { Collider2D _objectsToHit = Physics2D.OverlapBox(_attackTransform.position, _attackArea, 0); if (_objectsToHit.GetComponent<PlayerController>() != null && !PlayerController.Instance.pState.invincible) { _objectsToHit.GetComponent<PlayerController>().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 theDeath()
method everytime the boss is hit below a health value of [0].]- Add an additional parameter to the
EnemyGetsHit()
method’s if statement for callingDeath()
to check if the boss isalive
. This will stop further hits off the boss from callingDeath()
again as thealive
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’scutscene
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.pState.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. We’ll only create the boss file when we use the Save boss data method to prevent errors.
- 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 settingcallOnce
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() { if (!File.Exists(Application.persistentDataPath + "/save.boss.data")) //if file doesnt exist, well create the file { BinaryWriter writer = new BinaryWriter(File.Create(Application.persistentDataPath + "/save.boss.data")); } 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<Bench>(); 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<BoxCollider2D>(); } // 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 setparrying
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.
has upvoted this post. -
AuthorPosts
- You must be logged in to reply to this topic.
Advertisement below: