NVMe PRのFencingがおもしろい:旧Brokerに交代を伝えず、書き込みだけを止める

読んだもの Alibaba Cloudの阿里云云原生チームによる、RocketMQの高可用性の記事(出典は末尾)。生成AIと一緒に読んで整理したメモで、記事本文は自分でも開いて確認した。細部(実測値や予約種別など)は追えていないので、ここでは設計の発想だけ書く。 そもそも何が問題か Active/Standbyで、Broker AからBroker Bへ切り替えたい。ただ、Aが応答しないからといって、Aが止まっているとは限らない。ネットワーク分断やI/Oハングで、AもBも「自分がActiveだ」と思って同じデータを書くと、Split-Brainでデータが壊れる。 「BがActiveになったとAに通知すればいい」と最初は思う。でも通知が届かないから困っているのであって、リースや分散ロックを足しても、失効を認識できない旧ノードは残る。 旧ノードが交代を認識することに依存する設計は、障害時にいちばん当てにならない。 NVMe PRでどう解くか NVMe Persistent Reservation(PR)は、ストレージ側で「書き込める保持者」を決める仕組み。保持者以外の書き込みは、デバイスが拒否する。 記事の構成では、Multi-Attachで新旧ノードから同じディスクを見えるようにしておき、障害時に新ノードがPRを奪う。つまり、 Multi-Attach:同じディスクが見える(書き手の制限はしない) NVMe PR:書ける人を一人に絞る という役割分担になる。旧Brokerのプロセスが生きていても、交代を知らなくても、書き込みはストレージ側で止まる。 流れを図にするとこうなる(理解用の簡略図で、実際の実装手順そのままではない)。 【通常時】 Broker A (Master) Broker B (Shadow) │ WRITE可能 │ データ領域は未マウント │ │ leaseを読むだけ ▼ ▼ ┌─────────────────────────────────────┐ │ 共有ブロックストレージ │ │ NVMe PR: Aが書き込み保持者 │ └─────────────────────────────────────┘ 【障害発生】 Broker A Broker B │ 応答なし │ leaseが進まないことを検知 │ ※実は生きているかも │ │ ※交代を知らない ▼ │ PRを奪う(preempt) ▼ ▼ ┌─────────────────────────────────────┐ │ 共有ブロックストレージ │ │ A: WRITE拒否 ✕ B: WRITE許可 ○ │ └─────────────────────────────────────┘ 【引き継ぎ後】 Broker A Broker B │ 旧保持者 │ 新保持者 │ WRITEしても拒否される │ キャッシュ処理 → マウント │ │ → journal/WAL回復 │ ▼ │ サービス再開 Aは交代を知らないまま動いていてもいい。止めているのはAの心がけではなく、ストレージだ。 ...

2026年10月25日 · 1 min

省電力設計の本質:通信・位置情報・ディスプレイをどう絞るか

