OBSでDiscordの口パクさせるCSSをスマートにしたかった

OBSでDiscordの口パクさせるCSSをスマートにしたかった

最近Among Usを始めて、OBSで録画しつつそこにDiscordのメンバーを表示しておきたいな〜と思ったわけです。

OBSとDiscord StreamKit Overlayを使って実現していく

DiscordのメンバーをOBSに表示し、かつカスタムCSSでアバター画像をAmong Usのスキンがわかる画像に差し替える、という技術については、以下ごまさんの記事と、その元記事にありますので参考にしていただくとして…

これがやたらめんどくさいやり方だったのと、アバターとAmong Usのアイコンを同時に表示できない方法だったので、結局自分でゴリゴリ書くことになりました。

ほかにもカスタマイズされてる記事があったりするのですが、「OBS Discord CSS」とかで検索すると検索結果の上位に出てくるのはどれも元記事のコードをベースにしただけで、根本的な設計の部分は同じです。

唯一「大人数の配信時使うDiscordStreamKitOverlayのカスタムCSS」がそれに異を唱えた設計になっています。

元の設計がダメっていう意味ではないですよ。
目的によってはその設計じゃないといけないけど、おそらくその記事を検索してきた人のうち何割かのやりたかったことは、そのめんどくさいやり方じゃなくても実現できるっていうことです。
この記事の本題とは違うので詳細は後述しておきます。

やりたいことはこれ。

追記:
アイコンが右向いてて左に動くのに違和感もりもりだったので、実際には左向きの画像に差し替えて使用しています。

  • メンバーのアバターは表示したままにする
  • アバターの横にプレイ中のスキンカラーがわかるアイコンを表示
  • 発話スタート時にアイコンが跳ねるアニメーション
  • 発話中はアイコンは静かにしておく
  • アバターのボーダーは活かし、ユーザー名もアクティブ感を出す

もともとPC操作を録画するのにはQuickTimePlayerの画面収録を使っていたんですが、これだとデュアルディスプレイじゃない私の環境ではかなりしんどく、Among Usのゲーム画面だけを映し続けるというのが難しいのと、Discordの誰がしゃべっているのか&Among Usの中でお前は何色やねん???がわかりにくいので、よしOBSを導入しよう!ということになりました。

クルー画像、Discordのアバター、ユーザー名、それらの要素を個別にカスタマイズしたい!というお話です。

長くなりますが、仕組みを詳しく知りたい方向けに「なぜそうしたのか」、というところまで説明していきたいと思います。

参考記事のとおりに実装してみて困ったこと

Among Usのアイコン表示でDiscordのアバターが消えるのが困る

上記記事のやり方は、Discord StreamKit Overlayで生成されるブラウザ表示(HTMLコード)に対して、カスタムCSSでスタイリングを上書きすることで、Discordのアバター画像を自前で用意したAmong Usの画像に置き換えるという方法になっています。

実際にどんなHTMLになっているかは、Discord StreamKit Overlayで指定されるURLを開いてブラウザの検証ツールで確認することができます。

アバター画像部分は「avatar」というclass名がついたimg要素となっています。
上記記事では.avatarセレクタのcontentプロパティ部分を上書きし、画像URLを書き換えることで対応しています。

しかしこれだと当然Discordのアバター画像は消えてしまいます。

Among Usをプレイする上では誰が何色かだけわかっていれば問題ないと思いますが、初めてご一緒する方もいるのでアバターもあわせて表示しておきたかったわけです。

Discord発話中のステータスが紐づく要素がimg要素しかない

誰が何色かを知りたいだけなら、OBS側で個別にクルー画像を読み込んで、1人ずつ隣にクルー画像を置いておけばそれで済みます。
しかし、それだけだと誰が話しているかわかりにくいので、クルー画像込みでアニメーションさせたいという要件があります。

最初に思いつくこととしては、じゃあアバター画像は残して、どこかに擬似要素入れてアイコンを表示すればいいんじゃない?というのがありますが…

発話中はimg.avatar.speakingが付与されています。
この.speakingが発話中にグリーンのボーダーを表示しているわけです。

こうなってくると次の問題が出てきます。

  • img要素には擬似要素をつけようがない
  • 親要素には発話中であるステータスがつかない

