SAGE
A Simple Academic Game Engine
Loading...
Searching...
No Matches

1. Introduction

SAGE, the Simple Academic Game Engine, is a simple game engine for instructional applications. It is called simple because it consists of approximately 10,500 lines of open-source C++ code (exclusive of blank lines and block comments), whereas commercial game engines typically have millions of lines of code. It uses Microsoft DirectX 12 for graphics, DirectXTK 12 for graphics helpers and audio support, and TinyXML2 for reading XML files. It has been used by students in Ian Parberry's game programming classes to code 2D, 2.5D, and 3D games using Visual C++ under 64-bit Windows 11. It must be emphasized that SAGE is an instructional game engine. It is not intended for use outside the classroom and it is definitely not intended for commercial use. SAGE is currently a private, members-only repository. If you are an instructor and you wish to test it out for use in your game programming class, please email Ian Parberry.

The remainder of this page is divided into four sections. Section 2 explains the design patterns used in SAGE, Section 3 goes over some of the more important SAGE classes, Section 4 covers some of the conventions used in SAGE code, and Section 5 addresses the question "what next?"

2. Design Patterns

A design pattern is a general reusable solution to a commonly occurring problem within a given context in software design. It is not a finished design that can be transformed directly into source or machine code. Design patterns began with the so-called gang of four consisting of Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides. In 1994 they published a book called Design Patterns: Elements of Reusable Object-Oriented Software. SAGE uses what are called implementation strategy design patterns, which relate to source code organization. There are two types of implementation strategy design pattern: algorithm and data. SAGE uses three algorithm design patterns and four data design patterns.

2.1 Algorithm Design Patterns

The three algorithm design patterns used by SAGE are entity, manager, and component.

  1. An entity is a simple class that is concerned only with its own state and actions. Entities typically rely on components to perform tasks for them. Access to entities is typically through a manager.
  2. A manager contains a collection of entities. Managers are responsible for entity creation and destruction, and delegation of tasks to all entities for which the manager has responsibility. A manager typically stores its entities in a private container such as std::vector or std::list. The manager's entities typically list the manager as a friend so that the manager has direct access to the entity's data without the need for cumbersome access functions.
  3. A component is a class that provides services typically provided by the operating system such as graphics and sound.

2.2 Data Design Patterns

The four data design patterns used by SAGE are descriptor, monostate, accessor, and aggregator.

  1. A descriptor encapsulates information in the form of public member variables. They are typically used to replace a long parameter list with a single parameter.
  2. A monostate is a class that encapsulates a single instance of data to be shared between classes using class variables (static member variables). A monostate may be instantiated in the code, but it more generally has classes derived from it. Monostates are used to provide data to multiple entities, managers, components, without the need for global variables, long parameter lists, or local copies of the data. The Monostate Design Pattern is also called the Borg Idiom in the Python community.
  3. An accessor is a monostate consisting only of protected member variables.
  4. An aggregator is an accessor consisting of static protected pointers to components.

3. SAGE Classes

The following classes are provided by SAGE in namespace Sage. Do not instantiate them in your game unless instructed otherwise. Some have already been instantiated for you. Others have been designed to be derived from. Do not modify SAGE code in order to make your game run. Instead, you should derive your own class from the relevant SAGE class and add your functionality in overloaded member functions.

3.1 Input Devices

Table 1. SAGE input device classes.
SAGE Class
Design Pattern
Description
Usage
Sage::CController Component XBox controller handler Instantiated by SAGE
Sage::CKeyboard Component Keyboard handler Instantiated by SAGE
Sage::CMouse Component Mouse position handler Instantiated by SAGE

SAGE provides device handlers for an X-Box controller, the keyboard, and the mouse (see Table 1). For efficiency, these handlers are polled, and are not dependent on the Windows message passing system. All three components have a GetState function that should be called once per frame to capture the current state of the device. This state snapshot can then be examined by calling device-specific reader functions. One thing that you may not be expecting is that Sage::CMouse handles only the position of the mouse. Mouse buttons are handled by Sage::CKeyboard. This is a foible of the way Windows handles its devices.

Important Member Functions

Important Sage::CKeyboard public member functions include the following. See Sage::CKeyboard to see how other keyboard tests.