「Androidアプリ開発の極意」(木田学ほか著、テックファーム監修、技術評論社、2017年)を読み進めながら整理したメモがベースになっている。刊行から年数が経っている本だが、バッテリー消費を抑えるための設計判断そのものは古びていない部分が多い。 はじめに アプリが「重い」「電池が減る」と言われるとき、原因の多くはCPU負荷そのものより、通信・位置情報・ディスプレイの3つの使い方に集約される。個別の最適化テクニックを覚える前に、「なぜその実装がバッテリーを食うのか」という設計原理を押さえておくと、新しいAPIが出てきたときにも応用が効く。 基本方針はシンプルで、避けられるなら避ける、必要なら節約する設計にするの一言に尽きる。以下、この3領域それぞれで何が問題になり、Androidがどう対処してきたかを整理する。 通信:タイミングをずらし、量を減らす 決まった時間に一斉通信しない 複数のアプリが同じタイミングで定期通信を行うと、端末は本来スリープしていられる時間まで叩き起こされることになる。これに対するAndroidの答えがAlarmManagerのアラームバッチング機構だ。 AlarmManager.set()(setExact()ではない通常版)は、API 19(Android 4.4)以降、指定時刻ちょうどの起動が保証されない仕様に変わっている。システムが複数アプリのアラームをまとめて処理し、意図的に遅延・分散させることで端末のウェイクアップ回数そのものを減らす仕組みだ。厳密な時刻指定が必要なカレンダーの通知などはsetExact()を使う設計になっており、「本当に正確な時刻が必要か」を自問するだけで、使うAPIが変わる。 電源状態・接続状態に応じて挙動を変える 満充電/充電中/放電中/充電なし、バッテリー残量は取得できるので、これに応じて通信頻度を変える設計が可能 そもそもネットワークに繋がっていなければ、到達しないとわかっている通信を試みるだけ無駄になる。接続状態はAndroidでは容易に取得できる 3G等の従量課金回線では、通信量そのものを絞る設計も選択肢に入る(ただし経路をつど判定するコストとのトレードオフは意識しておきたい) バックグラウンド通信制限(データセーバー) Android N以降には「データセーバー」機能があり、有効化されているとフォアグラウンドでも通信を抑える方向に働く。アプリ側はこの挙動があり得ることを前提に設計しておく必要がある。 通信データ量そのものを減らす GZIP圧縮は最低限やっておく データはまとめて取得し、通信回数自体を減らす キャッシュを活用する レジュームダウンロード(範囲指定でのデータ取得)を使えば、中断位置から再取得して結合できる ポーリング vs プッシュ 定期的に通信して更新を確認する「ポーリング」に対し、サーバー側から必要なときだけ通知する「プッシュ」はバッテリー面で有利になりやすい。AndroidではFCM(Firebase Cloud Messaging)が定番で、以下の制約を踏まえて設計する。 項目 内容 データメッセージのペイロード上限 4096バイト(4KB) 通知メッセージのペイロード上限 2KB メッセージの有効期限(デフォルト) 4週間(約28日) ペイロードには最小限のデータ(IDなど)だけを載せ、本体データはAPI経由で取得する設計が定石になる。ペイロードに実データを詰め込みすぎると、将来的にサイズ上限に引っかかって通知が届かなくなるか、最悪クラッシュにつながる。 リトライ間隔を空ける 通信失敗時のリトライを短い間隔で繰り返すと、サーバー負荷とクライアントのバッテリー消費の両方を悪化させる。指数バックオフなど、間隔を徐々に空ける設計が基本になる。 通信量の主導権をユーザーに渡す 通信量に関わる挙動(画質、同期頻度など)をユーザー自身が選べるようにするのも有効な設計の一つ。 ディスプレイ:点灯そのものを減らす ディスプレイは単純に「光っている時間」がそのままバッテリー消費に直結する。不要な点滅を避けるのは当然として、注意すべきはWakeLockの扱いだ。 WakeLockは画面スリープを防ぐ機能で、バッテリーの敵として扱われがちだが、ダウンロード処理中など「画面がオフになると処理自体が失敗する」場面では必須になる。**「使わない」ではなく「必要な範囲だけ、必要な時間だけ保持して確実に解放する」**設計判断が求められる。 位置情報:APIの選び方で消費が変わる LocationServices(Google Play Services必須)を使う方が、LocationManager単体よりバッテリー効率が良い傾向にある LocationManagerを使う場合、プロバイダをNETWORK_PROVIDERに指定するとWiFi経由の測位のみになり、GPSを使うより省電力になる 精度が本当に必要な場面かどうかを見極め、過剰に高精度なプロバイダを選ばないことが基本方針になる。 実装をまとめて任せる:JobScheduler ここまでの対策(電源状態に応じた実行判断、リトライ間隔、実行タイミングの分散など)を個別に手組みするのではなく、JobScheduler一本に任せるという選択肢がある。バッテリー状態・実行遅延・リトライ間隔などを条件として設定でき、標準APIとして安定している。特別な理由がない限り、これを軸に設計するのが妥当だろう。 なお、Android N以降ではブロードキャスト配信の仕組み自体にも変更が入っている。CONNECTIVITY_ACTION(ネットワーク変更)のような特定のブロードキャストを受信対象アプリが一斉に受け取ることでパフォーマンスが悪化する問題への対処で、対象イベントは複数あるが、ここでは深追いしない。 Expo/React Nativeでは何に当たるか ここまでの話はAndroidネイティブAPIの話だが、Expo管理下のReact Nativeアプリでは、それぞれ対応するモジュールに置き換わる。「素のAndroid APIを直接叩けない」制約はあるが、内部的には同じ仕組みに乗っているケースが多い。 Androidネイティブの概念 Expoでの対応 備考 AlarmManager / JobScheduler expo-background-task SDK 53でexpo-background-fetchを置き換え。Android側は内部的にWorkManager(JobSchedulerベース)を利用 FCM(プッシュ通知) expo-notifications リモート通知の受信・権限管理をラップ。ペイロードサイズ等の制約はFCM側の仕様がそのまま適用される WakeLock expo-keep-awake useKeepAwake()フックまたは命令的APIで画面スリープを抑制。必要な範囲だけ有効化する設計方針は変わらない 接続状態の判定 expo-network オンライン/オフライン、接続種別(WiFi/セルラー等)の取得に使う バッテリー状態の取得 expo-battery 充電状態・残量の取得。電源状態に応じた通信頻度の出し分けに使える LocationManager / LocationServices expo-location 精度指定(Accuracy)でGPS/WiFi測位のどちらに寄せるかを制御できる 注意点は主に2つ。 ...

