iOS Test-Driven Development by Tutorials!

Make your iOS apps more testable, maintainable and
scalable—with the power of test-driven development.

Home Game Technologies Tutorials

How to Create a Twitch Chat Game with Unity

Learn to integrate the Twitch Chat API into your Unity game, so viewers can interact with the game during streaming.

Version

  • C# 7.3, Unity 2020.1, VS Code

Twitch is a streaming platform where content creators broadcast themselves live. While broadcasting, viewers interact with each other and the streamer through simple text chat.

Users chatting with a streamer

Some content creators realized they could use chat messages to control a game. The most famous example of this is Twitch Plays Pokemon, where viewers control a modified version of Pokémon Red through simple commands.

A clip of popular stream game, Twitch Plays Pokemon

Stream games are a great way to build and maintain a dedicated audience. They create a sense of community, friendly competition and engage viewers in new and exciting ways. Viewers love to feel like they can directly interact with their favorite content creators, and stream games are a great way to provide that experience.

In this tutorial, you’ll learn:

  • How to read messages from Twitch chat into Unity.
  • Validate commands that can interact with your game.
  • All about the different types of audience interaction you could implement.
  • Create your very own Pokémon-like Battle game can be played by the audience.
Note: This tutorial uses Unity version 2020.3. If you don’t have this version of Unity installed, you can use Unity Hub to install and manage multiple Unity versions across your projects. You’ll also need a Twitch account.

Getting Started

Click Download Materials at the top or bottom of this tutorial to download the starter and final projects. Extract them. Then open Twitch Chat Game Starter using the Unity Editor.

Take a look at Assets/RW. Here you’ll see your project folders, including:

  • Prefabs
  • Scenes
  • Scripts
  • Sprites

RW/Scenes contains two scene files: Battle and Golf.

For this tutorial, you’ll work in Battle. You can use Golf for a bonus game at the end of the article.

The first step in creating a Twitch Chat game is connecting your app to your Twitch account and parsing incoming messages.

The Twitch Chat Script

If you haven’t already, open the Battle scene. In Assets/RW/Scripts/Common create a script called TwitchChat.

Import the System.IO and System.Net.Sockets namespaces:

using System.IO;
using System.Net.Sockets;

In TwitchChat‘s body, create the fields shown below:

public string username; // 1
public string password; 
public string channelName; 

private TcpClient twitchClient; // 2
private StreamReader reader; // 3
private StreamWriter writer; 
private float reconnectTimer; // 4 
private float reconnectAfter; 

Here’s a code breakdown:

  1. These strings hold your twitch account credentials, letting your app connect to your account.
  2. You declare a private TcpClient, which establishes a connection with Twitch using your credentials.
  3. Then you create a private StreamReader and private StreamWriter. The StreamWriter sends your credentials to Twitch and the StreamReader pulls in the messages from chat.
  4. reconnectTimer tracks the amount of time passed since connecting while reconnectAfter sets how long to wait before attempting to reconnect. Sometimes the connection to Twitch closes automatically, so the app should attempt to reconnect after a short time.

Create a private method called Connect. Implement it as below:

private void Connect()
{
    twitchClient = new TcpClient("irc.chat.twitch.tv", 6667); // 1
    reader = new StreamReader(twitchClient.GetStream()); // 2
    writer = new StreamWriter(twitchClient.GetStream()); 
    writer.WriteLine("PASS " + password); // 3
    writer.WriteLine("NICK " + username); 
    writer.WriteLine("USER " + username + " 8 *:" + username); 
    writer.WriteLine("JOIN #" + channelName); 
    writer.Flush(); 
}

This method:

  1. Instantiates twitchClient with the URL for the Twitch chat API and the port.
  2. Then instantiates both reader and writer with twitchClient.GetStream().
  3. Uses writer to pass your credentials to Twitch.

Add the following to Start:

reconnectAfter = 60.0f;
Connect();   

This code sets reconnectAfter to 60 seconds and calls Connect.

Now TwitchChat is ready to start reading in chat messages from Twitch.

Reading in Chat Messsages From Twitch

Add the following ReadChat method:

public void ReadChat()
{
    if (twitchClient.Available > 0) // 1
    {
        string message = reader.ReadLine();

        if (message.Contains("PRIVMSG")) // 2
        {
            print(message); 
        }
    }
}

Here’s a code breakdown:

  1. In ReadChat, you check if twitchClient.Available is greater than 0. If so, you save reader.ReadLine() to message.
  2. If message contains “PRIVMSG”, it’s a user created message. Print the message to the Unity console.

Then inside Update, add:

if (twitchClient.Available == 0) // 1
{
    reconnectTimer += Time.deltaTime; 
}

if (twitchClient.Available == 0 && reconnectTimer >= reconnectAfter) // 2
{
    Connect(); 
    reconnectTimer = 0.0f; 
}

