Time for action – Experiments with function overriding
Let's get to our experiment with function overriding by changing the way the player's camera works. If you've ever played any overhead view games like Gauntlet you'll know what we're going to do. In games like that, the camera stays in a fixed position high above the player's head, looking down towards the player. To do that, we're going to override the GetPlayerViewPoint
function.
- We know from our look at vectors in the previous chapter that we can get the location of actors in the world. If we wanted to move our camera away from the player, we'll need the player's location and an offset that we can use to make sure the camera stays in the same location relative to the player, like in the following diagram:
We could just directly add the values in the function, but to keep things organized it's usually a good idea to keep variables like that in the default properties where they can easily be found and changed if desired. We may also want to use this value for other purposes, so it's good to keep it all in one variable instead of having to track down and change each time we use it.
Let's add our offset and its default property to our code.
class AwesomePlayerController extends UTPlayerController; var vector PlayerViewOffset; defaultproperties { PlayerViewOffset=(X=-64,Y=0,Z=1024) }
The Z value will make it, so our camera is above the player. You can set this value to whatever feels right to you, but for now I'm using 1024. We've also put a value in for X to make it so the camera is moved to the side a bit and not completely straight down. But why is it negative? This value was chosen so that the radar on the default HUD stays aligned with our current direction. Other than that it's really arbitrary, there's no reason it couldn't be positive or even moved to the Y value if we wanted.
- Now for the
GetPlayerViewPoint
function. Looking at where it's declared inController.uc
, we see it needs to be written like this:simulated event GetPlayerViewPoint(out vector out_Location, out Rotator out_Rotation)
So let's place the function in our
AwesomePlayerController
to override it.simulated event GetPlayerViewPoint(out vector out_Location, out Rotator out_Rotation) { }
- The first thing we need to do is call the parent class' version of the function. We'll cover the super, more in the next chapter, but basically when we're overriding functions, calling the super makes the code in our parent class' version of the function we're overriding to be also executed. For
GetPlayerViewPoint
this is important because otherwise the camera wouldn't work at all. Let's add the line to our function:simulated event GetPlayerViewPoint(out vector out_Location, out Rotator out_Rotation) { super.GetPlayerViewPoint(out_Location, out_Rotation); }
- At this point nothing has changed, if we compiled now and ran the game it would still be a first person viewpoint. Now we'll apply our offset. Add these lines after the call to the super:
if(Pawn != none) { out_Location = Pawn.Location + PlayerViewOffset; out_Rotation = rotator(Pawn.Location - out_Location); }
This is a fair bit of code, so let's go through it one step at a time. The
if
statement you should recognize from the section in the last chapter about flow control. In this case we're checking to see if ourPlayerController
has aPawn
.In the UDK, a
Pawn
is the physical representation of the player, the actual object in the world, with thePlayerController
being its brain in a sense. In the game thePlayerController
doesn't move, and indeed if we log ourAwesomePlayerController
's location in theGetPlayerViewPoint
function we'll see that once spawned it stays at the same location. In order for our camera to follow the player, we need to follow the Pawn since that is the actual visual actor of the player.Inside the if statement, the first line gets our
Pawn
's location and adds ourPlayerViewOffset
variable to it. Wherever thePawn
is, the camera will stay locked to it with this offset.The next line is a bit of math to figure out the camera's rotation. We want it to always point toward the player, so we subtract our camera's location from the
Pawn
's to get a vector that points toward the player, and then turn that vector into a rotator that the function can use. This is a handy vector equation. The best way to remember it is to visualize two vectors, A and B. If we wanted to figure out what vector C was in the following diagram:If we only have A and B, we can figure out what C is, by moving backwards along A, and then forwards along B as in the following diagram:
This would give us C = -A + B, or C = B – A. In our code B would be the Pawn's location and A would be the camera's, giving us our line of code:
out_Rotation = rotator(Pawn.Location - out_Location);
- Compile the code and run it.
What in the world is going on here? We seem to be invisible except for a floating gun, and we're shooting at the ground. A big part of programming is knowing that your code isn't going to work perfectly the first time you write it. I call this process "breaking it towards completion". It might be broken right now, but it's a lot closer to what we wanted than when we first started. Let's see if we can make it better, starting with the invisible player.
- By default you can't see your own
Pawn
. This might not make sense at first. We can see our arms and the gun in our hands, so what am I talking about? The things we see in first person view are actually different actors attached to us, usually cut off above the elbows so we only see the arms and the weapon in our hands. If we were able to see our ownPawn
, the animation of it running would frequently obscure the camera's view and make it look like a polygon factory exploded on our monitor. To prevent this, meshes have a variable calledbOwnerNoSee
. When that's set toTrue
, the owner of that actor can't see it. This is what we'll change in our function. Add a new line to the top of our if statement:Pawn.Mesh.SetOwnerNoSee(false);
Our function should now look like this:
simulated event GetPlayerViewPoint(out vector out_Location, out Rotator out_Rotation) { super.GetPlayerViewPoint(out_Location, out_Rotation); if(Pawn != none) { Pawn.Mesh.SetOwnerNoSee(false); out_Location = Pawn.Location + PlayerViewOffset; out_Rotation = rotator(Pawn.Location - out_Location); } }
- Compile and run the code.
That's better. We can see our
Pawn
now. We're a bit obscured by the crosshair, but we can ignore that for a minute. We have a bigger problem right now. If anyone attacked us we'd be totally screwed because we're shooting at the ground. Let's fix that next. - In a normal FPS game on the UDK, when a weapon fires it asks the
PlayerController
which direction we're facing so it knows what to shoot at. Normally thePlayerController
tells the weapon to use our camera's rotation. This isn't going to work in our case, as we've changed it, so the camera is always pointing toward the ground. To fix this we're going to override another function calledGetAdjustedAimFor
. Write the following code after ourGetPlayerViewPoint
function:function Rotator GetAdjustedAimFor( Weapon W, vector StartFireLoc ) { return Pawn.Rotation; }
This tells the weapon to use our Pawn's rotation instead of the camera's rotation. Since the Pawn never changes its pitch value (otherwise when we looked up it would look like we were lying on our back), this will make sure that we always shoot straight ahead. Our class should now look like this:
class AwesomePlayerController extends UTPlayerController; var vector PlayerViewOffset; simulated event GetPlayerViewPoint(out vector out_Location, out Rotator out_Rotation) { super.GetPlayerViewPoint(out_Location, out_Rotation); if(Pawn != none) { Pawn.Mesh.SetOwnerNoSee(false); out_Location = Pawn.Location + PlayerViewOffset; out_Rotation = rotator(Pawn.Location - out_Location); } } function Rotator GetAdjustedAimFor( Weapon W, vector StartFireLoc ) { return Pawn.Rotation; } defaultproperties { PlayerViewOffset=(X=-64,Y=0,Z=1024) }
- Compile and run the game again.
Much better! Now let's see if we can take care of that crosshair.
- Whether or not to show the crosshair is stored as a
config
bool in thePlayerController
class. This means we can't just change it in the default properties, since we can't setconfig
variables in the defaults. This means we can change it one of three ways. We can remove the crosshair from the Scaleform HUD file, but Scaleform is a bit out of the scope of this book. We can change theconfig
value in the INI files, but if the player were to change it the crosshair would appear again. For a more permanent solution, we can change thebNoCrosshair
variable in an overriddenPostBeginPlay
. - Change our
PostBeginPlay
function to look like this:simulated function PostBeginPlay() { super.PostBeginPlay(); bNoCrosshair = true; }
Our class should now look like this:
class AwesomePlayerController extends UTPlayerController; var vector PlayerViewOffset; simulated function PostBeginPlay() { super.PostBeginPlay(); bNoCrosshair = true; } simulated event GetPlayerViewPoint(out vector out_Location, out Rotator out_Rotation) { super.GetPlayerViewPoint(out_Location, out_Rotation); if(Pawn != none) { Pawn.Mesh.SetOwnerNoSee(false); out_Location = Pawn.Location + PlayerViewOffset; out_Rotation = rotator(Pawn.Location - out_Location); } } function Rotator GetAdjustedAimFor( Weapon W, vector StartFireLoc ) { return Pawn.Rotation; } defaultproperties { PlayerViewOffset=(X=-64,Y=0,Z=1024) }
- Compile and run the code.
Almost there! Now what's with that giant gun? Remember when I talked about the first person view, and how the arms and weapon we see are different than the ones everyone else sees. The giant floating gun is what we would normally see in first person view, so let's hide it.
- In our
GetPlayerViewPoint's
if statement, let's add this bit of code:if(Pawn.Weapon != none) Pawn.Weapon.SetHidden(true);
Now our code will check if we're holding a weapon and if so, hide it. Our function should now look like this:
simulated event GetPlayerViewPoint(out vector out_Location, out Rotator out_Rotation) { super.GetPlayerViewPoint(out_Location, out_Rotation); if(Pawn != none) { Pawn.Mesh.SetOwnerNoSee(false); if(Pawn.Weapon != none) Pawn.Weapon.SetHidden(true); out_Location = Pawn.Location + PlayerViewOffset; out_Rotation = rotator(Pawn.Location - out_Location); } }
- Compile and run.
Perfect!
What just happened?
We've overridden a few functions in our quest to get things how we want. Overriding functions allows us to take the functionality that already exists and tweak it to fit our purposes. We could do a lot of different things with the GetPlayerViewPoint
function for instance. With the right code it could be turned into an RTS click-to-move type of camera that isn't focused on our Pawn, or a sidescroller, or a third person over the shoulder camera.
Function overriding is the main reason why I say it's important to read through the source code. Knowing what already exists will help you figure out what you need to change to get the functionality you want out of the game. The two classes we've already subclassed, GameInfo
and PlayerController
, are good places to start reading, as well as Actor and Object for general functions available to all classes.
Next up we're going to take a look at how to use actor classes themselves as variables.