I come from many years of ActionScript and purely object-oriented coding (that’s why I felt strange with iTween‘s gameObject oriented-ness, and built my own object-oriented Unity tween engine – HOTween). Even if I immediately plunged into Unity quite seriously, with my first job being the development of a complex interactive museum, I’m into Unity since one year only. Thus I obviously can’t state that my approach is the best one. It’s just AN approach: I personally feel very comfortable with it, and it’s quite flexible.
Journeyballs uses a mix of centralized approach and in-scene aggregation. Various global managers control all the different aspects of the game (like scoring, saving, event dispatching, etc.), scene-specific “brains” control how each single level is setup and behaves, and gameObject-specific MonoBehaviours power game elements.
I prefer to use centralized approaches where possible, thus I rely heavily on static global managers. Some of them are singletons, and some of those singletons get attached to global (meaning non-destroyable) empty gameObjects in the scene (in case I need some MonoBehaviour power).
- GameManager: the core of the engine. As soon as its one-time initialization takes place, it loads the basic game settings (set via Journeyballs in-editor panels) and the user-specific data (via the SaveManager). After that, any other object can just query it to know game-specific settings to apply.
- AspectManager: when initialized, gets the current screen size, then calculates and stores all size-dependent properties. Also sets the textures resolution, depending on the screen’s pixels. All GUI related code relies heavily on it to know where to place stuff, allowing for a liquid layout that will fit any existing display.
- Notificator: all event dispatching goes through this singleton. This way, listening to an event simply consists of linking to a Notificator event. Maintaining a single global event dispatcher can be a little tricky, but if you do it correctly it is very powerful, and allows total separation of objects (since objects don’t need to be conscious of each other to access specific events).
- LevelManager: each time a new scene/level is loaded, this manager detects its level-specific configuration, and sets it.
- SaveManager: takes care of saving and loading user preferences and achievements. Relies on the wonderfully straightforward Easy Save 2 for its internal methods.
- ScoreManager: keeps track of all the scores and stars, with methods that let me quickly get single level as well as world scores, and compare them with older ones.
- SoundManager: caches all sounds. Playing a sound when needed simply consists in calling SoundManager.Play(SoundEnumValue).
- MenuManager: controls all menus, managing their animations, their contents, and their behaviors.
Each scene/level contains an empty gameObject, which works as a “brain”. This brain has a series of MonoBehaviours attached, used to set each level’s options while in Unity’s editor. When a level is loaded, the LevelManager queries these settings, and sets them.
One of the most useful sides of the brain is to set special events while in Unity’s editor. Journeyballs in-editor panel connects to the current level’s brain, allowing me to do various stuff. For example, I can select objects and define them as “activators”, then select other objects and set them to be activated by a specific activator. During runtime, these settings will be evaluated each time a level is loaded, and the required classes/MonoBehaviours will be attached to the activators and to their targets.
In short, the brain is not actually a brain (not in the case of Journeyballs at least – I did other projects where it was more brainous). It’s more like a database of behaviors, injected into a specific level after its creation.
The game elements
This part is simple, and it’s where I fully use Unity’s aggregation workflow. First of all, each game element has a main DestroyableMonoBehaviour attached (which is just a customized MonoBehaviour that allows to control how an object gets destroyed). After that, having an object behave in a specific way simply consists of adding the desired MonoBehaviours to it. For example, a spiky wheel which rotates on its Z axis will have both a “Hazard” MonoBehaviour and a “Rotator” MonoBehaviour attached.