Sentry Turret AI
Because the turrets have a lot of different states, you’ll want to reference the slides on the Turret AI during this part of the lab. This state machine diagram will especially be helpful:
You’ll add the state machine code to TurretHead
.
First, add a declaration of the following enum class
in TurretHead.h, before the declaration of class TurretHead
:
enum class TurretState
{
Idle,
Search,
Priming,
Firing,
Falling,
Dead
};
Add the following member data to TurretHead
:
- An instance of the
TurretState
and it should start out asTurretState::Idle
- A
float
for the state timer that starts out at0.0f
Then add the following member functions:
- An
UpdateXXX
function (whereXXX
is replaced by the state) for each state which takes in afloat deltaTime
. For now, these can just remain empty stubs - A
ChangeState
function that takes in theTurretState
you want to change to. It should change the state member variable to the requested state and reset the timer to 0.0f
Then, change your TurretHead::OnUpdate
function so it:
- Updates the state timer by delta time
- Uses a
switch
to run the correctUpdateXXX
based on the current state (don’t forget that you need `break statements)
Make sure your code still compiles.
Target Acquisition, Priming, and Firing
The turrets use the laser to acquire a target.
Back in LaserComponent
, add a class Actor* mLastHitActor
member variable that starts out as nullptr
and a getter function for it. Then, in LaserComponent::Update
, you should update mLastHitActor
to the actor hit by the last laser on that frame (so if there’s no portals this is the first laser, otherwise it’s the second one). If the last laser doesn’t hit an actor, then mLastHitActor
should be set to nullptr
. Furthermore, to prevent a potential crash where last hit actor points to a deleted portal for one frame, add a check at the end of LaserComponent::Update
that says that if the last hit actor is a Portal*
, set it to nullptr
instead.
We don’t want turrets to acquire a target on any actor. The turrets can only acquire actors which have health, which we will represent with the HealthComponent
. You’ll add the custom logic to HealthComponent
in the next part. But for now, just create a Component
subclass called HealthComponent
. Then add a HealthComponent
to both Player
and TurretBase
.
Next, add a helper function TurretHead
that checks for target acquisition. A target is acquired if the laser’s last hit actor is non-null AND has a HealthComponent
. You’ll want to save the acquired target in a member variable in TurretHead
for future reference.
Remember you can use GetComponent
to find out if an actor has a particular type of component.
Priming
In the Idle state, if a target is acquired, switch to the Priming state.
Put a breakpoint in UpdatePriming
and confirm that if you step into a turret’s laser, the breakpoint gets hit.
In UpdatePriming
, you have to check every frame if the last hit actor is still the acquired target. If it’s not, switch to the Search state. Otherwise, if at least 1.5 seconds has elapsed in Priming (you can use the state timer for this), switch to Firing.
Use breakpoints in UpdateSearch
and UpdateFiring
to confirm that you can transition from Priming to Search/Firing as expected.
Firing
In the Firing state, if the last hit actor is no longer the acquired target, then switch to Search.
You will add the logic to actually “fire” at the target in the next part.
Search
If while in the Search state, a target is acquired, switch to Priming.
In the Search state, the turret selects a random target on a circle. You’ll want to consult the slides for further information about how to calculate this, but here is the diagram:
Here are the relevant constants:
- SideDist = 75
- UpDist = 25
- FwdDist = 200
Then, over the course of 0.5 seconds, the turret head should interpolates its rotation from the center (identity quaternion) to face towards the target. Once that movement finishes, over the course of 0.5 seconds it rotates back towards the center (identity quaternion). It will then select a new random target to repeat this process.
Importantly, if the turret switches out of the Search state and then later comes back to Search, it’s possible the move was interrupted. So if the previous one second cycle was not completed, you want it to continue its previous move to prevent an awkward snap. Then, once the interrupted move completes, it would select a new random target and continue. Although this sounds complicated, it essentially just means you need member variables to help track where you were in the move so that the move can resume if needed.
Some other tips:
- You can use
Random::GetFloatRange
to get a random float - When calculating the quaternion to face the arbitrary target, you don’t need to worry about the edge cases if you calculated a point on the circle properly
- Use
Quaternion::Slerp
to do the interpolation
Confirm that if you get the turret into the Search state, it will start searching indefinitely, unless the target is reacquired. It will look like this:
Finally, add logic to the part where it begins a new one second search cycle – if more than 5 seconds have elapsed in the search state, instead of starting a new cycle, have the turret return to the Idle state.
Confirm that the turret stops searching and returns to Idle if it doesn’t reacquire a target after 5 seconds.
Falling and Portal Teleporting
One fun mechanic in Portal
is that you can put a portal under a turret to get it to fall to its death.
Add a Vector3
member variable for the fall velocity.
Then, add a helper function to TurretHead
check for portal teleport (it returns true if the teleport occurs):
- If a turret teleports, it can’t teleport again until 0.25 seconds elapse
- You can’t teleport unless there is both a blue and orange portal
- To check for intersection with a portal,
TurretHead
DOES NOT have aCollisionComponent
. Instead, you need to useGetParent()
to get the parent actor (which is the base of the turret) and then get itsCollisionComponent
- Set the parent’s position to the position of the opposite portal
- Add to the fall velocity a vector in the direction of the portal’s quat forward with a magnitude of 250
Then in the UpdateXXX
functions for Idle, Search, Priming, Firing, and Dead, first do a check for portal teleport – if the teleport occurs, you want to change states to falling and don’t do any of the other logic in the function.
Confirm that a turret will teleport if you put a portal under it, though it won’t actually fall just yet:
To get it to fall, do the following in UpdateFalling
:
- Update the parent’s position according to the fall velocity and delta time
- Check for a portal teleport (using the helper function). If you don’t teleport:
- Update velocity according to a gravity of <0, 0, -980> and delta time.
- Use
GetMinOverlap
between the parent’s collision and all colliders (keep in mind that the parent is also a collider, and you don’t want to collide against that), and update positions based on offset like usual
- If the length of the fall velocity is greater than 800.0f, normalize it and multiply by 800.0f (for a terminal velocity)
You should now be able to get the turrets to fall, and sort of land, though when they land they will float in the air:
Dead
Add a function to TurretHead
called Die
. In here, you should:
- Change the state to
TurretState::Dead
- Set the parent’s quaternion to one rotate about the x-axis by pi/2
- Disable the laser component (since we don’t support the idea of removing/disabling components inherently, you’ll just have to add a
bool
toLaserComponent
that controls if it’s disabled, and if it’s disabled you should clear out the laser vector and not create line segments inUpdate
anymore)
Also, add a Die
function to TurretBase
that just calls Die
on the turret head member variable.
If while falling, after applying the offset, check if the turret has a CollSide::Top
of AND a negative z velocity (meaning it’s falling down):
- Subtract 15 from the parent’s z-position (which will make it appear more on the ground).
- Call
Die()
- Furthermore, if the
CollSide::Top
collision was against anotherTurretBase
, you should adjust the parent’s position further by subtracting the collision component height / 2 from the z, and callDie
on the otherTurretBase
You should now be able to drop turrets to their death, and drop them on top of other turrets to kill those, as well:
Once you’ve pushed this code, you’re ready to move on to part 3.