(Disclaimer: I never programmed games in Java, only in C++. But the general idea should be applicable in Java too.
The ideas I present are not my own, but a mash-up of solutions I found in books or "on the internet", see references section.
I employ all this myself and so far it results in a clean design where I know exactly where to put new features I add.)
I am afraid this will be a long answer, it might not be clear when reading for the first time, as I can't describe it just top-down very well, so there will be references back and forth, this is due to my lacking explaining skill, not because the design is flawed. In hindsight I overreached and may even be off-topic. But now that I have written all this, I can't bring myself to just throw it away. Just ask if something is unclear.
Before starting to design any of the packages and classes, start with an analysis. What are the features you want to have in the game. Don't plan for a "maybe I'll add this later", because almost certainly the design decisions you make up-front before you start to add this feature in earnest, the stub you planned for it will be insufficient.
And for motivation, I speak from experience here, don't think of your task as writing a game engine, write a game! Whatever you ponder about what would be cool to have for a future project, reject it unless you put it in the game you are writing right now. No untested dead code, no motivation problems due to not being able to solve a problem that isn't even an issue for the immediate project ahead. There is no perfect design, but there is one good enough. Worth keeping this in mind.
As said above, I don't believe that MVC is of any use when designing a game. Model/View separation is not an issue, and the controller stuff is pretty complicated, too much so as to be just called "controller".
If you want to have subpackages named model, view, control, go ahead. The following can be integrated into this packaging scheme, though others are at least as sensible.
It is hard to find a starting point into my solution, so I just start top-most:
In the main program, I just create the Application object, init it and start it. The application's init()
will create the feature servers (see below) and inits them. Also the first game state is created and pushed on top. (also see below)
Feature servers encapsulate orthogonal game features. These can be implemented independently and are loosely coupled by messages. Example features: Sound, visual representation, collision detection, artificial intelligence/decision making, physics, and so on. How the features themselves are organized is described below.
Input, control flow and the game loop
Game states present a way to organize input control. I usually have a single class that collects input events or capture input state and poll it later (InputServer/InputManager) . If using the event based approach the events are given to the single one registered active game state.
When starting the game this will be the main menu game state. A game state has init/destroy
and resume/suspend
function. Init()
will initialize the game state, in case of the main menu it will show the top most menu level. Resume()
will give control to this state, it now takes the input from the InputServer. Suspend()
will clear the menu view from the screen and destroy()
will free any resources the main menu needs.
GameStates can be stacked, when a user starts the game using the "new game" option, then the MainMenu game state gets suspended and the PlayerControlGameState will be put onto the stack and now receives the input events. This way you can handle input depending on the state of your game. With only one controller active at any given time you simplify control flow enormously.
Input collection is triggered by the game loop. The game loop basically determines the frame time for the current loop, updates feature servers, collects input and updates the game state. The frame time is either given to an update function of each of these or is provided by a Timer singleton. This is the canonical time used to determine time duration since last update call.
Game objects and features
The heart of this design is interaction of game objects and features.
As shown above a feature in this sense is a piece of game functionality that can be implemented independently of each other. A game object is anything that interacts with the player or any other game objects in any way. Examples: The player avatar itself is a game object. A torch is a game object, NPCs are game objects as are lighting zones and sound sources or any combination of these.
Traditionally RPG game objects are the top class of some sophisticated class hierarchy, but really this approach is just wrong. Many orthogonal aspects can't be put into a hierarchy and even using interfaces in the end you have to have concrete classes. An item is a game object, a pick-able item is a game object a chest is a container is an item, but making a chest pick-able or not is an either or decision with this approach, as you have to have a single hierarchy. And it gets more complicated when you want to have a talking magic riddle chest that only opens when a riddle is answered. There just is no one all fitting hierarchy.
A better approach is to have just a single game object class and put each orthogonal aspect, which usually is expressed in the class hierarchy, into its own component/feature class. Can the game object hold other items? Then add the ContainerFeature to it, can it talk, add the TalkTargetFeature to it and so on.
In my design a GameObject only has an intrinsic unique id, name and location property, everything else is added as a feature component. Components can be added at run-time through the GameObject interface by calling addComponent(), removeComponent(). So to make it visible add a VisibleComponent, make it make sounds, add an AudableComponent, make it a container, add a ContainerComponent.
The VisibleComponent is important for your question, as this is the class that provides the link between model and view. Not everything needs a view in the classical sense. A trigger zone will not be visible, an ambient sound zone won't either. Only game objects having the VisibleComponent will be visible.
The visual representation is updated in the main loop, when the VisibleFeatureServer is updated. It then updates the view according to the VisibleComponents registered to it. Whether it queries the state of each or just queues messages received from them depends on your application and the underlying visualization library.
In my case I use Ogre3D. Here, when a VisibleComponent is attached to a game object it creates a SceneNode that is attached to the scene graph and to the scene node an Entity (representation of a 3d mesh). Every TransformMessage (see below) is processed immediately. The VisibleFeatureServer then makes Ogre3d redraw the scene to the RenderWindow (In essence, details are more complicated, as always)
Messages
So how do these features and game states and game objects communicate with each other?
Via messages. A Message in this design is simply any subclass of the Message class. Each concrete Message can have its own interface that is convenient for its task.
Messages can be sent from one GameObject to other GameObjects, from a GameObject to its components and from FeatureServers to the components they are responsible for.
When a FeatureComponent is created and added to a game object it registers itself to the game object by calling myGameObject.registerMessageHandler(this, MessageID) for every message it wants to receive. It also registers itself to its feature server for every message it wants to receive from there.
If the player tries to talk to a character it has in its focus, then the user will somehow trigger the talk action. E.g.: If the char in focus is a friendly NPC, then by pressing the mouse button the standard interaction is triggered. The target game objects standard action is queried by sending it a GetStandardActionMessage. The target game object receives the message and, starting with first registered one, notifies its feature components that want to know about the message. The first component for this message will then set the standard action to the one that will trigger itself (TalkTargetComponent will set standard action to Talk, which it will receive too first.) and then mark message as consumed. The GameObject will test for consumption and see that it is indeed consumed and return to caller. The now modified message is then evaluated and the resulting action invoked
Yes this example seems complicated but it already is one of the more complicated ones. Others like TransformMessage for notifying about position and orientation change are easier to process. A TransformMassage is interesting to many feature servers. VisualisationServer needs it to update GameObject's visual representation on screen. SoundServer to update 3d sound position and so on.
The advantage of using messages rather than invoking methods should be clear. There is lower coupling between components. When invoking a method the caller needs to know the callee. But by using messages this is completely decoupled. If there is no receiver, then it doesn't matter. Also how the receiver processes the message if at all is not a concern of the caller.
Maybe delegates are a good choice here, but Java misses a clean implementation for these and in case of the network game, you need to use some kind of RPC, which has a rather high latency. And low latency is crucial for interactive games.
Persistence and marshalling
This brings us to how to pass messages over the network. By encapsulating GameObject/Feature interaction to messages, we only have to worry about how to pass messages over the network. Ideally you bring messages into a universal form and put them into a UDP package and send it. Receiver unpacks message to a instance of the proper class and channels it to the receiver or broadcasts it, depending on the message.
I don't know whether Java's built-in serialization is up to the task. But even if not, there are lots of libs that can do this.
GameObjects and components make their persistent state available via properties (C++ doesn't have Serialization built-in.)
They have an interface similar to a PropertyBag in Java with which their state can be retrieved and restored.
References
- The Brain Dump: The blog of a professional game developer. Also authors of the open source Nebula engine, a game engine used in commercially successful games. Most of the design I presented here is taken from Nebula's application layer.
- Noteworthy article on above blog, it lays out the application layer of the engine. Another angle to what I tried to describe above.
- A lengthy discussion on how to lay out game architecture. Mostly Ogre specific, but general enough to be useful for others too.
- Another argument for component based designs, with useful references at the bottom.