Ai Invasion: Game Creation Overview

Jared Amlin
Nerd For Tech
Published in
15 min readJul 14, 2024

--

Hello readers! In this article I will talk about the features and mechanics in a small game I made as part of the Game Logic and Interactions course material by GameDevHQ. Let’s dive in!

In the Beginning

I have been provided with this sample scene, which has a sci-fi factory aesthetic, and a shooting box where the player will work from.

There is a first-person character controller holding a rifle, which can move, jump, crouch and aim.

There is also a single a robot prefab to use for the enemies.

I have been tasked with creating all of the mechanics and interactions for this game, so let’s get started with the enemy behavior.

Enemy State Machine

All enemies use the same enemy script and state machine behavior. The only thing that differs between them, is their starting health, movement speed, points awarded and the wave they begin to spawn in.

All assets are from the Filebase asset library by GameDevHQ. Check it out here!

Enemies start in the running state by default, being their ultimate goal is to get to your core reactor and destroy it. Other enemy states include cover, attack, death and game over.

I wanted to avoid checking a state machine in an update loop, so I instead set the enemy state as needed, before running the State Check method. I was able to get away with the enemies not having an update loop of any kind outside of a coroutine when attacking.

#region State Machine

private void StateCheck()
{
switch (_currentState)
{
case _AiState.attack:
Attack();
break;
case _AiState.running:
Running();
break;
case _AiState.cover:
Cover();
break;
case _AiState.death:
Death();
break;
case _AiState.gameOver:
GameOver();
break;
default:
Debug.LogWarning("There is no enemy state for this case");
break;
}
}

#endregion

Each enemy state comes with it’s own animations, which I will cover later in this article.

Enemy Navigation

Enemies spawn in randomly at one of three spawn points before making their way through a series of waypoints.

The running state is set by default, so enemies immediately start moving toward their initial waypoint. Here is the overall path they take while traversing the upper levels.

Since I am not using an update loop in the enemy script, I handle most of my logic on Collision by checking for a Waypoint Tag, then changing the state behavior based on the current waypoint.

When an enemy reaches a waypoint with a barricade in front of it, they go into a cover state which stops the agent for a random 1 to 5 seconds before moving to the next waypoint. These barricades can be hit and will play a sound and particle effect, but can not be destroyed and will reduce your accuracy rating at the end of the game.

There is one waypoint with an off-mesh link, where the enemies will pause before hurdling the fence and dropping down to the lower level. Mesh Renderer left on for visualization.

The animation transition needs some work, but here it is in action.

Once enemies reach the lower level, they have a 50% chance to skip a waypoint whenever they choose a new one. After jumping down at the first junction however, they have a random chance to move to one of three waypoints. This provides some randomization once they start moving around on the largest floor area this game has to offer.

Once the enemies reach the core reactor at the final waypoint, they enter into an attack state and begin striking the reactor.

Hit System

My article shared below is a detailed overview of how the hit system works, so I’ll provide you with a TLDR version for this article: Raycast > body colliders > interfaces > damage > animation triggers.

The main difference with this game is having enemies with various starting health values. While the basic robot can be shot down with a single head-shot, the medium robot will require two shots and the big robot can absorb three to the dome.

Animations

The enemy hit animations as explained in the article above, play a different hit animation depending on if the enemy was hit in the head, body, arm or leg. I took it one step further in this game than I did in that article, adding 4 colliders to the head, and 4 to the body. Enemies in this game play a different head hit animation, depending on if they were hit from the front, back or either side. The same principal works with the body. While the arms and legs have two colliders each, lower and upper limbs trigger the same animation.

A leg hit will not slow the enemy, but an arm hit will slow them temporarily while the hit animation plays. A body or a head hit will completely stop the agent for the duration of the hit reaction animation, before resuming the running state animation.

While the hit reaction and death animations come from any state, State machine animations are done through collision triggers. When the player collides with a waypoint behind a barricade, the cover state and animation are played.

Once the enemy gets to the core reactor, it will play an attack animation that uses an animation event to turn the collider on the attacking hand on and off, to damage the power core when it swings.

