イベント連携(JS Call Events)
Spokeからmetatellプラグインにイベントを送信し、インタラクティブな機能を実装する仕組みです。
仕組みの概要
JS Call EventはSpokeのトリガーシステムとJavaScriptのカスタムイベントを連携させます:
- Spokeでトリガーを設定 - Active/Passive Trigger
- イベントを送信 -
eventとdetailの2つのパラメータ - プラグインで受信 -
window.addEventListenerでイベントをキャッチ
Spokeでの設定
トリガータイプ
| タイプ | 説明 | 用途例 |
|---|---|---|
| Active Trigger | ユーザーがオブジェクトをクリック/インタラクト時に発火 | ボタン、アイテム収集 |
| Passive Trigger | ユーザーが範囲内に入った時に自動発火 | エリア検知、自動ドア |
| 2D Active Trigger | 2D UIから直接トリガー | UIボタン、メニュー |
パラメータ設定
Spokeのトリガー設定画面で2つのパラメータを設定:
{
"event": "find-password", // イベント名(任意の文字列)
"detail": { // イベントデータ
"index": 0,
"image_url": "https://example.com/treasure.png",
"message": "宝箱を見つけた!"
}
}
プラグインでの実装
基本的なイベントリスナー
import { useEffect } from 'react';
export const CustomOverlay = () => {
useEffect(() => {
// イベントリスナーの登録
const handleEvent = (event: CustomEvent) => {
console.log('Event received:', event.type);
console.log('Event data:', event.detail);
// イベントデータの処理
const { index, image_url, message } = event.detail;
// UIの更新など
};
// イベント名はSpokeで設定した名前と一致させる
window.addEventListener('find-password', handleEvent);
// クリーンアップ
return () => {
window.removeEventListener('find-password', handleEvent);
};
}, []);
return <div>Plugin UI</div>;
};
実装例:宝探しゲーム
公式プラグインのpassword-collection-modalを参考にした実装:
import { useState, useEffect } from 'react';
interface PasswordData {
index: number;
image_url: string;
found_at: string;
}
export const PasswordCollectionModal = () => {
const [passwords, setPasswords] = useState<PasswordData[]>([]);
const [isOpen, setIsOpen] = useState(false);
// ルームIDを取得(URLから)
const roomId = window.location.pathname.split('/').pop();
const storageKey = `passwords_${roomId}`;
// 初期化:localStorageから既存データを読み込み
useEffect(() => {
const stored = localStorage.getItem(storageKey);
if (stored) {
setPasswords(JSON.parse(stored));
}
}, [storageKey]);
// イベントハンドラー
useEffect(() => {
const handleFindPassword = (event: CustomEvent) => {
const { index, image_url } = event.detail;
// 新しいパスワードを追加
const newPassword: PasswordData = {
index,
image_url,
found_at: new Date().toISOString()
};
setPasswords(prev => {
// 重複チェック
if (prev.some(p => p.index === index)) {
return prev;
}
const updated = [...prev, newPassword];
// localStorageに保存
localStorage.setItem(storageKey, JSON.stringify(updated));
return updated;
});
// モーダルを表示
setIsOpen(true);
};
window.addEventListener('find-password', handleFindPassword);
return () => {
window.removeEventListener('find-password', handleFindPassword);
};
}, [storageKey]);
return (
<>
{isOpen && (
<div className="modal">
<h2>パスワードを見つけた!</h2>
<div className="password-grid">
{passwords.map(p => (
<img key={p.index} src={p.image_url} alt={`Password ${p.index}`} />
))}
</div>
<button onClick={() => setIsOpen(false)}>閉じる</button>
</div>
)}
</>
);
};