Tuesday, 25 June 2013

Object vs. Scene Hierarchy

If you've designed some applications before, you've probably got used to certain ways to define their basic structure. When writing traditional system applications, you learn that you have one entry point and you make a main control loop to manage things, and from there it's pretty straightforward. Following the object-oriented programming paradigm, you define objects and link them together in a big, happy, intuitive hierarchy. It is common to structure your logic first and then to think about visualisation, serialisation, and whatever else you need to do with it.

A simple game of flying an air plane might look somehow like this, for the first time in your head (and using class diagram from UML):

[Fig. 1: Application logic diagram]

The Application is your entry point. It accepts parameters, starts the game logic and supporting subsystems (logging, for example), and quits with an error if something goes terribly wrong. The GameController governs the game control loop. A player interacts with the AirPlane, rendered on screen, that is also affected by the Wind, which acts behind the scenes.

In Unity, however, the control loop is managed for you under the hood[1], the entry points are independent for each game object[2][3], and the editor is geared towards defining what you see in the scene instead of what is going on logically (which is not the same thing). So how do we get from the picture in our head to something akin to Unity that won't be a hell to maintain?



First, forget about the Application; it's already there. GameController will also play a quite different role. In some simple set-ups you won't even need it. Everything else needs to be transposed to Unity, and there are several ways to go about it.

It is important to realise that an object hierarchy is not a scene hierarchy. What we see at the Hierarchy pane in the editor is not the logic structure of our application. If you went and created an Application GameObject, put a MonoBehaviour on it, added a child GameObject named GameController, and so on, so you would have replicated the diagram, it wouldn't have worked. In the scene hierarchy, you link objects' transformations, yet in the object hierarchy (the diagram), you associate objects logically. Imagine, in our example, that the plane belongs to an airport. If we reassign it to another airport in the scene, it gets teleported to another place. Now, if the airport would have been an aircraft carrier ship, which would have moved, we would have been in real trouble. In the object hierarchy, on the other hand, such reassignment does only what we define it to do.

[Fig. 2: Object hierarchy transposed as scene hierarchy]

Okay, if nesting GameObjects is such a problem, why don't we bypass them altogether? We could build the object hierarchy depicted on the diagram from ordinary classes that do not derive from the MonoBehaviour. Then let it be instantiated and managed by a MonoBehaviour attached to an arbitrary GameObject. This somewhat reminds me of an MVC approach, of which I'll write a separate article later. Still, in order to draw the air plane and process player's input, we'd have to add another GameObject that would reference (and possibly update) the AirPlane object in our isolated object hierarchy. The same goes for every object that needs to be visualised. In a game, that would be about a vast majority. The number of objects would roughly double, there'd be an overhead of referencing between the associated pairs, it'd be more difficult to work with (classes would become fragmented) and it would hinder fast prototyping changes. You could argue that we had at least spared an unnecessary GameObject for the Wind, which is only simulated and not drawn. The truth is – we had also severed its parameters from tweaking in the editor. As you can see, this approach brings more disadvantages than benefits. Unless you're making a framework, I wouldn't recommend to use it.

The only option that remains is the middle ground – to utilise GameObjects as much as possible, but to organise them under different rules than logic.
  1. Each object in the diagram should be represented by a GameObject. Those are initially added to the scene as a flat list.
  2. If an object's position, size, or rotation depends on another object, it should be its child.
  3. If a number of objects resembles a collection, they should be grouped under a supplementary node that will work as a “container” of sorts.
This way the scene layout will be transparent and you can cross-reference it with a class diagram easier. In simple words, it will add to clarity. Our slightly expanded flying example would come up as follows (Application and ModelToRender are omitted because they are directly supported by Unity):

[Fig. 3: Scene hierarchy as it should be]

Now, there's no need for the Wind to be actually a GameObject. It will not be drawn, so it can be as easily attached as a script to, for example, the GameController. It will lose on some of the aforementioned clarity, however. And there's always the chance someone will come up with a striking idea of how it would be great to visualise the wind in the middle of the project.

Notice how many objects are on equal level. While in a traditional system application the object hierarchy usually resembles a pyramid, descending top down (from the Application object, in our example), Unity forces us to build in reverse. The reason why the pyramids are popular is that it's natural, starting from one entry point, to delegate more complicated tasks to serving objects. The objects on top control the objects directly below them, establishing a strict chain of command. Ideally, the objects below don't even know about anything on top, so inter-dependencies are minimised and thus removing code effects only local changes. This way we divide and conquer. With Unity, every MonoBehaviour script is an entry point of itself. Additionally, any MonoBehaviour can access any other MonoBehaviour at will. Such environment cannot be controlled and originally higher-standing managing objects, such as the GameController, take on a more passive role. There's no more need to “own” instances or update them; it's already provided for. For example, if you were to model a clock, instead of a clock object moving its hand objects you would do better to make the clock hands move on their own and only ask the clock about the time. This way, if you, let's say, later decided to add a clock hand that would display seconds, you wouldn't have to touch the clock object at all. In such set-up we don't need the GameController to reference other objects and update them. It becomes an arbiter that carries a global game state through multiple scene loads and executes general functionality, such as saving or loading data, for instance.



Considering what has been said about the bottom-up structuring, the directions of associations between objects in our diagram will often reverse. Composition will turn into aggregation, as we no longer own objects. Usually, we don't even instantiate or destroy them. An association will look like a plain reference in Unity, which we can pre-fetch and keep for faster access. We can achieve that either by calling the GameObject.Find() method[4] during initialisation, or by assigning an exposed variable in the editor. Both techniques are useful depending on a situation.

Inheritance may prove a little tricky though. Let's say we want to have different types of the AirPlane – a WarPlane and a PassengerPlane. The AirPlane will contain a common logic that has nothing to do with Unity whatsoever. Only the WarPlane and the PassengerPlane will be attached to GameObjects. It's easy to start writing the AirPlane as a simple class, yet the WarPlane and the PassengerPlane need a MonoBehaviour to draw them. So you might go and write a MonoWarPlane that would inherit from the MonoBehaviour and hold the WarPlane. That would in turn require a special kind of wings, which would have their own separate MonoBehaviour to draw them. And just like that you could end up doubling all objects, with all the ills of our second scenario about an isolated hierarchy. The solution is luckily simple, if not always obvious – derive base classes from the MonoBehaviour.

[Fig. 4: Inheritance trap]
[Fig. 5: Prefered way of inheritance]

Finally, we can sketch our example application's logic adapted to Unity:

[Fig. 6: Final sketch]

No comments:

Post a Comment