Ever since the launch of Twitch Plays Pokemon, I have been fascinated by the idea of users in a live stream having some form of agency over the content they watch.
This has evolved into some indie (and a handful of AAA) games integrating streaming capabilities, where messages sent in the stream’s chat can be used to influence effects in the game session. Pretty cool!
Now picture this, an MMO built from the ground up that lets users interact and impact the world without ever even loading a client. While this proof of concept is some ways away from that; by making use of some basic game design principles, we can boost user retention, engagement, and delight.
Why did I make this?
The #TeamTrees movement was kicked off as a celebration of Mr. Beast’s 20M Subscriber milestone, with the aim of planting 20 million trees around the world. Being supported by other content creators such as Mark Rober, SmarterEveryDay, and PewDiePie. His movement was picked up by news outlets around the world, even attracting large donations from large tech CEOs, including SpaceX (Elon Musk), Twitter (Jack Dorsey), Shopify (Tobias Lütke), and YouTube (Susan Wojcicki).
This movement struck a chord with me; in addition to donating to the cause, I wanted to do more for it. Having seen so many live streams that simply record and stream webpage, I knew it could be done better.
Getting started: Overview
Disclaimer: This prototype was put together in less than 24 hours — absolutely no consideration was given to code architecture or design! Use these learnings as a guideline, not documentation :)
This post will cover the integration and some logic for handling users/scores, though won’t include any Unity-specific guidance.
Tools/Services used in this project:
- Web Scraping (TeamTrees.org Counter)
- Youtube Live Streaming API (Parsing chat messages)
- Unity (Dev engine)
- Postman
- Visual Studio + Photoshop (use whatever code editor/photo editing suite you prefer)
- Lofi Hip Hop Chill Beats to Study and Relax To™
Displaying the #TeamTrees counter
The first thing we need to plug in is our counter from the TeamTrees page, using a GET request by calling ‘UnityWebRequest.Get’, we can grab the source from the TeamTrees.org website. I wrote a small regular expression (Regex) to then extract just the number of trees planted, storing it in a ‘goal’ variable where we interpolate between the current value and the goal. By having this extra line of code, the number climbs up to the new value, instead of jumping to it away, it’s the little things ❤
//Include "using UnityEngine.Networking;" namespace at the top of //your code
void Update() {
progress = (int)Mathf.Lerp(progress, goal, speed * Time.deltaTime);
text.text = progress.ToString("N0") + " \nTrees Planted";
}
IEnumerator GetCounter() {
while (true) {
UnityWebRequest www = UnityWebRequest.Get("https://teamtrees.org/");
yield return www.SendWebRequest();
if (www.isNetworkError || www.isHttpError) {
Debug.Log(www.error);
}
else {
var myRegExp = new Regex("(?<=data-count=\")(.*)(?=\">)");
Match a = myRegExp.Match(www.downloadHandler.text);
goal = int.Parse(a.Value);
if (first) { progress = goal; first = false; }
}
yield return new WaitForSeconds(5f);
}
}
Live Streaming API: Pre-requisites
Generating API Key
To pull messages from our chat, we’ll be using YouTube’s Live Streaming API. Before getting started, we need to generate an API key on the Google Developer Console — once generated, go to the API Console and select the project you just made. On the Enabled APIs Page, make sure the status for YouTube Data API v3 is ON.ref: Google’s page on generating an API Key
Getting Livestream ID
When calling the API, we need to provide a livestream ID — Note that this is not the same as your video ID, we have to make a separate call to find out what our REAL livestream ID is.
Enter the following URL into your browser / Postman:
https://www.googleapis.com/youtube/v3/videos?id={YOUR_LIVESTREAM_VIDEO_ID}&key={YOUR_API_KEY}&part=liveStreamingDetails
(Your video ID is the code at the end of your stream’s URL — https://www.youtube.com/watch?v=M4SnZMTlZ68)
Inside the response you’ll get an ‘activeLiveChatId’ at the bottom, we can then plug this into the next call to get our chat messages!
Pulling Chat Messages
I’d recommend checking out the documentation page for the LiveChatMessages API to familiarise yourself with the extra options available in case you want to play around with them. In this case, we’re only using the snippet, and authorDetails, as we’re only using messages and requesting for the sender’s username/channel ID.
You can test to see if this is working by pasting the following URL into your browser / Postman:
https://www.googleapis.com/youtube/v3/liveChat/messages?liveChatId={LIVE_CHAT_ID}&part=snippet,authorDetails&key={API_KEY}
Message Handling
With the API returning the chat info to us, we can start using it as a method of input! To break this code block down line-by-line, we’ll start with the etag.
etag — To make sure we don’t process messages more than once, I add the etag
property that gets sent with every snippet to a list, the etag
being a unique identifier for that message.
!tree — As we only want to handle a !tree
command, we’re excluding all other messages. Should we want to start accepting multiple commands, we can scrap this line and add our own handler in later.
authorChannelID — This is a unique identifier or a user’s channel, this will allow us to ignore adding a user to the queue if they are already in it, avoiding congestion.
PlayerPool — I created an object pool for a bunch of players, the Busy()
method returns either true/false, depending on whether a user with the same channel ID is already being rendered.
If all of these checks go through OK, then we add both the user’s display name and channel ID to a queue, then cache the message to avoid duplication later.
for (int i = 0; i < messages.Count; i++) {
if (!SavedIDs.Contains(messages[i]["etag"])) {
if (messages[i]["snippet"]["displayMessage"].Value == "!tree") {
if (!UserQueueID.Contains(messages[i]["snippet"]["authorChannelId"])) {
if (!PlayerPool.Busy(messages[i]["snippet"]["authorChannelId"].Value)) {
UserQueue.Add(messages[i]["authorDetails"]["displayName"]);
UserQueueID.Add(messages[i]["snippet"]["authorChannelId"]);
SavedIDs.Add(messages[i]["etag"]);
}
}
}
}
}
From here, you’ll have a list of qualifying usernames and channel IDs. Now the fun part begins, go do something with that data!
Bonus: Custom Characters
I thought it’d be fun to give every user their own unique avatar — sidestepping the issue of 2-minute latency UX, I used their username as a means of generating the character.
Prepping the character
I used a wonderful asset called Tiny RPG — Forest that contained an animated character sprite. As it was already coloured and split into different sprites, I generated my own sprite sheet, then split them up into key areas that I could recolour. I set the colours for each layer to white / grey for the standard / shaded areas of the image, as retaining the original colours would throw off my colouring later on.
Splitting these into sprites manually would’ve been an incredibly tedious task, thankfully, Pausebreak Studios released Easy Sprite Sheet Copy tool that copies over a sprite atlas data between sprite sheets.
With all of these sprites chopped up, there’s no need to manually position them as they’ll already be relative to the base layer. Dragging them into the animator gives us this spooky ghost-looking character.
Custom Colouring
To avoid completely random characters, I created a set of colours for each area (note the base layer having some funky looking colours, this is because I didn’t greyscale the sprites).