Game Engine Layout

Started by
16 comments, last by socky 10 years, 9 months ago

I'm finishing up my first engine layout for OpenGL, and this is what I got so far:

//This has info that's needed to draw images in the shader

[Material]

.char[3] diffuse, specular, ambient; //rgb that will be used in the shader

.char* textureData; //the rgba data from an image

.char* normalData; //the normal map from an image

.int textureID; //the textureID that will be filled when it's loaded in the GPU

//A mesh is the data to draw an object in screen

[Mesh]

.float* vertexes;

.float* normals;

.float* uvs;

.int* vertexIndexes;

.Material material; //A mesh can only have a single material

.MeshType meshType; //this will be an enum that will specify which shader to use, if it's glass, water, or something that need some specific shader effect

.int vboID; //the ID that will be filled when it's loaded in the GPU

//This resource object will contain all data about a single object, such as a "House"

[ResourceObject]

.string name; //a name to identify it, not sure if it'll be needed

.Mesh* meshes; //the resource object can have multiple meshes, such as a "door", "window", "walls", etc

//This is the class that will be responsible for loading and unloading objects from disk in the RAM

[ResourceEngine]

.list<ResourceObject> listObjects; //a list of all objects that are currently loaded

.list<Material> listMaterials; //a list of all materials that are currently loaded

.LoadObject(int count, ...); //load X resources in the list, will use "string" to identify them

.LoadMaterial(int count, ...); //same as above, but for materials

.UnloadObject(int objectID); //unload an object from the list

.UnloadMaterial(int materialID); //unload a material from the list

//This is the class responsible for loading the objects in the GPU and manage shaders

[GraphicEngine]

.list<Shader> listShaders; //a list of shaders, not sure if this will be needed

.LoadObject(ResourceObject* object); //load an object in the GPU, will fill the "vboID"

.LoadMaterial(Material* material); //load an material in the GPU, will fill the "textureID"

.Unload(int vboID); //unload a VBO

.Unload(int textureID); //unload a texture

.CompileShaders(string shaderCodes); //this will compile the shader, and fill the list and specify an ID to it, which an object will be able to call it if needed

//This class has functions specific to each OS that I'll be using, such as the currentTime or read/write files, and all other classes will access it

[OSFunctions]

.GetCurrentTime(); //returns the float of the currentTime

//Other functions that I'll be filling as the game requires

//This class will keep track of the game's ACTIONs and playerInput

[InputEngine]

.keyboard[256]; //For each key in the keyboard, has a bool to check if it's pressed or not

.mouse[8]; //Same for mouse, and mousewheel

//I'm thinking in moving these two to OS, since mobiles have different controls and it'll depend on the target platform the game will be

.list<Actions> listActions; //A list of all actions in the game, loaded from a resourceFile as strings such as "PLAYER_MOVERIGHT"

.list<Actions> playerInput; //A list of all actions input by the player, with timestamps

//This is an actual on-screen object, it uses a resourceObject data and there can be multiple of them on screen

[GameObject]

.ResourceObject* resourceObject; //the data of this object

.float positionX, positionY, positionZ;

.float directionX, directionY, directionZ;

.float speedX, speedY, speedZ;

//other variables needed depending on the game

//This is the main class where most of the code will be

[GameEngine]

.SceneGraph sceneGraph; //basically a list of all GameObjects currently on the level

.Initialize(); //empty function that's called on startup, will be filled differently for each game

.Update(); //empty function that's called every frame

.Draw(); //function that gets a list of objects that "can be seen" from the sceneGraph, and draws them

So, my game would be something like this:


int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    OSFunctions.Initialize(); //this will start the window based on which OS I'm in
    GameEngine gameEngine;
    gameEngine.Initialize(); //this function will load all stuff needed and set the default values for everything

    while(game.isRunning == true)
    {
        if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
        gameEngine.update(); //the main update function
        gameEngine.draw(); //and draw
    }
}
//The message proc function here with all input calling the InputEngine functions to update each key's states

And my GameEngine's Update() would have something like this:


void GameEngine::Update()
{
    switch(gameState)
    {
        case GameState::Startup_Loading:
        {
            //Loading from the disk to the resourceList
            ResourceEngine.Load(2, "logo", "title");

            //Now filling the objects' data before adding to the sceneGraph
            GameObject* gameLogo = new GameObject();
            gameLogo.resourceObject = listObjects[0];
            gameLogo.positionX = 10; //etc

            sceneGraph.add(gameLogo);
            
            gameState = GameState::Startup;
            break;
        }
        case GameState::Startup:
        {
            //displays the logo, counts a time, then changes the gameState to something else
            gameState = GameState::Title_Screen;
            break;
        }
        case GameState::MainGame_Loading:
        {
            resourceList.Clear();
            ResourceEngine.Load(1, "playerCharacter");            

            GameObject* playerCharacter = new GameObject();
            playerCharacter.resourceObject = listObjects[0];
            //fill positions and such
            
            gameState = GameState::MainGame;
            break;
        }
        case GameState::MainGame:
        {
            for(int i = 0; i < sceneGraph.GetVisibleObjectsCount; i++)
            {
                //each object has it's own update, which will check for input and change it's own state and manage it's own animations
                sceneGraph.GetVisibleObject(i).Update();
            }
            break;
        }
    }
}

This is how I think it'll be working, and I'll be coding lots of GameObjects for each game, get enums for flags and change them according to events in game, among other stuff.

This is my first engine/game, and I'd like to ask for opinions, advices and comments of how this can be done better, or if there's room for optimization, if there's anything wrong, etc...

I have little to no experience with engines, so any input is highly appreciated!

Advertisement

I have a question: Is this supposed to be used in one game? If so this engine seems fine, minus having no alpha which might be on purpose.

However if you then tried to adapt it to some other game the GameEngine class leaves much to be desired especially if the update function gets too large with too many different states. You can fix that with a slightly more advanced state machine to look after the states(Which would then be a separate class).

I was writing up a StateMachine example class when I remembered about the crazy number of gdnet articles, check these out:

http://www.gamedev.net/blog/812/entry-2256166-state-machines-in-games-%E2%80%93-part-1/

http://www.gamedev.net/blog/812/entry-2256167-state-machines-in-games-%E2%80%93-part-2/

http://www.gamedev.net/blog/812/entry-2256173-state-machines-in-games-%E2%80%93-part-3/

http://www.gamedev.net/blog/812/entry-2256179-state-machines-in-games-%E2%80%93-part-4/

http://www.gamedev.net/blog/812/entry-2256190-state-machines-in-games-%E2%80%93-part-5/

http://www.gamedev.net/page/resources/_/technical/game-programming/state-machines-in-games-r2982

Although, again your engine is very nice and if it works for your purpose don't over-engineer it.

Engineering Manager at Deloitte Australia

//This has info that's needed to draw images in the shader
[Material]
.char[3] diffuse, specular, ambient; //rgb that will be used in the shader
.char* textureData; //the rgba data from an image
.char* normalData; //the normal map from an image
.int textureID; //the textureID that will be filled when it's loaded in the GPU

You would be better off just using a texture pointer directly, or better still a shared pointer to a texture. Textures may have ID’s (and they very well should, to prevent redundant texture loads), but using that ID rather than a direct pointer wastes precious cycles, as you then need to find that texture via binary searches, hash tables, etc.
Additionally, colors should be floats, not char’s, and especially not signed char’s. They should also have 4 components, as that is what you will get inside the shader anyway. As far as the GPU is concerned, float[3] and float[4] consume the same space.


//A mesh is the data to draw an object in screen
[Mesh]
.float* vertexes;
.float* normals;
.float* uvs;
.int* vertexIndexes;
.Material material; //A mesh can only have a single material
.MeshType meshType; //this will be an enum that will specify which shader to use, if it's glass, water, or something that need some specific shader effect
.int vboID; //the ID that will be filled when it's loaded in the GPU

Separate the mesh from the low-level components that are used to render it.
A low-level class such as “CVertexBuffer” should have the VBO ID and the vertices, normals, UV’s, etc.
Another low-level class such as “CIndexBuffer” obviously contains the index data.

A mesh is a higher-level class that brings these low-level classes together. The mesh itself should never touch the vertex buffer’s internal data except on loading, and if you do want to mess with the vertex data it should be done through an interface provided by CVertexBuffer.
CVertexBuffer should also not be hard-coded to have normals and UV’s. The only thing a vertex buffer is guaranteed to have is vertex data. I would give you a pass since this is your first time, but think ahead to when you later want to add tangents and bitangents. This is fairly relevant since you already mentioned having normal-map textures. But not every object will have normal maps, so you clearly need to think carefully about a more dynamic vertex-buffer system.
And again, all of that dynamic mess will be entirely handled by the CVertexBuffer class. Its whole purpose is to make it easy for the rest of your engine to use vertex buffers. That is not job of the mesh, so don’t mix responsibilities.

