Detecting The Player’s Controller Type With the Unity Input System

Hello all, this is a walkthrough of my solution for detecting the device that the player is using in a single player “gamepad” based game. In my specific case, I’m using this information to have my UI constantly represent the correct “controls” UI for the type of device my player is using. At the time of writing this, my system is specifically built to sort between PlayStation & Xbox controllers, with Xbox being the default. With that said, this system is easily extendable, and I hope that will be clear through my code & explanations. This guide is meant to help those who have routed all of their input beneath the “game pad” umbrella in the Unity Input System, but still want the specific type of controller used by the player to be accessible.

This post is using the “New” Unity Input System, NOT the Unity Legacy Input

I ran into a lot of very interesting road blocks in my quest to create an abstracted UI Image system which would live react to the type of controller being used by the player…. It sounds funny, because you would think it would be simple to detect the type of controller being used by the player, but the way that the Unity Input System deals with game pad style controllers seems to do a pretty good job of burying this data.

There were 2 ways which I thought I could approach this. The first would be to go into my actual Input Map and specify control schemes by controller type, and then have a manager which detects input from any/all control schemes. and switches state based upon the most recent input. I chose not to do this because, in my opinion, one of the biggest appeals of the Unity Input System is that it supports the most common controller types all beneath the gamepad umbrella.

It’s important to note that in my project, I have my PlayerInput Behavior set to Invoke C# Events.

My first step in this system was listening in to the InputSystem.onDeviceChange, which must be assigned to a function which takes an InputDevice and InputDeviceChange. This event will fire off every time a change to device is detected. These include a controller being: added, disconnected, removed, & reconnected. InputDeviceChange represents which one of these events was detected. Here’s what those look like:

Note: _currentController is just a basic enum meant to internally store the controller state

    private void GameManagerRegisterInput()
    {
        //Binds onDeviceChange event to InputDeviceChanged
        InputSystem.onDeviceChange += InputDeviceChanged;

    }

    //Method called  when a device change event is fired
    private void InputDeviceChanged(InputDevice device, InputDeviceChange change)
    {
        switch (change)
        {
            //New device added
            case InputDeviceChange.Added:
                Debug.Log("New device added");
                

                break;
               
            //Device disconnected
            case InputDeviceChange.Disconnected:
                controllerDisconnected.Invoke();
                Debug.Log("Device disconnected");
                break;
            
            //Familiar device connected
            case InputDeviceChange.Reconnected:
                controllerReconnected.Invoke();
                Debug.Log("Device reconnected");
                

                break;
                
            //Else
            default:
                break;
        }
    }//Method called  when a device change event is fired
    public void InputDeviceChanged(InputDevice device, InputDeviceChange change)
    {
        switch (change)
        {
            //New device added
            case InputDeviceChange.Added:
                Debug.Log("New device added");
                
                //Checks if is Playstation Controller
                if (device.description.manufacturer == "Sony Interactive Entertainment" && _currentController != CurrentControllerType.PlayStation)
                {
                    //Sets UI scheme
                    Debug.Log("Playstation Controller Detected");
                    currentImageScheme.SetImagesToPlaystation();
                    _currentController = CurrentControllerType.PlayStation;
                    controllerTypeChange.Invoke();
                }
                //Else, assumes Xbox controller
                //device.description.manufacturer for Xbox returns empty string
                else if(device.description.manufacturer != "Sony Interactive Entertainment" && _currentController != CurrentControllerType.Xbox)
                {
                    Debug.Log("Xbox Controller Detected");
                    currentImageScheme.SetImagesToXbox();
                    _currentController = CurrentControllerType.Xbox;
                    controllerTypeChange.Invoke();
                }
                break;
               
            //Device disconnected
            case InputDeviceChange.Disconnected:
                controllerDisconnected.Invoke();
                _currentController = CurrentControllerType.Other;
                Debug.Log("Device disconnected");
                break;
            
            //Familiar device connected
            case InputDeviceChange.Reconnected:
                controllerReconnected.Invoke();
                Debug.Log("Device reconnected");
                
                //Checks if is Playstation Controller
                if (device.description.manufacturer == "Sony Interactive Entertainment" && _currentController != CurrentControllerType.PlayStation)
                {
                    //Sets UI scheme
                    Debug.Log("Playstation Controller Detected");
                    currentImageScheme.SetImagesToPlaystation();
                    _currentController = CurrentControllerType.PlayStation;
                    controllerTypeChange.Invoke();
                }
                //Else, assumes Xbox controller
                //device.description.manufacturer for Xbox returns empty string
                else if(device.description.manufacturer != "Sony Interactive Entertainment" && _currentController != CurrentControllerType.Xbox)
                {
                    Debug.Log("Xbox Controller Detected");
                    currentImageScheme.SetImagesToXbox();
                    _currentController = CurrentControllerType.Xbox;
                    controllerTypeChange.Invoke();
                }
                break;
                
            //Else
            default:
                break;
        }
    }    private void GameManagerRegisterInput()
    {
        //Binds onDeviceChange event to InputDeviceChanged
        InputSystem.onDeviceChange += InputDeviceChanged;

    }

    //Method called  when a device change event is fired
    private void InputDeviceChanged(InputDevice device, InputDeviceChange change)
    {
        switch (change)
        {
            //New device added
            case InputDeviceChange.Added:
                Debug.Log("New device added");
                

                break;
               
            //Device disconnected
            case InputDeviceChange.Disconnected:
                controllerDisconnected.Invoke();
                Debug.Log("Device disconnected");
                break;
            
            //Familiar device connected
            case InputDeviceChange.Reconnected:
                controllerReconnected.Invoke();
                Debug.Log("Device reconnected");
                

                break;
                
            //Else
            default:
                break;
        }
    }

