I grew up playing the Call of Duty games, and made most of my friends back home through playing with/against them in multiplayer and zombies. We would sometimes also take turns playing the campaign, and I would play it by myself at other times. I recently started playing some of the games' campaign mode, and really wanted to challenge myself to recreate an aspect of these games, as it is one of my goals to make a First-Person Shooter game in the near future.
Since I am specialising in Game AI Programming, I decided to make an enemy AI you would find in a typical FPS game. Over the course of this series, I will develop this AI with with goal of having it seem intelligent to the player, and put up a good, balanced challenge.
In this first part, I set up the very basics, most of them involving the player controller and weapon classes. I worked a little on the AI, but most of the work I did was on the player's end.
Having taken inspiration from my Weapon Customisation project, I set up the player controller, first-person camera controller, and weapon manager. Alongside that, I create a full-auto weapon and a pistol. I decided not to add customisation like in the previously-mentioned project, as my goal for this project was not to create a full FPS game, but I used a lot of the same code for the weapons, deciding to have the variables in the individual weapon's script instead of using scriptable objects to store attachment data that affect the weapons.
In the last project, recoil and weapon sway were not working properly, so I omitted them completely for this project, and I most likely will not bring them back for this series (but I definitely will for the future FPS game). With the player controller and health script / respawning working on the enemy, I started working on the AI.
I set it up to constantly look for cover using a ray that it fasts forward, at an angle that constantly sweeps downwards from the gameObject transform's forward direction until it reaches a maximum angle, then sweeps back until it makes an angle of 0 degrees with that same reference vector. This sweeping of the vector happens while the enemy has not found cover.
The way it knows when it found cover is by having the ray only collide with colldiers of objects on an isolated layer, which I called "ShortWall." Once the ray detects a so-called short wall, the enemy looks for a pre-determined transform that I set up as a child to the gameObject, and sets that transform's position to be its targetMovePosition. While searching for a child GameObject may not be the most efficient/optimised approach, it only happens once when the ray detects cover, and then the code to cast the ray and make it sweep doesn't run anymore. In the future, I will make it somehow know when it left cover, and have the function get called again, but for now this works well enough as a proof of concept.
void LookForCover()
{
//If is not hidden and didn't find cover yet
if (!bIsHidden && !bFoundCover)
{
//Sweep direction down and up from the forward direction
coverDetectionSweepingTransform.rotation =
Quaternion.Euler(coverDetectionSweepingAngles.x *
Mathf.Abs(Mathf.Sin(Time.time)),
coverDetectionSweepingAngles.y,
coverDetectionSweepingAngles.z);
//Ray forward if hasn't found cover yet
bFoundCover = Physics.Raycast(transform.position,
coverDetectionSweepingTransform.forward, out
coverDetectionRayHit, coverDetectionDistance, coverLayer);
if (hideBehindCoverRoutine == null && bFoundCover)
{
hideBehindCoverRoutine =
StartCoroutine(HideBehindCover(
coverDetectionRayHit.transform.Find(
"HidePoint").position));
}
}
}
The ray is being drawn using Unity's Debug.DrawRay() function, but I didn't include it here so that it's move clear what I'm doing.
LookForCover() is called every Update(), so every frame. It calls the Couroutine HideBehindCover() when it detects a short wall, which waits a certain duration to simulate the agent's reaction time, then sets its targetMovePosition to whatever Vector3 is passed into the function. It then updates the enemy's current state, which I can use to make it do different things in different scenarios.
IEnumerator HideBehindCover(Vector3 coverPos)
{
if (!bIsHidden)
{
//Simulate reaction time
yield return new WaitForSeconds(reactionTime);
//Go towards cover
targetMovePosition = coverPos;
currentState = EnemyState.STATE_MOVING_TO_COVER;
}
}
Another function I call every frame, in Update(), is MoveTowardsTargetPosition(). In that function, the gameObject looks at the targetMovePosition on the horizontal axis, but looks straight ahead on the vertical axis (to avoid rotating about its transform's x axis and looking like it's leaning like MJ).
//Look at target, but not on Y axis
transform.LookAt(new Vector3(targetMovePosition.x,
transform.position.y, targetMovePosition.z));
I admit, I'm not a fan of making a new Vector3 every frame, never have been, but it doesn't really affect performance so I'll live with it.
It then calculates a normalised vector to the targetMovePosition, multiplies by its moveSpeed and Time.deltaTime, and sets its rigidbody's velocity equal to that vector.
//Move towards target, keep rigidbody's y velocity untouched
rb.velocity = new Vector3(0f, rb.velocity.y, 0f) +
transform.TransformDirection(new Vector3((targetMovePosition -
transform.position).normalized.x, 0f, (targetMovePosition -
transform.position).normalized.z)) * moveSpeed *
Time.deltaTime * 100f;
The last thing to happen in this function is that the enemy decides what to do when it reaches its destination. This is where the enemy's currentState comes in:
//If close enough
if (Vector3.Distance(transform.position, targetMovePosition) <= 0.25f)
{
//Act according to current state
switch (currentState)
{
case EnemyState.STATE_DEFAULT:
{
//Stop moving
rb.velocity = Vector3.zero;
break;
}
case EnemyState.STATE_MOVING_TO_COVER:
{
//Crouch
if (!bIsCrouching)
ToggleCrouch();
//Stop moving
rb.velocity = Vector3.zero;
bIsHidden = true;
hideBehindCoverRoutine = null;
bFoundCover = false;
break;
}
default:
{
//Stop moving
rb.velocity = Vector3.zero;
break;
}
}
}
It's quite simple for now, and I may scrap this approach later on in the project, but if it's currently moving to cover and is close enough to the moveTargetPosition, then it crouches if it's not already, and stops moving.
The next steps for this project are to make the enemy fire at the player, and eventually fire from cover while leaning in/out of from behind the objects providing cover. I would also like to make it try and find a different spot to hide behind if the player can see the enemy, meaning its cover has been compromised / it is in an unsafe position. I would also like to setup a navigation mesh, similar to the project I did in Unreal Engine 4, and create a Behaviour Tree to run the enemy's AI, as Finite State Machines will get far too crowded and complicated for the level of intelligence I would like to simulate, and having it all be in a single script will drive me to insanity.
Make sure you look out for new posts on my LinkedIn account, where I will announce when the next blog post in this series will become available!
Comments