はじめに
NodeCGは動かせるようになったけど、どうやって改造したらいいんだ…
という方向けに、RTA in Japanのレイアウトを各自のイベント用に改造するための方法を記載します。
この記事のゴール
- 各自のイベント用のレイアウトを作成する。
用意するもの
用意するもの(必須)
- RTA in JapanのNodeCGレイアウトを動かしてみるで環境構築したNodeCG
用意するもの(オプション)
Microsoft製のエディタ。(以降VSCodeと記載)
Windows版、Mac版があり、比較的軽くてオススメです。
エディタの設定をプロジェクトに含めてgitにコミットしておくことで、複数人で設定を共有することができます。
エディタで開く
VSCodeを起動し、rtainjapan-layoutを開きます。

フォルダを開いたらターミナルを表示します。
以降のコマンドは、VSCode上で実行します。
VSCode上でターミナルを開くとこんな感じのウインドウになります。

Gitフォルダを削除
そのままだとRTA in Japanのgitととの差分が表示され続けて面倒なので削除してしまいましょう。
プロジェクトの一番上にある.gitというフォルダを削除します。
(必要な人のみ)レイアウト名を変える
レイアウトのrtainjapanの名称を自分のイベント名に変えてみましょう。
以降はmyrta-layoutsに変えた前提で記載していきます。
変更箇所は以下の通りです。
- ファイル名、フォルダ名
cfg/rtainjapan-layouts.json
rtainjapan-layouts
- ファイルの中身
rtainjapan-layouts/package.json
scripts/postinstall.ts
package.json
レイアウトの名前を変えたらもう一度インストールする必要があります。
最上位のpackage.jsonがあるフォルダで以下を実行します。
yarn
ビルドと起動
最上位のpackage.jsonがあるフォルダで以下を実行します。
yarn dev
ターミナルをもう一つ立ち上げ、以下を実行します。
yarn start
配信レイアウト編集
RTA in Japanの配信レイアウトはReactと呼ばれるJavaScriptのフレームワークで構成されています。初学者がいきなりReactを理解するのは非常に困難であるため、この記事では、以下を目標とします。
- 背景を変える
- ゲームの表示位置を変える
- ゲーム情報、走者情報の位置を変える
新規レイアウトの作成
配信レイアウトのソースは以下に格納されています。
myrta-layouts/src/graphics/views
サンプルとして、hd1.tsxを複製し、新しいレイアウトを作成してみましょう。
同フォルダにnewLayout.tsxとしてコピーします。
レイアウト内のpackage.jsonにgraphicsの配列があるので、以下を追記します。
{
"file": "newLayout.html",
"width": 1920,
"height": 1080
}
package.jsonに新しいレイアウトを追加したら、再ビルドします。
既にビルドを起動していた場合は、Ctrl+Cなどで停止してから実行してください。
なお、package.jsonを弄らない限り、以降このビルドコマンドを打つ必要はありません。
yarn dev
yarn start
追加したレイアウトが正しく認識されていれば、以下にアクセスした際に、追加したレイアウトが表示されているはずです。
http://localhost:9090/dashboard/#graphics