While this was the first step I took, I was working backwards a bit. With this event being listened to, we can react to new controllers being connected, to controllers losing connection, and a bunch of other events. These events usually represent a reason to re-evaluate the current images being displayed for controls.

I wrote a fairly simple Scriptable Object which holds all the textures we will be using in our “controls” UI. So this scriptable objects holds all the images used to represent xbox and playstation controls. Additionally, it has a set of private textures which hold the “current” image for that button. Through this abstraction, I can have the Scriptable Object flip it’s set of “current” images, and have all UI read from this ScriptableObject for the image it needs to display.

Now that I had the foundation, and a way to store/represent the “controller type” state, now I just needed to determine what type of controller was being connected/added, and pass that state on to my Scriptable Object. Using the API for the type Device, which is passed on this event as a direct reference to the Device that instigated the event, I was able to determine that the field description has sub fields which are used to define the specific type of controller, the manufacturer, and more. The field product is a string which would come out something like “Dual Shock 4” or “Xbox One Elite”. While this is certainly useful, I wanted things to remain as generic as they could be. The manufacturer proved to be the most abstracted I could get, while still distinguishing between the specific types of gamepads. However, herein lies the first issue I encountered. While the device.description.manufacturer field on a Playstation controller returns a nice & neat “Sony Interactive Entertainment”, that same field for the Xbox controller is entirely empty. Since I only have to support the 2 types, I wrote these conditions into an if/else, and called it a day. But as I extend the system to include more controller types, I would make use of the device.description.product and distinguish between the remaining types of controllers. Here’s what that same chunk from above looks like with these cases written in.

A note: currentImageScheme is my Scriptable Object described above. This scriptable object provides the texture to raw image prefabs in my UI. controllerTypeChange is an event which is listened to by the UI. The UI will react to this event by setting their texture to the “current” texture stored in the ScriptableObject

