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 Vector3
s 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 withmWorldTransform.GetTranslation()
GetWorldForward()
extracts the x-axis from the world transform withmWorldTransform.GetXAxis()
Now you need to add three member variables:
Actor* mParent
- A
std::vector<Actor*>
calledmChildren
- A
bool
calledmInheritScale
that defaults tofalse
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:
CalcWorldTransfrom
- If state is active, call
Update
on all components and thenOnUpdate
CalcWorldTransform
- Loop over each child in
mChildren
and callUpdate
on each (NOTOnUpdate
)
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’sGetWorldRotTrans
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:
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 theActor
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 ofMath::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 anew TurretHead
with the parent set tothis
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 fromMeshComponent
(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:
- Clear out the line segment vector
- Create the first line segment and insert it into the vector
- 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 useGetWorldForward()
)
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 aLength()
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, useMatrix4::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 theLineSegment
’sPointOnSegment
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:
- 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 - Take the point of collision from the
CastInfo
and transform it usingGetPortalOutVector
(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) - 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)
- 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.