Quick Engine Design Question
Hey all,
This question is easier than my last one (I hope!); I think I already know the correct answer, but I wanted to know what you guys think.
My engine has multiple subsystems: Renderer, Object Management, Network, etc. These all communicate between one another in the engine proper and to the game layer through an event manager. This event manager routes events (essentially the same thing as messages) to the proper subsystem or layer based on what's specified in the event object. The event manager maintains two stacks: an outStack and an inStack, for events to be sent out to the game layer, and recieved back in for routing to the engine's subsystems (I got this idea from an article in Game Programming Gems 4 where the engine was a message-based system; can't remember the article name, however).
The problem I'm running into is this: is it more efficient to have each subsystem and the game layer poll the event manager's stacks for events that relate to them, or is it best for the event manager to physically route these messages to the proper subsystem or game layer? It's easier to poll the stacks, but that implies a *lot* of looping through the stacks. I was thinking about implementing a stack for each subsystem and layer specific to events relating to them, but I'm not sure if that's the best solution either, since these stacks might balloon in size for something like a keypress event (which is triggered until the key is released; depending on the OS, this could be many events per second). This is a networked game, and graphically intensive, so I'm trying to make it as efficient as possible so more time can be spent in critical areas.
I really appreciate everyone's help so far. This is my first major engine-style project, and I want to do it right for reusability and easy modification later on.
Thanks,
Aviosity
The design that I've decided on is Basically the way you have it now. A Scheduler send events out to each subsystem when the event comes through the queue. The one big difference is that I don't accept incoming messages, I really don't know why you would need to either. The advantage to this solution is that the scheduler can choose how to pass the event, be it another thread or on the same thread, etc... It's a fairly simple concept for moving an engine to a multi-core platform. There are still some issues with it relating to synchronization, but those designs are fairly simple as well.
Hey jklein,
The reason I need to both send and recieve events is because the engine needs to report events and decide what to do with them (for example, if a 'w' key is pressed, an event needs to be sent to the game layer to determine which action to take), and to send the proper response back (since the 'w' key was pressed, move Player X forward x units). The reason this communication needs to be two-way is because I'm trying to keep the engine as separate from the game layer as possible to facilitate engine reuse. Am I going about this incorrectly?
Thanks,
Aviosity
The reason I need to both send and recieve events is because the engine needs to report events and decide what to do with them (for example, if a 'w' key is pressed, an event needs to be sent to the game layer to determine which action to take), and to send the proper response back (since the 'w' key was pressed, move Player X forward x units). The reason this communication needs to be two-way is because I'm trying to keep the engine as separate from the game layer as possible to facilitate engine reuse. Am I going about this incorrectly?
Thanks,
Aviosity
This doesn't answer your question, but I suggest working less on getting a generic engine working than you do getting your game working. By the time you finish part of your engine, you'll come up with some better/more elegant solution, and it will never end.
We use one way communication. Why does the input manager care what happens when w is hit. Fire off an event to the registered listener (our case it's a function pointer, or how ever many are registered). If a message needs to be sent back to something another event can be generated. One function pointer can easily handle multiple messages too (like the Windows Message Procedure does). I just found 2 way too complicated for our setup (makes multiprocessing a bit harder because of timing and syncs etc). Least I thought it would. I also couldn't find any reason to truely have 2 way that my above solution doesn't handle. Your engine may differ.
This may just be a matter of terminology, but you should only need "one-way" communication. You'll end up with some set of events, and for those events, zero or more listeners. If an incoming event generates some response, that response can be specified by posting another event or events.
There are a bunch of choices about how complicated you want to make event routing - for example, you can have listener chains (the Player object might register for keypresses, but only care about some keys, and tell the event router to pass along the 'Escape' key that it doesn't care about to the next listener in the chain, which could be a menu manager that does something in response).
You can also make event listening an integral part of the game itself. For example, a bullet shot into the world in injected into the physics/collision layer (by the gun generating a bullet event, and the physics/collision layer responding by doing a query into the world). If the collision layer determines that the bullet hit something, then an event is sent to that object. If the object is destructable, it registers the bullet hit, and decreases its hit points. If the hit points go to zero, and the thing is explosive, it blows itself up - and sends out an "explosion" event to things within a certain radius. The debris that's next to the explosive thing then kicks itself in the blast direction. Etc.
Polymorphism is your friend here, and sending an event is really just a deferred function call that may or may not actually bind to an object or objects.
Getting back to the one way vs. two way thing - if the receiver of an event wants to tell the sender something, then it can send an event to the sender. [You do need to be wary of object lifetimes in this whole thing, but that's a chapter unto itself.] This also means that polling is entirely irrelevant: The event dispatcher just keeps dispatching events to registered listeners, which will generate more events, which will keep getting dispatched, etc. Of course, you'll also need to make sure that you don't end up with any "infinite loops" in terms of never-ending event/response sets.
Cheers,
Jason
There are a bunch of choices about how complicated you want to make event routing - for example, you can have listener chains (the Player object might register for keypresses, but only care about some keys, and tell the event router to pass along the 'Escape' key that it doesn't care about to the next listener in the chain, which could be a menu manager that does something in response).
You can also make event listening an integral part of the game itself. For example, a bullet shot into the world in injected into the physics/collision layer (by the gun generating a bullet event, and the physics/collision layer responding by doing a query into the world). If the collision layer determines that the bullet hit something, then an event is sent to that object. If the object is destructable, it registers the bullet hit, and decreases its hit points. If the hit points go to zero, and the thing is explosive, it blows itself up - and sends out an "explosion" event to things within a certain radius. The debris that's next to the explosive thing then kicks itself in the blast direction. Etc.
Polymorphism is your friend here, and sending an event is really just a deferred function call that may or may not actually bind to an object or objects.
Getting back to the one way vs. two way thing - if the receiver of an event wants to tell the sender something, then it can send an event to the sender. [You do need to be wary of object lifetimes in this whole thing, but that's a chapter unto itself.] This also means that polling is entirely irrelevant: The event dispatcher just keeps dispatching events to registered listeners, which will generate more events, which will keep getting dispatched, etc. Of course, you'll also need to make sure that you don't end up with any "infinite loops" in terms of never-ending event/response sets.
Cheers,
Jason
Hey guys,
You're all great. It makes sense that an event generated by the 'w' key being pressed doesn't mean the event system should fire another event back to say what to do; it should just do it. Another step forward would probably be the listener pattern, where each subsystem listens for an event fire (I assume by polling the event stack, or having the event fired directly at the subsystem) and handling it internally.
Thanks so much...I have a feeling that this engine will be simple enough for most people to understand (this is a school-oriented project that will be worked on by multiple people, so simplicity and efficiency are goals one and two).
Dan - the reason I'm not focusing on the game *yet* is that this is supposed to be a professional-level game built by students and amateurs, with many mistakes along the way. I'm trying to minimize those mistakes by coming up with a solid, modular (subsystem-based) base engine that a game can be laid on top of. Sort of a separation of church and state :P. It's a learning experience, essentially, so we're trying to do it like pros would. Of course, we're still researching options actively such as pre-built engines and the like.
Thanks so much,
Aviosity
You're all great. It makes sense that an event generated by the 'w' key being pressed doesn't mean the event system should fire another event back to say what to do; it should just do it. Another step forward would probably be the listener pattern, where each subsystem listens for an event fire (I assume by polling the event stack, or having the event fired directly at the subsystem) and handling it internally.
Thanks so much...I have a feeling that this engine will be simple enough for most people to understand (this is a school-oriented project that will be worked on by multiple people, so simplicity and efficiency are goals one and two).
Dan - the reason I'm not focusing on the game *yet* is that this is supposed to be a professional-level game built by students and amateurs, with many mistakes along the way. I'm trying to minimize those mistakes by coming up with a solid, modular (subsystem-based) base engine that a game can be laid on top of. Sort of a separation of church and state :P. It's a learning experience, essentially, so we're trying to do it like pros would. Of course, we're still researching options actively such as pre-built engines and the like.
Thanks so much,
Aviosity
There's definitely a tricky balance there. One the one hand, if you don't have a lot of experience writing games, it's going to be very difficult to correctly anticipate what games really need from an engine. On the other hand, just focusing on the exactly what a given game needs can lead to some serious myopia and bad design.
Many moons ago, my design philosophy was something like "try to anticipate what client code will do, and build a complete design to accomodate those needs." I've since very much abandoned that approach. Even though I've been doing this quite awhile, and have broad enough experience across multiple game genres, I've basically resigned myself to the notion that significant amounts of anticipatory coding is almost always a waste of time.
So, how to deal with this?
I've adopted what I refer to as the rigid/flexible approach. Determine which specific subset of the overall problem domain you're actually going to handle, write code to address it, and then make sure that it's very clear to the code (via tons of asserts) and to the client (by way of appropriate function names and documentation) exactly what the code does and does not deal with. That's the rigid part.
The flexible part comes from not over-committing the design to a particular approach. By doing this (and this is, admittedly, the trickier part), you can change and adapt your code to whatever new requirements the clients (in this case, the people writing the game part) come up with. If your code is properly self-defending, it will be vocal about when someone is trying to use it in a way that it's not yet ready to handle. You can then either correct the client code ("Hmm, that's not really how I meant you to use this - here, try this other approach."), adapt your code ("ok, I hadn't anticipated that use, but that seems like a good idea - I'll add support for that"), or defer until later ("That might be a reasonable way of doing it, but the code doesn't currently support that. I'm currently working on this other section, but we can review your approach later, and perhaps adapt the code to support it.").
The first (rigid) part does actually support the second (flexible) part - as you write more code, you are more committed to a particular approach, and it becomes more difficult to change directions. If you write a bunch of code that nobody's using, you've wasted your time AND you've got more inertia when it comes time to changing directions. If you have a small, flexible, code base, you can quickly react to changing requirements. And, trust me, the requirements always change out from under you.
Oh, and also, you almost always want to start out being your own client: When designing system components, try to figure out exactly how you would go about using the system to accomplish its stated design goals. It's all to easy to get wrapped up in implementation details, or in lofty designs that don't have any bearing on the actual problem domain.
And, with all that said, I'll give the same advice I tend to give others: Use and adapt existing available code until you have enough design understanding to have a good reason not to. There's quite a bit of good stuff out there (and, well, lots of crap, too).
If you don't like the details, or a given codebase is too complex, simply write a cover layer abstraction that only exposes the parts you're interested in using. If you get it right, you can change out the codebase you're sitting on later for something else (possibly your own homebrew).
Cheers,
Jason
p.s. For a practical example of the rigid/flexible approach: I've been working on importing Collada data into my engine recently. The Collada spec is pretty big, and fully supporting all valid Collada data would be a big undertaking. Instead of trying to do that, my pipeline accepts a specific subset of the Collada spec, and will complain loudly if you try to feed it an asset that it can't handle. This makes it so that either the asset goes back for rework to fit it in the pipe, or (if appropriate) the pipeline gets adapted to handle the new variant.
Many moons ago, my design philosophy was something like "try to anticipate what client code will do, and build a complete design to accomodate those needs." I've since very much abandoned that approach. Even though I've been doing this quite awhile, and have broad enough experience across multiple game genres, I've basically resigned myself to the notion that significant amounts of anticipatory coding is almost always a waste of time.
So, how to deal with this?
I've adopted what I refer to as the rigid/flexible approach. Determine which specific subset of the overall problem domain you're actually going to handle, write code to address it, and then make sure that it's very clear to the code (via tons of asserts) and to the client (by way of appropriate function names and documentation) exactly what the code does and does not deal with. That's the rigid part.
The flexible part comes from not over-committing the design to a particular approach. By doing this (and this is, admittedly, the trickier part), you can change and adapt your code to whatever new requirements the clients (in this case, the people writing the game part) come up with. If your code is properly self-defending, it will be vocal about when someone is trying to use it in a way that it's not yet ready to handle. You can then either correct the client code ("Hmm, that's not really how I meant you to use this - here, try this other approach."), adapt your code ("ok, I hadn't anticipated that use, but that seems like a good idea - I'll add support for that"), or defer until later ("That might be a reasonable way of doing it, but the code doesn't currently support that. I'm currently working on this other section, but we can review your approach later, and perhaps adapt the code to support it.").
The first (rigid) part does actually support the second (flexible) part - as you write more code, you are more committed to a particular approach, and it becomes more difficult to change directions. If you write a bunch of code that nobody's using, you've wasted your time AND you've got more inertia when it comes time to changing directions. If you have a small, flexible, code base, you can quickly react to changing requirements. And, trust me, the requirements always change out from under you.
Oh, and also, you almost always want to start out being your own client: When designing system components, try to figure out exactly how you would go about using the system to accomplish its stated design goals. It's all to easy to get wrapped up in implementation details, or in lofty designs that don't have any bearing on the actual problem domain.
And, with all that said, I'll give the same advice I tend to give others: Use and adapt existing available code until you have enough design understanding to have a good reason not to. There's quite a bit of good stuff out there (and, well, lots of crap, too).
If you don't like the details, or a given codebase is too complex, simply write a cover layer abstraction that only exposes the parts you're interested in using. If you get it right, you can change out the codebase you're sitting on later for something else (possibly your own homebrew).
Cheers,
Jason
p.s. For a practical example of the rigid/flexible approach: I've been working on importing Collada data into my engine recently. The Collada spec is pretty big, and fully supporting all valid Collada data would be a big undertaking. Instead of trying to do that, my pipeline accepts a specific subset of the Collada spec, and will complain loudly if you try to feed it an asset that it can't handle. This makes it so that either the asset goes back for rework to fit it in the pipe, or (if appropriate) the pipeline gets adapted to handle the new variant.
Hey Jason,
I hear you. This is a very top-level engine design at this point, but it is rigid in what it'll do. It's designed for a multiplayer-only game, and of a specific small-group questing-type game; nobody's going to build Counterstrike or Flight Simulator on top of it without some SERIOUS changes to the underlying engine; it'd be better to start with a new one. However, my design of the engine is, as you said, going to incorporate the ability to change it in a modular fashion, for *when* the artists say "we need a particle system that can replace an enemy's head" I can go in and add some attachable properties and geometry modification code to the 'enemy' object. A lot of the code will be homebrew, but with a LOT of looking at OGRE and the such to see how they did it first. I'm sure the design will change a lot the more research I do.
Oh, and good luck with the Collada implementation. That's something I'd like to do here in the distant future, and I'm interested to see how it works out for everyone doing it now.
Thanks,
Aviosity
I hear you. This is a very top-level engine design at this point, but it is rigid in what it'll do. It's designed for a multiplayer-only game, and of a specific small-group questing-type game; nobody's going to build Counterstrike or Flight Simulator on top of it without some SERIOUS changes to the underlying engine; it'd be better to start with a new one. However, my design of the engine is, as you said, going to incorporate the ability to change it in a modular fashion, for *when* the artists say "we need a particle system that can replace an enemy's head" I can go in and add some attachable properties and geometry modification code to the 'enemy' object. A lot of the code will be homebrew, but with a LOT of looking at OGRE and the such to see how they did it first. I'm sure the design will change a lot the more research I do.
Oh, and good luck with the Collada implementation. That's something I'd like to do here in the distant future, and I'm interested to see how it works out for everyone doing it now.
Thanks,
Aviosity
Quote:Original post by aviosity
Oh, and good luck with the Collada implementation. That's something I'd like to do here in the distant future, and I'm interested to see how it works out for everyone doing it now.
Well, it wasn't the easiest thing ever, but I think it was worth it. I think it was easier in the past for me to deal directly with the Maya API (even with all of its foibles) to get data out, but I wanted a more package agnostic pipeline this time around.
So far, I've got end-to-end (package->Collada->Import->Engine->Render) support for meshes, skinned meshes, animation, and support for morph targets is about 4-6 implementation hours from completion.
This topic is closed to new replies.
Advertisement
Popular Topics
Advertisement