[UE4]マルチプレイクライアントからGameModeにアクセスする(&CDOを簡単に紹介)

#はじめに！

UE4の標準的なネットワークゲームではクライアント上ではGameModeはインスタンス化されず、アクセスできません！

※なぜクライアント上で作られないかは、クライアント上で作られるかどうかを示す *NetLoadOnClient* フラグが落ちているからです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/464248/2fbc38db-a371-c0b6-06f7-8d4625949206.png)

そのため、例えばGameModeにフラグなどを設定してあった場合、
GameStateなどにコピーしてレプリケート・・・なんてことをしてみたりすることもあると思います。

実はこれはちょっともったいないです。なぜなら **クライアントからGameModeにアクセスする事** は可能だからです。
<font color="Gray">(ただし完全なサーバー上にあるGameModeが通信されて複製されているものではありません→後述)</font>

#まずソース

ゲームモードのヘッダファイル

```c++
#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "TPSrc424GameModeGameMode.generated.h"

UENUM(BlueprintType)
enum class EGameRuleType : uint8
{
	Default,
	CaptureTheFlag,
	TeamDeathmatch,
	PvE
};

USTRUCT(BlueprintType)
struct FGameRule
{
	GENERATED_USTRUCT_BODY();

	UPROPERTY( EditDefaultsOnly, BlueprintReadOnly, Category="GameRule")
	EGameRuleType	RuleType;

	UPROPERTY( EditDefaultsOnly, BlueprintReadOnly, Category="GameRule")
	bool	bAllowOpeningStorage;

	UPROPERTY( EditDefaultsOnly, BlueprintReadOnly, Category="GameRule")
	bool	bDisableToObtainExp;
};


UCLASS(minimalapi)
class ATPSrc424GameModeGameMode : public AGameModeBase
{
	GENERATED_BODY()

public:
	ATPSrc424GameModeGameMode();

	static const ATPSrc424GameModeGameMode* GetGameModeCDO( const UObject* WorldContextObject );

	UFUNCTION( BlueprintCallable, Category="GameRule", meta = (WorldContext = "WorldContextObject"))
	static const FGameRule& GetGameRule( const UObject* WorldContextObject );
private:
	UPROPERTY( EditDefaultsOnly, Category="GameRule")
	FGameRule	Rule;
};
```

ゲームモードのソースファイル

```c++
// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved.

#include "TPSrc424GameModeGameMode.h"
#include "TPSrc424GameModeCharacter.h"
#include "UObject/ConstructorHelpers.h"

DEFINE_LOG_CATEGORY_STATIC(LogMyGameMode, Log, All);

ATPSrc424GameModeGameMode::ATPSrc424GameModeGameMode()
{
	// set default pawn class to our Blueprinted character
	static ConstructorHelpers::FClassFinder<APawn> PlayerPawnBPClass(TEXT("/Game/ThirdPersonCPP/Blueprints/ThirdPersonCharacter"));
	if (PlayerPawnBPClass.Class != NULL)
	{
		DefaultPawnClass = PlayerPawnBPClass.Class;
	}
}

const ATPSrc424GameModeGameMode* ATPSrc424GameModeGameMode::GetGameModeCDO( const UObject* WorldContextObject )
{
	const UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::Assert );
	const AWorldSettings* WorldSetting = World->GetWorldSettings(true, true);
	const AGameModeBase* GameModeBase = WorldSetting->DefaultGameMode.GetDefaultObject();
	const ATPSrc424GameModeGameMode* MyGameMode = Cast<const ATPSrc424GameModeGameMode>( GameModeBase );
	if (MyGameMode)
	{
		UE_LOG(LogMyGameMode, Log, TEXT("Name:%s"), *MyGameMode->GetName() );
	}
	return MyGameMode;
}
const FGameRule& ATPSrc424GameModeGameMode::GetGameRule( const UObject* WorldContextObject )
{
	const ATPSrc424GameModeGameMode* GameMode = GetGameModeCDO( WorldContextObject );
	if( GameMode == nullptr )
	{
		static FGameRule Dummy;
		return Dummy;
	}
	return GameMode->Rule;
}
```
ここまで。

**重要なのはGetGameModeCDOです。**
名前が示す通り、この関数でワールドに設定された**WorldSetting**を経由して、GameModeクラスの**ClassDefaultObject**を獲得しています。
CDOはアセットから取り出した素の状態を表していて、 **サーバー上にあるGameModeが通信されて複製されたものではありません。**

ClassDefaultObject(CDO)とは↓
[ClassDefaultObject＠docs.unrealengine.com](https://docs.unrealengine.com/ja/Programming/UnrealArchitecture/Objects/index.html)
この記事の後ろで少しCDOについて補足しています。

#使ってみよう！

##ThirdPersonCharacterのBlueprint
こんな感じで、おもむろにGetGameRule関数を読んでみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/464248/003ad65c-ff84-7544-d550-342f68b0bbc3.png)

##BP_GameMode
1.c++で作ったGameModeを継承して、ブループリントを生成
2.ClassDefaults中のGameRuleパラメータをいじる
3.WorldSettingに作ったGameModeを設定する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/464248/d901824e-c703-9fde-c57d-e8c1e8379cd0.png)

#いざ実食
PIEでNumberOfPlayersを4に設定して・・・Play！
全てのサーバー/クライアント上でBP_GameModeに設定されているパラメータを取得できています！！！
加えて、アクセスしているGameModeは *Default_\_BPGameMode\_C* という名前を持っていて、ClassDefaultObjectであることがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/464248/60a31324-82d8-fb85-2b77-a1707fb523c4.png)

上手く取り出すことができました！

#ClassDefaultObjectについて補足

ClassDefaultObject(CDO)はアクターやUObjectのデフォルトの状態をメモリ上に再現したものです。
例えばBlueprintがuassetファイルから読みだされたとき、そのファイルに記載された情報からメモリ上に原本となる状態をメモリ上に構築します。
NewObjectやSpawnActorによってそれらがワールドに生まれる時、このCDOを **複製して** ワールドに出現します。
次はその図解です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/464248/061beebb-1242-4856-fda3-7c52cf4117e7.png)

CDOは誰からも不要になってメモリから削除されるまで常にメモリ上にあるため、
これにアクセスしてデフォルト値を取り出すことができます。
例としてキャラクター毎に固有のMagicPointパラメータをクラスのパラメータに持っておいて、エナジードリンクを飲んだらMagicPointが全快するという仕様があるとすると以下のように実装可能です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/464248/e8ee2f28-fa85-2dd1-831c-4d851d1ed20d.png)

どのプロパティを出力するかはノードの詳細パネルから変更可能です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/464248/1e96021f-5e82-6a01-15dc-5388831a71ad.png)

BeginPlayで `Max Magic Point` プロパティに保存とかしなくても大丈夫ですよ！


#まとめ

GameModeはクライアント上で見えないように見えますが、ClassDefaultObjectだけはWorldSettingからアクセス可能です！
ClassDefaultObjectは一見、普通に生成されたActorと同じようにアクセス可能な型にみえますが、これは静的なデータで、
Worldにアクターとしてインスタンス化されているわけではありません。
「ショーケースに入った見本のようなもの」をイメージしてください。
<font color="red">**くれぐれもCDOを直接書き換えたりしないようにご注意ください！**</font>