Important Sage::CMouse public member functions include the following. See Sage::CMouse to see how to get other mouse data.

Important Sage::CController public member functions include the following. See Sage::CController to see how to test other controller buttons and controls.

3.2 Time

Table 2. SAGE timer classes.
SAGE Class
Design Pattern
Description
Usage
Sage::CEventTimer Component Timer for regularly scheduled events Instantiated in your game
Sage::CTimer Component High-accuracy timer Instantiated by SAGE

Your game must be able to measure your computers frame rate and frame interval, which is the amount of time between frames. This will typically vary between 30 frames per second (around 33 milliseconds per frame) and over 300 frame per second (around 3 milliseconds per frame). SAGE provides two timer classes (see Table 2). Sage::CTimer is a high-accuracy frame timer. It has a function Sage::CTimer::Tick that must be called once per frame to capture a snapshot of the time. Then, for example, the current frame time in seconds can be obtained as a floating point value by calling Sage::CTimer::GetFrameTime.

Sage::CEventTimer lets you handle regularly scheduled events such as flashing lights or the reloading of a weapon. The events can be scheduled with an optional amount of variability. To use it, instantiate one instance of Sage::CEventTimer for each event, providing the delay and optional amount of variability as parameters to the constructor. Thereafter, call the event timer's function Sage::CEventTimer::Triggered once per frame. This function will return true and reset the event if the delay time plus the variability time are greater than the elapsed time since the event was last triggered (or the time it was created, if it has never been triggered). Otherwise it will return false.

3.3 Sound

Table 3. SAGE sound classes.
SAGE Class
Design Pattern
Description
Sage::CSound Component Sound player
Sage::CSoundDesc Descriptor Sound descriptor
Sage::CPlayableSoundDesc Descriptor Playable sound descriptor

SAGE provides a sound player Sage::CSound that will let you play and mix multiple copies of multiple sounds in 3D audio at varying pitches and volumes. If all you wish to do is play the sound at default pitch and volume, or perhaps specify only the pitch and/or the volume, Sage::CSound has specific member functions for you. Anything more complicated needs you provide an instance of a sound descriptor Sage::CSoundDesc as a parameter. Sage::CPlayableSoundDesc is intended for internal use in SAGE only, which means that you should never need to use it in your game. Loading sounds from wav files will be covered in Section 3.7.

There are three basic functions for playing a sound:

  1. Sage::CSound::Play takes a sound index (usually a member of an enumerated type) and a position at which to play the sound (which defaults to the center of the world), and optionally the volume, or volume and pitch.
  2. Sage::CSound::Loop takes a sound index and optionally a position at which to play the sound, and it plays the sound repeatedly until some version of the Sage::CSound::Stop function is called.
  3. Sage::CSound::Vary takes a sound index, a position at which to play the sound, a coefficient of variability (which defaults to 0.25f) and plays the sound at a different volume and pitch each time it is called, varying by the coefficient of variability. A base volume and pitch may also be provided.

See Sage::CSound for more functions.

3.4 Game Objects

Table 4. SAGE game object classes.
SAGE Class
Design Pattern
Description
Sage::CObject Entity Basic game object
Sage::CObjectManager Manager Basic object manager

A game object is the abstract representation of a thing in your game, such as a character, a wall, or a missile. SAGE best practice is to derive your own game object class CObject from Sage::CObject, which provides base functionality to game objects. Sage::CObject provides stubs for the move and draw virtual functions Sage::CObject::Move and Sage::CObject::Draw, which are stubs that you should definitely override in your implementation of CObject.

It is also SAGE best practice to manage your objects with an object manager CObjectManager derived from Sage::CObjectManager, which provides base functionality to manage game objects. Sage::CObjectManager maintains an object list containing pointers to game objects, which are assumed to have been derived from Sage::CObject. It has a virtual function Sage::CObjectManager::Draw which calls the virtual Draw function of every object in the object list, and a virtual function Sage::CObjectManager::Move which calls the virtual Move function of every object in the object list, then performs collision detection and response between pairs of objects in the object list.