Whether you win the game or lose by game over, remaining enemies will randomly enter in to a celebratory dance sequence of animations.

Please don’t judge my messy animator too harshly! As you can see here, all of the hit and death animations are triggered from any state, by the collider on the body part that gets hit via the Ihittable interface. All hit animations return to the default running state with has exit time enabled.

Player

With the player movement already implemented, I just need to focus on the shooting, ammo system and cameras. I decided to go fairly generic with the raycasting, so the player is just checking for an Ihittable interface when shooting. Everything in the game that can be hit, from enemies to electric barriers, barricades and destructible barrels use the Ihittable interface.

Here is just the raycasting portion of my Fire method on the Player script. The player uses a layer mask to raycast, which is also shared by anything hittable in the game. The Pool Manager Mono Singelton is passed the position of the hit for setting the hit VFX active. While the Player only passes in a flat damage amount of 10, that amount will be greatly multiplied depending on the area of the enemy being hit.

//raycast forward from screen center
Ray rayOrigin = Camera.main.ViewportPointToRay(_centerPoint);

//store hit info
RaycastHit hitInfo;

if (Physics.Raycast(rayOrigin, out hitInfo, Mathf.Infinity, _hitMask))
{
PoolManager.Instance.RequestBulletSparks(hitInfo.point);

IHittable hit = hitInfo.collider.GetComponent<IHittable>();

if (hit != null)
{
hit.HitDamage(10, hitInfo.point);
}
}

Damage

Here is the Ihittable class that the raycast is looking for. The spawn position is used for object pooling in the hit VFX at the right position.

public interface IHittable
{
void HitDamage(int damageAmount, Vector3 spawnPosition);
}

The Hit class is on every piece of the enemy rig that has a different hit collider (head, body, legs and arms). Each script instance uses the Serialized Value of the hit multiplier to multiply the incoming damage amount by, before passing the final damage to the enemy script on the parent object. The string values are used to trigger the appropriate hit or death animation. The animation clip is just used to get it’s length, so I can stop the agent for the duration of it’s playback. The hit ID is used for overall hit tracking by the Game Manager, which will display on the Statistics screen once the game has concluded. The Audio Source is there to have 3D space audio playback on the enemies. This Hit class is also responsible for passing it’s damage amount to the Pool Manager to use on the damage UI display that pops up when hitting enemies.

[RequireComponent(typeof(AnimationClip))]
public class Hit : MonoBehaviour, IHittable
{
#region Variables

[SerializeField] private int _hitMultiplier;

[SerializeField] private string _hitTrigger;

[SerializeField] private string _deathTrigger;

[SerializeField] private AnimationClip _animationClip;

[SerializeField] private AudioSource _audioSource;

[SerializeField] private int _hitID;

private float _clipLength;

private IDamageable enemyDamageable;

#endregion

#region Start

private void Start()
{
enemyDamageable = GetComponentInParent<IDamageable>();

_clipLength = _animationClip.length;
}

#endregion

#region Hittable

public void HitDamage(int damageAmount, Vector3 spawnPosition)
{
int damage = damageAmount * _hitMultiplier;

PoolManager.Instance.RequestImpactSparks(spawnPosition);

GameObject newDamageDisplay = PoolManager.Instance.RequestDamageVFX(spawnPosition);

IDisplay display = newDamageDisplay.GetComponent<IDisplay>();

if (display != null)
{
display.DisplayText(damage.ToString());
}

enemyDamageable.Damage(damage, _hitTrigger, _deathTrigger, _clipLength, _hitID);

GameManager.Instance.AddEnemyHits(_hitID);

_audioSource.Play();
}

#endregion
}

The Idamageable on the enemy parent takes a few parameters to get this all working.

public interface IDamageable
{
int Health { get; }

void Damage(int damageAmount, string triggerName, string deathTriggerName, float clipLength, int hitID);
}

This Damage method on the enemy class got a bit more long winded than I originally intended, but it works and doesn’t seem to impact my performance. A variety of conditions are checked to determine if the enemy is still alive or not, as well as what body part the damage is coming from to trigger the correct animation.

