Portal Gun and Input Replay
With basic movement and platforming working, it’s time to add the portal gun.
Picking up the Portal Gun
Add a new subclass of Actor
called PortalGun
. It needs:
- A
MeshComponent
that uses the"Assets/Meshes/PortalGun.gpmesh"
mesh - A
CollisionComponent
with size(8.0f, 8.0f, 8.0f)
- An override of
OnUpdate
. In here, it should rotate at a rate ofMath::Pi
radians per second (just using the rotation angle, not a quaternion)
Then, in LoadActor
in LevelLoader.cpp, add a new case to create a PortalGun
(and don’t forget to set the actor
variable). The PortalGun
doesn’t have any special additional properties.
Confirm that past the first jump, you see an instance of PortalGun
and it rotates, like this:
Taking the Portal Gun
Now, change Player
to have a bool that says if the player has the gun (it should default to false for no gun). Create a function in player to “give” them the portal gun (for now, it should set the bool to true). Change HasGun
so correctly returns this bool rather than just always returning false.
Then, when the Player
intersects with the PortalGun
, you should destroy the PortalGun
actor and call the function to “give” the player the gun.
Then, in PlayerMove
add a member variable for a Crosshair*
and create one in the PlayerMove
constructor. The crosshair code is already implemented for you (though you will have to call member functions on it).
Confirm that when you intersect with the PortalGun
, it disappears from the ground and the portal crosshair shows up, like this:
PlayerMesh
When you pick up the portal gun, we want to show a first-person model of it so you can see that the player is carrying it. We’ll then update the position/rotation properly as the player moves through the world.
Create a new Actor
subclass called PlayerMesh
. It needs:
- A
MeshComponent
using the"Assets/Meshes/PortalGun.gpmesh"
mesh - Set the scale of the actor to
Vector3(1.0f, 2.5f, 2.5f)
- An override of
OnUpdate
In OnUpdate
, you need to:
- Use the renderer’s
Unproject
function, passing inVector(300.0f, -250.0f, 0.4f)
. CallSetPosition
using the vector you get back fromUnproject
- From the player’s camera component, get the pitch angle. Make a quaternion that represents this rotation. (Think about which axis you want to rotate about for “pitch”).
- From the player, get the yaw angle (this is just the player actor’s
GetRotation()
). Make a quaternion that represents this rotation. (Think about which axis you want to rotate about for “yaw”). - Given the pitch quaternion from (2) and the yaw quaternion from (3), use
Quaternion::Concatenate
to combine them (the parameter order should be pitch first, then yaw) - Call
SetQuat
using the quaternion you get back fromQuaternion::Concatenate
Finally, update the “give gun” function in Player
so it always creates a new PlayerMesh
actor.
If everything works properly, when you pick up the portal gun, you’ll notice you now have a first-person model of it. It should move and rotate properly with the player, like this:
Creating Portals
Now that you can pick up the portal gun, it’s time to add creating portals.
For now, update the constructor for Portal
so:
- It takes in a bool parameter saying whether the portal is blue or not
- Creates a
PortalMeshComponent
and callSetTextureIndex
on it, setting it to 0 if the portal is blue and 1 if it’s not - Creates a regular
MeshComponent
in that passes intrue
as the second parameter (so it has alpha) and:- Set the mesh to
"Assets/Meshes/Portal.gpmesh"
- Call
SetTextureIndex
on it, setting it to 2 if the portal is blue and 3 if it’s not
- Set the mesh to
The reason you make two different mesh components is because the PortalMeshComponent
is to handle the portal rendering effect (which we’ll add later) and the regular mesh component shows the outline.
In Game
add two class Portal*
private member variables, one for a blue portal and one for an orange portal. Update your GetBluePortal
and GetOrangePortal
functions to return the appropriate member variable instead of nullptr
. You also should add setter functions for the variables. You’ll use these to track whether there is already a portal of the specified type, and update them as you create portals.
Mouse Click
The leading edge of a left click should create a blue portal, and the leading edge of a right click should create an orange portal. There can only be one of each type of portal at any time, so, for example if there’s already a blue portal and you left click, the previous blue portal gets destroyed and you make a new blue portal.
We’ll handle creating portals in PlayerMove
. Since nearly everything will be the same between left click and right click (except whether it’s an orange or blue portal), you should make a CreatePortal
function that takes in a bool that’s true if the portal should be blue and false if it should be orange.
You can detect the state of the mouse buttons in ProcessInput
. For example, this gets the current state of the left mouse button:
bool leftMousePressed = mouseButtons & SDL_BUTTON(SDL_BUTTON_LEFT);
Similarly, you can get the current state of the right mouse button by replacing SDL_BUTTON_LEFT
in the above example with SDL_BUTTON_RIGHT
.
Add member variables so you can detect the leading edge of both the left and right mouse buttons.
If the player has the portal gun and you get a leading edge of either mouse buttons, call your CreatePortal
function. Remember that left click will make a blue portal and right click will make an orange portal.
Positioning the Portals
In CreatePortal
, you want to create a line segment and use the SegmentCast
function to find out which collider it intersects with, and get the point of intersection.
To figure out the start and end points of the line segment, use the picking technique discussed in lecture. (Check the slides for the discussion both on SegmentCast
and picking).
First, get the near plane point for the center of the screen by unprojecting Vector3(0.0f, 0.0f, 0.0f)
. Then, get a point close to the far plane by unprojecting Vector3(0.0f, 0.0f, 1.0f)
.
You can calculate a normalized direction from these two points. Then, for the LineSegment
:
- Set
mStart
to the near plane point - Set
mEnd
to the point that’s1000.0f
units away frommStart
in the normalized direction
One you have the LineSegment
, you can call SegmentCast
, passing in the vector of colliders as the first parameter.
SegmentCast
returns true
:
- Check if the actor you hit (which you can get from the
CastInfo
) is aBlock*
, because we will only allow portals to be created on blocks. You can usedynamic_cast
to test this. If it’s not aBlock*
, just end the function and don’t create a portal. - If it is a
Block*
:- Create a new
Portal
actor at the point of intersection (you can also get this from theCastInfo
) - If there’s already a portal of the same color, you should set the existing portal to
ActorState::Destroy
- Use the corresponding setter function in
Game
to update what it’s tracking as the current portal of that color
- Create a new
Confirm that if you don’t have the gun, left click and right click don’t do anything. Once you pick up the gun, you should be able to use left click to make blue portals and right click to make orange ones. Make sure the old portal goes away if you make a new one of the same color. The portals will not rotate properly yet, which means that you also will not be able to see portals that are facing in the -X direction yet (for example, on the wall that you face entering the portal gun room). It will look roughly like this:
Rotating the Portals
We want to rotate the portal to face in the direction of the normal of the surface the portal is on. You can get the normal from the CastInfo
as well.
Since the normal may be an arbitrary direction, you will need to set the portal’s quaternion to change the rotation. You’ll want to use the approach to create a quaternion to face in an arbitrary direction as discussed in lecture. Remember that:
- The original facing (before any rotation) is
Vector3::UnitX
- You want to rotate the portal to a desired facing of the normal of the surface (which you can get from
CastInfo
) - You have to calculate a (normalized) axis of rotation and the angle of rotation
- Don’t forget to account for the cases where the dot product is exactly 1 or exactly -1
The portals should now rotate correctly on the different walls and ceiling:
Adding Support for Portal Views
The Renderer
class has functionality to support rendering the scene from the view of the portal. To enable this, in Renderer.h change the MAX_PORTAL_RECURSIONS
variable to 1
. You may need to do a force rebuild of the game to get the change to take:
- On PC, select Build>Rebuild Lab09
- On Mac, press ⌘ + SHIFT + P to bring up the command palette and type in “rebuild” and select “CMake: Clean Rebuild”
Once you have two portals, you should see a view of the scene in the portal (though it will not be the correct view yet):
You’ll fix it so the portal actually shows where you’ll exit the portal in Lab 10.
Updating the Crosshair
When you create portals in PlayerMove
, you also want to update the crosshair to show the state of your portals. You can do this by calling SetState
on the crosshair. The state should be set as follows:
- If there’s only a blue portal,
CrosshairState::BlueFill
- If there’s only an orange portal,
CrosshairState::OrangeFill
- If there’s both a blue and an orange portal,
CrosshairState::BothFill
Make sure the reticules show up as expected. For example, if you have two portals you’ll see both sides of the crosshair filled in, like this:
Resetting Portals
Also in PlayerMove
, add support for the leading edge of the R
key. If this happens, you should destroy any portals that exist, reset the portal variables in Game
, and set the crosshair to CrosshairState::Default
.
After creating some portals, tap R
and make sure the portals disappear and the crosshair resets.
Input Replay
As mentioned in lecture, it’s helpful for testing purposes to be able to record and play back input sequences to confirm that the game behaves as expected. We’ve already provided an InputReplay
class as well as replay files for each Portal lab. With validation mode (the default), you’ll also be able to confirm that all your physics and other logic behaves correctly.
Implementing Functions in InputReplay
While all the complex logic for input replays is already implemented for you, there are a handful of functions at the bottom of InputReplay.cpp which you must implement for the replays (and validation) to not immediately fail.
Luckily, most of these functions are self-explanatory. Keep in mind you have access to the game via mGame
:
GetPlayerPosition
should return the player actor’s positionGetPlayerVelocity
should return the current velocity (from the player move)GetPlayerAcceleration
should return the current acceleration (from the player move)GetPlayerYaw
should return the player actor’s rotation angleGetPlayerPitch
should return the player’s first person camera pitch angleGetBluePortal
should return the pointer to the current blue portal (if there is one)GetOrangePortal
should return the pointer to the current orange portal (if there is one)
ResetPlayer Function
The last function to implement is a bit more complicated, however. The replay file assumes that the player is at the initial position, rotation, and camera orientation. If the player is not in this initial state, then the replay will immediately diverge.
To implement this, first add a Vector3 mInitialPos
member variable to player along with a getter/setter. Then in LevelLoader, when loading in the "Player"
add the following code to get the initial value of position (and set the player’s initial position to this):
Vector3 initialPos;
GetVectorFromJSON(actorValue, "pos", initialPos);
Also, if you don’t already have a function in CameraComponent
to reset the pitch angle to 0, add one for that, as well.
Then in ResetPlayer
, you need to:
- Set player position back to initial position
- Set player rotation back to 0
- Set pitch angle back to 0
Input Replay Setup
First, add the following member variables in Game
:
- A
std::string
containing the name of the current level - An
InputReplay*
to save the instance of theInputReplay
class
In Game::Initialize
, dynamically allocate the InputReplay
object.
In Game::Shutdown
, delete the InputReplay
object.
In Game::LoadData
, set the current level to "Assets/Lab09.json"
and change the call to LevelLoader::Load
to use the current level variable instead of just a string literal.
In Game::ProcessInput
:
- If the
P
key is down, callStartPlayback
on theInputReplay
instance, passing in the current level variable (you don’t need to worry about checking for the leading edge, becauseStartPlayback
won’t do anything if playback has already started) - Right after you set the
mouseButtons
andrelativeMouse
variables, but before the loop that callsProcessInput
on each actor, callInputPlayback
, passing in the same three parameters you later pass in toProcessInput
(you don’t need an if check becauseInputPlayback
won’t do anything if playback isn’t running)
In Game::UpdateGame
, right before the loop that calls Update
on each actor, call Update
on the InputReplay
instance.
When you load into the game, press the P
key. If the replay works correctly, you should get all the way to the end of the replay as in the video below. However, if you trigger an assertion some time during the playthrough, it means your implementation of player movement diverges from the expected. In this case, read below for how to debug this.
Validation Mode and Debugging
To get credit for the input replay spec (and a B), you need to ensure that the replay can start playing and at least make it to the glass wall before validation mode triggers an assertion. However, to get an A on this lab you must fix any divergence (and your game should pass all the way through in validation mode).
To best utilize validation mode, make sure you are running in the debugger (so on PC, you should use the filled-in green play button, and on Mac, you should use the bug button).
When validation mode detects a divergence, the debugger will trigger an assertion (which may show up as a dialog box) which that tells you to check the output log for details. For example, suppose you have a bug with the x velocity braking factor. Your output log might say:
WARN: Player velocity mismatch.
Expected: (359.999328, 0.632038, 0)
Actual: (399.999267, 0.632038, 0)
You can try to figure out what exactly was the character doing during the replay when the assertion happens (and you can also take a look at what’s in the window when it’s paused on it), and that plus the item which mismatched may give clues as to what is diverging in your implementation.
Some of these divergences are difficult to figure out, so if you may want to post on Piazza or come to office hours to get further assistance. However, as mentioned above, if you are just targetting a B, you don’t have to get all the way through, but make sure you at least make it to the glass wall so we can confirm that the replay does start and playback.
Once you’ve pushed your code, you should review the grading specifications to confirm you’ve satisfied them.