When an instance of your game object class CObject derived from Sage::CObject dies in your game, you must call its inherited function Sage::CObject::MarkForDeletion. At the end of Sage::CObjectManager::Move, after collision detection and response, all objects in the object list that have been marked for deletion are removed from the object list and deleted. Your CObjectManager::Move function which over-rides Sage::CObjectManager::Move can either call Sage::CObjectManager::Move or perform its own object motion and collision detection and response, and then call Sage::CObjectManager::DeleteMarkedObjects.

3.5 Rendering

Table 5. SAGE rendering classes.
SAGE Class
Design Pattern
Description
Sage::CCamera Component Camera
Sage::CCameraCommon Accessor Camera common variables
Sage::CRenderer3D Component 3d renderer
Sage::CScreenshotFlag Accessor Screenshot flag
Sage::CSpriteRenderer Component Sprite renderer
Sage::CTextureDesc Descriptor Texture descriptor

SAGE provides two rendering classes, Sage::CSpriteRenderer for 2D and 2.5D sprite games, and Sage::CRenderer3D. Control of the camera is provided by accessing the variables in Sage::CCameraCommon. Sage::CCamera is intended only for internal use in SAGE and should not be instantiated in your game. The camera can be accessed through the renderer using, for example, Sage::CSpriteRenderer::GetCameraPos and Sage::CSpriteRenderer::SetCameraPos. Sage::CScreenshotFlag, which enables a screen shot to be saved to a file in the current folder when the player presses the P key, is also intended only for internal use in SAGE and should not be instantiated in your game. The texture descriptor Sage::CTextureDesc is intended for use in 3D games only and should not be instantiated in a 2D or 2.5D sprite game.

Sage::CSpriteRenderer has several versions of Sage::CSpriteRenderer::Draw to draw a sprite, the simplest of which takes as parameters a sprite index, a position, and (optionally) an orientation angle in radians. If more control is desired, such as scaling and tinting, construct an instance of Sage::CSpriteDesc2D and pass a pointer to it to Sage::CSpriteRenderer::Draw instead. Since neither DirectX12 nor the DirectX12 Toolkit provide functions for drawing lines, rectangles, or circles, these functions are provided by Sage::CSpriteRenderer, although frankly they are a kluge.

The Sage::CSpriteRenderer constructor allows it to be used in three modes, specified by a parameter of the enumerated type Sage::eSpriteMode. Sage::eSpriteMode::Batched2D renders sprites using SpriteBatch from the DirectX12 Toolkit. Sage::eSpriteMode::Unbatched2D renders sprites natively. Both of these modes render using painter's algorithm, that is, sprites are drawn on top of each other in the order in which they are rendered. This means that the background must be drawn first, and that anything that must be drawn on top of anything, such as text or instrumentation, must be drawn last.

Sage::eSpriteMode::Unbatched3D is for use in 3D games (experimental at the moment) or in 2D or 2.5D games in which you want or need the capability to draw the sprites in any order, with the depth specified by the third dimension of a 3D vector. In this case, instead of calling Sage::CSpriteRenderer::Draw for each sprite, collect the 3D sprite descriptors in an instance of std::vector<Sage::CSpriteDesc3D> and pass that as a parameter to Sage::CSpriteRenderer::Draw which will sort them by depth (in Z-order) before rendering them.

For 2D games in particular, Sage::CSpriteRenderer includes multiple ways of drawing 2D sprites using Sage::CSpriteRenderer::Draw by providing the following parameters.

  1. The sprite index (usually a member of an enumerated type), a position, and optionally the orientation angle (in radians, measured counter-clockwise from the positive X-axis), which defaults to zero.
  2. A pointer to an instance of the 2D sprite descriptor CSpriteDesc2D. See Section 3.6 for more details.

3.6 Sprites

Table 6. SAGE sprite classes.
SAGE Class
Design Pattern
Description
Sage::CSprite Component Sprite
Sage::CSpriteDesc Descriptor Base sprite descriptor
Sage::CSpriteDesc2D Descriptor 2D sprite descriptor
Sage::CSpriteDesc3D Descriptor 3D sprite descriptor

Sage::CSprite is the abstract representation of a multi-frame sprite. It is intended only for internal use in SAGE and should not be instantiated in your game. Sage::CSpriteRenderer maintains a list of pointers to instances of Sage::CSprite and will take care of their management without any need for additional code in your game. The 2D sprite descriptor Sage::CSpriteDesc2D describes the properties of a two-dimensional sprite in a 2D or 2.5D game, and Sage::CSpriteDesc3D does the same for a three-dimensional sprite in a 3D game.

