Design Patterns in Unity With Among Us — Fix Wiring Task
In this article, I try to implement Factory and Observer Patterns by recreating Fix Wiring tasks
Check out the repository to this article.
“In order to fully understand this article you should be familiar with concepts like Dependency injection, Zenject, Unit Tests, SOLID and MVC”
The whole point of using design patterns, SOLID Principles, common architectural styles like MVC and etc. is to create a codebase that can be easily reused, maintained, and debugged. So it is pointless if we go for one of those without achieving the main goal.
In this tutorial, I will demonstrate how to implement the “Fix Wiring” task in Among Us using Factory and Observer patterns. Meanwhile, I will stick to SOLID principles as much as I can, use Zenject to handle dependencies, and also try to follow Model-View-Controller architecture. In the end, I will write a unit test to check if our code is testable.
Long story short. Let’s get going!
Starting with MVC
MVC is a little tricky in Unity. You may sometimes break its rules but it is always nice to follow this architecture. After all, if you can write Unit Tests, you have succeeded in implementing MVC!
The easiest part is always the Model. I store the very basic properties of a wire inside a class called WireModel:
I need a source to find out where the head of my wire starts. I don’t store the target here because its location may change by the logic. And there is another reason that I am storing a GameObject as a source and not a Vector3 which you will find out a bit later.
After that, I want to implement the Controller by creating an Interface called IWire:
It just has a single function. CheckFixed to check if the wire’s endPos has reached the target point.
Now for the last part, we will have a Monobehavior for our View as it will deal with the visualization and input. WireView will be something like this:
We have our methods up and ready to be filled.
Ensure the Single Responsibility Principle
For the wire, we know that it has just one responsibility and that is to declare if the wire has successfully reached the target. So to handle the wire visualization, we need another class that I called IWireRenderer:
At the back of my head, I knew that I’m going to use Unity’s LineRenderer to show the wire. But as time goes, a developer, product owner, or designer may decide to change that. So we should have a well-structured interface to ensure that modifying code won’t break the previous implementation.
Here we have the main method called SetPosition which locates point positions on the renderer. And a Disable method to disable renderer when wire reaches its target. With that, we update the WireView:
Factory Pattern
Until now we have dealt with wire’s logic and rendering. But we still need to provide a way to create our renderer. We shouldn’t give this responsibility to IWireRenderer because this will break SRP. So I separated the creation of the renderer into another class. This is where the Factory pattern comes to help. I create an interface called IWireFactory which will handle the creation of wire renderer:
the CreateWireRenderer method receives a WireModel as a parameter and deals with its creation. But unlike common Factory pattern implementation, it doesn’t return the product. As I said you don’t know how you will implement the renderer in the future. I tried to convert CreateWireRenderer to a generic method but I wasn’t quite successful, so I came up with another handy solution. I let another interface called ILineRendererWire be responsible for providing factory product:
Using these two interfaces, I implement a fully functional factory that creates a LineRenderer as its product and returns it. This is LineRendererWireFactory (I know the name is too big!):
You can see why I used a GameObject as a source in WireModel; just to attach the renderer to it!
Having this, we need to update the view once more:
I preferred to create the LineRenderer right away at the Start function.
After creating the renderer, we should use it! And that is the job of the WireRenderer class:
The Controller
Having the WireRenderer, we implement IWire like this:
It is simply calculating if the tip of the wire has reached the target or not (considering a threshold) and if it has, it disables the wireRenderer.
By adding an IWire property and filling OnMouse events, we update our View class one more time:
we have two problems here. First, we have broken the Single Responsibility Principle by implementing how the input should be calculated in the WireView. Second, we depend on Unity’s Input class which is hiding a dependency and also violating the Dependency Inversion Principle. So by extracting the input manager, we can solve this problem and achieve a much cleaner class for our view.
And finally, our View looks like this:
Observer Pattern
Now that each wire works, Let’s assume that we have n wires in our task. How do we want to check if all of them have been fixed or not?
It is so easy using Observer Pattern! After each wire gets installed, we should attach it to a class (TaskManager), and each time the CheckFixed happens, we should notify that class and it will update the task state. We implement the TaskManager like this:
As you see each time it gets updated, it checks all wires and decides the state of the task. The Completed flag is just for anyone who wants to know about the state of the task (which will be the Test Runner).
Having this, let's update Wire and WireView:
After a while of looking at this implementation, I doubted if I could call it “Observer” because the fixing calculation happens inside the Wire itself and doesn’t have anything to do with notifying other wires (like what observer patterns normally do). But then I thought in this situation, wires don’t need to know about each other and they should only notify the task manager. So we are good for now!
Dependency Injection with Zenject
For resolving dependencies, I decided to use Zenject. I could use it only for our WireView class because it is a Monobehaviour and Zenject is basically a solution for MonoBehaviour dependency issues. But it was no harm to use it in other classes. So I added Zenject to my classes like this:
For binding dependencies, I added a GameObjectContext and a MonoInstaller called WireInstaller to each GameObject containing WireView:
And a SceneContext with the following Installer:
And my game worked like a charm!!!
I know! It looks ugly, but we’re just talking about functionality now!
Writing a simple Unit Test
For the test, I used the Zenject Unit Test script. I just wanted to check if the dependencies work and Wire and FixingWiresTask work along properly. So I wrote this:
The test passes:
We can make it more practical by using more test cases:
That’s it! It was so enjoyable for me to code this article and I hope I have enough time to do more. You can always find the project and code in the repository in my Github.
Also, don't forget to check out my website and contact me if you have any questions.
Thank you for your time!