//Method called  when a device change event is fired
    public void InputDeviceChanged(InputDevice device, InputDeviceChange change)
    {
        switch (change)
        {
            //New device added
            case InputDeviceChange.Added:
                Debug.Log("New device added");
                
                //Checks if is Playstation Controller
                if (device.description.manufacturer == "Sony Interactive Entertainment")
                {
                    //Sets UI scheme
                    Debug.Log("Playstation Controller Detected");
                    currentImageScheme.SetImagesToPlaystation();
                    controllerTypeChange.Invoke();
                }
                //Else, assumes Xbox controller
                //device.description.manufacturer for Xbox returns empty string
                else
                {
                    Debug.Log("Xbox Controller Detected");
                    currentImageScheme.SetImagesToXbox();
                    controllerTypeChange.Invoke();
                }
                break;
               
            //Device disconnected
            case InputDeviceChange.Disconnected:
                controllerDisconnected.Invoke();
                Debug.Log("Device disconnected");
                break;
            
            //Familiar device connected
            case InputDeviceChange.Reconnected:
                controllerReconnected.Invoke();
                Debug.Log("Device reconnected");
                
                //Checks if is Playstation Controller
                if (device.description.manufacturer == "Sony Interactive Entertainment")
                {
                    //Sets UI scheme
                    Debug.Log("Playstation Controller Detected");
                    currentImageScheme.SetImagesToPlaystation();
                    controllerTypeChange.Invoke();
                }
                //Else, assumes Xbox controller
                //device.description.manufacturer for Xbox returns empty string
                else
                {
                    Debug.Log("Xbox Controller Detected");
                    currentImageScheme.SetImagesToXbox();
                    controllerTypeChange.Invoke();
                }
                break;
                
            //Else
            default:
                break;
        }
    }

So remember when I said I was working backwards? At this point, my system detects and reacts to big controller events, but at the start of the program has no idea what controller is being used. This is where I encountered the majority of my hardship, but I’m very proud of the end result!

Something I learned quickly is that the Unity Input System stores all known input devices, regardless of them being currently connected or not. So if my player is playing on their xbox controller, it’s batteries die, and they switch to their PS controller, InputSystem.devices still stores both devices. Additionally, while the type Device has a property enabled, this returns true for all devices registered to the Unity Input System. So all of this results in perhaps the most notable roadblock I encountered with this System: I could not find a way in which the Unity Input System distinguishes between the currently connected/in use controllers and those which are simply known by the Unity Input System, ie not in use at all. Devices can easily be individually assigned, and switched between, and detected, but there is no property of the Type Device (that I could find) which represents whether an individual device is actively connected. I pray I’m wrong about that, but the closest I could get was InputDevice.lastUpdateTime, which InputSystem inherits from. Even that field returns extremely unreliable values, as on PS it counts gyroscopic data as input, every frame, and in my tests, was returning the same value for all my devices, connected or not.

TlDr; I could not find a way to distinguish between connected and non connected input devices.

My response to this, however, is where I’m most proud of this code! I found that if I remove all the stored devices on game start, the controller being used by the player instantly re-constructs itself, and is quickly the only device stored in InputSystem.devices[]. From here, it’s easy to just check InputSystem.devices[0].manufacturer, and we have the same check on game start as we do when a controller event happens. Here’s that code!

Once again, currentImageScheme is my SO

//Called in GameManagerGameStart() to set the UI initially
    private void UIImageSchemeInitialSet()
    {
        //Disables all devices currently read by InputSystem
        for (int rep = 0; rep < InputSystem.devices.Count - 1; rep++)
        {
            InputSystem.RemoveDevice(InputSystem.devices[rep]);
        }

        if (InputSystem.devices[0] == null) return;
        
        //Checks the first slot of the InputSystem devices list for controller type
        if (InputSystem.devices[0].description.manufacturer == "Sony Interactive Entertainment")
        {
            //Sets UI scheme to PS
            Debug.Log("Playstation Controller Detected");
            currentImageScheme.SetImagesToPlaystation();
            _currentController = CurrentControllerType.PlayStation;
            controllerTypeChange.Invoke();
        }
        else
        {
            //Sets UI scheme to XB
            Debug.Log("Xbox Controller Detected");
            currentImageScheme.SetImagesToXbox();
            _currentController = CurrentControllerType.Xbox;
            controllerTypeChange.Invoke();
        }
    }

From there, to complete my system, all I had to write was a MonoBehaviour which stores a reference to my SO, and listens to my controllerTypeChange event, reacting by just setting its image to the corresponding image stored in the Scriptable Object. Here’s that code, for those who are curious!

public class ControllerImageScript : MonoBehaviour
{
    public UIImageSchemeSO uiImageScheme;
    
    public enum buttonImage
    {
        southButton,
        eastButton,
        northButton,
        westButton,
        rightTrigger,
        leftTrigger
    }

