Sign in to follow this  

Game Controls and Logic Question

This topic is 2320 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

I've been messing around with OpenGL/GLUT lately, and there's always been this design problem bugging me since I started creating Windows applications (instead of console) back with SDL. I would think it appropriate to separate the window control stuff from the actual game logic (e.g. separating the code that checks for mouse clicks from the code that actually moves the player to the location that you clicked the mouse). But this has posed a great deal of stress on me. I've yet to figure out a way to do this without using a ton of globals (or by faking globals through using static class singletons). Can someone please post a very small design snippet of an efficient and organized interface or help me out with this problem somehow?

One idea I had was to have all the game data extend a base window class which has all the control information in it like so:
[code]
class Window
{
// stuff for window events and callbacks
// updating key states and mouse states, etc.
};

class Game
{
public:
void Update()
{
// loop through all game objects and
// call their update functions
}

private:
GameObject objs[10]; // create 10 game objects
};

class GameObject : public Window
{
// inherits all of the window traits such
// as the current mouse coordinates, window width/height,
// etc.
};
[/code]

But from a true inheritance viewpoint, it makes absolutely no sense whatever to have a game object inherit window properties. However, what alternative is there?

Share this post


Link to post
Share on other sites
Something like this:
[code]
class MouseListener
{
public:
virtual ~MouseListener(){}
virutal void onClick(int x, int y, MouseButton button, /* ... */) = 0;
};

class Window
{
public:
Window(Mouse *mouse, MouseListener *mouseListener)
:
mouse(mouse),
mouseListener(mouseListener)
{
}

void update()
{
if(mouse->clicked())
{
mouseListener->onClick(mouse->x(), mouse->y(), mouse->button(), /* ... */);
}
}
private:
Mouse *mouse;
MouseListener *mouseListener;
};

class Player
{
public:
void shoot();

// ...
};

class PlayerShootMouseListener : public MouseListener
{
public:
PlayerShootListener(Player *player)
:
player(player)
{
}

virtual void onClick(/*... */)
{
player->shoot();
}
private:
Player *player;
};

int main()
{
Player player;
PlayerShootMouseListener mouseListener(&player);
Mouse mouse;
Window window(&mouse, &mouseListener);

while(true)
{
// input checking
mouse.click(42, 13, MouseButton::Left);

// Later ...
window.update();

// Rendering etc...
}
}
[/code]
As you can see, the GUI knows nothing about game objects and the game object knows nothing about the GUI. If you're using boost then something like boost::function<> can be used to reduce the amount of boilerplate code required.

Share this post


Link to post
Share on other sites
[quote name='rip-off' timestamp='1306695381' post='4817203']
Something like this:
[code]
class MouseListener
{
public:
virtual ~MouseListener(){}
virutal void onClick(int x, int y, MouseButton button, /* ... */) = 0;
};

class Window
{
public:
Window(Mouse *mouse, MouseListener *mouseListener)
:
mouse(mouse),
mouseListener(mouseListener)
{
}

void update()
{
if(mouse->clicked())
{
mouseListener->onClick(mouse->x(), mouse->y(), mouse->button(), /* ... */);
}
}
private:
Mouse *mouse;
MouseListener *mouseListener;
};

class Player
{
public:
void shoot();

// ...
};

class PlayerShootMouseListener : public MouseListener
{
public:
PlayerShootListener(Player *player)
:
player(player)
{
}

virtual void onClick(/*... */)
{
player->shoot();
}
private:
Player *player;
};

int main()
{
Player player;
PlayerShootMouseListener mouseListener(&player);
Mouse mouse;
Window window(&mouse, &mouseListener);

while(true)
{
// input checking
mouse.click(42, 13, MouseButton::Left);

// Later ...
window.update();

// Rendering etc...
}
}
[/code]
As you can see, the GUI knows nothing about game objects and the game object knows nothing about the GUI. If you're using boost then something like boost::function<> can be used to reduce the amount of boilerplate code required.
[/quote]

Lovely example, thank you very much! :)

[b]Edit:[/b] Would it be a good idea to make Window class have a dynamic array of mouse listeners and to say loop through all of them and call their individual click functions when the mouse is clicked? This way I can have multiple mouse listeners. I just wonder if that would effect the performance greatly in some negative way.