Sage::CSpriteDesc describes the properties common to 2D and 3D sprites, and Sage::CSpriteDesc2D and Sage::CSpriteDesc3D are derived from it. It is intended only for internal use in SAGE and should not be instantiated in your game.

Important Sprite Properties

Important sprite properties in Sage::CSprite include an array Sage::CSprite::m_pRect of bounding rectangles for a multi-frame sprite contained in a sprite sheet (a single image containing multiple frames) and useful reader functions such as Sage::CSprite::GetWidth and Sage::CSprite::GetHeight to get the width and height (respectively) of the sprite in pixels after it has been loaded.

Important Sprite Descriptor Properties

Important properties of Sage::CSpriteDesc, and therefore Sage::CSpriteDesc2D and Sage::CSpriteDesc3D include the sprite index, current animation frame, scale along the local X-axis of the sprite, scale along the local Y-axis of the sprite, roll angle (orientation), tint (which lets you color the non-black pixels of the sprite), and alpha value (amount of transparency).

3.7 Loading Media

Table 7. SAGE media loading classes.
SAGE Class
Design Pattern
Description
Sage::CLoadingThread Component Media loading thread
Sage::CMediaDesc Descriptor Media descriptor
Sage::CMediaList Component Media descriptor list

SAGE provides the ability to read media, that is, sprites and sounds sequentially or in parallel threads. The problem with the former is that your game window will freeze up while the media loads from disk, which can be for several seconds depending on the size of the media and the speed of your computers HD or SSD. While multi-threaded loading is preferable, sequential loading is provided for debugging and testing purposes.

In either case, SAGE makes use of a media descriptor class Sage::CMediaDesc which consists of a an index and a string. The index is used to store the piece of media in an array, and the string is used to search for an XML tag that specifies the media file from which it is to be read. To load sounds, insert the corresponding media descriptors into an instance of Sage::CMediaList and pass that as a parameter to Sage::CSound::LoadMT for asynchronous (i.e. multi-threaded) loading or to Sage::CSound::Load for sequential loading (see Section 3.3 for more information about Sage::CSound). To load sprites, insert the corresponding media descriptors into an instance of Sage::CMediaList and pass that as a parameter to Sage::CSpriteRenderer::LoadMT for asynchronous (i.e. multi-threaded) loading or to Sage::CSpriteRenderer::Load for sequential loading (see Section 3.6 for more information about Sage::CSpriteRenderer).

3.8 Particle Engine

Table 8. SAGE particle engine classes.
SAGE Class
Design Pattern
Description
Sage::CParticleDesc2D Descriptor 2D particle descriptor
Sage::CParticleDesc3D Descriptor 3D particle descriptor
Sage::CParticleEngine2D Component 2D particle engine
Sage::CParticleEngine3D Component 3D particle engine

SAGE provides a 2D particle engine Sage::CParticleEngine2D and a 3D particle engine Sage::CParticleEngine3D (under development), both derived from Sage::CParticleEngine. The latter is intended only for internal use in SAGE and should not be instantiated in your game. Sage::CParticle represents a particle and should also not be instantiated in your game. To create a particle in a 2D game, fill in a 2D particle descriptor Sage::CParticleDesc2D with the particle properties (including fade in, fade out, scale in, scale out) and pass it as a parameter to Sage::CParticleEngine2D::Create. Function Sage::CParticleEngine2D::Step must be called once per frame to animate 2D particles. To create a particle in a 3D game, fill in a 3D particle descriptor Sage::CParticleDesc3D with the particle properties and pass it as a parameter to Sage::CParticleEngine3D::Create. Function Sage::CParticleEngine3D::Step must be called once per frame to animate 3D particles.

Important Particle Descriptor Properties