位置調整
本題のレイアウトいじりです。
まずは、コピーしたレイアウトとHTMLがきちんと結びつくようにします。
Reactのおまじないみたいなものだと思って良いです。
ReactDom.render(<App />, document.getElementById('newLayout'));
上記を保存すると自動的に再ビルドが行われます。
以下にアクセスして、コピー元のHD1と同じレイアウトが表示されればOKです。
http://localhost:9090/bundles/myrta-layouts/graphics/newLayout.html
Reactはコンポーネント呼ばれる部品で構成されています。
コンポーネントの説明は省きますが、<table>とか<div>のようなタグを拡張して自分で機能を付加できるすごいタグだと思えば良いです。(有識者に怒られる解説)
RTA in Japanのレイアウトは以下のコンポーネントで構成されています。
- StyledContainer
- 背景画像と、ゲーム表示、カメラ表示エリアを担当するCSS
- xxxContainer系はそれぞれのCSS
- RunnerContainer
- 走者情報のCSS
- CommentatorContainer
- 解説者のCSS
- GameContainer
- ゲーム情報のCSS
- TimerContainer
- タイマーのCSS
- RtaijRunner
- 走者情報
- RtaijGame
- ゲーム情報
- StyledRuler
- ゲーム情報とタイマーの境界線
- RtaijTimer
- タイマー情報
- RtaijOverlay
- 左上のロゴ
- ツイート表示
- 画面下の背景をちょっと暗くするやつ
背景画像の差し替え
myrta-layouts/src/graphics/images/background.pngを置き換えるか、以下を好きな画像のパスにします。
import background from '../images/background.png';
ゲーム、カメラ表示領域の変更
RTA in Japanのレイアウトでは、StyledContainerのCSS、clip-pathによって、背景を切り抜くことでゲーム画面表示領域を表現しています。
const StyledContainer = styled(Container)`
background-image: url(${background});
clip-path: polygon(
0px 0px,
15px 0px,
15px 741px,
15px 1065px,
上記のような座標を手打ちするのは至難の業であるため、まずはツールでざっくりレイアウトを作りましょう。
clip path generatorを使います。
まずは背景画像のファイルを、画面にドラッグ&ドロップします。

解像度が高くて表示域が画面からはみ出してしまいましたが問題ありません。
ブラウザの表示を縮小しましょう。
左上のカクカクっとしたアイコン、Polygonal lasso Selectionを選択し、画面に表示する領域を囲んでいきます。クイックスやダンシングアイをやってる気分になれますね。良い子は後者でググってはいけない。
なお、線を引き終える時はダブルクリックです。
以下の例は、カメラ無し、右上にゲーム、という表示領域を作っている様子です。
CSSのパラメータが画面左下のテキストボックスに書かれているのがわかると思いますので、後は微調整をすればOKです。
上記手順で生成したCSSをレイアウトに適用し、保存してみましょう。
clip-path: polygon(
以下略
保存した後、ブラウザでレイアウトを表示すると、見事(?)適当に引いた線が反映されました。

ゲーム情報やタイマー情報の位置を変える
対応するxxxxContainer のCSSを変えるだけでOKです。
なお、GamerContainerとTimerContainerは共通CSSとしてinfoStyleが定義されています。
チョチョイとCSSをいじってみましょう。
const infoStyle = css`
position: absolute;
z-index: 10;
top: 200px;
height: 210px;
display: grid;
`;
const GameContainer = styled.div`
${infoStyle};
left: 0px;
width: 765px;
`;
const TimerContainer = styled.div`
${infoStyle};
left: 0px;
top: 300px;
width: 372px;
`;
・・・はい!というわけで表示の移動ができました。各々好みの位置に変えましょう。
コンポーネント作り
あんまり細かいことには触れないので、頑張って読み解いてください。以下メモ書き。
- コンポーネントは
myrta-layouts/src/graphics/componentsに格納されている -
rtaij-game.tsxなどは、NodeCGが持っているnodecg変数から自身に必要な値を取り出し、表示用の共通コンポーネントbase-info.tsxに渡している - 走者情報は
nameplateのコンポーネントを使っている - nodecgに変化があった時のイベント登録は
components直下の.tsxたちが全部請け負っている - nodecgが持ってる変数の情報は
myrta-layouts/src/replicants/index.tsに定義されているReplicantNameと、それに対応する定義。
雑に実装しちゃう
細けぇことはいいんだよ、俺は俺のタイマー表示したいんだよ的な人向けの参考資料。
最低限の要素だけで構成しています。
import '../styles/common.css';
import React from 'react';
import ReactDom from 'react-dom';
import styled from 'styled-components';
import {Container} from '../components/lib/styled';
import background from '../images/background.png';
import {CurrentRun, ReplicantName as R, Timer} from '../../replicants';
const StyledContainer = styled(Container)`
background-image: url(${background});
`;
const currentRunRep = nodecg.Replicant<CurrentRun>(R.CurrentRun);
const timerRep = nodecg.Replicant<Timer>(R.Timer);
/** このコンポーネントへの引数みたいなもの。今回は何も無い */
interface Props {}
/** このコンポーネント内で持つ一時変数みたいなもの */
interface State {
/** 走者情報 */
runner: string;
/** タイマー */
timer: string;
}
class App extends React.Component<Props, State> {
// コンポーネント生成時に一回だけ実行するやつ
constructor(props:Props){
super(props);
// タイマーが変わったら実行する関数をセット
timerRep.on('change', this.timerChangeHandler);
// 走者情報が変わったら実行する関数をセット
currentRunRep.on('change', this.currentRunChangeHandler);
// 初期値セット
this.state = {
runner: '',
timer: ''
}
}
/** 上でセットした、nodecgのタイマーが変わった時に動くやつ。stateにタイマーをセット */
private readonly timerChangeHandler = (newVal: Timer) => {
this.setState({timer: newVal.formatted});
};
/** 上でセットした、nodecgの走者情報が変わった時に動くやつ。stateに走者の名前をセット */
private readonly currentRunChangeHandler = (newVal: CurrentRun) => {
this.setState({runner: newVal.runners[0].name});
};
render(){
return(
<StyledContainer>
<div style={{color:'white'}}>
<h1>走者:{this.state.runner}</h1>
</div>
<div style={{color:'white'}}>
<h1>タイム:{this.state.timer}</h1>
</div>
</StyledContainer>
)}
}
ReactDom.render(<App />, document.getElementById('newLayout'));
上記のソースは、こんな感じで表示されます。
簡易的ではあるものの、ここまでできたら後は純粋なHTMLとCSSの知識だけでも作れますね。
(オマケ)コンポーネントをちゃんと作る人へ
ReactであればStorybookを使ってコンポーネント作りすることを強く推奨します。
理由は以下の通り。
- コンポーネント単体で表示確認できる
- ページ全体を作れていなくてもパーツの確認ができる
- ダッシュボードの実装と並行してパーツを作れる
- パーツ単体なのでデザイナーと意思疎通が取れやすい
- 表示パターンを網羅的に確認できる
- 例えば機種ごとにアイコンを表示したくなったとして、ダッシュボードからいちいち確認するのは面倒だし、そもそも表示確認を漏らす可能性が高い。
React Storybook入門:コンポーネントカタログがさくさく作れちゃうかもしれないオシャレサンドボックス環境を参考にしてみてください。
(オマケ)レイアウトごとにhtmlファイルをソースとして用意しなくてよい理由
レイアウトごとにhtmlを用意するんじゃないの?と疑問に思った方、鋭い指摘です。
RTA in JapanのNodeCGでは、ベースとなるhtmlを元として、Webpackの中で複製しています。つまりレイアウトを増やす時はReactのJSXを増やすだけで良いのです。
const files = globby.sync(`./src/${name}/views/*.tsx`);
for (const file of files) {
entry[path.basename(file, '.tsx')] = file;
}
〜〜〜〜〜中略〜〜〜〜〜〜
...Object.keys(entry).map(
(entryName) =>
new HtmlPlugin({
filename: `${entryName}.html`,
chunks: [entryName],
title: entryName,
template: `webpack/${name}.html`,
typekitId: process.env.TYPEKIT_ID,
}),
),

