Rails: レトロゲーム風の隠しコマンドをStimulusで実装する(翻訳)
私が構築してきたSaaPアプリやマーケティングサイトのほとんどには、ちょっとしたイースターエッグ風のお遊びを仕込んであります。こうしたささやかなUIのお遊びやジョークの種明かしをしたことはありませんが、気付いた人がニヤリとするような仕掛けです。
そうしたお遊びの1つがKonamiコード、すなわちレトロゲームの隠しコマンド(↑ ↑ ↓ ↓ ← → ← → B A)です。これは1980年代のKonamiのファミコンゲームが由来です。1987年の魂斗羅というシューティングゲームに、自機を30機追加できるこの隠しコマンドが仕込まれたことで伝説化しました。それ以来、文化的イースターエッグとして何百とも知れないほど多くのゲームやWebサイトで見かけるようになりました。
今回紹介したいのは、StimulusのValues APIを使って、他のStimulusコントローラからリッスン可能なカスタムイベントをディスパッチする方法です。
私のやりたいことを以下の動画で示します。
この記事のコードは、例によって以下のGitHubリポジトリにも置いてあります。
私の場合、HTMLから書くのが好きです。
<div data-controller="sequence" data-action="keydown@window->sequence#detect">
Type: "ArrowUp", "ArrowUp", "ArrowDown", "ArrowDown", "ArrowLeft", "ArrowRight", "ArrowLeft", "ArrowRight", "b", "a"
</div>
このHTMLでは、"sequence"という名前のStimulusコントローラをセットアップすることで、ウィンドウにキーダウンイベントのリスナーをアタッチしています。このページ内でキーが押されると、Stimulusのsequenceコントローラにdetectメソッドが呼び出されます。
Stimulusコントローラのコードは実にシンプルです。
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static values = {
sequence: {
type: Array,
default: ["ArrowUp", "ArrowUp", "ArrowDown", "ArrowDown", "ArrowLeft", "ArrowRight", "ArrowLeft", "ArrowRight", "b", "a"]
}
}
initialize() {
this.#resetSequence()
}
detect({ key }) {
this.#enteredKeys.push(key)
if (this.#isSequenceTooLong) this.#enteredKeys.shift()
if (this.#isSequenceMatched) {
console.log("Sequence matched! 🎉")
this.#resetSequence()
}
}
// privateメソッド
#enteredKeys = []
#resetSequence() {
this.#enteredKeys = []
}
get #isSequenceTooLong() {
return this.#enteredKeys.length > this.sequenceValue.length
}
get #isSequenceMatched() {
return this.#enteredKeys.join(",") === this.sequenceValue.join(",")
}
}
このsequenceコントローラは、入力したキーを配列で保持します。キーが押されるたびに、push()でその配列に追加されます。配列が長くなりすぎたら、キー入力の古い方からshift()で削除します。これによって、直近のキー入力の「スライディングウィンドウ」が作成されます。
#をプレフィックスしたprivateメソッド(本ブログ読者ならとっくにお馴染みですよね)を使ってコードをクリーンに仕上げています。#isSequenceTooLongゲッターメソッドで、入力したキーシーケンスの長さが超過していないかどうかをチェックしています。#isSequenceMatchedゲッターメソッドは、配列を結合して文字列にすることで、入力したキーを隠しコマンドのキーシーケンスと比較しています。
このコードをテストしてKonamiの隠しコマンドを入力してみると、ブラウザコンソールにSequence matched!🎉というメッセージがめでたく表示されます。その調子!🎉
それではコードを拡張して、コンソール出力をカスタムイベントのディスパッチに差し替えましょう。以下が変更部分です。
if (this.#isSequenceMatched) {
- console.log("Sequence matched! 🎉")
+ this.dispatch("matched", { detail: { sequence: this.sequenceValue } })
this.#resetSequence()
}
dispatchメソッドは、Stimulusでカスタムイベントをディスパッチするのに重宝します。このメソッドのeventName引数には、コントローラ名にコロン:区切りでプレフィックスしたもの(sequence:matched)を渡します。
この部分を省略なしで書くと以下のようになります。
const event = new CustomEvent("sequence:matched", {
bubbles: true,
detail: { sequence: this.sequenceValue }
})
window.dispatchEvent(event)
カスタムイベントは、JavaScriptのさまざまな部分同士で通信するための強力な手段です。このパターンはTurboでも多用されていて、turbo:loadやturbo:frame-renderといったイベントを使ってフレームワークの操作にフックをかけられます。Turbo Streamの背後で行われている詳しいしくみについては、以下の別記事を参照してください。
この「イースターエッグ」にちょっとした遊び心を加えたいので、隠しコマンドが一致したら紙吹雪(confetti)エフェクトを発動するようにしてみましょう。
最初はいつものように、HTMLでカスタムイベントをリッスンするようにします。
<div data-controller="party" data-action="sequence:matched@window->party#confetti"></div>
シンプルですね。ここでセットアップしたpartyというStimulusコントローラは、そのウィンドウのsequence:matchedイベントをリッスンします。イベントが発生すると、confettiメソッドを呼び出します。
続いてpartyコントローラも作成しましょう。
import { Controller } from "@hotwired/stimulus"
import JSConfetti from "js-confetti"
export default class extends Controller {
connect() {
this.confettiParty = new JSConfetti()
}
confetti() {
this.confettiParty.addConfetti({
confettiColors: ["#FF6600", "#C8102E", "#f1f5f9", "#003DA5"],
confettiRadius: 6,
confettiNumber: 500,
})
}
}
このコントローラでは、隠しコマンドが一致したときにjs-confettiというnpmライブラリでカラフルな紙吹雪を花開かせます。なお、カラーテーマにはネザーランド(オランダ)の国旗🇳🇱の色彩をあしらいました(オレンジ: #FF6600 、赤: #C8102E 、白っぽい色: #f1f5f9 、青: #003DA5 )。
js-confettiをプロジェクトに追加するのもお忘れなく。
bin/importmap pin js-confetti
# または
npm install js-confetti
このアプローチは再利用性も拡張性も優れています。同じイベントにさまざまなリスナーを追加して別のアクションをトリガーすることも、Konamiコード以外の隠しコマンドにカスタマイズするのも自由です。
以上でおしまいです!🎉皆さんもKonamiコマンドでアプリにお楽しみを追加できるようになりましたね。🎮🎊
関連記事
Rails: Turbo Streamsをスムーズに遷移するturbo-transitionライブラリを公開しました(翻訳)
概要
元サイトの許諾を得て翻訳・公開いたします。
日本語タイトルは内容に即したものにしました。