So, you started a new project in Unity. You added your first game objects and scripted their behaviour. Do you think that's it? Your game is ready to run? Well then, hold your horses. There are several core objects you would find in almost every application. And the chances are you will most likely need all of them.
When you create a scene, there's already a camera object – the Main Camera. Without it, you would only see a black screen.
Then there's the GameController – a class that you will write yourself, although it may be named differently. Sooner or later you'll need to keep a global state somewhere – enemies killed, matches won, levels explored... That would be your GameController. Sure, it may be composed of different parts – Stats, Preferences, Progress, Inventory, etc. – but it's a good practice to keep them under one parent object (or implement them as scripts attached to that parent GameObject in some cases). It becomes increasingly useful if you add another scene. The GameController doesn't die with the scene; it exists for the whole duration of the application (i.e. it should be marked with DontDestroyOnLoad()). That also means that the GameObjects attached to it are preserved as well. So if you want to keep anything around at all times, you can (and should) simply attach it to the GameController or any of its children. It might not be possible in rare cases (when the transformation of a persistent object depends on an object that will be destroyed), but otherwise it is a sound solution.
There's a problem, however, with the not destroyable objects in general. You can't just put them into a scene as regular. If you had multiple scenes and switched between them, the persistent objects would duplicate. You could put them into the first scene, which would be loaded on start-up and then never crossed again, and it would work in the final game. During development though, you would often run scenes at random and they would be missing the crucial information that's supposed to always be there. The only solution is to introduce a helper object that will always die with the scene. Upon initialisation it will check if the GameController is present, and if not, it will create it (from a prefab). We can call this watcher GameLauncher and put the global logic that doesn't apply outside the scene under it.
One of such global objects could be GUI. UnityGUI is entirely scripted. That means we can create a number of scripts, one script for each screen, for example, and put them virtually anywhere. But where does it make the most sense? Since we can have many screens, it's certainly more practical to dedicate a separate GameObject for them than to “hide” them in random existing objects. I've seen people attach the scripts to the main camera. It's one possibility. Still, there could be multiple cameras in the scene, they could be nested in other objects – it could become cumbersome to locate a script. Another option is to leave the GUI object freely in the scene. Although, if there were many independent game objects already, it would only add to the clutter. Also, there could be screens available through the whole application, such as pause menu, and the screens specific to a scene, such as HUD. We could signify the difference by attaching one GUI object to the GameController and another to the GameLauncher, which are the pillars that will not move.
When everything else is in place, you'll need to save and load your game state. So there would be a SaveManager, which Unity doesn't provide and which you'll have to implement yourself, too. This is a huge topic and I plan to write more on it later.
Saturday, 29 June 2013
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):
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.
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.
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.
Finally, we can sketch our example application's logic adapted to Unity:
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):
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.
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.
- Each object in the diagram should be represented by a GameObject. Those are initially added to the scene as a flat list.
- If an object's position, size, or rotation depends on another object, it should be its child.
- If a number of objects resembles a collection, they should be grouped under a supplementary node that will work as a “container” of sorts.
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.
Finally, we can sketch our example application's logic adapted to Unity:
Subscribe to:
Posts (Atom)