//This resource object will contain all data about a single object, such as a "House"
[ResourceObject]
.string name; //a name to identify it, not sure if it'll be needed
.Mesh* meshes; //the resource object can have multiple meshes, such as a "door", "window", "walls", etc

You are somewhat on the right track now, but I wouldn’t call it a “ResourceObject”. That is a very sweeping term that could encompass any form of resource, including models, terrain, textures, BGM, etc.
What you have just described is only a model. A model is a collection of meshes, which is exactly what you have described here.

This hints to the idea that you believe every 3D thing you see on the screen is a mesh or a collection of meshes. They aren’t. Terrain, for example, will use a heightmap of some sort with an LOD method of your choice (I recommend GeoClipmaps), which makes its rendering pipeline almost entirely different from how simple things such as doors and walls are drawn. For that matter, interiors of buildings are also drawn entirely differently so that they can take advantage of PVS’s etc.

I am not saying you need to get so complex with your own engine, especially on your first go, but I want to make sure you understand that in reality, very little of what you see on the screen of any modern 3D game is really just a basic mesh. Those are usually just characters, ammunition/weapons, and various other props. Other things, such as terrain, vegetation, buildings (especially interiors), skyboxes, etc., are all made efficient by utilizing rendering pipelines unique to themselves.


//This is the class that will be responsible for loading and unloading objects from disk in the RAM
[ResourceEngine]
.list<ResourceObject> listObjects; //a list of all objects that are currently loaded
.list<Material> listMaterials; //a list of all materials that are currently loaded
.LoadObject(int count, ...); //load X resources in the list, will use "string" to identify them
.LoadMaterial(int count, ...); //same as above, but for materials
.UnloadObject(int objectID); //unload an object from the list
.UnloadMaterial(int materialID); //unload a material from the list

I wouldn’t bring all of these things together into one mega-class. Again: Single Responsibility Principle.
Expanding on the concept I introduced on how diverse the types of objects in your game world ultimately could be, imaging trying to load and construct all of those objects in one place.
It will quickly become a major mess.

Firstly you need to familiarize yourself with Single Responsibility Principle.
In this case, let’s look at the potential diversity of the types of things that could go into your game world and pick separation points.
A model is so different from terrain that models and terrain could each be their own entirely separate modules (or libraries).
Of course they would both build on top of a common foundation (the graphics module/library, where CVertexBuffer and CShader live).
So let’s say that models are entirely their own module. In my engine it builds as its own .lib file, but in keeping things simple for your first time, let’s just say it’s a folder in your source tree. Anything under that folder has one job and one job only: Models.

When you break it down like this it starts to make sense that models should actually just load themselves. Models know what models are. They know what their binary format is. There is no reason to push the responsibility of the loading process onto any other module or class.

If you are worried that the model module is stepping out of its bounds by having direct access to disk, don’t give it direct access to disk. Give it a “stream”.
Now your model module/library is still just doing its job. It doesn’t know anything about hard drives, networks, or otherwise, yet via the abstract interface provided by streams it can actually load its own model data through both of them.

Because of the raw number of resource types, a “ResourceEngine” doesn’t really make sense, but it could find a nest in your design if you do it right. Just make sure you understand that a “resource engine” also has only one responsibility, and it is not the actual loading process for each resource. If anything, it only provides a high-level interface for loading, but passes off the actual loading process to each object it creates.
For example, if you ask it for a model, it will create a new model, prepare a file stream for the model, and let the model use that stream to load itself. It then destroys the model and returns NULL if loading failed, or it returns a shared pointer to that model.

Don’t use ID’s for everything, as, as I already mentioned, it wastes precious cycles.


//This is the class responsible for loading the objects in the GPU and manage shaders
[GraphicEngine]
.list<Shader> listShaders; //a list of shaders, not sure if this will be needed
.LoadObject(ResourceObject* object); //load an object in the GPU, will fill the "vboID"
.LoadMaterial(Material* material); //load an material in the GPU, will fill the "textureID"
.Unload(int vboID); //unload a VBO
.Unload(int textureID); //unload a texture
.CompileShaders(string shaderCodes); //this will compile the shader, and fill the list and specify an ID to it, which an object will be able to call it if needed

The graphics engine should provide low-level classes such as CVertexBuffer, CTexture2D, CTextureCube, CIndexBuffer, CShader, etc.
And when I say “provide” I don’t mean that you request these things from the graphics engine, I mean that the graphics module is where these things all live and can be taken by any class at any time. That’s how the model and terrain modules can be totally separate from each other while still sharing a common graphics module.

How high-level a graphics engine can and should be is partly a matter of opinion, but everyone will agree that it needs to be flexible and be equipped to handle the very wide variety of things that it will be used to draw.
That is why it is best to keep the features it provides as low-level as possible.

So a graphics module should basically just be a collection of the low-level parts needed to perform any abstract rendering process. You don’t even need (but you can if you want) to have an interface for activating textures and vertex buffers. CVertexBuffer and CTexture2D can handle these tasks themselves.

The main problem you have here is that the graphic module knows what a “ResourceObject” (a model) is when it should be entirely the other way around.
Because of how different the pipelines are for rendering a standard model vs. GeoClipmap terrain, trying to make a monolithic graphics module that knows about both of these and tries to render them properly all by itself is not only a mess but a violation of Single Responsibility Principle.

Things will go much more smoothly if the graphics module simply provides a set of low-level classes and a small interface for setting render states etc., and then let the models and terrain use these classes directly and set states directly before rendering themselves, by themselves.

Of note, that does not mean you are completely off base by thinking about sending the graphics module a class/structure to let the graphics module do some work of its own, setting states, activating textures, setting vertex buffers, etc.
But you went too high-level with it.
The graphics module can receive a structure with state settings (blending mode, culling mode, etc.) and pointers to textures to activate, but that is a mid-level feature and only acts as a helper function.
In other words, it is not doing anything special that models and terrain could not do themselves manually. Setting blending modes and textures could still be done manually, and even if you let the graphics module do it for you, it’s still just using its own already-exposed low-level API for it.


.GetCurrentTime(); //returns the float of the currentTime

It most certainly does not.
I already wrote a sub-chapter about this for my upcoming book, but I don’t have it with me right now.
I will post it here when I get home.



//This class will keep track of the game's ACTIONs and playerInput
[InputEngine]
.keyboard[256]; //For each key in the keyboard, has a bool to check if it's pressed or not
.mouse[8]; //Same for mouse, and mousewheel
//I'm thinking in moving these two to OS, since mobiles have different controls and it'll depend on the target platform the game will be
.list<Actions> listActions; //A list of all actions in the game, loaded from a resourceFile as strings such as "PLAYER_MOVERIGHT"
.list<Actions> playerInput; //A list of all actions input by the player, with timestamps

Handling input is much more complex than this, but I don’t think you would be able to handle its full intricacies on your first attempt.
This is one of those cases where I can only say, “You’re doing it wrong,” but will let you learn from personal experience. You will need that experience to understand why this is wrong even if I told you, and, besides, it would require a major restructure of your engine.
But I mention this is wrong not to just point and laugh, but to give you a head’s up on one potential place where you could do a lot better next time. If I said nothing at all you might be completely clueless on this part for a few iterations of your engine.


//This is an actual on-screen object, it uses a resourceObject data and there can be multiple of them on screen
[GameObject]
.ResourceObject* resourceObject; //the data of this object
.float positionX, positionY, positionZ;
.float directionX, directionY, directionZ;
.float speedX, speedY, speedZ;
//other variables needed depending on the game

For your first time, I am not going to nit-pick here, but my recommendations would be:
#1: Use vectors, not arrays of floats. Adding velocities and positions becomes a lot easier when you use overloaded CVector operators, and it just makes your code easier to maintain (especially in avoiding copy-paste errors and accidentally increasing X, Y, and Y, instead of X, Y, and Z).
#2: You have a chance to practice Single Responsibility Principle here. Every type of entity in your game, from the characters to the guns to the camera itself, will have a position, scale, and rotation. Sounds as though it is a good responsibility for a COrientation class. Another thing I have written for the book, which I can post when I get home, is just such a class.


//This is the main class where most of the code will be

Then you have not distributed enough of your code to other classes/modules.
To be frank, in my engine, this is probably the smallest class among the major non-helper type classes.

[GameEngine]
.SceneGraph sceneGraph; //basically a list of all GameObjects currently on the level

Just to point out: A scene graph is not a list, it is a hierarchy. It won’t be a scene graph unless you have a parent/child relationship in place (and I suggest you do; it really helps when you want that sword to stay in the guy’s hand).
But don’t misunderstand. In addition to that hierarchical relationship you do still need a flat list of objects for easy traversal.


.Initialize(); //empty function that's called on startup, will be filled differently for each game
.Update(); //empty function that's called every frame
.Draw(); //function that gets a list of objects that "can be seen" from the sceneGraph, and draws them

General Game/Engine Structure
Passing Data Between Game States


And my GameEngine's Update() would have something like this:

void GameEngine::Update()
{
    switch(gameState)
    {
        case GameState::Startup_Loading:
        {
        }
        case GameState::Startup:
        {
        }
        case GameState::MainGame_Loading:
        {
        }
        case GameState::MainGame:
        {
        }
    }
}

Once again: General Game/Engine Structure


L. Spiro

I restore Nintendo 64 video-game OST’s into HD! https://www.youtube.com/channel/UCCtX_wedtZ5BoyQBXEhnVZw/playlists?view=1&sort=lad&flow=grid

I am home and can post the promised topics from my book.

Note #1: I will not take a lot of time fixing the formatting, and images (and figures) will of course be missing. I don’t think you need them for your first attempt at an engine anyway.

Note #2: I want a clear separation between the topics I am posting from my book, including from this text as well. Each following post is a section of the book, and the separation between the posts makes the separation between the topics clear. It is not double-posting or spamming a topic, even though I will end up having 4 posts in a row in the same topic. I could have 1 gargantuan topic that no one would fully read, or, like with any long text, I can break it into multiple digestable parts.

Note #3: This was written for iOS, so the OS functions I use to get time will not work on Windows, but it is fairly straight-forward how to convert the code to have the same functionality.

L. Spiro

I restore Nintendo 64 video-game OST’s into HD! https://www.youtube.com/channel/UCCtX_wedtZ5BoyQBXEhnVZw/playlists?view=1&sort=lad&flow=grid

Time

Keeping track of time is important for any game, and it can often be difficult to do just right, as there are many small nuances to handle that, when done improperly, can lead to significant bugs that can be very difficult to trace and fix. Contributing to this is the fact that, at face value, keeping track of time seems to be a very simple task, luring many into a false sense of security when implementing their timers. This section will outline the important features a game timer should have, discuss some common pitfalls when implementing game timers, and provide a solid game timer implementation.

Important Features

Before anything else, it is important to outline what features a game timer should have and what it should do.

? Drift must be minimized. “Drift” refers to the accumulation of small amounts of error in time that cause the game’s timer to drift slowly away from what the actual game time should be.

? It must have some mechanism for pausing. This will be achieved by keeping a second time accumulator that only increments when the game is not paused.

? It should be in a resolution higher than milliseconds. Even though iOS devices are capped at rendering at 60 times per second, that still only leaves an accuracy of 16 (truncated) milliseconds per frame. The framework we will create will use a resolution of microseconds.

? It should have features to accommodate stepping through the game logic and the rendering logic at different rates, effectively decoupling them.

o Note that this type of decoupling is not related to multi-threaded rendering—moving the renderer onto its own thread is an entirely different subject.

? It must provide an interface for making it easy to get the delta time since the last frame in both microseconds and as a floating-point value. Floating-point values are better for physics simulations while microseconds are better for things that need to accumulate how much time has passed accurately and with as little drift as possible.

o Again, a focus on avoiding drift will be an important factor in how this is actually implemented.

? Overflow of the timers should be impractical. “Overflow” refers to the integral counter used to tell time accumulating beyond the limits of its integral form (as imposed by computer science) and wrapping back around to or past 0, which would trick sub-systems into thinking that a massive amount of time has passed since the last update (due to the unsigned nature of the integrals we will be using) which will effectively freeze most systems and cause unpredictable havoc with others. Given enough time, any timer will eventually overflow, so our objective is to make that it takes an unreasonable time to do so, for example over 100 years.

Common Pitfalls

If at first implementing a timer seems straightforward, reading over the previous section’s list of important features may make it more obvious that there is a lot of room for error, and these errors can be very hard to detect and trace, especially if they only occur over a long period of time.

? Using 32-bit floating-point values to store time is one of the biggest follies an inexperienced programmer will make, as due to its decreasing precision with higher numbers, your game will become noticeably choppier in a short amount of time, from somewhat choppy after a few hours to heavily choppy after a few days.
More advanced readers may instead use 64-bit floating-point values to accumulate time, but this has more subtle flaws, particularly related to drift. Small floating-point errors will accumulate over time, and while it may not be noticeable for a while it is also just as easy to accumulate time in the system’s native resolution with an unsigned 64-bit integer and eliminate drift to the best of the hardware’s capability. 64-bit doubles can still be used, but they should be re-derived each frame instead of accumulated directly.

? Updating the time more than once per game cycle, or in the most extreme case always getting the current time whenever you need it for any calculation is another common pitfall. Related time systems should be updated at specific intervals and only at those intervals. Examples of related time systems are those used for the sound thread, input timestamps, and the game’s primary timer.
In the event of input events, the timer for that system will be updated on each event it catches. For the sound thread, the timer will be updated once each time the sound manager updates. All sounds in it will then be advanced by that same amount of time to prevent any of them from updating faster or slower than the other sounds in the manager.
As for the game’s main timer, this should be updated once and only once on each game cycle, or game loop. Imagine the worst-case scenario in which the time is read from the system every time “time” is needed in an equation. If 2 balls fall from the same height at the same time, they should fall side-by-side. However, if the delta time—that is the time since the last update of each ball—is obtained by always checking the current system time, any hiccups in the physics system could trick one ball into believing it has fallen for a longer time on that frame and sneak ahead of the other ball.

? Using milliseconds instead of microseconds is a minor pitfall and may not show a significant change on iOS devices, but using microseconds is just as easy and just more accurate. As we will be using a macro for the resolution of our timers, however, any timer resolution will be easy to test in the end.

? Using a fixed framerate (such as always updating by 16.6 milliseconds, which will show slowdown when too many objects are on the screen etc.) is itself not a pitfall—it is entirely appropriate for some single-player games. The pitfall is when you make assumptions based on the framerate, especially when those assumptions impact other parts of the engine. There is an appropriate way to get a fixed framerate related only to the timer system and should be preferred over any assumptions that may impact other parts of the game such as the physics system. This method will be explained later.

Implementation: Basics

With the features went want to have and the pitfalls we want to avoid in mind we can begin our implementation of a timer class. The file will be part of the TKFoundation library and will be named TKFTime. The class declaration begins with the basic members we need to keep track of time without drift and a wrapper function for getting the current system time in its own native resolution. This is the time we will be accumulating.

Listing 2.3 TKFTime.h

namespace tkf {

class CTime {
public :
CTime();


// == Functions.
/** Gets the real system time in its own resolution.
* Not to be used for any reason except random-number
* seeding and internal use. Never use in game code! */
u64 GetRealTime() const;

protected :
// == Members.
/** The time resolution. */
u64 m_f64Resolution;

/** The time accumulator which always starts at 0. */
u64 m_u64CurTime;

/** The time of the last update. */
u64 m_u64LastTime;

/** The last system time recorded. */
u64 m_u64LastRealTime;
};

} // namespace tkf

TKFTime.cpp contains only the important includes, the constructor, and the implementation of GetRealTime().

Listing 2.4 TKFTime.cpp

#include "TKFTime.h"

#include <mach/mach.h>
#include <mach/mach_time.h>

namespace tkf {

CTime::CTime() :
m_u64CurTime( 0ULL ),
m_u64LastTime( 0ULL ) {
// Get the resolution.
mach_timebase_info_data_t mtidTimeData;
::mach_timebase_info( &mtidTimeData );
m_f64Resolution = static_cast<f64>(mtidTimeData.numer) /
static_cast<f64>(mtidTimeData.denom) / 1000.0;

m_u64LastRealTime = GetRealTime();
}

// == Functions.
/** Gets the real system time in its own resolution.
* Not to be used for any reason except random-number
* seeding and internal use. Never use in game code! */
u64 CTime::GetRealTime() const {
return ::mach_absolute_time();
}

} // namespace tkf

mach_absolute_time() returns the number of ticks since the device has been turned on and will be in a resolution native to the device. The values returned by mach_timebase_info() provide the numerator and denominator necessary to convert the system ticks into nanoseconds. Note that while we are using a 64-bit floating-point value for the conversion from system ticks to microseconds, we are not using it for any accumulation purposes, and as such it does not contribute to drift. Because it is used in multiplication later it has been divided by 1,000.0 so that it converts to microseconds rather than nanoseconds.

The next step is to add accumulation methods. This will be split into 2 functions: One that allows the number of system ticks by which to update and one that calls the former after actually determining how many ticks have passed since the last update. By splitting the update into these 2 functions we can keep other timers as slaves to another so that as the main timer updates, the amount by which it updates can be cascaded down to other timers so that they all update by the same amount. This will be important later.

