How To Make GameObjects Communicate With Runtime Instantiated Prefabs

Posted by : on

Category : Unity

Introduction

In Unity many times we find ourselves creating Game Objects at runtime. Connecting those game objects to existing game objects in the scene can be done with many different ways.

Each way has advantages and disadvantages that depend on our requirements. Some are easier to implement than others, while others can scale better with bigger projects.

Here are some ways I have found to accomplish this. For the examples, I use the following scenario:

Let’s say that we create enemies at runtime, but we want something to happen to those enemies when something happens to our player character, for example whenever we click our player the enemies change color.

Obviously this could be anything, like when our player dies, the enemies get destroyed or when our player reaches a certain position in our game world, the enemies return to their starting positions or whatever.

I will use the following script for our enemies:

public class Enemy : MonoBehaviour
{ 
   [SerializeField] private SpriteRenderer rd;
 
   public void ChangeColor(Color color)
   {
      rd.color = color;
   }
}

and the OnMouseDown() to represent what happens to the player.

The Find methods of Unity

Unity offers the Find group of methods, that searches the scene and finds game objects with certain parameters:

FindObjectOfType, FindObjectsOfType, FindFirstObjectByType, FindAnyObjectByType

These methods are very slow, they return objects of the provided type, the type can be a parameter to the method or a generic parameter, for example:

public class Player : MonoBehaviour
{
   private void OnMouseDown()
   {
      var enemy = (Enemy)FindObjectOfType(typeof(Enemy));
      ChangeEnemiesColor(enemy);
   }

    private void ChangeEnemyColor(Enemy enemy)
    {
       enemy.ChangeColor(Color.blue);
    }
}

will only change the color of the first enemy that will be found, while the following code:

private void OnMouseDown()
{
    var enemies = FindObjectsOfType<Enemy>();
    foreach (var enemy in enemies)
    {
        ChangeEnemyColor(enemy);
    }
}

will change the color of all the game objects that are of type enemy in our scene.

The FindFirstObjectOfType is recommended in place of FindObjectOfType because it is faster (but still very slow) and the FindAnyObjectByType is recommended if we don’t care about a specific object.

GameObject.Find

The Find method, returns a gameObject by its name in the hierarchy:

private void OnMouseDown()
{
    var go = GameObject.Find("Enemy(Clone)");
    if(go.TryGetComponent(out Enemy enemy))
        ChangeEnemyColor(enemy);
}

This is another bad solution, not only because of its performance, but also because we are dependent on strings that are the names of our objects when created at runtime.

FindWithTag and FindGameObjectsWithTag

These two methods find game objects by tag. Tags in Unity are strange, as they resemble labels. A game object has only one tag and that can be a problem if we want different game objects to react to different events from many sources.

Eventually our code will become a mess. Although faster than the previous methods, they don’t scale well with many different events from many different sources while still being slow because the engine has to perform a search.

The usage of strings here is also a problem, as it is used in two different places, in our code and also in the editor as a tag. This makes those methods really prone to bugs, as we have to remember whenever a tag changes, to search our code in all the places for the corresponding string.

Here is an example usage:

private void OnMouseDown()
{
    var gos = GameObject.FindGameObjectsWithTag("Enemy");
    foreach (var go in gos)
    {
        if (go.TryGetComponent(out Enemy enemy))
            ChangeEnemyColor(enemy);
    }
}

this works only as long our Enemy prefab has the “Enemy” tag.

Another problem with all the Unity provided find methods, is that they only work with active game objects. If for whatever reason, we have disabled game objects in our scene, that we want to perform something on them before we enable them, then we are out of luck.

Generally I don’t recommend those methods, unless we want to do a fast test to see if our logic works, then we can refactor by using one of the following ways to connect our game objects.

Static Events

A static event belongs to the type and not the instance. The following code will change the color of our enemies:

public class Player : MonoBehaviour
{
   public static event Action<Color> PlayerClicked;
   private void OnMouseDown()
   {
      PlayerClicked?.Invoke(Color.blue);
   }
}
public class Enemy : MonoBehaviour
{
   private void OnEnable()
   {
      Player.PlayerClicked += ChangeColor;
   }

   private void OnDisable()
   {
      Player.PlayerClicked -= ChangeColor;
   }

   [SerializeField] private SpriteRenderer rd;
 
   private void ChangeColor(Color color)
   {
      rd.color = color;
   }
}

This code has the problem that it will perform the same whenever one of our players gets clicked. If we want to make a change to our game that adds another player, for example we want to add co-op, then it is time for refactoring.

Besides that, it has the problem that all the global variables have, encapsulation is not existent, anything can subscribe to that event, plus because it is static, we have to remember to write code for the initialization of our scene if we use the domain reload feature of Unity.

Static Lists

This is my preferred method for fast prototyping. We can add a static list of enemies, either in the Player or in the Enemy class. Then we can iterate that List and call the enemy method we want.

Whenever an enemy is instantiated, it adds itself to the list and removes itself when it is destroyed or disabled. In which class we add the list, has different pros and cons.

