![]() |
SAGE
A Simple Academic Game Engine
|
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?"
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.
The three algorithm design patterns used by SAGE are entity, manager, and component.
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.The four data design patterns used by SAGE are descriptor, monostate, accessor, and aggregator.
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.
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 Sage::CKeyboard public member functions include the following. See Sage::CKeyboard to see how other keyboard tests.
Sage::CKeyboard::GetState, which must be called once per animation frame to poll the current keyboard state;Sage::CKeyboard::TriggerDown, which returns true if a key described by a WPARAM was up and went down in the current animation frame.Sage::CKeyboard::Down, which returns true if a key described by a WPARAM is currently down.Important Sage::CMouse public member functions include the following. See Sage::CMouse to see how to get other mouse data.
Sage::CMouse::GetState, which must be called once per animation frame to poll the current mouse state;Sage::CMouse::GetMove, which returns a vector describing the mouse motion since the last frame.Important Sage::CController public member functions include the following. See Sage::CController to see how to test other controller buttons and controls.
Sage::CController::GetState, which must be called once per animation frame to poll the current controller state;Sage::CController::IsConnected, which returns true if a controller is connected;Sage::CController::Vibrate, which sets the left and right rumble; andSage::CController::GetButtonAToggle, which returns true if button A on the controller was up and went down in the current animation frame.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.
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:
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.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.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.
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.
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.
CSpriteDesc2D. See Section 3.6 for more details.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 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 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).
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).
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 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.
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.
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.
C for class, for example, CRandom and CSprite.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.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.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.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)./// for a block comment and //< for an in-line comment.Get, for example, Sage::CCamera::GetYaw returns Sage::CCamera::m_fYaw.Set, for example, Sage::CCamera::SetYaw sets Sage::CCamera::m_fYaw and recomputes the camera's orientation matrix and view frustum.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.