    public buttonImage myButtonImage;

    private RawImage myImage;
    
    // Start is called before the first frame update
    void Start()
    {
        myImage = gameObject.GetComponent<RawImage>();
        SetButtonImage();
    }

    public void SetButtonImage()
    {
        if (myButtonImage == buttonImage.southButton)
        {
            myImage.texture = uiImageScheme.GetSouthButton();
        }
        else if (myButtonImage == buttonImage.eastButton)
        {
            myImage.texture = uiImageScheme.GetEastButton();
        }
        else if (myButtonImage == buttonImage.northButton)
        {
            myImage.texture = uiImageScheme.GetNorthButton();
        }
        else if (myButtonImage == buttonImage.westButton)
        {
            myImage.texture = uiImageScheme.GetWestButton();
        }
        else if (myButtonImage == buttonImage.leftTrigger)
        {
            myImage.texture = uiImageScheme.GetLeftTrigger();
        }
        else if (myButtonImage == buttonImage.rightTrigger)
        {
            myImage.texture = uiImageScheme.GetRightTrigger();
        }
    }
}

I spent all day on this code, and I found it a surprisingly underdocumented topic. I hope that this proves helpful to those who are in my situation, just as all the wonderful discussions/threads online helped me!

SODA and Finite State Machines

Hello All, I hope everyone is at least as well as the last time I posted on here, hopefully even better. I wanted to write here today to update on my current projects. I am happy to announce that Liquidators, a yearlong project I have been lucky enough to be the lead producer of, has released a playable and open demo, for free. I would love to have support from my site and receive feedback on this game! It is a survival horror game based on the real life events which followed the reactor meltdown at Chernobyl. The demo can be downloaded HERE.

I will of course keep my site in the loop about this game, as we plan for a steam release this summer, hopefully. However, this is a site dedicated to my personal code and projects so I will carry on to stuff that you will definitely find less interesting than a nuclear reactor survival horror game which is literally free right up there ^. You could play it for FREE but you’re still reading this? Fine, if you’ve made it this far lets talk about scriptable objects and how I’ve been using them in my Finite State Machines in a separate, yet still cool, side project.

A little about this “side” project:

Been in production for almost 3 weeks

I’m working as the only engineer, Game being built in Unity, Being built for gamepad controllers (using primitive input system (I know))

it’s a 2 player endless runner

Player 1(Dog) can choose when to throw player 2(Boomerang)

Boomerang player aims themselves before throw

Dog player only has control of left and right movement, constantly moving forward

Boomerang player can control left, right, and forward back, still constantly moving forward, just at adjusted rate

Players try to survive as long as possible

Each have abilities they can use to help each other

“Tokens” used when using ability

Cylinder is enemy only boomeang can kill, kills dog. Wall blockades kill both

Here’s a clip:

So here, you could imagine there are a few player states. Specifically, a state for each player when the boomerang is with the dog, for when the dog and boomerang are separate, and for when the boomerang dies and the dog persists. A Finite State Machine helps us out here because none of these states will be coexisting. So, each player will have their own instance of a StateMachine, which takes an iState, and they must communicate with one another, to ensure they are in proper states at all times, since one of their states being out of sync would inherently break the other’s state (since their controls are dependent upon one another).

If you have read my other posts here , or here, or here, or here or- okay you get it.. If you’ve seen those posts or the title of this one, you know that I love SODA. So given our state machine above, SODA fits into this very well. The first issue presented above is that our state machines must know the state of one another. SODA Events really help us with this here. When the dog throws the boomerang, it’s an event. The dog script doesn’t even communicate with the boomerang script directly. It simply invokes that event, and the GameEventListener on the Boomerang is quick to respond, switching to its “free” state, after a throwing thread, of course. This exists for all my state transitions here, except for the boomerang being caught by the Dog for that requires a synchronous tap of a button by both players. But you can see how that would ensure our State machine stay in our intended state.

