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

Theme: auto

Actor Parenting and Lasers

First, change the level to "Assets/Lab11.json".

We’re going to implement Actor parenting where one Actor can be the “child” of another and therefore transform relative to the parent’s coordinate space as opposed to relative to world space. We’ll test it out first by adding child parts of the door that move when the door opens.

Supporting parenting requires several changes to Actor.

First, make a new helper function called GetWorldRotTrans that returns a Matrix4. (Since it doesn’t modify member data, it should be a const member function). This calculates and returns a temporary matrix that uses the same formula as the world transform, except without the scale matrix. This is so that children have the choice of whether they’ll inherit the parent’s scale or not.

Now in Update, add an additional call to CalcWorldTransform at the beginning of Update. (So you’ll have a CalcWorldTransfrom call at the start of Actor::Update and at the end of Actor::Update. Both calls should happen regardless of whether the actor is paused).

Next, make two functions that return Vector3s by value (and are const member functions), which will use the technique we discussed in lecture about extracting a specific row or column from a matrix:

  • GetWorldPosition() extracts the translation component from the world transform with mWorldTransform.GetTranslation()
  • GetWorldForward() extracts the x-axis from the world transform with mWorldTransform.GetXAxis()

Now you need to add three member variables:

  • Actor* mParent
  • A std::vector<Actor*> called mChildren
  • A bool called mInheritScale that defaults to false

Next, change the signature of the Actor constructor to the following:

Actor(class Game* game, Actor* parent = nullptr);

This signature says the constructor takes in a second parameter for the parent, which defaults to nullptr. Setting the default value is important, because without it you would have to change every actor subclass constructor to accept and use this new argument, which would be very annoying.

Then, in Actor.cpp, change the implementation of the constructor so it takes in the Actor* parent and, in the initializer list, initializes mParent to parent. You don’t want to add the = nullptr part to the implementation, as default values are only specified in the declaration, not the implementation.

Add a GetParent() function that returns mParent.

Make sure your code still compiles!

Back in Actor, add two private functions: AddChild and RemoveChild. These both take in an Actor* and respectively add and remove it from the mChildren vector.

Now in the constructor, only call mGame->AddActor if mParent is nullptr. Otherwise, you want to call mParent->AddChild(this). This means the game won’t directly be tracking children actors (and instead, the parent will).

Similarly, in the destructor, only call mGame->RemoveActor if mParent is nullptr, otherwise call the mParent->RemoveChild function.

At the start of the destructor, you also need to keep deleting mChildren.back() until the mChildren vector is empty (this is just like how we had to delete Actors from the Game class because deleting the child will remove it from mChildren).

Next, at the end of Actor::Update, after your second call to CalcWorldTransform, loop over mChildren and call Update on each child.

This is why we call CalcWorldTransform twice during Update, because it makes sure the children will have the most up-to-date world transform based on the parent’s update.

So, the final version of Actor::Update should have an outline like this:

  1. CalcWorldTransfrom
  2. If state is active, call Update on all components and then OnUpdate
  3. CalcWorldTransform
  4. Loop over each child in mChildren and call Update on each (NOT OnUpdate)

The last thing to add is the real magic of parenting (multiplying matrices). In CalcWorldTransform, you want to first calculate mWorldTransform like before. However, you then need to check if mParent is set. If there’s a parent, then that means that the “mWorldTransform” you’ve calculated is not actually the world transform, but instead the object to parent space matrix. So, if there is a parent, you need to do this additional calculation to get it to actually be the actor’s world transform:

  • If mInheritScale is true, *= the parent’s world transform matrix
  • If mInheritScale is false, *= the parent’s GetWorldRotTrans matrix (the world transform matrix without the scale)

Similarly, in GetWorldRotTrans, if there’s a parent, multiply the return value by the parent’s result of GetWorldRotTrans before you return the Matrix4.

Make sure your code still compiles!

Adding Halves of the Door

Now that your code compiles, it’s time to test and see if parenting works by adding the actual doors to the door frame.

In Door.h, add two member Actor* member variables – one for the left half of the door, and one for the right half of the door.

In the Door constructor, dynamically allocate two Actor*s (one for each half), and save them in the member variables. Since you want these to be children of this door, you need to pass in this as the second parameter to the Actor constructor.

Both halves need a MeshComponent, with the following meshes:

  • Left half - "Assets/Meshes/DoorLeft.gpmesh"
  • Right half - "Assets/Meshes/DoorRight.gpmesh"

When you start the game, run all the way to the end and you should see a door with two halves of the door (the first two door frames you see along the way aren’t actually doors, so you won’t see halves for those). The door at the end, however, will look like this: Sides of door

Next, you want to set it up so when the door is “opened,” in addition to the door being removed from the colliders, the two sides of the door slide open.

You want the halves to slide open over the course of one second. You can do this by using a Vector3::Lerp and updating the position of both halves every frame in Door::OnUpdate. Both halves will start at Vector3::Zero, but the left half should end up at (0, -100, 0), and the right half should end up as (0, 100, 0).

Since the door halves are children, remember that when we’re updating the position, it’s relative to the parent, which is exactly what we want in this case!

Confirm that if you get the pellet to the energy catcher, the sides of the door slide open like this:

Setting Up Turrets

Make a new actor subclass called TurretHead. It has:

  • A parent, so needs to take in an Actor* parent parameter to its constructor, and passes that parameter to the Actor constructor in the initializer list
  • A scale of 0.75f
  • A position of Vector3(0.0f, 0.0f, 18.75f)
  • A MeshComponent using the "Assets/Meshes/TurretHead.gpmesh" mesh
  • An OnUpdate function that, for now, updates the actor’s rotation (just the yaw rotation angle) at a speed of Math::TwoPi/second

Next, make another actor subclass called TurretBase. It has:

  • A scale of 0.75f
  • A MeshComponent using the "Assets/Meshes/TurretBase.gpmesh" mesh
  • A CollisionComponent with size (25.0f, 110.0f, 25.0f), and is a collider
  • A destructor that removes itself from the colliders vector
  • A class TurretHead* mHead member variable, which is initialized in the constructor as a new TurretHead with the parent set to this

Then in LevelLoader, if the type is "Turret" create a TurretBase actor.

You should confirm that you see turrets in the level. The head should be spinning, but the base should be stationary, like this:

Lasers

Sentry turrets use a laser for their sight which makes it easy for the player to see where a turret is looking. We’ll draw the actual red laser using a mesh (it should NOT use alpha as it will not look correct with our portals otherwise). The default mesh has a x-length of 1 unit. This means we can scale the mesh in the x-direction to get it to the desired length based on how far it travels. We’ll also have to correctly rotate and translate this mesh depending on where we want to show the laser.

Create a new subclass of MeshComponent called LaserComponent:

  • In the body of the constructor, call SetMesh using the "Assets/Meshes/Laser.gpmesh" mesh
  • Add an override of Update (leave it empty for now)
  • Add an override of Draw. For now, just directly copy the implementation from MeshComponent (we will change it in a bit)
  • In the member data, add a std::vector<LineSegment> variable which will store the line segments corresponding to the lasers emitted by the laser component. (This means you need to include both "SegmentCast.h" and <vector>, as you can’t forward declare in this case)

Make sure your code compiles.

Update

In Lasercomponent::Update, on every frame you will:

  1. Clear out the line segment vector
  2. Create the first line segment and insert it into the vector
  3. Create an additional segment and insert it into the vector, if needed (based on portals)

We do this on every frame because the lasers may change depending on where the turret is facing, so this guarantees that we update the line segments as appropriate. For now, we’ll just do steps 1 and 2, and also not worry about collision (which will be handled a bit later).

The first line segment needs the following start and end points:

  • The start point is the owner’s world position (so you should use GetWorldPosition())
  • The end point is 350 units away from the start point in the direction of the owner’s world forward vector (so you should use GetWorldForward())

Transform

Next, add a helper function that takes in a LineSegment and returns a Matrix4. This function will return a world transform matrix that transforms the red laser mesh to start and end at the desired LineSegment points. You’ll need to make this matrix just like any other world transform matrix (by multiplying scale * rotation * translation), though the individual matrices will be slightly different:

  • For the scale matrix, use the version of Matrix4::CreateScale that takes in three floats (so it’s non-uniform). The scale for the x-component should be the length of the line segment (LineSegment has a Length() member function), and the y/z scales should stay at 1
  • For the rotation, you need to first make a quaternion that faces in the direction that the LineSegment faces. This is like how you calculated the quaternion for portals to face in the correct direction. Remember that you need to account for the cases where the dot product is exactly 1 or -1, just like with the portals. Once you create the quaternion, use Matrix4::CreateFromQuaternion to make the rotation matrix
  • For the translation, use the center point of the line segment and pass that to Matrix4::CreateTranslation. Hint: You can use the LineSegment’s PointOnSegment function to get this location.

Draw

Now in LaserComponent::Draw, change it so the entire existing body of the function is inside a loop that loops over every line segment in the member variable vector. Change the line that calls SetMatrixUniform on "uWorldTransform" so that instead of using the owner’s world transform, use the transform matrix calculated by your helper function (passing in the line segment for the current iteration of the loop).

Adding a Laser to TurretHead

In TurretHead, add two member variables: an Actor* for the actor which will have the laser, and a LaserComponent* (so that TurretHead can easily access the laser component).

In the constructor, create the laser actor setting this as the parent. The position for the actor should be (0, 0, 10). Then, create a LaserComponent, attaching it to the laser actor that you created.

Each turret should now have a laser and as the head spins around, the laser should, also. It will look like this. If it doesn’t work, see the debugging notes below:

Debugging Notes