Share this post


Link to post
Share on other sites
Relative to your application, mouse clicks are rare. It probably won't matter if you have a whole bucket of mouse listeners.

Generally there are two types of game GUI - the kind where clicking anywhere triggers the same limited set of responses (move/attack/shoot) or a button oriented GUI. In button oriented GUIs, you might want each button to have a listener rather than iterating across every listener.

For hybrid games (e.g. RTS with a button oriented control panel and an action-oriented game map) you might want to divide the screen view port into two separate objects. But for such a game you probably shouldn't be rolling your own GUI library.

Share this post


Link to post
Share on other sites
[quote name='rip-off' timestamp='1306767811' post='4817528']
Relative to your application, mouse clicks are rare. It probably won't matter if you have a whole bucket of mouse listeners.

Generally there are two types of game GUI - the kind where clicking anywhere triggers the same limited set of responses (move/attack/shoot) or a button oriented GUI. In button oriented GUIs, you might want each button to have a listener rather than iterating across every listener.

For hybrid games (e.g. RTS with a button oriented control panel and an action-oriented game map) you might want to divide the screen view port into two separate objects. But for such a game you probably shouldn't be rolling your own GUI library.
[/quote]

Thank you so much. You've been very helpful, and that polymorphism example you've showed me is really going to help with a lot of my future applications.

Share this post


Link to post
Share on other sites
Using polymorphism is a simple way to do it, requiring no additional dependencies. Using boost::function (soon to be std::function) you won't even need to write such interfaces. For stateless callbacks a function pointer can be the simplest solution, but it is also the least flexible.

As an aside, we generally prefer not to mark threads as "solved" on these forums. This is a discussion. For example, someone else may want to weigh in with some more advice, or perhaps a concrete example using boost/std function or might have a question about this design.

Share this post


Link to post
Share on other sites
[quote name='rip-off' timestamp='1306770348' post='4817550']
Using polymorphism is a simple way to do it, requiring no additional dependencies. Using boost::function (soon to be std::function) you won't even need to write such interfaces. For stateless callbacks a function pointer can be the simplest solution, but it is also the least flexible.

As an aside, we generally prefer not to mark threads as "solved" on these forums. This is a discussion. For example, someone else may want to weigh in with some more advice, or perhaps a concrete example using boost/std function or might have a question about this design.
[/quote]

Ah, sorry about that. Didn't know.

Share this post


Link to post
Share on other sites
What I'm about to suggest is most-likely overkill for most hobbyist games. While the listener pattern does work great for this, the logic will still be tied to the specific callbacks. If you wanted to provide support for joysticks or other external controllers your logic would need to be re-factored.

You can completely decouple input from logic by having a central location where window input events are translated into logic input events. For example: you can have events like:

[code]
class PlayerMoveEvent : public LogicEvent {

public:

PlayerMoveEvent(const Vector3& movement);

};

class PlayerShootEvent : public LogicEvent {

public:

PlayerShootEvent(BulletType bulletType, const Vector3& direction);

};
[/code]

These events are dispatched to whatever code handles the logic. This could also be implemented with listeners, however I prefer using events which gives me the option of asynchronous event handling (also my logic is implemented in Lua and events are more natural to implement than polymorphism).

Share this post


Link to post
Share on other sites
[quote name='c_olin' timestamp='1306893055' post='4818120']
What I'm about to suggest is most-likely overkill for most hobbyist games. While the listener pattern does work great for this, the logic will still be tied to the specific callbacks. If you wanted to provide support for joysticks or other external controllers your logic would need to be re-factored.

You can completely decouple input from logic by having a central location where window input events are translated into logic input events. For example: you can have events like:

[code]
class PlayerMoveEvent : public LogicEvent {

public:

PlayerMoveEvent(const Vector3& movement);

};

class PlayerShootEvent : public LogicEvent {

public:

PlayerShootEvent(BulletType bulletType, const Vector3& direction);

};
[/code]

These events are dispatched to whatever code handles the logic. This could also be implemented with listeners, however I prefer using events which gives me the option of asynchronous event handling (also my logic is implemented in Lua and events are more natural to implement than polymorphism).
[/quote]