The next BIG help from SODA in these FSMs is that my IntVariable type, the Scriptable Object int I’ve made, can be passed into my states, where they have free access to the value they need, and receive all live updates to that number via that reference. Confused? Imagine this: My dog player location is stored in my Vector3Variable Scriptable Object. In my state constructor, I take a type Vector3Variable _playerLocation. Now _playerLocation.value will be a reference to the exact spot in memory where my Player Location value is stored. Whether I just want to access it, or even adjust it, that value is live, and feeding into any other script that may need that live number. This is huge because normally I would have to make a reference to my player in every script that needs that number, and store it again in that script. Furthermore, by taking it in the state constructor, I don’t have to pester my player script to retrieve it every frame, because in the case of a value that is changed elsewhere, my state script will receive that update instantly through the magic of SODA!

I hope this makes sense, but in case it doesn’t, here’s some code:


//This is a state for player 1's movement, while the boomerang is in its backpack
//Here you have my declaration of the Variables I will need in this State
//Protecting these variables calmed down an empty value warning I was gettig
    protected Vector3Variable _dogLocation;
    
//Player speed can be changed at any time
    protected FloatVariable _playerSpeed;

//Player can move, or not
    protected BoolVariable _playerCanMove;

//Reference to Dog script
    protected DogPlayerMovement _player;

//Constructor
public ISDogRunning (Vector3Variable dogLocation, FloatVariable playerSpeed, BoolVariable playerCanMove, DogPlayerMovement player)
    {
//Simply taking the references I have, and setting equal to the Variable I need
        _dogLocation = dogLocation;
        _playerSpeed = playerSpeed;
        _playerCanMove = playerCanMove;
        _player = player;
    }

//.............................

//This is my state Tick, run every frame
public void OnStateTick ()
    {
        if (_playerCanMove.value)
        {
            //Moves player forwards
            _player.transform.Translate (Vector3.forward * Time.deltaTime * _playerSpeed);

            //Allows player to move LR
            _player.transform.Translate (Vector3.right * Time.deltaTime * Input.GetAxis ("P1Left Stick Horizontal") * horizontalMovementMod);

            //Throw boomerang when A pressed
            //Keyboard controls for debug
            if (Input.GetButtonDown ("P1A Button") || Input.GetKeyDown (KeyCode.E))
            {
                //Tells player to throw
                _player.BoomerangThrown (aimLocation);
            }

            //Reads for player using ability
            if (Input.GetButtonDown ("P1B Button") || Input.GetKeyDown (KeyCode.R))
            {
                _player.UseSelectedDogAbility ();
            }

            //Allows player to switch through abilities
            if (Input.GetButtonDown ("P1X Button") || Input.GetKeyDown (KeyCode.F))
            {
                _player.SwitchDogAbility ();
            }
        }

        //Gets player's aim
        //This state actually reads input from player 2 as well, as aiming component was initially part of dog
        //Axis is between -1 and 1, adding 1 and then dividing by 2 to get complete input
        aimLocation = Vector3.Lerp (_player.GetLeftAimLimit (), _player.GetRightAimLimit (), ((Input.GetAxis ("P2Left Stick Horizontal")) + 1) / 2);

        //Will rotate arrow assigned to plauer
        arrowGO.transform.LookAt (aimLocation);

        //Puts aim point at aim locations
        aimPointGO.transform.position = aimLocation;

        //Updates player's location
        _dogLocation.value = _player.transform.position;
    }

Frogger Remake & SODA

Hello all, I hope that this blog post finds you all well. Given the COVID-19 circumstances, I (and everyone I know) have been pushed into online classrooms. It’s been fine, but luckily it has also given me some personal time to try and post on this site, and work on more personal projects… That being said this post is a class project by Nicole Havin, and myself. Nicole worked on the mostly on the aesthetic side of things, implementing sounds, Timer UI and Main menu. I’m writing this post to talk more about Scriptable Object Dependent Architecture. I’m posting this game here to provide my readers with an opportunity to experience the architecture of which I will be writing about.

Disclaimers: I do not own anything in this game

This is a remake, for a class project, of Konami’s 1981 classic “Frogger”

https://simmer.io/@aidantakami/~bcd44750-53b0-3b08-07fd-bcb7b3778673