2026年9月9日 · 1 min

Androidアプリ開発の極意 4,5章ユーザー体験と非同期処理

前の記事同様に Androidアプリ開発の極意 | 技術評論社の「ユーザーストレス軽減」まわりの実装Tipsと、並行処理の選択肢をざっくり整理してExpo周りと絡めたもの。細かい実装には踏み込まず、要点だけ。 はじめに アプリが固まる原因の大半は、メインスレッド(UIスレッド)を長時間ブロックしていること。非同期処理の設計は結局「ユーザーを待たせない」という一点のためにある。 二部構成で一部は本を読んだ感想+α、二部がExpoだとどうなるかって話。 基本的にAIで出して、それを読んで調べたりしたもの。 第1部:Androidの話 Android(Java/Kotlin)側だけで完結する話。UXまわりの実装Tipsと、非同期処理の手段がsynchronizedからVirtual Threadsまでどう変遷してきたかを、Expoの話を挟まずに整理する。 UXまわりの小さな判断 時間のかかる処理にはProgressBarで進捗を見せる SharedPreferencesは永続化のためディスクI/Oが発生する セルラー通信時は重い処理の前に一言断りを入れると親切 エラーはその場で表示しつつ、Crashlytics/ACRAのような開発者向けツールで後から追える形が理想 NDKは高速化と引き換えにデバッグの複雑さが増す。基本的には奥の手 非同期処理はなぜ増え続けているのか synchronizedは「同時に入らない」ことしか保証しない。順番までは保証しない。 Threadを直接生成した場合も同様で、生成順と実行順は一致しない。順番を保証したいならExecutors.newSingleThreadExecutor()のような直列化の仕組みを使う。 ExecutorServiceは、スレッドを直接管理せず、プールとキューで管理する仕組み。スレッド数の制御や再利用をアプリ側のロジックから切り離せる。 AsyncTaskはAPI 30で非推奨になった。ライフサイクル管理が弱く、Activity/Fragment破棄後もタスクが動き続けてリークやクラッシュを招くのが理由。 今の主流はKotlin Coroutines。スレッドをブロックせず中断できる軽量な並行処理で、排他制御が必要ならMutexを使う。UIに紐づく短い処理はCoroutines、画面を離れても続けるべき処理はWorkManagerと使い分ける。 JVM側ではJDK 21でVirtual Threads(JEP 444)が正式機能になった。I/Oバウンドな処理を大量にさばくのに向くが、CPUバウンドには向かない。ただしこれはJDK/Java SE側の機能で、Android Runtime(ART)は非対応。Androidアプリでそのまま使えるわけではない。 Akkaはアクター単位でメッセージパッシングする設計思想。2022年にライセンスがApache 2.0からBSL(Business Source License)に変わり、2024年10月以降はproduction利用にライセンスキーが必須になった。個人・OSS・startupは無料キー、商用は原則サブスクという体系。汎用ツールというより大規模分散システム向けの専門ツールという位置づけに変わっている。 比較表(Android) 手段 排他制御 順序保証 位置づけ synchronized あり なし 低レベルの排他制御プリミティブ Thread直接生成 なし なし 基本的には避ける ExecutorService 設計次第 直列化すれば可 CPUバウンド処理で有効 AsyncTask なし なし API 30で非推奨 Kotlin Coroutines Mutexで対応 設計次第 Android/Kotlinの主流 Virtual Threads 従来通り 設計次第 JVM側、I/Oバウンド向け(ART非対応) Akka アクターモデル 条件付き 大規模分散システム向け 「排他制御」と「順序保証」は別物。ここを混同すると設計は破綻する。AsyncTaskはAPI 30で非推奨、今はCoroutines(短命処理)とWorkManager(永続処理)を使い分けるのが主流。Virtual Threads(JDK 21)はJVM側の話でARTは非対応。 ...