I think I understand what you're saying. So basically say we want both the keyboard key "W" and the joystick "Up" position to move the player forward. Both the keyboard callback and the joystick callback would translate their respective states to some sort of "Move Forward" event which is stored in the LogicEvent, and then the Player class and such would be specially designed to handle the more simple events from the LogicEvent class, correct?

Share this post


Link to post
Share on other sites
In our game engine, we have separate state classes that hold all game logic for the game.
This means the engine can be reused to almost any other type of game by just replacing those state classes.

These state classes registers themselves as listener to mouse/keyboard input, GUI and network making all decisions in the game.

Besides using listeners, the system is highly decoupled by using interfaces and MVC pattern:

Take a look at our client.cpp file, where everything is set up. Here is the only place which need to know what implementation is used, for example you see that it creates an instance of OgreView but as far as the rest of the system knows, it is just an IView, so if I wanted to change Ogre to something else, this is the only place in the whole engine that needs to be changed. The rest of the system treats happily the class as an IView and if the new implementation follows the same interface, no change is needed in the rest of the system.

[code]
/*--------------------------------------------
* copyright © 2010 Epic Online Entertainment
*/

#include <iostream>

#include "IModel.h"
#include "AbstractModel.h"
#include "IGeomReader.h"
#include "OgreGeomReader.h"

#include "IView.h"
#include "OgreView.h"

#include "IGUISystem.h"
#include "MyGUISystem.h"

#include "IControllerInput.h"
#include "OISController.h"
#include "INetworkController.h"
#include "SDLNetController.h"

#include "StateManager.h"
/*
#include "ISoundSystem.h"
#include "SDLSoundSystem.h"
*/

using namespace Abydos;
using namespace std;

#ifdef WIN32
int WINAPI WinMain (HINSTANCE hThisInstance, HINSTANCE hPrevInstance,
LPSTR lpszArgument, int nCmdShow)
#else
int main(int argc, char **argv)
#endif
{
IGeomReader *geomReader = new OgreGeomReader();
IModel *model = new AbstractModel(geomReader);
IView *view = new OgreView(model);

IControllerInput *input = new OISController(view);
INetworkController *net = new SDLNetController();

IGUISystem *gui = new MyGUISystem(view);

StateManager *stateManager = new StateManager();
stateManager->init(model, gui, net, input, view);
stateManager->switchState("Login");

/*
ISoundSystem *sdlSound = new SDLSoundSystem(model);
*/
bool runGame = true;
while(runGame) {
model->update();
input->update();
net->update();
view->update();
runGame = stateManager->update();
}
delete geomReader;
delete model;
delete view;
delete input;
delete net;
delete gui;
delete stateManager;
return 0;
}[/code]

Here is a snipped of our state login state class:

LoginState.h

[code]
/*--------------------------------------------
* copyright © 2010 Epic Online Entertainment
*/

#ifndef ABYDOS_LOGIN_STATE_H
#define ABYDOS_LOGIN_STATE_H

#include <string>

#include "State.h"
#include "StateManager.h"
#include "IView.h"
#include "IModel.h"
#include "IView.h"
#include "IGUISystem.h"
#include "GUISystemListener.h"
#include "INetworkController.h"
#include "NetworkListener.h"
#include "byte.h"

namespace Abydos {

class LoginState: public State, GUISystemListener, NetworkListener,
InputListener
{
public:
LoginState(IModel *model, IGUISystem *gui,
INetworkController *net, IControllerInput *input,
StateManager *stateManager, IView *view);
~LoginState();

void serverMessage(unsigned int eventId, byte *data);
void widgetClicked(std::string widgetName);
void keyPress(int key, char text);
void keyRelease(int key, char text);
void mouseMove(float absX, float absY, float absZ,
float relX, float relY, float relZ);
void mousePress(float x, float y, int button);
void mouseRelease(float x, float y, int button);
bool update();

private:
bool mRunGame;
IModel *mModel;
IGUISystem *mGui;
INetworkController *mNet;
IControllerInput *mInput;
StateManager *mStateManager;
IView *mView; // Not used in this state.
};

}

#endif[/code]


LoginState.cpp

