A UT200x player is composed of a number of classes that all work together to accept and process input from the player and handle collision with the world. Bots use much the same system, so processing of AI also takes place (for Bot-specific subclasses; more on that in a minute) within the same code. What follows is an attempt to explain the different classes involved and how they interact.
Contents
A Quick Nod to the Controller Class
Here's what the class header of Controller has to say.
Controllers are non-physical actors that can be attached to a pawn to control its actions. PlayerControllers are used by human players to control pawns, while AI Controllers implement the artificial intelligence for the pawns they control. Controllers take control of a pawn using their Possess() method, and relinquish control of the pawn by calling UnPossess().
Controllers receive notifications for many of the events occuring for the Pawn they are controlling. This gives the controller the opportunity to implement the behavior in response to this event, intercepting the event and superceding the Pawn's default behavior.
Although a full discussion of the properties of Controller is beyond the scope of this page there are a couple of interesting (to me anyway) properties defined at this level.
- bGodMode - Set at this level it allows computer controlled players to enter god mode.
- FovAngle - The controller's field of view - allows bots to have wider FOV's. There's potential here for some bot customisation for the competative, as some players use a non standard FOV.
PlayerController
The PlayerController class is where the real action happens as far as human players go. This class determines what the player sees when in first person view and in third person view and how the "camera" behaves. It handles things like view shaking and the display of fog. There are also handling functions in there for force-feedback events and support for more console commands than you can shake a stick at.
The most important thing to remember about the PlayerController is that it serves two purposes. The controller's ViewTarget is responsible for driving the location and rotation of the scene the player sees. The controller's Pawn is the pawn that is actually controlled by the PlayerController class. They do not have to reference the same Actor.
- Mychaeel: What do you mean by the last sentence?
- EntropicLqd: When playing in the game the ViewTarget is normally the same object as the controlled Pawn. This does not have to be the case though, and it would be quite possible to set the ViewTarget to an actor that's trailing the controller Pawn for example. I say that ... but I've not tested it yet - hopefully I'll have the chance tonight. I should be working on Customising the Player View tonight so I'll probably cover it off in there. Watch this space so to speak.
- Mychaeel: Ah, that. Yes, it works. :)
The PlayerController and the controlled Pawn are tied explicitly to each other. The controller receives many events to do with the current state and actions of the Pawn it's controlling. These event functions are actually invoked by the Pawn being controlled as it moves through the world and interacts with it's environment. The following code is fairly typical of the Pawn callbacks on offer. In the example below if the Pawn has a controller then it is up to the controller to return the view rotation. By default this will actually be the rotation of the Pawn.
simulated function rotator GetViewRotation() { if ( Controller == None ) return Rotation; return Controller.GetViewRotation(); }
Views
The view you see as a player is driven by an event defined at the PlayerController level. The function definition is shown below.
event PlayerCalcView(out actor ViewActor, out vector CameraLocation, out rotator CameraRotation )
The values passed in ViewActor, CameraLocation, and CameraRotation are derived from an alternate universe (I've not found where yet). In essence this function performs the following steps.
- Calls the SpecialCalcView() function on the Pawn (possibly the view target, possibly the currently possessed pawn). This gives the Pawn an opportunity to construct a custom view based upon what it sees. (used for remote control robots and stuff I guess - possibly even the 'deemer although I've not looked). If this function calculates the view the player sees then it should return true. NOTE: The Pawn's bSpecialCalcView property must be true for this function to be called.
- If a special view was not constructed by the pawn then the current view target of the controller is checked to make sure it still exists or is not about to be deleted from the world. If deletion is pending then a new view target is obtained.
- If the current view target is the current "Pawn" associated with the controller then either CalcBehindView() or CalcFirstPersonView() is called to produce the camera location and rotation required.
- If the current view target is the controller itself then the controller's rotation is used as the camera rotation.
- If the current view target is a projectile then the camera location is set to the view target's collision height, and the camera rotation is set to that of the projectile being followed.
- If none of the above conditions are met then the view target is checked to see if it is a Pawn. If this is the case then the following happens:
- If we are a client of a networked game and our view target is a player pawn then we use the location of the view target and the rotation of the controller (it reduces the perception of lag and allows you to look around in behind view).
- If are the server (or running a standalone game) then both the location and rotation of the view target is used.
- The Default Behaviour
So, lets examine the PlayerCalcView() function. I've reproduced it here in its entirety as there are a couple of things worth noting about it. I've taken the liberty of adding some additional comments to the code in order to cover the things I think are important.
// If desired, call the possessed pawn's own view calculation function. If this function // calculates the view the player should see then it must return true. Otherwise the // calculated view will be replaced by the usual view. if( Pawn != None && Pawn.bSpecialCalcView ) { // try the 'special' calcview. This may return false if its not applicable, and we do the usual. if( Pawn.SpecialCalcView(ViewActor, CameraLocation, CameraRotation) ) return; // EXIT point from function. } // If the controller does not have a view target, or, it's view target is about to be removed from // the level then we need to get one. if ( (ViewTarget == None) || ViewTarget.bDeleteMe ) { if ( bViewBot && (CheatManager != None) ) CheatManager.ViewBot(); else if ( (Pawn != None) && !Pawn.bDeleteMe ) SetViewTarget(Pawn); else SetViewTarget(self); } ViewActor = ViewTarget; // Set the output variable ViewActor with the new view target CameraLocation = ViewTarget.Location; // Set the output variable CameraLocation with the view targets location // If the view target of the controller is the currently possessed pawn then the // calculation of the view the player sees is deferred to the first person and // behind view calculation functions. if ( ViewTarget == Pawn ) { if( bBehindView ) //up and behind CalcBehindView(CameraLocation, CameraRotation, CameraDist * Pawn.Default.CollisionRadius); else CalcFirstPersonView( CameraLocation, CameraRotation ); return; // EXIT point from function } // When all else fails the view target of the controller is itself. If the camera // position is locked then a fixed rotation is used. If not then the output variable // is set to the controller's rotation. if ( ViewTarget == self ) { if ( bCameraPositionLocked ) CameraRotation = CheatManager.LockedRotation; else CameraRotation = Rotation; return; // EXIT point from function } // If we are watching a projectile and we are in first person view then raise the camera // vertically upwards by the projectiles collision height. The camera rotation is set to // the rotation of the controller. if ( ViewTarget.IsA('Projectile') && !bBehindView ) { CameraLocation += (ViewTarget.CollisionHeight) * vect(0,0,1); CameraRotation = Rotation; return; // EXIT point from function } // Set the camera rotation to the rotation of the current view target. CameraRotation = ViewTarget.Rotation; PTarget = Pawn(ViewTarget); if ( PTarget != None ) { // If the view target is a pawn, and we are a client of a networked // game then we need to do some additional processing if ( Level.NetMode == NM_Client ) { if ( PTarget.IsPlayerPawn() ) { // If we are looking at a player pawn then the view rotation // is set to the TargetViewRotation (how the TargetViewRotation // is set is a mystery). The view target's rotation is also set // to this value. PTarget.SetViewRotation(TargetViewRotation); CameraRotation = TargetViewRotation; } // Adjust the view targets eye height. PTarget.EyeHeight = TargetEyeHeight; if ( PTarget.Weapon != None ) PTarget.Weapon.PlayerViewOffset = TargetWeaponViewOffset; } else if ( PTarget.IsPlayerPawn() ) CameraRotation = PTarget.GetViewRotation(); // If we are not in behind view then the position the camera location // at the view target's eye height. if ( !bBehindView ) CameraLocation += PTarget.EyePosition(); } // This is the view calculations used to determine the "behind view" when // the View Target is not the same as the Possessed pawn. if ( bBehindView ) { CameraLocation = CameraLocation + (ViewTarget.Default.CollisionHeight - ViewTarget.CollisionHeight) * vect(0,0,1); CalcBehindView(CameraLocation, CameraRotation, CameraDist * ViewTarget.Default.CollisionRadius); } }
OlympusMons: I took this from http://wiki.beyondunreal.com/wiki/Customising_The_Player_View and put it here I though this was a better place.
Spectators
Spectating is also handled by the PlayerController class. A state hierarchy is used to enabled and disabled various player functions and generally keep things tidy. The hierarchy is shown below.
BaseSpectating +- Spectating | +- AttractMode (also defined in xGame.xPlayer) +- PlayerWaiting +- WaitingForPawn
If you spend any time looking through the spectating code (which does more or less what you'd expect) you'll notice something really odd. A pathalogical desire to set the view target.
// Spectating - AltFire() calls ServerViewSelf() to return to your corpse (if you've been killed or whatever) // ServerViewSelf() does the following: SetViewTarget(self); ClientSetViewTarget(self); // ClientSetViewTarget() is defined as: function ClientSetViewTarget( Actor a ) { SetViewTarget( a ); }
In fact, every call to ClientSetViewTarget( target ) is preceded with a call to SetViewTarget( target ). Is this simply an example of redundant code within the code base or something more sinister? I'll leave it up to the reader to decide.
DaWrecka: More likely this is code that executes on the server. SetViewTarget() sets the server's copy of ViewTarget, but the client needs to have it updated; Hence the code calls ClientSetViewTarget(), which is replicated to the client and updates the client's side to match the server's. The presence of ClientSetViewTarget() in the replication block under "reliable if(Role == ROLE_Authority)" would seem to back me up.
Movement and Control
Player movement and control can be split into two sections. The first is capturing input from the player and doing stuff with it. The second is ensuring that the player's position in the world is consistent between the client and the server.
Capturing Player Input
There are two classes shipped with UT2003 responsible for handling the player's input, and managing the input configuration. Both of these are in the Engine class. The two classes are shown below.
Engine.PlayerInput +- Engine.XBoxPlayerInput
The PlayerInput class is defined as being within PlayerController. This means that the class has visibility of the attributes of the PlayerController class. These classes only exist on the client. The entry points into the PlayerInput class from a handling point of view are shown below.
// This function is called from PlayerController.PlayerTick( float DeltaTime ) // to figure out what the player is up to. event PlayerInput( float DeltaTime ) // This function is called from PlayerController.PlayerMove( float DeltaTime ) // it determines whether the player is dodging and if so, in which direction. function Actor.eDoubleClickDir CheckForDoubleClickMove(float DeltaTime)
It's pretty arcane in there. The logic within the double click move function is particularly tortuous. I'll need sleep before tackling that one.
Player Movement and Replication
Mychaeel: Player movement replication roughly works like this: Locally, the player moves freely, and the client sends information about that to the server. The server then tries to replay those moves when it receives them, and then sends information back to the client on whether the movements worked out or not (and, if not, where the player actually is right now). That way you don't experience any lag in your own movement when playing on a game server unless you interact with conflicting dynamic objects (for instance, other players or movers or projectiles).
Here's what the PlayerController class has to say about the player's movement replication. It's a good summary of exactly what is going on.
Here's how player movement prediction, replication and correction works in network games:
Every tick, the PlayerTick() function is called. It calls the PlayerMove() function (which is implemented in various states). PlayerMove() figures out the acceleration and rotation, and then calls ProcessMove() (for single player or listen servers), or ReplicateMove() (if its a network client).
ReplicateMove() saves the move (in the PendingMove list), calls ProcessMove(), and then replicates the move to the server by calling the replicated function ServerMove() - passing the movement parameters, the client's resultant position, and a timestamp.
ServerMove() is executed on the server. It decodes the movement parameters and causes the appropriate movement to occur. It then looks at the resulting position and if enough time has passed since the last response, or the position error is significant enough, the server calls ClientAdjustPosition(), a replicated function.
ClientAdjustPosition() is executed on the client. The client sets its position to the servers version of position, and sets the bUpdatePosition flag to true.
When PlayerTick() is called on the client again, if bUpdatePosition is true, the client will call ClientUpdatePosition() before calling PlayerMove(). ClientUpdatePosition() replays all the moves in the pending move list which occured after the timestamp of the move the server was adjusting.
I guess the above comments really beg the question, "How exactly do replicated functions work then?".
Related Topics
Discussion
OlympusMons: Ok I put all the stuff that was on the player controller page here for an overview similar to Controller Overview. This is probably temporary until the PlayerController page can be brought to standard formatting. If I was wrong in doing this please fix this ;)
SuperApe: Looks pretty good for the most part.
Category:Legacy Refactor Me – Some things here just need a little careful organization.