ソースコードを見ると、li.voice-stateが「data-reactid」というカスタムデータ属性を持っていて、その値にユーザーIDが入っています。
つまり本来であれば各ユーザーを示している要素の単位は、li要素のはずです。
li要素に発話中であるかどうかのステータスがついていれば、つまり.speakingが付与されていれば、その中身のimg要素もdiv要素も好きにいじれて擬似要素も発話中かどうかでスタイルを切り替えることができたのに…

だいたい.userがついてるのが中身のdiv要素っていうあたりもよく意味がわかりません。
そこが.nameならまだわかる。

Discord StreamKit OverlayのHTML/CSS設計を愚痴っていてもしょうがないので頭をひねります。

Discord発話中ステータスを紐づかせるためにセレクタ(結合子)を駆使する

Emmetなんかを使ってる人だと「^」を使うことで対象の親要素を選択できてほしいという気持ちがあると思います。

たとえば

.speaking ^ li.voice-state::before

とかできたらいいよな〜。

あとはSelectors Level 4で実現できそうな:has擬似クラスを使えれば、

li.voice-state:has(.speaking)::before

みたいなことができるかもしれません。

全ブラウザが対応してくれればな〜。

以前なんかの記事でも書いたかもしれませんが、「E ~ F」という結合子は後続の要素しか指定できないのに、「関連セレクタ」とか「兄弟セレクタ」みたいな名前で呼ばれ始めて、みんなもっと便利に使えると思っていたけど、結果として「E + F」っていう結合子(隣接セレクタ)ばっかり使ってたみたいな現状があると思います。
(どこかの記事からそうなったのか、最初の頃の仕様でそう呼ばれていたのか記憶がありませんが)

Selectors Level 4だと

an F element preceded by an E element

と記述されていて、先行という意味合いがちゃんと書かれていて(Level3でも書かれてましたが)ありがたいです。
日本語で通りやすくするなら先行よりも「後続結合子」とか、わかりやすくするために「後続セレクタ」とかって呼ばれてればいいのになって思います。
結合子とセレクタを厳密に呼び分ける変態と話す機会がほぼないので、だいたい子セレクタとか呼んじゃう。

隣接セレクタ(隣接兄弟結合子)を使う

話がそれましたが、今回はimg.speakingのほかにdiv.userspan.nameがあるため、これらを利用させてもらうことにします。

今回、アイコンを表示する位置はアバターの左側にしたいので、既に表示されていて位置を基準にしやすいspan.nameに擬似要素を入れていきます。


.user .name::before {
    position: absolute;
    top: -16px;
    left: -100px;
    content: "";
    display: block;
    width: 40px;
    height: 54px;
    background-image: url(画像のURL);
    background-size: 480px auto;
    transform: translateY(0);
}

ちなみに表示する画像についてですが、私はスプライトにしてしまいました。
Among Usのクルー画像スプライト

クルーを変更するときにはbackground-positionを修正します。


/* なつみ */
li.voice-state[data-reactid*="DiscordのユーザーID"] .user .name::before {
    background-position: -320px 0;  
}

で、あとは発話中のスタイルやアニメーションをつけるために隣接セレクタで上書きしていきます。


/* 発話中アバターアニメーション */
.avatar.speaking,
.avatar.speaking + .user {
    transform: translateX(-20px);
}

.avatar.speaking + .user .name {
    background: #41DAC6!important;
}

.avatar.speaking + .user .name::before {
    animation: speaking 0.2s ease 0s forwards;
    border-color:rgba(0,0,0,0) !important;
}

@keyframes speaking {
0% {
    transform: translateY(0);
}
50% {
    transform: translateY(-12px);
}
100% {
    transform: translateY(0);
}
}

最終的なカスタムCSSの解説

最終的なコードがこれです。


/*-- ユーザー --*/

/* colors

black: 0
blue: 40
brown: 80
cyan: 120
green: 160
lime: 200
orange: 240
pink: 280
purple: 320
red: 360
white: 400
yellow: 440

*/

/* なつみ */
li.voice-state[data-reactid*="DiscordのユーザーID"] .user .name::before {
    background-position: -320px 0;  
}

/*-- 共通スタイル--*/

li.voice-state {
    padding: 20px 0 0 60px;
}