#region Damage

public void Damage(int damageAmount, string triggerName, string deathTriggerName, float clipLength, int hitID)
{
_currentHealth -= damageAmount;

//trigger damage UI if exploding barrel hit
if (hitID == _barrelHit)
{
GameObject newDamageDisplay = PoolManager.Instance.RequestDamageVFX(this.transform.position);

IDisplay display = newDamageDisplay.GetComponent<IDisplay>();

if (display != null)
{
display.DisplayText(damageAmount.ToString());
}
}

//plays a hit animation
if (_currentHealth > 0)
{
_animator.SetTrigger(triggerName);

//stop the agent when hit with a head, body or exploding barrel hit
if (hitID == _headHit | hitID == _bodyHit | hitID == _barrelHit)
{
//don't stop the agent while jumping
if (_isJumping == false)
_agent.isStopped = true;
}
else if (hitID == _armHit)
{
if (_isJumping == false)
//temporarily slow the agent if hit in the arm
_agent.speed = _hitSpeed;
}

SetClip(clipLength);

StartCoroutine(_waitRoutine);
}
else //plays a death animation
{
if (!_isDead)
{
_isDead = true;

//stops a potential wait routine
StopCoroutine(_waitRoutine);

//stop the agent after root motion leg death animation has completed
if (hitID == _legHit)
{
Invoke(_stopAgent, clipLength);
}
else
_agent.isStopped = true;

_animator.SetTrigger(deathTriggerName);

_currentState = _AiState.death;

GameManager.Instance.AddDeathHits(hitID);

StateCheck();
}
}
}

#endregion

Camera System

I wanted more than a single FPS camera, so I used Cinemachine virtual cameras to create a zoom system. All cameras are following and looking at the same target, but have a different FOV, look speed and post process volume.

The Camera Manager only handles the camera switching and camera shake, while the Player script keeps track of the look speeds for it’s own behavior.

The player starts at the most zoomed out camera by default, but can zoom in and out between four cameras as needed. Camera switching is done by changing priority levels of the virtual cameras, and letting the Cinemachine Brain use it’s default blend time.

As you zoom in, the FOV changes, the look speed decreases, the vignette tightens and eventually the reticule changes.

My hope here is for the player to take advantage of a stunned enemy, and zoom in for a better shot.

Each camera also has different amounts of Perlin noise to simulate a ranged rifle effect. This noise is also used to shake the camera slightly when shooting, and heavily when the reactor takes damage.

The Player has inputs for zooming in and out, which then contact the Camera Manager. The Camera Manager checks which camera is active, and then changes to the desired camera by setting the priority levels.

Ammo System

Even though you only have seven rounds in your clip, you start the game with an additional fifty in your reserve. If your clip runs empty, the reload mechanic will automatically fill your clip from your reserve. There is a short cool down time that comes with reloading, so the player can also manually reload by right-clicking. When you manually reload, the player requests the amount needed to fill the clip, rather than asking for a full clip reload. The radial UI meter as well as the reloading UI display in the middle of the reticule, let the player know when the reload is happening. No shooting can happen while reloading and sorry…no reloading animation just yet.

While your ammunition is limited, you do not have a maximum amount. Each wave reloads the players clip, as well as the reserve plus an additional 10 rounds per wave. So, you start the game with fifty rounds in your reserve, and will get +60 to your reserve before wave 2, +70 before wave 3, +80 before wave 4, and so on and so forth. If you make good clean shots and can finish a wave with ammo in your reserve, it will roll-over to later waves when you will need that ammo more.

Game Over Condition

The first game over condition is when enemies attack your core reactor and fully deplete it’s health.

The second condition is if you run out of ammo before all remaining enemies in the wave have been destroyed, after the spawn timer has ended. I know this is kind of a crappy losing condition, but I also want accuracy and inventory management to be rewarded. If you blast away at limbs haphazardly, you will run out of ammo sooner rather than later.

