Unitale — 分散型Minecraftミニゲームネットワーク、独立業務として
5年間のソロ開発:カスタムSpigotフォーク、88,000行のJava SDK、型安全なミニゲームフレームワーク、21,000行のSymfonyバックエンドを構築。10万人以上のユニークプレイヤーと1万以上の同時接続ピークを支えました。
- Java 8
- Spigot (unispigot-serverフォーク)
- BungeeCord
- Redisson
- Hazelcast
- Redis Pub/Sub
- Symfony 3.3
- Doctrine ORM
- MariaDB
- Predis
- Maven / Nexus
- 分散システム
背景
Unitaleは私が2016年6月から2021年3月までソロで設計・実装・運用したMinecraftミニゲームネットワークです。5年間で累計5,500コミット、Java + PHP合わせて122,000行を、5つの相互依存するリポジトリに分けて開発しました:サーバーフォーク、Bukkit SDK、汎用ミニゲームフレームワーク、2つの完成ミニゲーム、ステートレスバックエンド。本番環境では、ユニークプレイヤー10万人以上、ピーク同時接続1万以上、Spigotインスタンス50台以上を継続的にオーケストレーションしていました。
目的はMineplexやHypixelのようなモノリシック・クラスタにミニゲームを一つ追加することではなく、型付きPub/Sub、サーバー間キャッシュ、清潔なライフサイクルといった分散基盤を据え、ソロの開発者が数日で新ミニゲームを投入でき、シーズンごとにランタイムを書き直さずインフラを水平スケールできる構造を作ることでした。
担当した役割
ソロのフルスタックアーキテクト兼エンジニア。プラグインとSpigot側はJava、バックエンド側はPHP、本番運用も含めて全てを担当しました。5年間、一人で、Minecraftプロトコルから分散状態管理、ミニゲーム横断の経済システム、モニタリング、デプロイまで、チェーン全体を所管しました。
構築したもの
サーバーフォーク — unispigot-server
Spigotプラグインではなく、Minecraft 1.10.2サーバーのカスタムビルドです。fr.unitale:unispigot-server:1.10.2-R0.1としてパッケージ化し、プライベートNexusで配布。SDKと全ミニゲームはscope=providedでこのサーバーに依存しており、Spigotのアップストリームリリースをそうしてネットコードやインベントリレンダリングをパッチできるようにしました。
SDK — 88,638行、3,885コミット
他のすべてが乗るJavaの基盤です。構造的に重要な部分:
- 型付きRedis Pub/Subバス:
CommonPubSubとRequestPubSubは、各値が独自のConsumer<String>ディスパッチハンドラを持つenum(ChannelType、RequestType)を公開します。プレイヤー間リクエスト(フレンド、パーティ、招待)では、コールバック署名がカリー化されています —Function<UUID, Function<String, Function<RequestActionType, Function<UniPlayer, Consumer<String>>>>>— これにより、ハンドラマップを手書きせずに、清潔な多態性非同期ディスパッチャを実現しています。 - デュアルストア分散レイヤー:Redissonを過渡的なPub/Subレイヤーに、Hazelcastを共有キャッシュ(進行中の試合、プライバシーコード、マッチメイキング履歴)に使用。Hazelcastマップ
parties_dispatching、game_requests_history、privacy_codesがクラスタの生きたメモリです。 - 11以上の構造化APIクライアント:
SpigotPlayerRedisApiClient、SpigotServerRedisApiClient、FeatureRedisApiClient、PartyRedisApiClient、SettingsRedisApiClient、ModoRedisApiClientなどRedis側に加え、PlayerRestApiClient、BoxRestApiClient、BoostRestApiClientなどRESTファミリーも併設。各ドメインは片方が落ちたときにREST → Redisのフォールバックを知っています。 - NMS直接アクセス(ProtocolLibではなく)。
fr.unitale.sdk.protocol.packetsパッケージには、サーバー側でレンダリングをインターセプトするUniPacketPlayOut*クラスが約30個あります(カスタムインベントリ、プレイヤー/NPCモーフ、EntityManagerとCustomProjectileEntityによるカスタムエンティティ)。外部プロトコル依存ゼロ、ホットパスでのCPUオーバーヘッドもゼロです。 - 国際化は多言語
ResourceBundle(fr_FR、en、es)で実現。20以上の.propertiesファイルと、{{example.magic.key}}形式のキー置換システムにより、コードに触れずにテキストを編集可能にしました。
ミニゲームフレームワーク — GameSDK2、6,315行
Engine<I extends Instance<I, M, T>, M extends GameMap<I, M, T>, T extends UniTeam<I, M, T>> — 3つの自己束縛ジェネリクスでパラメータ化されたエンジン。各ミニゲームに独自の型トリプレットを宣言させ、ランタイムキャストを完全に排除します。その上にモジュールシステムを構築:Module<I, M, T> extends InstanceListener、カスケード式ライフサイクル — WaitingModule → KitChoiceModule / KitViewModule → TeamModule → GameModule → DropModule → SpectateModule。各モジュールは状態遷移で清潔に登録/解除され、1つのモジュールのバグがインスタンス全体を破壊するのを防ぎます。
プレイヤーの試合間ルーティングは2種類のルームに基づきます:DispatchingRoom(マッチメイキング計算用の待機エリア)とPassingRoom(ゲーム間中継)。QueueManagerは共有Hazelcast IMapを読み、どのインスタンスも中央コーディネータなしで他のインスタンスのスロットを予約できます。
ミニゲーム
- LaserGame(1v1 / 2v2、2,162行、194コミット):7種類のレンズ(
FireballLens、GradientLens、NormalLens、SpiralLens、TriangleLens、WaveLensなど)と7種類の爆発(BurstExplosion、DropItemExplosion、LavaExplosion、NormalExplosion、SmokeExplosion、StarExplosion)を交差させたレーザー武器のプロシージャルマトリックス。レンズ×爆発の組み合わせごとにユニークな武器が生まれ、コード重複を排除しつつアリーナFPSに期待される多様性を実現しています。 - Arena(マルチチーム、4,052行、346コミット):8つのテーマ別マップ(
Endare、Endare_hell、Wow、Pokeball、Mario、Hyperball、Volcan、Masterball)、30以上の合成可能エフェクト(CupidEffect、DuckEffect、FlameThrowerEffect、HammerEffect、MagicWandEffect、PokeballEffect、ToiletBrushEffectなど)、そして5つの多態性リスナー(BlockPlaceListener、BowShootListener、EntityDamageByPlayerListener、InteractListener、ProjectileHitListener)が、巨大なif/elseなしで各イベントを関連エフェクトにディスパッチします。
バックエンド — Symfony書き直し、21,087行、515コミット
2020年、永続化と経済システムをJavaモノリスから切り離し、Symfony 3.3 + Doctrine ORM + MariaDB + Predisのバックエンドに移しました。25のモジュラーバンドルに分割:PlayerBundle、GameBundle、GameServerBundle、GuildBundle、ShopBundle、UserBundle、PermissionBundle、SanctionBundle、BoxBundle、FeaturesBundle、BoostBundle、SessionBundle、RedisBundleなど。Spigotサーバーとの契約はデフォルトでRESTになり、RESTタイムアウト時にはRedisson Pub/Subフォールバックで復元 — このレジリエンスパターンがピーク時の夜を何度も救いました。
このバックエンドがサーバー間経済を運用します:ゴールド(ソフト通貨)、エメラルド(プレミアム)、コスメティック(オーラ、スキン)をミニゲームの勝利で解禁し、ネットワーク内のどのサーバーでも保持します。
最も難しかった点
2016年当時、Kubernetesも近代的なオーケストレータもない状況で、サーバー間リアルタイム状態同期を実現すること。当時の競合(Mineplex、Hypixel)は独自ミドルウェアを持つモノリシック・クラスタで動いており、BungeeCordは単なるTCPプロキシでした。私はRedisson(過渡的、Pub/Sub)とHazelcast(分散マップ、ロック)を組み合わせ、即時イベント配信と一貫したキャッシュの両方を実現することを選びました — 現代のKafka + Redisスタックに見られるパターンですが、2016年のBukkit JVM上では自明とは程遠いものでした。
もう一つの構造的な難所は、チームなしで汎用ミニゲームフレームワークを健全に保つことでした。<I extends Instance<I, M, T>, M extends GameMap<I, M, T>, T extends UniTeam<I, M, T>>のような自己束縛入れ子ジェネリクスは設計コストが高く、MCプラグインのエコシステムでは稀です(多くのネットワークがモノリシックなGameEngineを継承し、積極的にキャストします)。この規律を5年間ソロで守ったからこそ、LaserGameとArenaがコピペなしで80%のコードを共有できました。Pub/Subディスパッチに使われたカリー化された関数型署名も同じ方向性です:QAが背後にいないからこそ、コンパイラに整合性を証明させる必要がありました。
成果
- ユニークプレイヤー10万人以上、ピーク同時接続1万以上、バックエンドインスタンス50台以上を継続的にオーケストレーション。
- 本番運用5年間(2016年6月 → 2021年3月)、5,500以上のコミット、Java + PHP 122,000行、すべてソロで保守。
- 複数の内部ゲームでコピペなしに再利用されたミニゲームフレームワーク — 「ゲーム以前にフレームワーク、フレームワーク以前にSDK」という賭けが検証された結果。
- このキャリア最長の証明:本格的な分散アーキテクチャは一人でも維持できる。Pub/Sub、整合性キャッシュ、清潔なライフサイクル、REST/Redisレジリエンスといったスキルが、その後のDofus Touch(ライブMMOバックエンド)、Authlete(マルチテナントOAuth SaaS)、そして現在のConexia Tahiti(フラクショナルCTO)で活きています。