この記事はSTYLY Advent Calendar 2025の8日目の記事です。
「Lexiphere」シリーズ記事
概要編
Webページ構築編 (本稿)
STYLYシーン実装編
Unity TextMesh Proのベストプラクティス
要約
さて、一週間以内にリアルタイム執筆される文章をARで表示するシステムを作らなくちゃいけなくなったぞ。
今だとバックエンドとか何を使うのがいいのか、Claudeに相談しつつFirebase(Firebase Hosting + Firestore)に決定。
WebページもほぼClaudeCodeにお任せで出来上がったぞ。万歳。
前提
LiveNovelingのパフォーマンスは15分程度、1回のみ。
リアルタイムで入力した文字がARで表示される。
来客者はそれぞれ自分のスマホでそれを見ることができる。
まずクライアント側はSTYLYシーンで作ることにした。
WebARでもいけそうな気はするが、ちょっとどの程度うまく出来るのかが読めないので、時間がない状況では自分が最も把握している技術を選ぶ。
STYLYシーンはC#スクリプトは持ち込めずPlaymakerで処理を書く必要がある。
PlaymakerからはHTTPS通信が可能なので、定期的に特定のURLへリクエストを飛ばせば何とかなりそう。
構成図
これがあらましの構成。
妥協した点として、リアルタイム執筆されたものをディスプレイで見て、一言一句間違えずに入力するという無駄な工程を挟んでいる。
これはそもそものLiveNovelingが、縦書き原稿用紙を模したエディタで入力しているものをそのまま見せる、という内容故である。
Webページ側も同等の表示を実現できるなら執筆者に直接入力してもらい、入力画面自体をDisplayアウトしつつARのための投稿も賄えてベストだった。
ただ今回はかなりタイトなスケジュールだったのと、縦書き表示周りは何かとトラブる予感があったのでやむを得ずこの形になった。
その結果、下記の画面を睨みながら間違えずに写経するという難度の高いタイピングゲームみたいなことをする羽目になった。
うん、次にやる機会までには改善しよう。
![]() |
|---|
| リアルタイムで執筆された小説が、このようにディスプレイに映される |
試算
Firebaseを使うにあたり、ざっと試算してみる。
10文字前後ごとに送信するとして、約5秒に1回程度の送信と仮定。
15分間のパフォーマンスで180回の送信となる。
送信者は1人だけ。
イベントの観客は最大でも100名限定。
(実際には全員がスマホでARを見続けるとは考えづらいが)
それぞれのクライアントは3秒ごとにポーリングする。
15分間のパフォーマンスだと、15分÷3秒で300回のAPIアクセス。
クライアント側だけで約30,000回のアクセスとなる。
Firebaseの無料枠(Sparkプラン)では下記のようになっている。
ドキュメントの読み取り 50,000/日
ドキュメントの書き込み 20,000/日
ふむ、十分余裕をもって無料枠内に収まりそうだ。
Firestoreの構成
これまであまりFirestoreを扱ったことがなかったのだが、スキーマ定義なしにいきなり何となく書き込んだりできるイージーさはNoSQLならではという感じ。
小規模でシンプルな構成ならこれでいい。
また下記のようなURLで直接REST APIにアクセスできるのも手軽で良い。
https://firestore.googleapis.com/v1/projects/YOUR_PROJECT_ID/databases/(default)/documents/COLLECTION_ID/DOCUMENT_ID
今回は、表示など制御用のメタデータと投稿本文のメッセージ群で構成している。
メタデータ
メタデータは、STYLYシーン再生時にまず読み取って、現在のメッセージインデックスの位置や表示関連の設定を行う。
このデータにREST APIでGETすると、下記のようなJSONが返される。
{
"name": "projects/xxxxxxx/databases/(default)/documents/texts/metadata",
"fields": {
"replayStartIndex": {
"integerValue": "4"
},
"offsetZ": {
"doubleValue": 6.00001
},
"sentencePoolSize": {
"integerValue": "3"
},
"offsetY": {
"doubleValue": 0.10001
},
"currentIndex": {
"integerValue": "138"
},
"charPoolSize": {
"integerValue": "5"
},
"lastUpdated": {
"timestampValue": "2025-10-25T09:29:54.898Z"
},
"requestInterval": {
"doubleValue": 5.70001
},
"sentenceWait": {
"doubleValue": 5.00001
},
"scale": {
"doubleValue": 0.6501
},
"offsetX": {
"doubleValue": 1e-05
}
},
"createTime": "2025-10-20T18:41:32.679351Z",
"updateTime": "2025-11-09T06:58:45.538216Z"
}
注意点として、firestore上はNumber型になっているフィールドは、値次第で項目名が変わる。
これがちょっと厄介で、入力値を1.0としても整数と判断されてintegerValueで返されてしまう。
JSONのパースを楽にしたいので下記のように、実際の影響がないレベルに小さな小数を付け加えることでdoubleValueになるよう工夫した。
"offsetZ": {
"doubleValue": 6.00001
},
メッセージデータ
1送信ごとの本文のデータ。
中身はシンプルで、index値と本文の文字列を持っている。
クライアント側は現在のindex+1のデータを一定間隔で取得しに来る。
{
"name": "projects/xxxxxxx/databases/(default)/documents/texts/messages/items/109",
"fields": {
"index": {
"integerValue": "109"
},
"timestamp": {
"timestampValue": "2025-10-25T09:21:51.724Z"
},
"text": {
"stringValue": "それはちょっとだけ、バイオリンの音に似ている。"
}
},
"createTime": "2025-10-25T09:21:51.824665Z",
"updateTime": "2025-10-25T09:21:51.824665Z"
}
投稿フォーム
あまりWebの開発に明るくないので、ほぼ完全にClaudeCodeにお任せで出来上がった。
CSSなども含めてモダンな書き方を知らないし、コードを読まずに進めてみた。
上がってきたものを触って、ちょこちょこと修正指示を出して、というやり取りを1〜2時間も繰り返せばシンプルなものは仕上がるという体験は非常に素晴らしい。
![]() |
|---|
| 入力画面は自分だけが見るので動けば十分。デザインは適当 |
次回
基本的な仕組みはほんの一晩で仕上がった。
うぅむ、やはりWebとAI駆動開発は相性が良い。
次回はSTYLYシーン実装編となる。
AIのサポートを活かしづらいPlaymakerでの実装テクニックを中心に解説する予定。
乞うご期待。




