Table of Contents
Now that I’ve completed the Swingball prototype, I’m shifting my focus to how to handle inter-scene communication. Based on my google-fu, there are a few ways to do this:
- Limit the game to a single scene.
- Use static variables.
- Use singletons.
- Write to a file on disk.
Right now, I’m using option #2. I’m not a fan of any of these approaches though. Breaking the game up into scenes has performance and maintenance benefits. Static variables can be modified by any caller in the code – it can become a maintainability nightmare at scale. They also complicate unit testing.
Singletons are similar to static variables in how they complicate unit testing. Relying on I/O means that we need to serialize and deserialize data each time there’s a scene transition. This could become a performance and maintainability mess. Then I watched this GDC talk about ScriptableObjects in Unity.
ScriptableObjects
ScriptableObjects are similar to MonoBehaviours except that they aren’t attached to a GameObject. They can be used by the inspector and are typically created before runtime. They’re visible from any scene. Controlled use of ScriptableObjects might be the solution.
Inter-scene communication
PlayerScore class
Right now, I’m using this class to communicate the player score from the “MainScene” to the “ScoreScene”:
public static class PlayerScore
{
public static bool Win { get; private set; }
public static float TimeSpan { get; private set; }
public static void SetScore(bool win, float timeSpan)
{
Win = win;
TimeSpan = timeSpan;
}
}
I can change a few lines in this class to make it a ScriptableObject:
using UnityEngine;
[CreateAssetMenu(fileName = "PlayerScoreDTO", menuName = "ScriptableObjects/DTOs/Player Score", order = 1)]
public class PlayerScoreScriptableObject : ScriptableObject
{
public bool Win { get; private set; }
public float TimeSpan { get; private set; }
public void SetScore(bool win, float timeSpan)
{
Win = win;
TimeSpan = timeSpan;
}
}
First, I change the name of the class and inherit from ScriptableObject.
Second, I change the class (and its members) from static to instance members.
Third, I add the CreateAssetMenu attribute at the top. When I create a new instance of the class in the editor, it will be called “PlayerScoreDTO” by default. I can find it in the context menu by right-clicking and then selecting “Create -> ScriptableObjects -> DTOs -> Player Score”.
Classes that reference PlayerScore
That’s all well and good, but now my GoalDetectBehaviour class can’t find PlayerScore. The static class it was referencing no longer exists.
using UnityEngine;
using UnityEngine.SceneManagement;
public class GoalDetectBehaviour : MonoBehaviour
{
[SerializeField]
private CircleCollider2D goalCollider;
private void OnTriggerStay2D(Collider2D collision)
{
if (collision.gameObject.CompareTag("Player"))
{
// This line is causing the error - it can't find PlayerScore.
PlayerScore.SetScore(win: true, timeSpan: Time.timeSinceLevelLoad);
SceneManager.LoadScene("ScoreScene", new LoadSceneParameters(LoadSceneMode.Single));
}
}
}
The solution here is simple. We add a serializable “PlayerScoreScriptableObject” member and reference that instead.
using UnityEngine;
using UnityEngine.SceneManagement;
public class GoalDetectBehaviour : MonoBehaviour
{
[SerializeField]
private CircleCollider2D goalCollider;
[SerializeField]
private PlayerScoreScriptableObject playerScoreDTO;
private void OnTriggerStay2D(Collider2D collision)
{
if (collision.gameObject.CompareTag("Player"))
{
playerScoreDTO.SetScore(win: true, timeSpan: Time.timeSinceLevelLoad);
SceneManager.LoadScene("ScoreScene", new LoadSceneParameters(LoadSceneMode.Single));
}
}
}
So now it works, right?
Nope. We still need to link everything together in the inspector.
Link this file up with the scripts we modified, and we’re good to go! The game works as intended. Is there anywhere else we can use ScriptableObjects?
Modifying scripts without recompiling
When data is defined in a script, then to modify that data we need to recompile the script (and often the entire Unity project).
One way of dealing with this is to declare those attributes as public (or declare them as private and use the SerializeField attribute). This lets the developer set the fields directly in the Unity editor.
The other way is to use a ScriptableObject, like above. This has the advantage of encapsulating potentially complex behaviour, as well as giving us certain “presets” we can swap in and out as needed. For instance, maybe on larger levels we want the camera follow speed to be different.
I’ll be using that option. Take a look at the following class.
using UnityEngine;
public class FloatPlayerBehaviour : MonoBehaviour
{
private const float DisabledGravityScale = 0.1f;
private const float EnabledGravityScale = 1f;
private void OnTriggerStay2D(Collider2D collision)
{
if (collision.gameObject.CompareTag("Player"))
{
collision.gameObject.GetComponentInParent<Rigidbody2D>().gravityScale = DisabledGravityScale;
}
}
private void OnTriggerExit2D(Collider2D collision)
{
if (collision.gameObject.CompareTag("Player"))
{
collision.gameObject.GetComponentInParent<Rigidbody2D>().gravityScale = EnabledGravityScale;
}
}
}
There are two member values that can be moved to a SciptableObject: DisabledGravityScale and EnabledGravityScale. Let’s move them into their own ScriptableObject.
using UnityEngine;
[CreateAssetMenu(fileName = "FloatPlayerCoefficients", menuName = "ScriptableObjects/Prefabs/Wall/WaterWall/FloatPlayerCoefficients")]
public class FloatPlayerCoefficients : ScriptableObject
{
[SerializeField]
private float disabledGravityScale;
[SerializeField]
private float enabledGravityScale;
public float DisabledGravityScale => disabledGravityScale;
public float EnabledGravityScale => enabledGravityScale;
}
Then, modify the FloatPlayerBehaviour script to accept a ScriptableObject.
using UnityEngine;
public class FloatPlayerBehaviour : MonoBehaviour
{
[SerializeField]
private FloatPlayerCoefficients coefficients;
private void OnTriggerStay2D(Collider2D collision)
{
if (collision.gameObject.CompareTag("Player"))
{
collision.gameObject.GetComponentInParent<Rigidbody2D>().gravityScale = coefficients.DisabledGravityScale;
}
}
private void OnTriggerExit2D(Collider2D collision)
{
if (collision.gameObject.CompareTag("Player"))
{
collision.gameObject.GetComponentInParent<Rigidbody2D>().gravityScale = coefficients.EnabledGravityScale;
}
}
}
Once we set the “coefficients” field in the Unity inspector, the game works as intended!
I’ve skipped the other three examples for the sake of brevity, but I followed the same basic steps. Using ScriptableObjects, we can make the inter-scene communication and parameter tinkering in Unity easier to maintain.