The structure of my game logic

Started by
12 comments, last by L. Spiro 12 years, 6 months ago
I'm just starting on the framework of my game and I'm not really sure if I'm moving in the right direction. I want my code to be API and platform agnostic as I can. I'm currently developing on Linux using OpenGL and plan to develop for Windows using OpenGL as well, though I want to allow the potential for other APIs like DirectX. My starting point is window and context creation.

Here is what I have so far:

[source lang=cpp]
#ifndef PLANCK_RENDERSYSTEM_H
#define PLANCK_RENDERSYSTEM_H

#include <string>
#include <memory>

namespace Planck {

class Window;

class RenderSystem
{
public:
class Context {};
virtual Planck::Window* createWindow(const std::string& title, int width, int height) = 0;
static Planck::RenderSystem* Create(const std::string& name);
};

class Window
{
public:
virtual Planck::RenderSystem::Context* getContext() = 0;
};

}

#endif /* PLANCK_RENDERSYSTEM_H */
[/source]

[source lang=cpp]
#include <string>
#include "RenderSystem.h"

Planck::RenderSystem* CreateGLXRenderSystem();
Planck::RenderSystem* CreateWinGLRenderSystem();

Planck::RenderSystem* Planck::RenderSystem::Create(const std::string& name)
{
if(name == "OpenGL") {
#if defined __unix__
return CreateGLXRenderSystem();
#elif defined _WIN32
return CreateWinGLRenderSystem();
#else
return NULL;
#endif
}
else {
return NULL;
}
}
[/source]

[source lang=cpp]
#include <string>
#include <GL/gl.h>
#include <GL/glx.h>
#include "RenderSystem.h"

namespace {

class GLXWindow : public Planck::Window
{
public:
GLXWindow(const std::string& title, int width, int height)
{}
Planck::RenderSystem::Context* getContext()
{
return context;
}
private:
Planck::RenderSystem::Context* context;
};

class GLXRenderSystem : public Planck::RenderSystem
{
public:
Planck::Window* createWindow(const std::string& title, int width, int height)
{
return new GLXWindow(title, width, height);
}
private:

};

}

Planck::RenderSystem* CreateGLXRenderSystem()
{
return new GLXRenderSystem;
}
[/source]

[source lang=cpp]
//example
using namespace Planck;
int main()
{
RenderSystem* renderSystem = RenderSystem::Create("OpenGL");
if(!renderSystem)
{
std::cerr << "Error: Could not create render system." << std::endl;
return EXIT_FAILURE;
}
Window* mainWnd = renderSystem->createWindow("OpenGL Example", 800, 600);
RenderSystem::Context* context = mainWnd->getContext();
}
[/source]
Advertisement
First, what's your question? Code review?

Second, unless you are sure you had enough experience, such like developed several complicated games on both Windows and Linux, I would not suggest to write your framework or library directly.
Instead if I were you I would develop a real game, and then extract the independent code from the game to a library, then wrap to cross platform.

Developing a framework without real application, you will found your framewok can't be used in real app after you finish it.

https://www.kbasm.com -- My personal website

https://github.com/wqking/eventpp  eventpp -- C++ library for event dispatcher and callback list

https://github.com/cpgf/cpgf  cpgf library -- free C++ open source library for reflection, serialization, script binding, callbacks, and meta data for OpenGL Box2D, SFML and Irrlicht.

At this point there is nothing that strikes me as the wrong direction particularly, but you do need a stronger focus on organization.

If you want to support many platforms, you need an organization more like what I explained here.
Don’t clutter your code with a bunch of #ifdef’s.

You also need comments.
Every function should be commented. I use Doxygen style because my work needs actual documentation. I recommend you treat your work just as seriously, but if you must be lazy you should still at least use some form of per-function commenting.

One thing I would not do is to select a graphics mode by name.
In fact you don’t need to support OpenGL and Direct3D within the same build. Games haven’t allowed dynamic switching between them for years. Make one build for OpenGL and one build for Direct3D 9. And one for Direct3D 11 later.
Even if you do have a mode selection routine, use enumerations instead of names. Names are easy to misspell and consume too much space. Enumerate things you want to support.



One of the things I mentioned here is never to declare and define a function at the same time (except for templates). Don’t put function definitions inside your classes. Put them at the bottom of your header file, or tucked away inside a .inc.



As for actual game organization, make one or two master functions for starting up the engine, and one for shutting it down.
I use two. First you initialize the memory manager, then create your game class and set its state factory and other things to prepare it for your game, then call a secondary initializer that tells the engine it is ready to create the window, start the sound device, etc.

This is an example (ignore my explicit use of lse::, I like to be very specific about what classes/objects/types I am referencing):