/* アバター */
.avatar,
.avatar + .user {
    transform: translateX(0);
    transition: all 0.1s 0s ease-out;
}

.user .name {
    position: relative;
    overflow: visible;
    display: inline-block;
}

.user .name::before {
    position: absolute;
    top: -16px;
    left: -100px;
    content: "";
    display: block;
    width: 40px;
    height: 54px;
    background-image: url(画像のURL);
    background-size: 480px auto;
    transform: translateY(0);
}

/* 発話中アバターアニメーション */
.avatar.speaking,
.avatar.speaking + .user {
    transform: translateX(-20px);
}

.avatar.speaking + .user .name {
    background: #41DAC6!important;
}

.avatar.speaking + .user .name::before {
    animation: speaking 0.2s ease 0s forwards;
}

@keyframes speaking {
0% {
    transform: translateY(0);
}
50% {
    transform: translateY(-12px);
}
100% {
    transform: translateY(0);
}
}

アバターと自前のクルー画像の両方を表示させるためと、アニメーションごにょごにょしたため記述がかなり増えましたが、ユーザーを増やしたりクルーのスキンカラー変更するのは一番上のところをいじるだけなので運用はまあまあ楽です。

なんでこう書いてるの?っていうのをパーツごとに解説していきます。

ユーザーごとの設定部分

ここは先ほども紹介したとおり、background-positionを切り替えることでスプライトの表示位置が変わり、色を変更できます。
プレイ時にカラーを変更する場合にもすぐ対応できるように、数値をコメントで添えてあります。覚えられるなら書かなくてもいいやつです。


/*-- ユーザー --*/

/* colors

black: 0
blue: 40
brown: 80
cyan: 120
green: 160
lime: 200
orange: 240
pink: 280
purple: 320
red: 360
white: 400
yellow: 440

*/

/* なつみ */
li.voice-state[data-reactid*="DiscordのユーザーID"] .user .name::before {
    background-position: -320px 0;  
}

ユーザーごとに記述するのではなく、カラー基準にすることもできます。


/* black */
li.voice-state[data-reactid*="DiscordのユーザーID"] .user .name::before {
    background-position: 0 0;   
}

/* blue */
li.voice-state[data-reactid*="DiscordのユーザーID"] .user .name::before {
    background-position: -40px 0;   
}

/* brown */
li.voice-state[data-reactid*="DiscordのユーザーID"] .user .name::before {
    background-position: -80px 0;   
}

・
・
・

あらかじめ全カラーを書いておいて、ユーザーIDの方を変更します。
幸い、DiscordのユーザーIDは数字の羅列になっているため、エディタだと大抵ダブルクリックでID部分のみ選択してサクッとペーストできるので早いと思います。

  • 同じカラーを選択する人がいなければ楽
  • 同じカラーの人がいたらもう1セットコピペが必要
  • 参加していないカラーをコメントアウトしなければならない

ということになるので、日頃かなりの大所帯でプレイすることが多くほとんどの色が使用されたり、イツメンだけどみんなちょくちょく色を変更したい、といった条件であれば楽なやり方になります。

もし日頃そこまで人数が多くなくて、メンツの色もだいたい固定されているような場合には、特定のユーザーを全員記述しておいて、参加するメンバーのみコメントアウトを外すという運用でもいいかもしれません。

あと工夫するとしたら、OBSのカスタムCSS欄にはベースのスタイリングのみ記述しておいて、毎回、録画開始前に参加するメンバーのみコピペしていくというのもいいですね。ユーザー用CSSはファイルをわけて保存しておくなど。
条件次第なので人それぞれやりやすい方法でやってください。

共通スタイルの設定

次に共通スタイル部分の設定です。

まず、デフォルトのスタイルでは要素がすべて左寄せになっています。
クルー画像をアバター画像の左に置いたり、アニメーションで跳ねさせるために、topとleftに余白がほしいです。
そこで親要素にpaddingをもたせています。


/*-- 共通スタイル--*/

li.voice-state {
    padding: 20px 0 0 60px;
}

アバターとユーザーネームの設定

続いてアバター画像とユーザーネームの要素に対して、デフォルトのtransformとtransitionを設定しておきます。


/* アバター */
.avatar,
.avatar + .user {
    transform: translateX(0);
    transition: all 0.1s 0s ease-out;
}