Listing 2.5 TKFTime.cpp

/** Calculates how many system ticks have passed

* since the last update and calls UpdateBy()
* with that value. */
void CTime::Update( bool _bUpdateVirtuals ) {
u64 u64TimeNow = GetRealTime();
u64 u64Delta = u64TimeNow - m_u64LastRealTime;
m_u64LastRealTime = u64TimeNow;

UpdateBy( u64Delta, _bUpdateVirtuals );
}

/** Updates by the given number of system ticks. */
void CTime::UpdateBy( u64 _u64Ticks, bool _bUpdateVirtuals ) {
m_u64LastTime = m_u64CurTime;
m_u64CurTime += _u64Ticks;

}

Accumulating time in system resolution is simple. Note that inside of Update() the delta was calculated with a simple subtraction without any special-case handling for the wrap-around of the system tick counter. Due to how machines handle integer math, this is correct, as the wrap-around case will still result in the correct delta time.

Our timer will need to return the current microseconds at any given time so it is best to calculate that value up-front. Doing so is simple as it is just a matter of a single multiplication with the timer resolution. The CTime class will receive a new member called m_u64CurMicros and UpdateBy() will be modified.

Listing 2.6 TKFTime.cpp

/** Updates by the given number of system ticks. */

void CTime::UpdateBy( u64 _u64Ticks, bool _bUpdateVirtuals ) {
m_u64LastTime = m_u64CurTime;
m_u64CurTime += _u64Ticks;

m_u64CurMicros = static_cast<u64>(m_u64CurTime * m_f64Resolution);

}

Next, we need microsecond values for the current time and for how much time has passed. Handling this in a way that avoids drift is trickier. Let’s consider only the case of determining the number of microseconds since the last frame, as a u64 type. Inside of UpdateBy() we could simply convert _u64Ticks to microseconds, since that represents the amount of time that has passed since the last update. However this always truncates and we never get back any of those truncates microseconds, which is exactly what causes drift. Instead, microseconds since the last update must be calculated based off the raw ticks of the last update subtracted from the raw current ticks (after the update). This will add a member to CTime called m_u64DeltaMicros, and with it UpdateBy() must be modified.

Listing 2.7 TKFTime.cpp

/** Updates by the given number of system ticks. */

void CTime::UpdateBy( u64 _u64Ticks, bool _bUpdateVirtuals ) {
m_u64LastTime = m_u64CurTime;
m_u64CurTime += _u64Ticks;

u64 u64LastMicros = m_u64CurMicros;
m_u64CurMicros = static_cast<u64>(m_u64CurTime * m_f64Resolution);
m_u64DeltaMicros = m_u64CurMicros - u64LastMicros;

}

m_u64CurMicros and m_u64DeltaMicros can get returned by inlined methods GetCurMicros() and GetDeltaMicros(), not shown here for brevity. Objects that want to accumulate time should accumulate with GetDeltaMicros(). Another time-related attribute that is commonly needed is the amount of time that has passed in seconds, often in 32-bit floating-point format since it is generally a small value. This value will also be updated once inside UpdateBy() and obtained by an inlined accessor method called GetDeltaTime().

Listing 2.8 TKFTime.cpp

m_u64DeltaMicros = m_u64CurMicros - u64LastMicros;
m_f32DeltaTime = m_u64DeltaMicros *

static_cast<f32>(1.0 / 1000000.0);

Implementation: Pause

The time class updates in such a way as to avoid drift and overflow, and buffers common time values for fast access via inlined accessors. The next feature to add is pausing. This is very simple and largely a repeat of previous updating code. Pausing is implemented via a second set of accumulators called “virtual” accumulators. Virtual ticks will be optionally accumulated on each update, and the rest (virtual current microseconds, virtual delta microseconds, and virtual delta time) will be updated based off that in the same way the normal deltas and time values are updated. A new set of “Virt” members are added to CTime and updated inside of UpdateBy().

Listing 2.9 TKFTime.cpp

if ( _bUpdateVirtuals ) {
m_u64VirtCurTime += _u64Ticks;
u64 u64VirtLastMicros = m_u64VirtCurMicros;
m_u64VirtCurMicros = static_cast<u64>(m_u64VirtCurTime
* m_f64Resolution);
m_u64VirtDeltaMicros = m_u64VirtCurMicros - u64VirtLastMicros;
m_f32VirtDeltaTime = m_u64VirtDeltaMicros *
static_cast<f32>(1.0 / 1000000.0);

}

Additionally, inlined accessors are added to access these virtual timers (not shown here for brevity). When objects can be paused, they should always exclusively use the virtual set of time values. The standard time values are incremented every frame and can be used to animate things that never pause such as menus. Meanwhile, the game world will update based on the virtual set of time values and all game objects will stop moving when the game is paused.

Implementation: Fixed Time-Stepping

Fixed time-stepping is itself sometimes a difficult concept to grasp, and proper implementations are often difficult to get right. Fixed time-stepping refers to decoupling the logical updates from the rendering (this form of decoupling is unrelated to multi-threaded rendering), executing logical updates only after a certain fixed amount of time has passed while rendering as often as possible, regardless of how much time has passed. There are several important reasons why this is done. Firstly, logical updates are where physics, AI, scripts, etc., are run. By spacing these updates out over a few frames rather than every frame CPU usage is reduced and the overall framerate can increase. Secondly, and most importantly, physics simulations require a constant update time each time they are run in order to avoid exploding scenes and other buggy behavior. Finally, fixed time steps are essential for keeping synchronization over network play as close as possible. While it is still possible for clients to lose sync and there does need to be a mechanism to re-sync clients anyway, things are greatly simplified when non-interactive parts of the scene are run by fixed time steps, which all but ensures they will remain in sync for all clients.

Conceptually it can be viewed as shown in Figure 2-7. Logical updates, in a perfect world, would happen at perfectly even intervals. Renders happen as fast as possible, on every game loop or game cycle, which means there may be longer delays between some renders and short delays between others.

Figure 2-7

Unfortunately the logical updates and renders are executed on the same thread, and since renders happen every loop it is impossible to actually place a logical update anywhere except on the same intervals as the renders. That is, you cannot have a render every loop and somehow insert a logical update half-way into the loop. It turns out, however, that this is nothing but a conceptual problem. Inside of the game, actual universal time doesn’t matter. Imagine scenario 1 in which a render is delayed for 3 seconds and logical updates occur every second. In a perfect world, we would wait 1 second, perform 1 logical update, wait another second and update logic, wait 1 more second and update logic, and then render. But to the player who is looking at the screen, this process yields exactly the same result as if we waited 3 seconds, then updated the logic 3 times quickly, each update by 1 second, and then rendered. The simulation has, in either case, moved forward by 3 seconds before the render, and the render itself will be identical in both scenarios. In other words, the logical updates don’t actually need to occur at the correct times in real time—the only thing that matters is if the same number of logical updates have been executed before the render. In practice, it looks like Figure 2-8.

Figure 2-8

In Figure 2-8, the logical updates occur at the same time as the renders because they are on the same thread and can only execute inside of a single game loop. The original spacing of the logical updates is shown in grey. Instead of executing the logical updates at arbitrary times within the game loop, we simply change the number of time the logical updates occur on each game loop. Notice that the second logical update was pushed back a bit due to some lag, but in total 2 logical updates took place before the second render, which means the game simulation had advanced by, let’s say 2 seconds (borrowing from our previous example), which means the render still depicts the game state the same way it would have had we been able to execute the logical update at literal 1-second intervals.

Notice also that if the game loop and render execute too quickly, sometimes too little time passes to justify another logical update. In this case 0 logical updates were issued on that game loop and the loop goes straight to rendering. This saves on CPU usage. Although not shown here, it can be extrapolated that 0 and 1 are not the only number of times that might be necessary to run the logical update in a single game loop. If there is a strong lag and the delay between frames increases enough, it may be necessary to run 2, 3, or more logical updates in a single loop. Our CTime class is going to determine for us how many times to update the logic on each game loop. Actually performing the updates will be covered in the next chapter. Additionally, an astute reader may have realized that in these simple examples it doesn’t make sense to render unless a logical update has taken place because you would be rendering the exact same scene with all the objects in their same locations etc. While this is true for the simple example provided here, there is also a solution to this involving interpolation. Again, this is handled at a higher level in the system and will be explained in detail in the next chapter, but it is worth mentioning now because in order to perform that interpolation we will need the CTime class to give us an extra value back when it calculates how many logical updates to perform.

The main question we want the CTime class to answer is how many times we need to update the logical loop each tick. For this, a new method is added to CTime called GetFixedStepUpdateCountFromTicks().

