Skip to main content Link Search Menu Expand Document (external link)

Theme: auto

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:

Turret AI State Machine

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 as TurretState::Idle
  • A float for the state timer that starts out at 0.0f

Then add the following member functions:

  • An UpdateXXX function (where XXX is replaced by the state) for each state which takes in a float deltaTime. For now, these can just remain empty stubs
  • A ChangeState function that takes in the TurretState 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:

  1. Updates the state timer by delta time
  2. Uses a switch to run the correct UpdateXXX 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.

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:

Search random target

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 a CollisionComponent. Instead, you need to use GetParent() to get the parent actor (which is the base of the turret) and then get its CollisionComponent
  • 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:

  1. Update the parent’s position according to the fall velocity and delta time
  2. Check for a portal teleport (using the helper function). If you don’t teleport:
    1. Update velocity according to a gravity of <0, 0, -980> and delta time.
    2. 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
  3. 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 to LaserComponent that controls if it’s disabled, and if it’s disabled you should clear out the laser vector and not create line segments in Update 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 another TurretBase, you should adjust the parent’s position further by subtracting the collision component height / 2 from the z, and call Die on the other TurretBase

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.