これによって、発話中のステータスが外れた瞬間に、デフォルトの表示にカクッと戻ってしまうのを防ぎ、滑らかにアニメーションで引っ込むようにできます。

それからユーザーネームを表示しているspan要素には、下記の設定をしておきます。


.user .name {
    position: relative;
    overflow: visible;
    display: inline-block;
}

クルー画像の擬似要素を絶対位置で指定するためには、親要素となるspan.nameのpositionがstatic以外になっていなければなりません。
position:relative;を設定しておくことで、親要素の位置を基準にしてクルー画像を配置することができるようになります。

また、デフォルトだとspan要素のdisplayはinlineになっているので、これをinline-blockにしてpositionが効くようにします。

さらに、クルー画像はspan.nameのボディの外側にはみ出してしまうので、はみ出した部分も表示されるようにするため、overflowをvisibleにしておきます。

クルー画像の設定

クルー画像の表示には、span.nameの擬似要素を利用します。


.user .name::before {
    position: absolute;
    top: -16px;
    left: -100px;
    content: "";
    display: block;
    width: 40px;
    height: 54px;
    background-image: url(クルー画像のURL);
    background-size: 480px auto;
    transform: translateY(0);
}

また、すべてのクルー画像で読み込むファイルはスプライト化していて共通、サイズなども共通なので、ここでひとまとめに設定します。

ここで、なぜbackgroundプロパティのショートハンドでひとまとめにしなかったかというと、ユーザー別のカラー設定を編集しやすいよう、ファイルの一番上に置いておきたかったからです。

CSSはセレクタの優先度が同じ場合には後から出てきた記述のほうを優先して上書きするため、カラーを切り替えているbackground-positonを先に指定していても、ここでbackgroundプロパティを使ってしまうと、background関連の値がすべて上書きされてしまいます。
(ショートハンドで指定していないプロパティも、デフォルト値を指定したことになってしまう)

そこで、今回の設定で最低限必要なbackground-imageとbackground-sizeのプロパティを個別で記述しました。

あとはアニメーションが発生していない間のデフォルト位置の指定としてtransformを設定してあります。

発話中のアニメーション設定

発話中はimg.avatar.speakingが付与されますので、セレクタに.speakingを追加して発話中のスタイリングを行います。


/* 発話中アバターアニメーション */
.avatar.speaking,
.avatar.speaking + .user {
    transform: translateX(-20px);
}

.avatar.speaking + .user .name {
    background: #41DAC6!important;
}

.avatar.speaking + .user .name::before {
    animation: speaking 0.2s ease 0s forwards;
}

アバターとユーザーネームのアニメーションは、ただ位置が左に移動するだけの一方通行な変化なので、transitionを使って済ませました。
そのため発話中のセレクタに設定するのはtranslateX()のみとなります。
ついでに、個人的にはアバター画像のボーダーだけだとパッと見で見分けにくいかなと感じていたので、ユーザーネームの背景も変更しました。

ちなみに、クルー画像はユーザーネームのspan.nameの擬似要素で内包されているので、同時に左に移動することになります。

問題はクルー画像が発話スタート時に一度だけ上に飛び跳ねるという動きです。
これはアニメーションのキーフレームを使って、スタート位置→移動先の位置→最終位置の順でtranslateY()を変更していきます。
animationプロパティでforwardsを指定することで、繰り返すことなく一方通行のアニメーションを行い、最終位置で留まるという動きになります。


@keyframes speaking {
0% {
    transform: translateY(0);
}
50% {
    transform: translateY(-12px);
}
100% {
    transform: translateY(0);
}
}

なおforwardsではなくinfiniteにすれば無限に繰り返し動き続けます。喋ってる間ずっとぴょこぴょこさせたい場合に使えます。

元記事のコードの不可解だった点

やり始める前にコードを読んでいてちょっとよくわからないなあと思っていて、いざやり終わってみて、そういうことだったのかなあと気づいたので触れておきます。
元記事で紹介されているコードの一番上の行、


li.voice-state:not([data-reactid*="discordのuser_id"]) { display:none; }

これがなんのためにあるのかがついぞわかりませんでした。

このコードがやっていることは、

