Visitor pattern in Unity3d

Application of the visitor software design pattern in game development using Unity3d engine

George Vasilchenko
8 min readSep 3, 2020

Abstract

Game development as a discipline is challenging on its own compared to traditional software development. The ability to solve performance-related, architectural and other challenges is often the key to success in the field. Because of these and many other factors, it is often a good practice to follow certain software principles and common practices to enforce the maintainability and extensibility of a product. Software design patterns come in handy in such scenarios. They can make the code base more maintainable, extensible, and contribute significantly to the overall lifetime of a product. One of the use cases to be examined is the application of the visitor pattern (Gamma, et al. 1995) in the context of game development. Possible challenges and benefits of the pattern implementation are outlined respectively in this writing.

Preface

The scenario used is completely fictional and resembles an isolated use case. The degree of design completeness in the classes is not meant to be a subject for evaluation of any kind and is simplified to emphasize the point of the paper and not to clutter the code with proper, secure programming construction. Edge case checks are omitted along with many other optimization-related techniques to illustrate the point of the topic.

This writing is a purely personal interpretation and vision of the matter based on the challenges I have encountered while coding and trying to accept Unity3d component-based architecture.

Full source code can be found in my GitHub repo: https://github.com/george-vasilchenko/unity-visitor

The Problem

To examine the usefulness and applicability of the visitor pattern within game logic, we sketch the following scenario. Consider the following diagram, to begin with:

The setup is quite straightforward. We have an abstract base class with a few common members for a character. See the code for the CharacterBase class:

Each of the child classes implements the required members accordingly. The characters have a default set of stats assigned during the initialization. The starting level for each character is 1. To perform an attack, according to my fictional scenario, it is important to calculate damage for the attack. There are hundreds of ways this can be implemented of course but, in my case, it will be a calculation based on the weighted distribution of the stats of a particular character. Here is the implementation of the Attack and CalculateDamage methods from the Archer class:

The idea is that each character, based on its type (Archer, Paladin, etc) has its leading trait. Archer, for instance, has agility as his main trait. In contrast, a magician uses intelligence as the main characteristics. This trend is implemented in the CalculateDamage methods for each of the characters.

The problem here, however, is that we keep this distribution information in each character class. At the first sight, this seems reasonable, but if we will implement a more advanced way of resolving these numbers, we will need to introduce dependencies in each character class and the code will become less flexible.

Let’s consider another scenario for the characters. The ability to level-up in the game can be a desired feature. Usually, this kind of functionality is essential. The ability to increase the level, in my scenario, impacts the damage amount that a character can produce. With each level, normally, a character would deal more and more damage, this is also the case in my fictional scenario. Consider the following implementation of the IncreaseLevel method:

We increment the level member variable each time the method is called. For us to come up with a proper stat increase for a given level, we should increment each stats property each time the level is increased. Using the CreateFromOtherWithDeltas method of structure CharacterStats, gives us the updated stats instance that is built on top of the existing one and with the delta for each property. The implementation of the CharacterStats structure:

To sum it up, we calculate stats deltas based on the level of the character and we update the stats reference with a new object. This, in turn, will be used in the CalculateDamage method to come up with the final damage amount.

The direction where this implementation is heading is obvious. We keep extending the class with the implementation of every new level, besides, in a more advanced scenario, the resolution of the stats values would be delegated to an external class which in turn would become a dependency for the character class. And this repeats for each new character we desire to add in the game. This will bring maintenance problems, high complexity of the character classes, and tight coupling between possible external modules that provide numeric data for this logic.

The Promising Solution

The Visitor pattern, according to Gamma, et al. (1995), is intended to

“represent an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates.”

This definition strikes as a possible solution to achieve the needed maintainability in the classes from the fictional scenario. Let’s try to implement such a pattern and analyze whether it will enhance the quality and design of the program.

I have gone ahead and refactored the code to fit the pattern. A few more classes have emerged. Consider the new diagram:

Types like IDamageable and Enemy are just supplementary components to make the scenario more or less complete, they are used in the Attack method of each character. I have introduced a few more types, specifically ICharacterVisitor<T> and two implementations of this interface: StatsDistributionVisitor and StatsIncreasePerLevelVisitor. The first one was designed to take responsibility for the distribution logic that is used by each character in a specific way, and the latter is meant to tackle the level-based stats problem. I moved all related code from the character classes into the visitor classes respectively. StatsDistribution structure was introduced to encapsulate the concept of stats deltas. The code for the distribution visitor:

Here we can see that values that were provided in the CalculateDamage method are moved into this class and it is way easier to maintain this structure. The stats distribution logic is now in one place and can be easily extended by any external source. For instance, we can use a ScriptableObject (Unity, 2018) instance to manipulate these values from the Editor, which would be a good solution for level designers and testers.

The second visitor class is responsible for determining a boost for the stats for each character based on the level. Here is the implementation in code:

The level stats logic was moved from the character classes into the visitor class. Similarly, this class can also take advantage of additional dependencies. This approach takes the burden concerning level-based upgrades away from the character classes.

Let’s take a look at the actual use of the visitor classes by, say, the Mage class:

The size of the class is reduced. The amount of level related logic will no longer affect this class because the logic is placed in the visitor class. The distribution logic is also substituted with the call to the Visit method of the distribution visitor.

Testing

To try out both implementations, I have set up a few simple editor-based unit tests. The tests take care of creating instances of the characters. Each character gets its enemy to attack. To not overengineer the scenario and to not sway from the topic, the assessment logic simply examines the change in the health of each enemy. Additionally, the information is logged into the console to illustrate the difference. The tests are identical concerning the implementation before the visitor pattern and after, the only difference is the character class implementations. Here is the sample code for one of the tests:

The results of the tests are identical. Here is the result for the implementation without the visitor pattern:

Here is the result after the visitor pattern was implemented:

Thoughts

Considering the amount of effort it takes to implement game logic in game development, it is essential to find an efficient way to do that. Using software design patterns is considered good practice but the patterns have to be applied with a reason and caution. It is often tempting to over-complicate design and implementation, which, in result, puts the success of a product at risk. Unity3d coding approach follows a component-based design (Component-based software engineering, 2020). It is quite challenging to follow object-oriented principles in such an environment when architecture works against you. However, in certain isolated components, it is possible to tackle problems by following common best practices. The visitor pattern appears to be useful in scenarios when we have to deal with multiple objects of the same structure that have to implement certain operations. Visitor lets us keep related operations together by defining them in one class (Gamma, et al. 1995). This opens additional opportunities to make use of the reach feature set of the engine when we want to influence the data of the visitor classes.

Afterword

The implementation of the visitor pattern lacks the so-called common Apply method. This is done intentionally and considered unnecessary by me because the way the instances of the visitor classes are resolved satisfies the overall idea of the pattern. Of course, it is possible to add another level of abstraction in the character classes such as ArcherDamageSystem, ArcherStatsSystem that would in turn use the visitor classes and expose the Apply method, but I believe this is not the point here and ignoring Apply method is acceptable due to the circumstances.

References

Component-based software engineering. (2020, June 29). In Wikipedia. Retrieved from https://en.wikipedia.org/w/index.php?title=Component-based_software_engineering&action=history

Gamma, Erich, et al. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley, 1995.

Technologies, Unity. “ScriptableObject.” Unity, 15 Oct. 2018, docs.unity3d.com/Manual/class-ScriptableObject.html.

--

--

George Vasilchenko

Believes in software development as a professional discipline as opposed to the ability to write code.