Unreal Development Kit Game Programming with UnrealScript:Beginner's Guide
上QQ阅读APP看书,第一时间看更新

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.

  1. 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:
    Time for action – Experiments with function overriding

    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.

  2. Now for the GetPlayerViewPoint function. Looking at where it's declared in Controller.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)
    {
    }
  3. 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);
    }
  4. 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 our PlayerController has a Pawn.

    In the UDK, a Pawn is the physical representation of the player, the actual object in the world, with the PlayerController being its brain in a sense. In the game the PlayerController doesn't move, and indeed if we log our AwesomePlayerController's location in the GetPlayerViewPoint 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 our PlayerViewOffset variable to it. Wherever the Pawn 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:

    Time for action – Experiments with function overriding

    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:

    Time for action – Experiments with function overriding

    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);
  5. Compile the code and run it.
    Time for action – Experiments with function overriding

    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.

  6. 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 own Pawn, 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 called bOwnerNoSee. When that's set to True, 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);
        }
    }
  7. Compile and run the code.
    Time for action – Experiments with function overriding

    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.

  8. 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 the PlayerController 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 called GetAdjustedAimFor. Write the following code after our GetPlayerViewPoint 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)
    }
  9. Compile and run the game again.
    Time for action – Experiments with function overriding

    Much better! Now let's see if we can take care of that crosshair.

  10. Whether or not to show the crosshair is stored as a config bool in the PlayerController class. This means we can't just change it in the default properties, since we can't set config 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 the config 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 the bNoCrosshair variable in an overridden PostBeginPlay.
  11. 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)
    }
  12. Compile and run the code.
    Time for action – Experiments with function overriding

    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.

  13. 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);
        }
    }
  14. Compile and run.
    Time for action – Experiments with function overriding

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.