Chat (GPW) - Guide

[SMPL-GPW-02]

This documents how to understand and apply the benefits of Chat - Overview in game development. Or watch this video.

📘

Variations On A Theme

There are two repos for the "Global Price Wars" game each with distinct learning goals.

  1. Chat GPW Sample Project - Simpler project, the GPWBasicDataFactory uses random, local data values. You are currently viewing the documentation for this project.
  2. Chat GPW2 Sample Project With MicroStorage - Complex project, the GPWMicroStorageDataFactory uses Beamable Microservices and MicroStorage with data values stored in shared database

Download

These learning resources provide a better way to build live games in Unity.

SourceDetail
1. Download the Chat GPW Sample Project
2. Open in Unity Editor ( Version 2020.3.23f1 )
3. Open the Beamable Toolbox
4. Sign-In / Register To Beamable. See Installing Beamable for more info
5. Rebuild the Unity Addressables : Unity → Window → Asset Management → Groups, then Build → Update a Previous Build
6. Open the Scene01Intro Scene
7. Play The Scene: Unity → Edit → Play
8. Click the "Start" Button
9. Enjoy!

Note: Supports Mac & Windows and includes the Beamable SDK

Rules of the Game

  • 1 player starts the game with limited turns
  • After the final turn, the game is over
  • The player's final score is calculated as Cash + Bank - Debt
  • Each turn, the player makes decision to increase the total cash
    • Use money in Cash. Buy items at a low price, Sell items at a high price
    • Move money into the Bank. The bank pays interest to the player after each turn
    • Move money out of Debt. The debtors charge interest to the player after each turn
    • Chat with other players to better understand the market price of items

Pro Tip: Sell all owned items before the final turn to increase the final score.

Screenshots

Scenes

Scene01IntroScene02GameScene03ChatScene04SettingsScene05Leaderboard

Design Overview

Many Beamable features are used to implement the game design and game rules.

Features

📘

Related Features

More details are covered in related feature page(s).

Chat - Allow players to communicate in-game
Connectivity - Indicates status of network connection availability
Content - Allow game maker to store project-specific data
Inventory - Allow player to manage inventory
Leaderboards - Allow player to manage leaderboard

Content

Using the Beamable Content feature, this game stores core data with ease and flexibility.

Remote ConfigurationLocationProduct

Class Organization

Each scene uses the GPWController class to interact with the local data model and the remote services.

Here is a high-level chart showing the _partial _structure.

Design of Data

The data within the RuntimeDataStorage is vital for the core game loop.

The game design requires that all users in the same game (e.g. same chat session) to see a shared world of consistent pricing. This is accomplished using the data structure and data factory below.

Data Structure
The structure contains all the locations of the game. Within each location is information about each product; including its price and quantity.

  • List < LocationContentView >
    • LocationData (e.g. "Africa")
    • List < ProductContentViews >
      • ProductData (e.g. "Chocolate")
      • MarketGoods
        • Price (e.g. "10")
        • Quantity (e.g. "3")

Data Factory
The IDataFactory gets data as shown in the diagram below.

  • A. The RuntimeDataStorage calls to GetLocationContentViews() at the start of each game session
  • B. Here in the GPW game the GPWBasicDataFactory creates pseudorandom price/quantity for all products. A random-seed value is shared across all game clients for consistent pricing. This solution is easier to understand, but likely not robust enough for a production game.

Development

Here is an overview of project organization.

  • Project Window - Suggested entry points for game makers include the Scenes folder and Scripts folder
  • Project Configuration - The Configuration class holds settings used throughout the game. Game makers can adjust settings at runtime or edit-time
  • Project Scene (Chat) - The Scene03Chat.unity scene holds all chat-related functionality
Project WindowProject ConfigurationChat Scene

Code

The core chat functionality is centralized in the sample game project's GameServices class.

Here are some highlights of the Beamable SDK's ChatService class.

Subscribe to Chat Changes

IBeamableAPI beamableAPI = await Beamable.API.Instance;
ChatService chatService = beamableAPI.Experimental.ChatService;
chatService.Subscribe(ChatService_OnChanged);

Handle Chat Changes

