Camera Follow with Scriptable Objects (Unity Tutorial)

In this tutorial I'm going to give you an introduction to scriptable objects in Unity, using a simple example - a camera follow script.

robot-gif-1.gif

What are Scriptable Objects?

In its simplest form, think of a scriptable object as a script that doesn't need to be attached to a game object. For example: to create a game manager you don't need to create an empty game object to attach a script to. This means your object can live outside of the scene hierarchy and be available across multiple scenes.

Another advantage to scriptable objects is that they don't lose their values when you stop playing a scene. You can make adjustments in play mode and your changes won't be lost.

Separate your data

If you've done any serious Web development, you know the importance of separating your UI from your data. In a similar fashion, scriptable objects let you do the same. If you want to change the color of a button, you don't need to alter the button directly. In fact if you want to change the color of several buttons, you just need to create something like a skin / theme scriptable object and have all the buttons reference that.

Camera follow script design

For a real world example, let's create a camera follow script that uses scriptable objects. What would we need?

  • A player - for the camera to follow around
  • A character controller - so we can move the player with the keyboard
  • A script - to move the camera based on the players position

Think about the script part. Would the script go on the player? Would it need a game object reference to the camera? Would it need to access the camera, move it and access it's components?

What about the reverse? You can have the camera monitor the players position. But then you still need to get a reference to the player. What if you wanted to give the camera the ability to change views from one player to another?

What if you wanted to add some position debugging or create a mini radar map to show the players position on the screen? You'd need to add more game object references and figure out whose script is going to access whose data.

Or you can keep it simple. You can simply have the player update a scriptable object with its position. Then any other game object could access the scriptable object or choose to ignore it. The player doesn't need to access any other game objects and those game objects don't need to access the player. It's a nice clean separation.

Step 1: Create a project and scene

For this example, create a new Unity 3D project and scene. All work is going to be in the editor, so it doesn't matter what type of project you create. This should work on both Mac and Windows.

  • Set the Main Camera position to X: 0, Y: 1, Z: -10

Step 2: Create a floor

We are going to create a scene where a robot is using physics and gravity to move around. If we don't add a floor first, the robot will fall into infinity as soon as you hit play!

  • In the Hierarchy dropdown menu, select Create / 3D Object / Plane
  • In the Inspector change the plane name to Floor
  • Set the position to X: -2.5, Y: 0, Z: 0
  • Set the scale to X: 10, Y: 1, Z: 10
  • In the Mesh Collider component check Convex

Step 3: Import a player asset

For this example I'm using the free Tiny Robot Pack by Threebox which you can download and import from the Unity Asset Store:

https://assetstore.unity.com/packages/3d/characters/robots/tiny-robots-pack-98930

To follow along you can use that pack or even something simple like a cube or cylinder. It won't matter. I'll give the instructions assuming you went with the robot.

My one complaint about the asset (I'm using Version 1.1) is that when you import, it puts all it's folders under Assets. Let's fix that.

If by the time you read this the author of the asset already fixed that, you can skip this step.

  • Open the Project panel
  • Create a folder under Assets called Robot
  • Drag under the new folder the Plane, Prefabs, Robo controllers, Robo1, Robo2, Robo3, Stand and Standard Assets folders as well as the scene demo and the Readme file

Step 4: Dress up the scene

The all white floor will make it hard to see or notice movement. We can borrow a texture from the Robot package to fix that.

  • Select Floor in the Hierarchy
  • Drag Robot / Plane / Plane.mat to the Inspector

It will also make it easier to see movement if there is a stationary object in the scene. Let's add a cube to the scene.

  • In the Hierarchy, drop down the menu and select Create / 3D Object / Cube
  • Set it's position to X: 0, Y: 1, Z: 5

You are free to replace the texture of the floor and cube with anything you want. You can also add a few more obstacles, like cubes, cylinders or something from an asset pack.

Step 5: Create the player

This assumes that we had to create a folder called Robot for the robot asset. If by chance the asset author released an update and finally packaged everything under a folder, but with a different name, substitute "Robot" in the asset paths for that.

  • Go to the Project panel
  • Drag Robot / Prefabs / Robo1 into the Scene Hierarchy
  • In the Inspector, set the position to X: -2.5, Y: 0.8, Z: 0
  • Drag Robot / Robo controllers / Robo1 Ctrl to the Animator component Controller field (or just click the circle next to that field and select if from the list)
  • In the Inspector, click Add Component
  • Search for and select Character Controller and add it to the robot

Step 6: Create a player control script

  • Create a new folder under Assets called Scripts
  • Create a folder under Scripts called Mono
  • Right click in the Mono folder and select Create / C# Script
  • Rename the script PlayerController
  • Replace the code with the following and save it (include the commented out lines, I'll address them soon):
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(CharacterController))]
public class PlayerController : MonoBehaviour {

    private CharacterController _controller;

    // public PlayerPosition playerPosition;

