-
Notifications
You must be signed in to change notification settings - Fork 0
Application Layer
The engine provides an application abstraction class to make distinction on three different types of application software; console application is a computer program designed to be used via text-only computer interface, window application is a platform-dependent program for windowed interfaces, and game application which makes use of the engine components to build programs for entertainment.
Our area of interest is the game application, however there is a window application sample called TriangleDemoApp in the Demos project, which shows how it can be displayed a triangle on a windowed interface following a few steps. The WindowApplication class is responsible of initializing the necessary systems such as basic core system functions to create a windowed interface, the file system which manages assets used by the program in a specific directory, and the rendering system which draws a triangle using an appropriate graphic effect file. This class is also responsible for running the application in a single loop until the program is terminated, usually after an exit command is received from the core system, and finally to shut it down. We have introduced briefly the program’s cycle of life to show how to implement a simple program in the engine using a minimum number of instructions. However, we have to realize that these instructions hide complicated and laborious implementations of subsystems, which we will explain in greater detail.
The GameApplication class (Application/GameApplication.h) can be accessed through a singleton instance, which is a common design pattern for accessing major subsystems. The engine uses the singleton as a static global instance to gain direct access to any subsystem such as core system, audio system, rendering system…
class GameApplication : public Application, public EventListener
{
public:
virtual bool OnInitialize();
virtual void OnTerminate();
virtual void OnRun();
eastl::shared_ptr<ResCache> mResCache;
eastl::shared_ptr<EventManager> mEventManager;
eastl::shared_ptr<FileSystem> mFileSystem;
eastl::shared_ptr<Renderer> mRenderer;
eastl::shared_ptr<System> mSystem;
struct GameOption mOption;
protected:
void InitTime();
unsigned int UpdateTime();
virtual bool OnEvent(const Event& ev);
void OnUpdateGame(unsigned int elapsedTime);
void OnUpdateView(unsigned int timeMs, unsigned int elapsedTime);
void OnRender(unsigned int elapsedTime);
GameViewList mGameViews;
};The game initialization starts in a clean and robust way the different parts of the game. Regardless in which platform we are executing the program, the initialization process is order sensitive and involves starting the following services; The core system which handles low level operations for every platform and act as an intermediary between the hardware and the operating system. The rendering system which is used for displaying graphics in either DirectX or OpenGL API, both of them are available and can be easily changed in the project building settings. The file system which is frequently used for other subsystems to perform I/O file operations. The resource cache which is responsible for loading assets on demand. The cache requires of an initial size to have a minimum control of the available memory, and also need to register loaders of different resource types (i.e. images, audio, effects). The game option which is a data structure handled by an XML file that holds players default options. The Event Manager which is the glue that make all the different game subsystems work separately, while still be able to communicate with each other. The timer which initializes and updates a high-resolution timer to satisfy the needs of real-time applications. The time provided by the system doesn’t have sufficient resolution for measuring elapsed times, but the OS and main processors support high-resolution timer which can be accessed in a platform-specific manner. There are parts in the initialization process which needs to be handled separately as we show in the code below. Notice the use of a preprocessor symbol exposing platform-specific code to decide at compile time what part of the code will be executed.
bool GameApplication::OnInitialize()
{
#ifdef _WINDOWS_API_
mSystem = eastl::shared_ptr<System>(new WindowsSystem(mWidth, mHeight));
mSystem->SetEventListener(this);
HWND handle = reinterpret_cast<HWND>(mSystem->GetID());
#endif
}The shutdown process undoes everything in the opposite order of initialization, so we can be sure that there isn’t any memory leak due to dependencies between resources. In general, every game system should be deallocated in the reverse order of which they were created and each data structure should be traversed and freed.
void GameApplication::OnTerminate()
{
mFileSystem->RemoveAllDirectories();
delete GameLogic::mGame;
GameLogic::mGame = nullptr;
DestroyNetworkEventForwarder();
}The game is running in a single loop and it updates the game logic and game views once per tick. However modern game engines require of cooperative multitasking techniques to put multiple systems in their own discrete execution time, keeping all different systems decoupled from each other while still be able to run concurrently. During the game loop, the game needs to run the core system services to process any of the OS events and dispatch them to the game application. It also must update the timer by calculating the total live time of the application and the elapsed time in milliseconds as the time difference since the last iteration of the game loop. Both variables are important to update the game logic/views and to render the scene once per game tick.
void GameApplication::OnRun()
{
if (OnInitialize())
{
OnPreidle();
while (IsRunning())
{
if (mQuitting)mSystem->OnClose();
mSystem->OnRun();
const unsigned int elapsedTime = UpdateTime();
OnUpdateGame(elapsedTime);
OnUpdateView(Timer::GetTime(), elapsedTime);
OnRender(elapsedTime);
OnIdle();
}
OnTerminate();
}
}The Core system is a collection of routines that are used at the lowest level of any library. The engine provides a standard C/C++ library interface in a single header file Core/CoreStd.h which can be included once to support the following functions:
- Assertions are lines of error-checking code that are inserted to catch logical mistakes and violations of the programmer’s assumptions.
- Memory management provides ways to dynamically allocate portions of memory at request, and free it for reuse when no longer needed. Though we are using the default memory manager which comes with the default C-Runtime libraries, virtually every game engine implements their own custom memory allocation system to ensure high-speed allocations and deallocations and to limit the negative effects of memory fragmentation.
- Data structures and algorithms. A data structure is an arrangement of data in a computer’s memory which are manipulated by algorithms. The most common third-party library currently used is the C++ Standard Template Library (STL). However, the engine uses the EA Standard Template Library since it is an STL implementation intended for use in videogames which emphasis on high performance above all other considerations.
The engine’s core system handles the target platform, referring to the hardware on which a given operating system (OS) runs. It supports cross-platform development by abstracting their low-level functionalities and restrictions in specific implementations. The System class provides encapsulation for platform-agnostic development (see Application/System/System.h) but currently is only implemented for Windows32 platform. The system handles OS operations for windowed interfaces and for processing different system events such as, device input (mouse and keyboard), graphic user interface (GUI) or user events. Thus, we have implemented an interface which allows the system event to communicate with the game application. It is the event listener class which allows only one derived class such as the game application to be subscribed in order to receive any system event (its definition can be seen in Application/System/EventSystem.h).
void System::SetEventListener(EventListener* listener)
{
mEventListener = listener;
}
bool System::OnPostEvent(const Event& event)
{
bool absorbed = false;
if (mEventListener)
absorbed = mEventListener->OnEvent(event);
return absorbed;
}The core system also provides the following services: Input Devices, File System, Resource Cache, Event Manager, Process Manager and Threading, which can be accessed through a single header file Core/Core.h. They have been designed without using abstraction, that is, in the same code were added any platform specific code and separated by using preprocessor symbols. For the time being, it has been tested and proved on Windows platform, but the design is not ideal as it will get more complicated as we add more functionality from different platforms.
Input Devices. All human interface devices provide input to the game software inside the application layer. Once the application layer handles the raw input, it is handed off to the game view layer, usually for a human player, to interpret the raw input and translate it into a game command. The implementation of input devices is platform-dependent and it has been created interfaces to support the most familiar hardware device for a PC, that is, the keyboard and mouse. Most forms of input can be broken down into two categories: digital and analog. A digital form of input has a binary state: a button typically is digital, either it is pressed, or it isn’t without any in-between state. An analog form of input has a range of values returned by the device. One common analog device is a joystick, which will have a range of values in two dimensions. Regardless of the platform or the type of device in use, we will describe core techniques which are commonly applied for controlling the state of the input device. The polling technique is implemented in the engine and consists on reading the hardware devices once per iteration. This means explicitly querying the state of the device, either by reading hardware registers directly or via a higher software interface. A more common technique in advanced game engines is the callback or messages. In this method a human interface device only sends data to the game engine when the state of the controller changes in some way. It registers input callbacks based on the devices of interest, and when they change its state, the callback gets the control.
The BaseKeyboardHandler and BaseMouseHandler are public APIs that implement reactions to events which are sent by their hardware user interface device. These interfaces must be implemented in control classes to convert input from devices to commands that can change the game state. Control objects can attach APIs from any installed device and are guaranteed to receive device input in a standard and predictable way. The source of that control can be, for example, a player or a camera, and it only requires to implement the same interface to fit the unique needs of the control.
class BaseKeyboardHandler
{
public:
virtual bool OnKeyDown(const KeyCode c) = 0;
virtual bool OnKeyUp(const KeyCode c) = 0;
};
class BaseMouseHandler
{
public:
virtual bool OnWheelRollUp() = 0;
virtual bool OnWheelRollDown() = 0;
virtual bool OnMouseMove(const Vector2<int> &pos, const int radius) = 0;
virtual bool OnMouseButtonDown(const Vector2<int> &pos, const int radius,
const eastl::string &buttonName) = 0;
virtual bool OnMouseButtonUp(const Vector2<int> &pos, const int radius,
const eastl::string &buttonName) = 0;
};As we can see, most handler functions represent an action taken when something happens to an input device, such as when a button is pressed. In this system, it is possible to query the current state of a digital input device, for example, it is possible to grab an array of booleans that describe the state of every key on the keyboard (or any other digital input device).
File Systems controls how data is stored and retrieved. The file system manages access to both the content of files and the metadata about those files. The game engine’s file system addresses the following areas of functionality; managing file names, paths and metadata information associated with each file, opening, closing, reading individual files and scanning the contents of a directory. All of this is available in the file system header Core/IO/FileSystem.h.
mFileSystem = eastl::shared_ptr<FileSystem>(new FileSystem());
// Always check the application directory.
mFileSystem->InsertDirectory(Application::ApplicationPath);
mFileSystem->InsertDirectory(ApplicationPath + "/../../Assets/");A mount point is a directory on which the file system is mounted to organize files. When creating the mount point, a directory tree is built recursively with all the containing files and directories. Mount points enable operations over the tree directory and is frequently used to check for existing files or directories. These operations are handled by the FileList interface which generates a list of all containing files, and by the ReadFile interface which is used to open and read specific resources in the directory.
class ResourceMountPointFile : public BaseResourceFile
{
public:
ResourceMountPointFile(const eastl::wstring resFileName);
virtual bool Open();
virtual int GetRawResource(const BaseResource &r, void** buffer);
virtual int GetNumResources() const;
virtual eastl::wstring GetResourceName(unsigned int num) const;
virtual bool IsUsingDevelopmentDirectories(void) const {return false;}
virtual bool ExistFile(const eastl::wstring& filename) const;
virtual bool ExistDirectory(const eastl::wstring& dirname) const;
}Resource Cache is a data storing technique that provides the ability to access data or files at higher speed. It is designed as a centralized subsystem which manages all types of resources used by the game, the available memory space and the process of loading resources. The resource manager handles the directory resources using a mount point and provides the following basic functionality; The ability to deal with multiple types of resources, create and delete resources and, inspect and modify existing resources. The ability of instancing a resource at runtime and to maintain its reference using smart pointers.
Each resource has its own lifetime requirements, some must be loaded when the game first starts up and must stay resident in memory for the entire duration of the game, that is a global resource. Other resources have a lifetime that matches for a particular game level. There are several strategies used to manage the resource lifetime, to predict which assets will be used and to load them before they are needed. The engine implements a simple LRU strategy which discard the least recently used resources.
class BaseResource
{
public:
eastl::wstring mName;
BaseResource(const eastl::wstring &name);
};The engine needs a way to reference each resource in a uniquely manner. This resource reference enables the cache to maintain a registry of loaded resources as a dictionary, a collection of key-value pairs to match a particular resource id with its data. When a resource is requested by calling the GetHandle() function, the resource manager looks up the resource by its GUID (globally unique identifier) within the resource registry. If the resource is already loaded in the cache, it returns a smart pointer to it. Otherwise we have a cache miss, in which case the resource cache will read the resource from the file, load it into memory using an appropriated resource loader, allocate the required memory of the resource if necessary, and finally register the smart pointer in a dictionary to keep track of the new loaded resource. Whenever a resource is loaded into memory, an entry is added to the resource registry dictionary, using a generated GUID as the key. Whenever a resource is unloaded, its registry entry is removed.
As we have mentioned, resource loaders are created to handle separately different type of resources that matches certain formats or name extensions. The BaseResourceLoader provides an interface that abstracts how a specific type of resource should be loaded into memory. Depending on the requested resource format, it is the resource manager responsibility to find the proper resource handler, allocate it in memory in case that the resource is a raw buffer, and finally load it using the resource handler. Besides, it must ensure that only one copy of each unique resource exists in memory at any given time. At last, it unloads resources that are no longer needed based on the LRU strategy to make room for new ones.
eastl::shared_ptr<ResHandle>& resHandle =
ResCache::Get()->GetHandle(&BaseResource(ToWideString(texture.c_str())));
if (resHandle)
{
const eastl::shared_ptr<ImageResourceExtraData>& extra =
eastl::static_pointer_cast<ImageResourceExtraData>(resHandle->GetExtra());
textures.push_back(extra->GetImage());
}eastl::shared_ptr<ResHandle> ResCache::GetHandle(BaseResource * r)
{
eastl::shared_ptr<ResHandle> handle = Find(r);
if (handle==NULL)
handle = Load(r);
else
Update(handle);
return handle;
}The engine work with different type of resources:
- 3D Object Meshes/Object Animation Data: In order to store game geometry, the engine uses Assimp (Open Asset Import Library) to import well-known 3D model formats in a uniform manner. It is an open source library suitable as a general-purpose 3D model converter. There is a list of available file formats which the library supports in the MeshFileLoader class (Grahic/Scene/Element/Mesh/MeshFileLoader.h). Currently the mesh loader converts animated meshes for 3ds skeletal animation models, Quake md3 models, and static meshes for Quake bsp maps. 3D object and environment geometry are stored in memory as a set of 3D space points with accompanying data that describes how these points are organized into polygons and how the polygons should be rendered. Additionally, it is stored changes in position and orientation over time of animation data.
- Map/Level Data: This data takes up very little space and are usually stored in a format that is easy to work with, such as XML. The engine integrates a public domain XML parser called TinyXML.
- Texture Data: Texture storage is one of the big budget areas for games and is usually handled by third-party libraries. In our engine is handled by stb, which is a single file public library for C/C++. It supports many image formats which can be seen in ImageResourceLoader class (Grahic/Image/ ImageResourceLoader.h). Textures are stored as bitmap files using different formats to allocate a certain number of bits for red, green, blue, and alpha channels. The resource loader uses a 32-bit (8888 RGBA) which is the least compact way to store bitmaps, but retains the most information.
- Sound, Music, and Audio Effects: They usually takes most space on games, especially when the games have a strong story component. Sound formats in digital audio are commonly stored in either mono or stereo, sampled at different frequencies that the human ear can hear such as 44KHz, 22KHz, and 11KHz, and accurate to either 8 or 16 bits per sample. The audio system support WAV and OGG file formats using respectively resource handlers.
Event Manager. The Game Engine is an event-driven architecture that establishes communications between decoupled subsystems. Each subsystem is responsible for subscribing to the game events and also provide delegate functions to handle them. The game event system is the glue that holds the architecture together and manages all communications going on between the game logic and game views. If the game logic makes a change, an event is sent, and all the game views subscribed to the event will receive it. If a game view wants to send a command to the game logic, it does so through the event system. A game event is an action or occurrence generated by some authoritative system. They do encapsulate information such as the GUID (a global identification for tracking purposes), type, data which is explicitly defined depending on the event, and the time of creation. Whenever some authoritative system in the game makes a significant change, it fires off an event. Then, the game must notify to all the registered subsystems that the event has occurred by calling their delegate functions. A delegate function is basically a pointer which is called to handle the event on its own way, and is added to the event manager as a listener.
bool GameLogic::Init(void)
{
BaseEventManager::Get()->AddListener(
MakeDelegate(this, &GameLogic::RequestDestroyActorDelegate),
EventDataRequestDestroyActor::skEventType);
}
void GameLogic::RequestDestroyActorDelegate(BaseEventDataPtr pEventData)
{
eastl::shared_ptr<EventDataRequestDestroyActor> pCastEventData =
eastl::static_pointer_cast<EventDataRequestDestroyActor>(pEventData);
DestroyActor(pCastEventData->GetActorId());
}The event manager is a global singleton which processes all kind of events and calls the registered delegate methods in two different ways: The TriggerEvent function fires an event and have all listeners respond to it immediately, and the QueueEvent function queues the event to be processed during the event manager update tick. The update process takes all the queued events and calls their registered delegate methods once per loop. There is a double processing queue in which one is used for events being actively processed and the other for new events. It is separated that way because if we only had a single queue it might happen that it never runs out of processing events.
//We need to trigger a synchronous event to ensure that any systems
//responding to this event can still access a valid actor if need to be.
eastl::shared_ptr<EventDataDestroyActor> pEvent(new EventDataDestroyActor(actorId));
BaseEventManager::Get()->TriggerEvent(pEvent);
// fire an event letting everyone else know that we created a new actor
eastl::shared_ptr<EventDataNewActor> pNewActorEvent(new EventDataNewActor(pActor->GetId()));
BaseEventManager::Get()->QueueEvent(pNewActorEvent);Process Manager is a game subsystem that handles all the processes during the game lifetime and updates them every game tick. It implements a simple but powerful technique that creates a chain of cooperative processes in which the processes that have dependencies with others can be attached, that is, one would wait for the other to complete before starting. A process is the instance of a computer program that is being executed by one or many threads. It may occupy a variety of states; When a process is first created, it occupies the uninitialized state. In this state, the process awaits admission to the running state after calling the init method. After passing to the running state, instructions will be executed in the update method by a single thread. Once the process has terminated, it calls the appropriate exit function depending on if it succeeded, failed or aborted and finally the process is removed from the process table.
eastl::shared_ptr<SoundProcess>sound(new SoundProcess(rh,mVolume,mLooping));
processManager->AttachProcess(sound);
// fade process affects to the sound process
if (mFadeTime > 0)
{
eastl::shared_ptr<FadeProcess>
fadeProc(new FadeProcess(sound, mFadeTime, mVolume));
processManager->AttachProcess(fadeProc);
}Threading. Concurrent programming is a technique for creating software that can run in multiple, independent pieces simultaneously. In the engine, a real-time concurrent process can be created and managed from both the OS and the Process Manager. The communication between real-time processes and the game is handled by the Event Manager.
Wiki
Game Engine
Graphics
Game Engine Showcase