Sunday 15 July 2012

Understanding CryEngine 3 code: Moving the player

Let's have a look at how player input gets turned into character movement. But first, we need to talk about game actions.

Game actions

Remember these? The things you assign buttons to. The default control mapping is defined in Game\Libs\Config\defaultProfile.xml. The file looks like this:

<profile version="0">
 <actionmap name="singleplayer" version="22">
 <!-- singleplayer specific keys -->
  <action name="save" onPress="1" consoleCmd="1" keyboard="f5" />
  <action name="loadLastSave" onPress="1" consoleCmd="1" keyboard="f9" />
  <action name="load" onPress="1" consoleCmd="1" keyboard="f8" />
 </actionmap>

 <actionmap name="player" version="22">
 <!-- player keys -->
  <action name="use" onPress="1" onRelease="1" keyboard="f" />
  <action name="xi_use" onPress="1" onRelease="1" xboxpad="xi_y" ps3pad="pad_triangle"/>
  <action name="attack1" onPress="1" onRelease="1" onHold="1" keyboard="mouse1" xboxpad="xi_triggerr_btn" ps3pad="pad_r2"/>
  <action name="zoom" onPress="1" onRelease="1" keyboard="mouse2" />
  <action name="moveleft" onPress="1" onRelease="1" retriggerable="1" keyboard="a" />
  <action name="moveright" onPress="1" onRelease="1" retriggerable="1" keyboard="d" />
  <action name="moveforward" onPress="1" onRelease="1" retriggerable="1" keyboard="w" />
  <action name="moveback" onPress="1" onRelease="1" retriggerable="1" keyboard="s" />
  <action name="jump" onPress="1" onRelease="1" keyboard="space" xboxpad="xi_a" ps3pad="pad_cross"/>
  <action name="crouch" onPress="1" onRelease="0" retriggerable="1" keyboard="lctrl" xboxpad="xi_b" ps3pad="pad_circle"/>
  <action name="prone" onPress="1" onHold="1" keyboard="z" />
  <action name="sprint" onPress="1" onRelease="1" retriggerable="1" keyboard="lshift" xboxpad="xi_thumbl" ps3pad="pad_l1" />
  <action name="special" onPress="1" onRelease="1" keyboard="t" xboxpad="xi_thumbr" />
  <action name="leanleft" onPress="1" onRelease="1" onHold="1" keyboard="q" />
  <action name="leanright" onPress="1" onRelease="1" onHold="1" keyboard="e" />
  <action name="zoom_out" onPress="1" keyboard="mwheel_down" xboxpad="xi_shoulderl" ps3pad="pad_l1"/>
  <!-- CONSOLE SPECIFIC CONTROLS START -->
  <action name="xi_movey" xboxpad="xi_thumbly" ps3pad="pad_stickly"/>
  <action name="xi_movex" xboxpad="xi_thumblx" ps3pad="pad_sticklx"/>
  <action name="xi_rotateyaw" xboxpad="xi_thumbrx" ps3pad="pad_stickrx"/>
  <action name="xi_rotatepitch" xboxpad="xi_thumbry" ps3pad="pad_stickry"/>
  <!-- CONSOLE SPECIFIC CONTROLS END -->
 </actionmap>
</profile>

You map actions to buttons and define how they are triggered. I won't go into the details of the file format today.

Now, the important bit is these action names are not exactly things you can pull out of nowhere. In the code, there's a file named GameActions.actions which looks like this:

DECL_ACTION(moveleft)
DECL_ACTION(moveright)
DECL_ACTION(moveforward)
DECL_ACTION(moveback)
DECL_ACTION(jump)
DECL_ACTION(crouch)
DECL_ACTION(prone)
DECL_ACTION(togglestance)
DECL_ACTION(sprint)
DECL_ACTION(special)
DECL_ACTION(leanleft)
DECL_ACTION(leanright)

This file gets injected into the GameActions class during pre-processing, and the game code will need that to link actions to callback functions. That code gets executed in the constructor of CGameActions, at some point during the game startup process.

CPlayerInput

This class is responsible for processing game actions. The instance of CPlayerInput is created during the first update of the CPlayer instance.

The constructor of CPlayerInput enables the Actor to capture game actions, and then registers the callbacks for a bunch of actions.

CPlayerInput::OnAction()

Whenever you do anything that is mapped to an action (button press/release, mouse/joystick movement), this function gets called. It has 3 arguments:
  • The action being activated.
  • The activation mode (e.g. press/release/hold).
  • The value (used for joystick and mouse input).
The first important thing the function does is to dispatch this information to the correct function that will actually take care of processing the information. That's what we're going to take a look at in a minute.

Then, it's doing a bunch of things related to zoom and held items. I'm not sure what's the difference with the dispatched actions. Maybe that stuff is legacy and the Dispatcher is the method to use.

Eventually, the code gives Lua a chance to process the action by calling the Actor script's OnAction() function.

Moving the player


Finally, we're entering on the actual topic. Let's have a look at what happens when I move forward. If you look at the sample of the action map above, the moveforward action is bound to the W key on the keyboard. In the constructor of CPlayerInput, we see that the moveforward action is handled by CPlayerInput::OnMoveForward().

If you've followed what I've been talking about up until now, you may have guessed that if I press W, CPlayerInput::OnMoveForward() will be called via CPlayerInput::OnAction(). However, something interesting is happening. The game seems to have picked up the fact that I have a French keyboard, and swapped the mapping for W and Z. However, because the logic behind is hidden somewhere in the engine code, I'm unable to tell whether it does that only for movement or for everything (too lazy to conduct the experiment, plus I suspect it does it for everything).

bool CPlayerInput::OnActionMoveForward(EntityId entityId, const ActionId& actionId, int activationMode, float value)
{
 if (CanMove())
 {
  if(activationMode == 2)
  {
   if(!(m_moveButtonState&eMBM_Back))
   {
    // for camera
    m_moveStickUD = 0;
    m_deltaMovement.y = 0;

    if (!(m_moveButtonState&eMBM_Left) && !(m_moveButtonState&eMBM_Right))
     m_actions &= ~ACTION_MOVE;
   }
  }
  else 
  {
   m_actions |= ACTION_MOVE;
  }

  if(CheckMoveButtonStateChanged(eMBM_Forward, activationMode))
  {
   if(activationMode != 2)
   {
    ApplyMovement(Vec3(0,value * 2.0f - 1.0f,0));

    // for camera
    m_moveStickUD = value * 2.0f - 1.0f;
    m_deltaMovement.y = value;
   }

   m_checkZoom = true;
   AdjustMoveButtonState(eMBM_Forward, activationMode);
  }
 }

 return false;
}

This function is called on two occasions: when the button is pressed, and when the button is released. As we've seen before, this information is store in the activationmode variable. Even though it is an enum (EActionActivationMode), they test the variable against hard coded numbers...

The first half of the function handles the release of the button. It's testing that the that player isn't also pressing the "move backwards" button as the same time before resetting the movement variables. Then there's this m_actions bitfield, which basically indicates a state the character is in.

The second half is handling what happens when you press the button. It's double checking that the MoveButtonState has actually changed. That thing is a bit special case as for movement, it's not uncommon to have several configurations enabled at the same time, so there's an extra layer of tests to make sure that we modify movement variables only if necessary.

So if we press the move forward key, we end up calling the ApplyMovement() function. This function takes a vector that is constructed from the value of the action (which is 1 for a button press). I'm not to sure what the bias is for but I don't think it makes a difference.

Knowledge Nugget: That function call teaches us that the forward axis for Actors is +Y.