If we add it to the Enemy class, we keep the information about our enemies where it belongs, there is no reason for our Player class to have a list of enemies and that won’t scale well, if we have a list in our Player for every different type of object that is created at runtime:

public class Enemy : MonoBehaviour
{
   [SerializeField] private SpriteRenderer rd;

   public static List<Enemy> EnemyList;
   
   private void Awake() => EnemyList.Add(this);

   private void OnDestroy() => EnemyList.Remove(this);

   public void ChangeColor(Color color) => rd.color = color;
}
public class Player : MonoBehaviour
{
   private void OnMouseDown()
   {
      foreach (var enemy in Enemy.EnemyList)
      {
         enemy.ChangeColor(Color.blue);
      }
   }
}

On the other hand, this creates a dependency in the wrong direction. A high level policy (our player) is dependent in a lower level policy (the Enemy). Any time we make a change to the Enemy class there is a chance to affect the player class. This can be really cumbersome in classes that can often change, like UI objects that we generate at runtime.

Besides those pros and cons, static lists suffer from the same problems of static events: They are global variables and we have to remember to write initialization logic if we use the domain reload in Unity.

Mediator

Using a mediator is not a different way than the previous two. It is just a way, to encapsulate our data in a different class and avoid the dependencies between the classes that use that data.

A mediator class can be done in a different number of ways:

  1. We can have a class that contains the static lists we need. The same problems of the static variables exist in this solution too, but at least now the Player class and the Enemy class don’t know about each other and a change to one won’t affect the other.
  2. Our mediator can be a singleton. Now our list doesn’t have to be static, but this makes our mediator prone to all the singleton’s problems.
  3. Our mediator can be a Monostate. That is essentially the same solution as 2. The monostate ensures a single state of our List, but has potentially the same problems that a singleton has.

Our mediator doesn’t have to contain a list. It can actually have an event that the Enemy subscribes to and the Player invokes indirectly via a public method.

When the relevant method in the mediator that performs an action to the Enemy is called, can be controlled in the Player class. That makes it easier for the player class to perform checks when to call that method. For example adding a second player now, can be done easily by performing the logic we need before calling it. (ex. when a player is clicked a boolean flag becomes true, if all players have their flags true only then we call the method.)

Scriptable Object

The Scriptable Object solution, is my preferred solution for larger projects. Its only problem is that it takes more time to implement because we have to write more code, but it scales better, is not a dependency nightmare, any change can be done easily, the logic is contained and can be easily swapped in the editor.

The idea is that the scriptable object will contain the Player events and any prefab that needs those events will have a field that takes our scriptable object.

Scriptable objects in Unity behave like singletons, they have the same state, but are also assets. That means they get unloaded from memory when there are no more references to them, so they don’t take space in memory when they are not needed any more, in contrast with static variables that are always there. Beside that, they don’t have the global variable problem of the static lists. They can be accesses only by the objects we want them to.

Because we can add the scriptable object in our prefabs, no search in runtime when the prefabs are instantiated is needed, only the subscription to the relevant events. Here is an example:

Our Scriptable Object:

[CreateAssetMenu(fileName = "PlayerEvents", menuName = "PlayerEvents", order = 0)]
public class PlayerEvents : ScriptableObject
{
   public event Action<Color> PlayerClicked;

   public void OnPlayerClicked(Color color) => PlayerClicked?.Invoke(color);
}

Our Player class:

public class Player : MonoBehaviour
{
   [SerializeField] private PlayerEvents playerEvents;
   
   private void OnMouseDown() => playerEvents.OnPlayerClicked(Color.blue);
}

Our Enemy class:

public class Enemy : MonoBehaviour
{
   [SerializeField] private SpriteRenderer rd;
   [SerializeField] private PlayerEvents playerEvents;

   private void Awake() => playerEvents.PlayerClicked += ChangeColor;
   private void OnDestroy() => playerEvents.PlayerClicked -= ChangeColor;

   private void ChangeColor(Color color) => rd.color = color;
}

For every new player event we want to add, we add it to the PlayerEvents scriptable object and we call it from the player class after any checks we want to perform.

Whichever class needs to have knowledge of when something happened to the player, it just needs a reference to our PlayerEvents scriptable object so it can subscribe to the event.

Our prefabs can also have a reference to the PlayerEvents SO without the need to search for the Player or the mediator at runtime and without having any global static fields.

Finally there are no dependencies between the Player and the Enemy class, the Scriptable Object acts as a mediator between them.

Any class can know when something happened to the player, by subscribing to one, many or all the events, just by having a reference to our SO. We can easily test our gameobjects in isolation by adding to the scene only the ones we want without the need to bring more to the scene because of dependencies and if there are no references to the scriptable object it gets automatically unloaded from memory by Unity.

All in all, I think this is the most elegant solution that scales better, with the only con that it requires a little bit more code.

Conclusion

If you have any more solutions for creating dependencies for prefabs instantiated at runtime, I would love to hear from you.

I hope you found this post useful. Thank you for reading and as always, if you have any questions or comments you can use the comments section or contact me directly via the contact form or by email. Also if you don’t want to miss any of the new blog posts, you can always subscribe to my newsletter or the RSS feed.


Follow me: