【UE4】 Simulate In Editor (SIE) 中にゲームのUIを非表示にする

2021年9月9日

更新情報

UIを元に戻す処理を Shutdown() 関数で呼び出していましたが、タイミングによっては GameViewPort のポインタが取得できずに動作しない事があったので、PIEの終了デリゲート内で呼び出す様に変更しました。

はじめに

Unreal Engine のエディターにはプレイ中のアクターの状態を確認したり、変更したりできるシミュレーション状態があります。(Unityで言うところのシーンビューの様なもの)

これは大変便利な機能なのですが、困ったことにUMGで表示したゲームのUIが画面に残ってしまう為、場合によっては作業に支障をきたします。

たとえが画面のど真ん中にUIが表示されているとき

イジェクトボタンまたは、F8キーを押してシミュレーション状態に移行させてみると

カメラをどんな角度に変更しても、UIはビューポートに描き込まれるので常に画面内に表示されてしまいます。

この例だと微妙ですが、もっと画面を覆い隠すUIだったりすると、確認の邪魔でストレスなので、シミュレーション中はゲームのUIを非表示にできないか検証してみました。

解決策

今回の解決方法は、私の知る限りでは C++での拡張が必要です。
この点はご了承ください。

シミュレーション状態(SIE)に切り替わったことを検知する

UIの表示切替を作る前に、そもそもシミュレーション状態になったかどうかを検知できなければ仕方ありません。

まずはそれを検知するイベント(デリゲート)を探してみたところ
FEditorDelegates クラスに OnSwitchBeginPIEAndSIE というそれらしいきものが見つかりました。

エンジン内での呼び出し個所を検索してみると、void UEditorEngine::ToggleBetweenPIEandSIE( bool bNewSession ) 関数で呼ばれているのでどうやらビンゴです。

デリゲートについての解説は割愛しますが、こんな感じでデリゲートの追加、削除ができます。
エディター専用コードになるので、WITH_EDITOR で囲っています。

FDelegateHandle handleSwitchPIEAndSIE_;

//デリゲートの登録
#if WITH_EDITOR
	/** delegate for a PIE event (begin, end, pause/resume, etc) (Params: bool bIsSimulating) */
	handleSwitchPIEAndSIE_ = FEditorDelegates::OnSwitchBeginPIEAndSIE.AddUObject( this, &ThisClass::OnSwitchPIEAndSIE );
#endif

//デリゲートの削除
#if WITH_EDITOR
	FEditorDelegates::OnSwitchBeginPIEAndSIE.Remove( handleSwitchPIEAndSIE_ );
#endif

UGameInstance を継承して自作クラスを作成する

問題はこのコードをどこに書くべきかですが、レベルに依存させたくないのでアクターから呼び出すのは避けたいです。

そこでレベルを跨いでも存在し続ける UGameInstance を継承した自作クラスを作成し、そこに書いてみます。

メニューの「ファイル / 新規C++ クラス」からC++クラスを作成

生成されたコードを開いて、UGameInstance の初期化と終了をオーバーライドします。
そこで先ほどのデリゲート処理を記述します。

デリゲートのbool型引数がポイントで trueの時=SIEに入った。falseの時=PIEに戻った。という意味の様です。

void UMyGameInstance::Init()
{
#if WITH_EDITOR
	/** delegate for a PIE event (begin, end, pause/resume, etc) (Params: bool bIsSimulating) */
	handleSwitchPIEAndSIE_ = FEditorDelegates::OnSwitchBeginPIEAndSIE.AddUObject(this, &ThisClass::OnSwitchPIEAndSIE);
#endif
}

void UMyGameInstance::Shutdown()
{
#if WITH_EDITOR
	FEditorDelegates::OnSwitchBeginPIEAndSIE.Remove(handleSwitchPIEAndSIE_);
#endif
}

void UMyGameInstance::OnSwitchPIEAndSIE(const bool bIsSimulating)
{
	//TODO:ここにゲームのUIを切り替えるコードを書く
}

ビルドする前準備

今回は エディター用の FEditorDelegatesクラスを使うので UnrealEd モジュールを有効化する必要があります。
これを有効にしないと、モジュールのリンクができずにビルドに失敗します。

また、今後のステップでUIのコアシステムである Slate モジュールも必要なので一緒に有効化してしまいましょう。

参考:インゲームでスレートを使用する
 このページの「プロジェクト設定」の項目で解説されています。

ビルドの設定ファイルを開く

以下の様に [ProjectDir]/[ProjectName]/Source/[ProjectName]/[ProjectName].build.cs ファイルを開きます。

以下の様に編集して UnrealEd と Slate を有効化しましょう。
Slateはコメントアウトされたコードが既に記述されているので、コメントインするだけでOKです。

// Fill out your copyright notice in the Description page of Project Settings.

using UnrealBuildTool;

public class PlatformShooter : ModuleRules
{
    public PlatformShooter(ReadOnlyTargetRules Target) : base(Target)
    {
        PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

        PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore" });

        PrivateDependencyModuleNames.AddRange(new string[] { });

        //
        // Slateを有効化
        //
        // Uncomment if you are using Slate UI
        PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });

        //
        // UnrealEdを有効化
        //
        if (Target.Type == TargetRules.TargetType.Editor)
        {
            PublicDependencyModuleNames.Add("UnrealEd");
        }

        // Uncomment if you are using online features
        // PrivateDependencyModuleNames.Add("OnlineSubsystem");

        // To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true
    }
}

これで一旦ビルドしてみましょう。

GameInstanceを自作クラスに差し替える

自作クラスを作った次のステップは、GameInstanceクラスの差し替えを行います。

プロジェクト設定を開き「プロジェクト / マップ&モード」で自作のGameInstanceクラスに変更します。

ゲームのUIの表示切替を実装する

これでSIEの切替が検知できる様になったので、次はゲームのUIを切り替えてみます。

UMGでAddViewportしたウィジェットは、Slate の SGameLayerManager 以下で管理される様なので、
このオブジェクトを取得して表示を切り替える関数を作成します。

void UMyGameInstance::SetGameLayerManagerVisibility(const UObject* WorldContextObject, bool bIsVisible)
{
	const UWorld* world = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull);
	if (world != nullptr && world->IsGameWorld())
	{
		const UGameViewportClient* gameViewportClient = world->GetGameViewport();
		if (gameViewportClient != nullptr)
		{
			auto gameLayerManager = static_cast<SGameLayerManager*>(gameViewportClient->GetGameLayerManager().Get());
			if (gameLayerManager)
			{
				const EVisibility newVisibility = bIsVisible ? EVisibility::SelfHitTestInvisible : EVisibility::Collapsed;
				gameLayerManager->SetVisibility(newVisibility);
			}
		}
	}
}

完成したコード

これで表示切替もできる様になったので、SIEの切替デリゲート内でこれを呼び出してあげれば完成です。
ただ、注意点としては PIE or SIE が終わった時にUIを表示状態に戻す必要がありますので、
今回は Shutdown 関数内で戻す様にしています。
今回は EndPIE デリゲートでUIを戻す様にしています。

MyGameInstance.h

// Fill out your copyright notice in the Description page of Project Settings.
#pragma once

#include "CoreMinimal.h"
#include "Engine/GameInstance.h"
#include "MyGameInstance.generated.h"

/**
 * 
 */
UCLASS()
class PLATFORMSHOOTER_API UMyGameInstance : public UGameInstance
{
	GENERATED_BODY()

public:

	// 初期化
	virtual void Init() override;

	// 終了
	virtual void Shutdown() override;

private:

	// ゲームのUIを表示切替
	void SetGameLayerManagerVisibility(const UObject* WorldContextObject, bool bIsVisible);
	
#if WITH_EDITOR

	// PIE to SIE の切替イベント
	UFUNCTION()
		void OnSwitchPIEAndSIE(const bool bIsSimulating);

	// PIE の終了
	UFUNCTION()
		void OnEndPIE(const bool bIsSimulating);

	// デリゲートハンドル
	FDelegateHandle handleSwitchPIEAndSIE_;
	FDelegateHandle handleEndPIE_;
	
#endif

};

MyGameInstance.cpp

// Fill out your copyright notice in the Description page of Project Settings.
#include "MyGameInstance.h"

#include "Editor.h"
#include "Slate/SGameLayerManager.h"

/**
 * @brief 初期化
 */
void UMyGameInstance::Init()
{
#if WITH_EDITOR

	//
	// デリゲートを登録する
	//
	handleSwitchPIEAndSIE_ = FEditorDelegates::OnSwitchBeginPIEAndSIE.AddUObject(this, &ThisClass::OnSwitchPIEAndSIE);
	handleEndPIE_ = FEditorDelegates::EndPIE.AddUObject(this, &ThisClass::OnEndPIE);
	
#endif
}

/**
 * @brief 終了
 */
void UMyGameInstance::Shutdown()
{
#if WITH_EDITOR
	//
	// デリゲートを抹消する
	//
	FEditorDelegates::OnSwitchBeginPIEAndSIE.Remove(handleSwitchPIEAndSIE_);
	FEditorDelegates::EndPIE.Remove(handleEndPIE_);
#endif
}

/**
 * @brief ゲームのUIを表示切替
 * @param WorldContextObject 
 * @param bIsVisible 
 */
void UMyGameInstance::SetGameLayerManagerVisibility(const UObject* WorldContextObject, bool bIsVisible)
{
	const UWorld* world = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull);
	if (world != nullptr && world->IsGameWorld())
	{
		const UGameViewportClient* gameViewportClient = world->GetGameViewport();
		if (gameViewportClient != nullptr)
		{
			auto gameLayerManager = static_cast<SGameLayerManager*>(gameViewportClient->GetGameLayerManager().Get());
			if (gameLayerManager)
			{
				const EVisibility newVisibility = bIsVisible ? EVisibility::SelfHitTestInvisible : EVisibility::Collapsed;
				gameLayerManager->SetVisibility(newVisibility);
			}
		}
	}
}


#if WITH_EDITOR

/**
 * @brief PIE to SIE の切替イベント
 * @param bIsSimulating 
 */
void UMyGameInstance::OnSwitchPIEAndSIE(const bool bIsSimulating)
{
	//
	// シミュレーション状態じゃなければUIを表示する
	//
	const bool bIsVisible = !bIsSimulating;
	SetGameLayerManagerVisibility(this, bIsVisible);
}

/**
 * @brief PIE の終了イベント
 * @param bIsSimulating
 */
void UMyGameInstance::OnEndPIE(const bool bIsSimulating)
{
	//
	// 次にプレイした時の為に表示状態に戻す
	//
	SetGameLayerManagerVisibility(this, true);
}

#endif

参考サイト