第2.6章: サーバーvsクライアントアーキテクチャ
ホーム | << 前へ: ファイル構成 | サーバーvsクライアントアーキテクチャ
要約: DayZはクライアント-サーバー型のゲームです。書くすべてのコードは特定のコンテキスト -- サーバー、クライアント、または両方で実行されます。この分割を理解することは、セキュアで機能的なMODを書くために不可欠です。この章では、コードがどこで実行されるか、どちら側にいるかの検出方法、マルチパッケージMODの構造化方法、サーバーとクライアントのコードを適切に分離するパターンを説明します。
目次
- 基本的な分割
- 3つの実行コンテキスト
- コードの実行場所を確認する
- mod.cppのtypeフィールド
- config.cppのtypeフィールド
- マルチパッケージMODアーキテクチャ
- ゴールデンルール
- スクリプトレイヤーとサイドのマトリックス
- プリプロセッサガード
- 一般的なサーバー-クライアントパターン
- リッスンサーバーの落とし穴
- 分割MOD間の依存関係
- 実世界の分割例
- よくある間違い
- 判断フローチャート
- サマリーチェックリスト
基本的な分割
DayZは専用サーバーモデルを使用します。サーバーとクライアントは別々の実行ファイルを実行する別々のプロセスです。ネットワーク経由で通信し、エンジンがエンティティ、変数、RPCの同期を処理します。
+------------------------------------------------------------------+
| |
| 専用サーバー |
| - ヘッドレスプロセス(ウィンドウなし、GPUなし) |
| - 権限あり: ゲーム状態を所有 |
| - エンティティのスポーン、ダメージ適用、データ保存 |
| - プレイヤーなし、UIなし、キーボード入力なし |
| - 実行: MissionServer |
| |
+------------------------------------------------------------------+
^ ^
| ネットワーク(RPC、同期変数) |
v v
+---------------------------+ +---------------------------+
| | | |
| クライアント1 | | クライアント2 |
| - ウィンドウ、GPUあり | | - ウィンドウ、GPUあり |
| - ワールドをレンダリング | | - ワールドをレンダリング |
| - プレイヤー入力処理 | | - プレイヤー入力処理 |
| - UIとHUDを表示 | | - UIとHUDを表示 |
| - 実行: MissionGameplay | | - 実行: MissionGameplay |
| | | |
+---------------------------+ +---------------------------+3つの実行コンテキスト
1. 専用サーバー
専用サーバーはヘッドレスプロセスです。ウィンドウ、グラフィックカード出力、モニター、キーボード、マウスはありません。ゲームロジックを実行するためだけに存在します。
主な特徴:
- 権限あり -- サーバーの状態が真実。サーバーがプレイヤーの体力が50と言えば、50です。
- プレイヤーオブジェクトなし --
GetGame().GetPlayer()は専用サーバーでは常にnullを返します。 - UIなし -- ウィジェットを作成したりメニューを表示するコードはクラッシュするか静かに失敗します。
- 入力なし -- キーボードもマウスもありません。
- ミッションクラス -- サーバーは
MissionServerをインスタンス化します(MissionGameplayではない)。
2. クライアント
クライアントはプレイヤーのゲームです。ウィンドウがあり、3Dグラフィックスをレンダリングし、オーディオを再生し、入力を処理します。
主な特徴:
- プレゼンテーション層 -- クライアントはサーバーが指示するものをレンダリングします。
- プレイヤーあり --
GetGame().GetPlayer()はローカルプレイヤーのPlayerBaseインスタンスを返します。 - UIとHUD -- すべてのウィジェット作成、レイアウト読み込み、メニューコードがここで実行されます。
- 限定的な権限 -- クライアントはアクションを要求できますが、サーバーが決定します。
- ミッションクラス -- クライアントは
MissionGameplayをインスタンス化します。
3. リッスンサーバー(開発/テスト)
リッスンサーバーは同じプロセス内でサーバーとクライアントの両方です。
主な特徴:
IsServer()とIsClient()の両方がtrueを返す -- これが専用サーバーとの重要な違い。MissionServerとMissionGameplayの両方のフックが実行される- 開発専用 -- 本番サーバーは常に専用
- バグを隠す可能性がある -- リッスンサーバーで動作するコードが専用サーバーで壊れることがある
コードの実行場所を確認する
if (GetGame().IsServer())
{
// 真: 専用サーバー、リッスンサーバー
// 偽: リモートサーバーに接続したクライアント
// 用途: サーバーサイドロジック(スポーン、ダメージ、保存)
}
if (GetGame().IsClient())
{
// 真: リモートサーバーに接続したクライアント、リッスンサーバー
// 偽: 専用サーバー
// 用途: UIコード、入力処理、視覚効果
}
if (GetGame().IsDedicatedServer())
{
// 真: 専用サーバーのみ
// 偽: クライアント、リッスンサーバー
}
if (GetGame().IsMultiplayer())
{
// 真: マルチプレイヤーセッション(専用サーバー、リモートクライアント)
// 偽: シングルプレイヤー/オフラインモード
}真理値表
| メソッド | 専用サーバー | クライアント(リモート) | リッスンサーバー |
|---|---|---|---|
IsServer() | true | false | true |
IsClient() | false | true | true |
IsDedicatedServer() | true | false | false |
IsMultiplayer() | true | true | false |
GetPlayer() が返すもの | null | PlayerBase | PlayerBase |
一般的なパターン
// ガード: サーバー専用ロジック
void SpawnLoot(vector position)
{
if (!GetGame().IsServer())
return;
// サーバーのみがエンティティを作成
EntityAI item = EntityAI.Cast(GetGame().CreateObjectEx("AK101", position, ECE_PLACE_ON_SURFACE));
}
// ガード: クライアント専用ロジック
void ShowNotification(string text)
{
if (!GetGame().IsClient())
return;
// クライアントのみがUIを表示可能
NotificationSystem.AddNotification(text, "set:dayz_gui image:icon_pin");
}mod.cppのtypeフィールド
type = "mod"(両側)
MODはサーバーとクライアントの両方で読み込まれます。
使用タイミング: ほとんどのMODがこれを使用します。共有型(エンティティ定義、設定クラス、RPCデータ構造)を持つMODは、両側が同じ型を知るために type = "mod" である必要があります。
type = "servermod"(サーバーのみ)
MODはサーバーのみで読み込まれます。クライアントはそれを見ず、ダウンロードせず、存在を知りません。
使用タイミング: クライアントがアクセスすべきでないサーバーサイドロジック:
- スポーンアルゴリズム(プレイヤーがルートを予測するのを防止)
- AIブレインロジック(エクスプロイト分析を防止)
- 管理コマンドとサーバー管理
- データベース接続と外部API呼び出し
- アンチチート検証ロジック
セキュリティの重要性
スポーンロジックが type = "mod" パッケージにある場合、すべてのプレイヤーがそれをダウンロードします。PBOをデコンパイルして、スポーンアルゴリズム、ルートテーブル、管理パスワード、アンチチートロジックを読むことができます。機密のサーバーロジックは常に type = "servermod" パッケージに入れてください。
config.cppのtypeフィールド
config.cpp 内(CfgMods セクション)にも type フィールドがあります。このフィールドは mod.cpp のtypeフィールドと一致させてください。不一致があると予測不能な動作になります。
config.cpp には defines[] 配列も含まれ、クロスMOD検出用のプリプロセッサシンボルを有効にします:
class CfgMods
{
class StarDZ_AI
{
type = "mod";
defines[] = { "STARDZ_AI" }; // 他のMODが #ifdef STARDZ_AI を使用可能
};
};マルチパッケージMODアーキテクチャ
なぜ複数パッケージに分割するのか?
type = "mod" の単一MODフォルダはすべてをクライアントに配布します。機密のサーバーロジックを持つMODの場合、分割が必要です。
クライアントパッケージ(type = "mod")に含まれるもの:
- エンティティクラス定義(両側がクラスの存在を知る必要がある)
- RPC ID定数とデータ構造(両側が送受信)
- GUIレイアウト、imagesets、スタイル
- クライアントサイドUIコード(
#ifndef SERVERでラップ) - モデル、テクスチャ、サウンド
サーバーパッケージ(type = "servermod")に含まれるもの:
- マネージャー/コントローラークラス(スポーンロジック、AIブレイン)
- サーバーサイド検証とアンチチート
- 設定読み込みとファイルI/O
- 管理コマンドハンドラ
- 外部サービス統合(Webhook、API)
依存チェーン
サーバーパッケージはクライアントパッケージに依存します。逆は決してありません:
// クライアントMOD: config.cpp
class CfgPatches
{
class MyMod_Scripts
{
requiredAddons[] = { "DZ_Scripts" }; // サーバーへの依存なし
};
};
// サーバーMOD: config.cpp
class CfgPatches
{
class MyModServer_Scripts
{
requiredAddons[] = { "DZ_Scripts", "MyMod_Scripts" }; // クライアントに依存
};
};ゴールデンルール
ルール1: サーバーが権限を持つ
サーバーがゲーム状態を所有します。何が存在し、どこに存在し、何が起こるかを決定します。
ルール2: クライアントはプレゼンテーションを処理
クライアントはワールドをレンダリングし、サウンドを再生し、UIを表示し、入力を収集します。ゲーム結果を決定しません。
ルール3: RPCがブリッジ
リモートプロシージャコール(RPC)は、サーバーとクライアントが通信する唯一の構造化された方法です。
ルール4: クライアントを信頼しない
クライアントからのデータは改ざんされている可能性があります。常にサーバーで検証してください。
責任マトリックス
| タスク | 場所 | 理由 |
|---|---|---|
| エンティティのスポーン | サーバー | アイテム複製を防止 |
| ダメージの適用 | サーバー | ゴッドモードハックを防止 |
| エンティティの削除 | サーバー | グリーフエクスプロイトを防止 |
| プレイヤーデータの保存 | サーバー | サーバーサイドの永続ストレージ |
| アクションの検証 | サーバー | アンチチート強制 |
| 権限のチェック | サーバー | クライアントは自己認可できない |
| UIパネルの表示 | クライアント | サーバーにはディスプレイがない |
| キーボード/マウスの読み取り | クライアント | サーバーには入力デバイスがない |
| サウンドの再生 | クライアント | サーバーにはオーディオ出力がない |
| エフェクトのレンダリング | クライアント | サーバーにはGPUがない |
スクリプトレイヤーとサイドのマトリックス
| レイヤー | 専用サーバー | クライアント | リッスンサーバー | 備考 |
|---|---|---|---|---|
1_Core | コンパイル | コンパイル | コンパイル | すべての側で同一 |
2_GameLib | コンパイル | コンパイル | コンパイル | すべての側で同一 |
3_Game | コンパイル | コンパイル | コンパイル | 共有型、設定、RPC |
4_World | コンパイル | コンパイル | コンパイル | エンティティは両側に存在 |
5_Mission (MissionServer) | 実行 | スキップ | 実行 | サーバー起動/シャットダウン |
5_Mission (MissionGameplay) | スキップ | 実行 | 実行 | クライアントUI/HUD初期化 |
レイヤー1〜4はすべての側でコンパイル・実行されます。レイヤー5(5_Mission)で分割が明示的になります。
プリプロセッサガード
SERVERの定義
エンジンは専用サーバー用にコンパイルする際に自動的に SERVER を定義します。これはコンパイル時チェックであり、ランタイムチェックではありません:
#ifdef SERVER
// このコードはサーバーでのみコンパイルされる
// クライアントバイナリにはまったく存在しない
#endif
#ifndef SERVER
// このコードはクライアントでのみコンパイルされる
// サーバーはこのコードを見ない
#endif共有MODでのクライアントミッションフック
クライアントMOD(type = "mod")に modded class MissionGameplay が含まれる場合、#ifndef SERVER でラップする必要があります。そうしないと、専用サーバーがそれをコンパイルしようとしますが、MissionGameplay がサーバーに存在しないため失敗します:
#ifndef SERVER
modded class MissionGameplay
{
protected ref MyClientUI m_MyUI;
override void OnInit()
{
super.OnInit();
m_MyUI = new MyClientUI();
}
};
#endif一般的なサーバー-クライアントパターン
パターン1: サーバーサイド検証とクライアントフィードバック
最も基本的なマルチプレイヤーMODパターンです。クライアントがアクションを要求し、サーバーが検証し、結果を返送します。
パターン2: 設定同期(サーバーからクライアント)
サーバーが設定を所有します。プレイヤーが接続すると、クライアントが表示を調整できるように関連設定をクライアントに送信します。
パターン3: エンティティ状態同期
サーバーとクライアントの両方に存在するエンティティがカスタム状態を同期する必要がある場合に使用します。
パターン4: 権限チェック
権限は常にサーバーでチェックされます。クライアントはUI目的で権限データをキャッシュできますが(ボタンのグレーアウトなど)、サーバーが最終的な権限です。
リッスンサーバーの落とし穴
リッスンサーバーはサーバーとクライアントの境界を曖昧にするため、最も危険な環境です。
1. IsServer()とIsClient()の両方がtrue
void MyFunction()
{
if (GetGame().IsServer())
{
DoServerThing(); // リッスンサーバーで実行
}
if (GetGame().IsClient())
{
DoClientThing(); // リッスンサーバーでも実行!
}
// リッスンサーバーでは両方のブランチが実行される!
}修正: 排他的ブランチが必要な場合は else if を使用するか IsDedicatedServer() をチェック。
2. MissionServerとMissionGameplayの両方が実行
リッスンサーバーでは、modded class MissionServer と modded class MissionGameplay の両方がフックを実行します。
3. リッスンサーバーでGetGame().GetPlayer()が動作する
専用サーバーでは GetGame().GetPlayer() は常にnullを返します。リッスンサーバーではホストプレイヤーを返します。これに誤って依存するコードはテスト中に動作しますが、実際のサーバーではクラッシュします。
4. リッスンサーバーでのテストはバグを隠す
公開前に必ず専用サーバーでテストしてください。 リッスンサーバーテストは素早い反復に便利ですが、適切な専用サーバーテストの代替にはなりません。
分割MOD間の依存関係
requiredAddons[]が読み込み順序を制御
MODをクライアントとサーバーパッケージに分割する場合、サーバーパッケージはクライアントパッケージを依存として宣言する必要があります。
defines[]によるオプション依存検出
CfgMods の defines[] 配列は、他のMODが #ifdef でチェックできるプリプロセッサシンボルを作成します。
ソフトvsハード依存
ハード依存: requiredAddons[] に記載。依存が欠けているとMODが読み込まれません。
ソフト依存: コンパイル時に #ifdef で検出。MODは関係なく読み込まれますが、依存が存在する場合に追加機能を有効にします。
// StarDZ Coreへのソフト依存
#ifdef STARDZ_CORE
class SDZ_AIAdminConfig : StarDZConfigBase
{
// Coreが読み込まれている場合にのみ存在
};
#endifよくある間違い
間違い1: クライアントでサーバーロジックを実行
// 間違い: これはクライアントで実行 -- どのプレイヤーでもアイテムをスポーンできる!
void OnButtonClick()
{
GetGame().CreateObjectEx("M4A1", GetGame().GetPlayer().GetPosition(), ECE_PLACE_ON_SURFACE);
}
// 正しい: クライアントが要求、サーバーが検証してスポーン
void OnButtonClick()
{
ScriptRPC rpc = new ScriptRPC();
rpc.Write("M4A1");
rpc.Send(null, MyRPC.SPAWN_REQUEST, true);
}間違い2: サーバー専用MODにUIコード
修正: すべてのUIコードはクライアントパッケージ(type = "mod")に入れ、#ifndef SERVER でラップ。
間違い3: サーバーでGetGame().GetPlayer()
修正: サーバーでは、プレイヤーはイベント、RPC、またはイテレーションを通じて渡されます。
間違い4: リッスンサーバー互換性の忘れ
IsServer() と IsClient() が相互排他的であると仮定しないでください。
間違い5: オプションMOD検出に#ifdefを使用しない
間違い6: 共有型をサーバーパッケージにのみ配置
修正: 共有データ構造(RPCデータ、エンティティ定義、設定クラス)はクライアントパッケージ(type = "mod")に入れてください。
間違い7: クライアントでサーバーファイルパスをハードコード
修正: サーバーが設定を読み込み、RPC経由でクライアントに関連データを送信します。クライアントはサーバーの設定ファイルを直接読みません。
判断フローチャート
エンティティを作成/破棄するか?
/ \
はい いいえ
| |
UIを表示するか? UIを表示するか?
/ \ / \
はい いいえ はい いいえ
| | | |
エラー! サーバー クライアント データクラスまたは
(エンティティ = のみ RPC定数か?
サーバー、 / \
UI = クライアント はい いいえ
-- 再設計) | |
共有 ファイルの読み書き
(クライアント または検証するか?
MOD) / \
はい いいえ
| |
サーバー 共有
(servermod) (クライアントMOD、
IsServer/
IsClientで
ガード)サマリーチェックリスト
分割MODを公開する前に確認してください:
- [ ] クライアントパッケージが
mod.cppとconfig.cppの両方でtype = "mod"を使用 - [ ] サーバーパッケージが
mod.cppとconfig.cppの両方でtype = "servermod"を使用 - [ ] サーバーの
config.cppがrequiredAddons[]にクライアントパッケージをリスト - [ ] すべての共有型(RPCデータ、エンティティクラス、列挙型)がクライアントパッケージにある
- [ ] すべてのサーバーロジック(スポーン、検証、AIブレイン)がサーバーパッケージにある
- [ ]
MissionGameplayのmoddedクラスが#ifndef SERVERでラップされている - [ ] nullチェックなしのサーバーでの
GetGame().GetPlayer()呼び出しがない - [ ] サーバーパッケージにUI/ウィジェットコードがない
- [ ] オプションの依存が直接参照ではなく
#ifdefガードを使用 - [ ]
defines[]配列がmod.cppとconfig.cpp間で一致 - [ ] リッスンサーバーだけでなく専用サーバーでテスト済み
- [ ] サーバー設定ファイルがサーバーサイドで読み込まれ、クライアントが直接読み取るのではなくRPC経由で同期
