Skip to content

Game Structure

enriquegr84 edited this page Mar 11, 2020 · 1 revision

The showcase game dig into the internals of the game architecture and show how to tie the application layer, the game logic, and game views together with the event system. It features most of the game components we have explained so far, such as graphics, physics and AI. Still, other game components need to be completed in the engine, for instance, the networking system which would allow us to create a multiplayer game over the Internet. The game project is created separately from the engine in order to differentiate the game code from the engine code. In the GameEngineTutorial/Source directory can be found the Quake game project folder which contains all the game-specific C++ code for the entire game. Not only the source code must be clearly separated but also any kind of file asset in the game. In the GameEngineTutorial/Assets directory can be found all the game-related content which includes audio, graphic effect, and XML files for configuration and data-driven actors.

In the engine, the GameApplication, BaseGameLogic and BaseGameView are the core classes that control the entire game and are used as base for the application, logic and view classes. They manage game-specific code using a common design pattern, the template method pattern. It is a behavioral design pattern that defines the skeleton of the game application superclass, and allow subclasses to override specific virtual functions in order to add game code without changing the base class structure.

In the application layer, the QuakeApp game application which extends the GameApplication class is created and initialized in the main startup function. Since most of the work is done in the GameApplication base class, the purpose of its derived class is to act as a configuration by overriding the following virtual function; The RegisterGameEvents() virtual function allows the game-specific subclass to register all its game events during the game initialization. The pure virtual CreateGame() function responsible for creating the game logic, and the virtual functions responsible for handling game views, such as AddView and RemoveView.

class QuakeApp : public GameApplication
{
public:
   // Abstract base class.
   QuakeApp();
   virtual ~QuakeApp();

   virtual void CreateGame();

   virtual void AddView(const eastl::shared_ptr<BaseGameView>& pView);
   virtual void RemoveView(const eastl::shared_ptr<BaseGameView>& pView);
   virtual void RemoveViews();
   virtual void RemoveView();

protected:
    virtual void RegisterGameEvents(void);
    virtual void CreateNetworkEventForwarder(void);
    virtual void DestroyNetworkEventForwarder(void);
};

The logic layer is where the gameplay takes place, that is, the specific way in which players interact with a game through the game rules. The gameplay system is defined in the QuakeLogic class which derives from GameLogic and handles all the different the game states in which it can be in with two functions: OnUpdate() and ChangeState().

The game logic OnUpdate() vritual function begins by updating the lifetime of the object. It works as a state machine consisting on a switch statement which processes the current game state and manage the transitions to other states. This function also performs any per-frame operations such as, updating all the game objects, the process manager and the physics system.

The ChangeState() virtual function is called whenever we need to transit to a new state. Most of this state processing happens in the base GameLogic, and the QuakeLogic needs to override this function in order to implement game-specific state processing. The base GameLogic is only interested on detecting when the game begins so it can wait for players to connect, and the QuakeLogic overrides this function for handling game-specific actors creation and spawn.

class BaseGameLogic
{
public:
   virtual eastl::weak_ptr<Actor> GetActor(const ActorId id) = 0;
   virtual eastl::shared_ptr<Actor> CreateActor(
      const eastl::string &actorResource, 
      tinyxml2::XMLElement *overrides, 
      const Transform *initialTransform = NULL,
      const ActorId serversActorId = INVALID_ACTOR_ID) = 0;
   virtual void DestroyActor(const ActorId actorId) = 0;
   virtual bool LoadGame(const char* levelResource) = 0;
   virtual void SetProxy() = 0;
   virtual void OnUpdate(float time, float elapsedTime) = 0;
   virtual void ChangeState(enum BaseGameState newState) = 0;
   virtual void SyncActor(const ActorId id, Transform const &transform) = 0;
};

