Methods of organizing the interaction between scripts in Unity3D.

Introduction.

Even average Unity3D project is very quickly filled with a large number of various scripts and there is a question of interaction of these scripts with each other.

This article offers some various approaches to organization of such interactions from simple to advanced and describes to what problems can lead each of approaches, and will also offer ways of solution of these problems.

Approach 1. Appointment through Unity3D editor.

Let it be in our project are two scripts. First scratch is responsible for addition of points in game, and second for user interface, which displays number of scored points on game screen.

We will call both scripts – managers: ScoresManager and HUDManager.

In what way manager, who is responsible for screen menu, can receive current quantity of points from manager, who is responsible for addition of points?

It is supposed, that in hierarchy of objects (Hierarchy) of a scene there are two objects, for one of which ScoresManager script is appointed, and for other – HUDManager script.

One of approaches contains the following principle:

In UIManager script we define a variable type ScoresManager:

public class HUDManager : MonoBehaviour
{
    public ScoresManager ScoresManager;
}

But ScoresManager variable needs to be initialized by instance of a class. For this we will choose object in hierarchy of objects, to which HUDManager script is appointed and in object settings we will see ScoresManager variable with None value.

Further, from a window of hierarchy we move the object, that contain ScoresManager script, in area, where None is written and we appoint it the declared variable:

Then, we have an opportunity from HUDManager code address to ScoresManager script, thus:

public class HUDManager : MonoBehaviour
{
    public ScoresManager ScoresManager;
 
    public void Update ()
    {
        ShowScores(ScoresManager.Scores);
    }
}

Everything is simple, but game isn’t limited to only gathered points, HUD can display the current lives of player, menu available actions of player, information on level and many other things. Game can total in itself tens and hundreds of various scripts, that need to receive information from each other.

To obtain data from other script in one script we should describe every time a variable in one script and to appoint (move manually) it by means of the editor, that on its own tiresome work, which can easily forget to be made and then long to look for what of variables isn’t initialized.

If we want to edit something, rename a script, all old initialization into hierarchies of objects, connected with renamed script, will be reseted and it is necessary to appoint them again.

At the same time, such mechanism doesn’t work for prefab (prefab) – dynamic оbject сonstruction from a template. If any prefab needs to address to manager located in hierarchy of objects, you won’t be able to appoint to prefab an element from hierarchy, and it is necessary firstly to сonstruct an object from prefab and after that programmatically to appropriate an instance manager of variable to just сonstructed object. Blind work, unnecessary code, additional coherence.

Following approach solves all these problems.

Approach 2. “Singltons”.

We will apply the simplified classification of possible scripts, which are used at game construction. First type of scripts: “scripts-managers”, second: “scripts-game-objects”.

The main difference of one from others that “scripts-managers” always have the single copy in game while “scripts-game-objects” can have more instances.

Examples:

As a rule, in a single copy there are scripts which are responsible for general logic of user interface, for playing of music, tracking of level termination condition, system of tasks management, display of special effects and so on.

At the same time, scripts of game objects are in a large number of copies: each birdie from “Angry Birds” is guided by script instance of a birdie with their unique state; for any unit in strategy the unit script instance, containing its current quantity of lives, position in the field and personal purpose is constructed; behavior of five different icons is provided with various instances of the same scripts, which are responsible for this behavior.

In an example from previous step HUDManager and ScoresManager scripts always are in a single copy. For their interaction we will apply a pattern “singleton” (Singleton, same as single).

In ScoresManager class we will describe static ScoresManager property, in which a single copy of  manager of points will be stored:

public class ScoresManager : MonoBehaviour
{
   public static ScoresManager Instance { get; private set; }
   public int Scores;
}

It is also necessary to initialize Instance property with instance of a class, which is constructed by Unity3D framework. As ScoresManager the successor of MonoBehaviour is, so it participates in life cycle of all active scripts in a scene and during initialization of a script it Awake method get called. In this method we place Instance property initialization code:

public class ScoresManager : MonoBehaviour
{
	public static ScoresManager Instance { get; private set; }
	public int Scores;

	public void Awake()
	{
		Instance = this;
	}
}

Then, it is possible to use ScoresManager from other scripts as follows:

public class HUDManager : MonoBehaviour
{             
   public void Update ()
   {
      ShowScores(ScoresManager.Instance.Scores);
   }
}

Now there is no need for HUDManager to describe a ScoresManager field and to appoint it in Unity3D editor, any “script-manager” can provide access to itself through static Instance property, which will initialize as Awake.

