Models, views, controllers, presenters, oh my!
Unfortunately, I can’t reveal too much about the game I’m currently working on, but I can say that it’s like a board game. For the sake of this post, let’s assume that the game is chess: there is a game board, there are some players, and each player has a bunch of pieces that either have a position on the game board, or have been captured. In this post, I’ll talk a bit about how to architecture such a game in software.
At first sight, a board game lends itself perfectly to a Model-View-Controller (MVC) pattern. At the heart of it is the model, which represents the game state. In the case of chess, this state is very simple:
- the location of all the pieces
- whose turn it is
Observe how there is nothing here about how the state should be displayed. Do we render it as sprites in OpenGL? Do we use ASCII-art? Is the model purely for an AI player to contemplate, so we don’t display it at all? The model doesn’t care.
Now we might want to display the game state on the screen. This is what the view is for. It might iterate through all pieces in the model and draw them, or it might keep a list or tree of sprites to be drawn, which is updated as needed. Traditionally, the view is kept up to date by listening to change events from the model.
And, of course, the player wants to indicate their chosen move to the game. This is handled by the controller. It responds to user input events (for example, clicks and touch events) and updates the model accordingly. The view, in turn, is notified and redraws itself.
The problems with Model-View-Controller
Suppose we don’t want to just display the chess pieces, but also animate them. Since the model represents just the ‘pure’ game state, it doesn’t and shouldn’t know anything about animation. So the view needs to become stateful. It needs to keep track of the animation state: where a piece is coming from, where it’s going to, and how far along the path it is.
Although this approach sounds OK at first, there is a snag: the view sometimes needs to know why the model changed. Consider a castling move in chess. The rook and king move at the same time, so instead of the two pieces passing through each other, we might want to animate the king jumping over the rook. If the view just animates each piece individually, such an animation is impossible.
Conceivably, the view could consider all pieces that moved, detect that both the king and the rook moved at the same time, and play the castling animation – but we’d be deeply embedding knowledge of game logic into the view, which is not what we want!
We could give ourselves a way out by adding state to the model: keep track of the ’types’ of moves that happened (or just the last one). But this bloats the model just because the view is too limited. It seems we have painted ourselves into a corner.
The Model-View-Presenter pattern
The Model-View-Presenter pattern sounds similar to MVC at first, only the controller has been swapped out by a presenter. But as we’ll see, the role of the View has also changed. The model remains as before.
Let’s talk about changes to the view first. In MVC, the view would listen to updates from the model, and update itself accordingly. In MVP, however, the view contains no update logic at all! It is just a dumb bag of ‘stuff to be displayed’. In HTML, the view would be the DOM.
The presenter does takes on the role of controller, in the sense that it sends state updates to the model. However, the presenter is also responsible for updating the view. This dual responsibility means that the presenter can update the view not just in response to model changes, but in response to user input as well. This means it can respond to the user’s action, not just (as in MVC) to that action’s outcome.
It might be instructive to look at a diagram comparing MVC (left) to MVP (right):
In our chess example, if the user indicated a castling move, the presenter knows that it’s a castling move, not just any ordinary move, so it can apply the appropriate animation when sending the updates to the view.
At this point, you may wonder whether the presenter, with its large area of responsibility, doesn’t become a big ball of spaghetti. I think this is indeed a risk. However, the key observation is that the presenter can be kept entirely stateless: all state is held in the model and the view, and the presenter merely mediates between the two. This helps to keep the presenter somewhat, well, presentable.
You may wonder why there isn’t an arrow from model back to viewmodel in the rightmost diagram. This is because, in my game, the presenter is the only object that can trigger model changes. Since it knows that the model can’t change behind its back, it never needs to poll or watch for changes.
In practice
I started writing my game as MVC, but quickly found that the view and controller weren’t as separate as they should be. For example, the view is represented as a scene graph containing one sprite for each game piece to be drawn. But for the controller to process user input, it needs to know where these pieces are on the screen; in other words, it needs to talk to the view. So unwittingly, I had reinvented the MVP pattern already; knowing about the pattern just helped me clean the code structure up a bit.
You may not even have any choice whether to use MVC or MVP. When using a scene
graph or the HTML DOM, the tree used for rendering is also used for receiving
and processing of input events, so you couldn’t decouple the view and
controller even if you wanted to. In libgdx’s scene2d
scene graph
implementation, the view consists of a tree of Actor
s; the presenter hooks up
event handlers to the Actor
s and responds to events by updating the model and
the scene graph accordingly. Each Actor
can also contain one or more
Action
s, which are animations that update the Actor
’s state each frame.
I hope this post helps people who are struggling to get a clean architecture for their board-like game. It’s no silver bullet, but I found that my code became cleaner and more testable using this approach. Be sure to leave any questions or remarks in the comments below!
Further reading: