Tuesday, 12 June 2012

Understanding CryEngine 3 code: loading a level

Last time I took a look at the startup sequence of a CryEngine 3 game. Now, it's time to investigate what happens when loading a level, up to the spawning of the player's character. This will eventually lead us to the Lua side of the engine.

The investigation will start from when the player selects a level from the main menu.

Game Rules

In the sample game, when the player clicks on a level from the game's frontend, the following function is called:
void CUIGameEvents::OnLoadLevel( const char* mapname, bool isServer, const char* gamerules )
{
 if (gEnv->IsEditor()) return;

 ICVar* pGameRulesVar = gEnv->pConsole->GetCVar("sv_GameRules");
 if (pGameRulesVar) pGameRulesVar->Set( gamerules );
 m_pGameFramework->ExecuteCommandNextFrame(string().Format("map %s%s", mapname, isServer ? " s" : ""));
 if ( m_pGameFramework->IsGamePaused() )
  m_pGameFramework->PauseGame(false, true);
}

Before calling the map console command to load the level, it's changing a console variable to set the "game rules" to what's being passed to the function. For instance, if you load the Forest level, the game rules will be "SinglePlayer".

So what are those game rules? Well, as far as I understand (and as the name would suggest), they represent the core of the gameplay code. There's a class called GameRules.cpp. One would assume that there would be one child class per game mode. However, this is the not the case.

Instead, CGameRules loads a Lua script corresponding to the actual game rules. Then, any callback on CGameRules gets forwarded to the Lua script after being processed internally. The way I see it, the C++ side of the Game Rules is there to handle game-wide and/or "critical" code, while the Lua side sticks to game mode-specific tasks.

CGameRules::Init()

Soon after the loading screen show up, CGameRules::Init() gets called. It caches some pointers, registers itself to listen to View (read camera) changes, and then starts the Lua/C++ entanglement:

m_script = GetEntity()->GetScriptTable();
if (!m_script)
{
 // script table not found
}
else
{
 m_script->GetValue("Client", m_clientScript);
 m_script->GetValue("Server", m_serverScript);
 m_script->GetValue("OnCollision", m_onCollisionFunc);
}

m_collisionTable = gEnv->pScriptSystem->CreateTable();

m_clientStateScript = m_clientScript;
m_serverStateScript = m_serverScript;

The entity tied to the Game Rules instance (who knows where that comes from (probably the game framework)) holds a reference to the associated script. Then it's doing stuff with it, but before going into those details, let's have a look at that script:

SinglePlayer = {
 DamagePlayerToAI =
 {
  helmet  = 4.0,
  kevlar  = 0.75,

  head    = 50.0,
  torso   = 1.2,
  arm_left = 0.65,
  arm_right = 0.65,
  hand_left = 0.3,
  hand_right= 0.3,
  leg_left = 0.65,
  leg_right = 0.65,
  foot_left = 0.3,
  foot_right= 0.3,
  assist_min =0.8,
 },

 --8<---
 Client = {},
 Server = {},
 
 -- this table is used to track the available entities where we can spawn the
 -- player
 spawns = {},
}

Like most things Lua, it starts with a table. Then there's a bucketload of functions that are defined, but I'm not copying the entire file as it's about 1800 lines long. The important thing here is the two highlighted lines. Two sub-tables are created to hold functions that are either client-specific or server-specific. That is obviously only relevant for multiplayer.

Back to C++, we can see that the code is looking for three things in our game rules table:

  • The client functions table
  • The server function table
  • the collision function
This way, the code can call the appropriate script functions.

Soon after that, the lua function SinglePlayer.Client:OnInit() is called, which by default does nothing but preparing the fade from black to game.

CGameRules::OnClientConnect()

Near the end of the loading, when the player is ready to get spawned, this function is called, and in turn calls SinglePlayer.Server:OnClientConnect(). This function is quite important as this is the place where the player is spawned. Actually, it's a two step process:

function SinglePlayer.Server:OnClientConnect( channelId )
 local params =
 {
  name     = "Dude",
  class    = "Player",
  position = {x=0, y=0, z=0},
  rotation = {x=0, y=0, z=0},
  scale    = {x=1, y=1, z=1},
 };

 player = Actor.CreateActor(channelId, params);
 
 if (not player) then
   Log("OnClientConnect: Failed to spawn the player!");
   return;
 end
 
 local spawnId = self.game:GetFirstSpawnLocation(0);
 if (spawnId) then
  local spawn=System.GetEntity(spawnId);
  if (spawn) then
   --set pos
   player:SetWorldPos(spawn:GetWorldPos(g_Vectors.temp_v1));
   --set angles
   player:SetWorldAngles(spawn:GetAngles(g_Vectors.temp_v1));
   spawn:Spawned(player);
   
   return;
  end
 end

 System.Log("$1warning: No spawn points; using default spawn location!")
end;

The player's actor first gets created at an arbitrary position, and is then spawned properly. As you can see the "proper" spawn process is kind of manual, as in not handled by code. It may sound strange, but it means that scripter has complete control over the logic behind spawning, which may or may not be a good thing (I think it is). It then lets the entity know that it has spawned something, should the spawn point do any additional logic (e.g. a spawn particle effect).

I'll talk more in depth about about the player creation in another post.

CGameRules::OnClientEnteredGame()

Lastly, this callback is triggered when the player is ready to play. Nothing interesting happens in the default implementation.

After that, the game can actually begin!