I did make sure to add a check to avoid one potential scenario…killing the last enemy with your final bullet. Don’t worry…that won’t cause you a game over. Wave cleared and fully reloaded!

This is an infinite wave shooter that does have an easter egg of a game win scenario, if you are feeling adventurous.

VFX

None of the particle systems in this game are being instantiated or destroyed. All of the heavily used particle effects such as the impact sparks, rigidbody impact particles, debris particles and hit display effects are being object pooled.

Non-pooled effects include the muzzle flash, bullet round tracer, explosions and reactor smoke, all just get set active as needed.

The hit damage VFX are a little more tricky, being I need to pass in a damage amount for them to display when they set active. Another Interface called Idisplay that takes a string parameter is all I needed to pass in the damage amount.

public interface IDisplay
{
void DisplayText(string text);
}

Here is where the Hit class passes it’s info to the damage display prefab.

GameObject newDamageDisplay = PoolManager.Instance.RequestDamageVFX(spawnPosition);

IDisplay display = newDamageDisplay.GetComponent<IDisplay>();

if (display != null)
{
display.DisplayText(damage.ToString());
}

Audio

The main soundtrack is a theme I worked on many years ago, while the other music and sound FX are free to use licenses I found online.

I didn’t want to neglect audio in this game, so it is fairly rich with sound. There is a main menu soundtrack that fades out as you start a wave, and then fades in a new soundtrack for the current wave. A similar soundtrack transition happens when changing from an active wave to game over.

#region Coroutines

private IEnumerator FadeInRoutine(AudioSource source, float timeToFade)
{
float timeElapsed = 0f;

while(timeElapsed < timeToFade)
{
source.volume = Mathf.Lerp(0, 1, timeElapsed / timeToFade);
timeElapsed += Time.deltaTime;
yield return null;
}
}

private IEnumerator FadeOutRoutine(AudioSource source, float timeToFade)
{
float timeElapsed = 0f;

while (timeElapsed < timeToFade)
{
source.volume = Mathf.Lerp(1, 0, timeElapsed / timeToFade);
timeElapsed += Time.deltaTime;
yield return null;
}

source.Stop();
}

#endregion

Other than that, I have sound FX for button hover, select, shooting, reloading, enemy hits, enemy deaths, barricade and electric barrier hits, barrel explosions and reactor damage. If you want to hear it for yourself, you will have to give the game a try!

Menus

The main menu starts with a short skippable animation. The player then has options for Controls, Objectives and Enemy menus.

Once the start game option is selected, a short skippable cinematic shows the core reactor and a simple objective overview. The cinematic ends with the player in control and ready to press the start wave button. You can however, use this as an opportunity to get comfortable with the controls before starting the first wave.

Game over and Game win both provide a similar menu, only with a difference of text and background color. The player then has the option to either restart the game, or see the statistics for their currently finished round.

The statistics menu shows a wide variety of what has been tracked during gameplay. The various Hit ID’s let me keep track of the different areas being hit, as well as being able to calculate the player accuracy against their total shots fired.

Lighting

I wanted to go for some nice cool and warm lighting, where the main platforms are warm and the spaces between them are cool. Even the cool shadowed spaces are using cool lights, so as to not be completely black and hard to see the enemy targets.

The core reactor light, as well as the spot lights on the enemy mid and far paths are real-time lights. Everything else is baked lighting using light probes and reflection probes.

Extras

As extra credit, I had the opportunity to implement hittable barriers that respawn after a few seconds. These barriers will disappear after one hit, protect the enemy, play a sound effect and particle effect, then respawn after 5 seconds.

The exploding barrels can be shot to damage enemies within it’s proximity. They are four of them placed around the scene which respawn every wave. They deal 800 damage which will take out the two smaller robots, but not the big Fundam.

Below is a link to the playable WebGL, which of course killed my HD graphics! I will be sure to post a high res windows downloadable in the near future.

Thanks for reading and happy gaming!

--

--

Jared Amlin
Nerd For Tech

I am an artist and musician, that is currently diving headfirst into game development with C# and Unity3D.