I don't want to go too deeply about what MVP is and how it differs from other MV* patterns (such as the classic MVC), but here's a quick diagram stolen from Wikipedia.
The key takeaways are that views are dumb and easily interchangeable, presenters contain the business logic, a presenter updates ideally one (but can update more) view, application state lives in the model objects (which are as equally dumb as views). That's all you should really need to know to follow the rest of this post, but please read up on MVP more if you're not familiar with it and understand the difference between MVC.
When I first began game development, I had a hard time structuring my code. I initially couldn't grok how to apply all of the golden rules of regular GUI development to games. Recently, it started to click. You can apply a MV* pattern to games, very easily in fact, and create a clean code base that's organized, maintainable and can be easily changed (we all know how volatile a game's design and feature set can be!). So lets talk about how an MVP pattern can be applied to a Unity code base.
I'm not going to provide any specific code in this post (there's good reason why as you'll see later). This is strictly theory.
Let's say we have a player prefab. Normally, you may just write a bunch of specific scripts to do one thing and one thing only (hopefully) and attach each different script to the prefab. While this does work, I find it chaotic, especially when other scripts need to start talking to each other or one script needs to have its behavior changed slightly for one specific type of prefab. To do things the MVP way instead, we're going to apply a two scripts to the prefab called PlayerView and PlayerPresenter.
PlayerView will represent the View portion of MVP (well, duh!). PlayerView will contain zero game logic. It will strictly be responsible for handling the visual representation of the player, accepting input to pass along to the presenter, and exposing important properties that you may want to have adjustable in the Inspector view of the Unity editor, like health, walking speed, etc. PlayerView will listen for input from the player and pass along the input to the view's backing presenter via events and passing the presenter model objects with the necessary data.
PlayerPresenter will represent, can you guess it, the Presenter portion. Now earlier I said presenters contain the game logic, and in a lot of cases this is true, however I'm going to throw another pattern at you (I'M GOING DESIGN PATTERN CRAZY). Instead of putting all of the game logic for the player in PlayerPresenter, we're going to make use of the command pattern, or a variation of it. PlayerPresenter will be responsible for creating the necessary model objects (based on data from PlayerView) and sending those model objects to Task objects, which handle the actual game logic.
Model objects are very dumb. They simply encapsulate data to pass around. A bunch of properties, nothing more.
Task objects live to do one thing, and one thing very well. They accept model objects from the presenters, do a bunch of work, calculate player score or create a projectile object to spawn, for example, and if necessary sends the results of the work back to the presenter to update the view with. This creates extremely modular, reusable game logic that can be accessed from any presenter that calls it. This allows us to create flat class hierarchies as well, which is a great thing. We could let the presenters handle the game logic and perform the actual work, and in some cases you may, but that game logic isn't easily shared elsewhere and you risk either creating deep class hierarchies to share the logic, or repeating code.
So let's step back and see how a real example would play out. Let's go through an example of a player pressing the shoot button to fire a rocket from his rocket launcher.
- PlayerView receives a shoot input signal, notifies PlayerPresenter
- PlayerPresenter receives notification of the input, creates a SpawnProjectileModel model object of current player position, direction and weapon type (rocket launcher for this example) to send to the SpawnProjectileTask.
- SpawnProjectileTask receives the model object sent from PlayerPresenter, and spawns a new rocket launcher prefab with the data provided via the SpawnProjectileModel model object.
- PlayerPresenter receives notification from SpawnProjectileTask that the rocket spawned successfully and notifies PlayerView.
- PlayerView updates its AmmoCount property to deduct one, which updates the ammo count graphics.
- Done!
This may seem like a lot of steps and indirection to deal with to just fire a rocket, but you can easily change the games look and behavior without a ripple effect. Changing the player from a bad-ass marine to a human-hating robot requires you to only change the View class. Enemies can call the same SpawnProjectileTask as the player does and if the game logic should ever need to change, simply update SpawnProjectileTask and both Enemy and Player pick it up without having to touch either.
Now the reason I didn't provide any actual code and stuck purely to theory is because there is a fantastic framework for Unity to do everything I described so far (plus more), StrangeIoC, which has excellent code samples and diagrams in the documentation and I felt it does better justice. StrangeIoC is an inversion of control, MVP framework. It's fairly new, but I'm using it for Overtime and I don't think I can code a Unity game without it. It's continually evolving and if anything I've talked about in this post is jiving with you, I highly suggest you give StrangeIOC serious consideration. Hopefully, I've convinced you to start considering a MV* type architecture for your next game.
Hi, I enjoyed your article and think it is one of the best such explanations I've read in my long quest to improve my code architecture. But the very end threw me for a loop. Your recommendation of StrangeIoC.
ReplyDeleteI got into StrangeIoC, could not understand it, but kept up, and after trying various examples and reading all the docs 4 or 5 times I finally felt as if I understood it. And I came to the conclusion (you and the creators of StrangeIoC will swear the opposite) that StrangeIoC is not in any way suited to the Unity way of doing things. It takes everything I love about Unity, turns it around and makes it into a painful experience. Such that I am still questing and ironically ended up here.
I am not disagreeing with you. I am just stating my impression of StrangeIoC. And now you got me thinking about it again. I tried their StrangeRocks example and it is not nearly enough to learn how to architect a full Unity game, especially one with GameObjects that have a lot of components (The Unity way). The GameObjects in the StrangeRocks example have no components except for the View and Mediator scripts.
Also in your article you mention attaching two component to prefabs: a PlayerView script and a PlayerPresenter script. Is the PlayerPresenter what StrangeIoc refers to as a Mediator?
Anyway, great article and I'd love for you to go into this much deeper on you blog.
So lets agree on what we mean by "the Unity way of doing things". Normally, you would create MonoBehaviour scripts that perform one specific thing on a GameObject. For example, you could have a FlashColors script that'll, as the name implies, flash the GameObject different colors. You could also have a PlayerController script that accepts input from the player and moves the GameObject. You'd create all these very specific scripts meant to do one thing only, and attach them all to your GameObject to give it its functionality.
DeleteStrangeIoC is giving you a framework to do essentially the same exact thing, just how you're doing it is done is a much more object-oriented, decoupled, modular fashion, while also giving you the benefits of dependency injection.
Instead of creating a FlashColors MonoBehaviour script, you create a FlashColorsCommand class that extends the base Command class from the StrangeIoC framework. Similarly, instead of creating a PlayerController MonoBehaviour script, you create a PlayerControllerCommand. Instead of these Command logic being executed directly within the view (the actual game scene), we rely on having lightweight View scripts (these do live in the game scene) to fire events to the appropriate Commands based on game events (example, two objects collided in the scene, so we call a DestroyEnemyCommand) and user input.
Overall, you're achieving the same thing, but in a much better, organized fashion (at least, I greatly think so). Do you have any experience developing GUI software (be it web or desktop applications) using a Model-View-Controller architecture? I find some previous experience with that greatly helps in understanding StrangeIoC.
Regarding the PlayerPresenter, this would equate to the Mediator in StrangeIoC.
The documentation and guide for StrangeIoC is actually undergoing a rewrite to hopefully be more clear and cover better examples.
In the meantime, if you're not ready to give up (please don't! I promise there's a light at the end of the tunnerl :) ), you can check out the additional resources at http://strangeioc.github.io/strangeioc/resources.html including a Google Groups if you want to post any further questions or reservations about the framework.
Very good Post but one thing confuses me,
ReplyDeleteCan you explain where you keep your current State/Model (health, ammo etc.)?
The way I understand it you have 2 MonoBehaviours(View, Presenter) and the Presenter keeps the state? Wouldn't it be bad to keep the state as a Monobehaviour, because that couples model and view?
For Demons with Shotguns, things like health and ammo are properties of the PlayerView class. Sometimes, this is okay and you can get away with it. In the future games though, I'll probably just have a single instance PlayerStateModel object that has those properties and is then injected into whatever class needs access to them.
DeleteIn one of the branches for strange there is a possibility for DynamicInjection there the view gets an id and the injector decides what to inject based on the id. I think that is a good solution maybe ;) Have you taken a look at that? I always struggled with the concept of having many instances of the same thing with properties in strange. Binding correct views with models etc.
DeleteYep, that sounds like named injections. This is good if you need a very specific concrete implementation of an interface injected into a class. One example where I use this is to inject the proper starting state class for my player's FSM (it'll inject StandingState for the IState property).
DeleteIn essence the Dynamic injection uses named injections. But it has an essential advantage. The name in the Dynamic Injection is not fixed. This Way you can with a simple [DynamicInjection] Attribute inject specific and variable named injections. based on a method that returns the wanted id. Its a good solution to maybe solve the this view displays this model problem.
ReplyDelete