メインコンテンツまでスキップ

イベント連携(JS Call Events)

Spokeからmetatellプラグインにイベントを送信し、インタラクティブな機能を実装する仕組みです。

仕組みの概要

JS Call EventはSpokeのトリガーシステムとJavaScriptのカスタムイベントを連携させます:

  1. Spokeでトリガーを設定 - Active/Passive Trigger
  2. イベントを送信 - eventdetailの2つのパラメータ
  3. プラグインで受信 - window.addEventListenerでイベントをキャッチ

Spokeでの設定

トリガータイプ

タイプ説明用途例
Active Triggerユーザーがオブジェクトをクリック/インタラクト時に発火ボタン、アイテム収集
Passive Triggerユーザーが範囲内に入った時に自動発火エリア検知、自動ドア
2D Active Trigger2D 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>
)}
</>
);
};