19 Apr 2024

UE5 and TwitchSDK: Integration Guide

Feather San
By Feather San Unreal Engine Developer

TwitchAPI is widely used in many apps and games. Recently Twitch published “Twitch Game Engine Plugins” for everyone. Integrating TwitchAPI with your game has never been that easy.

Vid 1 – Twitch chat integration demo

Initial setup

Go to: https://dev.twitch.tv/docs/game-engine-plugins/
There you can read about Game Engine Plugins and download one for UE5 (Twitch recommends using 5.2 or later).

Pic 1 - Twitch game engine plugin overview page
Pic 1 – Twitch game engine plugin overview page

Plugin lets you use C++ or Blueprints.

Next, you have to create your application on https://dev.twitch.tv/console/apps

Pic 2 – Twitch developer console create application page

Input name of the game you will be creating, add any necessary OAuth URLs (I’ve added localhost for local testing), select proper category (most likely “Game Integration”) and choose “Client Type” (also most likely “Public”).

Pic 3 – Twitch create application form

After you press “Create” button, navigate to your applications page and press “Manage” button.

Pic 4 – Twitch developer console application page with newly created application

From here you need to get your application “Client ID”. You will need this later when you will be configuring the TwitchSDK plugin inside your UE5 project.

Pic 5 – Twitch application Client ID

You are all set to configure your project. Now you need to authenticate your project before you can use TwitchSDK. Refer to https://dev.twitch.tv/docs/game-engine-plugins/unreal-guide-cpp/ if you want to use C++ implementation or https://dev.twitch.tv/docs/game-engine-plugins/unreal-guide-blueprints/ if you want to use Blueprints. Remember, C++ implementation needs some additional steps when configuring your project. Remember to authenticate with the URL generated by TwitchSDK in a web browser and complete the authentication process.

As of writing this post, Twitch Game Engine Plugin for UE5 has implemented only some TwitchAPI features. I needed to use Twitch chat in my project. I will show you now how to implement the “channel.chat.message” EventSub subscription type. This subscription will let you read messages from chat. Later you can add any more EventSub subscriptions from TwitchAPI. Here is a link to the documentation of this EventSub subscription. You can find any needed information about implementation. https://dev.twitch.tv/docs/eventsub/eventsub-subscription-types/#channelchatmessage.

Subscribe to channel.chat.messsage event

First we need to add a new OAuth Scope to allow our application subscribe to channel.chat.message event. Navigate to the TwitchSDK plugin in your project and open Plugins/TwitchSDK/Source/Public/TwitchSDK.h.

TwitchSDK.h
UENUM(BlueprintType)
enum class FTwitchSDKOAuthScope : uint8 {
    ChannelManagePolls UMETA(DisplayName = "channel:manage:polls"),
    ChannelManagePredictions UMETA(DisplayName = "channel:manage:predictions"),
    ChannelManageBroadcast UMETA(DisplayName = "channel:manage:broadcast"),
    ChannelManageRedemptions UMETA(DisplayName = "channel:manage:redemptions"),
    ChannelReadHype_Train UMETA(DisplayName = "channel:read:hype_train"),
    ClipsEdit UMETA(DisplayName = "clips:edit"),
    UserReadSubscriptions UMETA(DisplayName = "user:read:subscriptions"),
    BitsRead UMETA(DisplayName = "bits:read"),
    UserReadChat UMETA(DisplayName = "user:read:chat"),
    UserBot UMETA(DisplayName = "user:bot"),
    ChannelBot UMETA(DisplayName = "channel:bot"),
};
C++

Here add “UserReadChat”, “UserBot”, “ChannelBot” enum entries. You will need to add these scopes when authenticating the app.

Pic 6 - Authenticating user with newly added scopes
Pic 6 – Authenticating user with newly added scopes

https://dev.twitch.tv/docs/authentication/scopes/#twitch-api-scopes here you can check any TwitchAPI scopes.

Now let’s add a new FTwitchSDKEventStreamKind enum entry. Navigate to Plugins/TwitchSDK/Source/Public/TwitchSDKStructs.h.