    // Use this for initialization
    void Start () {
        _controller = GetComponent<CharacterController>();
    }

    // Update is called once per frame
    void Update () {

        Vector3 next = new Vector3(Input.GetAxisRaw("Horizontal"), 0, Input.GetAxisRaw("Vertical"));
         if (next != Vector3.zero)
             transform.rotation = Quaternion.LookRotation(next);
         _controller.Move(next / 8);

        // playerPosition.position = transform.position;
        // playerPosition.rotation = transform.rotation;
    }
}

Step 7: Attach the script

  • Save the script
  • Select Robo1 in the Hierarchy so that it's components show in the Inspector window
  • Drag the PlayerController script into the Inspector

Step 8: Test the controller

  • Save everything and Play the scene
  • You should be able to move the robot around the scene using the arrow keys or the WASD keys on your keyboard
  • Stop playing

Step 9: Create a scriptable object position base class

To track the player position you can create a scriptable object that contains a Vector3. You can also add a Quaternion to track rotation.
What happens if you want to set or track the position of another object? You could create a player position class and copy all the fields. Or you could create a base class and derive any future position trackers from that. Why don't we go with that and create a base class called "SoPosition" ('So' for ScriptableObject).

  • Create a folder under Scripts called My Objects
  • Right click in the My Object folder and select Create / C# Script
  • Rename the script SoPosition
  • Replace the code with the following and save it:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(menuName = "My Objects/So Position")]
public class SoPosition : ScriptableObject {

    public Vector3 position;
    public Quaternion rotation;

}

Notice that this is a little different from a GameObject script. For starters, instead of being derived from MonoBehaviour it's derived from ScriptableObject.

One important thing to note is that scriptable objects do not use any frame based methods like Start or Update. They are more like general purpose C# objects.

The CreateAssetMenu attribute is a way of adding a scriptable object to the Create menu so you can instantiate an instance. That will be covered later in this tutorial.

Step 10: Create the derived class

Now let's create a new scriptable object derived from the SoPosition base class.

  • Right click in the My Objects folder and select Create / C# Script
  • Rename the script PlayerPosition
  • Replace the code with the following and save it:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(fileName = "PlayerPosition", menuName = "My Objects/Player Position", order = 1)]
public class PlayerPosition : SoPosition {

}

Even though it doesn't have any additional methods or properties right now, you could add them later. This way you are free to add functionality that will only apply to the player position. It won't affect the base class that may be used elsewhere.

This creates a type of object (PlayerPosition). Unity's compiler will know that if you declare a PlayerPosition property that only PlayerPosition instances should be used. It won't let you accidentally use, say a CameraPosition object in it's place.

Step 11: Instantiate a player position object

  • In the Project panel Assets folder create a folder called ScriptableObjects
  • In that folder, right click and select Create / My Objects / Player Position
  • Click on the object instance and view it in the Inspector
  • You should see values for the position and rotation in the Inspector

Side note: Could you have just instantiated a SoPosition object and renamed it PlayerPosition? Yes. But then you lose the ability to restrict usage by type.

Step 12: Map the player position object to the player controller

  • Edit PlayerController.cs
  • Uncomment the three lines that reference PlayerPosition and save the file
  • Your code should now look like this:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(CharacterController))]
public class PlayerController : MonoBehaviour {

    private CharacterController _controller;

    public PlayerPosition playerPosition;

    // Use this for initialization
    void Start () {
        _controller = GetComponent<CharacterController>();
    }

    // Update is called once per frame
    void Update () {

        Vector3 next = new Vector3(Input.GetAxisRaw("Horizontal"), 0, Input.GetAxisRaw("Vertical"));
         if (next != Vector3.zero)
             transform.rotation = Quaternion.LookRotation(next);
         _controller.Move(next / 8);

        playerPosition.position = transform.position;
        playerPosition.rotation = transform.rotation;
    }
}
  • Click on Robo1 in the Hierarchy
  • Drag Assets / ScriptableObjects / PlayerPosition to the Inspector panel and onto the Player Controller / Player Position field

As an alternative, you could have clicked the circle next to the field and selected from the list. Unity would have shown you a list of instances that match the PlayerPosition type.

Step 13: Test the player position object

  • Make sure you deselect Maximize on Play for the Game scene panel
  • Click on ScriptableObjects / PlayerPosition so you can monitor the values in the Inspector
  • Play the scene and use the arrow keys to move around
  • Notice that as you move, the values for the PlayerPosition update
  • Changing the values will have no effect because they are being set by the script with every frame update
  • Stop playing

The good news is that you are done working on the Player game object (Robo1). You won't have to alter the Player game object again for this tutorial.

Step 14: Create a camera offset object

Now we need to create another object to hold the camera's position offset. We could instantiate an instance of SoPosition and rename it. But then we could accidentally overwrite a CameraOffset object with any other object derived from SoPosition. We don't want to allow ourselves or anyone else on the team to accidentally do that.