private void ChatService_OnChanged(ChatView chatView)
{
	_chatView = chatView;
	foreach (RoomHandle roomHandle in _chatView.roomHandles)
	{
		if (IsLocalPlayerInRoom(roomHandle.Name))
		{
			roomHandle.OnMessageReceived -= RoomHandle_MessageReceived;
			roomHandle.OnMessageReceived += RoomHandle_MessageReceived;
		}
	}
}

Send Chat Message

IBeamableAPI beamableAPI = await Beamable.API.Instance;
ChatService chatService = beamableAPI.Experimental.ChatService;
_chatService.SendMessage("MyRoomName", "HelloWorld!");

Chat Manager

The Scene03ChatManager.cs is the main entry point to the chat scene logic. It relies on the GameServices class for the chat-related operations.

using System;
using System.Text;
using System.Threading.Tasks;
using Beamable.Common.Api;
using Beamable.Experimental.Api.Chat;
using Beamable.Samples.Core.Data;
using Beamable.Samples.Core.UI;
using Beamable.Samples.GPW.Data.Storage;
using Beamable.Samples.GPW.Views;
using UnityEngine;

namespace Beamable.Samples.GPW
{
   /// <summary>
   /// Handles the main scene logic: Chat 
   /// </summary>
   public class Scene03ChatManager : MonoBehaviour
   {
      //  Properties -----------------------------------
      public Scene03ChatUIView Scene03ChatUIView { get { return _scene03ChatUIView; } }

      //  Fields ---------------------------------------
      [SerializeField]
      private Scene03ChatUIView _scene03ChatUIView = null;
      
      //  Unity Methods   ------------------------------
      protected async void Start()
      {
         
         // Clear UI
         _scene03ChatUIView.ScrollingText.SetText("");
         _scene03ChatUIView.ScrollingText.HyperlinkHandler.OnLinkClicked.AddListener(HyperlinkHandler_OnLinkClicked);
         
         // Top Navigation
         _scene03ChatUIView.GlobalChatToggle.onValueChanged.AddListener(GlobalChatButton_OnClicked);
         _scene03ChatUIView.LocationChatToggle.onValueChanged.AddListener(LocationChatButton_OnClicked);
         _scene03ChatUIView.DirectChatToggle.onValueChanged.AddListener(DirectChatButton_OnClicked);
         SetChatMode(ChatMode.Global);
         
         // Input
         _scene03ChatUIView.ChatInputUI.OnValueSubmitted.AddListener(ChatInputUI_OnValueSubmitted);
         _scene03ChatUIView.ChatInputUI.OnValueCleared.AddListener(ChatInputUI_OnValueCleared);

         // Bottom Navigation
         _scene03ChatUIView.BackButton.onClick.AddListener(BackButton_OnClicked);
         
         // Load
         _scene03ChatUIView.DialogSystem.DelayBeforeHideDialogBox =
            (int)_scene03ChatUIView.Configuration.DelayAfterDataLoading * 1000;
         
         await ShowDialogBoxLoadingSafe();
         SetupBeamable();
      }