li.voice-state(各ユーザーごとの要素)のうち、「data-reactid」というカスタムデータ属性の値の文字列に指定したDiscordのユーザーID文字列が含まれているもの以外はdisplay:none;にする

という指示です。

もうちょっとシンプルに言うと、

DiscordのユーザーIDがこれじゃない人のリストは全部非表示

ってことです。

これをもっと人間が読んで意味がわかりやすい書き方のコードにすると、


li.voice-state {
    display: none;
}

li.voice-state[data-reactid*="discordのuser_id"] {
    display: block;
}

ですね。

なんでそんなことする必要があるのかわからず…

そもそもリストにはDiscord StreamKit Overlayで生成する際に指定したチャンネルに入っているメンバーしか表示されません。
使い方として思いつくのは、たとえば誰彼構わずとりあえずチャンネルに入っていて、プレイヤーや配信者のみミュートを外す運用のチャンネルの場合ですかね…?
そうすると


li.voice-state[data-reactid*="表示したいユーザー1"],
li.voice-state[data-reactid*="表示したいユーザー2"],
li.voice-state[data-reactid*="表示したいユーザー3"],
li.voice-state[data-reactid*="表示したいユーザー4"] {
    display: none;
}

みたいな感じで、ホワイトリスト形式で表示するユーザーを全員指定していかないといけないので、めちゃくちゃ大変そう…
しかも元のコードだと:not()を使っているので、上記のような併記ではなく


li.voice-state:not([data-reactid*="表示したいユーザー1"]):not([data-reactid*="表示したいユーザー2"]):not([data-reactid*="表示したいユーザー3"]):not([data-reactid*="表示したいユーザー4"]):not([data-reactid*="表示したいユーザー5"]) { display:none; }

って感じになるはずなんで、編集がめんどくさそうと思いました。

とりあえず私がAmong Usをプレイするときは、プレイヤーしか入室しないし、録画に含めたいDiscordのリストもプレイヤーのみなので、この処理はまったく必要ありません。

元記事のCSS設計でないと実現できないこと

で、ちゃんと記事を読むと

ちょっと手間ですがコラボなんかの際は上記手順を人数分実施します。

とあります。

これ要するに、OBS側でユーザー個別のソースを作って、ユーザーの立ち絵を1人ずつわけて表示しようとしてるっぽいですね。
だからCSSも特定の1人以外は非表示としてるわけですね。

Among Usだと死んだ人用のチャットを用意して移動する運用のDiscordサーバもあって、プレイ中にユーザーが抜けたり入ったりするのでユーザーの位置が変わって鬱陶しいと思う人もいると思います。

下記のような条件のときにはその方がやりやすいというか実現できないのは確かです。

  • 立ち絵のサイズやpositionが異なる
  • 配信中に特定のユーザーだけリアルタイムでスタイルを変えたい
  • チャットを出入りしてもユーザーを非表示にしたくない

要するにユーザーの表示がリストという意味合いでなくなる場合には個別にソースを作って配置したほうが良いです。

なんでユーザーごとに作業するって話になってるのか最後までわからなくて混乱していましたが、謎がとけました。すっきり。

私はとにかくその時誰がしゃべってるかとスキンカラーだけわかれば良いので、今回の目的のようにAmong Usの色分け程度の使い道であれば、ひとつのソースにひとつのカスタムCSSで全員分設定できるほうが楽ですね。
OBSのソースもこれだけで、作ったCSSを1回コピペするだけで済みます。

ちなみにユーザーの表示順序を固定したい場合、別に個別ソースにしなくてもできます。
たとえば配信者自身は常に一番上に表示しておきたいならul自体をflexで組んで、liのorderをデフォルト1にして、自分のユーザーIDのliだけ0に指定すればOK。
同じ要領で、そのときの参加メンバー全員の並び順を指定することも可能です。

さらに楽ちんに!CSS変数を使ってリファクタリング

目的は達成できましたが、さらに使いやすくするためにCSS変数を利用していきます。
が、記事が長すぎるので後編に分けました。
よろしければこちらもご覧ください。

この記事に関する補足とFAQ

自分がプレイしたときの他人視点で、ああこういうふうに見えてたんだ〜とか、裏でこんなツッコミ入れてたのか〜っていうのがわかって面白いので、みんなどんどん録画して共有してほしいですね!

