概要
動作確認環境: UE 5.4+
「GameMode、GameState、PlayerState…名前は似ているけど何が違うのかわからない」――これはUE開発を始めた多くの方が最初にぶつかる壁の1つです。特にマルチプレイヤーゲームでは、どのクラスがどこに存在し、何が複製されて何がされないのかを正しく理解していないと、サーバー・クライアント間のデータ同期で苦労します。
この3クラスの役割分担を理解すれば、シングルプレイヤーでもマルチプレイヤーでも、堅牢で拡張性の高いゲームロジックを構築できます。
3クラスの比較
まず全体像を把握しましょう。
| クラス | 存在場所 | 役割 | 複製 |
|---|---|---|---|
| GameMode | サーバーのみ | ゲームのルール定義・実行 | されない |
| GameState | サーバー+全クライアント | ゲーム全体の状態管理 | される |
| PlayerState | サーバー+全クライアント | 各プレイヤーの状態管理 | される |
GameModeがサーバーのみに存在するのが最も重要なポイントです。これはゲームのルール(勝利条件、スポーン位置、参加条件など)をクライアント側で改変させないための設計です。クライアントが知る必要のあるゲーム状態は、GameStateやPlayerStateを経由して同期されます。
GameMode:ルールブック
GameModeはゲームのルールそのものを定義するクラスです。「審判」や「ルールブック」と考えるとイメージしやすいでしょう。
主な管理内容:
- プレイヤーの参加・退出時の処理(ログイン/ログアウト)
- Pawnのスポーン位置・タイミング・条件
- マッチの開始・終了条件
- ゲームモード固有のルール(デスマッチ、CTFなど)
AGameModeBase vs AGameMode
UEにはGameModeの基底クラスが2つあります。新規プロジェクトのデフォルトは AGameModeBase です。
| クラス | 特徴 | 推奨用途 |
|---|---|---|
AGameModeBase | シンプルで軽量 | シングルプレイヤー、非マッチベースのゲーム |
AGameMode | マッチステートマシン搭載 | チームデスマッチ等のマッチベースマルチプレイヤー |
AGameMode は AGameModeBase を継承し、以下の6つのマッチステートを管理するステートマシンが組み込まれています: EnteringMap → WaitingToStart → InProgress → WaitingPostMatch → LeavingMap(異常時は Aborted)。マッチの開始待機やポストゲーム画面の制御に使います。
GameModeはプロジェクト設定、レベルのワールド設定、またはURL引数として設定でき、レベルごとに異なるGameModeを使い分けることも可能です。
GameState:スコアボード
GameStateはゲーム全体の現在の状態を管理し、全クライアントに同期するクラスです。GameModeが「ルールブック」だとすれば、GameStateは「スコアボード」や「タイマー」のようなものです。
主な管理内容:
- ゲーム経過時間(
GetServerWorldTimeSeconds()でサーバー同期された正確な時間を取得) - 接続プレイヤーリスト(
PlayerArrayプロパティ) - チームスコアなど、特定のプレイヤーに紐付かないゲーム全体の情報
- ゲーム開始状態(
HasBegunPlay())
GameStateはあくまでゲーム全体に関わる状態を管理するためのものです。個々のプレイヤー固有のデータ(個人スコアや名前など)は、次に説明するPlayerStateで管理するのが適切です。
PlayerState:個人の成績表
PlayerStateはゲームに参加している個々のプレイヤーの状態を管理するクラスです。全クライアントに複製されるため、たとえばスコアランキングUIで他プレイヤーの情報を表示するといった用途に直接利用できます。
組み込みのプロパティとメソッド:
- プレイヤー名:
GetPlayerName() - スコア:
GetScore()/SetScore() - Ping:
GetPingInMilliseconds() - プレイヤーID:
GetPlayerId() - ボット判定:
IsABot()
GameStateの PlayerArray にはPlayerStateのインスタンスが格納されています。サーバーや各クライアントはこの配列を通じて、接続中のすべてのプレイヤーの状態を参照できます。
情報フローの実例
3クラスがどのように連携するのかを、具体的なシナリオで見てみましょう。
プレイヤーが敵を倒してスコアを獲得する場合:
- プレイヤーのPawnが敵を倒す
- PlayerController(またはPawn)がサーバーに「敵を倒した」イベントを通知
- サーバー上の GameMode がルールに基づいて「スコアを加算する」処理を実行
- GameModeが該当プレイヤーの PlayerState を取得し、スコアを更新
- PlayerStateのスコア変数は
Replicated設定なので、サーバーでの変更が自動的に全クライアントに伝播 - 各クライアントのUIがPlayerStateの変更を検知し、スコア表示を更新
このように、GameModeがサーバーサイドでルールを執行し、その結果をGameState/PlayerStateを介して全クライアントに伝達するのが基本的な設計パターンです。
C++実装スケルトン
実際に3クラスをカスタムして使う場合の最小限の実装例を示します。
カスタムGameMode
// MyGameMode.h
#pragma once
#include "GameFramework/GameModeBase.h"
#include "MyGameMode.generated.h"
UCLASS()
class AMyGameMode : public AGameModeBase
{
GENERATED_BODY()
public:
AMyGameMode();
void OnPlayerScored(APlayerController* Scorer, int32 Points);
virtual void PostLogin(APlayerController* NewPlayer) override;
virtual void Logout(AController* Exiting) override;
};
// MyGameMode.cpp
#include "MyGameMode.h"
#include "MyGameState.h"
#include "MyPlayerState.h"
AMyGameMode::AMyGameMode()
{
// カスタムのGameStateとPlayerStateクラスを指定
GameStateClass = AMyGameState::StaticClass();
PlayerStateClass = AMyPlayerState::StaticClass();
}
void AMyGameMode::OnPlayerScored(APlayerController* Scorer, int32 Points)
{
if (AMyPlayerState* PS = Scorer->GetPlayerState<AMyPlayerState>())
{
PS->AddScore(Points);
}
}
コンストラクタで GameStateClass と PlayerStateClass を指定すると、UEが自動的にカスタムクラスのインスタンスを生成します。
カスタムGameState(レプリケーション変数付き)
// MyGameState.h
#pragma once
#include "GameFramework/GameStateBase.h"
#include "MyGameState.generated.h"
UCLASS()
class AMyGameState : public AGameStateBase
{
GENERATED_BODY()
public:
// Replicatedを付けた変数は全クライアントに自動同期される
UPROPERTY(Replicated, BlueprintReadOnly, Category = "Score")
int32 TeamAScore = 0;
UPROPERTY(Replicated, BlueprintReadOnly, Category = "Score")
int32 TeamBScore = 0;
virtual void GetLifetimeReplicatedProps(
TArray<FLifetimeProperty>& OutLifetimeProps) const override;
};
// MyGameState.cpp
#include "MyGameState.h"
#include "Net/UnrealNetwork.h"
void AMyGameState::GetLifetimeReplicatedProps(
TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// DOREPLIFETIMEマクロで複製する変数を登録
DOREPLIFETIME(AMyGameState, TeamAScore);
DOREPLIFETIME(AMyGameState, TeamBScore);
}
UPROPERTY(Replicated) でマークした変数は、GetLifetimeReplicatedProps で DOREPLIFETIME マクロを使って登録する必要があります。Net/UnrealNetwork.h のインクルードも忘れずに。
ヒント:
DOREPLIFETIME_CONDITIONを使うと、条件付きレプリケーション(オーナーのみ、初回のみなど)を設定できます。帯域幅の節約に有効です。
カスタムPlayerState
// MyPlayerState.h
#pragma once
#include "GameFramework/PlayerState.h"
#include "MyPlayerState.generated.h"
UCLASS()
class AMyPlayerState : public APlayerState
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = "Score")
void AddScore(int32 Points);
UPROPERTY(Replicated, BlueprintReadOnly, Category = "Score")
int32 KillCount = 0;
virtual void GetLifetimeReplicatedProps(
TArray<FLifetimeProperty>& OutLifetimeProps) const override;
};
// MyPlayerState.cpp
#include "MyPlayerState.h"
#include "Net/UnrealNetwork.h"
void AMyPlayerState::AddScore(int32 Points)
{
// APlayerStateの組み込みメソッドでスコアを更新
SetScore(GetScore() + Points);
KillCount++;
}
void AMyPlayerState::GetLifetimeReplicatedProps(
TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(AMyPlayerState, KillCount);
}
APlayerState には SetScore() / GetScore() が組み込まれています。カスタムの変数(ここでは KillCount)を追加する場合は、同じく Replicated + DOREPLIFETIME のパターンで登録します。
RepNotify: 変数の変更をクライアント側で検知してUIを更新したい場合は、
UPROPERTY(ReplicatedUsing=OnRep_KillCount)とUFUNCTION() void OnRep_KillCount()を組み合わせます。サーバーで値が変更されると、クライアント側でOnRep_コールバックが自動的に呼ばれるため、スコア表示の更新処理をここに書くのが定石です。
シングルプレイヤーの場合: マルチプレイヤーを想定しないゲームでは、GameState/PlayerStateを省略してGameModeに多くのロジックを寄せる簡略パターンも有効です。ただし、将来マルチプレイヤーに拡張する可能性がある場合は、最初から3クラスに分離しておくと移行コストが大幅に下がります。
まとめ
- GameMode = ルールブック。サーバーのみに存在し、クライアントには複製されない
- GameState = スコアボード。ゲーム全体の状態を全クライアントに同期
- PlayerState = 個人の成績表。プレイヤーごとの情報を全クライアントに同期
- GameModeのコンストラクタで
GameStateClass/PlayerStateClassを指定してカスタムクラスを使う UPROPERTY(Replicated)+DOREPLIFETIMEマクロでレプリケーション変数を登録