Listing 2.10 TKFTime.cpp

/** Gets the the number logical updates necessary for a fixed time

* step of the given number of system ticks as well as the fraction
* between logical updates where the simulation will be after
* updating the returned number of times. */
u32 CTime::GetFixedStepUpdateCountFromTicks( u64 _u64Ticks,
f32 &_f32Ratio, u64 &_u64RemainderTicks ) {
u64 u64TimeNow = GetRealTime();
u64 u64Delta = u64TimeNow - m_u64LastRealTime;

u32 u32Count = static_cast<u32>(u64Delta / _u64Ticks);
u64 u64AddMe = u32Count * _u64Ticks;
m_u64LastRealTime += u64AddMe;
_u64RemainderTicks = u64Delta - u64AddMe;

_f32Ratio = static_cast<f32>(static_cast<f64>(u64AddMe) /
static_cast<f64>(_u64Ticks));
return u32Count;

}

Notice that since GetRealTime() is called directly this should not be used in conjunction with Update(). This utility function is meant to be used with UpdateBy(). Notice that after getting the real time it doesn’t directly copy that into m_u64LastRealTime. If it did, then the next time this is called on the next frame the delta would not include the previous frame’s time. If the deltas don’t accumulate across frames, the proper number of logical updates per frame can’t be calculated—remember that it may take several frames before a logical update is needed. Next, notice that m_u64LastRealTime is modified by adding the count multiplied by the ticks. The CTime class will expect you to call UpdateBy() with the same number of ticks, u32Count times. Again, this is to remove drift. While we will lose a few ticks to rounded off fractions now, they will be regained on later frames. Finally, when the logical update has executed u32Count number of times, each time adding _u64Ticks to its current simulation time, the logical update’s time will be a tick that is a multiple of _u64Ticks but the real game time will be somewhere between that time and the next logical update (the logical update’s time + _u64Ticks). _f32Ratio is calculated to represent the fraction of the real time between logical updates. This will be useful in the next chapter. The actual number of ticks between logical ticks is stored in the return-by-reference parameter _u64RemainderTicks, which will later be needed to update the render time for each cycle.

L. Spiro

I restore Nintendo 64 video-game OST’s into HD! https://www.youtube.com/channel/UCCtX_wedtZ5BoyQBXEhnVZw/playlists?view=1&sort=lad&flow=grid

Orientation

Objects in the 3D world of your game will need some representation of their orientation. In geometry, orientation refers only to the rotational aspect of a body, but in video games orientation is often defined as their translations, scales, and rotations combined. While the representations for translations and scales are straightforward (3-dimensional vectors), representing rotation is often a fuzzy area that is easy to get wrong even though there is no single best way to get it right. One way that inexperienced programmers will often try is to use Euler angles representing how far left/right and up/down the object is facing. This is especially often used on players and cameras, exemplifying another common mistake in which players and cameras have a different orientation system from the other game objects. This makes it easy to modify rotations but makes the run-time slow, as updating objects over time requires an expensive sine and cosine each frame for each object.

It is generally best to use vectors for the rotation part of an orientation, but then the question becomes a matter of which vectors to store. Some implementations save space by storing only the Forward and Up vectors and determine the Right vector when needed with a slight penalty to performance. Because this book focuses on run-time performance and because most devices these days, including iOS devices, the primary focus of this book, have substantial memory, our implementation will also store the Right vector. The overhead in updating objects’ rotations becomes greater, but most objects in a game scene will often be static with only a few objects updating at all, and fewer still updating frequently. On the other hand, time is saved when generating matrices for rendering since the Right vector does not need to be re-determined each time.

The shell for the COrientation class is fairly straightforward. It contains each of the properties an orientation needs—position, scale, and rotation—along with a dirty flag to let us know what, if anything, has been updated. The dirty flag allows us to rebuild matrices only if they actually change, saving a lot of computational expense on objects that rarely or never move.

Listing 2.13 TKFOrientation.h

#include "../TKFFnd.h"

#include "../Matrix/TKFMatrix44.h"

#include "../Vector/TKFVector3.h"

namespace tkf {

class COrientation {

public :

COrientation();

// == Functions.

/** Gets/sets the position. */

inline CVector3 & Pos() {

m_u32Dirty |= TKF_DF_POS;

return m_vPos;

}

/** Gets the position. */

inline const CVector3 & Pos() const {

return m_vPos;

}

/** Gets/sets the scale. */

inline CVector3 & Scale() {

m_u32Dirty |= TKF_DF_SCALE;

return m_vScale;

}

/** Gets the scale. */

inline const CVector3 & Scale() const {

return m_vScale;

}

/** Checks the dirty flag. */

inline bool IsDirty() const {

return m_u32Dirty != 0;

}

protected :

// == Enumerations.

/** Dirty flags. */

enum TKF_DIRTY_FLAGS {

TKF_DF_POS = (1 << 0),

TKF_DF_SCALE = (1 << 1),

TKF_DF_ROT = (1 << 2),

TKF_DF_NORMALIZE = (1 << 3),

};

// == Members.

/** Position. */

CVector3 m_vPos;

/** Scale. */

CVector3 m_vScale;

/** Right vector. */

CVector3 m_vRight;

/** Up vector. */

CVector3 m_vUp;

/** Forward vector. */

CVector3 m_vForward;

/** Dirty flag. */

u32 m_u32Dirty;

};

} // namespace tkf

Our use of const-correctness allows us to dirty the position and scale only if they are accessed for writing and not for reading. The dirty flag also has a bit for normalization of the rotations, another expensive operation we wish to avoid if possible.

So far the COrientation class is very straightforward. Each frame, objects’ orientations will be updated and then a matrix will be built for that object to pass to the renderer. Building the matrix is also very straightforward, and is as follows.

Listing 2.14 TKFOrientation.cpp

/** Creates the matrix representing this orientation. */

void COrientation::CreateMatrix( CMatrix44 &_mMatrix ) const {

_mMatrix._11 = m_vRight.x * m_vScale.x;

_mMatrix._12 = m_vRight.y * m_vScale.x;

_mMatrix._13 = m_vRight.z * m_vScale.x;

_mMatrix._14 = 0.0f;

_mMatrix._21 = m_vUp.x * m_vScale.y;

_mMatrix._22 = m_vUp.y * m_vScale.y;

_mMatrix._23 = m_vUp.z * m_vScale.y;

_mMatrix._24 = 0.0f;

_mMatrix._31 = m_vForward.x * m_vScale.z;

_mMatrix._32 = m_vForward.y * m_vScale.z;

_mMatrix._33 = m_vForward.z * m_vScale.z;

_mMatrix._34 = 0.0f;

_mMatrix._41 = m_vPos.x;

_mMatrix._42 = m_vPos.y;

_mMatrix._43 = m_vPos.z;

_mMatrix._44 = 1.0f;

}

Additionally, a function for updating a matrix only if the dirty flag is set is added. It clears the dirty flag after updating the matrix.

Listing 2.15 TKFOrientation.cpp

/** Updates the given matrix only if the dirty flag is set. */

inline void COrientation::UpdateMatrix( CMatrix44 &_mMatrix ) const {

if ( IsDirty() ) {

Normalize(); // Normalizes rotation vectors if flag set.

CreateMatrix( _mMatrix );

m_u32Dirty = 0UL;

}

}

Working with and updating rotations is a bit more involved but mainly revolves around keeping the 3 vectors orthogonal, which in itself is actually fairly trivial. The most important features for the interface to have are those that allow an object to point at another object, to set the rotation directly, and to rotate around an axis by a given amount. Just as it is easy to create a matrix with the rotation vectors as shown in the CreateMatrix() method it is easy to extract them, so we will start with the simplest methods first.

Listing 2.16 TKFOrientation.cpp

/** Sets the rotation given a matrix. */

void COrientation::SetRot( const CMatrix44 &_mMatrix ) {

m_vRight = _mMatrix.GetRow( 0 );

m_vUp = _mMatrix.GetRow( 1 );

m_vForward = _mMatrix.GetRow( 2 );

m_u32Dirty |= TKF_DF_ROT | TKF_DF_NORMALIZE;

}

Setting the rotation via a forward and up vector, like in common “look at” functions, is also straightforward.

Listing 2.17 TKFOrientation.cpp

/** Sets the rotation given a a forward and up vector. */

void COrientation::SetRot( const CVector3 &_vForward,
const CVector3 &_vUp ) {

m_vForward = _vForward;

m_vRight = _vUp % _vForward;

m_vUp = _vForward % m_vRight;

m_u32Dirty |= TKF_DF_ROT | TKF_DF_NORMALIZE;

}

