07 Feb 2010:
This post describes the general structure of Evidyon’s client program.
The client program starts up in the WinMain method in “winmain.cpp” by creating a new instanceof the EvidyonClient class and invoking its execute() method.
This method runs the primary state-machine for the client that moves it between execution modes. Each mode represents something like loading the game’s resources, waiting for input at the account-login menu or simulating the game world. One interesting thing to note is that the state-machine object is built like a queue, so one can push states to be executed in sequence into the machine. This is how the program first enters the VCS_STARTUP state (specified in EvidyonClient’s constructor), where it loads up the game’s resource file. Each state has its own implementation file, so the code that gets run here is in statestartup.cpp.
Over in the stateStartup method, the client goes through loading each of its major components: graphics, input, network, system and resources. They refer to the following definitions:
Graphics - creates a Win32 window for the game and initializes Direct3D
Input - Would have initialized DirectInput, but this isn’t used so the method is empty
Network - Initializes the WinSock library via ENet
System - Misc. initialization; now used to set up the admin console if it is enabled
Resources - loads the client’s game file. This one is huge!
Each of these components is implemented in a file acquireX.cpp
(in the project’s Initialization folder) where X
is the component’s name.
After each of these has completed, the execution path proceeds back to the execute() method and the next state is run. If the game successfully initialized, the client will attempt to connect to the remote server in the VCS_CONNECT
state. As is logical, this is implemented in the stateconnect.cpp file.
Once a connection is obtained, the connect state pushes the account-login state. Unfortunately, stateaccountlogin.cpp is a mess. This state is responsible for creating accounts, validating passwords and logging in. From here, the user could go to the credits screen (VCS_CREDITS
) be disconnected (VCS_CONNECT
) or be successfully logged in (VCS_LOGGED_INTO_ACCOUNT
).
After successfully logging in, stateLoggedIntoAccount()
will display the list of characters specified by the server and allow the user to create a new one, delete an existing character or enter the world with a character. Entering the world is where the good stuff is, and since this is a quick overview let’s just go to VCS_WORLD_MAIN
(stateworldmain.cpp
).
Entering the world is half handled by the logged-into-account state and the actual stateWorldMain method. The former will try to be sure existing objects are removed and initialize them with new values contained in the reply from the server–for example, the current time, the character’s stats/appearance, attributes, and world location. The latter sets up all the state-specific GUI components and other data.
There is so much state-specific data in the main world execution that, at some point, I pulled it all out into a StateWorldMainContext structure so that it wouldn’t all be allocated on the stack. Look there for all of the game components.
The first section of the “main game loop” is a giant message-processor. Since I started out with only a dozen or so messages and added them as I went along, having if…else if… else if… else if… didn’t seem like such a bad thing at the time. As this grew I didn’t refactor and what you see here is the result. Right around line 1000 is where the madness ends and the next section starts. It’s a big switch()
based on user actions that turns requests into network messages or results. Action::TYPE_MOVE
, for example, is generated by the input subsystem whenever the user is trying to move their character around. However, I didn’t like this way of doing things and tried to implement stuff in ActionProcessor for all of the newer actions. Being on a huge time restriction, it wasn’t feasible to rewrite the old code to use ActionProcessor (even though it would have been much cleaner) but that’s where things were going. At ~1350 you’ll see the ActionProcessor being updated.
// Handle all user events
context->action_queue.process(&context->action_processor);
The final section, guarded by allowFrameToBeRendered()
, is the section that invokes all of the draw calls for the scene. The following bit of code is so small it could easily be lost among the thousands of other lines, but it is what renders the entire 3d world:
// Render all of the objects in the world
size_t number_of_textures = renderstate_manager_.numberOfTextures();
for (Texture::TextureIndex texture = 0; texture < number_of_textures; ++texture) {
map_renderer_.render(texture, &renderstate_manager_, client_x, client_z, light_radius);
visual_fx_renderer_.render(texture);
scenery_renderer_.render(texture, &renderstate_manager_);
skinned_mesh_renderer_.render(texture, &renderstate_manager_);
}
This section goes through each texture in the game, in order, and renders the geometry that uses that texture. I’ll write a full post on how this works later, but just know that it is implemented fairly efficiently–the textures are sorted in a specific way, unused ones are ignored, and so on.
From this main gameplay state, the program can be disconnected or return to the login page using the state machine.
That’s it for our whirlwind tour of the client!