All the different game states are represented by the BaseGameState enumerator. Initially, the game is presented in the main menu BGS_MAINMENU in which the game-specific state will add a human view for the menu interface. The menu has two main options: create a new game with the selected options or join a game by filling out the host editbox (the second option is still not implemented). After clicking on the Start button, it is sent the event that request to start a new game, making the game to wait for the players to connect in the BGS_WAITINGFORPLAYERS state. The first thing that game-specific ChangeState() does is to call the base class since there may be important processing to do. Specifically, in this state the base class removes the front game view, which is assumed to be the main menu and reads the options from the application layer to find out how many players are expected. With this information the game-specific class will be able to add all the expected views for any type of player (human players, AI players or remote players) to the list of views that the game application maintains. Once it is confirmed that all the players are ready, the game will change to the loading game environment state BGS_LOADINGGAMEENVIRONMENT. If it is a network game, the host tells each attached client which XML file level to load. All loading happens from the local machine, in which the base game logic will load the XML file level as static actor, and will call the game-specific delegate function to load any actors which play a role in the game level. Each client remains idle in the state BGS_WAITINGFORPLAYERSTOLOADENVIRONMENT until all human players report that their environments are loaded. After changing to the BGS_SPAWNINGPLAYERACTORS state, the game will spawn all the players into their level, it will attach them to their related type of view, and it will send the new actor event to let other systems know that an actor has been created.

The view layer is responsible for presenting the game, accept input, and translate that input into commands for the game logic. We have seen that the QuakeApplication can attach three different type of views: a view for a local human player, a view for an AI player, and a view that represents a player on a remote machine. The view for the human player uses the graphic system to render the scene as well as the user interface, and the audio system for displaying sounds. There are two classes which inherit from the human view, QuakeMainMenuView that defines the main menu view for the game, and QuakeHumanView that defines the main game view. They need to override the same functions but obviously perform different tasks.

class QuakeHumanView : public HumanView
{
public:
   virtual bool OnMsgProc( const Event& event );	
   virtual void RenderText();	
   virtual void OnUpdate(unsigned int timeMs, unsigned long deltaMs);
   virtual void OnAttach(GameViewId vid, ActorId aid);

   virtual void SetControlledActor(ActorId actorId);
   virtual bool LoadGameDelegate(tinyxml2::XMLElement* pLevelData) override;

protected:
   eastl::shared_ptr<QuakePlayerController> mGamePlayerController;
   eastl::shared_ptr<QuakeCameraController> mGameCameraController;

   eastl::shared_ptr<QuakeStandardHUD> mGameStandardHUD;
   eastl::shared_ptr<Node> mPlayer;
};

The QuakeHumanView relies on the HumanView class to keep track of the audio through the process manager, display and update a list of visual screen elements such as the user interface on top of the scene, forward the input events to the attached screen elements, and if the input event is not consumed by any screen element then it will be passed to a corresponding device handler. The QuakeHumanView class hooks into the event handler from the application layer for user interface processing or input device handling. It has a reference to a QuakeStandardHUD object for UI rendering and two references to controller objects which allows the human player to interact with the game. The QuakeCameraController class implements a free-fly camera node and the QuakePlayerController class represents the currently controlled player node. Both of them are updated each frame but only one of them can be active to receive input message. It is a common practice to factor control systems that have a particular interface, like keyboard controls, into a class that can be attached and detached as necessary. Both controller classes wire in to the base HumanView class and respond to its events by implementing the BaseMouseHandler and BaseKeyboardHandler interfaces. Keyboard and mouse events are recorded as they happen and are processed in the HumanView OnUpdate() function to translate these interface events into gameplay events, for example pressing the left mouse button could trigger the fire weapon event.

So far, we have introduced the three main layers in a generic manner which could be applied to any kind of game. At this point, we will proceed to describe the game in detail, explaining every single implemented feature. But before getting into specific aspects of Quake, it is important to have an overall understanding of the game engine system which we have provided through the documentation.

Clone this wiki locally