2026年8月18日 · 1 min

Androidアプリ開発の極意 2,3章あたり ANR、UIスレッド

Androidアプリ開発の極意 | 技術評論社 この本読んでる。2017年に出版してて、内容には古いものがいくつかある。 そこはAIの使いどころ。とりあえず学習したことを軽くメモして、中身について古い場合は指摘を入れたり、今は何が主流なのかといったことを補完するような指示を出した。 サポート付きの読書っぽいことをしながら読み進めて、文章をまとめた。 概要 React Native/Expoでアプリを書いていると、「なぜかカクつくが、クラッシュログには何も残っていない」という現象に何度もぶつかる。 原因を追っていくと、大体はUIスレッドを塞いでいるか、メモリを食い潰しているかのどちらかという同じ根っこに行き着くことが多い気がする。 この記事では、Androidアプリ開発の極意 | 技術評論社の3章UIスレッド/ANRの基礎から、React NativeにおけるJS-ネイティブのスレッド分離、コルーチンとasync/awaitの正体、Android/iOS/RNの非同期処理対応、そして見落とされがちなメモリ管理(OOM・LMK・Javaのメモリリーク・useEffectのクリーンアップ)までを、実務で踏んだり踏みそうな地雷も交えて整理した。 なお、私は仕事でしばしReact Native Expoを利用しているため、なるべくそちらに沿った内容に寄せて本文を作成する。 それによって本の内容とは離れることがしばしあるが、そこは予め留意してほしい。 全体像 まずスレッドの分離構造をざっくり図にする。 [Android/ネイティブ] UIスレッド(メインスレッド) ── 描画・入力処理を担当 │ │ 5秒ブロック → ANRダイアログ ▼ ワーカースレッド/コルーチン(Dispatchers.IO等) ── 重い処理はここへ逃がす [React Native/Expo] JSスレッド ── ビジネスロジック・状態更新・レンダリング計算 │ │ (ブリッジ/JSI経由) ▼ ネイティブUIスレッド ── 実際の描画 │ (任意) Worklet用の別JSコンテキスト ── react-native-worklets-core / Reanimated ポイントは、Androidのネイティブ層には「ANR」という可視化された強制終了の仕組みがあるが、React NativeのJSスレッドが詰まっても同じ仕組みは働かないという非対称性にある。以下、順番に見ていく。 UIスレッド/ANRの基礎 Androidでは、UIスレッド(メインスレッド)が入力イベントに応答できない状態が一定時間続くと、システムが強制的にANR(Application Not Responding)ダイアログを出す。Android公式ドキュメントによれば、入力イベント(キー押下や画面タップなど)に5秒以内に応答しない場合にANRがトリガーされる。これはフォアグラウンドの入力ディスパッチに関する条件で、他にもブロードキャストレシーバーやサービスの応答遅延など複数のANR条件が存在する。 一方、React Native/Expoでは事情が異なる。RNのアーキテクチャはJSスレッドとネイティブUIスレッドが分離されている。JSスレッドが重い処理で詰まっても、ネイティブ側のUIスレッド自体がブロックされているわけではないため、Android OSが検知する「入力ディスパッチのタイムアウト」という意味でのANRダイアログは出にくい。ただし、体感としては「タップしても反応がない」「アニメーションがカクつく」という、ユーザーから見ればANRと区別のつかない現象が起きる。ANRダイアログが出ないぶん、むしろ発見が遅れて厄介とも言える。 補足: 「RNならANRが原理的に起きない」という理解は不正確。JSスレッドが完全にブロックされている間、そのJSスレッドに紐づくタッチイベント処理やRedux的な状態更新も止まるため、ユーザー体験としては同種の「固まり」が発生する。OS側の検知メカニズムが働かないだけで、問題自体は消えていない。 重い処理の逃がし方:InteractionManager(旧API)とWorkletsは別物 RN側で「重い処理をUI/JSスレッドから逃がす」ための手段として、従来は InteractionManager がよく使われていたが、2026年8月リリースのReact Native 0.87で InteractionManager は削除済みであり、公式は代替として requestIdleCallback を案内している。0.82〜0.86の時点では非推奨警告が出るようになっていたため、既存プロジェクトでこの警告を見た人も多いはずだ。以降は「過去にどう動いていたか」の説明として読んでほしい。 ...

