Today we will talk about how to store, receive and transmit data within the game. About the wonderful thing called ScriptableObject, and why it is wonderful. We’ll touch on the benefits of singltons in organizing scenes and transitions between them.
This article describes a piece of a long and painful way of developing a game, various approaches used in the process. Most likely, there will be a lot of useful information for beginners and nothing new for "veterans".
Links between scripts and objects
The first question facing a novice developer is how to tie all the written classes together and set up the interactions between them.
The easiest way is to specify a link to the class directly:
public class MyScript : MonoBehaviour { public OtherScript otherScript; }
And then manually attach the script through the inspector.
This approach has at least one major drawback - when the number of scripts exceeds several dozen, and each of them requires two or three links to each other, the game quickly turns into a web. One glance at her is enough to cause a headache.
Much better (in my opinion) to organize a system of messages and subscriptions, within which our objects will receive the information they need - and only that! - without requiring half a dozen references to each other.
However, having tested the topic, I found out that ready-made solutions in Unity are criticized by all and sundry. Writing from scratch a similar system for myself seemed to me a non-trivial task, and therefore I am going to look for simpler solutions.
ScriptableObject
To know about ScriptableObject you need, in fact, two things:
- They are part of the functionality implemented inside Unity, like MonoBehaviour.
- Unlike MonoBehaviour, they are not tied to the objects of the scene, but exist in the form of separate assets and are able to store and transfer data between gaming sessions .
I immediately loved them with hot love. In a way, they have become my panacea for any problem:
- Need to store game settings? ScriptableObject!
- Create inventory? ScriptableObject!
- Write AI? ScriptableObject!
- Write information about the character, enemy, subject? ScriptableObject will never let you down!
Without thinking twice, I created several classes of the ScriptableObject type, and then a storage for them:
public class Database: ScriptableObject { public PlayerData playerData; public GameSettings gameSettings; public SpellController spellController; }
Each of which stores in itself all the useful information and, possibly, links to other objects. Each of them is enough to bind once through the inspector - they will not get anywhere else.
Now I do not need to specify an infinite number of links between the scripts! For each script, I can
once specify a link to my repository - and it will get all the information from there.
Thus, the calculation of the speed of the character takes a very elegant look:
And if, say, a trap should only work on a running character:
if (database.playerData.isSprinting) Activate();
And the character does not need to know anything about the spells, nor about the traps. It simply receives data from the repository. Not bad? Not bad.
But almost immediately I encounter a problem. ScriptableOnjects cannot store references to scene objects directly. In other words, I cannot create a link to the player, link it through the inspector and forget about the question of the coordinates of the player forever.
And if you think it makes sense! Assets exist outside the scene and can be accessed in any of the scenes. And what happens if you leave a link to an object in another scene inside an asset?
Nothing good.
For a long time, a crutch worked for me: a public link is created in the repository, and then each object, the link to which you need to remember, filled this link:
public class PlayerController : MonoBehaviour { void Awake() { database.playerData.player = this.gameObject; } }
Thus, regardless of the scene, my storage first gets a link to the player and remembers it. Now any, say, the enemy should not keep in itself a link to the player, should not look for him through FindWithTag () (which is a rather resource-intensive process). All he does is contact the repository:
public Database database; Vector3 destination; void Update () { destination = database.playerData.player.transform.position; }
It would seem: the system is perfect! But no. We still have 2 problems.
- I still have to manually specify a link to the repository for each script.
- It is inconvenient to assign links to scene objects inside a ScriptableObject.
About the second in more detail. Imagine that a player has a spell of fire. The player casts it, and the game says to the repository: the light is cast!
database.spellController.light.CastSpell();
And this generates a series of reactions:
- A new (or old) gameobject light is created at the cursor point.
- A GUI module is launched, which tells us that the light is active.
- Enemies receive, say, a temporary bonus to the detection of a player.
How to do all this?
It is possible for each object interested in the fire, right in Update () and write, they say, and so, each frame monitor the light (if (database.spellController.light.isActive)), and when it lights up - react! And do not care that 90% of the time this check will be idling. On several hundred objects.
Or organize it all in the form of ready-made links. It turns out that the unpretentious function CastSpell () should have access to the links to the player, to the light, and to the list of enemies. And this is at best. Too many links, eh?
You can, of course, save everything important in our repository when launching the scene, scatter links on assets, which, in general, are not intended for this ... But I again violate the principle of a unified repository, turning it into a web of links.
Singleton
This is where singleton comes into play. In essence, this is an object that exists (and can exist) only in a single copy.
public class GameController : MonoBehaviour { public static GameController Instance;
I tie it to an empty scene object. Let's call it GameController.
Thus, I have an object in the scene that stores
all the information about the game. Moreover, it can move between scenes, destroy its counterparts (if there is already another GameController on the new scene), transfer data between scenes, and, if desired, implement saving / loading of the game.
Of all the scripts already written, you can remove the link to the data warehouse. After all, now I do not need to configure it manually. All references to the objects of the scene are removed from the repository and transferred to our GameController (all the same, we will most likely need them to save the state of the scene when leaving the game). And then I fill it with all the necessary information in a convenient way. For example, in Awake () of a player and enemies (and important objects of the scene), the addition of themselves to GameController is prescribed. Since now I work with Monobehaviour, the links to the objects of the scene fit into it very organically.
What do we get?
Any object can get
any information about the game that it needs:
if (GameController.Instance.database.playerData.isSprinting) ActivateTrap();
At the same time, there is absolutely no need to set up links between objects, everything is stored in our GameController.
Now there will be nothing difficult in saving the state of the scene. After all, we already have all the necessary information: enemies, objects, player position, data storage. Simply select the scene information you want to save and write it to a file using FileStream when exiting the scene.
Danger
If you have read this far, I should warn you about the dangers of such an approach.
A very bad situation develops when many scripts refer to one variable inside our ScriptableObject. There is nothing bad in getting the value, but when a variable is started to be affected from different places - this is a potential threat.
If we have a saved playerSpeed variable, and we need the player to move at different speeds, do not change playerSpeed in the repository, get it, save it to a temporary variable and impose speed modifiers on it.
The second point - if any object has access to anything - this is a great power. And a big responsibility. And it must be approached with caution so that some fungus script would not accidentally break completely your entire AI of enemies. Properly configured encapsulation will reduce the risks, but will not save you from them.
Also, do not forget that singletons are gentle creatures. Do not abuse them.
For today - everything.
Much has been gleaned from official tutorials on Unity, something from unofficial ones. I had to reach something myself. So, the above approaches may have their own hazards and shortcomings, which I missed.
That is why discussion is welcomed!