INT APIENTRY wWinMain( HINSTANCE _hInstance, HINSTANCE _hPrevInstance, LPWSTR _lpCmdLine, INT _iCmdShow ) {
static_cast<HINSTANCE>(_hInstance);
static_cast<HINSTANCE>(_hPrevInstance);
static_cast<LPWSTR>(_lpCmdLine);
static_cast<INT>(_iCmdShow);

lse::CEngine::LSE_ENGINE_INIT eiInit = {
64 * 1024 * 1024,
true
};
lse::CEngine::InitEngine( eiInit );

INT iRet;

{ // Scope the game class so that it is destroyed before lse::CEngine::DestroyEngine() is called.
// We do not need to make a custom game class for this primitive demo. We would want to make a custom game class
// that inherits from lse::CGame if we wanted to handle any game data not handled by lse::CGame. Any real game
// will have a custom game class.
lse::CGame gGame;

gGame.SetStateFactory( LSENEW ::CStateFactory() );

// Before running the "game" we need to tell it where to begin (which state).
gGame.SetNextState( LST_S_MODELTEST, 0, true );

// After creating a game class, we can finish initializing the engine.
lse::CEngine::LSE_ENGINE_SECONDARY_INIT esiSecondInit = {
&gGame,
800UL, 600UL,
0UL, 0UL,
"L. Spiro Engine",
false
};
lse::CEngine::SecondaryInit( esiSecondInit );


// From here, simply run the game. It will handle the ticking of the game object and passing window messages
// to it so it can handle input, etc.
iRet = lse::CEngine::Run();

}

// The game class and window have been destroyed by this point. Shut down the rest of the engine.
lse::CEngine::DestroyEngine();
return iRet;
}



The flow is simple.
The engine provides a base class called CState. When you make your game, your state classes inherit from that class, and you give the game class a state factory that can generate your custom states (by enumeration).
When you call Run(), the engine will execute the current state by calling its Tick() and Draw() functions as needed, which allows your custom game to handle its own logic and to perform its rendering.
When you want to move on to another part of the game, like going from the main menu to the options menu, to the gameplay screen, just call SetNextState() and the engine will go to that state on the next cycle.

You can set up your system how you like, but this is very modular and helps break the flow of your game down into manageable blocks.


I don’t see anything wrong with your current implementation as far as overall game construction goes. The hierarchy of your classes makes sense so far. Mainly you could just use some organization.


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


lse::CEngine::LSE_ENGINE_INIT eiInit = {
64 * 1024 * 1024,
true
};


lse::CEngine::LSE_ENGINE_SECONDARY_INIT esiSecondInit = {
&gGame,
800UL, 600UL,
0UL, 0UL,
"L. Spiro Engine",
false
};



Regarding comments. To me, these pieces of code needs more comments then anything else, yet is has none. All the other functions you call in your example main are relatively self explanatory. I don't get it...
That is true. I should comment those initializers more than I did.
I have Doxygen documentation which explains what those calls do, and I guess I figured that was enough, but in actuality there is not such a thing as “enough” commenting (within limits of sanity).

The first one just sets up the memory manager, telling it how big to make the initial heap and the “true” indicates that the heap is growable.
The second one specifies the game class, windowed resolution, full-screen resolution (0 = desktop resolution), window title, and whether or not it is full-screen or windowed. False = windowed.


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 myself, YogurtEmperor. What is the purpose of this:
[source lang="cpp"]
static_cast<HINSTANCE>(_hInstance);
static_cast<HINSTANCE>(_hPrevInstance);
static_cast<LPWSTR>(_lpCmdLine);
static_cast<INT>(_iCmdShow);
[/source]It looks like you're casting the parameters to the types to which they're already defined in the parameter list, and then not really doing anything with them...?
In Microsoft Visual Studio I compile with warning level 4 and fix all warnings so that my products never produce errors or warnings when built.
This is one way to get rid of the “unused parameter” warning.


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 is one way to get rid of the “unused parameter” warning.


You can also leave off the parameter names:

INT APIENTRY wWinMain( HINSTANCE /*_hInstance*/, HINSTANCE /*_hPrevInstance*/, LPWSTR /*_lpCmdLin */, INT /*_iCmdShow*/ ) {
// static_cast<HINSTANCE>(_hInstance);
// static_cast<HINSTANCE>(_hPrevInstance);
// static_cast<LPWSTR>(_lpCmdLine);
// static_cast<INT>(_iCmdShow);

// .
// .
// .
}
I’m going to switch to that, because my way still leaves a warning on the strictest levels of LLVM regarding no-side-effect statements.


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

Ah. Thank you for the explanation. ;)

This topic is closed to new replies.

Advertisement