しらたきchさんで使用して頂いている店主ルーレットについて、どうやって動作させているのか、どんな技術を使っているのか気になるという意見を多く頂いたので、今回簡単にどういう手順で機能を実現しているのかを解説したいと思います。
▼使っていただいた配信です
なお、ルーレットの完成までの手順を全部書いていくと膨大な執筆量になることが予想されるので続くかは分かりませんが、需要があれば検討します。
※注意点
まず前提として、私はコードを書くことを専門にしているというわけではなく、今回作成したツールに関しても理屈さえ分かれば動作部分は誰でも作れるような簡単なことしかしていませんし、本物のプロであればもっとまともな手順で実装するところを面倒臭がって力技で解決している部分があります。専門の学校で教わるような厳格なプログラミングのチュートリアルではないので、「ふ~ん そんな仕組みか~」くらいの感覚で読んでください。
OBSでの表示方法について
このツールは、OBSのブラウザ機能を使ってHTMLを読み込んで使います。OBSのブラウザ機能には「対話」という機能が備わっており、これにより対話型(ユーザーがアクションを起こした際にそれに応じて進行する)のプログラムをOBS上で動作させることが出来ます。
HTMLでWebページを作成するときと同じ手順でツールを作るので、デバッグにはGoogle Chromeなど一般的なブラウザを使用することも出来ますが、表示位置などの調整にOBSも使用するため、前もってOBS上で表示できるようにしておくと良いです。
なお、当然ですがHTMLを使って出来そうなことは、このブラウザ機能を使って読み込むことで大概のことは出来ます。例ですが非同期通信(Ajax)についてはこちらの記事も参考にしてください。
余談ですが、このブラウザ機能を一般的なWebページを表示する目的で使用することは、確実に安全なページであることを確認できている場合を除いてなるべく避けたほうが良いです。通常のブラウザではセキュリティ上の対策として自動的にブロックされている機能も、OBSは素通ししてしまう場合があるので、悪質なサイトにアクセスしてしまった場合にローカルのファイルが盗み取られるなどの被害を受ける可能性があります。
HTML構造について
今回のツールではcanvas要素を使用した描画は一切使用しておらず、全ての演出をHTMLのdivなどの要素を移動したり、表示非表示の切り替えをしたり、画像を変更するなどをして実現しています。このため、後から必要になる要素も初めから非表示(styleがdisplay:none)の状態で仕込んでおく必要があります。今回使用したツールでは以下のようになっています。
※あえて全体を表示していますが長いので流し見してください。更に下で今回のポイントに絞って紹介しています。
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>店主ハウスだ!</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script type="text/javascript" src="control.js"></script>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="showing-area">
<p class="testmode">テストモード中です!</p>
<div id="main-art">
<img id="door-base" src="img/base.png" alt="base">
<img id="door-tenshu" src="img/red.png" alt="tenshu">
<img id="door-tenshu2" src="img/green_attack.gif" alt="tenshu">
<div id="door-back"></div>
<img id="door-slide" src="img/door_left.png" alt="door">
<p id="door-text1"></p>
<p id="door-text2"></p>
</div>
<div id="chime">
<img id="chime-base" src="img/chime.png" alt="chime-base">
<button onClick="pushChime();"></button>
</div>
<h4 id="next">次へ進む▶</h4>
<video id="cutin"></video>
<div id="rou-pre">
<p id="pre-message">赤店主対策アイテムを持っているようだ。<br>なにか使いますか?</p>
<p id="vase-count"></p>
<p id="hole-count"></p>
<h4 id="use-hole">おとしあなのタネを使う</h4>
<h4 id="hole-explain">説明</h4>
<h4 id="use-vase">やりすごしの壺を使う</h4>
<h4 id="vase-explain">説明</h4>
<h4 id="nouse">使わない</h4>
</div>
<div id="selecter">仕掛ける▶</div>
<div id="rou-art">
<p id="rou-mes">ルーレット<br>スタート!</p>
<div id="rou-control">
<p id="control-message"></p>
<p id="control-message2"></p>
<h4 id="up">▲</h4>
<h4 id="down">▼</h4>
<h4 id="start-stop"></h4>
</div>
</div>
</div>
<div id="roulette-area">
<div id="throw"></div>
<div id="rou-body">
<div id="hole"></div>
<div id="rou-tenshu"></div>
<div id="rou-target"></div>
</div>
<div id="rou-selected"></div>
</div>
<div id="event-area">
<img id="choice-img" alt="2choice" src="img/2choice.png">
<p id="get-text"></p>
<div id="get-item"></div>
<video id="choice"></video>
<div id="event-tenshu"></div>
<h4 id="right">右</h4>
<h4 id="left">左</h4>
</div>
<div id="control-area">
<div>
<label><input type="checkbox" name="test-mode"> テストモードの有効化</label><br>
<select name="test-list">
<option value="x1">基本テスト1</option>
<option value="x2">基本テスト2</option>
<option value="r1">赤店主1</option>
<option value="r2">赤店主2</option>
<option value="r3">赤店主3</option>
<option value="b1">青店主1</option>
<option value="g1">緑店主1</option>
<option value="g2">緑店主2</option>
<option value="y1">黄店主1</option>
<option value="y2">黄店主2</option>
<option value="n1">無人1</option>
<option value="n2">無人2</option>
</select><button onClick="testup();">↑</button><button onClick="testdown();">↓</button>
<p>音量<input type="range" id="volume" min="0" max="1" value="0.1" step="0.01" onChange="volchange();"></p>
<button id="resetstate">初期化する</button>
<p id="times"></p>
</div>
</div>
</body>
</html>
無駄に要素数が多いですが、シーンごとに要素のセットごと非表示にして別のシーンの要素セットを表示するという仕組みで作っていたためこのようになってしまいました。
「扉が横に開いて普通に赤店主が出てくる」というシーンに必要な部分だけを抜き出したものが以下です。
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>店主ハウスだ!</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script type="text/javascript" src="control.js"></script>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="showing-area">
<div id="main-art">
<img id="door-base" src="img/base.png" alt="base">
<img id="door-tenshu" src="img/red.png" alt="tenshu">
<div id="door-back"></div>
<img id="door-slide" src="img/door_left.png" alt="door">
<p id="door-text1"></p>
</div>
</div>
</body>
</html>
使っている画像は「左扉」「左扉以外の全体」「店主」の3つです。これらを上手いこと組み合わせることで扉が閉まった状態と開いた状態を表現しています。
CSSによる表示位置の調整とアニメーションの準備
当然ですがHTMLだけの状態だと左上から順番に画像が表示されるだけで終わってしまうので、CSSを使って位置を指定しておく必要があります。
今回の場合ではOBS上での表示位置だけ考えれば良いので、1920×1080以外で表示されることはないという前提で作ってしまって問題ないので、基本は全て「position:absolute」として「width, height, top, left, z-index」の値を設定することによって位置調整をしています。
今回の描画に使用するCSSだけを抜粋すると以下のようになります。
/* CSS Document */
body {
width: 1920px;
height: 1080px;
font-family: "りいポップ角";
}
#main-art {
position: absolute;
left: 320px;
top: 220px;
overflow: hidden;
width: 1275px;
height: 711px;
z-index: 100;
}
#main-art img {
position: absolute;
}
#door-base {
top: 0;
left: 0;
width: 1275px;
height: 711px;
z-index: 105;
}
#door-tenshu {
z-index: 101;
width: 600px;
height: 600px;
left: 105px;
top: 100px;
}
#door-slide {
top: 0;
left: 0px;
width: 1275px;
height: 711px;
z-index: 104;
}
#door-back {
top: 0;
left: 0px;
width: 1275px;
height: 711px;
background-color: #27221C;
z-index: 10;
}
#door-text1 {
position: absolute;
font-size: 120px;
line-height: 80px;
padding-top: 30px;
text-align: center;
opacity: 0;
z-index: 150;
text-shadow:
4px 4px 5px #000000,
-4px 4px 5px #000000,
4px -4px 5px #000000,
-4px -4px 5px #000000,
4px 0px 5px #000000,
0px 4px 5px #000000,
-4px 0px 5px #000000,
0px -4px 5px #000000;
}
.sub {
font-size: 70px;
}
@keyframes door1 {
0% {left:0;}
100% {left:360px;}
}
@keyframes fadein {
0% {opacity: 0;}
100% {opacity: 1;}
}
スタイルシートでは初期状態のCSSを記述しておき、進行状況に応じてJavaScript側でCSSの変更をして移動など演出の操作を行います。
一番下の@keyframesではアニメーションの仕込みをしています。0%からスタートして100%になるまで対象をどのように変化させるのかを記述しています。
この例だとdoor1はleftが0から360pxまで変化するように指定しているので、左から右に移動するアニメーションになり、fadeinはopacityが0から1まで変化するように指定しているので、徐々に浮き上がってくるようなアニメーションになります。
このままではアニメーションはされませんが、後でJavaScriptで要素に対してdoor1やfadeinなどを割り当てることにより、指定の要素をアニメーションさせることができるようになります。
JavaScriptによるアニメーション制御
今回はインターホンを押して赤店主の通常の登場演出が選択された直後を想定し、そこからの動作プログラムを追ってみます。
1. テキストの準備
まずは店主登場後に出てくるテキストの準備をします。初めの時点ではopacityが0に設定してあるので、存在はするけれど全く見えないという状態で置いてあることになります。
実際のルーレットでは、赤店主以外が選ばれた場合はこちらで店主の画像の変更も行っておきます。インターホンを押した際に演出の抽選が行われ、演出ごとに演出コードを割り当てておき、演出コード別に動作を分岐させています。
$('#door-text1').html('紅しょうが<br><span class="sub">BENI SHOUGA</span>');
$('#door-text1').css('color','#ff5555');
$('#door-text1').css('left','330px');
$('#door-text1').css('top','-120px');
2. 扉を開ける
扉を開けるアニメーションは、左扉の要素(#door-slide)に対して先程準備したdoor1を設定することで簡単に実現できます。記述としては以下のようになります。
// var Audio = new Audio(); これはプログラムの一番最初の方に記述しておきます
Audio.src = "./sound/door1.mp3";
Audio.play();
$('#door-slide').css("animation","door1 1.3s 1 linear forwards");
これで#door-slideにanimation:door1 1.3s 1 linear forwards;のcssが追加されます。
この記述は、「1.3秒かけてdoor1を1回だけ行ってアニメーション終了の状態を維持してください」という意味になります。ちなみにlinearは0%から100%まで一定の速度で行うという意味です。
このアニメーションを行うのと同時にAudioを使って扉が開く時のガラガラという音も鳴らしています。
3. 店主が登場して文字が出てくる
扉が開き終わってから店主が「いらっしゃいませ」と発言し、店主の名前が出てくる演出を追加します。以下のコードを扉を開けるコードの直後に記述します。
setTimeout(function(){
$('#door-text1').css("animation","fadein .5s 1 linear forwards");
Audio.src = "./voice/welcome1.mp3";
Audio.play();
}, 1300);
2で設定した扉が開くアニメーションが終わるまでに1.3秒かかるため、扉が開き終わるまで待ってから店主のセリフと文字の表示をする必要があります。これを実現するためにsetTimeoutで1300ms=1.3s後に指定のプログラムを動作させるようにディレイをかけます。
setTimeoutで起動する関数は、別で作ってからその名前を指定しても大丈夫ですが、今回は1回しか使わないので無名関数を使用しています。
これで扉が開いて名前の文字が出てくる演出まで作ることが出来ます。
最後に
全て設定できたものがこちらになります。(サイズが1920×1080に固定されている都合でブラウザで直接動かすとはみ出るので録画したものになります)
このようにタイマーとCSS変更を上手く使うだけで、難しいことをしなくてもアニメーションを作成することが出来ます。
ただし、今回の例ではサイズが固定されているという前提で成り立っているので、このようにOBS上でアニメーションを動かしたい場合は、事前にOBSでどのくらいのサイズにするのか決めてからプログラムを作り始めるようにしてください。
なにか質問等あればTwitter(@rit_nd)またはritの生放送中(YouTube, niconico)のコメント等で頂けたらお答えします。