2026年8月16日 · 2 min

Terraform学習1:variable/outputが分離しているのは何のためか

Terraformを触り始めると、variableとoutputが真逆の役割を持っていることはすぐわかる。だが「なぜこの2つが分離しているのか」「なぜvalidationが型チェックと別枠なのか」まで理解しないと、秘匿情報をうっかりstateに残す事故につながる。今回は自分のTerraform構成(variables.tf, outputs.tf, versions.tfのbackend部分)を題材に、設計意図を整理する。 variableは「設定とロジックの分離」のための入口 variableは、値をリソース定義本体に直書きせず、外部から注入するための仕組みだ。値を変えるたびにリソース定義そのものを編集しなくて済むようにする、という目的がある。 値の入力元は主に4つある。 default(ブロック内で指定) terraform.tfvars 環境変数(TF_VAR_xxx) -varオプション(CLI実行時) defaultの有無で意味が変わる 意味 defaultあり 共通・変更頻度が低い値。省略可能(例: aws_region, project_name) defaultなし 環境固有・必須の値。指定しないとplan/apply時にエラー(例: budget_alert_email, github_org) defaultの有無は「省略可能かどうか」の表明であり、そのままその変数の性格(共通設定か、環境固有の必須値か)を表している。 validationは型チェックとは別レイヤーの検証 variableのtypeは形式のチェックしかしない。type = stringは「文字列であること」しか保証せず、「その文字列が正しい値かどうか」は見ない。 そこを埋めるのがvalidationブロックだ。値の中身・意味をチェックする。例えばSSMパラメータ名は/始まりである必要がある、EBSルートボリュームは8GiB以上必要、といった業務ルールをここに書く。 variable "image_id" { type = string description = "The ID of the machine image (AMI) to use for the server." validation { condition = length(var.image_id) > 4 && substr(var.image_id, 0, 4) == "ami-" error_message = "The image_id value must be a valid AMI ID, starting with \"ami-\"." } } validationはapply前、ローカルのplan段階でエラーを弾ける。無駄なapply実行を未然に防げるのが最大の利点で、クラウド側にリクエストが飛ぶ前に「その値はそもそもおかしい」と教えてくれる。 ...

2026年7月25日 · 2 min

AI コーディングエージェントを飼い慣らす:最強の「AGENTS.md」の書き方