Adding rotations is only slightly more difficult but it is an important foundation for other routines, such as the one we will later add to rotate around an arbitrary axis. The most useful routine for our purposes will be to add a rotation given by a matrix. The code looks daunting, but it’s not too complicated.

Listing 2.18 TKFOrientation.cpp

/** Adds a rotation from a matrix. */

void COrientation::AddRot( const CMatrix44 &_mMatrix ) {

m_vRight = CVector3(

m_vRight * CVector3( _mMatrix._11, _mMatrix._21, _mMatrix._31 ),

m_vRight * CVector3( _mMatrix._12, _mMatrix._22, _mMatrix._32 ),

m_vRight * CVector3( _mMatrix._13, _mMatrix._23, _mMatrix._33 ) );

m_vForward = CVector3(

m_vForward * CVector3( _mMatrix._11, _mMatrix._21, _mMatrix._31 ),

m_vForward * CVector3( _mMatrix._12, _mMatrix._22, _mMatrix._32 ),

m_vForward * CVector3( _mMatrix._13, _mMatrix._23, _mMatrix._33 ) );

m_vUp = CVector3(

m_vUp * CVector3( _mMatrix._11, _mMatrix._21, _mMatrix._31 ),

m_vUp * CVector3( _mMatrix._12, _mMatrix._22, _mMatrix._32 ),

m_vUp * CVector3( _mMatrix._13, _mMatrix._23, _mMatrix._33 ) );

m_u32Dirty |= TKF_DF_ROT | TKF_DF_NORMALIZE;

}

With this method in place, rotating around an arbitrary axis is fairly simple. We simply construct an axis-rotation matrix and pass it to this method.

Listing 2.19 TKFOrientation.cpp

/** Rotates by the given amount around the given axis. */

void COrientation::RotateAxis( const CVector3 &_vAxis,
f32 _f32Radians ) {

CMatrix44 mRot;

AddRot( mRot.RotationAxis( _vAxis, _f32Radians ) );

}

The code for CMatrix44::RotationAxis() is shown here for completeness.

Listing 2.19 TKFMatrix44.cpp

/** Builds a matrix representing a rotation around an arbitrary axis. */

CMatrix44 & CMatrix44::RotationAxis( const CVector3 &_vAxis,
f32 _f32Radians ) {

f32 fS = ::sinf( _f32Radians );

f32 fC = ::cosf( _f32Radians );

f32 fT = 1.0f - fC;

register f32 fTX = fT * _vAxis.x;

register f32 fTY = fT * _vAxis.y;

register f32 fTZ = fT * _vAxis.z;

f32 fSX = fS * _vAxis.x;

f32 fSY = fS * _vAxis.y;

f32 fSZ = fS * _vAxis.z;

_11 = fTX * _vAxis.x + fC;

_12 = fTX * _vAxis.y + fSZ;

_13 = fTX * _vAxis.z - fSY;

_14 = 0.0f;

_21 = fTY * _vAxis.x - fSZ;

_22 = fTY * _vAxis.y + fC;

_23 = fTY * _vAxis.z + fSX;

_24 = 0.0f;

_31 = fTZ * _vAxis.x + fSY;

_32 = fTZ * _vAxis.y - fSX;

_33 = fTZ * _vAxis.z + fC;

_34 = 0.0f;

_41 = 0.0f;

_42 = 0.0f;

_43 = 0.0f;

_44 = 1.0f;

return (*this);

}

These methods give the orientation class a solid foundation for use within the framework. All entities in our game world, including the camera, will make use of this utility class.

L. Spiro

I restore Nintendo 64 video-game OST’s into HD! https://www.youtube.com/channel/UCCtX_wedtZ5BoyQBXEhnVZw/playlists?view=1&sort=lad&flow=grid

I have a question: Is this supposed to be used in one game? If so this engine seems fine, minus having no alpha which might be on purpose.

It's my first engine, and I hope to start developing all my games with it.

By alpha you mean transparency? I forgot to add it to the texture/object.

Thank you for the links, that's going to be useful for flag events since I think there's going to be a lot of them until I finish a game and I was looking for some better way to manage it, besides the states.

You would be better off just using a texture pointer directly, or better still a shared pointer to a texture. Textures may have ID’s (and they very well should, to prevent redundant texture loads), but using that ID rather than a direct pointer wastes precious cycles, as you then need to find that texture via binary searches, hash tables, etc.

Sorry but I didn't understand it, what do you mean by not using the ID, but the pointer instead?

A ResourceObject will have a pointer to a Material which has the textureID, that's the unsigned int that gets the value from the glGenTextures(count, unsigned int*) function, and I have that so I can keep track if the texture's been loaded in the RAM (has a value > 0) or not (any value > 0), and when I have to draw I call glBindTexture(GL_TEXTURE_2D, object->material.textureID).

Is there a better way to do this?

Those are usually just characters, ammunition/weapons, and various other props. Other things, such as terrain, vegetation, buildings (especially interiors), skyboxes, etc., are all made efficient by utilizing rendering pipelines unique to themselves.

Oh, I totally forgot about those... would creating a specific object for each of these types be the best solution? Something like a TerrainObject which doesn't need to have a mesh, but materials only and the height data?

When you break it down like this it starts to make sense that models should actually just load themselves. Models know what models are. They know what their binary format is. There is no reason to push the responsibility of the loading process onto any other module or class.

Wouldn't this make it harder for different games? I was going for the ResourceEngine with the idea that I'll be building my resource binary files from another program, my map editor, which will probably be different for each game, and I'd only have code to writing/reading from disk in that class so it's easier to change for the game I'm writing.

The main problem you have here is that the graphic module knows what a “ResourceObject” (a model) is when it should be entirely the other way around.

I remember that at first, my idea was to have the models load & draw themselves, they'd have access to the draw functions, but I choose to have a single class responsible for all the drawing and the models only being objects with their data stored so it's easier to change the draw functions if I ever need it.