This game is buggy and was made under a time crunch for class. I am only posting this game here to give examples of the architecture I am speaking of, and showing how it is used in this specific build. I am also experimenting with UnityWebGL and trying to get those games hosted here on my site. For those who don’t know, exporting to UnityWebGL carries with it a plethora of issues in how the game in converted to be playable via a browser. There are certainly lots of bugs which I have not run into on the WebGL version. So please consider this as you play this game! Please feel free to contact me if you find any bugs! If this goes well I will continue to try and import playable demos of the architecture of which I’m writing about. Give it a few plays through and I’ll catch you on the other side.

This short experience is build to be modular, easily debuggable, and dependency free through the use of SODA Game Events & Listeners, and SODA Variables.

I’d like to first draw your attention to how when the player is in the first half of the map they must avoid collisions. Collisions here are from incoming cars, which are deadly to frogger. However, the second half of the map, this is flipped. The second half of the map the player must seek collisions. Collisions in the latter half of the map are from the stable log platforms or the finish line colliders.

So how might we “normally” do this? I would say it would involve an edge trigger collider that would tell the player script that it needs to collide or else die. But alas, the eternal game dev issue of detecting if something isn’t there, in this case that something would be the log. I don’t want to get into how we might complete “this” way of solving the issue, but it’s just messy and involves heavy dependencies.

What is the SODA way of doing this? I appreciate you asking. Imagine if we could just have a Scriptable Object which holds a bool value, and we call it BoolVariable “isOverWater”. We set up a collider at the edge of land and water and have that trigger toggle between the 2 states of isOverWater, true and false. Now how do we get logs in there? Simple, the same thing, just have a single BoolVariable, playerIsOnMe, which all log prefabs reference. Does that make sense? This single variable is being operated on by all instances of the log prefab. These logs simply set the variable to true OnTriggerEnter() and to false OnTriggerExit(). Here’s that code:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class LogScript : MonoBehaviour
{

    //BoolVariable to represent if player is on this log
    public BoolVariable playerIsOnMe;

    //Used for spawning purposes
    private bool startsOnRight;
    public SpriteRenderer sr;

    //Used to move the player when on log
    [SerializeField]public IntVariable logSpeedToPlayer;
    [SerializeField] public int logSpeed;


    //Flips sprite depending on start side
    private void Start()
    {
        if(transform.position.x > 0)
        {
            startsOnRight = true;
            sr = gameObject.GetComponent<SpriteRenderer>();
            sr.flipX = true;
        }
    }

    // Update is called once per frame
    void Update()
    {
        //Moves the log in accordance to where it was spawned
        if (!startsOnRight)
        {
            transform.position += transform.right * logSpeed * Time.deltaTime;
        }
        else
        {
            transform.position -= transform.right * logSpeed * Time.deltaTime;
        }
    }


    public void OnTriggerEnter2D(Collider2D col)
    {
        //Player enters log
        if (col.gameObject.CompareTag("Player"))
        {
            //Set BoolVariable value
            Debug.Log("Player is on");
            playerIsOnMe.SetValue(true);

            //Gives this logs speed to the IntVariable which gives it to the Player
            if (!startsOnRight)
            {
                logSpeedToPlayer.SetValue(logSpeed);
            }
            else
            {
                logSpeedToPlayer.SetValue(-logSpeed);
            }
        }
        
    }

    public void OnTriggerExit2D(Collider2D col)
    {
        //Player Leaves Log
        if (col.gameObject.CompareTag("Player"))
        {
            Debug.Log("Player is off");
            playerIsOnMe.SetValue(false);
            logSpeedToPlayer.SetValue(0);
        }
        
    }
}

If this makes any sense to you, then odds are you can understand what is going on in the rest of this script (beyond the OnTriggers), but more on that later. The main note here is that when the player enters the log, beneath the Debug statement, the BoolVariable playerIsOnMe is set to true. This lets the player movement script know to continue displaying the idle sprite, this bool lets the GameManager know the player is still alive. These BoolVariables (playerIsOnMe and isOverWater) are both accessible by all other scripts, and never providing conflicting values because all scripts are refrencing the same ScriptableObject.