AI コーディングエージェントを飼い慣らす:最強の「AGENTS.md」の書き方 近年、Gemini、Claude、Aider といった「自律型 AI コーディングエージェント」が開発現場に浸透しつつあります。彼らは指示一つでファイル構成を理解し、コードを書き、テストを実行して修正まで行います。 しかし、エージェントを使い始めた多くのエンジニアが直面するのが、**「AI エージェントの暴走」**です。 型定義を面倒くさがって any を連発する プロジェクト独自のディレクトリ構造を無視して勝手に utils/ を作る vitest --watch などの終了しないコマンドを叩いてフリーズする 指示していないリファクタリングを始めて関係ないファイルを壊す これらの問題を防ぎ、エージェントを「熟練のペアプロ相手」に変える魔法のファイル、それが AGENTS.md です。本記事では、AI エージェントに守らせるべきルールを定義する AGENTS.md の書き方と、その設計思想を徹底解説します。 なぜ AGENTS.md が必要なのか AI エージェントは、人間が数年かけて培った「プロジェクトの阿吽の呼吸」を知りません。 エージェントに与えられるコンテキスト(ファイル内容や履歴)は有限であり、その中で彼らは「最も確率的に正しそうな推論」を行います。その結果、彼らはしばしば**「最も楽な道(=技術的負債を生む道)」**を選んでしまいます。 AGENTS.md をプロジェクトのルートに配置する理由は、**「エージェントの推論空間に強制的な制約(ガードレール)を設けるため」**です。 なぜその設計にしたか:外部メモリとしての役割 エージェントは毎回ゼロベースで思考するわけではありませんが、多くのファイルを見すぎることで逆に重要なルールを見失う(ロスト・イン・ザ・ミドル現象)ことがあります。AGENTS.md という明確なルールブックを定義し、エージェントの起動時やタスク開始時に必ず読み込ませることで、思考のブレを最小限に抑えることができます。 AGENTS.md に書くべき内容の 4 つの分類 効果的な AGENTS.md は、以下の 4 つのセクションで構成するのがベストプラティスです。 禁止事項 (Prohibitions): 致命的なエラーや環境のハングを防ぐ 命名規則・コーディング基準 (Standards): コードの品質を一定に保つ アーキテクチャ原則 (Architecture): システムの整合性を維持する テスト・検証方針 (Testing): 修正の正しさを担保する それぞれのセクションについて、具体的な記述例を見ていきましょう。 1. 禁止事項 (Prohibitions) エージェントが最もやりがちな「環境破壊」を防ぐための最重要セクションです。 ルール 理由(なぜその設計にするか) インタラクティブコマンドの禁止 npm init や git commit (メッセージ入力待ち) など、ユーザー入力を待つコマンドは CLI エージェントをフリーズさせます。 Watch Mode の禁止 vitest --watch 等はプロセスが終了しないため、エージェントが「完了」を検知できなくなります。 any 型の原則禁止 AI は型の整合性を取るのが面倒になると any で逃げようとします。これは長期的な保守性を著しく低下させます。 勝手な依存関係の追加禁止 package.json を書き換えて新しいライブラリを入れる行為は、セキュリティやビルドサイズに影響するため、人間の許可を必須にします。 実例コード ## 禁止事項 - **コマンド実行**: - `vi`, `nano`, `top` などのインタラクティブなコマンドは実行しない。 - `npm start`, `vitest --watch` などの終了しないプロセスは背景実行 (`&`) するか、単発実行モードを使用すること。 - **TypeScript**: - `any` 型の使用は厳禁。どうしても必要な場合はコメントで理由を明記すること。 - **Git**: - ユーザーの明示的な指示なしに `git commit` や `git push` を行わない。 2. 命名規則・コーディング基準 (Standards) プロジェクトの「見た目」と「一貫性」を守るためのルールです。ここを疎かにすると、エージェントは自分の得意なスタイル(多くの場合、学習データで最も多いスタイル)で書き始めてしまいます。 ...

2026年3月28日 · 2 min

React + Canvas 2D API で作るターン制ローグライク:論理と描画を切り離す設計

React + Canvas 2D API で作るターン制ローグライク:論理と描画を切り離す設計 React でゲームを作る際、多くの開発者が最初に直面するのが「DOM で描画するか、Canvas で描画するか」という選択です。特に数千枚のタイルや多数のユニットが登場するローグライクゲームでは、DOM 要素の管理はすぐにパフォーマンスの限界に達します。 本稿では、React の強力な状態管理(useReducer)と、Canvas 2D API の命令的な描画を組み合わせ、滑らかなアニメーションを実現しつつ堅牢なゲームロジックを維持する設計手法について解説します。 1. 概要:なぜ React と Canvas を組み合わせるのか React は「宣言的」な UI 構築に長けていますが、毎秒 60 回の頻度で数千の DOM 要素を更新するような動的な描画には向いていません。一方で、Canvas は「命令的」であり、ピクセル単位での高速な描画が可能ですが、状態と描画の同期を自分で行う必要があります。 この二つの「いいとこ取り」をするのが、「ロジックは React(useReducer)で、描画は Canvas で」 という役割分担です。 本記事で構築するアーキテクチャ Core Logic (useReducer): ゲームの「真実の状態(State of Truth)」を管理。ターン単位で離散的に変化する座標などを扱う。 Render Loop (requestAnimationFrame): Canvas 上で毎フレーム実行される描画処理。 Interpolation (補完): 離散的な論理座標を、滑らかな描画座標へと変換するローカル状態管理。 2. 設計判断:論理座標と描画座標の分離 ローグライクゲームは基本的に「ターン制」です。プレイヤーが右に移動したとき、内部データ(Core State)では x: 10 から x: 11 へと一瞬で書き換わります。しかし、これをそのまま描画すると、キャラクターがワープしたように見えてしまいます。 滑らかな移動(アニメーション)を実現するためには、以下の二種類の状態を明確に分ける必要があります。 なぜ分離が必要か 種類 管理場所 特徴 役割 論理座標 (Logical Position) useReducer (Global) 整数(タイル単位)。 当たり判定、AI、クエスト進行など。 描画座標 (Visual Position) Canvas 内の Actor クラス (Local) 小数点を含むピクセル単位。 滑らかな移動、揺れ、エフェクト。 判断理由: Core State にアニメーションの「途中経過(x: 10.2 など)」を持たせてしまうと、ゲームロジックが描画の都合に汚染されます。例えば、「まだ移動アニメーション中だから攻撃はできない」といった判定をロジック層で書く必要が出てき、コードが複雑化します。ロジック層は常に「今は(論理的に)どこにいるか」だけを知っていれば良いのです。 ...

