# レイアウトを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からツイートしたり表示する配信を弄れたりします。難しそう。