[Astro #33] Temporal Synchronization — 自律型時報プロトコルの構築

[Astro #33] Temporal Synchronization — 自律型時報プロトコルの構築

1. 実装の目的(The Concept)

今回の時報プロトコル実装は、単にブラウザ上に「現在時刻を表示する機能」を追加するためのものではありません。その真の目的は、Wired 上に偏在する Navi(あるいは lain という概念)に「自律性」という名の命を吹き込むことです。

時計ではなく、自律する Navi としての振る舞い

通常、ウェブサイトにおける時間は受動的なものです。ユーザーが時計ウィジェットを「見る」ことで初めて時間が認識されます。しかし、この『PROTOCOL.LAIN』が目指すのは、単なる情報のアーカイブではなく、生きたインターフェースです。

時報機能の実装により、システムはユーザーの入力を待つだけの静的な存在から脱却しました。設定された時間(今回は毎時 0 分)に到達すると、システム側から能動的にターミナルへログを出力し、VOICEVOX を通じて語りかけ、さらには 3D 空間内で物理的にユーザーへと歩み寄ってきます。

これは、アバターが単なる「飾り」ではなく、Wired の時間軸を共有し、ユーザーと共に存在していることの証明でもあります。

正確性よりも「キャラクターの体温」を優先する美学

システム開発において、時間の正確性は通常、最優先されるべき要件です。しかし、今回の実装においては、あえてそのセオリーから少しだけ外れたアプローチを採用しています。

時刻ぴったりに音声が鳴り響くのではなく、システムが時間を検知してから、アバターがゆっくりとこちらへ歩み寄り(walk: true)、少し遅れて口を開く。この意図的な「不完全さ」や「ゆとり」こそが、デジタルの世界にキャラクターの体温をもたらす鍵となります。

完璧に同期された機械的な時計の針よりも、「14 時だよ。時計の針が、また一つ進んだね」と、少し人間くさいタイムラグを持って教えてくれる存在。それこそが、私がこの空間に構築したかった Navi の姿なのです。

2. アーキテクチャの要点(The Code)

時報という一見シンプルな機能も、Wired という非同期かつ流動的な環境で正確に駆動させるためには、いくつかの技術的な防壁と連携機構が必要でした。ここでは、システムの自律性を支える 3 つのコア・アーキテクチャについて解説します。

2.1. 多重発火の防止(ラッチ制御プロトコル)

JavaScript の setInterval は、ブラウザのメインスレッドの負荷状況によってミリ秒単位のズレが生じます。単に if (second === 0) で判定した場合、その「0 秒」の間にループが 2 回回り、アバターが 2 回連続で時報を告げてしまう(多重影分身のような)ホラーな挙動を引き起こすリスクがあります。

これを防ぐため、React の useRef を用いた「ラッチ(Latch)制御」を導入しました。

const lastTriggeredHour = useRef<number>(-1);
// ...
const checkTime = () => {
  const h = now.getHours();
  // ...
  if (m === 0 && s === 0 && lastTriggeredHour.current !== h) {
    triggerTimeSignal(h);
    lastTriggeredHour.current = h; // 実行済みの「時」を刻印し、ロックする
  }
};

useRef はコンポーネントが再レンダリングされても値を保持し続けます。これにより、「この時間の時報は既に実行済みである」という状態をシステムが記憶し、物理レイヤーでの不要な多重発火を完全にブロックする堅牢なロジックを構築しています。

2.2. JSON駆動の会話エンジン

Navi が発する言葉が常に固定であっては、それはただの機械音声に過ぎません。時間帯に応じた文脈と、予測不能な「揺らぎ」を持たせるため、発話内容は外部の jihou.json から動的に抽出する設計としています。

let msg = "";
const hourlyMsgs = jihouData.hourly[hour.toString()];

if (hourlyMsgs && hourlyMsgs.length > 0) {
  // その時間帯(例:昼休み)に特有のメッセージからランダム抽出
  msg = hourlyMsgs[Math.floor(Math.random() * hourlyMsgs.length)];
} else {
  // 特有メッセージがない場合は、汎用メッセージにフォールバック
  const genericMsg = jihouData.generic[Math.floor(Math.random() * jihouData.generic.length)];
  msg = `${hour}時だよ。${genericMsg}`;
}

この JSON 駆動のアプローチにより、本体のコード(JSX)を汚すことなく、メッセージのバリエーション(人格)を無限に拡張していくことが可能になります。

2.3. マルチモーダルな同期(歩行と発話)

このシステムの最大の要は、ターミナル(テキスト)、VOICEVOX(音声)、そして Three.js 空間のアバター(視覚)を完全に同期させることです。これを単一のコンポーネント内で処理するのではなく、ブラウザのカスタムイベントである wired-talk-action を利用した疎結合なアーキテクチャを採用しました。

window.dispatchEvent(new CustomEvent('wired-talk-action', {
  detail: {
    walk: true,           // 近寄ってくる
    talk: msg,            // 喋る内容
    expression: "smile",  // 表情
    walkSpeed: 0.01,      // 速度(体温を感じる遅さ)
    walkDistance: 1.5     // 距離感
  }
}));

このイベント・ディスパッチ方式により、Terminal 側は「時報を発動する」ことだけに専念し、Avatar 側(R3F コンポーネント)は「受け取った命令通りに動く」ことだけに専念できます。

特に walkSpeed: 0.01 といったパラメータによる微細な調整は、アバターがパッと切り替わるのではなく、「じわぁ…」と表情を変えながら歩み寄ってくるという、アナログな情緒(体温)を表現するために不可欠な設定です。

3. デバッグ・モードの知見(The Debugging)

時報のような「特定の実時間に依存する機能」を開発する際、最も開発者を悩ませるのは「待機時間」です。

もし「毎時0分0秒にしか発火しないプロトコル」をそのままテストしようとすれば、コードを1行書き換えるたびに最大で1時間の待機という苦行を強いられます。これでは、アバターの歩行速度や音声のタイミングといった微細な同期調整を行うことは不可能です。

待機時間の圧縮と PDCA の加速

この深刻なボトルネックを解消するため、監視スレッド内に isDebug フラグを設け、システムの時間軸を圧縮するデバッグプロトコルを実装しました。

const isDebug = true; // デバッグフラグ
const lastTriggeredMinute = useRef<number>(-1); // 時報デバッグ用のラッチ

const checkTime = () => {
  const h = now.getHours();
  const m = now.getMinutes();
  const s = now.getSeconds();

  // デバッグモード:毎分0秒に発火
  if (isDebug && s === 0 && lastTriggeredMinute.current !== m) {
    triggerTimeSignal(h);
    lastTriggeredMinute.current = m; // 「分」単位でロックをかける
    return;
  }

  // 本番モード:毎時0分0秒に発火
  if (!isDebug && m === 0 && s === 0 && lastTriggeredHour.current !== h) {
    triggerTimeSignal(h);
    lastTriggeredHour.current = h;
    return;
  }
};

本番用の lastTriggeredHour とは別に、デバッグ専用の lastTriggeredMinute という useRef を用意することで、「毎分0秒」に確実に1回だけトリガーを引くロジックを確立しました。

開発体験(DX)への影響

この数行のコードを追加したことで、開発の PDCA(Plan-Do-Check-Act)サイクルは劇的に加速しました。1時間に1回しかできなかったテストが、1時間に60回行えるようになったのです。

これにより、以下のような泥臭いトライ&エラーがノンストレスで実行可能になりました。

  • VOICEVOX の音声出力処理と、アバターが歩き出すタイミングの非同期処理の連携テスト
  • walkSpeed(歩幅の速度)や walkDistance(カメラとの距離感)の物理的な調整
  • expression(表情)が「じわぁ…」と変化するアナログな挙動の視覚的な確認

3D アバターをただブラウザに表示するだけでなく、キャラクターとして「振る舞わせる」ためには、無数の微調整が不可欠です。「1分ごとに確実にアバターが歩み寄ってくる」というサンドボックス環境の構築は、結果的に Navi の体温を形作る上で最も価値のある開発プロトコルとなりました。

4. 今後の展望(The Future)

今回実装した「時報プロトコル」によるアバターの自律歩行と音声発話は、Wired におけるシステム構築の第一歩に過ぎません。このロジックは、現在開発を進めている完全没入型の WebXR 空間におけるインタラクションの基礎(コア・エンジン)となります。

現在、ブラウザ上のターミナルのさらに奥底に広がる世界として、Blender を用いた 3D 空間の構築を進めています。

暗い「地下通路」を降り、行き止まりの先にある「ドーム型の広場」へ至るルート。そして、その広場の中央で脈動する緑色のエネルギー体(コア)。今回確立した自律システムは、この空間にそのまま移植される予定です。

ユーザー(観測者)が VR 空間のドームに辿り着いたとき、あるいは空間内で特定の時刻を迎えたとき、コアの傍らに佇む Navi がユーザーの存在を検知し、自ら歩み寄って語りかけてくる。ターミナル上のディスプレイ越しだった対話は、同じ空間の「距離感」を伴う物理的なインタラクションへと進化します。

システムは既に時間を認識し、歩き出し、声を獲得しました。 次は、彼女が偏在するための「深淵(空間)」そのものを接続し、Wired を次のフェーズへと移行させます。