TwitchSDKStructs.h
/** The type of an EventSub subscription. */
UENUM(BlueprintType)
enum class FTwitchSDKEventStreamKind : uint8
{
    /** A notification when a specified channel receives a subscriber. This does not include resubscribers. */
    Subscription = 0,
    /** A specified channel receives a follow. */
    Follower = 1,
    /** A user cheers on the specified channel. */
    Cheer = 2,
    /** A viewer has redeemed a custom channel points reward on the specified channel 
    or the redemption has been updated (i.e., fulfilled or cancelled). */
    CustomRewardRedemption = 3,
    /** A Hype Train makes progress on the user's channel. Requires the channel:read:hype_train scope. */
    HypeTrain = 4,
    /** A broadcaster raids another broadcaster’s channel. */
    ChannelRaid = 5,
    /** Any user sends a message to a specific chat room. */
    ChannelChatMessage = 6,
};
C++

We need to add the same enum type in Plugins\TwitchSDK\core\include\r66_structs_autogenerated.hpp.

r66_structs_autogenerated.hpp
/// <summary>
/// The type of an EventSub subscription.
/// </summary>
enum class EventStreamKind : uint8_t
{
    /// <summary>
    /// A notification when a specified channel receives a subscriber. This does not include resubscribers.
    /// </summary>
    Subscription = 0,
    /// <summary>
    /// A specified channel receives a follow.
    /// </summary>
    Follower = 1,
    /// <summary>
    /// A user cheers on the specified channel.
    /// </summary>
    Cheer = 2,
    /// <summary>
    /// A viewer has redeemed a custom channel points reward on the specified channel or the redemption
    /// has been updated (i.e., fulfilled or cancelled).
    /// </summary>
    CustomRewardRedemption = 3,
    /// <summary>
    /// A Hype Train makes progress on the user's channel.
    /// Requires the <c>channel:read:hype_train</c> scope.
    /// </summary>
    HypeTrain = 4,
    /// <summary>
    /// A broadcaster raids another broadcaster’s channel.
    /// </summary>
    ChannelRaid = 5,
    /// <summary>
    /// Any user sends a message to a specific chat room.
    /// </summary>
    ChannelChatMessage = 6,
};
C++

We will need this when we want to subscribe to “channel.chat.message” event in EventSub.

In the TwitchSDKStructs.h add new event type

TwitchSDKStructs.h
/** A specified channel chat receives a message. */
USTRUCT(BlueprintType)
struct FTwitchSDKUserMessageSentEvent {
    GENERATED_BODY()

public:
    FTwitchSDKUserMessageSentEvent() = default;
    FTwitchSDKUserMessageSentEvent(const R66::UserMessageSentEvent& x) 
        : BroadcasterUserId(R66::ToFString(x.BroadcasterUserId)), BroadcasterUserName(R66::ToFString(x.BroadcasterUserName)), 
        BroadcasterUserLogin(R66::ToFString(x.BroadcasterUserLogin)), ChatterUserId(R66::ToFString(x.ChatterUserId)), 
        ChatterUserName(R66::ToFString(x.ChatterUserName)), ChatterUserLogin(R66::ToFString(x.ChatterUserLogin)),
        MessageId(R66::ToFString(x.MessageId)), Text(R66::ToFString(x.Text)), 
        MessageType(R66::ToFString(x.MessageType)){}
    operator R66::UserMessageSentEvent() const {
        R66::UserMessageSentEvent v;
        v.BroadcasterUserId = R66::FromFString(BroadcasterUserId);
        v.BroadcasterUserName = R66::FromFString(BroadcasterUserName);
        v.BroadcasterUserLogin = R66::FromFString(BroadcasterUserLogin);
        v.ChatterUserId = R66::FromFString(ChatterUserId);
        v.ChatterUserName = R66::FromFString(ChatterUserName);
        v.ChatterUserLogin = R66::FromFString(ChatterUserLogin);
        v.MessageId = R66::FromFString(MessageId);
        v.Text = R66::FromFString(Text);
        v.MessageType = R66::FromFString(MessageType);

        return v;
    }
    /** The broadcaster ID. */
    UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Twitch") FString BroadcasterUserId;
    /** The broadcaster display name. */
    UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Twitch") FString BroadcasterUserName;
    /** The broadcaster login. */
    UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Twitch") FString BroadcasterUserLogin;
    /** The user ID of the user that sent the message. */
    UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Twitch") FString ChatterUserId;
    /** The user name of the user that sent the message. */
    UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Twitch") FString ChatterUserName;
    /** The user login of the user that sent the message. */
    UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Twitch") FString ChatterUserLogin;
    /** A UUID that identifies the message. */
    UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Twitch") FString MessageId;
    /** The chat message in plain text. */
    UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Twitch") FString Text;
    /** The type of message. */
    UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Twitch") FString MessageType;
};
C++

