Referencing components in raw c# classes

We recently started using the ServiceLocator pattern to develop a new game we’re prototyping.

We’re putting lots of functionality away from MonoBehaviours and ScriptableObjects, and adding it into raw C# classes definied as Services accessible by the ServiceLocator.

But we’re having problems referencing stuff that is in the scene so we’re using Singletons, FindObjectByType, and GameObject. Find to reference stuff in the scene to those raw C# classes with functionality.

Is there any more clean and S.O.L.I.D way to reference stuff from the scene to these raw C# classes?

Service locator comes as a solution for referencing stuff, if you are having problems referencing things the chances are that you are not using SL correctly. I mean, are you setting the service into the locator at awake? if yes, the reference should be static accessible so I need more to understand what exactly is the problem

Hmmm I mostly end up using a MonoBehaviour as the ModelViewController of sorts, meaning it passes everything down to the C# classes that drive the behaviour, including references, event methods, and more.

I then try to make the C# classes independent from UnityEngine.Object references as much as possible. For example instead of working with GameObject or Component or Asset references, instead I use one or another form of indexing. For example rather than referencing prefab assets that I need to draw on a grid, my grid actually stores indexes into a lookup table (aka TileSet). This reduces runtime memory storage, enables Burst/Jobs optimizations. The lookup can be in the MonoBehaviour, or a ScriptableObject (referenced by the MonoBehaviour).

Downside is that you need to manage indexes that are “missing references” but that’s still pretty easy to do by providing a default replacement asset (the usual pink quad or perhaps something else that stands out, like a giant cow - a game I worked on had archers and mages shoot cows when you forgot a projectile assignment in the database :smile: ).

Would be nice to see a code example what you currently do and why that needs improvement.

3 Likes

A simple example in my current work

My service

public interface ISelectPiece
{
    public void Select() { }
    public void ClearSelection() { }
}

A NONE version of the service just to be sure that it will do nothing in a common scenario

public class SelectPieceNone : ISelectPiece { }

The locator side

public static partial class ServiceLocator
{
    private static ISelectPiece selectPiece = new SelectPieceNone();

    public static ISelectPiece SelectPiece
    {
        get => selectPiece ??= new SelectPieceNone();
        set => selectPiece = value;
    }
}

And one of my services at some scene

public class SelectPiece : MonoBehaviour, ISelectPiece
{
    [Title("References")]
    [Required]
    [SerializeField]
    private Selector selector;
   
    [Required]
    [SerializeField]
    private Highlighter highlighter;
  
   .
   .
   .
}
1 Like

A method I use, which I apologize may be completely off topic:

public class Master : MonoBehavior
{
    public static List<Items> allWeapons = new List<Items>();
    public static List<Items> allShields = new List<Items>();
}
public class Items : MonoBehavior
{
    public float cost;
    public int durability;
    public GameObject owner;
}
public class Weapons : Items
{
    void Awake()
    {
        Master.allWeapons.Add(this);
        cost = 9.5f;
        durability = 100;
    }
}
public class Player : MonoBehavior
{
    List<Items> inventory = new List<Items>();
  
    void CollectAllUnOwnedWeapons()
    {
        for (int i = 0; i < Master.allWeapons.Count; i++)
        {
            if (Master.allWeapons[i].gameObject.activeSelf
                && Master.allWeapons[i].owner == null)
            {
                inventory.Add(Master.allWeapons[i]);
                Master.allWeapons[i].owner = gameObject;
            }
        }
    }
}

Or something of the sort. I personally have all the classes inherited from “Master”, so no need to call it before any variable changes, or modifications.

However searching for classes, while iterating a list of classes, can be slower than just using the gameObject in the scene and using GetComponent() to get that objects class/script. Especially when using raycasts or collision checks to modify said objects variables.

And it can be argued just using tags or layers, for the same effect. But I’m sure this type of code structure has its pros and cons. But if you’re looking for out-of-the-box thinking, then my method is definitely it! :smile:

Making some of the services MonoBehaviour seems to be the answer.
Who takes care of assigning that MonoBehaviour service into the ServiceLocator?
Or does the MonoBehaviour itself manage its Subscribe and Dispose cycle ?

I was going to suggest something similar what wideeyenow suggested, except that I don’t see any need to make something a MonoBehaviour if you don’t explicitly need to. The service that maintains the collection of relevant MonoBehaviour references, Master in wideeyenow’s example, could also be a pure C# class if you prefer.

When the MonoBehaviour or other components need to be referenced, they can register and deregister with some relevant service, either in Awake and OnDestroy, or OnEnable and OnDisable, depending on the intended design. Communication between classes and components can take place through those services, and it can be done with strong type dependencies, or it can all be loosely coupled. Interfaces and events help with loose coupling.

For example, all player MonoBehaviours could register with some relevant service when they are activated. The same could be true for all enemy related MonoBehaviours, and relevant other components or objects they reference. This could be one service that managers the players and enemies, or separate services that focus on one concern or the other. In addition to registering themselves centrally with a service, they can subscribe to observe events on that service, such as EnemyCreated, Enemy Destroyed, or PlayerAdded, PlayerRemoved. They could receive those events with the references to the relevant objects as event data, or they could get references to, for example, an AllEnemies list from that service as needed.

Abstracting some of these ideas far enough can leave you with a totally general purpose messaging system or something like it.

I saw a former military veteran in another thread describe it in similar terms to having a central command on the battlefield. If one unit needs to make decisions they relay information to central command, who has access to all of the necessary data for making bigger picture connections, and central command relays the relevant information back to the units.

However, there are so many different ways to tackle these kinds of issues, and without having detailed knowledge of exactly how your particular project is intended to work I can only offer a general opinion. Hopefully it helps inspire you toward a solution you enjoy.

1 Like

I agree that just having the service locator expose methods that can be used to register (and probably deregister) services is the most flexible approach.

Example implementation:

public sealed class Service
{ 
    public static TService Get<TService>() => Shared<TService>.Instance ?? throw new NullReferenceException($"Service {typeof(T).Name} not found!");
    public static void Register<TService>(TService service) => Shared<TService>.Instance = service;
    public static void Deregister<TService>(TService service) => Shared<TService>.Instance = default;
 
    private static class Shared<TService>
    {
        public static TService Instance;
    }
}

Oh, also I should mention there are some parts of the Unity engine that effectively fulfill some of this inter-object communication. Don’t forget about the services that already exist, such as the physics system. When two objects collide the MonoBehaviours on them will receive collision events, and when you do raycasts, like from the cursor into the game world, you will get the references you need to the objects that are hit.

Yap, the service will register/remove itself through unity messages in a monobehavior scenario, and normally I have somekind of “scene setup manager” that register/remove plain class services from service locator at some specific load/unload steps

1 Like

Instead of referencing them with Singletons, FindObjectByType, and GameObject you can instead let them register themselves by creating a child class of Monobehaviour that will do this stuff for you.

1 Like