If we look inside the ApplyMovement() function, we see that all it does is adding the vector passed as an argument of the function to a variable called m_deltaMovement, making sure that each component is in the [-1,1] range. However, the weird thing is that after the call to that function, the Y component of m_deltaMovement is overwritten anyway, making the call to the function a bit pointless. Anyway, if we follow the variable's usage, it brings us to CPlayerInput::PreUpdate(). Let's see what this function has in store for us.

It all begins with a CMovementRequest. Because the actual class that handles actor motion is CMovementController and its children, CPlayerInput can only gathers the information the player gives it, interpret it in a sensible way. The reason it's all bundled in one object is network play, I suppose. Presumably it allows the server to drop a frame of input altogether in order to keep people in sync.

The first half of the PreUpdate function does a lot of data processing on the mouse input data and the xbox controller to determine the amount of rotation we need to request. Then, the m_deltaMovement variable we talked about earlier gets added to the request (after a bit of processing as well).

Next, the target stance is determined, according the m_actions bit field (those are different from GameActions, and basically represent states the player can be in).

Then it goes on about setting a "pseudo speed", which in the default implementation is simply 2 if we're sprinting, and 1 otherwise. If I understand correctly, the pseudo speed is fed into the animation graph, in order to adjust the animation accordingly.

Lastly, after doing some seemingly complicated calculation about whether strafing should be allowed or not, it sends the request to the Movement Controller, which is going to be our next stop.

Player Updates

Ok, now there's a lot of calls to functions that do lots of thing. So I'm going to give you the flow by means of a sequence diagram, and then highlight the important stuff happening in each function (note that I'm mostly looking at what is making the character look forward, so it is quite likely that I'm overlooking bits of code that are important for something else):

  • CPlayer::PrePhysicsUpdate
    • Determines whether movement is defined by physics (by default, false for AI and 3rd person player (which then use animation driven locomotion), true in every other situation)
    • Updates the Movement Controller.
    • Process rotation and then movement (provided it is allowed).
    • Sets requested stance, or force appropriate stance
  • CPlayerMovementController:UpdateNormal:
    • Sets a "body target", which is a prediction of the Character's next position, in world space.
    • Sets the desired velocity to whatever our action requested (that's in character space).
    • Prepares the new stance.
    • Handles leaning and and jumping.
    • Adjusts the speed according to the stance.
  • CPlayerMovement::Process:
    • Calls different processing function depending on the player movement mode (flying, on ladder, swimming, in Zero G or on the ground).
  • CPlayerMovement::ProcessOnGroundOrJumping:
    • Gets information from the Player about Air Control and Air Resistance.
    • Gets the player's current gravity. Knowledge nugget: gravity is a 3D Vector, so we can suspect that it is possible to alter the direction of gravity.
    • Scales the speed if the player is backpedaling.
    • Transforms desired movement from player space to world space.
    • Process potential jumping
    • Scales the speed if the player is in shallow water
    • Slows the player down on sloped terrain.
    • Stores the definitive desired movement in a character movement request structure.
  • CPlayerMovement::AdjustForEnvironment:
    • Scales the desired movement by the player's mass factor, which is determined by the mass of the currently equipped item (if any). Knowledge nugget: there is already code for going faster with the knife.
  • CPlayerMovement::Commit
    • Transfers the movement request to the Animated Character (CryAction black box...)
After all that, it goes in the darkness of the engine again, where the physics engine will actually update the position of the character.

Conclusion

The whole process looks a bit complex (especially when it comes to actually applying movement), but we have to keep in mind that it does handle a lot of things.

The important things to remember are:
  • The list of available game actions is semi hard coded.
  • Movement can be animation driven.
  • There's a bit field (m_actions) defining the player's movement states.
  • At some point, you do end up fiddling with the quaternion matrices.
If I were to modify the movement behaviour, I would try making the following modifications:
  • If necessary, change the action handling commands.
  • Change CPlayerMovementController::Update() to match the new stances/movement conditions.
  • Change the appropriate function or make a new one in CPlayerMovement::Process()

No comments:

Post a Comment