How are these two BoolVariables being compared? Once again, well thought out question. So since both these Bools are being determined elsewhere in the script, our PlayerMovement script, which also serves as the animator controller, simply has to reference these 2 variables values to know the state of the player. I did this in Frogger by having an event at the end of every “Jump” animation which would check the state of the 2 BoolVariables, as at the end of the Jump animation the player would either be securely on a log or dead.

About the IntVariables: If you understand the above architecture then you’ll enjoy knowing what’s going on with the IntVariable logSpeedToPlayer in the above code. Basically, that IntVariable, when set to 0, will not affect the player’s movement at all. When set to x it will apply a constant drag to the player to the right to simulate the effect of the player being on the log. So… Do you see why this is so rad? I can just SET THIS INT in my LOG SCRIPT and then my player will INSTANTLY BE EFFECTED by the change to this variable! COME ON!!! How Cool?! So notice that logspeed is just a regular int, but it is used to set the value of logSpeedToPlayer when needed. For now, that’s it! I will try to be more present on here, but I’m busy! But I hope you enjoy! and stay safe!

Scriptable Object Events in Unity and My Cutscene Manager

Hello all. Finals season is over, and I have the most persistent cold I’ve ever experienced… which means I have a lot of time. Luckily, I’ve been working on this project with my friends and had the opportunity to write some really cool code that I’d like to share here. The other engineer on the project, and myself have been trying to build the architecture of this game utilizing a very powerful tool: Scriptable Objects. If you don’t know what these are, I highly recommend watching this talk by Ryan Hipple. TL;DR You can utilize Scriptable Objects to store data, as opposed to individual method calls and dependencies within code, to keep the architecture modular, easily debuggable, and less prone to collapse on you. A Scriptable Object will store a variable and allow any function to access it, or modify it, so that all methods can just respond to this one value. Think “global variable” that exists in the editor.

Another very cool principle in the talk by Ryan is using Scriptable Object Game Events (which take the same above principal, but utilize UnityEngine.Events;) to interface between everything in the scene that would otherwise be dependent through code. So here’s what I wrote: A cutscene manager for my game using Unity Timeline and Scriptable Object Game Events.

How it works: I have a UnityEvent StartScene that is Invoked OnTriggerEnter().

public class StartCutscene : MonoBehaviour
{

    //Event to start scene
    public  UnityEvent startScene;


    //on trigger enter
    private void OnTriggerEnter(Collider other)
    {
        //if player
        if(other.tag == "Player")
        {
            //raise
            startScene.Invoke();
            Debug.Log("Invoked");

        }
    }

}

Then this is what it looks like in editor. As a matter of fact, take my whole “CutsceneTrigger” prefab while you’re at it:

Note the UnityEvent is calling on the Game Event Scriptable Object “PlayCutscene” and Raising it. This signals to any Game Event Listeners (Once again, a Scriptable Object implementing the functionality of a Unity Game Event Listener) elsewhere in the scene that were set to listen to our Game Event Scriptable Object. In this case, I have 2 prefabs listening in on “PlayCutscene”. The first one is my CutsceneManager.

So take a nice long look at that beauty, and I think you might fully see what’s going on here. First off, I have the Game Event Listener which is making a call within my CutsceneManager Script to startNextCutscene(). startNextCutscene utilizes a Dictionary, which I have serialized at the top of the prefab. This dictionary takes the string name and the associated PlayableDirector, which is what controls Unity Timeline Playables. If you’re familiar with Dictionaries then you know they’re not serializable in Editor, but we can fix that with a nifty little work around in code.

//Serialized Dictionary Class
[System.Serializable]
public class Cutscenes
{
    public string cutsceneName;
    public PlayableDirector cutscene;

}


//Cutscene Manager will play the next applicable cutscene, storing all in a dictionary
public class CutsceneManager : MonoBehaviour
{

    //Note to design
    [TextArea]
    public string Notes = "Names should have no spaces or numbers and triggers should be place in order of encounter";

    //Array of Cutscenes, which contain our Dictionary compontents
    public Cutscenes[] toDictionary;

    //Dictionary that takes string name and PlayableDirector
    public Dictionary<string, PlayableDirector> listOfScenes;

    //Int to monitor which have played
    private int selection;

    //Unity Event to on trigger end of scene
    public UnityEvent endCutsceneEvent;


