# レイアウトをNodeCG化した話 [名古屋RTAオフ](https://twitter.com/NagoyaRTA)で「ゲームセンターCXマラソン」というイベントを開催しています。 課長が挑戦したゲームのRTA走者を各シーズンごとに募集し、マラソン形式で披露してもらおうというイベントです。 このイベントで使っているレイアウトは、シーズン1の開催直前にOBSを駆使して作っていました。 シーズン1終了後、シーズン2,3と続いていくにあたって、各種情報の切り替えを自動化したいと思い、レイアウトを[NodeCG](https://www.nodecg.dev/)で作りなおしました。[^1] レイアウトの**デザインは、同じものを使っていく**予定なので、1回作ってしまえば、情報を入れ替えるだけで準備が完了する。ということが、大きなメリットです。 プログラムよく分からない勢[^2]でもなんとかNodeCGを扱うことが出来るんだよー[^3]ということで、その時の記録を残しておこうと思います。 正しい知識のある人間ではないので、信頼度40%くらいで見てください。 ## 従来のレイアウト ![](https://i.imgur.com/BWwK8aF.jpg) OBSの機能を活用して、それっぽいレイアウトを作っていました。 具体的には、右上のロゴ以外テキストソースを使用しています。 フォントはフリーの「[Nuあんこもち](http://kokagem.sakura.ne.jp/font/mochi/)」というものを使いました。 このフォントがとても便利で、「╭」や「┣」を入力すると、枠線になってくれます。 各種情報の周りにある線は、この罫線を利用して作りました。 この手法は、作成に時間はかかりませんが、本番中にゲーム・走者情報を**手作業で**書き換える必要があります。 また、Twitchの配信タイトル・カテゴリー(ゲームタイトル)も手作業で書き換えなければなりませんでした。 シーズン1の本番中に、スタッフが行う主な作業は - レイアウトのテキスト変更 - Twitchの情報変更 - タイマーの操作 - 出走、完走ツイート - 各走者の配信を配信に映す (ブラウザソースのURL・トリミングの変更) こちらの5つでした。 準備は楽ですが、本番中は忙しいです。 ## NodeCGを使おう NodeCGを使って、レイアウトをいい感じにしていきます。 とはいえ、0からプログラムを書いていったわけではなく、既存のbundleを流用しました。 - [nodecg-speedcontrol](https://github.com/speedcontrol/nodecg-speedcontrol) RTAイベント向けの汎用bandleです。通称"Speedcontrol"。 これをベースに作成されたレイアウトが、数々のイベントで使用されています。 ダッシュボードが日本語に対応しているので、使いやすい。 配信に載せるレイアウト(graphics)は無いので、自分で作りましょう。 - [speedcontrol-simpletext](https://github.com/speedcontrol/speedcontrol-simpletext) Speedcontrol用に作られた、簡易レイアウトです。 文字のみですが、走者情報やタイマーをhtmlで表示する事ができます。 この2つを使えば、簡単にhoraroやOengusのスケジュールから、各種情報をインポートして、画面に表示することができます。 [configを書いて](https://github.com/speedcontrol/nodecg-speedcontrol/blob/master/READMES/Configuration.md)、[TwitchAPIの設定](https://qiita.com/pasta04/items/2ff86692d20891b65905)をしてあげれば、Twitchの配信タイトル・カテゴリの変更も自動でしてくれます。便利! simpletextは各情報ごとにhtmlが作られているので、それぞれ元あったOBSのテキストソースと入れ替えてあげれば、NodeCG化は完了です。 ![](https://i.imgur.com/MUMoa1L.png) いい感じに配置してあげましょう。 初期設定だと黒文字なので、cssいじらないと見えない可能性があります。 これで、本番中にスタッフが行う作業が - NodeCGの操作 - 出走・完走ツイート - 各走者の配信を配信に映す (ブラウザソースのURL・トリミングの変更) になりました。[^4] - レイアウトのテキスト変更 - Twitchの情報変更 - タイマーの操作 この3つの作業が、NodeCGのダッシュボードをクリックするだけで済むようになったので、とても楽になりましたね。 ## もうちょっと頑張ってみよう 切り替え作業は楽になりましたが、各情報ごとにhtmlがバラバラになっています。 OBSにブラウザソースをたくさん読み込まないといけないので、ちょっと面倒ですね。 というわけで、1つのhtmlにまとめましょう。 ここからは少しプログラムの知識が必要になります。 ### 仕組みを見てみよう まずは、speedcontrol-simpletextで各情報がどのように表示されているのか見てみます。 どのファイルでもいいですが、ここではゲームタイトルを表示する`game-title.html`を例にします。 ```htmlembedded= <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <!-- CSS --> <link href="css/common.css" rel="stylesheet"/> <!-- JavaScript --> <script src="../bower_components/jquery/dist/jquery.min.js"></script> <script src="js/run-info.js"></script> </head> <body> <span id="gameTitle"></span> </body> </html> ``` javascriptを使って、htmlに文字を表示しています。 ```htmlembedded= <span id="gameTitle"></span> ``` という部分を覚えておいてください。 他のhtmlにも、そのファイル名に対応したidが振られています。 jqueryは、便利なものがあるんだなぁ程度の認識で、とりあえずは良いと思います。文字を表示してくれているのは、このjqueryです。 次に`run-info.js`をのぞいてみましょう。 //の後に書いているコメントの部分は書き換えています。元は[こちら](https://github.com/speedcontrol/speedcontrol-simpletext/blob/master/graphics/js/run-info.js) ```javascript= 'use strict'; $(() => { // どのバンドルから情報を持ってくるかを指定。 // nodecg-speedcontrolからひっぱってくるよ!というのを変数に入れている var speedcontrolBundle = 'nodecg-speedcontrol'; // どの要素に情報を表示するかを指定。表示するのにjqueryを利用しています、 // このidに表示するよ!というのを変数に入れている。 // idは'#id名'で指定 var gameTitle = $('#gameTitle'); //id="gameTitle" var gameCategory = $('#gameCategory'); // id="game-category" var gameSystem = $('#gameSystem'); // id="game-system" var gameEstimate = $('#gameEstimate'); // id="game-estimate" var player = $('#player'); // id="player" var twitch = $('#twitch'); // id="twitch" // nodecg-speedcontrolのrunDataActiveRunの情報が書き換わったら、プログラムを動かすよ。 // 動かすのはupdateSceneFieldsというこの下にあるプログラムだよ。 var runDataActiveRun = nodecg.Replicant('runDataActiveRun', speedcontrolBundle); runDataActiveRun.on('change', (newVal, oldVal) => { if (newVal) //変数newValに更新された情報を入れて、このプログラムを動かすよ updateSceneFields(newVal); }); // 各情報をhtmlに出力するプログラムだよ。 // 変数runDataには変数newValが入るよ。 function updateSceneFields(runData) { //id.html(情報);という形を書くとjqueryが指定したidの部分に情報を表示してくれるよ gameTitle.html(runData.game); gameCategory.html(runData.category); gameSystem.html(runData.system); gameEstimate.html(runData.estimate); // #playerと#twitchがあるかを確認 if (player.length || twitch.length) { // 開いているURLの後ろに#数字があったら、変数playerNumberに入れる // たとえば、http://localhost:9090/bundles/speedcontrol-simpletext/graphics/player.html#2 なら2が入る // 何もなかったら1が入る var playerNumber = parseInt(window.location.hash.replace('#', '')) || 1; // 配列は1つ目の要素を0,2つ目の要素を1で呼び出すので、上の数字から1を引く var team = runData.teams[playerNumber-1]; // チームに複数のプレイヤーがいる場合、現在はカンマで囲まれたリストで出力されます。 if (team) { player.html(team.players.map((player) => player.name).join(', ')); twitch.html(team.players.map((player) => player.social.twitch).join(', ')); } } } }); ``` まずは、NodeCG特有のReplicantについて説明します。 ([ここ](https://zenn.dev/cma2819/articles/nodecg-implements-replicant)を見てもらえれば分かると思いますが一応ね) コードの中では ```javascript= nodecg.Replicant('runDataActiveRun', speedcontrolBundle); ``` でReplicantを読み込んでいます。 ここでは`nodecg-speedcontrol`というバンドルの`runDataActiveRun`というReplicantを読み込んでいます。 (変数`speedcontrolBundle`の中身は`nodecg-speedcontrol`) Replicantってなんだよ!とお思いでしょう、自分もここで躓きました。 `nodecg/db/replicants/nodecg-speedcontrol`にSpeedcontrolが出力しているReplicantが保存されています。(.repファイル) NodeCGで扱う情報は、ほぼすべてここに保存されています。 `runDataActiverun.rep`を見てみましょう。ファイルそのままではなく、整形してあります。 ```json= nodecg/db/replicants/nodecg-speedcontrol/runDataActiverun.rep { "teams": [ { "id": "875c07f0-2fe9-4bbe-9ae3-2782b93080f2", "players": [ { "name": "そらんちゃん", "id": "e79f49f5-a0b4-4ee0-8f02-d298cf297638", "teamID": "875c07f0-2fe9-4bbe-9ae3-2782b93080f2", "social": {}, "externalID": "e79f49f5-a0b4-4ee0-8f02-d298cf297638" } ] } ], "customData": {}, "id": "4535deb0-d2aa-4896-b084-c38f3ea24700", "externalID": "0", "game": "ロックマン2", "system": "FC", "category": "Any% (Difficult)", "gameTwitch": "Mega Man 2", "scheduledS": 1612076400, "scheduled": "2021-01-31T16:00:00+09:00", "estimateS": 3000, "estimate": "00:50:00", "setupTime": "00:10:00", "setupTimeS": 600 } ``` 要は[jsonファイル](https://developer.mozilla.org/ja/docs/Learn/JavaScript/Objects/JSON)です。 ```javascript= var runDataActiveRun = nodecg.Replicant('runDataActiveRun', speedcontrolBundle); runDataActiveRun.on('change', (newVal, oldVal) => { if (newVal) //変数newValに更新された情報を入れて、このプログラムを動かすよ updateSceneFields(newVal); }); ``` `run-info.js`のこの部分は、`runDataActiveRun.rep`の情報が更新されるたびに、変数`newVal`の中へ新しい情報を入れ、`updateSceneFields`を動かしています。 ```javascript= // 各情報をhtmlに出力するプログラムだよ。 // 変数runDataには変数newValが入るよ。 function updateSceneFields(runData) { ``` この`runData`には`newVal`が入ります。 つまり、`runDataActiveRun.rep`の中身がそのまま入ります。 ```javascript= //id.html(情報);という形を書くと、htmlのjqueryが指定したidの部分に情報を表示してくれるよ gameTitle.html(runData.game); ``` `runData.game`は、`runDataActiveRun.rep`の`game`の情報を指定しています。 ```json= "game": "ロックマン2", ``` この部分ですね。 `ロックマン2`という文字列が`game-title.html`の ```htmlembedded= <span id="gameTitle"></span> ``` この部分に表示されます。 そのほかの情報も同じように表示できます。 タイマーも仕組みは違いますが、同じような方法だと思ってください。 走者情報は、ひとつ深いところに入っているので、指定の仕方が変わります。 元々書かれていたやり方がよくわからなかったので、書き換えて使いました。 元々の`run-info.js`が ```javascript= if (team) { player.html(team.players.map((player) => player.name).join(', ')); twitch.html(team.players.map((player) => player.social.twitch).join(', ')); } ``` こうなっていたのを ```javascript= player.html(team.players[0].name); // player.html twitch.html(team.players[0].social.twitch); // twitch.html ``` こうしました。 元のプログラムだと、`player.html`のurlの末尾に#数字を追加すれば、複数人走者がいる場合にも対応できますが、変更後の場合は、プログラムに書き足す必要があります。 ```javascript= player.html(team.players[0].name); // player.html twitch.html(team.players[0].social.twitch); // twitch.html player.html(team.players[1].name); // player.html twitch.html(team.players[1].social.twitch); // twitch.html ``` 元あった`run-info.js`を1人用、2人用に`run-info2.js`を作成して運用していました。 ### 改造して自分のレイアウトを作ろう 完成系はこんな感じです。 (twitchタグ使ってtwitter IDを表示していますが、気にしないでください。) ![](https://i.imgur.com/ytL8rKc.png) `game-title.html`を改造していきます。 まずは、1つのhtmlファイルに色々な情報を表示させます。 各種情報は、`run-info.js`が表示してくれますが、タイマーは`timer.js`が表示します。 まずはこれを読み込みましょう。headタグの中に書きます。 ```htmlembedded= <head> (省略) <script src="js/timer.js"></script> (省略) </head> ``` 次に、bodyタグの中に情報を表示する場所を作ります。 これは、spanタグを並べるだけなので、簡単ですね。 ```htmlembedded= <body> <span id="gameTitle"></span> <span id="gameCategory"></span> <span id="timer"></span> <span id="gameEstimate"></span> <span id="player"></span> <span id="twitch"></span> </body> ``` ![](https://i.imgur.com/6scYOJo.png) しかし、この書き方だと情報が横に並んでしまいます。 全体をdivタグを囲って、pタグをつかったり`<br>`を使ったりすれば改行できます。 今回は、`<br>`を使いました。 ![](https://i.imgur.com/61eL1yq.png) ファミコンっぽい見た目は、[NES.css](https://nostalgic-css.github.io/NES.css/)で表現しています。 こちらもheadタグの中に書いて読み込みます。 ```htmlembedded= <head> (省略) <link href="https://unpkg.com/nes.css/css/nes.css" rel="stylesheet" /> (省略) </head> ``` 次にGAME・CATEGORY・infoを囲います。 [NES.css](https://nostalgic-css.github.io/NES.css/)のContainersのサンプル通りに、それぞれdivタグで挟みます。 後々、見た目をいじりやすいようにidやらclassやらも設定しておきます。 ```htmlembedded= <body> <div class="nes-container with-title is-rounded is-dark" id="Game"> <span class="title">GAME</span> <span id="gameTitle"></span> </div> <div class="nes-container with-title is-rounded is-dark" id="Category"> <span class="title">CATEGORY</span> <span id="gameCategory"></span> </div> <div class="nes-container with-title is-rounded is-dark" id="info"> <div class="title">info</div> <span id="timer"></span> <span id="gameEstimate"></span> <span id="player"></span> <span id="twitch"></span> </div> </body> ``` ![](https://i.imgur.com/IYq2H9E.png) 大分それっぽくなったと思います。 infoの中には、タイマー・EST・走者名・Twitter IDの4つの情報が表示されているので、分かりやすく見出しを付けます。 ```htmlembedded= <div class="nes-container with-title is-rounded is-dark" id="info"> <div class="title">info</div> <span id="midasi">TIME</span> <span id="timer"></span> <span id="gameEstimate"></span> <span id="midasi">PLAYER</span> <span id="player"></span> <span id="twitch"></span> </div> ``` ![](https://i.imgur.com/wZHvGaw.png) このままでは、ただ横に並んでいるだけなので、表示する場所を変えてみましょう。 ついでに、フォントもドット調の[PixelMplus](https://fontfree.me/373)に変更します。 ![](https://i.imgur.com/xRAzqkl.png) `common.css`をいじりました。ほぼ完成です。 内容は~~面倒くさい~~とても長いので、一部分だけ載せます。 ```css= #Game { /* 表示する場所の指定 */ position: absolute; top: 10px; left: 10px; /* 大きさの指定 */ width: 600px; height: 90px; } /* id:twitchのフォントサイズと行の高さを変更 */ #info #twitch { font-size: 40px; line-height: 50%; } /* id:twitchの前に@を差し込む */ #twitch::before {content: " \@";} ``` info欄は`line-height`を使って、縦の位置調整をしています。 画像は、まだ調整しきってない状態です。 最後に、htmlの右上にイベントロゴを追加して、twitter IDの前にツイッターロゴを差し込みます。 ツイッターロゴはNES.cssにあるものを使います。 ```htmlembedded= <img src="img/logo.png" id="logo"></img> (省略) <i class="nes-icon twitter is-midium"> </i><span id="twitch"></span> ``` ![](https://i.imgur.com/7wDyJu1.png) 完成! ### 解説情報をどう載せよう 正規手段だと[Cmaさん](https://twitter.com/cma2819)が、[speedcontrol-additions](https://github.com/cma2819/speedcontrol-additions)というものを作っているので、こちらを使うのがいいと思います。 自分は使い方がよくわからなかったので、Speedcontrolに元々あったけど使っていなかった、`gameRegion`と`gameSystem`に解説の名前とTwitter IDを設定して何とかしました。 また、解説がいないときに、前のゲームの解説者情報がレイアウトに残る。ということがあったので、解説者がいないときは、`gameRegion`と`gameSystem`を表示している部分を非表示にするプログラムを書きました。 ```javascript= //解説がいるかどうかを判断して、解説者情報の部分(div id="comment")の表示非表示を切り替える if (!runData.region) { document.getElementById("comment").style.display="none"; } else { document.getElementById("comment").style.display="block"; } ``` とてもごり押し ## おわりに 無理やり動かしているので、[NodeCGのドキュメント](https://www.nodecg.dev)や[RTA in Japanのレイアウト](https://github.com/RTAinJapan/rtainjapan-layouts)などのコードを参考にしたほうがいいです。 Speedcontrolを使っているものだと、[OMEのレイアウト](https://github.com/cma2819/ome-speedrun-layout)や[第2回不思議のダンジョンシリーズRTAフェスのレイアウト](https://github.com/yagamuu/mysrtafes2021-layouts)を参考にするのがいいんじゃないかなぁと思います。 あくまで、正しい知識を身につけるのが面倒な人がNodeCGを使うとこうなるよという文章です。 [NodeCGのDiscordサーバー](https://discord.com/invite/GJ4r8a8)に日本語チャンネルもあるので、分からないことがあったら優しい人が教えてくれると思います。 何書けばいいんだと悩みながら書いてきましたが、どうだったでしょうか? 動画を見ながら書いていたら1日が終わりました。 意見・感想・文句などがありましたら[ご連絡ください。](https://twitter.com/hukujunn/) 好評だったら、情報の正確性を上げてRTAGamersに投稿するかもしれません。 --- [^1]: 最初から使わなかったのは、NodeCGを使うのにかかるコスト >>> 削減できる作業量 と思って、使わなくてもいいかなぁと思っていた時期があったからです。 プログラムを学んでこなかったので作れない。コミュ障なので、人に頼むのが本当につらいという感じです。 [^2]: 授業で軽くC#かなにかを触ったのと、ここ1年で[JavaScript Primer](https://jsprimer.net)・[ProgateのHTML&CSS初級編](https://prog-8.com/lessons/html/study/1)あたりを読んだくらいです。 [N予備校](https://www.nnn.ed.nico/)のプログラミング入門も進めていますが、レイアウトを作った当時は、役に立つほど進んでいませんでした。 [^3]:扱うことができる(理解しているとか自分で作ることができるというわけではない) [^4]: NodeCGからツイートしたり表示する配信を弄れたりします。難しそう。