ReadChat(); // 3

Here, you:

  1. Check if twitchClient.Available is 0. If it is 0, you add Time.deltaTime to reconnectTimer.
  2. If reconnectTimer is greater than or equal to reconnectAfter, the app disconnected for a minute. In that case, reconnect with Connect, and reset reconnectTimer to 0.
  3. Then, call ReadChat() to read in the messages.

Later, TwitchChat will filter commands from regular messages. For now, you’ll test to make sure it works.

In the Hierarchy, add TwitchChat to GameManager, found in Managers. Fill in the TwitchChat fields with your Twitch credentials.

The username and channel name fields should both be your Twitch username. Head over to twitchapps.com/tmi to generate a API token. Copy this into the password field.

Shows how the Twitch Chat script should look in the Unity Inspector.

With your credentials filled out, you’re ready to test!

Press Play and open your Twitch channel in a web browser. Try typing some messages into the chat. Even when not broadcasting, you’ll see them appear in Unity’s console.

Stream chat appearing in the unity console

Parsing Chat Commands

You want to be able to differentiate between then messages sent to your app and the user that sent them.

In RW/Scripts/Common, create a script called ChatMessage. Note it is not a Monobehaviour and simply holds two variables:

[System.Serializable]
public class ChatMessage
{
    public string user;
    public string message;
}

ChatMessage holds a message and the username of the user that sent it. It also uses System.Serializable to store it in a List.

In TwitchChat, update ReadChat to:

public ChatMessage ReadChat() // 1
{
    if (twitchClient.Available > 0)
    {
        string message = reader.ReadLine();

        if (message.Contains("PRIVMSG"))
        {
            // Get the username
            int splitPoint = message.IndexOf("!", 1); // 2
            string chatName = message.Substring(0, splitPoint); 
            chatName = chatName.Substring(1);

            //Get the message
            splitPoint = message.IndexOf(":", 1); 
            message = message.Substring(splitPoint + 1);
            ChatMessage chatMessage = new ChatMessage(); // 3
            chatMessage.user = chatName;
            chatMessage.message = message.ToLower();
            return chatMessage;
        }

    }
    return null; // 4
}

This updated code:

  1. Returns a ChatMessage and removes the print call.
  2. Splits the message to get the body of the message and the username of the user who sent it.
  3. Stores these values in a new ChatMessage and returns it.
  4. Since ReadChat now needs to return something, it returns null at the bottom of the method, when no connection was available.

Now you need to process messages to find game commands, validate them, then execute them.

Parsing and Storing Commands

Create a script in RW/Scripts/Common called IGameCommand. It’s short and sweet:

using System.Collections.Generic;
public interface IGameCommand
{
    string CommandString { get; } 
    string ShortString { get; }  
}

IGameCommand has a CommandString field, which stores the string required to execute it. For example, Battle has users vote for in-game options, so a CommandString for a vote command would be !vote.

Additionally, IGameCommand has a ShortString field, holding a shorthand version of the command. For example, a command with a CommandString of !vote could have the ShortString !v.

In RW/Scripts/Common, create a script called ChatCommand.

If a ChatMessage contains a valid command, its data is stored in a ChatCommand. Import the System namespace (and System.Collections.Generic if it wasn’t added by default). Then give ChatCommand the [System.Serializable] decorator and create the fields shown below:

using System.Collections.Generic;
using System;

[System.Serializable]
public class ChatCommand
{
    public IGameCommand command; // 1
    public string username; // 2 
    public DateTime timestamp; // 3 
    public List<string> arguments; // 4 

    //...

These fields store the:

  1. Command script for the command the user submitted.
  2. Username of the user that submitted the command.
  3. Exact time the user submitted the command, which can be useful for logging user actions and debugging gameplay issues.
  4. Arguments submitted with the command.

ChatCommand needs a constructor and two methods: Execute and ParseCommandArguments. Insert these below the public fields:

public ChatCommand(ChatMessage message, IGameCommand submittedCommand) // 1
{
    arguments = new List<string>(); // 2
    command = submittedCommand; // 3
    username = message.user; 
    timestamp = DateTime.Now; 
    ParseCommandArguments(message); // 4
}

public void ParseCommandArguments(ChatMessage message)
{
    string[] splitMessage = message.message.Split();
  
    if (splitMessage.Length > 1)
    {
        for (int i = 1; i < splitMessage.Length; i++)
        {
            arguments.Add(splitMessage[i]);
        }
    }
}

Here's a code breakdown:

  1. The constructor takes two arguments: ChatMessage and IGameCommand.
  2. Then it instantiates arguments to a new List.
  3. Next, it assigns submittedCommand to command, message.user to username and sets timestamp to DateTime.Now.
  4. Finally, it calls ParseCommandArguments and passes in message.

ParseCommandArguments splits the message to isolate any values after the command string and adds them to arguments. For example, if a user submits !vote 1, then 1 is the stored argument.

The Game Manager

The GameManager is your app's the brain. It uses your scripts to read incoming messages, parse commands and execute valid commands in-game.

In RW/Scripts/Common, create GameManager. Then attach GameManager to Managers/GameManager in the scene hierarchy.

Shows attaching the GameManager script to the GameManager GameObject.

Open the GameManager script. Above the GameManager class, declare an enum called GameState. Give it two values: Playing and GameOver:

public enum GameState
{
    Playing,
    GameOver
}

In the GameManager class, add:

public GameState gameState; // 1
public ChatCommand currentCommand; // 2 

private TwitchChat chat; // 3
private List<ChatCommand> newCommands; // 4
private IGameCommand[] gameCommands; // 5

These fields hold:

  1. An enum that identifies the game's current state.
  2. The command the game manager is currently executing.
  3. A reference to TwitchChat so GameManager can read in chat messages.
  4. The list of user submitted commands.
  5. An array of valid game commands, attached to the same object as the GameManager.

Next, implement Start and a private SetGameState as shown below:

void Start()
{
    gameCommands = GetComponents<IGameCommand>();
  
    newCommands = new List<ChatCommand>();
    chat = gameObject.GetComponent<TwitchChat>(); 
  
    SetGameState(GameState.Playing); 
}

private void SetGameState(GameState newGameState)
{
    gameState = newGameState;
}

Start initializes some of the class's fields and sets the default game state. SetGameState takes a GameState and sets the gameState field.

Now, create a public method called ResetGame:

public void ResetGame()
{
    if (gameState != GameState.Playing)
    {
        SetGameState(GameState.Playing);
    }
}

If it's not already set to Playing, ResetGame sets gameState to Playing. Later, this method will return the game to its original state, so a new game can start.

Create a public method called EndGame and implement it as below:

public void EndGame()
{
    if (gameState != GameState.GameOver)
    {
        SetGameState(GameState.GameOver);
    }
}

If it's not already set to GameOver, EndGame sets gameState to GameOver. Later, this method will do everything needed when a game is over, such as displaying a scoreboard.

Next, open IGameCommand. Add this method signature to the bottom of the interface:

bool Execute(string username, List<string> arguments, GameManager gm = null);

Now IGameCommand has a method called Execute that returns a boolean and takes three parameters:

  • A string storing the username of the player that submitted the command.
  • The List of strings storing arguments passed with the command. For example, an argument for !vote would be the option's number. So, the user would submit the command !vote 1.
  • A GameManager, set to null by default.

Now, go back into ChatCommand. Add the following at the bottom of the class:

public bool Execute(GameManager gm)
{
    return command.Execute(username, arguments, gm);
}

Execute calls command.Execute, passing in the required variables: username, arguments and GameManager. It also returns a boolean indicating if the command was successful.

Of course, you need to check that any incoming chat message matches a valid command.

Validating Commands

Back in GameManager, create a private method called CommandIsValid that takes a ChatMessage and returns an IGameCommand, as shown below:

private IGameCommand CommandIsValid(ChatMessage chatMessage) 
{
    if(chatMessage != null) 
    {
        string commandString = chatMessage.message.Split()[0]; // 1

        foreach (IGameCommand command in gameCommands) // 2
        {
            if (commandString == command.CommandString || commandString == command.ShortString) // 3
            {
                return command;
            }
        }
    }       
    return null; // 4
}

In this code:

  1. CommandIsValid splits the ChatMessage and gets the first string to check if its a valid command string, for example !vote.
  2. It loops through each command in gameCommands.
  3. If the command string matches a IGameCommand's CommandString or ShortString, then it returns the command.
  4. If it doesn't find a match, it returns null.

Now, replace Update with FixedUpdate. Declare a private method ProcessCommands after it. Implement them both as below:

void FixedUpdate()
{
    if (gameState == GameState.Playing) //1
    {
        ChatMessage recentMessage = chat.ReadChat(); //2

        IGameCommand command = CommandIsValid(recentMessage); //3
        if (command != null) //4
        {
            ChatCommand newCommand = new ChatCommand(recentMessage, command); //5
            newCommands.Add(newCommand);
        }

        ProcessCommands(); //6
                  
    }
}

private void ProcessCommands()
{
    if (newCommands.Count > 0)
    {
        newCommands[0].Execute(this); //7
        newCommands.RemoveAt(0);
    }
}

In FixedUpdate, the code:

  1. Checks if gameState is GameState.Playing.
  2. Calls chat.ReadChat and stores the returned ChatMessage in a variable called recentMessage.
  3. Then checks if the command is valid by calling CommandIsValid, passing in recentMessage. It stores what returns in a IGameCommand called command.
  4. Checks if command isn't null, meaning your app received a valid command.
  5. Then creates a new ChatCommand, passing in the required variables.
  6. Calls ProcessCommands to execute the stored commands.

In ProcessCommands, the code:

  1. Gets the oldest command in newCommands and calls Execute, then removes that command from the list.

That's all you need to read and execute incoming commands! Time to test all this code!

Testing Commands

In RW/Scripts/Battle, create a script called Vote. This command will print incoming !vote commands to the console so you can test if your Twitch integration works.

Attach Vote to the GameManager object in the scene hierarchy. GameManager only uses commands attached to the same GameObject.

Have Vote implement IGameCommand, and add the following public fields:

public class Vote : MonoBehaviour, IGameCommand
{
    public string CommandString => "!vote";
    public string ShortString => "!v";
}

IGameCommand fields CommandString and ShortString are set to !vote and !v, respectively.

Finally, implement method the Execute method from IGameCommand, that takes the required arguments and prints the message:

public bool Execute(string username, List<string> arguments, GameManager gm = null)
{
    print(username + " sent the !vote command");
    return true;
}

Since Execute needs to return a boolean, return true for now.

Open TwitchChat again. In Update, remove the call to ReadChat(). Now, Vote's Execute takes care of this.

Return to Unity and Play the app. Enter the !vote and !v commands into your Twitch chat and watch the console in Unity reflect these messages.

Testing vote command, with a message appearing in the Unity console

Congratulations! You now have Commands from Twitch being recognised by your Unity game. But how should you use them?

Your Twitch Audience and You

The first thing you need to decide when designing a game for Twitch is how the audience will interact with the game, the player/streamer, or both!

Audience vs Audience

Audience vs Audience games pit viewers against one another. One popular example is Marbles on Stream, where viewers claim a marble that races down a custom track.

Marbles on Stream being played

Marbles on Stream doesn't have any viewer interaction beyond a viewer joining the race, but that doesn't mean games like that can't exist! After completing this tutorial, you'll get the tools and assets you need to create your own Audience vs Audience mini-putt style game.

Audience vs Game

Popularized by Twitch Plays Pokemon, Audience vs Game experiences have become the most common type of stream game on Twitch. In AvG games, all viewers can control the player character and work cooperatively to beat the game.

Some AvG games use voting mechanics to choose the next action. In contrast, others allow a complete free-for-all, executing every command submitted to the chat for a hilariously chaotic experience.

Twitch Plays Pokemon being played

Audience vs Streamer

In Audience vs Streamer games, viewers use commands to help or hinder the streamer's progress in a single-player game. Shockingly, AvS games seem to be the least popular style of stream game, which may be due to the challenges that come with Audience-to-streamer interaction, such as delay. However, there are quite a few AvS games available on online marketplaces with overall positive reviews.

Common Design Challenges

While there are a variety of stream games, all are subject to some common challenges. These challenges are unique to Twitch Chat games, so you may not have considered them before now.

Delay

Nearly all live streams suffer from some form of delay between what the streamer does and what the viewer sees. When designing a game with audience interaction, it's important to take delay into account so the experience is fair and balanced for most viewers. The delay viewers experience can vary depending on their internet speeds or geographical locations.

On a similar note, Twitch has an automatic spam filter to prevent users from sending the same message twice in a short time. Unfortunately, there's no way to turn this feature off, so you must give your audience multiple ways to send the same command.

Audience Interaction

The more interaction you include in your game, the more challenges you have to consider. Games like Marbles on Stream have limited viewer interaction, which prevents delay-related issues.

Twitch Plays Pokemon started as a complete free-for-all. Eventually, it became so popular its developers had to implement a voting system so viewers could make progress in the game. Audience interaction can be tough to design effectively, but if done well, it makes viewers feel more engaged in the experience.

Streamer Interaction

Commonly, streamers take on the role of the commentator, describing what's happening on screen in an entertaining way. However, if you plan to create an Audience vs Streamer experience, you'll need to consider how complex the streamer's role needs to be to make the game entertaining to play and watch.

Where's the Fun?

When making any game, the first question you should ask is, "Where's the fun?" What aspect of your game do viewers find entertaining?

Twitch Plays Pokemon was fun because of a shared story that anyone could be a part of. Marbles on Stream is fun because it's similar to watching a sport and rooting for your favorite team.

The fun in a game should be simple and easy to describe in a few sentences. If you find yourself explaining your game at length, or having to provide a lot of context to describe your idea, consider simplifying your game until you can identify a short and satisfying gameplay loop.

In this tutorial, you'll focus on the Audience vs Game style of stream game, in the form of a simplified Pokémon-like Battle scene.

Exploring the Sample Scene

Battle Scene Screenshot

If you're familiar with the Pokémon video game franchise, your first thought might be, "Wow, this looks almost identical to Pokémon. Like, basically a complete ripoff of the battle sequence from Pokémon Fire Red".

However, if you thought this, you'd be WRONG. It is, in fact, a complete ripoff of the battle sequence from Pokémon Leaf Green.

Of course, there are a few major differences between this scene and Pokémon, aside from Aknee and Bubblesnek.

You'll find the moves at the bottom of the screen. They all have a green label with a number symbol on the left and a red label on the right.

Screenshot of an attack option from the Battle scene

The green label displays the option number of the attack, which viewers use to vote for their preferred option. The red label shows the current number of votes for that option.

VoteTimer is another critical UI element. It's the text box in the center of the scene.

Screenshot of the text box shown in the center of the Battle scene

The text box tells viewers to decide the next move using the command !vote followed by an option number.

It also displays a timer counting down to the end of voting. Then it uses the move with the most votes against the opponent. You'll see all moves announced in the description box on the bottom right in classic Pokémon style.

After the audience makes their move, the opponent attacks. Two health bars show how much damage the audience or the opponent takes: top left for the opponent and bottom right for the audience.

If Bubblesnek still has some health left after the opponent attacks, another vote triggers.

Creating Commands

You need to tell Vote what options are available for voting. To do this, you'll create a script called VoteOption that stores an option's properties and instructions on what should happen if the option wins the vote.

In RW/Scripts/Battle, create a script called VoteOption.

Before you open VoteOption, navigate to RW/Prefabs/Battle in the Project pane. Open ActionButton.

Attach VoteOption to the prefab. Save the changes and close the Prefab Editor. Now all the ActionButtons in the scene have this script. You can find the ActionButtons in the scene hierarchy under UI/BattleCanvas/Player/Actions.

Open the VoteOption script.

Import UnityEngine.UI and add System.Serializable to VoteOption like this:

using UnityEngine.UI;
[System.Serializable]
public class VoteOption : MonoBehaviour
{  
    //...

Using System.Serializable lets you store the class in a List.

Next, declare the following public variables inside VoteOption:

public int optionNumber; // 1
public Text voteCounter; // 2 
public int voteCount; // 3  
public Text optionNumberLabel; // 4
public bool voteActionExecuted; // 5 
public IVoteAction voteAction; // 6            

These fields store:

  1. The option's number used for voting. For example, in the command !vote 1, 1 is the optionNumber.
  2. A red label on the ActionButton, used to display the current number of votes for this option.
  3. The total number of votes for this option.
  4. A green label on the ActionButton, used to display the option number for this VoteOption.
  5. Whether this VoteOption executed its in-game action.
  6. An IVoteAction that executes if this option receives the most votes.
Note: I created the IVoteActions for you. You'll find them in RW/Scripts/Battle.

After Start, create a method called SetOptionNumberText:

public void SetOptionNumberText()
{
    optionNumberLabel.text = "#" + optionNumber.ToString();
}

This sets the option number label to the optionNumber.

Mark Start as private and implement it as below:

private void Start()
{
    SetOptionNumberText();
    voteAction = GetComponent<IVoteAction>();
}

Here, this code calls SetOptionNumberText and sets voteAction to the IVoteAction attached to the same VoteOption GameObject.

Now you need a method to call when VoteOption receives a vote. Create a method called AddVote:

public void AddVote() 
{
    voteCount++;
    voteCounter.text = voteCount.ToString();
}

This method increases the voteCount by one and updates voteCounter.text with voteCount's value.

Nex, create a method called ClearVotes, which clears votes before a new vote phase:

public void ClearVotes()
{
    voteCount = 0; 
    voteCounter.text = voteCount.ToString(); 
    voteActionExecuted = false; 
}

This sets voteCount to 0, updates voteCounter.text and sets voteActionExecuted to false.

Finally, create a method called ExecuteOption, which checks if voteActionExecuted is false:

public void ExecuteOption()
{
    if (!voteActionExecuted) 
    {
        voteAction.Execute(); 
        voteActionExecuted = true; 
    }
}

If voteActionExecuted is false, this method calls voteAction.Execute and sets voteActionExecuted to true. Checking if voteActionExecuted is false prevents you from accidentally executing the same action twice.

Return to Unity. In the hierarchy, go through each ActionButton in UI/BattleCanvas/Player/Actions and give each a unique Option Number from 1 to 4. Expand each button and drag the Text inside OptionNumberLabel into Option Number Label. Drag the Text inside VoteCount into VoteCounter.

Setup for each ActionButton in the Inspector.

Leave Vote Count at 0 and Vote Action Executed as false.

Expanding the Vote Script

Time to expand Vote to use VoteOption.

Open Vote.

Import UnityEngine.UI:

using UnityEngine.UI;   

Declare the following public variables underneath ShortString:

public float votePhaseLength; // 1  
public bool isVotingOpen; // 2  
public List<VoteOption> voteOptions; // 3 
public GameObject voteTimerObject; // 4 
public Text voteTimerLabel; // 5 
public VoteOption selectedOption; // 6 
public BattleManager battleManager; // 7 
private float votePhaseTimer; // 8 

Here's a variable breakdown:

  1. How long in seconds the audience has to submit a vote.
  2. Allow voting during the vote phase, as well as prevent voting after the vote phase ends.
  3. Available VoteOptions.
  4. Voting timer in the VoteTimer text box, shown in the center of the scene.
  5. Text UI element in VoteTimer.
  6. The VoteOption that won the majority vote.
  7. The BattleManager.
  8. Remaining time of the vote phase.

In Execute, replace what you currently have with:

public bool Execute(string username, List<string> arguments, GameManager gm = null)
{
    if (isVotingOpen) // 1
    {
        if(int.TryParse(arguments[0], out int optionVoted))  //2
        {
            foreach (VoteOption option in voteOptions) // 3
            {
                if (option.optionNumber == optionVoted) 
                {
                    option.AddVote(); 
                }
            }
            return true; // 4
        }
    }
    return false; // 5
}

This updated code:

  1. Checks if isVotingOpen is true.
  2. If true, it gets the number of the option that received a vote by parsing the first argument into an int.
  3. For each VoteOption in voteOptions, it checks if it's optionNumber matches the number submitted. If it does, it calls AddVote for that option.
  4. Outside the loop, it returns true to indicate that the command executed successfully.
  5. If a user submits a vote while voting is closed, or if the option passed in wasn't parsed to an int, you want Execute to indicate that it failed. So, return false outside the isVotingOpen check.

Create a method called OpenVoting:

public void OpenVoting() 
{
    isVotingOpen = true; // 1
    voteTimerObject.SetActive(true); // 2
    foreach (VoteOption option in voteOptions) 
    {
        option.ClearVotes(); // 3
    }
    votePhaseTimer = votePhaseLength; // 4
}

OpenVoting opens a new vote phase by:

  1. Setting isVotingOpen to true.
  2. Activating voteTimerObject.
  3. Clearing all votes.
  4. Resetting votePhaseTimer.
  5. Next, create a method called CloseVoting and implement it as below:

    public void CloseVoting() 
    {
        isVotingOpen = false; // 1
        voteTimerObject.SetActive(false); // 2
        voteOptions.Sort((VoteOption a, VoteOption b) => a.voteCount < b.voteCount ? 1 : -1);
        selectedOption = voteOptions[0];  // 3
        if (battleManager.battlePhase == 0) 
        {
            battleManager.battlePhase = 1; // 4
        }
    }
    

    Here's a code breakdown:

    1. CloseVoting sets isVotingOpen to false to prevent counting new votes.
    2. Then it also deactivates voteTimerObject.
    3. Next, it sorts the VoteOptions from highest voteCount to lowest and selects the most voted option.
    4. Finally, it sets the battle phase to 1. When battlePhase is 0, BattleManager waits for voting end. When battlePhase is 1, BattleManager knows to proceed by announcing the chosen action.

    Finally, add the following code in Update:

    // count down vote phase timer
    if (battleManager.battlePhase == 0 && !isVotingOpen) // 1
    {
        OpenVoting(); 
    }
    if (isVotingOpen) // 2
    {
        if (votePhaseTimer > 0) 
        {
            votePhaseTimer -= Time.deltaTime; 
            voteTimerLabel.text = votePhaseTimer.ToString("0"); 
        }
        else // 3
        {
            CloseVoting(); 
        }
    }
    

    Here, you:

    1. Check if battleManager.battlePhase is 0 and if isVotingOpen is false. If it is, you call OpenVoting.
    2. Since voting is open, you check if votePhaseTimer is greater than 0. If so, you reduce the time by Time.deltaTime and update voteTimerLabel.
    3. Then, if votePhaseTimer is less than or equal to 0, you call CloseVoting.

    Expanding BattleManageer

    Next, open RW/Scripts/Battle/BattleManager.cs. It has several commented out references to Vote. I didn't want them sending errors to the console while you worked on getting Vote ready.

    Now Vote is ready, uncomment:

  • The Vote field on line 38.
  • AnnouncePlayerAction on lines 84-90/.
  • ExecutePlayerAction on lines 95-102.

The BattleManager code shares a lot of functionality with Vote, but controls the opponent rather than the player character. It also controls the battle's pace and displays the current action of the player or opponent.

Save your scripts and go back into Unity. With the comments removed, BattleManager now has a public Vote field in the inspector.

Click BattleManager in the hierarchy. Then drag the GameManager object into the Vote field of the BattleManager script.

BattleManager setup in the Inspector

Select GameManager in the hierarchy and look at the Vote component. Set the following values:

  1. Vote Phase Length to 20. Feel free to play around with this value.
  2. Drag each of the VoteOption buttons you set up earlier onto the list of Vote Options.
  3. Vote Timer Object to UI/BattleCanvas/VoteTimer.
  4. Vote Timer Label to UI/BattleCanvas/VoteTimer/VoteTimerLabel.
  5. Battle Manager to Managers/BattleManager.

The setup for the Vote component in the Game Manager.

Game Over and New Game Conditions

The game isn't quite ready to play yet. You still need to add some game logic, like what should happen when the battle is over?

Battle needs its own logic for when you consider a game to be over, as well as instructions for what to do when that happens.

It also needs instructions on how to start a new game, such as resetting the health of both creatures. You'll start by validating the game is actually over.

Game Over Check

Before executing a Game Over Action, you need a script that checks if the game has reached game over conditions. For GameManager to check these conditions no matter what game it's managing, you need to create an interface, so GameManager always knows what to expect.

In RW/Scripts/Common, create a new script called IGameOverCheck. Open it and replace the default code with:

public interface IGameOverCheck
{
      bool IsGameOver(GameManager gm);
}

This interface defines one method that takes a GameManager as an argument and returns a boolean.

Any script implementing this interface must have an IsGameOver that returns true if the game over conditions are met. Even though it's small, interfaces like this can be a handy way of defining a type for a class without forcing it to inherit from a parent class.

Now all GameManager has to do is look for an IGameOverCheck and call IsGameOver to know whether or not to end the game. This also means any script using IGameOverCheck can be as simple or as complex as needed.

Time to create a Game Over Check specific to Battle. Create a script in RW/Scripts/Battle called BattleGameOverCheck and open it.

BattleGameOverCheck uses the interface you created and defines the Battle scene's game over conditions.

The battle should end when one of the...Backpack...Demons...Badmons...

The battle should end when one of the Legally Distinct Pokémon-like Battle Creatures reaches 0 HP. :]

In BattleGameOverCheck, implement IGameOverCheck as below:

using UnityEngine.UI; 
public class BattleGameOverCheck : MonoBehaviour, IGameOverCheck
{
    public Slider playerHealth; 
    public Slider opponentHealth; 

    public bool IsGameOver(GameManager gm)
    {
        if (playerHealth.value <= 0 || opponentHealth.value <= 0) 
        {
            return true; 
        }
        return false; 
    }
}

This Game Over Check looks at two different Sliders, representing the player's and opponent's health. In IsGameOver, it returns true if either slider's value is less than or equal to 0. Otherwise, it returns false.

Game Over Action

Just like IGameOverCheck, you need a common definition of what the GameManager should do if a game is over.

In RW/Scripts/Common, create a script called IGameOverAction.

public interface IGameOverAction
{
    void Execute(GameManager gm);
}

IGameOverAction defines one method, Execute, that takes a GameManager.

Back in RW/Scripts/Battle, create a script called BattleGameOverAction:

public class BattleGameOverAction : MonoBehaviour, IGameOverAction
{
    public GameObject gameOverPanel; 

    public void Execute(GameManager gm)
    {
        gameOverPanel.SetActive(true); 
    }
}

BattleGameOverAction implements IGameOverAction and has a public reference to the GameOverPanel UI element. Execute in BattleGameOverAction activates the GameOverPanel.

While GameManager now knows how to end a game, you need to tell it how to start over.

New Game Action

Like the Game Over Action, you need to define what GameManager needs to prepare the scene for a new game.

In RW/Scripts/Common, create a script called INewGameAction, which is pretty much identical to IGameOverAction:

public interface INewGameAction
{
    void Execute(GameManager gm);
}

When a new Battle game starts, both health bars should reset and the game over panel should hide.

Now, in RW/Scripts/Battle, create a script called BattleNewGameAction.

BattleNewGameAction implements INewGameAction, as shown below:

using UnityEngine.UI;

public class BattleNewGameAction : MonoBehaviour, INewGameAction 
{
    public Slider playerHealth; 
    public Slider opponentHealth; 
    public GameObject gameOverPanel; 

    public void Execute(GameManager gm)
    {
        playerHealth.value = 100; 
        opponentHealth.value = 100; 
        gameOverPanel.SetActive(false); 
    }
}

It has public variables for both health sliders and the game over panel. In Execute, set the both health bar values to 100 and deactivate the game over panel.

Back in the editor, add BattleGameOverCheck, BattleGameOverAction and BattleNewGameAction to the Managers/GameManager object and set their fields to the appropriate GameObjects:

In Battle Game Over Action set Game Over Panel to UI/BattleCanvas/GameOverPanel.

Then, in Battle Game Over Check set:

  • Player Health to UI/BattleCanvas/Player/PlayerHealth.
  • Opponent Health to UI/BattleCanvas/Opponent/OpponentHealth.

In Battle New Game Action set:

  • Player Health to UI/BattleCanvas/Player/PlayerHealth.
  • Opponent Health to UI/BattleCanvas/Opponent/OpponentHealth.

Finally, in Battle New Game Action set Game Over Panel to UI/BattleCanvas/GameOverPanel.

The setup for Game Manager's Battle Game Over Action, Battle Game Over Check and Battle New Game Action components.

Revisiting the GameManager

Currently, GameManager is configured to handle incoming commands and run corresponding actions. But you need to expand it to use the last three interfaces you created.

Open RW/Scripts/Common/GameManager.cs. Add these private fields above Start:

private IGameOverCheck gameOverCheck; 
private IGameOverAction gameOverAction; 
private INewGameAction newGameAction; 

In Start, use GetComponent to assign these variables to scripts attached to this GameObject, as shown below:

gameOverCheck = GetComponent<IGameOverCheck>(); 
gameOverAction = GetComponent<IGameOverAction>(); 
newGameAction = GetComponent<INewGameAction>(); 

Then, in ResetGame, call newGameAction.Execute before setting the game state, passing this as a reference to the GameManager:

public void ResetGame()
{
    if (gameState != GameState.Playing)
    {
        newGameAction.Execute(this); // 1
        SetGameState(GameState.Playing);
    }
}

In EndGame, call gameOverAction.Execute before setting the game state. Pass the keyword this as a reference to the GameManager:

public void EndGame()
{
    if (gameState != GameState.GameOver)
    {
        gameOverAction.Execute(this);
        SetGameState(GameState.GameOver);
    }
}

Update FixedUpdate as shown below:

void FixedUpdate()
{
    if (gameState == GameState.Playing)
    {
        if (gameOverCheck == null || !gameOverCheck.IsGameOver(this))
        {
            ChatMessage recentMessage = chat != null ? chat.ReadChat() : null;
            IGameCommand command = recentMessage != null ? 
                                    CommandIsValid(recentMessage) : null;
            if (command != null)
            {
                ChatCommand newCommand = new ChatCommand(recentMessage, command);
                newCommands.Add(newCommand);
            }

            ProcessCommands();
        }
        else if (gameOverCheck.IsGameOver(this))
        {
            EndGame();
        }
    }
}

FixedUpdate now checks if gameOverCheck is null, or if gameOverCheck.IsGameOver returns false. This prevents new commands from storing and processing if the game is currently over.

Now, it also includes a check if gameOverCheck.IsGameOver returns true. If so, it calls EndGame.

Go back into Unity. To trigger BattleNewGameAction, you need to link GameManager.ResetGame to UI/BattleCanvas/GameOverPanel/NewGameButton.

Select NewGameButton and find the On Click trigger list on the Button component. Add a new trigger. Drag GameManager into the empty object slot and select GameManager.ResetGame in the dropdown list.

Setup for the NewGameButton's OnClick trigger.

That's it! Give it a try with some friends!

A GIF showing the Battle Scene in action

Using What You've Learned

You have the foundation and the know-how to create Twitch Chat games! Time to test those newfound skills.

There's another scene in the project called Golf. Use what you learned with Battle to turn it into a functional game!

Need help? Just open the spoiler below to find out how.

[spoiler title="Golf Game Requirements"]
Golf has the following requirements:

  • A GameManager keeps track of players and can retrieve a player by username.
  • Include a join command so audience members can join the game.
  • When an audience member joins, spawn a golf ball for them in a random color with their twitch username displayed above the ball.
  • Include commands to let a player:
    • Set the direction of their ball.
    • Set the power of their stroke.
    • Launch their ball in the specified direction, at the specified power.
  • A Game Over Check checks if all players have reached the hole.
  • A Game Over Action disables commands and displays a scoreboard with the players listed from highest score to lowest score.
  • A New Game Action resets the course and lets audience members submit commands.

See the following GIFs for an idea of what these features look like:

A player joining a Golf game

A Golf player setting the direction of their ball

A Golf player setting the power of their ball

A Golf player launching their ball

If you get stuck while working on Golf, check out the completed version in the Final assets to see some solutions.
[/spoiler]

Where to Go From Here?

Use the Download Materials button at the top and bottom of this tutorial to download both the starter and final projects. You can use the final project to compare with your own work.

In this tutorial you learned how to read messages from Twitch chat, and how to use them to interact with your game. Or two games if you did the bonus Golf game too!

Check out Introduction to Multiplayer Games With Unity and Photon to learn more about creating multiplayer games with Unity.

And while you are hanging out on Twitch, don't forget to follow Unity Tech for live streams of Made With Unity games and developer showcases. :]

I hope you enjoyed this article. If you have any questions or comments, please join the forum discussion below. See you in the next one!

Reviews

More like this

Contributors

Comments