Pluses:

  • there is no need to describe a script field and to appoint it through Unity3D editor.
  • it is possible safely to edit a code, if something falls off, the compiler will let know.
  • now it is possible to address to other “scripts-managers” from prefab, through Instance property.

Minuses:

  • approach provides access only to “scripts-managers” existing in a single copy.
  • strong coherence.

We will dwell upon last “minus”.

Let us develop game, in which there are characters (unit) and these characters can die (die).

Somewhere there is a code site, which checks whether our character died:

public class Unit : MonoBehaviour
{
    public int LifePoints;

    public void TakeDamage(int damage)
    {
        LifePoints -= damage;
        if (LifePoints <= 0)
            Die();
    }
}

How game can react on the death of character? Set of various reactions! I will give some variants:

  • it is necessary to remove the character from a game scene that he wasn’t displayed on it any more.
  • in game points for each died character are added, it is necessary to add them and to update value on screen.
  • on the special panel all characters in game are displayed, where we can choose the specific character. When the character died, we need to update the panel, or move away the character from it, or display that he is dead.
  • it is necessary to replay sound effect of death of the character.
  • it is necessary to replay visual effect of death of the character (explosion, blood splashes).
  • the system of game achievements has achievement, which counts total number of the killed characters during all the time. It is necessary to add to the counter of just died character.
  • the system of game analytics sends the fact of death of the character to external server, to us this fact is important for process tracking of the player.

Considering all above-mentioned, Die function can look as follows:

private void Die()
{
   DeleteFromScene();
   ScoresManager.Instance.OnUnitDied(this);
   LevelConditionManager.Instance.OnUnitDied(this);
   UnitsPanel.Instance.RemoveUnit(this);
   SoundsManager.Instance.PlayUnitDieSound();
   EffectsManager.Instance.PlaySmallExplosion();
   AchivementsManager.Instance.OnUnitDied(this);
   AnaliticsManager.Instance.SendUnitDiedEvent(this);
}

It turns out that the character after its death has to send out to all components, which are interested in it, this sad fact, he has to know about existence of these components and has to know that they are interested in it. Isn’t that a bit too much knowledge just for a small unit?

As game, logically, is a very linked structure, so events, occurring in other components, have interest to third, unit isn’t special here.

Examples of such events (by no means all):

  • The condition passing of level depends on number of points scored, if you have 1000 points – you have passed level (LevelConditionManager is connected with ScoresManager).
  • When we score 500 points, we reach an important stage of passing of level, it is necessary to replay a festal melody and visual effect (ScoresManager is connected with EffectsManager and SoundsManager).
  • When the character restores health, it is necessary to replay effect of treatment over the picture of the character in panel of the character (UnitsPanel is connected with EffectsManager).
  • and so on.

As a result of such communications we come to a picture similar on following, where everyone knows about everything:

Example with death of the character is squib, it is necessary to report about death (or other event) to six different components not so often. But variants, when at some event in game, function, in which there was an event, reports about it to 2-3 other components, meets pretty often on all code.

The following approach tries to solve this problem.

Approach 3. World ether (Event Aggregator).

We will enter the special EventAggregator component, main function of which is to store the list of events occurring in game.

Event in game is the functionality giving to any other component opportunity both to subscribe for itself, and to publish the fact of commission of this event. Realization of functionality of event can be any on taste of developer, it is possible to use standard solutions of language or to write own realization.

Example of simple realization of event from last example (about death of a unit):

public class UnitDiedEvent
{
    private readonly List<Action<Unit>> _callbacks = new List<Action<Unit>>(); 

    public void Subscribe(Action<Unit> callback)
    {
        _callbacks.Add(callback);
    }

    public void Publish(Unit unit)
    {
        foreach (Action<Unit> callback in _callbacks)
        callback(unit);
    }
}

We add this event to “EventAggregator”:

public class EventAggregator
{
   public static UnitDiedEvent UnitDied;
}

Now, Die function from previous example with eight lines will be transformed to function with one line of code. We don’t have need to report that the unit died to all interested components and to know about these interested. We simply publish the event fulfillment fact:

private void Die()
{
  EventAggregator.UnitDied.Publish(this);
}

And any component, to which this event is interesting, can react to it as follows (on example of manager, who is responsible for number of points scored):

public class ScoresManager : MonoBehaviour
{
    public int Scores;

    public void Awake()
    {
        EventAggregator.UnitDied.Subscribe(OnUnitDied);
    }