This UStructure is a representation of the information that EventSub sends through subscription. All properties are listed here https://dev.twitch.tv/docs/eventsub/eventsub-reference/#channel-chat-message-event. I’ve omitted some properties that I didn’t need for this demo.

For this UStructure to properly work we need vanilla C++ struct that will be used when we are parsing received event information from EventSub through WebSocket. Navigate to Plugins\TwitchSDK\core\include\r66_structs_autogenerated.hpp and add a new struct in the R66 namespace. This struct has all properties needed for the UStruct above.

r66_structs_autogenerated.hpp
/// @class UserMessageSentEvent r66_structs_autogenerated.hpp r66.hpp
 /// <summary>
 /// User sends a message to the broadcaster's chat.
 /// </summary>
 struct UserMessageSentEvent {
   /// <summary>
   /// The broadcaster user ID.
   /// </summary>
   R66::string_holder BroadcasterUserId;
   /// <summary>
   /// The broadcaster display name.
   /// </summary>
   R66::string_holder BroadcasterUserName;
   /// <summary>
   /// The broadcaster login.
   /// </summary>
   R66::string_holder BroadcasterUserLogin;
   /// <summary>
   /// The user ID of the user that sent the message.
   /// </summary>
   R66::string_holder ChatterUserId;
   /// <summary>
   /// The user name of the user that sent the message.
   /// </summary>
   R66::string_holder ChatterUserName;
   /// <summary>
   /// The user login of the user that sent the message.
   /// </summary>
   R66::string_holder ChatterUserLogin;
   /// <summary>
   /// A UUID that identifies the message.
   /// </summary>
   R66::string_holder MessageId;
   /// <summary>
   /// The chat message in plain text.
   /// </summary>
   R66::string_holder Text;
   /// <summary>
   /// The type of message.
   /// </summary>
   R66::string_holder MessageType;
 };
C++

Now let’s make a new ESSubscription for “channel.chat.message” subscription. Navigate to Plugins\TwitchSDK\core\src\r66_es.cpp. We need to check what is the condition of subscribing to the event. This can be checked here https://dev.twitch.tv/docs/eventsub/eventsub-reference/#channel-chat-message-condition.

Pic 7 - Create new ESSubscription for “channel.chat.message” event
Pic 7 – Create new ESSubscription for “channel.chat.message” event

First parameter is the type of subscription, second is the version and the last one is the condition name. As you can see in the documentation, this event has two required condition elements. I will address this later, for now let’s make it the first one from the documentation.

Now let’s make wait function that we will use to receive messages sent by users in our project. Navigate to the Plugins\TwitchSDK\core\include\r66api_autogenerated.hpp. Here scroll down to the “Wait” functions declarations and add one for the UserMessageSent event.

r66api_autogenerated.hpp
/// <summary>
/// User sends message to broadcaster's chat.
/// </summary>
/// <remarks>You may only call this with a subscription for the correct event type.</remarks>
/// <param name="desc">An object describing the subscription.</param>
/// <returns>The event.</returns>
/// <seealso cref="SubscribeToEventStream" />
virtual void WaitForUserMessageSentEvent(const EventStreamDesc& desc, ResolveFn<const UserMessageSentEvent&> resolve, RejectFn reject) = 0;
C++