      //  Other Methods  -----------------------------
      private async void SetupBeamable()
      {
         // Setup Storage
         GPWController.Instance.PersistentDataStorage.OnChanged.AddListener(PersistentDataStorage_OnChanged);
         GPWController.Instance.RuntimeDataStorage.OnChanged.AddListener(RuntimeDataStorage_OnChanged);
         GPWController.Instance.GameServices.OnChatViewChanged.AddListener(GameServices_OnChatViewChanged);
         
         // Every scene initializes as needed (Max 1 time per session)
         if (!GPWController.Instance.IsInitialized)
         {
            await GPWController.Instance.Initialize(_scene03ChatUIView.Configuration);
         }
         else
         {
            GPWController.Instance.PersistentDataStorage.ForceRefresh();
            GPWController.Instance.RuntimeDataStorage.ForceRefresh();
         }
      }

      
      private async Task<EmptyResponse> ShowDialogBoxLoadingSafe()
      {
         // Get roomname, and fallback to blank
         string roomName = "";
         if (!_scene03ChatUIView.DialogSystem.HasCurrentDialogUI && GPWController.Instance.HasCurrentRoomHandle)
         {
            RoomHandle roomHandle = GPWController.Instance.GetCurrentRoomHandle();
            roomName = roomHandle.Name;
         }

         if (_scene03ChatUIView.DialogSystem.HasCurrentDialogUI)
         {
            await _scene03ChatUIView.DialogSystem.HideDialogBoxImmediate();
         }
         _scene03ChatUIView.DialogSystem.ShowDialogBoxLoading(roomName);
         return new EmptyResponse();
      }
      
      
      private async void SetChatMode(ChatMode chatMode)
      {
         // Change mode
         GPWController.Instance.RuntimeDataStorage.RuntimeData.ChatMode = chatMode;

         // THis mode can be reached by clicking chat text too
         // so update the button to look selected
         if (chatMode == ChatMode.Direct)
         {
            _scene03ChatUIView.DirectChatToggle.isOn = true;
         }
         
         // Show mode specific prompt
         await ShowDialogBoxLoadingSafe();
         
         // Update
         GPWHelper.PlayAudioClipSecondaryClick();
         GPWController.Instance.RuntimeDataStorage.ForceRefresh();
      }
      
      
      private async void RenderChatOutput()
      {
         if (!GPWController.Instance.GameServices.HasChatView)
         {
            return;
         }
         
         if (!GPWController.Instance.HasCurrentRoomHandle)
         {
            return;
         }
         
         RoomHandle roomHandle = GPWController.Instance.GetCurrentRoomHandle();

         ChatMode chatMode = GPWController.Instance.RuntimeDataStorage.RuntimeData.ChatMode;
         StringBuilder stringBuilder = new StringBuilder();
         stringBuilder.AppendLine("---- RenderChatOutput Room ----");
         stringBuilder.AppendLine($"Title: {roomHandle.Name}");
         stringBuilder.AppendLine($"Topic: {GPWHelper.GetChatRoomTopic (chatMode)}");
         stringBuilder.AppendLine($"Players: {roomHandle.Players.Count}");
         stringBuilder.AppendLine($"Messages: {roomHandle.Messages.Count}");
         stringBuilder.AppendLine().AppendLine();
         
         foreach (Message message in roomHandle.Messages)
         {
            long playerDbid = message.gamerTag;
            string alias = "";
            try
            {
               alias = await GPWController.Instance.GameServices.GetOrCreateAlias(playerDbid);
            }
            catch (Exception e)
            {
               Debug.Log("E: " + e.Message);
            }
            
            //Temporarily override alias to reduce confusion. Its by the local player but 
            //from a previous account. 
            if (!GPWController.Instance.GameServices.IsLocalPlayerDbid(playerDbid) &&
                alias == GPWHelper.DefaultLocalAlias)
            {
               alias = MockDataCreator.CreateNewRandomAlias(GPWHelper.DefaultRemoteAliasPrefix);
            }
                         
            if (GPWController.Instance.RuntimeDataStorage.RuntimeData.ChatMode == ChatMode.Direct ||
                GPWController.Instance.GameServices.IsLocalPlayerDbid(playerDbid))
            {
               stringBuilder.AppendLine($"[{alias}]: " + message.content);
            }
            else
            {
               // When NOT in direct chat, and NOT the local player, renders clickable text
               // Clicks are handled above by "HyperlinkHandler_OnLinkClicked"
               stringBuilder.AppendLine($"[{TMP_HyperlinkHandler.WrapTextWithLink(alias, playerDbid.ToString())}]: " + message.content);
            }
         }
         
         _scene03ChatUIView.ScrollingText.SetText(stringBuilder.ToString());
         
         await _scene03ChatUIView.DialogSystem.HideDialogBox();
         _scene03ChatUIView.ChatInputUI.Select();
      }
      
      
      //  Event Handlers -------------------------------
      private async void HyperlinkHandler_OnLinkClicked(string href)
      {
         if (GPWController.Instance.RuntimeDataStorage.RuntimeData.ChatMode == ChatMode.Direct)
         {
            throw new Exception("HyperlinkHandler_OnLinkClicked() ChatMode cannot be ChatMode.Direct. ");
         }

         SetChatMode(ChatMode.Direct);
         
         RoomHandle roomHandle = GPWController.Instance.GetCurrentRoomHandle();
         long dbid1 = GPWController.Instance.GameServices.LocalPlayerDbid;
         long dbid2 = long.Parse(href);
         await GPWController.Instance.GameServices.JoinDirectRoomWithOnly2Players(dbid1, dbid2);
      }
      
      
      private async void ChatInputUI_OnValueSubmitted(string message)
      {
         RoomHandle roomHandle = GPWController.Instance.GetCurrentRoomHandle();
         await GPWController.Instance.GameServices.SendMessage(roomHandle.Name, message);
         _scene03ChatUIView.ChatInputUI.Select();
      }


