ゲームの実況動画をインタラクティブに
本ポストはゲームの動画配信において、動画の視聴者が操作できるボタンをブラウザに表示し、
そのボタンの有効化切り替えをゲーム側から制御する仕組みについて説明します。
サンプルは次の通りです。
このgifはブラウザで表示させているものをキャプチャしたものです。カラフルな四角や黒い球などは、PCビルドのUnityで描画したものを動画としてエンコードしてブラウザにストリーミングしています。
その動画の上に、ブラウザ側で4つのボタンを重ねています。
動画視聴者がボタンをクリックすると、その色と同じ四角から黒い球が生成されます。
ゲーム側の四角はランダムで表示されたり消えたりしますが、消えているときはブラウザ側で描画されているボタンも不活性化され、視聴者がクリックできなくなります。
動画ですので、実際のゲームからブラウザに届くまでの遅延が発生しますが、動画内で四角が消えるタイミングと、ボタンが不活性化するタイミングが完全に同期していることが分かります。
動画の視聴者がゲーム内に影響を与えることに加えて、ゲーム側の状況が動画視聴側に反映される、双方向通信を実現しています。
このシステムの実装には、インタラクティブな動画配信を可能にするGenvid SDKを使用しています。
Genvidについて
Genvidは、インタラクティブなライブ配信を実現するSDKです。
https://www.genvidtech.com/ja/
Genvidはクラウドサーバー上で動かすサーバミドルウェアと、ブラウザ側で動画と同期した通信処理を行うJavaScriptライブラリで構成されています。
jsライブラリが動作できる環境であれば配信アプリやプラットフォームは問わず使用でき、主にFacebookとTwitch上で使用されています。
最近では、Facebook Gamingで配信されている『PAC-MAN COMMUNITY』のなかで、視聴者をゲームに招待できるゲーム配信者向けの機能「Play with Streamer」で使用されています。
https://www.facebook.com/fbgaminghome/blog/pac-man-community
また、このゲームにはFacebook Interactivesを利用した[Watch]タブが用意されています。このタブでは、Facebook Gamingストリーマーによるゲームのライブ配信を楽しめます。Watchモードでは、迷路がUnreal Engineを利用した3Dストリーミングに変わり、視聴者は動画プレイヤーと直接対話してどちらの側に付くかを決めたり、AI PAC-MANやゴーストをパワーアップさせて競い合わせることができます。
このタイトルではブラウザ上のゲームはHTMLで動作し、Watchタブで表示される動画はサーバー上でUnreal Engine 4を使って描画されています。
そのほかの事例として、Unityを使いFacebook上で配信されている「Rival Peak」と、UE4を使った「Project Raven」があります。
今回のデモではGenvid SDK for Unityを使っていますが、Unreal Engine 4でも利用可能です。
Genvidのシステムは、AWSまたはAzureのWindowsクラウドサーバー上で動作させ、動画データをTwitchに送信します。また、動画視聴者のブラウザに対してゲーム内のデータをブロードキャストします。
Windowsを使う理由はここでゲームのexeも動作させるからです。GenvidSDKがクラウド上で動画のエンコードとTwitchへの送信、動画視聴者のブラウザへのデータ配信や、逆にブラウザからのデータを収集を行い、ゲームexeに渡します。
プレイヤーが遊ぶゲームは、ローカルのPCやスマホ、ゲーム機などです。Windowsサーバー上のゲームとは、マルチプレイヤーと同じ仕組みで接続します。P2PでもPhoton Cloudのようなサービスでもかまいません。
Genvidは「Windowsクラウドサーバー上でゲームのexeを動作させる」システムなので、開発環境はWindows 10または11となります。
実装解説
Genvid SDKの開発環境構築については、Genvidディベロッパーサイトの日本語マニュアルからご確認ください。
https://www.genvidtech.com/for-developers/
SDK同梱の「Cube」サンプル、およびAsset Storeで配布している「Tanks」サンプルで、どのようにGenvidのシステムを利用するかが理解できます。
https://assetstore.unity.com/packages/templates/tutorials/genvidtanks-sample-161598
以降のガイドは、「Cube」サンプルのUnity版の動作が確認できた環境にて、そのサンプルを改造する形で実装していきます。
Genvid EventとGenvid Streamについて
今回紹介するGenvid SDK側で使用する機能は「Genvid Event」「Genvid Stream」と呼ばれるものになります。
Eventが「視聴者のブラウザからゲームへ」データを渡す処理、Streamが「ゲームから視聴者のブラウザへデータを渡す処理」になります。
Eventについては以下の記事を参照にしてください。
Streamについては以下の記事を参照にしてください。
この例では、プレイヤーの位置データを動画と一緒に配信して、ブラウザ上でプレイヤーの位置と同期したラベルを出しています。
ブラウザ側の実装
動画を視聴するウェブサイトで表示するボタンなどのパーツは、一般的な動的Webページ同様、html, javascript, cssファイルで構成します。
ゲーム動画配信サイトである「Twitch」でGenvidを使用する場合、「Twitch Extensions」と呼ばれる仕組みを使って開発したhtml+jsを表示させます。
これは、Twitch動画の上に文字や画像、ボタンなどを重ねて表示できる仕組みで、審査に通ればTwitch公式サイト上でだれでも閲覧できるようになります。
ボタンの表示はhtmlとcssで構成しますが、Genvidシステムとの通信を行うjsからはGenvid SDKに含まれるjsライブラリ(genvid.umd.js)を使用します。
genvid.umd.jsはGenvidインストールフォルダのapi\web\distにあります。
具体的に必要なファイルは
-index.htm
-style.css
-overlay.js
-genvid.umd.js
の4点です。これらが同じディレクトリにある必要があります。
index.html
htmlにはビデオの表示と、その上に重ねて表示するボタンとラベル、ラベル表示の下地になるcanvas要素を配置します。
今回は「dropBombRed」などの名称を持つボタン4つの配置となります。
<!doctype html>
<html>
<head>
<title>Genvid Overlay</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="video_player"></div>
<button class="button red" type="button" id="red">DropBomb</button>
<button class="button blue" type="button" id="blue">DropBomb</button>
<button class="button green" type="button" id="green">DropBomb</button>
<button class="button yellow" type="button" id="yellow">DropBomb</button>
<script src="genvid.umd.js"></script>
<script src="overlay.js"></script>
</body>
</html>
style.css
cssファイルにはボタン、ラベル、canvasの設定をしていきます。
ポイントはpositionにabsolute属性を指定することで、これによってビデオの上に各要素を重ねて表示させます。
今回は各色のボタンと、それぞれが不活性化(disabled)されたときの見え方を定義します。
body {
background-color: black;
}
.button {
cursor: pointer;
position: absolute;
padding: 1.5vw;
opacity: 0.7;
border-radius: 8px;
color:white;
}
.button:active {
opacity: 1;
}
.button.red {
background-color: red;
top: 0vw;
}
.button.blue {
background-color: blue;
top: 6vw;
}
.button.green {
background-color: green;
top: 12vw;
}
.button.yellow {
background-color: yellow;
top: 18vw;
color:black;
}
.button:disabled {
background-color: gray;
color:black;
}
overlay.js
Genvidとの通信とCanvasの表示などを制御します。
まず、htmlのボタンの参照をbuttonElements配列に保存しておきます。
前半では、ゲームからデータを受け取るStreamの処理を行います。
genvidClientのインスタンスを作成し、onStreamsReceivedコールバックで届いたjsonをバースし、onDrawコールバックタイミングで描画処理を行います。
今回は、Unityから「SpawnerState」という名前のデータを受け取り、draw関数に渡します。
draw関数内では、届いたデータをbyteデータをbits arrayとして解釈し、bool arrayに変換したうえでbuttonElements配列に保存した4つのボタンの有効化・非活性化を設定しています。
var genvidClient;
var buttonElements = document.querySelectorAll("[type=button]");
fetch("/api/public/channels/join", { method: "post" })
.then(function (data) { return data.json() })
.then(function (response) {
genvidClient = genvid.createGenvidClient(response.info, response.uri, response.token, "video_player");
genvidClient.onStreamsReceived(function (dataStreams) {
for (let stream of [...dataStreams.streams, ...dataStreams.annotations]) {
for (let frame of stream.frames) {
try {
frame.user = JSON.parse(frame.data);
}
catch (e) {
console.log(e, frame.data);
}
}
}
});
genvidClient.onDraw(function (frame) {
let gameDataFrame = frame.streams["SpawnerState"];
if (gameDataFrame && gameDataFrame.user) {
draw(gameDataFrame.user);
}
});
genvidClient.start();
})
.catch(function (e) { console.log(e) })
function draw(gameData) {
let boolArray = comvertStateBitsToBoolArray(gameData.stateBits);
for (let index = 0; index < buttonElements.length; index++) {
buttonElements[index].disabled = !boolArray[index];
}
}
function convertStateBitsToBoolArray(bitsData)
{
let boolArray = [false,false,false,false,false,false,false,false];
for (let index = 0; index < 8; index++) {
boolArray[index] = (bitsData & (1 <<index)) !== 0;
}
console.log(boolArray[0] +" "+ boolArray[1])
return boolArray;
}
jsの後半では、動画サイトからゲームへ送信するデータの処理を行います。
配列buttonElementsに格納されたボタンへ、クリックしたときにどんなGenvid Eventを送信するかを設定していきます。
buttonElementにはhtml側でidとして色情報が「red」「blue」などのように指定してあります。
keyが「dropBomb」、valueが「red」などボタンの色を格納したデータをイベントとして送信します。
Array.prototype.forEach.call(buttonElements, function(buttonElement) {
buttonElement.onclick = function () {
genvidClient.sendEvent([{
"key": ["dropBomb"],
"value": buttonElement.id
}])
}
});
Unity側の実装
オブジェクトの配置やスポーンについては一般的な話のため、省略します。
Genvidとの連携に必要なスクリプトとして、ゲームから動画サイトへデータを送信するためのスクリプトと、
動画サイトから届いたデータをゲーム側で受け取るためのスクリプトを作成します。
ゲームから動画サイトへデータを配信(砲台が有効かどうか)
Unityゲーム内で、砲台はランダムに表示・非表示されます。
そのステート情報を、動画のタイムコードと同期させた形でデータを配信します。
データ量を小さくするため、4つの砲台のオンオフ情報をBitArrayで保存した後、それを1バイトの情報としてシリアライズし、送信します。
砲台にはそれぞれ黒い球を生成する「BombSpawner」スクリプトがアタッチされていますが、そのアクティブ情報をチェックして、BitArray化しています。
SubmitSpawnerState関数は、Genvid UnityプラグインのGenvid Streamを制御するスクリプトから定期的にコールします。
public class SpawnerStates: MonoBehaviour
{
public List<BombSpawner> bombSpawnerList = new List<BombSpawner>();
BitArray spawnerStates = new BitArray(4);
public void SubmitSpawnerState(string streamId)
{
if (GenvidSessionManager.IsInitialized)
{
for (var index = 0; index < spawnBombs.Count; index++)
{
var spawnBomb = spawnBombs[index];
spawnerStates[index] = spawnBomb.gameObject.activeSelf;
}
GenvidSessionManager.Instance.Session.Streams.SubmitGameDataJSON(streamId, new SpawnerStates(spawnerStates));
}
}
[Serializable]
public class SpawnerStates
{
[SerializeField] public byte stateBits = 0;
public SpawnerStates(BitArray bits)
{
byte[] bytes = new byte[1];
bits.CopyTo(bytes, 0);
stateBits = bytes[0];
}
}
}
シーンに配置したGenvid Streamゲームオブジェクトのインスペクターで、SubmitSpawnerState関数を呼び、そのID名をSpawnerStateとしています。
ここで指定したID名で、ブラウザ側実装で設定したoverlay.jsの中でデータを受け取ります。
動画サイトからデータを受け取る(砲台のボタンを押されたとき)
逆に、ブラウザ側から送信されたデータの受け取り処理を作ります。
ブラウザ側のoverlay.jsで、keyが「dropBomb」、valueが「red」などボタンの色としたデータを送信する処理を書きました。
こんどはそれをUnity側で受信した時にどんなメソッドを呼ぶかを指定していきます。
黒い球のインスタンスを作るBombSpawnerスクリプトは、自分がアタッチされたキューブの色の名前を保持しています。
dropBombというキーのデータがきたとき、その値のstringを調べて、同じ名前が指定されているキューブに対してSpawn関数を呼びます。
public List<BombSpawner> bombSpawnerList = new List<BombSpawner>();
public void DropBomb (string eventId, GenvidSDK.EventResult[] results, int numResult, IntPtr userData)
{
string spawnerColor = results[0].key.fields[0];
int amount = (int)results[0].values[0].value;
var spawner = bombSpawnerList.FirstOrDefault(s => s.colorName == spawnerColor);
if (spawner.enabled == true)
{
spawner?.Spawn();
}
}
Unityのヒエラルキー上では、この「DropBomb」関数がEventが届いた時に呼ばれるよう、Genvid Eventのインスタンスのインスペクターにアタッチします。
Genvidシステム用の設定
Genvid Eventでは、大量の視聴者から一斉にデータが届いた時のために、クラウドサーバー上でデータ量を小さくしてからゲームに渡す仕組みを持ちます。
簡単に言えば、「キャラクターAに投票」というデータが1万人の視聴者から届いた時、「キャラクターAに1万票」という1つのデータにまとめてしまうような処理です。
このまとめる処理の設定を、Genvidではjsonスキーマとして記述し、サーバーに読み込ませます。
詳しくは以下の記事をご参照ください。
このデモの場合は、「dromBomb」というデータをどのように減らすかについて設定します。
{
"version": "1.7.0",
"event": {
"game": {
"maps": [
{
"id": "dropBomb",
"source": "userinput",
"where": {
"key": [
"dropBomb"
],
"name": "<name>",
"type": "string"
},
"key": [
"dropBomb",
"<name>"
],
"value": 1
}
],
"reductions": [
{
"id": "dropBomb",
"where": {
"key": [
"dropBomb",
"<name>"
]
},
"key": [
"<name>"
],
"value": [
"$sum"
],
"period": 250
}
]
}
}
}
動作テスト(ローカル実行)
以上のスクリプトはオーバーレイ部分と、Unityからデータを送信する部分、eventの設定ファイルのみの例です。
Genvidサーバーを動作させるには、クラウドサーバー用意してGenvidシステムを動作させるための各種設定ファイルが必要です。
クラウドサーバーを使わず、PCの中でテスト実行する場合は、SDKに同梱されている「cube」サンプルから設定ファイルを流用することで動作させることができます。
ローカルクラスタの開始
https://www.genvidtech.com/doc/ja/SDK-1.32.0/development_guide/game_integration/utilisation.html
- Genvid操作用pythonスクリプトのgenvid-toolboxのインストール
- Genvid bastionのローカルPCでのインストール
- システム環境変数「GENVID_STATIC_BINDING」をtrueに設定してポートを3000に固定
- 作業用ディレクトリを作り、{任意のディレクトリ}/publicに作成したhtml,css,jsとgenvid.umd.jsを配置
- /app/BuildにUnityからビルドしたデータ一式を配置
- 「cube」サンプルからbackendフォルダ、configフォルダ、templatesフォルダ、package.json、Web.pyをコピーして作業用ディレクトリに配置
- configフォルダのうちevents.jsonをこの記事のevents.jsonに置き換える
- templates\local\unity.nomad.tmplの「unity_Cube.exe」をビルドしたexe名に変更
- web.pyから
dict(name="stream", required=True),
dict(name="web", required=True),
を削除
- コマンドプロンプトやPowerShellで作業ディレクトリに移動してnpm install
- Genvid Bastionを立ち上げてpy web.py loadコマンドで設定一式を読み込む
以上の手続きでローカル実行が可能です。
実際の配信で使う際の注意
今回はデモとして、ボタン入力で即発射される仕組みにしました。実際の配信で何万人も見られるような場合は、視聴者介入には何らかの制限を設ける必要があります。
視聴者が好き放題ボタンを押せるようだと、画面が球で埋め尽くされてゲーム側でメモリが足りなくなります。
具体的には操作できるタイミングにクールダウンタイムを設けたり、1試合ごとにボタンを押せる視聴者をランダムで割り当てたり、どこから球を出すかを投票制にするなどといった改良により、ゲームの崩壊を防ぐことができます。
このあたりはゲームデザインが壊れないためにも必要な調整ですが、あまりにも視聴者が操作できない時間が長いと退屈ですので、バランスが必要になります。