[code]
/*--------------------------------------------
* copyright © 2010 Epic Online Entertainment
*/

#include "LoginState.h"

#include <iostream>
#include <assert.h>
#include <cstdlib>
#include <cstring>

#include "Connection.h"
#include "Property.h"

//#include "crypto.h"
#include "protocol.h"
#include "byte.h"
#include "debug.h"
#include "mixed.h"

#define PORT 2000

#define LOGIN_BUTTON "Login/LoginButton"
#define ACCOUNT_NAME "Login/AccountName"
#define ACCOUNT_PASSWORD "Login/AccountPassword"
#define SIGNUP_BUTTON "Login/Signup"
#define QUIT_BUTTON "Login/Quit"
#define SERVER_LIST "Login/Server"
#define MODEL_CHECK_BUTTON "Login/ModelCheck"
#define THEME_CHECK_BUTTON "Login/ThemeCheck"

using namespace Abydos;
using namespace std;

LoginState::LoginState(IModel *model, IGUISystem *gui,
INetworkController *net, IControllerInput *input,
StateManager *stateManager, IView *view)
: State (model, gui, net, input, stateManager, view)
{
mModel = model;
mGui = gui;
mNet = net;
mStateManager = stateManager;
mView = view;
mInput = input;

mGui->registerListener(this);
mNet->registerListener(this);
mInput->registerListener(this);

mModel->setProperty("gui", "background",
Property("login640x480.jpg"));
mGui->loadLayout("AbydosLogin.layout");

mGui->registerWidget(QUIT_BUTTON);
mGui->registerWidget(SIGNUP_BUTTON);
mGui->registerWidget(LOGIN_BUTTON);
mGui->registerWidget(MODEL_CHECK_BUTTON);
mGui->registerWidget(THEME_CHECK_BUTTON);

mGui->enableWidgetPassword(ACCOUNT_PASSWORD, true);
//SoundSystem::getSingleton()->play("stars.mp3");

mRunGame = true;
}

LoginState::~LoginState()
{
mGui->unregisterWidget(QUIT_BUTTON);
mGui->unregisterWidget(SIGNUP_BUTTON);
mGui->unregisterWidget(LOGIN_BUTTON);
mGui->unregisterWidget(MODEL_CHECK_BUTTON);
mGui->unregisterWidget(THEME_CHECK_BUTTON);

mGui->unloadLayout("AbydosLogin.layout");
mGui->unregisterListener(this);
mNet->unregisterListener(this);
mInput->unregisterListener(this);
}

bool LoginState::update()
{
return mRunGame;
}

void LoginState::serverMessage(unsigned int eventId, byte *data) {
switch(eventId) {
case CONN_RESET:
cout << "LoginState: CONN_RESET" << endl;
break;

case CONN_ESTABLISHED:
cout << "LoginState: CONN_ESTABLISHED " << endl;
break;

case ACCOUNT_LOGIN_SUCCESS:
debug("LoginState: ACCOUNT_LOGIN_SUCCESS");
mStateManager->switchState("Lobby");
break;

case ACCOUNT_LOGIN_FAIL:
debug("LoginState: ACCOUNT_LOGIN_FAIL");
break;

default:
cout << "Unknown event: " << eventId << endl;
}
}

void LoginState::widgetClicked(string widgetName)
{
cout << "LoginState::widgetClicked" << widgetName << endl;
if(widgetName == QUIT_BUTTON)
mRunGame = false;

if(widgetName == LOGIN_BUTTON) {
string account = mGui->getWidgetText(ACCOUNT_NAME, "Edit");
string password = mGui->getWidgetText(ACCOUNT_PASSWORD, "Edit");
Connection *conn = Connection::getSingleton();
if(!conn->isConnected())
conn->connect("server.next-gen.cc", 2000);

Connection::getSingleton()->protocolSend(
"css", ACCOUNT_LOGIN, account.c_str(), password.c_str());
}

if(widgetName == SIGNUP_BUTTON) {
assert(mStateManager);
mStateManager->switchState("Signup");
}

if(widgetName == MODEL_CHECK_BUTTON) {
mStateManager->switchState("ModelCheck");
}

if(widgetName == THEME_CHECK_BUTTON) {
mStateManager->switchState("ThemeCheck");
}

State::widgetClicked(widgetName);
}

