Unity GameLink Tutorial

This tutorial walks through building a small Unity scene that does GameLink authentication and reacts to a viewer poll.

You will learn how to install the GameLink library and collect user authentication, then initialize your extension by connecting to the GameLink server with the user's credential.

We then demonstrate the use of the MEDKit and GameLink polling functionality with a basic UI where users can "vote" on RGB values to create a color.

📘

Prerequisites

The tutorial assumes the following preparation:

  • You have obtained an Extension Client ID and have registered the secret on dev.muxy.io.
  • You know your twitch.tv channel ID.
  • You have git, node and npm installed on your system and accessible through the command line.

The sample code builds on the Unity 2D core project template, using editor version 2020.3.18f1 (LTS).

Install the GameLink Library

To set up the development environment:

  1. Create a new Unity project using the 2D core project template.
  2. In the Unity Package Manager, add a package to the project using Add package from git URL.
  3. Enter the following URL: https://github.com/muxy/gamelink-unity.git
Install library from gitInstall library from git

Install library from git

Create the Muxy GameLink singleton

For this project, the Muxy GameLink instance will live in a singleton instance in a script in a manager GameObject.

  1. Create an empty GameObject and name it MuxyManager.

  2. Add in a new script component with a script named MuxymanagerScript.

  3. Add the following initialization code to the script, to create an SDK instance and prepare for authentication.

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

// Needed to use the unity GameLink integration
using MuxyGameLink;

public class MuxyManagerScript : MonoBehaviour
{
    // Fill this in with the correct value.
    private static string ClientID = "my-client-id";

    // The manager object provides access to the MEDKit library.
    private static SDK medkit;
    private static WebsocketTransport transport;

    // Singleton instance property
    public static SDK Instance
    {
        get
        {
            create();
            return medkit;
        }
    }

    private static void create()
    {
        // Only create a new manager object if one doesn't exist
        if (medkit == null)
        {
            // Lock the CilentID to ensure that this creation process
            // only happens once.
            lock (ClientID)
            {
                // Create the MEDKit manager object.
                medkit = new SDK(ClientID);

                // Note that there is no transport created here.
                // You can't connect to the GameLink server until
                // the system can authenticate. This can be done
                // automatically when there is an existing refresh token,
                // but for this example, we show only PIN authentication.

                // Attach a debug handler for debugging purposes.
                medkit.OnDebugMessage((string msg) =>
                {
                    Debug.Log(msg);
                });
            }
        }
    }

    // Keeps user's the PIN code when entered.
    [SerializeField]
    private InputField pinInput = null;

    public void Authenticate()
    {
        lock(Instance)
        {
            if (transport == null)
            {
                // Create the transport here, and connect to the appropriate stage.
                transport = new WebsocketTransport();
                transport.OpenAndRunInStage(medkit, Stage.Sandbox);
            }

            Instance.AuthenticateWithPIN(pinInput.text, (AuthenticationResponse resp) => {
                Error err = resp.GetFirstError();
                if (err != null)
                {
                    Debug.Log(err);
                    return;
                }

                // You will normally save the token for future authentications,
                // but we skip that step for now.
                string tk = medkit.User.RefreshToken;
            });
        }
    }
}

Create the Extension.

We will start with a ready-made skeleton code for a Muxy-powered extension.

  1. For our example, download the MEDKit Extension Starter.

📘

Although this example uses the TypeScript skeleton and Vue3, neither TypeScript nor Vue are required to create a GameLink-enabled extension.

  1. Navigate to the Project folder, and create a new folder, named Extension~. The tilde in the filename marks the folder to be ignored in Unity's asset explorer.
  2. Copy the contents of the ts/ directory from the MEDKit Extension Starter into the new folder. The resulting directory structure should look like this:
Extension directory structureExtension directory structure

Extension directory structure

  1. Modify the .env file to contain your Extension Client ID and Twitch Channel ID:
VUE_APP_CLIENT_ID=*my-client-id*
VUE_APP_TESTING_CHANNEL_ID=*12345*
VUE_APP_TESTING_USER_ID=*12345*
  1. To install the dependencies for the extension, open a command shell, navigate to the folder, and run the command npm install.

  2. To run an automatically refreshing version of the extension, run the command npm run serve .

Adding an Authentication UI

To authenticate GameLink calls, the user must be able to log in with their Twitch Client ID and a PIN.
We will build a simple interface using Unity.UI, with an InputField, a Text and a Button.

  1. Right-click in the scene and choose UI > Canvas to create a new Unity UI canvas.
  2. Right-click on the new canvas and add the three components, from the UI menu:
    InputField, Text, and Button.
  3. Rename the new input field to Pin Input and the new button to Auth Button.
  4. Drag the new UI elements so they don't overlap. The resulting scene should look like this:
New unedited elementsNew unedited elements

New unedited elements

  1. Adding the Button and Input fields automatically creates child Text objects that contain the display text. Click the Text fields in the Hierarchy pane to edit the text.
Scene hierarchyScene hierarchy

Scene hierarchy

  • Set the Input/Text value to "PIN" (this is where the user will enter their PIN when the get it).
  • Set the Button/Text value to "Auth" (this button submits the PIN value).
  • Change the color of the top-level Text field so it shows up against the grey background, until it looks like the final image.
Simple authentication UISimple authentication UI

Simple authentication UI

  1. When the MuxymanagerScript script was compiled, it created a new input field named "Input" in the MuxyManager object that the script is attached to.

Find this field in the Inspector tab and point it at the new Pin Input field:

Attach PIN Input to MuxyManager objectAttach PIN Input to MuxyManager object

Attach PIN Input to MuxyManager object

  1. Hook up the Auth button's click operation to invoke the MuxyManagerScript.Authenticate method that we defined in the MuxyManager object:
Add on-click behavior to Auth buttonAdd on-click behavior to Auth button

Add on-click behavior to Auth button

Getting a GameLink PIN

Users with the broadcaster role can get a GameLink PIN. By default, the config page has broadcaster capabilities, so that is where we will implement the authentication functionality.

  1. In the Config App component (src/config/App.vue), modify the content in the <template> section to have a field to display the token:
<template>
  <div class="config">
    <h1>Broadcaster Configuration</h1>

    <h2>Token</h2>
    <h3>{{ token }}</h3>
  </div>
</template>
  1. Modify the content in the <script> to obtain the token and set it:
type TokenRequest = Record<string, never>;
type TokenResponse = { token: string };

  // ...
  setup() {
    const token = ref("");

    // MEDKit is initialized and provided to the Vue provide/inject system
    const medkit = provideMEDKit({
      channelId: globals.TESTING_CHANNEL_ID,
      clientId: globals.CLIENT_ID,
      role: "broadcaster",
      uaString: globals.UA_STRING,
      userId: globals.TESTING_USER_ID,
    });

    medkit
      // Wait for the loading process to finish
      .loaded()
      .then(() => {
        // Get a GameLink token
        return medkit.signedRequest<TokenRequest, TokenReponse>("POST", "gamelink/token", {});
      })
      .then((tk: TokenResponse) => {
        // Set the token value
        token.value = tk.token;
      });

    // Expose the token to the template.
    return {
      token,
    };
  },
  // ...
  1. To obtain a PIN, navigate to http://localhost:4000/config.html. Refreshing the page gives you a new PIN.

    You might have to modify the port number of the link to reflect where node is serving the page.

  2. Run the Unity game, enter the PIN, and click Auth.
    The debug console should show a successful authentication.

At this point, we have not gotten a refresh token, so you will have to enter a new PIN each time you restart the game.

Implementing a poll

The GameLink polling feature lets you create a set of choices and allow viewers to submit votes.
As an example, we will create a simple color selector and poll users for red, green, and blue values.

Creating the poll interface

The polling interface will live in the overlay, which is what viewers will see.

  1. In the Overlay App component (src/overlay/App.vue), modify the content in <template> to set up the color selector.
<div class="overlay">
    <h1>Overlay Extension</h1>
    <h2>Target Color</h2>
    <div class="square" :style="squareColor"></div>

    <h3>R ({{ red }})</h3>
    <input type="range" min="0" max="255" class="slider" v-model.number="red" />

    <h3>G ({{ green }})</h3>
    <input type="range" min="0" max="255" class="slider" v-model.number="green" />

    <h3>B ({{ blue }})</h3>
    <input type="range" min="0" max="255" class="slider" v-model.number="blue" />
  </div>
  1. Modify the <script> content to monitor the slider input and create a color.
import { defineComponent, ref, computed, watch } from "vue";
import globals from "@/shared/globals";

// This is used to debounce sending poll results to the server,
// to prevent a lot of unneeded requests.
import debounce from "lodash.debounce";

import { provideMEDKit } from "@/shared/hooks/use-medkit";
import { ChannelState } from "@/shared/types/channel-state";