こちらの記事全編を通して、「このセレクタ余分じゃない?もっとダイエットできる」みたいなコードリファクタのご意見があるかとは思いますが、どこにいるなんの要素に対して何をしているかがわかりやすいようにあえて書いているので無視してください。

OBSに導入するまでの動画解説

VTuberのArikaさんがこの記事をもとにしたコードの導入手順を動画で解説されています。

私のブログはもともとCSSを書ける技術者向けの記事になっているのでハードルが高いという人もいると思いますが、Arikaさんの動画はCSS自体を書いたことがない人でもできるようにわかりやすく丁寧に説明されていて、素材も配布されているのでぜひ見に行ってみてください!

コードの転載・二次利用について

この記事を公開してから数ヶ月の間で、嬉しいことに、記事を参考にしてくださっているAmongUs実況配信者さんがいて、何人かTwitterでコードの使用許諾について問い合わせをいただきました。

AmongUsの実況動画というテーマの性質上、IT系の人以外からの問い合わせが来るようになって、当たり前ですが人によって技術ブログに掲載されているコードの扱いについての感覚がマチマチだな〜と思いました。

下記にDMで回答した内容プラスアルファで私の基準について説明しておきます。

自分のAmongUs実況動画でも使用していいですか?
もちろんです。URL教えてくれると喜びます。
AmongUs以外の実況動画で使用してもいいですか?
もちろんです。URL教えてくれると喜びます。
実況動画で使用した場合に概要欄に記事リンクを掲載する必要がありますか?
なくても問題ありません。あったら嬉しいやつです。
コードをカスタマイズして使用してもいいですか?
もちろんです。
それを友達に教えてもいいですか?
もちろんです。
カスタマイズしたコードを自分のブログで公開してもいいですか?
ブログや動画でコードそのものを引用(転載)したりカスタマイズしたコードを公開する場合には、参考にした元記事はだいたいリンク張ってたりします。
当たり前ですが「これ私が1から作りましたー!」はダメです。
コードにも著作権てありますよね?
あります。
ただ、オープンソースでもないプロダクトのソースを丸パクリしたらまあ怒られますが、IT系の文化的にブログなどで公開されているTipsのコピペはまず問題ありません。本人の備忘録や他の人に参考にしてもらうために意図的にシェアしてるものと考えて大丈夫です。
ただし著者や権利者がコードの使用や転載・二次利用等を禁止している場合には従ってください。
無断で使っているサイト・動画を見つけました!
私に通報しなくて大丈夫です。

無断でやっていい範囲のラインについて

無断でやっていい範囲のラインはこんな感じかなーという、あくまでも個人的な感覚は

  1. 自身のAmongUs実況プレイ動画への導入
  2. 個人的利用の範囲での再配布(友人に教える等)
  3. ブログ・動画での著者なりのカスタマイズコード紹介

までです。

下記は事前に報告してほしい、というラインです。

  1. オリジナルコードそのものをまるごと転載
  2. メディアのコンテンツとしてとりあげる
  3. ジェネレーターその他プロダクトへの組み込み
  4. 個人的利用の範疇を超えた再配布
  5. 有償配布、またはキャンペーンなどで対象ユーザーにアカウント登録やフォロー等何らかの代償となる行為をさせたうえでの配布

本来の目的である、自身の実況動画への導入とは異なる使用方法で、当人にとって利があると思える状態、つまりコンテンツのネタ稼ぎ、視聴数稼ぎ、その他集客などになる場合は、あらかじめ一報入れて了承を得ておいたほうが、読者・視聴者からの通報もなく、トラブルにならないと思います。
(「紹介してあげて宣伝になるからいいでしょ!」はビジネスでは嫌われるやつなので)

かんたんにまとめると、本人自身が汗水たらしカスタマイズした内容などを技術情報として掲載したり人に教えるのはOK、もともと他人が作ったものを売り物にするのはNG、みたいな感じで判断してもらえればと思います。

技術的な質問について

CSS自体の基本的な知識がなくカスタマイズできない、新色追加しようとしたけどうまくいかない、といった問い合わせをいただくことがあります。
質問を送っていただくこと自体はまったく問題ありませんが、私の方が常に回答できるわけではないので、すべてにお返事をするとは限りません。ご了承ください。

最終更新日:2021年6月23日