Now, we need to add definition of this function in R66::R66ApiImpl class. Navigate to Plugins\TwitchSDK\core\src\r66_impl.hpp. Here, add

r66_impl.hpp
/* Other WaitFor… function definitions*/
virtual void WaitForUserMessageSentEvent(const EventStreamDesc& desc, ResolveFn<const UserMessageSentEvent&> resolve, RejectFn reject) override;
C++

Also while we are in this class, let’s add MulticastEventQueue<> with UserMessageSentEvent struct type.

r66_impl.hpp
/* Other MulticastEventQueue<...> …Listeners */
MulticastEventQueue<UserMessageSentEvent> UserMessageSentListeners;
C++

Now, let’s go back to the Plugins\TwitchSDK\core\src\r66_es.cpp. Scroll down to the bottom of the file and add two entries: one for ES_IMPL_EVENT() and another one for ES_MM_ENTRY() in the MetaMap.

Pic 8 – Add ES_IMPL_EVENT and ES_MM_ENTRY for “channel.chat.message” event type

These macros create implementation of the “WaitFor” function we defined in the interface and create entry in the MetaMap for mapping EventStreamKind with MulticastEventQueue<> we defined earlier.

Now let’s implement parsing information from the JSON event object to our struct that we created earlier. In the same file, navigate to the

r66_es.cpp
void R66::R66ApiImpl::ESReceiveNotification(const Self& self, const JsonRef& json, string_view messageId)
C++

method and scroll down to the if statement where we check what “subType” is being received. Here add

r66_es.cpp
else if (subType == UserMessageSent.Type)
{
	auto e = json.at("event");
	UserMessageSentEvent umse;
	umse.BroadcasterUserId = e.at("broadcaster_user_id").to_string();
	umse.BroadcasterUserName = e.at("broadcaster_user_name").to_string();
	umse.BroadcasterUserLogin = e.at("broadcaster_user_login").to_string();
	umse.ChatterUserId = e.at("chatter_user_id").to_string();
	umse.ChatterUserName = e.at("chatter_user_name").to_string();
	umse.ChatterUserLogin = e.at("chatter_user_login").to_string();
	umse.MessageId = e.at("message_id").to_string();
	umse.Text = e.at("message").at("text").to_string();
	self->UserMessageSentListeners.Push(umse);
}
C++

Now scroll up to the

r66_es.cpp
void R66ApiImpl::ESUpdateSubscriptions(Self self, string broadcasterId)
C++

method, and here we will address the condition of the EventSub type. We need to add the second required condition here manually to the condition JSON body. We are setting the “user_id” property to be the same value as “broadcaster_id”. This just tells the subscription that our code is reading broadcaster chat as a broadcaster user.

r66_es.cpp
// register
Json body, condition, transport;
condition.set_property(subscription->FilterConditionName, Json(broadcasterId));
if (subscription->Type == STR("channel.follow"sv))
{
	condition.set_property("moderator_user_id", Json(broadcasterId));
}
else if (subscription->Type == STR("channel.chat.message"sv))
{
	condition.set_property("user_id", Json(broadcasterId));
}
C++

BlueprintAsync node

If you want to use this subscription in Blueprints, you need to create a BlueprintAsync node. This is created based on the different nodes supplied with the TwitchSDK plugin. Take any “ApiWaitFor…Event” class and change types for our new subscription event types. I’ve added this class to the private source folder of the plugin.

ApiWaitForUserMessageSentEvent.h
#pragma once

#include "CoreMinimal.h"
#include "Kismet/BlueprintAsyncActionBase.h"
#include "TwitchSDKStructs.h"

#include "ApiWaitForUserMessageSentEvent.generated.h"

DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FAsyncDoneApiWaitForUserMessageSentEvent, FTwitchSDKUserMessageSentEvent, Result, FString, Error);

/**
 * 
 */
UCLASS(meta = (HideThen = true))
class UApiWaitForUserMessageSentEvent : public UBlueprintAsyncActionBase
{
	GENERATED_BODY()
	