If your lasers match the above video, you can skip this section. However, if you don’t see the lasers, you can debug this by putting a breakpoint in LaserComponent::Update. The very first time you hit this breakpoint after you launch the game is for the first turret.

This first turret should have a line segment with these positions:

mStart	{x=400.000000 y=25.0000000 z=-71.2500000 }	Vector3
mEnd	{x=364.875580 y=373.233093 z=-71.2500000 }	Vector3

If this seems correct, next try putting a breakpoint in your helper function to calculate the transform matrix for the laser. The first time you hit the breakpoint should be for the same line segment above. Here are the expected matrices.

Scale matrix:

{350.000031, 0.00000000, 0.00000000, 0.00000000}	float[4]
{0.00000000, 1.00000000, 0.00000000, 0.00000000}	float[4]
{0.00000000, 0.00000000, 1.00000000, 0.00000000}	float[4]
{0.00000000, 0.00000000, 0.00000000, 1.00000000}	float[4]

Rotation matrix:

{-0.100355506, 0.994951665, 0.00000000, 0.00000000}	float[4]
{-0.994951665, -0.100355506, 0.00000000, 0.00000000}	float[4]
{0.00000000, -0.00000000, 1.00000000, 0.00000000}	float[4]
{0.00000000, 0.00000000, 0.00000000, 1.00000000}	float[4]

Translation matrix:

{1.00000000, 0.00000000, 0.00000000, 0.00000000}	float[4]
{0.00000000, 1.00000000, 0.00000000, 0.00000000}	float[4]
{0.00000000, 0.00000000, 1.00000000, 0.00000000}	float[4]
{382.437805, 199.116547, -71.2500000, 1.00000000}	float[4]

Final matrix:

{-35.1244316, 348.233124, 0.00000000, 0.00000000}	float[4]
{-0.994951665, -0.100355506, 0.00000000, 0.00000000}	float[4]
{0.00000000, 0.00000000, 1.00000000, 0.00000000}	float[4]
{382.437805, 199.116547, -71.2500000, 1.00000000}	float[4]

If this all seems correct but you still don’t see the laser, it probably means you’re doing something wrong in draw.

Using SegmentCast

Right now, the laser always is 350 units in length, even if it collides against something. To dynamically update the laser based on a collision, you must use SegmentCast. You want the line segment to collide against all actors (i.e. you need to add a getter function in Game that returns the actor vector by const reference), not just colliders. However, you don’t want the line segment to collide against the turret itself, or it will just immediately stop.

The SegmentCast function optionally takes in an actor to ignore. To support this in LaserComponent, add a new Actor* member variable for the ignore actor as well as a setter function. When you create the LaserComponent in TurretHead, also set the ignore actor of the LaserComponent to the mParent of TurretHead (which will be the turret base that actually has the collision).

In Update, after you calculate the line segment for the first laser, use SegmentCast passing in all the actors as the actors to collide against. For the ignore actor, pass in member variable. If SegmentCast hits something, you should update the end point of the laser line segment to the point of collision.

You either need to update the end position of the line segment before you add it to the vector, or if you want to access it after you’ve added it to the vector, you can use .back() to get a reference to the last element inserted into the vector.

Your lasers should now dynamically change their end points based on collisions with actors. You can check this most easily by confirming the lasers get stopped by the small wall in between the first two turrets:

Lasers and Portals

First, remove the code in TurretHead::OnUpdate that rotates it every frame (as we won’t actually want the turrets to do this).

If the first line segment’s SegmentCast hits a portal, you need to create a second line segment that represents the part of the laser that’s on the other side of the portal (and add that line segment to the vector). This second line segment also needs to test for collisions and stop if there is a collision.

Technically, if you setup the portals in a specific way, the same laser could infinitely travel between two portals, but that would cause an infinite loop. To prevent this, we just don’t care if the second line segment hits a portal.

As with other portal teleporting, you can only create the second line segment if the first line segment collides with a portal and there is both an orange and blue portal in the world. Then you need to use your trusty GetPortalOutVector function again. You want to calculate the second line segment as follows:

  1. Transform the direction vector of the first line segment using GetPortalOutVector (think about the correct w value). This is the direction of the second line segment
  2. Take the point of collision from the CastInfo and transform it using GetPortalOutVector (again, think about the correct wo value). The start position of the second line segment should be this calculated position portal plus 5.5 units in the direction calculated in (1)
  3. The (initial) end position of the second line segment should be 350 units away from the start point from (2) in the direction calculated from (1)
  4. When you do the SegmentCast for this second line segment, instead of passing the member variable ignore actor (which corresponds to the turret), you need to pass in the “exit” portal as the ignore actor, as otherwise the second line segment would just immediately collide with the “exit” portal and stop

Try placing one portal in front of the first turret, and test out different spots for the second portal, confirming that the second laser shows up, is in the correct direction, and still collides with things (you can try walking in front of it to test collision). It should look like this:

Once you’ve pushed this code, you’re ready to move on to part 2.