2026年3月28日 · 4 min

React で作る堅牢なゲームセーブシステム:localStorage と Reducer を疎結合に保つ設計

React で作る堅牢なゲームセーブシステム:localStorage と Reducer を疎結合に保つ設計 ゲーム開発において、プレイヤーの進行状況を保存する「セーブ / ロード」は最も重要な機能の一つです。Web ブラウザで動作する React アプリケーションの場合、最も手軽な保存先は localStorage です。 しかし、単純に localStorage.setItem をコードのあちこちに散りばめてしまうと、テスタビリティが低下し、データ構造の変更(スキーマ変更)に弱いシステムになってしまいます。 本記事では、架空の RPG 『React Odyssey』を例に、SavePort インターフェースと Reducer の初期化関数を組み合わせた、クリーンで堅牢なセーブ / ロードの実装方法を解説します。 1. 概要:なぜ「直接 localStorage」を避けるのか React の useReducer を使ったゲーム開発では、ゲームの状態(State)は一つの大きなオブジェクトとして管理されます。これを JSON.stringify して localStorage に保存するのは簡単です。 しかし、以下の理由から、ロジックの中に直接 localStorage を書くべきではありません。 副作用の分離: Reducer は純粋関数であるべきです。セーブ処理(副作用)を Reducer の中に入れることはできません。 環境非依存: 将来的に保存先を IndexedDB やクラウド(Firebase 等)に変更したくなったとき、コードを大幅に書き換える必要が出てきます。 テストのしやすさ: localStorage が存在しない Node.js 環境(Vitest 等)でロジックのテストを行う際、モック化が容易である必要があります。 これらを解決するために、Dependency Inversion Principle(依存性逆転の原則) に基づいた設計を採用します。 2. SavePort 設計:抽象化の定義 まずは、ゲームエンジン側が必要とする「セーブ機能」のインターフェースを定義します。これを SavePort と呼びます。 // domain/save/save-port.ts /** * 保存されるデータの構造(スキーマ) * ゲームの現在の状態に加え、メタ情報を付与する */ export interface SaveData { version: number; // セーブデータのバージョン timestamp: string; // 保存日時 state: GameState; // 実際のゲーム状態 } /** * セーブ/ロードに関する抽象インターフェース */ export interface SavePort { /** データを保存する */ save(data: SaveData): Promise<void>; /** データを読み込む。存在しない場合は null を返す */ load(): Promise<SaveData | null>; /** セーブデータが存在するか確認する */ exists(): Promise<boolean>; /** セーブデータを削除する */ clear(): Promise<void>; } なぜインターフェースにするのか? ゲームのメインロジック(UseCase)は、この SavePort を通じて読み書きを行います。この時点では「どうやって保存するか」は知らなくてよく、「保存できる」という事実だけに依存させます。 ...

2026年3月28日 · 4 min

Reducer パターンだけでゲーム状態を管理する:副作用ゼロのアーキテクチャ