void LoginState::keyPress(int key, char text)
{
mGui->keyPress(key, text);
State::keyPress(key, text);
}

void LoginState::keyRelease(int key, char text)
{
mGui->keyRelease(key, text);
State::keyRelease(key, text);
}

void LoginState::mouseMove(float absX, float absY, float absZ,
float relX, float relY, float relZ)
{
mGui->mouseMove(absX, absY, absZ, relX, relY, relZ);
State::mouseMove(absX, absY, absZ, relX, relY, relZ);
}

void LoginState::mousePress(float x, float y, int button)
{
mGui->mousePress(x, y, button);
State::mousePress(x, y, button);
}

void LoginState::mouseRelease(float x, float y, int button)
{
mGui->mouseRelease(x, y, button);
State::mouseRelease(x, y, button);
}[/code]


When the game enters the playing state, if starts to feed data into the model, which the view uses to draw, decoupling the data from the logic and view gives a really nice architecture, the side effects of a good architecture like this is that you can easily change screen resolution by deleting the view and start a new one (since the data the view draws, in our case 3D but could also extended to GUI) since all data is stored in the model.
In most cases, MVC is used in a system were you draw GUI, not 3D, however in our case it is quite the reverse, it is something we will adress in the soon future.

Share this post


Link to post
Share on other sites
[quote name='Master Jake' timestamp='1306688064' post='4817153']
I would think it appropriate to separate the window control stuff from the actual game logic (e.g. separating the code that checks for mouse clicks from the code that actually moves the player to the location that you clicked the mouse). [/quote]

Separating logic out is an important part of good software engineering. It's great you picked up on that need.

Of course, usually the way it works is you start off writing a Horrible Mess, then later (but not too much later) you go through and separate it out. If you're inexperienced, you probably want to start this way. Worrying about architecture before you can "play" your first game is usually a Bad Thing.



[quote]I've yet to figure out a way to do this without using a ton of globals (or by faking globals through using static class singletons).[/quote]

One thing I didn't understand for a long time was why you should avoid globals. All programs are taught that globals and singletons are a sin against mankind, but the actual [i]lesson[/i] to be learned is omitted from the lecture. My advice: use globals and/or singletons where you feel you need to. There will come a point where one of two things will happen:

1) You will forget where something is declared, or
2) You will want two instances of something that there is (by definition) only one copy of.

Both can be fixed. How hard it is to fix it depends on how proliferated the problem is in your code.

One important lesson often omitted is that EVERY application eventually references a singleton. You eventually want to reference THE screen, THE database, THE network, THE sound subsystem, etc. It's helpful to have those things loosely coupled for testing (talking to a mock database, for example), but in an actual application, there's only going to be one of each of these things in existence. Ever. So, it's perfectly fine to make them singletons.

Keep in mind the cost of making something local (as opposed to global) is that you are responsible for keeping track of it. You always either have to pass a reference to it or some object which keeps a reference to it. This kind of plumbing can be its own nightmare to manage.

In terms of events, what you probably want is two layers of events. You have UI events (mouse clicks and keyboard inputs) and you have game events (move commands, attack commands, etc). Then, at the top level of your program, your job is to translate UI events into game events and pass them to your game's event loop. From the event loop's perspective, all it does is expose one callback ("sendEvent"), and other than that, all it knows about are events. The UI, on the other hand, knows how to create a game event and how to send it (using "sendEvent"), but it doesn't know what the events mean past what creates them.

So as far as defining a class hierarchy, if it were me, I'd create a singleton game event loop, give it a sendEvent() method, then have the UI callbacks (onMouseClick() or whatever) create game events and send them via GameLoop::singleton()->sendEvent(...). There's no real need for inheritance here. (Though I'm sure you could find another good architecture that uses it well).

If you have a setup like this, and you need to swap out the UI component (say you're porting your game to a console system or a smartphone platform), you just need to rewrite the UI with whatever toolkit is at your disposal (provided by iOS or PS SDK or whatever). As long as you keep most of the game logic in the event loop, you don't have to rewrite very much at all.

Share this post


Link to post
Share on other sites

This topic is 2320 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this