Important properties of Sage::CParticleDesc, and therefore Sage::CParticleDesc2D and Sage::CParticleDesc3D include the particle's velocity, acceleration, friction, rotational velocity, lifetime in seconds, scale-in and scale-out fractions (as a fraction of lifetime), and fade-in and fade-out fractions (also as a fraction of lifetime). For example, with lifetime equal to 1.5f seconds, scale-in equal to 0.2f and scale-out equal to 0.5f, the particle grows from zero to full size in 0.2f*1.5f = 0.3f seconds, remains at full size for 0.45 seconds, and shrinks to zero in 1.5f*0.5f = 0.75f seconds. Similarly, with lifetime equal to 1.5f seconds, fade-in equal to 0.2f and fades-out equal to 0.5f, the particle fades in from fully transparent to fully opaque in 0.2f*1.5f = 0.3f seconds, remains at full opaqueness for 0.45 seconds, and fades out to full transparency in 1.5f*0.5f = 0.75f seconds. Both scaling and transparency may be used together.

3.9 Windows and Settings

Table 9. SAGE windows and settings classes.
SAGE Class
Design Pattern
Description
Sage::CSettings Accessor Game settings
Sage::CSettingsManager Manager Game settings manager
Sage::CWindow Component Window handler
Sage::CWindowDesc Descriptor Window descriptor

Sage::CSettings contains game settings that are read from an XML file by Sage::CSettingsManager. Any of your game classes that require these settings should be derived from Sage::CSettings. The window descriptor Sage::CWindowDesc contains some important Windows properties such as the window and instance handles for your game. Any of your game classes that require these properties should be derived from Sage::CWindowDesc. Sage::CWindow handles Windows specific operations required to manage your game. It should be instantiated in your main.cpp.

3.10 Other

Table 10. Other SAGE classes.
SAGE Class
Design Pattern
Description
Sage::CComponent Aggregator Component pointers
Sage::CRandom Component Pseudo-random number generator

Sage::CRandom is a pseudo-random number generator that can be used to generate pseudo-random integers, floating point values, and colors. It can be set to generate a different sequence of pseudo-random values every time your game is played, or, for debugging purposes, to generate the same sequence of pseudo-random values every time your game is played.

Sage::CComponent contains pointers to important components that are instantiated for you by SAGE. These include a sound player, a timer, a pseudo-random number generator, and handlers for a keyboard, mouse, and XBox controller. Any of your game classes that require these components should be derived from Sage::CComponent.

4. Some Sage Code Conventions

  1. SAGE class names always start with an upper-case C for class, for example, CRandom and CSprite.
  2. SAGE code is in namespace Sage, so you will have to access tham as, for example, Sage::CRandom and Sage::CSprite. This will help you to distinguish the SAGE code from the non-SAGE code in the examples.
  3. Generally, a class Sage::XXX will have its declaration in a header file called SageXXX.h and its implementation in a code file called SageXXX.cpp. There are some exceptions, for example, templated code.
  4. Class member variables have names that start with a lower-case m for member followed by the underline character, like this: m_. The next character or two will be a lower-case string indicative of the variable type, followed by the first letter of the variable's descriptive name, which will always be upper-case. For example, m_fFriction is a floating point variable for friction, m_nNumFrames is an integer variable for the number of frames (which could be int, UINT, or size_t), and m_pTimer is a pointer to a timer.
  5. In 2D SAGE games the Origin of the coordinate space it at the lower-leftmost point of the window. The positive X-axis goes to the right, the positive Y-axis does up, and the positive Z-axis does into the screen.
  6. The orientation of a sprite in SAGE, that is, the angle that it is to be rotated, is called m_fRoll, because in 3D games we have three orientation angles, yaw (rotation about the Y-axis), pitch (rotation about the X-axis) and roll (rotation about the Z-axis).
  7. SAGE is documented using Doxygen which allows us to generate html documentation from programmer-readable comments. Doxygen comments start with /// for a block comment and //< for an in-line comment.
  8. Public member functions that simply return the value stored in a private or protected member variables start with Get, for example,
    Sage::CCamera::GetYaw returns Sage::CCamera::m_fYaw.
  9. Public member functions that modify the values stored in private or protected member variables start with Set, for example,
    Sage::CCamera::SetYaw sets Sage::CCamera::m_fYaw and recomputes the camera's orientation matrix and view frustum.

5. What Next?

If SAGE is not already installed on your computer, please read and follow the installation instructions. Finally, the sage repository comes with a simple game called the Blank Game.