Good question, maybe something like this? Just wrote this up on a whim.
======== Application Layer =======
Manager
- Logically manages UI elements relative to a given schema.
- Useful for:
Manages the state of the application
Initial propagator or consumer of input
Propagates UI events
Functions as a window manager
Scheme
- The type of UI to render and input to receive, a logical I/O context.
- Useful for:
Logically unify platform backends (Mobile, Web, PC etc)
Typically separate schema for input and output
Captures platform specific input or propagates to the application
Determines how panes are rendered and interacted with
Platform
- The actual I/O context, platform specific backend.
- Useful for:
Deploying on multiple platforms (DirectX, SDL, Console, iOS etc)
Abstracting differences between platforms
Simplifies porting of applications
Pane
- A GUI element that communicates feedback to the user.
- Useful for:
Managing discrete I/O interactions
Abstracting different types of UI with content
Capturing UI events
======== Game Layer ========
Model
- A pure data representation of the game, a game state.
- Useful for:
searching the state tree for AI
serialization for saves and replays
replication for networking
System
- A stateless and pure function that evaluates the game state, modifying it agnostically in some way.
- Useful for:
Isolation of logical interactions
Test-driven development
Maintaining fidelity amongst interactions
Entity
- A key for a list of components stored in a database as represented by a game state.
- Useful for:
Simplification of interactions
No limiting inheritance hierarchy
No confusion as to how entities should communicate
A configuration of components IS the "gameplay entity," the 'key' doesn't matter!
Simplified state management
Component
- A set of isolate data corresponding to some System(s) aggregated as an entity.
- Useful for:
Isolating salient interactive elements
See 'System'
Ancillary
- Object used by system(s) with which to communicate to the application layer.
- Useful for:
Propagate events to the UI for arbitrary feedback
Query the game state to evaluate the validity of a mapped action
Manage special feedback mechanisms, ie Cameras (which are part of the UI)
Manage special effects that may update per frame, rather than with game logic
Manage external libs, such as an oop physics engine (a system writes changes to the state)
May not be stateless, but game logic shouldn't depend on its state
Also used by the application to serialize the state
Manage networking and other forms of communication
The idea is pretty straightforward. Logically isolate everything as much as possible such that it is trivial to replace, remove, or reconfigure any aspect of the program.
The "Game" is just a set of stateless systems that act upon and modify the Model, or a game state. Game states are easily hashed, serialized, replicated and otherwise insanely useful for data management (such as networking, saving, replays, etc). The Model, of course, is just a database containing entities which are just keys for sets of components that represent a salient actor within the game. The Model will also contain some other details, like the current tick and the seed.
Some of our logic may depend on foreign libraries that don't immediately seem to fit into our wonderful world of stateless systems and pure data game. For these, a system may defer logic to some foreign process. For example, say we're using Bullet. Bullet is very OOP, isn't easy to serialize, and isn't stateless. We can use an ancillary object to manage the physics simulation, but we're still writing data to/from this ancillary via a system. Particle effect management, sounds, cameras and some other logically arbitrary but essential feedback features that have logical components can also be managed by ancillaries. Ancillary comes from the latin ancilla, which means slave girl. These bitches also send UI events to our application so that it knows when to draw things. The UI can use an ancillary to query the game state to determine whether an action is valid or not. These gals really just manage how the logic of the game communicates with the UI. The nature of the component-based system will necessarily promote the isolation of ancillary features into separate and unique objects- so it's kinda convenient.
Now, how does an ancillary actually query the game to determine if an action is valid? If the relevant system with which we are querying for is broken up into subroutines, we can easily use some of those routines to check it out. We can even duplicate the game state, run it through the system with the action appended and check to see what sort of UI event that system throws to another ancillary. It really just depends-- but it should probably be a chunk of specialized logic to test the validity. You may not also 'need' a querying technique. For example-- if you walk north but there is a wall and don't want the player to lose a turn, the system just bubbles a UI Event (via an ancillary) and returns to the system responsible for catching the action. If you equip something in a place that isn't valid, some checks should fail and the system can throw a UI event. The main issue here is that we want to know pre-emptively if it will fail so that we can more directly communicate to the player what they're doing wrong; however, if somehow a command gets through that isn't valid, the System shouldn't allow it. The ancillary cannot be performing game logic, it's purely a UI greaser.
So now we get to the actual application. First we have the platform we're running on. The Platform is represented by a set of interfaces filled out by native code. SDL, for example, will do the actual drawing, but we don't want to make calls to SDL because we want the freedom to drop, say, DirectX or even WebGL in as a backend. Easy, just fill out the interface and link for distribution on that platform. The logic of our UI, Application, and of our Game are not at all influenced by the platform.
Schema are logically distinct UI systems. Say our graphical interface is tiled like a traditional Console. That is a logical object, not a graphical or platform dependent one. The Logical console consumes drawing requests and sends them to some platform for rendering via an interface. You'll typically only have one input scheme, since the input needs to be rationalized by the platform code first, and all the input scheme will do is provide a mapping from platform to application actions (or rather, communicate platform 'intent' to the application). The output scheme could vary between desktop and mobile (which needs to be optimized for a small screen), but if you design it with mobile in mind, the differences could be really marginal.
From here we have a Manager to deal with Panes, focus, and determining how and where to send input. Panes contain formatting information that is drawn to the screen in a manner that is consistent with the logical output scheme. You probably want any pane capable of reacting to any UI event. For example, a camera pane may flash red when you're avatar is struck by an enemy, just as the health bar will tick down and the message log updated. These can all use the same UI event to update, just interpreted in different ways.
Panes serve two purposes relative to the game logic, helping the user format input in a way that an ancillary can use to validate, and evaluating UI events sent from ancillaries to update the screen.
And that's more or less how I'd structure a video game application. The only events that really occur are UI events, because everything else is communicated directly to where it needs to go. Input is passed from platform, to application, to logic, but I don't know if I would call that an event exactly. You could do input events also, but typically only the focus pane or the manager will be catching it, so you can just let the manager defer the input command.
Where does the query code and modification code reside, and how does the UI have access to it? It's definitely logic code. I assume we can put it in a System, even if it's not the "usual" kind that runs every step. Does the application/GUI layer know about the System being used? I guess what I'm saying is that I somewhat dislike that the application layer is so aware of the game's internals
Yea, the application doesn't need to know anything about the game. Input is always a logic activity, but since input is meaningless without feedback and may target the application first, the application needs to decide what to do. Don't get overzealous with systems though, they're purpose is to modify game logic, nothing more. If something sits between the UI and the game state, like a query, then create a managed and isolated area for that to take place (an ancillary).
It's also not inappropriate to manage Systems as an FSM instead of iterating over each of them for every game action. In this way, each system is a sub-routine that can be switched to if the prior system necessitates it. For example, the ticking system that progresses the state of the simulation forward may defer to subroutines to handle gas and fire propagation, and other things like that.
Also, you can make your entire application use a component based entity system, but I haven't considered a way to rationalize that. In a way, Panes are like entities and Widgets are like components, but the states of these objects aren't of logical importance, so it's kind of pointless. Use OOP to run your application, and CES to manage your game logic.
I wonder if there is some nice halfway solution, where the application layer is a bit more decoupled from the game logic itself, but you keep this querying, which is definitely less involved than a bajillion events going all over the place for the smallest action...
KISS. Hopefully you can see how an Ancillary object could figure this out in an isolated manner.
Ooooh, using a plain dict would actually be really lightweight and cool! Do you have any particular opinion on the relative merits of Entity Systems over just Component-based Entities (which allow game logic within components/entities)? I feel those would be better adapted to games that aren't real-time. At least the class-based design may accommodates both?
All game engines (that matter) use hybrid systems as opposed to pure OOP or pure CES.
An entity is a base class with virtual event handlers for every possible game event.
Components can be applied to entities, which are then managed by a system.
The systems modify relevant components and produce events for entities to handle if they want.
While this is VERY easy to understand and use, it results in HUGE amount of obfuscation. Why? Because it can be very confusing where game logic should actively reside, and it can be difficult to find where game logic is actually taking place. You also lose the ability to serialize and replicate without extra work, and you sacrifice the fidelity of game states, making it difficult to search the state space. If you're working in a group, it can be tedious to manage where logic should actually go.
Also, if you don't mind me asking - where and how did you learn all of these these things? You seem to have very clear ideas on most of these issues, which is great
Uhh... I guess when I first taught myself HaXe, it forced me to think about programming in a more modular way. HaXe is a language designed to translate its code into code for many platforms (C++, Java, JavaScript/HTML5, AS3, emscripten, C#/Mono, etc). While this is wonderful for logic, there is always platform backend specific issues that you have to keep in mind. I just begun naturally thinking about how to make all of the elements of my programs interchangeable, so it would be easy to drop-in different renderers or input schema.
I've also worked with some of the bigger game engines, so I've had some opportunities, out of necessity in some cases, to extrapolate their overall design. They are functional, but I don't like the overall approach. Independently I began researching CES, but there aren't very many programming paradigms that allow you to utilize it as efficiently as possible- specifically, you're just pretending to be data-driven while actually in an OOP paradigm. A lot of tutorials and documentation talks about Components and Entities as classes, but I think this is wrong because now every single type of component needs a specialized hashing method, extra work to register them with systems, which now also must be objects, which makes it less obvious what your memory footprint will be-- it's just... like programming a GUI system, which every programmer should hate >_<.
Anyhow-- the question got me thinking about what I actually do, because I haven't really thought about it explicitly before. Other people will have their own approaches, just find what makes sense and works for you.