    //establishes Dictionary from serialized "Cutscenes"
    public void Awake()
    {

        //Instantiates Dictionary
        listOfScenes = new Dictionary<string, PlayableDirector>();

        //Fills that shit up
        for(int rep = 0; rep < (toDictionary.Length); rep++)
        {
            listOfScenes.Add(toDictionary[rep].cutsceneName, toDictionary[rep].cutscene);
        }


    }



    //Starts next cutscene
    public void startNextCutscene()
    {

        //Sets temp Playable Director
        Debug.Log("Signal Recieved");
        PlayableDirector temp = listOfScenes[toDictionary[selection].cutsceneName];


        //Starts cutscene
        Debug.Log("Starting...");
        temp.Play();

        //Event "stopped" is assigned to endCutscene, will call this function on raise
        temp.stopped += endCutscene; 
 
        //Increments cutscenes
        selection++;



    }

    //Invokes UnityEvent to tell rest of scene
    private void endCutscene(PlayableDirector aDirector)
    {
        //Ends the cutscene
        endCutsceneEvent.Invoke();

        Debug.Log("Cutscene Ended");
    }

}

So I’ll try to break this down for those who can’t understand my comments, but basically at the very top we have the serialized class Cutscenes, which I’m taking all the components necesary for my dictionary. Then storing them in an array toDictionary. Then, on Awake() I’m taking those individual values from the array and storing them together in my Dictionary ListOfScenes. Also note that I have a UnityEvent here at the top.

So this is where startNextCutscene() comes into play. I take the next PlayableDirector in order and store it in my temp, but note how in the search through my Dictionary ListOfScenes I’m using the corresponding string stored in the Array toDictionary to index the needed PlayableDirector. Just thought that was sick. A little wonky… but cool.

Then temp is played, which cues the animation sequence of the PlayableDirector, otherwise known as the Playable. The PlayableDirector then sets off an event upon finishing which I’m listening to and invoking my own UnityEvent endCutsceneEvent.

This is where my CameraManager comes in. Note that in the above picture of my CutsceneManager Prefab, my Unity Event endCutsceneEvent is attached to another Scriptable Object Game Event “EndCutscene”

From the bottom up, the CameraManager is listening to 2 events, EndCutscene and PlayCutscene. Remember, these are our scriptable events, any listener in the scene has access to them! So these are both invoking responses within my CameraManager script. At the top you can see the manager takes 2 Cameras, as this script is responsible for switching between the cutscene camera, used in our PlayableDirector Playables, and the main camera, used for gameplay. Don’t worry, this is not the only script working on the camera right now, this is just helping us manage between the 2 specifically with Playables. Here’s that code:

[System.Serializable]

public class CameraManager : MonoBehaviour
{

    //Declares the cameras needed
    public Camera playerCam;
    public Camera cutsceneCam;

    public void Awake()
    {
        switchAfterCutscene();
    }


    //Starts the cutscene, swapping cameras
    public void cutsceneStart()
    {

        //Cameras swapped
        switchBeforeCutscene();
        
        //Debug
        Debug.Log("play cutscene");


    }

    //Ends the cutscene, swapping cameras
    public void cutsceneEnd()
    {
        //Cameras swapped back
        switchAfterCutscene();

        Debug.Log("Cutscene end");
    }


    //Swaps active cameras
    private void switchBeforeCutscene()
    {
        playerCam.enabled = false;
        cutsceneCam.enabled = true;

    }

    //Swaps active cameras
    private void switchAfterCutscene()
    {
        cutsceneCam.enabled = false;
        playerCam.enabled = true;
    }
}

Pretty self explanatory, but just note that the methods switchBeforeCutscene() and switchAfterCutscene() are both triggered by the corresponding Game Events in scene, in addition to the startNextCutscene() in the CutsceneManager. I wanted to show you to give an example of how using GameEvent Scriptable Objects allows multiple scripts to interface with one another without ever knowing the other exists!

You have to see how this is such a god send for devs who want to keep dependencies low and the engine easy to use for designers. Here, a designer who has been briefed can easily establish a whole system of cutscenes in their level without ever having to see a single class.

Happy holidays to everyone! I’ll be back soon with more.