Skip to content

Implementation Specifics

Andreas Stange edited this page Jun 6, 2022 · 1 revision

The following names noteworthy implementation details of UltraStar Play.

CommonSceneObjects

There is a prefab called CommonSceneObjects, which should be placed in every scene.

It holds the single instances of manager classes (e.g. SettingsManager) and other GameObjects that are relevant for all scenes.

Singleton Pattern / Single Instance Classes

The singleton pattern has been implemented a little different in UltraStar Play than one might expect.

It has been implemented using tags that identify the instances in the object hierarchy. An instance of the GameObject with this tag is added to the CommonSceneObjects prefab to make it available in every scene. Afterwards, a static getter for this instance can be implemented as follows:

public class SceneNavigator
{
    public static SceneNavigator Instance
    {
        get
        {
            return GameObjectUtils.FindComponentWithTag<SceneNavigator>("SceneNavigator");
        }
    }
}

The CommonSceneObjectsBinder binds these instances such that they can be used via dependency injection (see wiki page on UniInject). Example:

...
using UniInject;

public class MyCoolScript : INeedInjection
{
    [Inject]
    private SceneNavigator sceneNavigator;
    
    void Start() {
        // Do something with the sceneNavigator instance.
    }
}

In UltraStar Play, most classes that implement the singleton pattern use the suffix Manager, for example SettingsManager.

Loading a Scene with Parameters

The SceneNavigator class holds a static collection of SceneData objects. This is used to temporarily transfer data from one scene to the next. The control class of the newly opened scene can query this data and store it in a non-static field.

Every scene should be playable without the need to navigate other scenes before. As a result, a Control class should have a sensible default for its SceneData.

Example:

SongSelectSceneControl.cs:

private void OpenSingSceneWithSelectedSong() {
    SingSceneData singSceneData = ...
    SceneNavigator.Instance.LoadScene(EScene.SingScene, singSceneData);
}
SingSceneControl.cs:

void Start() {
    // Load scene data from static reference, or use default if none
    SingSceneData defaultSingSceneData = ...
    singSceneData = SceneNavigator.Instance.GetSceneData(defaultSingSceneData);
}

Serializable

Model classes should have the [Serializable] annotation and use serializable fields if possible. The annotation will make these classes visible to Unity's serialization system. This means their instances will be visible in the inspector.

Properties with a backing field are serialized. Such a property requires both, a get and set method. Example:

[Serializable]
public class Bla
{
    public int MySerializableProperty { get; private set; }
    public int MyNonSerialzableProperty { get; }

    public Bla()
    {
        MySerializableProperty = 1;
        MyNonSerialzableProperty = 2;
    }
}

You can see all serializable fields in the Inspector in Debug mode. This includes non-public members of a class.

Unity Callback Function Guidelines

Some methods of a MonoBehaviour such as Awake and Start are called by the Unity platform. See the manual for the execution order of these methods.

A detail is the execution order when instantiating a MonoBehaviour at runtime:

  • Awake and OnEnable will be called before the call to Instantiate(...) returns.
  • Start will be called normally.

In addition to this, a MonoBehaviour can implement some interfaces to receive further callbacks. For example, ISerializationCallbackReceiver has methods that are called before and after serialization.

UltraStar Play uses a similar approach to notify scripts about certain events, in particular for depencendy injection:

  • ISceneInjectionFinishedListener will be notified when scene injection has been finished. This is done in the Awake method of the SceneInjectionManager and before any Start method is called.
  • IInjectionFinishedListener will be notified when injection of the object itself has been finished. This is done in the Inject method of the Injector.

As rule of thumb the methods should be used for the following:

  • Awake: Resolve references, e.g. call GetComponent or GetComponentInChildren. Do not assume that other references have been resolved yet.
  • OnInjectionFinished: Do setup using injected values. The method is called every time after injection of this particular object has finished.
  • OnSceneInjectionFinished: Do setup using injected values. The method is called once after scene injection has finished.
  • Start: Do setup (using injected values). You can assume that injected fields have been resolved. You can assume that OnInjectionFinished has been called before.
  • OnEnable: Perform setup steps that needs to be undone later in OnDisable. Note that OnEnable is called directly by Unity when a script is instantiated at runtime. Thus, you cannot assume that injection has finished yet in this method.
  • OnDisable: Undo stuff that has been set up in OnEnable

Start vs. OnInjectionFinished

Consider you have a ReceiverScript that needs to be notified by SenderScript about some event, which is issued in SenderScript's Start method.

In this case, subscribing to the event in ReceiverScript's Start method might be too late, because SenderScript's Start method could have been called already.

Instead, To handle this, you can inject SenderScript and subscribe to its event stream in OnInjectionFinished. This works because scene injection is done before any Start method is called, and thus before the event is fired.

public class ReceiverScript : INeedInjection, IInjectionFinishedListener
{
    
    [Inject]
    private SenderScript senderScript;

    public void OnInjectionFinished ()
    {
        senderScript.MyEventStream.Subscripte(newValue => Debug.Log(newValue));
    }
}
Clone this wiki locally