I am using Unity 2021.3.27f1. I am creating a game for personal use that will be played on an old, refurbished PC (possibly from 2017) with an Intel Core i7 processor. I am a hobby programmer with very little coding experience. The Unity junior programmer pathway is the extent to what I know about C# and the Unity Editor.
The game is a very basic shmup that is the final project of the pathway and is geared toward demonstrating a basic understanding of the 4 pillars of OOP as taught by the pathways. The project is basically ready for polishing and submission except for a single feature that is driving me crazy.
The feature in question is an enemy counter that determines when a wave of enemies is cleared, and a new wave is spawned. The number of enemies in the scene is equal to the current wave number and a tank mob can summon an infantry mob at random intervals throughout the wave. Wave number is limited to the amount of available preset positions for every mob type. As waves progress, these positions eventually all fill up until no more enemies can be assigned to a post and the game is won.
With small numbers of enemies (20 seems to be the magic number), the counter sems to work perfectly, and all the numbers match up. For every mob spawned the counter is incremented, for every mob destroyed the counter is decremented, and when no more enemies are in the scene, a new wave is spawned. However, at about 24 enemies and upwards, I believe somewhere along the line a mob is not being counted when it is created or is being counted twice when it is being destroyed. This bug causes the game to spawn a new wave of enemies with the remaining enemies from the previous wave still in the scene and unaccounted for. If they are killed, they throw the count of even more.
During test plays I have:
-
checked my collision matrix to make sure all the layers were interacting properly
-
assigned continuous dynamic collision detection to the infantry and player class in the inspector to use Unity’s built in methods for predicting collisions(I’m pretty sure that’s what this does but could be wrong, I may have misunderstood the manual)
-
tried to use debug statements to see each individual type of collision that would cause the counter to decrement and when the spawn events that would increment the counter occurred. This did not lead to any causes of the bug as the math always balanced out, but there were still obvious stragglers left from the previous scene.
-
tested gameplay over and over, using different amounts of preset positions, starting at 54, working my way down to 20
-
During some of the plays even the runs with higher numbers of mobs had a working counter, meaning sometimes the wave would start at the correct moment and sometimes it would be off by 1 enemy or more.
I believe that spawning an infantry during the wave and its interactions with the player are what is throwing the count off. This is evidenced by test plays in which I utilize the players shield to put the infantry in a defensive state where they flee to a defensive position. This causes their numbers to build up overtime until they force their fleeing comrades into my shield for a kill.
All mobs are children of a parent class that handles their destruction collisions and logic that activates cooldown states for actions and buffs. The children classes all contain code for unique skills and logic pertaining to movement and triggers that contribute to their interactions and behaviors in the scene.
All mobs use a rigid body, even if motionless, because it was easier for me to handle the collisions this way and most posts I have read say that you don’t need to worry about the cost of rigid bodies until they reach the thousands.
The following code is where I believe the problem comes from.
The Mob parent class, specifically the collisions that cause the destruction of an object:
// mob takes damage equal to player strength, destroys mob if less than 1 health
protected virtual void TakeDamage()
{
mobHealth -= playerController.playerStr;
if (mobHealth <= 0)
{
Destroy(gameObject);
SpawnManager.DecerimentEnemyCount();
// debug
SpawnManager.IncrementBulletKill();
Debug.Log("BULLET KILL AT: " + Time.frameCount + " " + gameObject.tag + " BK: " + SpawnManager.bulletDeath + " EC: " + SpawnManager.enemyCount);
}
}
protected virtual void OnCollisionEnter(Collision collision)
{
// if bullet hits mob, set bullet to false to repool and reet duration bool, finally damage the monster
if (collision.gameObject.CompareTag("bullet-player"))
{
collision.gameObject.GetComponent<BulletPlayerController>().fired = false;
collision.gameObject.SetActive(false);
TakeDamage();
}
// deal damage to playerdestroy infantry instance on contact, unawarded
if (collision.gameObject.CompareTag("Player") && !playerController.isShielding)
{
DealDamage();
Destroy(gameObject);
SpawnManager.DecerimentEnemyCount();
// debug
SpawnManager.IncrementSuicide();
Debug.Log("SUICIDE AT: " + Time.frameCount + " S: " + SpawnManager.suicide + " EC: " + SpawnManager.enemyCount + "PH: " + playerController.playerHealth);
}
// destroy mob on contact with player if it is shielding
if (collision.gameObject.CompareTag("Player") && playerController.isShielding)
{
Destroy(gameObject);
SpawnManager.DecerimentEnemyCount();
// debug
SpawnManager.IncrementShieldKill();
Debug.Log("SHIELD KILL AT: " + Time.frameCount + " SK: " + SpawnManager.shieldKill + " EC: " + SpawnManager.enemyCount);
}
}
The Tank Mob, with special attention to SummonInfantry():
public class TankMobController : MobController
{
// shield reference
[SerializeField] GameObject shield;
// allow access to spawn manager
private SpawnManager spawnManager;
// reference to summonable mob
[SerializeField] GameObject mobSummon;
[SerializeField] int spawnCooldownMin;
[SerializeField] int spawnCooldownMax;
// spawner state
[SerializeField] bool isSpawning = false;
// spawner offset
[SerializeField] float spawnOffset = 1.0f;
protected override void Start()
{
base.Start();
StartCoroutine(ToggleSpawnState());
}
protected override void Update()
{
base.Update();
ToggleShieldActive();
SummonInfantry();
}
// turn shield object on or off depending on state
void ToggleShieldActive()
{
if (actionEnabled && !SpawnManager.Instance.gameOver)
{
shield.SetActive(true);
}
else
{
shield.SetActive(false);
}
}
// raise health, limited to MaxHealth
public virtual void RaiseHealth()
{
if (mobHealth == 0)
{
// if you see this the tank is throing off your count
Debug.Log("I SHOULD BE DEAD");
}
mobHealth += 1;
if (mobHealth > MaxHealth)
{
mobHealth = MaxHealth;
}
}
// toggle the state of the spawner
IEnumerator ToggleSpawnState()
{
while (true && !SpawnManager.Instance.gameOver)
{
yield return new WaitForSecondsRealtime(Random.Range(spawnCooldownMin, spawnCooldownMax));
isSpawning = !isSpawning;
}
}
// summon an infantry unit above the tank if isSpawning state is true and reset for another summon
void SummonInfantry()
{
if (isSpawning && !SpawnManager.Instance.gameOver)
{
Instantiate(mobSummon, new Vector3(transform.position.x, transform.position.y + spawnOffset, transform.position.z), transform.rotation);
isSpawning = false;
SpawnManager.IncrementEnemyCount();
// debug
SpawnManager.IncrementInfantrySummoned();
Debug.Log("spawned at: " + Time.frameCount + " IC: " + SpawnManager.summoned + "EC: " + SpawnManager.enemyCount);
}
}
}
The Infantry mob, has an attached empty game object with a collider set as a trigger for sensing the player:
public class InfantryMobController : MobController
{
// player reference
private PlayerController player;
// allow access to rigid body
private Rigidbody infantryRb;
// attack state
[SerializeField] bool offensive = true;
// post references
[SerializeField] Transform[] posts;
private Transform post;
// attributes unique to an infantry mob
[SerializeField] float m_Speed;
public float Speed
{
get { return m_Speed; }
set { m_Speed = value; }
}
// boundary references
private int boundaryRange = 9;
protected override void Awake()
{
base.Awake();
// allow access to rigid body
infantryRb = GetComponent<Rigidbody>();
post = AssignPost();
player = GameObject.Find("player").GetComponent<PlayerController>();
}
// Update is called once per frame
protected override void Update()
{
base.Update();
MoveWithAI();
}
// infantry movement with simple AI determining offensive or defensive movement
void MoveWithAI()
{
if (offensive && !SpawnManager.Instance.gameOver)
{
Movement.MovetTo(player.playerRb, infantryRb, Speed);
}
else
{
Movement.MoveTo(post, infantryRb, Speed);
StartCoroutine(Cooldown());
}
Movement.KeepInBounds(infantryRb, boundaryRange);
}
// assign a random defensive post for unit to flee to when defensive
Transform AssignPost()
{
int postNumber = Random.Range(0, posts.Length);
post = posts[postNumber];
return post;
}
// action cooldown timer
private IEnumerator Cooldown()
{
yield return new WaitForSecondsRealtime(cooldownMax);
offensive = true;
}
// raise speed
public void RaiseSpeed()
{
Speed += 0.1f;
}
private void OnTriggerStay(Collider other)
{
// change to defense mode when in range of the shielded player
if (other.gameObject.CompareTag("Player") && player.isShielding)
{
offensive = false;
}
}
}
I can try to answer any questions as best I can.
If anyone needs to see more code just ask and I can post whatever you might think you need. The SpawnManager code that increments or decrements is just variable++; or variable--;, if that helps.
Sorry for the long post, but I thought supplying as much information as I could, would be helpful in finding a solution. I am stumped here and would really appreciate any help or advice offered.
Thank you in advance, your time and consideration are appreciated.