Skip to main content
GameDev.net gamedev.net
🔒 Locked

Are events for gameplay logic?

Started by d34dl0ck3d Nov 27, 2025 at 3:55 PM 5 replies 1.3k views
Original Post
d34dl0ck3d
d34dl0ck3d

I'm writing a C++ game engine, and I have events such as KeyboardEvent, MouseEvent, and WindowEvent. However, after reading some of "Game Coding Complete, 4th Edition" by Mike McShaffry (which I know is a bit dated), its event system focuses on things like ObjectMoved, ObjectCreated, ObjectDestroyed, and specific actions like GuardPickedNose, meanwhile input is handled through callbacks/forwarding/delegation (I'm unsure what the correct terminology is) and it's not just this book though the event chapter on gameprogrammingpatterns.com also discusses using events for things like tutorials and combat. So it does have me thinking maybe I am using events for the wrong things? is there a reason why you wouldn't want these things as events?

For a bit of context, here is the code from the Game Coding Complete GitHub. The first block of code is how they handle input, and the second block of code is the event system.

class IKeyboardHandler
{
public:
	virtual bool VOnKeyDown(const BYTE c)=0;
	virtual bool VOnKeyUp(const BYTE c)=0;
};

DXUTSetCallbackMsgProc( GameCodeApp::MsgProc );

LRESULT CALLBACK GameCodeApp::MsgProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, bool* pbNoFurtherProcessing, void* pUserContext )
{
	switch (uMsg) 
	{
		case WM_SYSKEYDOWN:
		{
			if (wParam == VK_RETURN)
			{
				*pbNoFurtherProcessing = true;
				return g_pApp->OnAltEnter();
			}
			return DefWindowProc(hWnd, uMsg, wParam, lParam);
		}


		case WM_CLOSE:
		{
			if (g_pApp->m_bQuitting)
			{
				result = g_pApp->OnClose();
			}
			else
			{
				*pbNoFurtherProcessing = true;
			}
			break;
		}


		case WM_KEYDOWN:
        case WM_KEYUP:
		case WM_CHAR:
		case WM_MOUSEMOVE:
		case WM_LBUTTONDOWN:
		case WM_LBUTTONUP:
		case WM_RBUTTONDOWN:
		case WM_RBUTTONUP:
		case MM_JOY1BUTTONDOWN:
		case MM_JOY1BUTTONUP:
		case MM_JOY1MOVE:
		case MM_JOY1ZMOVE:
		case MM_JOY2BUTTONDOWN:
		case MM_JOY2BUTTONUP:
		case MM_JOY2MOVE:
		case MM_JOY2ZMOVE:
		{
			if (g_pApp->m_pGame)
			{
				BaseGameLogic *pGame = g_pApp->m_pGame;
				AppMsg msg;
				msg.m_hWnd = hWnd;
				msg.m_uMsg = uMsg;
				msg.m_wParam = wParam;
				msg.m_lParam = lParam;
				for(GameViewList::reverse_iterator i=pGame->m_gameViews.rbegin(); i!=pGame->m_gameViews.rend(); ++i)
				{
					if ( (*i)->VOnMsgProc( msg ) )
					{
						result = true;
						break;	
					}
				}
			}
			break;
		}
	}

	return result;
}
class EvtData_New_Actor : public BaseEventData
{
	ActorId m_actorId;
    GameViewId m_viewId;

public:
	static const EventType sk_EventType;

	EvtData_New_Actor(void) 
	{
		m_actorId = INVALID_ACTOR_ID;
		m_viewId = gc_InvalidGameViewId;
	}

    explicit EvtData_New_Actor(ActorId actorId, GameViewId viewId = gc_InvalidGameViewId) 
        : m_actorId(actorId),
          m_viewId(viewId)
	{
	}

    virtual void VDeserialize(std::istrstream& in)
    {
        in >> m_actorId;
		in >> m_viewId;
    }

	virtual const EventType& VGetEventType(void) const
	{
		return sk_EventType;
	}

	virtual IEventDataPtr VCopy(void) const
	{
		return IEventDataPtr(GCC_NEW EvtData_New_Actor(m_actorId, m_viewId));
	}

	virtual void VSerialize(std::ostrstream& out) const
	{
		out << m_actorId << " ";
		out << m_viewId << " ";
	}


    virtual const char* GetName(void) const
    {
        return "EvtData_New_Actor";
    }

	const ActorId GetActorId(void) const
	{
		return m_actorId;
	}

    GameViewId GetViewId(void) const
    {
        return m_viewId;
    }
};


class IEventManager
{
public:

	enum eConstants { kINFINITE = 0xffffffff };

	explicit IEventManager(const char* pName, bool setAsGlobal);
	virtual ~IEventManager(void);

    virtual bool VAddListener(const EventListenerDelegate& eventDelegate, const EventType& type) = 0;

	virtual bool VRemoveListener(const EventListenerDelegate& eventDelegate, const EventType& type) = 0;

	virtual bool VTriggerEvent(const IEventDataPtr& pEvent) const = 0;

	virtual bool VQueueEvent(const IEventDataPtr& pEvent) = 0;
	virtual bool VThreadSafeQueueEvent(const IEventDataPtr& pEvent) = 0;

	virtual bool VAbortEvent(const EventType& type, bool allOfType = false) = 0;

	virtual bool VUpdate(unsigned long maxMillis = kINFINITE) = 0;

	static IEventManager* Get(void);

};
None
TooOld2rock-nRoll
TooOld2rock-nRoll

On those situations, it's critical to remind, there is no single BEST solution.

It will always be dependent on why you would want things one way or another.