Reducer パターンだけでゲーム状態を管理する:副作用ゼロのアーキテクチャ 概要 現代のフロントエンド開発において、Redux や React の useReducer を通じて「Reducer パターン」は広く浸透しました。しかし、このパターンを本格的なゲーム開発、特に複雑なロジックが絡み合う RPG やシミュレーションゲームに適用しようとすると、多くの開発者が「副作用」の壁にぶつかります。 「ダメージ計算に乱数を使いたい」「ゲーム内の経過時間を管理したい」「マスターデータを参照したい」……。 これらの要素を素直に実装すると、Reducer の外側にある状態や関数に依存してしまい、純粋関数としての美しさとテストのしやすさが失われてしまいます。 本記事では、すべての副作用を引数として注入し、gameReducer(state, action, masters, random) という純粋関数のみでゲームの全ロジックを完結させる「副作用ゼロ」のアーキテクチャについて解説します。 課題:なぜゲーム開発で Reducer は敬遠されるのか 一般的な Web アプリケーションの Reducer は単純です。「ボタンを押したらフラグを反転させる」「入力された文字列を状態に保存する」といった操作がメインだからです。 しかし、ゲームでは以下のような「非決定的な要素」や「巨大な静的データ」が頻繁に登場します。 1. 乱数 (Randomness) の扱い クリティカルヒットの判定、モンスターのドロップアイテム、マップの自動生成など、ゲームは乱数の塊です。Reducer の中で Math.random() を呼んだ瞬間、その関数は「同じ入力に対して同じ出力を返す」という純粋性を失います。 2. 静的データ (Master Data) の参照 モンスターのステータス表、アイテムの定義、スキル効果など、ゲームには膨大な「変わらないデータ」があります。これを Reducer の外にあるグローバル変数から参照すると、テスト時にそのグローバル変数の状態も気にしなければならなくなります。 3. 時刻 (Time) と経過の管理 「3時間経過したらスタミナが回復する」「夜になるとモンスターが強くなる」といった時間依存の処理です。new Date() を Reducer で使うのは、乱数と同様に純粋性を破壊します。 4. 非同期処理 (Async/Fetch) サーバーから最新のイベント情報を取得したり、セーブデータをロードしたりする処理です。Reducer は同期的に実行される必要があるため、非同期処理をそのまま中に書くことはできません。 OOP(オブジェクト指向)との比較 クラスベースの OOP では、player.attack(monster) のようにメソッドを呼び出します。これは直感的ですが、内部で this.hp -= damage のように状態を直接書き換えます(ミュータブル)。 小規模なら良いですが、プロジェクトが大きくなり「攻撃時にスキルが発動し、その効果で回復し、さらにログに記録し……」と連鎖が始まると、どこで何が起きたかを追跡するのが不可能になります。 設計:副作用を「引数」に封じ込める 副作用を排除するための解決策はシンプルです。「必要なものはすべて外から渡す」、つまり依存性の注入 (DI) です。 ...

2026年3月28日 · 4 min

TypeScript npm workspaces でゲームロジックを UI から完全分離する

TypeScript npm workspaces でゲームロジックを UI から完全分離する 概要 モダンなフロントエンド開発、特にゲーム開発において、「ロジック」と「表示(UI)」の分離は永遠の課題です。React や Vue などのフレームワークにロジックが密結合してしまうと、テストが困難になり、将来的に別のプラットフォーム(例えば Web から React Native や CLI ツールへ)に展開する際の大きな障害となります。 本記事では、TypeScript npm workspaces を活用して、ゲームロジックを独立したパッケージ (packages/core) として切り出し、React UI (apps/client) から完全に分離する設計手法を解説します。また、外部 I/O や非決定的な処理(乱数など)を抽象化する Port / Adapter パターンについても触れます。 課題:なぜロジックが UI に染み出すのか? 多くのプロジェクトでは、気づかないうちにロジックが React コンポーネントや Hooks の中に漏れ出していきます。 // 密結合な例 const PlayerStats = () => { const [hp, setHp] = useState(100); const handleAttack = () => { // UI の中で計算ロジックが動いている const damage = Math.floor(Math.random() * 10) + 5; setHp(prev => Math.max(0, prev - damage)); }; return <button onClick={handleAttack}>攻撃を受ける</button>; }; このような設計には以下の課題があります: テストの困難さ: Math.random() が直接使われているため、結果が不安定でユニットテストが書きにくい。 再利用性の欠如: この「ダメージ計算ロジック」を、サーバーサイドや別の UI フレームワークで使い回すことができない。 依存の混入: ロジックを動かすために React の実行環境(レンダリングサイクル)が必要になる。 設計:npm workspaces による物理的隔離 ロジックを「物理的に」隔離するために、以下の monorepo 構成を採用します。 ...

2026年3月28日 · 3 min