export default defineComponent({
  setup() {
    // Create three values that will be modified by the sliders
    const red = ref(0);
    const green = ref(0);
    const blue = ref(0);

    // MEDKit is initialized and provided to the Vue provide/inject system
    const medkit = provideMEDKit({
      channelId: globals.TESTING_CHANNEL_ID,
      clientId: globals.CLIENT_ID,
      role: "viewer",
      uaString: globals.UA_STRING,
      userId: globals.TESTING_USER_ID,
    });

    // This is a computed style, used to show the preview square.
    const squareColor = computed(() => {
      return {
        backgroundColor: `rgb(${red.value}, ${green.value}, ${blue.value})`,
      };
    });

    // Watch for changes on the RGB components, and then send the
    // votes up after a 250 milliseconds of no changes.
    watch(
      [red, green, blue],
      debounce((next) => {
        const [r, g, b] = next;

        medkit.vote("square-r", r - 128));
        medkit.vote("square-g", g - 128));
        medkit.vote("square-b", b - 128));
      }, 250)
    );

    return {
      red,
      green,
      blue,

      squareColor,
    };
  },
});
</script>

Loading or refreshing http://localhost:4000/overlay.html now displays a page with access to the three polling sliders. Moving the sliders changes the preview square on the web page.

Once we set up a response object, it will also change the color of the square in-game.

Creating a response object

To receive user votes, you must subscribe to polling events. Your listener registers an event handler to process new votes.

For this example, we will create a simple 2D square sprite in the scene, and place it so that it can be seen on game start. A script will subscribe to OnPollUpdate events, and the event-handler callback will change the RGB values of the sprite renderer based on the results of the three polls.

  1. Attach a new script component with a new script, named SquareScript.cs.
  2. Add the following script to subscribe and register the event handler.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

using MuxyGameLink;

public class SquareScript : MonoBehaviour
{
    // These variables store the result of each poll as a color value.
    double r;
    double g;
    double b;

    private SpriteRenderer cachedSpriteRenderer;

    // This function subscribes to OnPollUpdate events,
    // and defines the event handler callback.

    void Start()
    {
        // Use the singleton SDK instance that we created in the manager script.
        MuxyManagerScript.Instance.OnPollUpdate((PollUpdateResponse resp) =>
        {
            switch (resp.PollId)
            {
                // These poll IDs are hardcoded, and are set in coordination
                // with the extension. Since poll values are limited to the
                // range [-128, 128], remap those to 0-255 and divide to get
                // the final color component value.
                //
                // This function is called from the websocket transport thread
                // which is not the Unity main thread, so it uses no Unity operations.
                case "square-r":
                    {
                        r = ((resp.Mean + 128) / 255.0);
                        Debug.Log("Set R: " + r);
                        break;
                    }
                case "square-g":
                    {
                        g = ((resp.Mean + 128) / 255.0);
                        Debug.Log("Set G: " + g);
                        break;
                    }
                case "square-b":
                    {
                        b = ((resp.Mean + 128) / 255.0);
                        Debug.Log("Set B: " + b);
                        break;
                    }
            }
        });

        // Get the sprite renderer and cache it, and also
        // store the default colors.
        cachedSpriteRenderer = GetComponent<SpriteRenderer>();
        r = renderer.color.r;
        g = renderer.color.g;
        b = renderer.color.b;
    }

    void Update()
    {
        // Set the sprite color every frame
        var c = new Color((float)r, (float)g, (float)b);
        cachedSpriteRenderer.color = c;
    }
}

Deploying the Extension

Now that we have a working extension, the next step is to deploy the extension to twitch.tv in a hosted test.

  1. In the command shell, stop the npm run serve command.
  2. Run npm run build to build a production version of the extension.
    This creates a dist/ directory, and a .zip file that contains all the files needed for the extension.
  3. In a browser, navigate to dev.twitch.tv, and to the Extension Management console.
Management UIManagement UI

Management UI

  1. In the console, create a new version (if one does not yet exist), then click Manage to enter the management UI for that version.
Twitch Extension Management ConsoleTwitch Extension Management Console

Twitch Extension Management Console

  1. In the Asset Hosting tab, fill out the form fields. Make sure that:
    • Type of Extension is Video - Fullscreen
    • Video - Fullscreen Viewer Path is set to overlay.html
    • Both Config Path and Live Config Path are set to config.html
Twitch Extension Management UITwitch Extension Management UI

Twitch Extension Management UI

  1. Save your changes.

  2. In the Access tab, add yourself to the Streamer Allowlist and the Testing Account Allowlist.

  3. In the Files tab, click Choose File and upload the ZIP file that you created in Step 2.

  4. In the Status tab, click Move to Hosted Test.

At this point, you can install the extension on your channel. It should be visible to all users, and you can interact with it.

Update the Environment in Setup Code

After moving to hosted test, the game will no longer respond to the polls. This is because extensions hosted on Twitch talk to the production API, rather than the sandbox API that you used in development.

  1. In MuxyManagerScript.cs, change the Stage value in the call to OpenAndRunInStage():
// ...
    transport = new WebsocketTransport();

    // Was transport.OpenAndRunInStage(medkit, Stage.Sandbox);
    transport.OpenAndRunInStage(medkit, Stage.Production);
    // ...