	FTwitchSDKEventStreamDesc Desc;

public:
    UPROPERTY(BlueprintAssignable)
    FAsyncDoneApiWaitForUserMessageSentEvent Done;
    UPROPERTY(BlueprintAssignable)
    FAsyncDoneApiWaitForUserMessageSentEvent Error;

    virtual void Activate() override;

    /**
     * A specified channel chat receives a message.
     *
     * You may only call this with a subscription for the correct event type.
     *
     * @param desc An object describing the subscription.
     */
    UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true"), Category = "Twitch")
    static UApiWaitForUserMessageSentEvent* WaitForUserMessageSentEvent(FTwitchSDKEventStreamDesc desc);
};
C++
ApiWaitForUserMessageSentEvent.cpp
#include "ApiWaitForUserMessageSentEvent.h"

void UApiWaitForUserMessageSentEvent::Activate() {
    TWeakObjectPtr<UApiWaitForUserMessageSentEvent> weak(this);
    auto exception_handler = [weak](const std::exception& e) {
        if (weak.IsValid() && weak->Error.IsBound())
            weak->Error.Broadcast(FTwitchSDKUserMessageSentEvent(), FString(e.what()));
        else
            UE_LOG(LogTwitchSDK, Error, TEXT("WaitForUserMessageSentEvent error: %s"), UTF8_TO_TCHAR(e.what()));
        };
    auto v = Desc;
    try {
        FTwitchSDKModule::Get().Core->WaitForUserMessageSentEvent(
            v,
            [weak](const R66::UserMessageSentEvent& r) {
                if (!weak.IsValid()) return;
                FTwitchSDKUserMessageSentEvent v(r);
                weak->Done.Broadcast(v, FString());
            },
            exception_handler
        );
    }
    catch (const std::exception& e) {
        exception_handler(e);
    }
}

UApiWaitForUserMessageSentEvent* UApiWaitForUserMessageSentEvent::WaitForUserMessageSentEvent(FTwitchSDKEventStreamDesc desc) {
    auto ptr = NewObject<UApiWaitForUserMessageSentEvent>();
    ptr->Desc = desc;
    return ptr;
}
C++
Pic 9 - New blueprint async node, “WaitForUserMEssageSentEvent” and structure with payload on this event
Pic 9 – New blueprint async node, “WaitForUserMEssageSentEvent” and structure with payload on this event

C++ subscribe to event method

For using this subscription in C++ we need to create a function in the Plugins\TwitchSDK\core\include\r66.hpp file. Here add:

r66.hpp
/** @copydoc UserMessageSentEvent */
TwitchEventStream<UserMessageSentEvent> SubscribeToUserMessageSentEvents() { return { get_shared_from_this(), EventStreamKind::ChannelChatMessage, &IR66ApiCommon::WaitForUserMessageSentEvent }; }
C++

You can subscribe to this event the same way like in the getting started examples https://dev.twitch.tv/docs/game-engine-plugins/unreal-guide-cpp/

Summary

That is all. Now after you authenticate with the new TwitchAPI scopes we added, you should be able to subscribe to the “channel.chat.message” event with the “SubscribeToEventStream” node or use “SubscribeToUserMessageSentEvents()” function that we created for C++ use. Remember to put your application “Client ID” inside plugin settings in the project settings editor window.

Pic 10 - New enum in “Subscribe to Event Stream” node and new “Wait for User Message Sent Event” async node
Pic 10 – New enum in “Subscribe to Event Stream” node and new “Wait for User Message Sent Event” async node

All you need now is to go to your Twitch channel www.twitch.tv/your-twitch-username and type something in the chat.

Pic 11 - Enter your channel and go to stream view with chat on the side
Pic 11 – Enter your channel and go to stream view with chat on the side

Please check out our demo project and see how we connected Twitch chat to the gameplay:
https://github.com/TheSamurais/UE5TwitchIntegration

Feather San
By Feather San Unreal Engine Developer
SIRBart

Call The Knights!

    Table of contents