I'm using OpenGL for now, so all the OpenGL functions are in this GraphicEngine class and I just need to know how should I display an object's data on screen, and if I were to change to Direct3D or something else specific (mobiles that don't use OpenGL) I would only have to rewrite this part.

I don't know if this ever happens, but is there a better way to deal with this?

This is one of those cases where I can only say, “You’re doing it wrong,” but will let you learn from personal experience. You will need that experience to understand why this is wrong even if I told you, and, besides, it would require a major restructure of your engine.

Oh but that's exactly what I'm trying to avoid... I'm fine with having to rewrite a lot of my engine code as I get more experienced, but I'm trying to get at least the basics (and properly understand it) so I don't go fully bad right off the start. I had another post about handling player input and that's what I could get from it, I've seen some other posts and tutorials but it's still... very "high level", there's much concept and little to no code or explanations that I can understand so I'm quite lost in this part.

I'm sorry about the questions, I appreciate very much these replies, but I'd just like to ask the "why"s to understand why something is better than another and such.

Thank you very much for the information, I'm still reading the last 2 topics on Time and Orientation, and everything has been a great help so far!


or it returns a shared pointer to that model.

Don’t use ID’s for everything, as, as I already mentioned, it wastes precious cycles.

what if the ID is the offset into an array of shared texture pointers?

Norm Barrows

Rockland Software Productions

"Building PC games since 1989"

rocklandsoftware.net

PLAY CAVEMAN NOW!

http://rocklandsoftware.net/beta.php


or it returns a shared pointer to that model.

Don’t use ID’s for everything, as, as I already mentioned, it wastes precious cycles.

what if the ID is the offset into an array of shared texture pointers?

this is pretty much what my engine is doing.

typically, integer values are used.

a person could probably even get along ok with a short, working under the assumption that it is unlikely there will ever be more than 32k textures.

in a few cases, I have used bytes for color and normal arrays, mostly because in some places RAM use is an issue (*1), and using bytes I only need about 1/4 as much space. this mostly then just leaves floats for XYZ and ST coordinate arrays.

*1: my engine currently is operating fairly close to the limits of a 32-bit address space, so involves some amount of "trying to avoid wasting memory where possible".

this is mostly done with things like the voxel terrain geometry, though ironically the geometry for the terrain tends to be a fair bit smaller than the actual raw voxel data (most of the voxels don't actually end up needing geometry). better probably would have been using "voxel indices" instead, but sadly this would require some non-trivial changes and introduce new possible issues (need to re-index voxels on block-updates, ...). (it is a hard choice between the issues of indices and currently needing to spend 8-bytes for every voxel, when many/most chunks only have a small number of unique voxel types...).

also considered a few times was possibly separating the high-level object types from their rendering (using generic "Mesh" objects for nearly everything), but sadly this hasn't been done.

as-is, there are a few semi-generic cases though:

Mesh: generally represents a piece of static geometry;

Brush: pretty much anything that looks like a world brush (may include patches and meshes as well as plane-bound brushes);

Model: pretty much anything that is instantiated and placed in the scene, uses vtables for sub-types (may hold Mesh or other more specialized model types, such as skeletal models or sprites);

Region/Chunk: main systems used by the terrain system (a Chunk is basically an elaborate case of a mesh, with more involved rendering logic to deal with the various cases, and may potentially hold raw voxel data, and potentially also Mesh or Light objects, *2);

...

*2: visible chunks currently optionally hold voxel data. it may be absent in cases where the engine has decided to store it in an RLE-packed format.

the Region object may also hold Brush or Entity/SEntity objects (as an alternative to storing them in World), but this still isn't really fully developed (spawned entities are not persistent, ...). note that these are stored using Region-local coordinates. I chose region for holding this stuff (vs Chunk) mostly due to it being a slightly more convenient size and involving probably less overhead.

granted, a lot of this may be N/A depending on the type of game.

You would be better off just using a texture pointer directly, or better still a shared pointer to a texture. Textures may have ID’s (and they very well should, to prevent redundant texture loads), but using that ID rather than a direct pointer wastes precious cycles, as you then need to find that texture via binary searches, hash tables, etc.



Sorry but I didn't understand it, what do you mean by not using the ID, but the pointer instead?
A ResourceObject will have a pointer to a Material which has the textureID, that's the unsigned int that gets the value from the glGenTextures(count, unsigned int*) function, and I have that so I can keep track if the texture's been loaded in the RAM (has a value > 0) or not (any value > 0), and when I have to draw I call glBindTexture(GL_TEXTURE_2D, object->material.textureID).
Is there a better way to do this?

OpenGL may be a state machine, but you need to think of each object you get from OpenGL (textures, vertex buffers, etc.) as…objects. In other words, a class called CTexture2D (use whatever naming convention you like) completely wraps around and manages the lifetime of the GLuint that OpenGL gives you.
That GLuint does not exist in the eyes of any other class in your whole engine. Nothing needs to know what that thing is because that is the soul responsibility of the CTexture2D class.
Here are actual examples from my own engine:
	// == Various constructors.
	LSE_CALLCTOR COpenGlStandardTexture::COpenGlStandardTexture() :
		m_uiTexture( 0 ) {
	}
	LSE_CALLCTOR COpenGlStandardTexture::~COpenGlStandardTexture() {
		ResetApi();
	}
	/**
	 * Activate this texture in a given slot.
	 *
	 * \param _ui32Slot Slot in which to place this texture.
	 * \return Returns true if the texture is activated successfully.
	 */
	LSBOOL LSE_CALL COpenGlStandardTexture::Activate( LSUINT32 _ui32Slot ) {
		if ( !m_uiTexture ) { return false; }
		if ( m_ui32LastTextures[_ui32Slot] != m_ui32Id ) {
			m_ui32LastTextures[_ui32Slot] = m_ui32Id;
			COpenGl::ActivateTexture( _ui32Slot );
			::glBindTexture( GL_TEXTURE_2D, m_uiTexture );
		}

		return true;
	}
	/**
	 * Reset everything related to OpenGL textures.
	 */
	LSVOID LSE_CALL COpenGlStandardTexture::ResetApi() {
		if ( m_uiTexture ) {
			::glDeleteTextures( 1, &m_uiTexture );
			m_uiTexture = 0;
		}
	}
	/**
	 * Create an OpenGL texture and fill it with our texel data.  Mipmaps are generated if necessary.
	 *
	 * \return Returns true if the creation and filling of the texture succeeds.  False indicates a resource error.
	 */
	LSBOOL LSE_CALL COpenGlStandardTexture::CreateApiTexture() {
		ResetApi();

		// Generate a new texture.
		::glGenTextures( 1, &m_uiTexture );
		glWarnError( "Uncaught" );	// Clear error flag.
		::glBindTexture( GL_TEXTURE_2D, m_uiTexture );

		[…] LOTS OF PREPARATION CODE OMITTED.


		// Set filters etc. first (improves performance in many drivers).
		::glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, iMagFilter[ui32FilterIndex] );
		::glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, iMinFilter[ui32FilterIndex] );
		if ( m_ui32Usage & LSG_TFT_ANISOTROPIC ) {
			::glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, CStd::Max<LSUINT32>( 1UL, CFnd::GetMetrics().ui32MaxAniso >> 1UL ) );
		}

		static const GLint iClamp[] = {
			GL_CLAMP_TO_EDGE,
			GL_REPEAT,
			GL_MIRRORED_REPEAT,
		};
		::glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, iClamp[m_wmWrap] );
		::glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, iClamp[m_wmWrap] );

		if ( glWarnError( "Error setting filters on standard texture" ) ) {
			ResetApi();
			COpenGlStandardTexture::ClearLastSetTexAndUnbindAll();
			return false;
		}


		// Now fill in the texture data.
		switch ( m_iTexels.GetFormat() ) {
			case LSI_PF_DXT1 : {}
			case LSI_PF_DXT3 : {}
			case LSI_PF_DXT5 : {
				COpenGl::glCompressedTexImage2D( GL_TEXTURE_2D, 0, GetOpenGlInternalFormatStandard( m_iTexels.GetFormat() ),
					piUseMe->GetWidth(), piUseMe->GetHeight(), 0,
					CDds::GetCompressedSize( piUseMe->GetWidth(), piUseMe->GetHeight(), CDds::DdsBlockSize( m_iTexels.GetFormat() ) ),
					piUseMe->GetBufferData() );
				break;
			}
			case LSI_PF_DXT2 : {}
			case LSI_PF_DXT4 : {
				return false;
			}
			default : {
				::glTexImage2D( GL_TEXTURE_2D, 0, GetOpenGlInternalFormatStandard( m_iTexels.GetFormat() ),
					piUseMe->GetWidth(), piUseMe->GetHeight(), 0, GetOpenGlFormatStandard( m_iTexels.GetFormat() ),
					GetOpenGlTypeStandard( m_iTexels.GetFormat() ), piUseMe->GetBufferData() );
			}
		}
		

		if ( glWarnError( "Error creating standard texture" ) ) {
			ResetApi();
			return false;
		}
		return true;
	}
Notice that the class manages the lifetime of m_uiTexture and correctly destroys it if the class itself its destroyed. This ensures no resource leaks are possible, at least at this level.
Every resource you get from OpenGL should be managed similarly.

That means that your material will never ever have the GLuint ID of an OpenGL texture. It will have a (hopefully shared) pointer to CTexture2D or such.
You asked about DirectX below. This is also heavily related to that, and I will explain below.


Those are usually just characters, ammunition/weapons, and various other props. Other things, such as terrain, vegetation, buildings (especially interiors), skyboxes, etc., are all made efficient by utilizing rendering pipelines unique to themselves.



Oh, I totally forgot about those... would creating a specific object for each of these types be the best solution? Something like a TerrainObject which doesn't need to have a mesh, but materials only and the height data?

Probably.



When you break it down like this it starts to make sense that models should actually just load themselves. Models know what models are. They know what their binary format is. There is no reason to push the responsibility of the loading process onto any other module or class.



Wouldn't this make it harder for different games? I was going for the ResourceEngine with the idea that I'll be building my resource binary files from another program, my map editor, which will probably be different for each game, and I'd only have code to writing/reading from disk in that class so it's easier to change for the game I'm writing.

If you change your model’s format you will have to change loading code somewhere either way. It is no more difficult to open and edit “Model.cpp” than “ResourceManager.cpp”.
But it will be much cleaner and easier to browse to the location where you want to edit if you keep the loading code separate, because Model.cpp will not be a monolithic 10,000-line file.


The main problem you have here is that the graphic module knows what a “ResourceObject” (a model) is when it should be entirely the other way around.