      private void ChatInputUI_OnValueCleared()
      {
         GPWHelper.PlayAudioClipSecondaryClick();
      }
      
         
      private void GlobalChatButton_OnClicked(bool isOn)
      {
         if (isOn)
         {
            SetChatMode(ChatMode.Global);
         }
      }
      
      
      private void LocationChatButton_OnClicked(bool isOn)
      {
         if (isOn)
         {
            SetChatMode(ChatMode.Location);
         }
      }
      
      
      private void DirectChatButton_OnClicked(bool isOn)
      {
         if (isOn)
         {
            SetChatMode(ChatMode.Direct);
         }
      }
      
      
      private void BackButton_OnClicked()
      {
         StartCoroutine(GPWHelper.LoadScene_Coroutine(
            _scene03ChatUIView.Configuration.Scene02GameName,
            _scene03ChatUIView.Configuration.DelayBeforeLoadScene));
      }
      
      
      private void PersistentDataStorage_OnChanged(SubStorage subStorage)
      {
         PersistentDataStorage persistentDataStorage = subStorage as PersistentDataStorage;
         _scene03ChatUIView.PersistentData = persistentDataStorage.PersistentData;
         _scene03ChatUIView.LocationContentView = GPWController.Instance.LocationContentViewCurrent;
         RenderChatOutput();
      }
      
      
      private void RuntimeDataStorage_OnChanged(SubStorage subStorage)
      {
         RuntimeDataStorage runtimeDataStorage = subStorage as RuntimeDataStorage;
         _scene03ChatUIView.RuntimeData = runtimeDataStorage.RuntimeData;
         RenderChatOutput();
      }
      
      
      private void GameServices_OnChatViewChanged(ChatView chatView)
      {
         RenderChatOutput();
      }
   }
}

Additional Experiments

Here are some optional experiments game makers can complete in the sample project.

Did you complete all the experiments with success? We'd love to hear about it. Contact us.

DifficultySceneNameDetail
BeginnerGameTweak Configuration• Update the Configuration.asset values in the Unity Inspector Window

Note: Experiment and have fun!
BeginnerVariousAdd New Graphics• Change the colors, fonts, and sprites for cosmetics
IntermediateGameAdd a New Product item• Open the Content Manager
• Duplicate an existing ProductContent
• Update all field values
• Update the existing RemoteConfiguration
• Publish the content
IntermediateGameAdd a New Location item• Open the Content Manager
• Duplicate an existing LocationContent
• Update all field values
• Update the existing RemoteConfiguration
• Publish the content
AdvancedChatAdd user-to-user item transactions• Design and implement new functionality
AdvancedVariousStore player progress between sessions• Add Beamable Cloud Save to store the existing PersistentData object
AdvancedVariousAdd shared database so the local price and local quantity update across all players• Add Beamable Microservices With a DLL of any 3rd party database

Advanced

This section contains any advanced configuration options and workflows.

Learning Resources

These learning resources provide a better way to build live games in Unity.

SourceDetail
1. Download the Chat GPW Sample Project
2. Open in Unity Editor ( Version 2020.3.23f1 )
3. Open the Beamable Toolbox
4. Sign-In / Register To Beamable. See Installing Beamable for more info
5. Rebuild the Unity Addressables : Unity → Window → Asset Management → Groups, then Build → Update a Previous Build
6. Open the Scene01Intro Scene
7. Play The Scene: Unity → Edit → Play
8. Click the "Start" Button
9. Enjoy!

Note: Supports Mac & Windows and includes the Beamable SDK