    private void OnUnitDied(Unit unit)
    {
        Scores += CalculateScores(unit);
    }	
}

In Awake function manager subscribes for event and delegate who is responsible for processing of this event. The handler of event accepts an instance of the dead unit as parameter and adds quantity of points depending on type of this unit.

In the same way, all other components, to whom the event of unit death is interesting, can be signed for it and process when the event occurs.

As a result, the chart of communications between components when everyone component knew about each other, turns into the chart when components know only about events which occur in game (only about interesting them events), but thei don’t care, from where these events came. The new chart will look as follows:

I like other interpretation: suppose the “EventAggregator” rectangle stretched extensively and took in itself all other rectangles, turned into world borders. In my head on this chart “EventAggregator” in general is absent. “EventAggregator” it simply the world of game, certain “game air” where various parts of game shout “Hey, people! Unit so-and-so died!”, and all listen to air and if some of the heard events interests them, they will react to it. Thus, there are no communications, each component is independent.

If I am a component and I am responsible for publication of some event, I shout in air that this died, this received level, shell crashed into the tank. And I don’t care if somebody cares. Perhaps, nobody listens to this event now, and it can be one hundred other objects is signed on it. I, as author of event, don’t care, I know nothing about them and I don’t want to know.

Such approach allows to enter easily new functionality without change old. Let’s say we decided to add system of achievements to ready game. We create new to a component system of achievements and subscribe for all interesting us events. No other code changes. It isn’t necessary to go on other components and cause from them system of achievements and tell it to count my event. Besides, everyone, who publish events in the world, knows nothing about system of achievements, even about the fact of its existence.

Remark:

Saying that no other code changes, of course I dissemble a little bit. It can appear so that system of achievements interest events, which earlier simply weren’t published in game because any other system didn’t interest before. And in this case, to us it will be necessary to decide what new events to add to game and who will publish them. But in ideal game are already all possible events, also air is filled with them in spades.

Pluses:

– not coherence of components, it’s suffice just to publish the event and it doesn’t matter who interested in it.

– not coherence of components, I simply subscribe for necessary to me events.

– it is possible to add separate modules without change in the existing functionality.

Minuses:

– it is necessary constantly to describe new events and add them to the world.

– violation of functional atomicity.

We will explore the last minus in full detail:

Put the case we have “ObjectA” object, in which «MethodA» method get called. «MethodA» method consists of three steps and get called in itself three other methods, which carry out these steps consistently (“MethodA1”, “MethodA2” and “MethodA3”). In second «MethodA2» method there is a publication of some event. And there is a following: everyone, who is signed on this event, will start  processing it, carrying out some logic. In this logic there can be also a publication of other events, which processing can also lead to publication of new events and so on. Publications tree and reaction in some cases can expand very strongly. It is so difficult to tweak such long chains.

But the most terrible problem, which can occur here, it when one of chain branches brings back into “ObjectA” and starts processing event by activation of some other «MethodB» method. It turns out that «MethodA» method haven’t executed all steps yet, as it was interrupted on the second step, and comprises now not valid state (in 1 and 2 step we changed a condition of object, but the last change of step 3 wasn’t made yet) and thus “MethodB” in the same object begins to carry out, having such not valid state. Such situations generate mistakes, and very difficult to recognise, lead to that it is necessary to control an activation methods order and publication of events, when, according to the logic, there is no need to do it and enter additional complexity, which it would be desirable to avoid.

Decision point:

It’s not difficult to solve the described problem, it is enough to add functionality of postponed reaction to event. As simple realization of such functionality we can get storage, in which we will put the occurred events. When the event occurred, we don’t carry out it immediately, and simply keep somewhere at ourselves. And at the time of occurrence run queue of functionality some components in game (in Update method, for example) we check for existence of occurred events and processing if there are such events.

Thus, by «MethodA» method execution there is no its interruption, and the published event everyone, who are interested in it, write down to their special storage. And only after they  have their turn, they will get an event from storage and process it. At this moment all “MethodA” will be complete and “ObjectA” will have a valid state.

Conclusion.

Computer game is difficult structure with a large number of components, which closely interact with each other. It is possible to construct a set of mechanisms of organization of this interaction, I prefer the mechanism, described by myself, based on events and to which I came in the evolutionary way overcoming difficulties. I hope somebody will also like it and my article will make things clear and will be useful.

Leave a Reply

avatar
  Subscribe  
Notify of