
About Duel defenders:
Duel defenders is a tower defense game where you also have to attack the enemy base, hence the name.
This was a developer only project, I worked together with one other developer.
Duel defenders is a tower defense game where you also have to attack the enemy base, hence the name.
This was a developer only project, I worked together with one other developer.
This wave generation system was a lot of fun to make, it was a bit of a challenge to make it so that the waves would get harder over time, but I managed to do it. The way I did this was by making a probability system, where the probability of an enemy being spawned was based on the wave number and difficulty scale given to each enemy type.
public IEnumerator SpawnWave(int wave, float enemyAmnt, float multiplier)
{
// wait for wave to start
OnStartNewWave?.Invoke();
yield return new WaitForSeconds(timeBetweenWave);
float amount = enemyAmnt * ((wave * multiplier) / 2);
while (true)
{
// spawn enemies
for (int i = 0; i < amount; i++)
{
// get random EnemyKind
EnemyKind enemyType = GetEnemyKind(wave);
GameObject randomEnemy = null;
// set randomEnemy var
switch (enemyType)
{
case EnemyKind.Easy:
randomEnemy = easy[Random.Range(0, easy.Count)];
break;
case EnemyKind.Medium:
randomEnemy = medium[Random.Range(0, medium.Count)];
break;
case EnemyKind.Hard:
randomEnemy = hard[Random.Range(0, hard.Count)];
break;
}
// choose random tile to place enemy on
MeshRenderer mesh = randomEnemy.GetComponentInChildren<MeshRenderer>();
int randomTile = Random.Range(0, spawnableTiles.Length);
// calculate position to spawn new enemy
Vector3 offset = new Vector3(0, mesh.bounds.size.y / 0.9f, 0);
Vector3 pos = spawnableTiles[randomTile].transform.position + offset;
// spawn spawn effect
Instantiate(SpawnEffect, pos, Quaternion.Euler(-90, 0, 0));
yield return new WaitForSeconds(0.3f);
// spwawn new enemy on calculated position
GameObject newEnemy = Instantiate(randomEnemy, pos, Quaternion.identity);
// play audio
if (Cam.WhereCamAt == CamAt.player) SpawnAudio.Play();
// add enemy to currentEnemies list
AiBase enemy = newEnemy.GetComponent<AiBase>();
currentEnemies.Add(enemy);
// set enemy path to a random path on the player field. (3, 6) is player field, (0, 2) is enemy field
enemy.PathToFollow = Random.Range(3, 6);
// wait between each enemy spawned
yield return new WaitForSeconds(timeBetweenEnemies - (timeBetweenEnemies / 10 * wave) + 0.1f);
}
break;
}
spawnWaveCoroutine = null;
}
private EnemyKind GetEnemyKind(int wave)
{
// calculate easy and medium probability (hard is not necessary)
float easy = SpawnProbability(EnemyKind.Easy, wave);
float medium = SpawnProbability(EnemyKind.Medium, wave);
// random value to choose what enemy to return
float randomValue = Random.value;
// return the enemykind of which the probability is closest to the value created
if (randomValue < easy)
return EnemyKind.Easy;
else if (randomValue < easy + medium)
return EnemyKind.Medium;
else
return EnemyKind.Hard;
}
This turret system is pretty straight forward, it checks if enemies are near it and which one is the closest, then proceeds to shoot that. This, of course, is the base turret class, all turrets have their own script that inherrit from this.
public virtual Vector3 ClosestEnemy()
{
// start with position zero and high distance
Vector3 closestPosition = Vector3.zero;
float closestDistance = 100f;
// loop through each enemy in enemylist (each enemy in your trigger collider)
foreach (AiBase enemy in EnemyList)
{
// check distance per enemy
float distance = Vector3.Distance(transform.position, enemy.transform.position);
// if distance closer than closestDistance, set it to this distance
if (distance < closestDistance)
{
closestDistance = distance;
closestPosition = enemy.transform.position;
}
}
// return position of closest enemy
return closestPosition + new Vector3(0, 0.5f, 0);
}
public virtual void Shoot()
{
// remove empty slots from EnemyList with reverse for-loop
for (int i = EnemyList.Count - 1; i != -1; i--)
{
if (EnemyList[i] == null) EnemyList.RemoveAt(i);
}
// if there is atleast 1 enemy
if (EnemyList.Count > 0)
{
// play shoot sound
if (IsCamInRightplace()) shootSound.Play();
// make turret lookat enemy
turretOrienter.transform.LookAt(ClosestEnemy());
// instantiate bullet, make it lookat enemy and set the damage
Bullet projectile = Instantiate(bullet, turretOrienter.transform.position, Quaternion.Euler(-90, 0, 0));
projectile.transform.LookAt(ClosestEnemy());
projectile.damage = Damage;
}
}
At first, I had a bit of trouble making this system; I didn't know how to make it compact and still give it all the functionality I wanted it to have. When I began working on this, I made it have a reference to every kind of tower, but then I realised that that wasn't necessary; it only needed to have a reference to the tower that it was going to build, so what I ended up doing was making the shop buttons instantiate an instance of this class, and giving it a turret refrence which that button is supposed to spawn. This fixed all my problems and made it so that I could have this class as compact as I wanted it to be.
private void MakeGhostTower(GameObject tower)
{
// instantiate turret
currentTowerRef = Instantiate(tower, new Vector3(0, -100, 0), Quaternion.identity);
// set refrence to which side spawned turret
currentTowerRef.GetComponent<Turret>().WhoSpawnt(1);
// set turret variable and set its active state to false
turret = currentTowerRef.GetComponent<Turret>();
currentTowerRef.GetComponent<IPlacable>().SetEnabled(false);
}
private void UpdatePos()
{
// get mousepos and make world pos raycast
Vector2 mousePos = Input.mousePosition;
Ray ray = Camera.main.ScreenPointToRay(mousePos);
// shoot raycast from camera to mouse world pos
if (Physics.Raycast(ray, out RaycastHit hit, 1000, LayerMask, QueryTriggerInteraction.Ignore))
{
// if hit tile set the position to that tile + offset
if (hit.transform.TryGetComponent<Tile>(out Tile tile) && tile.occupent == null)
{
MeshRenderer mesh = currentTowerRef.GetComponentInChildren<MeshRenderer>();
Vector3 offset = new Vector3(0, mesh.bounds.size.y, 0);
currentTowerRef.transform.position = hit.transform.position + offset;
}
}
}