I remember that at first, my idea was to have the models load & draw themselves, they'd have access to the draw functions, but I choose to have a single class responsible for all the drawing and the models only being objects with their data stored so it's easier to change the draw functions if I ever need it.
I'm using OpenGL for now, so all the OpenGL functions are in this GraphicEngine class and I just need to know how should I display an object's data on screen, and if I were to change to Direct3D or something else specific (mobiles that don't use OpenGL) I would only have to rewrite this part.

I don't know if this ever happens, but is there a better way to deal with this?

For a model to be able to draw itself and for all of the rendering commands to be neatly located inside the graphics module and only the graphics module are not mutually exclusive.

When I said a model would draw itself, I didn’t mean that it would call OpenGL functions directly. The graphics module should hide away all the calls to OpenGL/DirectX and provide a single unified interface that the models can use to set blending modes and render.

In my engine, the LSGraphicsLib library has a CFnd class which if, of course, the foundation for all graphics. Here are examples.
OpenGL:
	/**
	 * Sets culling to on or off.
	 *
	 * \param _bVal Whether culling is enabled or disabled.
	 */
	LSE_INLINE LSVOID LSE_FCALL COpenGl::SetCulling( LSBOOL _bVal ) {
		_bVal = _bVal != false;
		if ( m_bCulling != _bVal ) {
			m_bCulling = _bVal;
			if ( _bVal ) { ::glEnable( GL_CULL_FACE ); }
			else { ::glDisable( GL_CULL_FACE ); }
		}
	}

	/**
	 * Sets the cull winding order.
	 *
	 * \param _cmMode The cull winding.
	 */
	LSE_INLINE LSVOID LSE_FCALL COpenGl::SetCullMode( LSG_CULL_MODES _cmMode ) {
		assert( _cmMode == LSG_CM_CW || _cmMode == LSG_CM_CCW );
		if ( m_cmCullMode != _cmMode ) {
			m_cmCullMode = _cmMode;
			static const GLenum eModes[] = {
				GL_CW,
				GL_CCW
			};
			::glFrontFace( eModes[_cmMode] );
		}
	}
DirectX 9:
	/**
	 * Sets culling to on or off.
	 *
	 * \param _bVal Whether culling is enabled or disabled.
	 */
	LSE_INLINE LSVOID LSE_FCALL CDirectX9::SetCulling( LSBOOL _bVal ) {
		_bVal = _bVal != false;
		if ( m_bCulling != _bVal ) {
			m_bCulling = _bVal;
			if ( !_bVal ) {
				m_pd3dDevice->SetRenderState( D3DRS_CULLMODE, D3DCULL_NONE );
			}
			else {
				static D3DCULL cModes[] = {
					D3DCULL_CCW,
					D3DCULL_CW
				};
				m_pd3dDevice->SetRenderState( D3DRS_CULLMODE, cModes[m_cmCullMode] );
			}
		}
	}

	/**
	 * Sets the cull winding order.
	 *
	 * \param _cmMode The cull winding.
	 */
	LSE_INLINE LSVOID LSE_FCALL CDirectX9::SetCullMode( LSG_CULL_MODES _cmMode ) {
		assert( _cmMode == LSG_CM_CW || _cmMode == LSG_CM_CCW );
		if ( m_cmCullMode != _cmMode ) {
			m_cmCullMode = _cmMode;
			if ( m_bCulling ) {
				static const D3DCULL cCull[] = {
					D3DCULL_CCW,
					D3DCULL_CW
				};
				m_pd3dDevice->SetRenderState( D3DRS_CULLMODE, cCull[_cmMode] );
			}
		}
	}
Then in LSFFnd.h (the .h for the CFnd class), I do this:
#ifdef LSG_OGL
	typedef COpenGl						CBaseApi;
#elif defined( LSG_OGLES )
	typedef COpenGlEs					CBaseApi;
#elif defined( LSG_DX9 )
	typedef CDirectX9					CBaseApi;
#elif defined( LSG_DX10 )
	typedef CDirectX10					CBaseApi;
#elif defined( LSG_DX11 )
	typedef CDirectX11					CBaseApi;
#endif	// #ifdef LSG_OGL

[…]

	/**
	 * Sets culling to on or off.
	 *
	 * \param _bVal Whether culling is enabled or disabled.
	 */
	LSE_INLINE LSVOID LSE_FCALL CFnd::SetCulling( LSBOOL _bVal ) {
		CBaseApi::SetCulling( _bVal );
	}

	/**
	 * Sets the cull winding order.
	 *
	 * \param _cmMode The cull winding.
	 */
	LSE_INLINE LSVOID LSE_FCALL CFnd::SetCullMode( LSG_CULL_MODES _cmMode ) {
		CBaseApi::SetCullMode( _cmMode );
	}
Now the graphics module is doing its job. My models and terrain can call CFnd::SetCulling() and CFnd::SetCullMode() without having to worry about the underlying API.
To finish the example of my OpenGL texture class, here are the same methods on the DirectX 9 side of things:
	// == Various constructors.
	LSE_CALLCTOR CDirectX9StandardTexture::CDirectX9StandardTexture() :
		m_pd3dtTexture( NULL ) {
	}
	CDirectX9StandardTexture::~CDirectX9StandardTexture() {
		ResetApi();
	}
	/**
	 * Activate this texture in a given slot.
	 *
	 * \param _ui32Slot Slot in which to place this texture.
	 * \return Returns true if the texture is activated successfully.
	 */
	LSBOOL LSE_CALL CDirectX9StandardTexture::Activate( LSUINT32 _ui32Slot ) {
		if ( !m_pd3dtTexture ) { return false; }
		if ( m_ui32LastTextures[_ui32Slot] != m_ui32Id ) {
			m_ui32LastTextures[_ui32Slot] = m_ui32Id;
			if ( FAILED( CDirectX9::GetDirectX9Device()->SetTexture( _ui32Slot, m_pd3dtTexture ) ) ) { return false; }
			CDirectX9::SetMinMagMipFilters( _ui32Slot, m_tftDirectX9FilterType, m_tftDirectX9FilterType, m_bMipMaps ? D3DTEXF_LINEAR : D3DTEXF_NONE );
			CDirectX9::SetAnisotropy( _ui32Slot, m_bMipMaps ? (CFnd::GetMetrics().ui32MaxAniso >> 1UL) : 0UL );
			
			static const D3DTEXTUREADDRESS taAddress[] = {
				D3DTADDRESS_CLAMP,
				D3DTADDRESS_WRAP,
				D3DTADDRESS_MIRROR,
			};
			CDirectX9::SetTextureWrapModes( _ui32Slot, taAddress[m_wmWrap],
				taAddress[m_wmWrap] );
		}
		return true;
	}
	/**
	 * Reset everything related to Direct3D 9 textures.
	 */
	LSVOID LSE_CALL CDirectX9StandardTexture::ResetApi() {
		CDirectX9::SafeRelease( m_pd3dtTexture ); // Sets m_pd3dtTexture to NULL by itself.
	}
	/**
	 * Create a DirectX 9 texture and fill it with our texel data.  Mipmaps are generated if necessary.
	 *
	 * \return Returns tue if the creation and filling of the texture succeeds.  False indicates a resource error.
	 */
	LSBOOL LSE_CALL CDirectX9StandardTexture::CreateApiTexture() {
		CDirectX9::SafeRelease( m_pd3dtTexture );

		[…] LOTS OF SETUP CODE OMITTED.

		// Reroute DXT textures.
		switch ( piUseMe->GetFormat() ) {
			case LSI_PF_DXT1 : {}
			case LSI_PF_DXT2 : {}
			case LSI_PF_DXT3 : {}
			case LSI_PF_DXT4 : {}
			case LSI_PF_DXT5 : {
				return CreateApiTextureDxt( (*piUseMe), ui32Usage, pPool );
			}
		}


		// Create a blank texture.
		HRESULT hRes = CDirectX9::GetDirectX9Device()->CreateTexture( piUseMe->GetWidth(), piUseMe->GetHeight(),
			m_bMipMaps ? 0UL : 1UL,
			ui32Usage,
			d3dfTypes[piUseMe->GetFormat()], pPool,
			&m_pd3dtTexture, NULL );
		if ( FAILED( hRes ) ) {
			return false;
		}
		

		[…] LOTS OF MIPMAP ETC. CODE OMITTED.

		return true;
	}

I'm sorry about the questions, I appreciate very much these replies, but I'd just like to ask the "why"s to understand why something is better than another and such.

If you really want to tackle full and smooth input systems, I have described much of the process here.


what if the ID is the offset into an array of shared texture pointers?

It isn’t bad on performance, but what is the purpose?
This could be just a matter of personal taste so I won’t really say it is wrong etc., but I personally would not go this way, mostly for reasons a bit higher-level than this.

My preference is not to have a global texture manager in the first place. Textures used on models are almost never the same as those used on buildings and terrain, so using a manager to detect and avoid redundant texture loads between these systems separately makes sense, and improves performance (searches will take place on smaller lists) while offering a bit of flexibility.
Each manager is also tuned specifically for that purpose. The model library’s texture manager has just the basic set of features for preventing the same texture from being loaded into memory twice. The building library uses image packages similar to .WAD files (a collection of all the images for that building in one file) and its texture manager is designed to work with those specifically.
Finally, the terrain library’s texture manager is well equipped for streaming textures etc.


Ultimately that is why I prefer to just have a pointer to the texture itself. No limits that way.
But it isn’t a big deal if you want to use indices in an array.


L. Spiro

I restore Nintendo 64 video-game OST’s into HD! https://www.youtube.com/channel/UCCtX_wedtZ5BoyQBXEhnVZw/playlists?view=1&sort=lad&flow=grid

This topic is closed to new replies.

Advertisement