Wednesday, 20 June 2012

Understanding CryEngine 3 code: player character

Now, I want to know about the player character, especially about the way it's set up code-wise.

Entity definition

In CryEngine 3, entities are objects that are present in the level. They are defined by 2 files: an XML file and a Lua file.

The xml file lives in Game/Entities and has a .ent extension. The content looks as follows:

<entity name="Objective" script="Scripts/Entities/Multiplayer/Objective.lua"/>

Yep, just that. As far as I understand, the only purpose of the .ent files is to expose the entity to the editor. The lua file is much more interesting (though I won't do a full explanation here as I want to stay focussed on the topic at hand: the player).

Script.ReloadScript( "SCRIPTS/Entities/actor/BasicActor.lua");

Player = {

 AnimationGraph = "HumanMaleFullBody.xml",
 UpperBodyGraph = "HumanMaleUpperBody.xml",

 type = "Player",
 
 Properties = 
 { 
  Perception =
  {
   --how visible am I
   camoScale = 1,
   --movement related parameters
   velBase = 1,
   velScale = .03,
   --ranges   
   sightrange = 50,
  } ,

  fileModel = "Objects/Characters/Agent/Agent.cdf",
  clientFileModel = "Objects/Characters/Agent/Agent.cdf",
  fpItemHandsModel = "Objects/Weapons/Agent_fp/Agent_fp_Global.chr",
  objFrozenModel= "Objects/Characters/Agent/Agent.cdf",
  fileHitDeathReactionsParamsDataFile = "Libs/HitDeathReactionsData/HitDeathReactions_PlayerSP.xml",
 },
 
 gameParams =
 {
  lookAtSimpleHeadBone = "Bip01 Head",
  canUseComplexLookIK = true,

  inventory =
  {
   --8<--
  },

  Damage =
  {
   health = 150,
  },

  ammoCapacity =
  {
   bullet=40*7,
   --8<--
  },
 
  stance =
  {
   {
    stanceId = STANCE_STAND,
    normalSpeed = 1.25,
    maxSpeed = 3.5,
    walkSpeed = 1.7,
    runSpeed = 4.0,
    sprintSpeed = 6.6,
    heightCollider = 1.2,
    heightPivot = 0.0,
    size = {x=0.4,y=0.4,z=0.3},
    viewOffset = {x=0,y=-0.05,z=1.625},
    modelOffset = {x=0,y=0,z=0.0},
    name = "combat",
    useCapsule = 1,
   },
   {
    stanceId = STANCE_CROUCH,
    --8<--
   },
  },
  
  sprintMultiplier = 1.5,--speed is multiplied by this amount if sprint key is pressed -- 1.2 for a more counter-striky feel
  strafeMultiplier = 0.75,--speed is multiplied by this amount when strafing
  backwardMultiplier = 0.7,--speed is multiplied by this amount when going backward
  grabMultiplier = 0.5,--speed is multiplied by this amount when the player is carry the maximun amount carriable
    
  inertia = 10.0,--7.0,--the more, the faster the speed change: 1 is very slow, 10 is very fast already 
  inertiaAccel = 11.0,--same as inertia, but used when the player accel
   
  jumpHeight = 1.0,--meters
  
  slopeSlowdown = 3.0,
  
  leanShift = 0.35,--how much the view shift on the side when leaning
  leanAngle = 15,--how much the view rotate when leaning
 
 },

 Server = {},
 Client = {},

}

CreateActor(Player);
Player:Expose();

I've snipped out a number of things. The important bits are highlighted, as usual. First, the script is loading another script, in order to be able to call functions defined in it (among other things).

Then, we can see the model and the animation graph being set, within a Properties table. In entities exposed to the editor, this table defines the parameters exposed to the editor.

Next, inside a table called gameParams, a table of stances is initialised. Stances define a number of parameters that affect the movement and the camera. I'll go into the details of this in another post.

As in GameRules (and actually any kind of entity) there are tables to hold client and server specific functions.

Lastly, there's that CreateActor() function. Its definition lives in BasicActor.lua. After a quick read, we learn that this is basically a way of making the Player entity inherit properties from the BasicActor entity. I'm actually surprised that they do this using table copy rather than metamethods.

Oh and for people who come from Unreal like me, there's a potential confusion to clear up. CryEngine's Actor is the equivalent of Unreal's Pawn, and CryEngine's Entity is the equivalent of Unreal's Actor.

Code-side player initialisation


CPlayer::Init()

If you remember last post on level loading, you know that the player is created from Lua during the OnClientConnect() method of the game rules. After a short journey in the darkness of CryAction, CPlayer::Init() is called. As CPlayer is a child of CActor, CActor::Init() is called as well.

However, during level loading and before OnClientConnect() gets called, CPlayer::Init() is called about 16 times from the depths of CryAction, so I don't really have a clue what's going on here, but that doesn't seem to have anything to do with spawning the player.

The first interesting thing this function does is creating a new CPlayerMovementController. In CPlayerMovementController::Reset(), we can observe a number of things:

m_aimInterpolator.Reset();
m_lookInterpolator.Reset();
m_updateFunc = &CPlayerMovementController::UpdateNormal;
m_targetStance = STANCE_NULL;

  • Interpolators are used for aim and look, suggesting smoothing by default.
  • The system seems to be designed to allow changing the update function depending on the situation.
  • Target stance also implies lerping from one stance to another.
Back in CActor::Init(), after the Animated Character object extension has been initialised, it's doing a bunch of binding for the Animation Graph. Then, another object extension called Inventory is added. Conclusion, Game Object Extensions seem to look a lot like Unreal's Actor Components.

CPlayer::Revive()

Still in CPlayer::Init(), after the CActor version of that function has returned, CPlayer::Revive() is called. Once again, the function of the parent class is called. As that function can be called during the game, it has a slightly different behaviour when called from the Init function. The only difference is that it calls CGameRules::OnRevive(), which does little more than calling the Lua equivalent of this callback (from the client side). Note that by default, this Lua function is not implemented.

Then, SetActorModel() is called, which just forwards to the Lua version of the function (belonging to BasicActor). Knowledge Nugget: the actual mesh assignment is done in Lua.

Next, game parameters are loaded. This is done by loading the gameParams Lua table we talked about earlier, and assign the values of its members to the members of a C++ structure. Game Params contains the stances as well as various movement and camera-related parameters.

Once the parameters are set, the Actor gets physicalised. This involves getting information from another Lua table called physicsParams. After that physicalisation, we can see the following code block:

// for the client we add an additional proxy for bending vegetation to look correctly
if (IsPlayer())
{
 primitives::capsule prim;

 prim.axis.Set(0,0,1);
 prim.center.zero(); prim.r = 0.4f; prim.hh = 0.2f;
 IGeometry *pPrimGeom = gEnv->pPhysicalWorld->GetGeomManager()->CreatePrimitive(primitives::capsule::type, &prim);
 phys_geometry *pGeom = gEnv->pPhysicalWorld->GetGeomManager()->RegisterGeometry(pPrimGeom, 0);
 pe_geomparams gp;
 gp.pos = Vec3(0.0f,0.2f,0.7f);
 gp.flags = geom_colltype_foliage;
 gp.flagsCollider = 0;
 pGeom->nRefCount = 0;
 //just some arbitrary id, except 100, which is the main cylinder
 GetEntity()->GetPhysics()->AddGeometry(pGeom, &gp, 101);
}

We learn that it is possible to add (and most likely remove) collision primitives whenever we feel like it. After that, the physics get reset to some sensible values, and the stand is set to the Stand one by default. Then, the movement controller gets reset, but we'll talk about that next time.

After another bunch of initialisations, the code eventually calls the Lua function OnRevive() on the Actor. At that point, we leave CActor::Revive(), but we're still at the beginning of CPlayer::Revive(). So what more is there to do? Not much apart from resetting a boatload of variables. At some point in the function we see that it's trying to reset the PlayerInput, but at that point in the loading process, it doesn't exist yet. It gets created during the first update of the Player.

At the end of the day, the most important thing we learned here is that once again, a huge amount of the data description work is done Lua side. The other big thing is that the attachment of meshes to the actor is also done in script.