The steps are just like they were for creating a PlayerPosition.

  • Right click in the Assets / Scripts / My Objects folder and select Create / C# Script
  • Rename the script CameraOffset
  • Replace the code with the following and save it:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(fileName = "CameraOffset", menuName = "My Objects/Camera Offset", order = 1)]
public class CameraOffset : SoPosition {

}

Step 15: Instantiate a camera offset object

  • In the Assets / ScriptableObjects folder, right click and select Create / My Objects / Camera Offset
  • Click on the object instance and view it in the Inspector
  • You should see values for the position and rotation in the inspector

Unlike the PlayerPosition, this is something that can be set by you. Technically so can the PlayerPosition instance - but those values will be immediately written over by a script at runtime. The CameraOffset values can be set and remain constant unless altered in the Inspector or by a script.

  • Set the CameraOffset to X: 0, Y: 2, Z: -7
  • Rotation should be all 0's

That's done. We won't have to touch the CameraOffset object again unless you want to adjust it.

Step 16: Write a camera controller script

With all the other pieces in place there is only one thing left to do. Write a script to start the camera in a position relative to the robot based on the CameraOffset. Then with each frame move the camera relative to the current PlayerPosition values.

  • Right click in Assets / Scripts / Mono and create a new C# script
  • Rename the script CameraController
  • Replace the code with the following and save the file:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CameraController : MonoBehaviour {

    public PlayerPosition playerPosition;

    public CameraOffset cameraOffset;

    // LateUpdate is called after Update each frame
    void LateUpdate () 
    {
        transform.position = playerPosition.position + cameraOffset.position;
    }
}
  • In the Scene Hierarchy, click on Main Camera
  • Drag the CameraController script to the Inspector
  • In the Inspector under Camera Controller (Script) click the circle next to the Player Position field and select the PlayerPosition instance from the list
  • Repeat the last step for the Camera Offset field and select CameraOffset

Step 17: Test the camera controller script

  • Save everything and play the scene
  • When you move the player using the arrow or WASD keys the camera should now follow
  • Stop playing

Step 18: Adjust the offset at runtime

One of the great things about scriptable objects is that they let you make adjustments while in play mode and preserve them. In this step I will show you how to adjust the camera offset while in play mode and preserve your new settings.

  • In the Game panel deselect Maximize on Play
  • Click on Assets / ScriptableObjects / CameraOffset
  • Click Play
  • Make changes to the CameraOffset Position values in the Inspector
  • For example, try X: 3, Y: 3, Z: -7
  • Notice in the Game panel that the camera has a new offset position relative to the robot
  • Click in the Game panel so it has focus
  • Use the arrow keys to move around
  • Notice that the new offset is preserved
  • Stop playing
  • Press Play again
  • Notice that even after stopping and playing that the new offset is still preserved

How will an app remember the values?

When you create a new instance of a scriptable object, the file created under the Assets folder is a *.asset file.

After saving your scene and your project, open up a command line. Look at the contents of the Assets/ScriptableObjects folder. You should see *.asset files representing your objects. If you look at the contents of the files you should see the the values of the position and rotation properties when you saved the project.


Listing: Assets/ScriptableObjects/CameraOffset.asset

%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &114*****
MonoBehaviour:
  m_ObjectHideFlags: 0
  m_CorrespondingSourceObject: {fileID: 0}
  m_PrefabInternal: {fileID: 0}
  m_GameObject: {fileID: 0}
  m_Enabled: 1
  m_EditorHideFlags: 0
  m_Script: {fileID: 115*****, guid: 51******************************, type: 3}
  m_Name: CameraOffset
  m_EditorClassIdentifier: 
  position: {x: 0, y: 2, z: -7}
  rotation: {x: 0, y: 0, z: 0, w: 0}

When you build your app, the *.asset files are incorporated into the build. You can test this by building and running the app for your current operating system. Then change the camera offset, rebuild and note that the new offset is being preserved and used by the build.

Wrapping up

Wasn't that easier than the alternative? You didn't have to write any messy code in either the Player or the Main Camera to accommodate the other object. Each object just needed to read or write the player position object.

If you want to add yet another object to use or display the player position, you won't have to go back and hack either the Player or the Main Camera. Just write a new script to read the PlayerPosition instance and reference it in a new game object.

Exercises for the reader

You could make the tutorial a bit more interesting by adding 3D assets to the scene. You can either use generic shapes or get something like a pack of rocks from the asset store. As you move around the scene, the robot will either bump or roll over those objects.

You could also add walls or rails to keep the robot from falling off the edge into infinity. The choices are up to you.

GitHub Repo

The final version of the project in this tutorial can be found here:

Unite Austin 2017 - Game Architecture with Scriptable Objects


Unite 2016 - Overthrowing the MonoBehaviour Tyranny in a Glorious Scriptable Object Revolution

References