Sweeper
An overview of the gameplay and system architecture of this Minesweeper rendition
Jump to Code Feature
Showcase
More effects…
Play like the classic…
Win the game to see your stats…
Or lose to get some encouragement.
Manager Implementation
The manager implements the Singleton Pattern, and is MonoBehavior-based. There is an abstract AManager class where all manager classes inherit from. It uses C# generics to alleviate most of the repetitive setup required for a singleton manager class. Check out the Github page fore more detail.
Above is a code snippit of the AManager class. I didn’t go with lazy instantiation because I found it to create a lot of null reference exceptions which often came out of nowhere. I opted for manually placing manager classes in scenes also because there are only a few scenes and a few managers.
I later learned that it is considered a bad practice to reference other objects in the Awake method which I did a lot in this project. No wonder.
For an implementation of ScriptableObject-based Singleton manager, see project Snatcher for more details.
Value Reference
I used ScriptableObjects for value referencing very extensively throughout the project. Since I realized that, I created a base abstract class ATypeRef for all of the common use cases, inlcuding bool, float, int, string, and Vector3 values.
The code for ATypeRef is super simple as below.
Below is an example of how to make a concreate class of ATypeRef.
Event System
The event system is ScriptableObject-based, and I learned this from Dapper Dino’s video series. The system consists of three parts: the ScriptableObject event, the Monobehaviour event listener, and UnityEvent responses.
Below is a diagram demonstrating the relations between the three parts when the game loads and listeners start registering for callbacks. Note that ScriptableObject is an asset inside the asset folder, meaning that it is scene-transcendent, whereas a listener is a transient MonoBehaviour that is a component on a GameObject in a scene.
The next diagram shows what happens when some other entity X triggers the event.
The examples above show the event type Void which is similar to C# Action event that takes no argument and returns nothing. There are other event types based on different data types that are serializable. Below are three diagrams showing the respective inheritance relations of all three parts to the system.
AGameEvent is an abstract class that is of type ScriptableObject. It is the parent of all event classes.
UnityEvent is a class provided by Unity that we can override. It is the parent of all UnityEvent classes that handles the responses the user can specify in the Inspector.
AGameEventListener again is an abstract class. It implements the interface IGameEventListener that handles firing responses specified in the user-created UnityEvent instances.
More details on the GitHub page.
The Listener registers to listen to the event’s invokation when OnEnable and deregisters from it when OnDisable.
The UnityEvent does not do anything yet.
Animation System
The animation system implements the Facade Pattern by encapsulating DoTween’s tweening functionalities into a ScriptableObject. Objects like Transform, Image, or TextMeshPro Text can pass themselves as arguments into these ScriptableObjects and perform animations.
I came up with two use cases for these animations: one where the target/destination values are known and can be serialized in the Inspector, and the other where the values are only known at run time.
The first case is useful when tweening UI elements where I already know, say, the extent to which I want a button to scale up. This type of animation is packaged into ASerializedTargetAnimation as shown below:
An example of ASerializedTargetAnimation is ImageColorTo:
As you can see, the target color for the Image, the variable _targetColor, is to be set in the Inspector.
You can also see, as with the case of ADynamicTargetAnimation too, that the first argument of PerformAsync is always the object to perform the animation on. This means that we can create a “preset” animation and pass all compatible types themselves to use this animation.
The second case is useful when randomness is introduced, for example, when I want every block to rotate to face toward some target and I want that target to be assigned dynamically at run time. This type of animation is packaged into ADynamicTargetAnimation as you can see below:
This time, the target is actually specified as the second argument of PerformAsync, which allows it to be determined at run time.
One use case that I have in the project is the RotateTowards animation:
Find more examples on the GitHub page.