Maybe you decided on the architecture side of planing that the system event manager and the game event manager should be separated, avoiding to overcrowd and bottleneck a single program touch point (and making switch/cases half the size :D). This would also permit the program subroutines that don't care fore system events to just filter in game events and vice versa.

Perhaps your system events are best dealt on demand (once every 500ms or such), wile the game events are treated as same time priority.

I like working with registered listeners for events, setup a thread to filter what is expected by the listeners and discarding what is irrelevant. Putting everything on dedicated fifo structures and let them serve themselves on demand every game loop. Just to be sure, I also let them register callbacks for high priority events, those will be sent directly by the filter as they come in (very useful for sound events that DEPEND on causality with visual events).

Study more design patterns and you will see everything is just variations on proxy algorithms, may I suggest https://gameprogrammingpatterns.com/

"No, you're never too old to rock and roll
If you're too young to die"
RmbRT
RmbRT

Handling user input is not the same as events that occur within the game. User input gets polled at least once per frame, but sometimes also multiple times per frame (for example in a different thread that performs the game world simulation).

Things that happen in the game world might either be modeled as a signal that you can register a callback for, or as a message that gets passed around like a packet, and then polled by the recipient (instead of the emitter executing the recipient's logic directly). You can also use the inverse model and use state machines and replace events with state transitions.

It really depends on what exactly it is you need the computer to do. If you can break down exactly what should happen in terms of pseudo machine operations and data accesses, then a high-level language solution should naturally arise from that. And try to not make a generic system, but something that addresses only the specific problem you actually have, and leverage all the knowledge you have about this specific problem. For example, is this event only consumed by a single listener? Or does the handling of that event always have the same logic? Does it have multiple emitters? In what situational context does this event even occur? Etc.

Look into data oriented design, then all the other questions about style become trivial, because then you start to think about how to instruct the machine to do a specific thing in the most efficient way, instead of worrying about idioms and patterns and all that. There's a free book on data oriented design, and there are also some great talks about it on youtube. How you instruct the machine should not be a matter of style, but a matter of performance considerations. Different things have vastly different performance on a computer. For example a memory access can easily take dozens of cycles to complete, but you can issue around 10 memory accesses in parallel. Virtual function calls are not just slow because of the pointer load, but because they prevent speculative/ahead of time execution. If you have many events that get regularly issued, it is best to execute a loop of homogeneous logic for a single event type on multiple data points, and then have one loop per event type, rather than one big loop that goes over all events of all kinds and executes heterogeneous logic within the loop. This video demonstrates the data-oriented design mindset nicely:

Walk with God.
LorenzoGatti
LorenzoGatti

There can be events and polling both on the input side (e.g. key pressed or released messages vs. polling currently pressed keys) and on the game logic side (e.g. queues of commands, possibly with nontrivial canceling and reordering logic, vs. querying what is the desired movement direction this frame).

Separating them (and isolating complications like remapping inputs or recording abstract commands for replays), converting cleanly, and keeping open to reorganizations and alternatives (e.g. compatible movement commands from analog thumbsticks, keyboard arrows and digital joysticks and gamepads) is in your best interest.

Omae Wa Mou Shindeiru
TooOld2rock-nRoll
TooOld2rock-nRoll

@LorenzoGatti I, painfully, implemented a three way api to exactly keep ALL the options available.

All HID have dedicated interfaces that can be accessed directly.

Keeping all the HID eventful and part of the event manager where listeners can register for specific events.

And the player api makes the input mapping agnostic from any existing hid event to the playable character api.

I really don´t like how complicated this all ended up, following and debugging events around is quit a pain, so perhaps you are right.

But! If the algorithm do its job correctly, it made things so fucking simple at the user layer that it's almost like “just click to map input”.

I have the luxury of limiting my input array of choices though, in the end, the character api only understand “PS3 controller” like input keys.

"No, you're never too old to rock and roll
If you're too young to die"
Alberth
Alberth

d34dl0ck3d said:
its event system focuses on things like ObjectMoved, ObjectCreated, ObjectDestroyed, and specific actions like GuardPickedNose, meanwhile input is handled through callbacks/forwarding/delegation (I'm unsure what the correct terminology is) and it's not just this book though the event chapter on gameprogrammingpatterns.com also discusses using events for things like tutorials and combat. So it does have me thinking maybe I am using events for the wrong things? is there a reason why you wouldn't want these things as events?

Game events solve a communication problem in your game. If you need to have the game send an ambulance for rescuing the guard that picked their nose, then in some way the event of the guard picking their nose needs to be communicated to the ambulance service so they can dispatch the ambulance. Of course you can have many such triggers, and if you go out and try find them, you'll likely find a lot of them. So, if you have an event system, it will likely be used a lot.

I don't have much experience with events in game, but I guess you'd make a generic game event processing system, you connect all game entities to it, and he presto, you've got a highway for exchanging random information between different parts of the game. This is the attractive side of such a system, the event system operates sort-of independent of the other game logic.

As with all additions, it's not for free. It takes additional processing power to operate. Also a flaw can be that it isn't very smart in delivering events to entities. For example, if you just deliver each event message to all entities that want it, your code will do a lot random access to many different entities. That will trash your CPU caches and cost performance. In other words, the game event system is easy to design and to use, but can be very costly. In a sense, it's a bit too powerful and too simple.

An alternative is to handle such communication as part of the normal game/update logic. That probably requires more thought about the communication patterns and how to handle them, but it gives more control on entity access, and can avoid the “jumping around” behavior of delivering events to entities. What makes sense in your case depends on the complexity of the game and the frequency of game events.

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.