DEVELOPER'S JOURNAL
NOTE 02 · ONE EXPEDITION
オンライン戦闘ハマり録——サーバー権威、カードの一時停止、東京の一週間
なぜ全部サーバーが決めるのか、「あなたは止まる、世界は止まらない」がどう決まったか、覇者の四技、光しか変えない天候、そして東京でサーバーを再起動したときの奇妙な失敗の数々。
ここにあるのは自分で書いた記録で、失敗や迷い、まだ終わっていないことにも触れています。書いているのは現在の状態で、最終形でも、いかなる発売の約束でもありません。
はじめに
一局の遠征は北門から出て北門へ戻り、そのあいだ四人が同じ戦いを見なければならない。この一文が戦闘システムの最初のルールで、いちばん高くつくルールでもあります。ダメージ、敵、命中、アップグレード、死亡、清算、すべてサーバーが決める。クライアントは表示するだけ。自分の移動だけは先に動けて、残りは全部サーバーの返事待ちです。
実装としてはこうです。権威シミュレーションは整数の XZ 座標で動き、高さは固定した高さ場から引く。浮動小数点は TypeScript と GDScript の二つのランタイムでビット単位に一致しないのに、状態ハッシュは合わなければならない、だから整数。プロトコルは 30 Hz、スナップショットは 15 Hz、クライアントが予測するのは自分の移動と向きだけ。楽な道ももちろん考えました——各自で計算して最後に突き合わせる——でもそれだと「同じ戦い」が「だいたい似た四つの戦い」になり、誰が落ちて誰が再接続して誰が先に覇者の腕が上がるのを見たかが全部もめる話になる。四人で一画面と決めた以上、「誰が決めるか」に初日から曖昧さは置けません。
あなたは止まる、世界は止まらない
レベルアップは単純で、敵が倒れると経験値、レベル二から八、レベルごとに三枚から一枚。カード画面のタイトルは「アップグレード」ではなく「変異を選択」にしました。一局のなかで育ったものは次の一局に持ち越さないので。戦技アップグレードが小さいほう、伝説アップグレードは四と八で、三枚を広げてゆっくり選ばせます。
問題は「ゆっくり選ぶ」のところ。一人用なら選んでいるあいだ世界を止めればいい。四人で戦うとき、誰の世界を止めるのか。落ち着いたルールはこうです。選んでいる本人は固定——動けず、攻撃できず、傷つかない——けれど森の他の人は戦い続け、時計も進む。まだ操作できる全員が選んでいるときだけ時計が止まる。誰も贔屓しないし、少し残酷でもある。カードを選んでいる数秒で仲間が囲まれているかもしれない。だから小さいほうのアップグレードはあとで画面を止めない形に変えました。隅に「1〜3 で選択」と出るだけで、戦いながら選べる。巻物を本当に広げるのは伝説アップグレードだけです。
門の向こうの四つの技
道の最後、遺跡の門の向こうに邪悪なミノタウロス。登場時に場の残りの敵をちょうど四体のゴブリンに入れ替え、それ以上でも以下でもなく、補充もなし。それから四つの技、近接攻撃、斧刃薙ぎ払い、大地踏み、猛牛突進。どれも落ちる前に地面に予兆——扇形、円、長い通路——輪郭が先に出て、それから埋まっていく。体力三割で咆哮、以後は速く、隙も短い。
ダメージの数値より予兆にずっと時間を使いました。数値は調整できても、プレイヤーが「次に何が起きるか」を読めなければ調整に意味がない。ひとりで作るゲームはテストもひとりで、いちばん自分を騙しやすいのが「次の技は知っている」という感覚です——そりゃ知っている、自分で書いたので。だから予兆は初見の人にも読める形でなければならない。
実機スクリーンショット · 開発中ビルド雨が変えるのは光
戦場には三つの天候があります。晴天、雨、風。雨は光を落として色を冷やし、雨の線が降って地面が光を返す。風は白い筋が戦場を横切り、樹冠が一方向へ傾く——幹は動かず、葉だけが揺れる。
ここははっきり書きます。天候が変えるのは絵だけ。地形も衝突も敵の判断もダメージも変えないし、サーバーには同期しない。切り替えの入口はまだ開発者用のデバッグ引き出しにあり、プレイヤー向け画面にはありません。公式サイトの晴天・雨・風の切り替えは見えているとおりのもの、同じ戦場・同じカメラの三表現の比較。「動的な天候が戦術を変える」のほうが売れる文句なのはわかりますが、いまの実装ではないので書きません。サイトの一文一文にリポジトリ内の対応物があること、これがこのプロジェクトが自分に課しているルールです。
東京の一週間
再起動の証明
オンライン部分でいちばん辛かったのはコードではなく、サーバーが戦闘中に再起動しても清算が正しく無効になり誰にも報酬が渡らない、という一点の証明でした。そのために東京のサーバーを何度も再起動したのですが、失敗の仕方が回を追うごとにひどくなる。認証情報の期限切れ。stdin と input を同時に渡したコマンド、Python がその場で拒否して子プロセスは起動すらしない。(None, [], []) と (None, tuple(), tuple()) を別物として扱い、永遠に「はい」と言えない検証器。最後はコンテナ停止前の三十秒の猶予、シミュレーションがそこを走り抜けて「無効」を通り越し普通の「失敗」になった。三十秒を五秒にしてようやく通りました。
この種の操作はやり直し禁止で毎回個別承認、というルールだったので、十数回でさすがに音を上げてエージェントに「承認させないでくれ、この作業パッケージのやることは何でも承認する」と残したら、それが本当に正式な承認になりました。その週の終わり、オンライン段階の締めは指標一つが一つ足りないまま——三十分の混合負荷で連続超過の最長が三回、契約は三回未満——その穴を抱えたまま先へ進み、穴は穴として文書に残しています。通過に化粧はしていません。
小道具に衝突がなかった
遊んで見つけた
8 月 6 日、少し遊んでみて二つ見つけました。戦場の小道具に衝突がない——石の山も台座もすり抜けられる。それと樹冠の遮蔽フェードが消えていた。木の後ろに回ると葉が透けるはずが透けない。
一つ目は「小道具に衝突体を付ける」では直りません。位置はサーバーが計算するので、クライアント側の衝突体はプレイ上は存在しないも同然だからです。遮りはサーバー側の場のデータに書き込む必要があり、それで場の identity が変わり、パッケージ済みの旧クライアントは新しい対戦に入れなくなる。「小道具に衝突がない」の一言の代償がコンテンツ移行一回。二つ目はそこまで劇的ではなく、フェードのコードは消えておらず、新しい場に探しているノードがなかっただけで、静かに動かなくなっていた。エラーは出ない、ただ何もしない。
どちらも遊んで見つけたもので、テストではありません。数千項目のチェックのどれも「人は祭壇をすり抜けられるか」とは訊いていませんでした。
おわりに
8 月 10 日、東京のサーバーを前回の安定版に戻そうとして失敗、証明書チェーンの検証が通らず、自動ロールバックは成功、サービスは残っています。ロールバックの成功は復旧の成功ではない、文書にもそう二文で書いてあります。その日から LAN がこのプロジェクトの「手の届く開発用デプロイ」になりました。同じ接続方式、同じプロトコルと identity、ただ東京にないだけ。
戦闘はまだ動いています。四人は動き、覇者の四技はあり、天候は引き出しの中、清算は全員かゼロか、手触りと数値と観戦の細部は変わり続けている。この一編はいまの姿と、なぜこの形なのかの記録です。