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

Theme: auto

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 of Math::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:

  1. Use the renderer’s Unproject function, passing in Vector(300.0f, -250.0f, 0.4f). Call SetPosition using the vector you get back from Unproject
  2. 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”).
  3. 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”).
  4. 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)
  5. Call SetQuat using the quaternion you get back from Quaternion::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 call SetTextureIndex on it, setting it to 0 if the portal is blue and 1 if it’s not
  • Creates a regular MeshComponent in that passes in true 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

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’s 1000.0f units away from mStart 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:

  1. Check if the actor you hit (which you can get from the CastInfo) is a Block*, because we will only allow portals to be created on blocks. You can use dynamic_cast to test this. If it’s not a Block*, just end the function and don’t create a portal.
  2. If it is a Block*:
    1. Create a new Portal actor at the point of intersection (you can also get this from the CastInfo)
    2. If there’s already a portal of the same color, you should set the existing portal to ActorState::Destroy
    3. Use the corresponding setter function in Game to update what it’s tracking as the current portal of that color

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): Portal view basics

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: Crosshair BothFill

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 position
  • GetPlayerVelocity 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 angle
  • GetPlayerPitch should return the player’s first person camera pitch angle
  • GetBluePortal 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 the InputReplay 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, call StartPlayback on the InputReplay instance, passing in the current level variable (you don’t need to worry about checking for the leading edge, because StartPlayback won’t do anything if playback has already started)
  • Right after you set the mouseButtons and relativeMouse variables, but before the loop that calls ProcessInput on each actor, call InputPlayback, passing in the same three parameters you later pass in to ProcessInput (you don’t need an if check because InputPlayback 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.