虚幻引擎_游戏暂停菜单

需求:

在虚幻引擎里用C++实现一个菜单, 在游戏中按esc就可弹出, 并且会立即暂停游戏,这是一个双人游戏本地分屏游戏, 我希望在屏幕中间显示这个, 而不是在任意一边屏幕显示;

有三个按钮:重新开始;返回主菜单;返回游戏

解决方案:

这是一个非常经典的需求。在虚幻引擎(Unreal Engine)中实现这个功能,最好的架构是:使用 C++ 处理逻辑(按钮点击、暂停、关卡切换),使用 UMG (Widget Blueprint) 处理视觉布局

针对你的"双人分屏"且要求"菜单居中显示在整个屏幕上(而不是某个玩家的半个屏幕)"的需求,核心技巧在于使用 AddToViewport() 而不是 AddToPlayerScreen()。

以下是完整的实现步骤:

第一步:创建 C++ Widget 类

首先,我们需要创建一个继承自 UUserWidget 的 C++ 类,用来处理三个按钮的逻辑。

  1. 在编辑器中新建 C++ 类,父类选择 UserWidget,命名为 MyPauseMenuWidget。

MyPauseMenuWidget.h

cpp 复制代码
#pragma once

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "MyPauseMenuWidget.generated.h"

/**
 * 暂停菜单 Widget 类
 */
UCLASS()
class YOURGAME_API UMyPauseMenuWidget : public UUserWidget
{
	GENERATED_BODY()

public:
	// 初始化函数,用于绑定按钮事件
	virtual bool Initialize() override;

	// 提供一个设置所属 PlayerController 的方法,方便后续操作
	void Setup();
	
	// 关闭菜单逻辑
	void Teardown();

protected:
	// 使用 meta = (BindWidget) 将 C++ 变量与 UMG 中的按钮自动绑定
	// 注意:UMG 里的按钮名字必须和这里变量名完全一致
	UPROPERTY(meta = (BindWidget))
	class UButton* Btn_Resume; // 返回游戏

	UPROPERTY(meta = (BindWidget))
	class UButton* Btn_Restart; // 重新开始

	UPROPERTY(meta = (BindWidget))
	class UButton* Btn_MainMenu; // 返回主菜单

private:
	// 按钮点击回调函数
	UFUNCTION()
	void OnResumeClicked();

	UFUNCTION()
	void OnRestartClicked();

	UFUNCTION()
	void OnMainMenuClicked();
};

MyPauseMenuWidget.cpp

cpp 复制代码
#include "MyPauseMenuWidget.h"
#include "Components/Button.h"
#include "Kismet/GameplayStatics.h"

bool UMyPauseMenuWidget::Initialize()
{
	if (!Super::Initialize()) return false;

	// 绑定点击事件
	if (Btn_Resume)
	{
		Btn_Resume->OnClicked.AddDynamic(this, &UMyPauseMenuWidget::OnResumeClicked);
	}
	if (Btn_Restart)
	{
		Btn_Restart->OnClicked.AddDynamic(this, &UMyPauseMenuWidget::OnRestartClicked);
	}
	if (Btn_MainMenu)
	{
		Btn_MainMenu->OnClicked.AddDynamic(this, &UMyPauseMenuWidget::OnMainMenuClicked);
	}

	return true;
}

void UMyPauseMenuWidget::Setup()
{
	// 1. 添加到视口
	// 关键点:使用 AddToViewport() 会覆盖整个游戏窗口(跨越分屏)
	// 如果使用 AddToPlayerScreen() 则只会显示在玩家1的半个屏幕里
	this->AddToViewport(9999); // ZOrder设高一点确保在最上层

	// 2. 设置输入模式为 UI Only,这样鼠标可以点击,且键盘操作不会传给角色
	APlayerController* PC = GetOwningPlayer();
	if (PC)
	{
		FInputModeUIOnly InputData;
		InputData.SetWidgetToFocus(this->TakeWidget());
		InputData.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
		PC->SetInputMode(InputData);
		PC->SetShowMouseCursor(true);
		
		// 3. 暂停游戏
		UGameplayStatics::SetGamePaused(GetWorld(), true);
	}
}

void UMyPauseMenuWidget::Teardown()
{
	// 1. 移除 UI
	this->RemoveFromParent();

	// 2. 恢复输入模式
	APlayerController* PC = GetOwningPlayer();
	if (PC)
	{
		FInputModeGameOnly InputData;
		PC->SetInputMode(InputData);
		PC->SetShowMouseCursor(false);

		// 3. 恢复游戏
		UGameplayStatics::SetGamePaused(GetWorld(), false);
	}
}

void UMyPauseMenuWidget::OnResumeClicked()
{
	Teardown(); // 执行关闭菜单逻辑即可
}

void UMyPauseMenuWidget::OnRestartClicked()
{
	// 恢复时间(防止重开后游戏还是暂停状态)
	Teardown(); 

	// 获取当前关卡名称并重新加载
	FString CurrentLevelName = UGameplayStatics::GetCurrentLevelName(this);
	UGameplayStatics::OpenLevel(this, FName(*CurrentLevelName));
}

void UMyPauseMenuWidget::OnMainMenuClicked()
{
	Teardown();
	
	// 填写你的主菜单关卡名字,例如 "MainMenuMap"
	UGameplayStatics::OpenLevel(this, FName("MainMenuMap"));
}

第二步:在 PlayerController 中触发菜单

通常我们只允许 玩家1 (Player 0) 按 ESC 暂停游戏。

  1. 打开你的 PlayerController C++ 类(如果没有自定义的,请创建一个)。

  2. 确保你在项目设置 (Project Settings) -> Input 中绑定了 Action Mapping,名字叫 PauseGame,按键设为 Escape。

MyPlayerController.h

cpp 复制代码
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "MyPauseMenuWidget.h" // 引用上面的头文件
#include "MyPlayerController.generated.h"

UCLASS()
class YOURGAME_API AMyPlayerController : public APlayerController
{
	GENERATED_BODY()

protected:
	virtual void SetupInputComponent() override;

	// 触发暂停函数
	void TogglePauseMenu();

public:
	// 在编辑器里指定我们做好的 WBP
	UPROPERTY(EditAnywhere, Category = "UI")
	TSubclassOf<UMyPauseMenuWidget> PauseMenuClass;

private:
	// 保存实例化的 Widget 指针
	UPROPERTY()
	UMyPauseMenuWidget* PauseMenuInstance;
};

MyPlayerController.cpp

cpp 复制代码
#include "MyPlayerController.h"
#include "Kismet/GameplayStatics.h"

void AMyPlayerController::SetupInputComponent()
{
	Super::SetupInputComponent();

	// 绑定 ESC 键
	InputComponent->BindAction("PauseGame", IE_Pressed, this, &AMyPlayerController::TogglePauseMenu);
}

void AMyPlayerController::TogglePauseMenu()
{
    // 如果是分屏游戏,通常只允许主玩家(Player 0)控制暂停
    // IsLocalController() 检查是否是本地玩家
    // UGameplayStatics::GetPlayerControllerID 检查是否是 ID 0
	if (IsLocalController() && UGameplayStatics::GetPlayerControllerID(this) == 0)
	{
		if (PauseMenuClass)
		{
			// 如果菜单还没创建,就创建一个
			if (!PauseMenuInstance)
			{
				PauseMenuInstance = CreateWidget<UMyPauseMenuWidget>(this, PauseMenuClass);
			}

			// 如果菜单已经在显示(IsInViewport),就关闭它;否则就打开它
			if (PauseMenuInstance->IsInViewport())
			{
				PauseMenuInstance->Teardown(); // 再次按 ESC 返回游戏
			}
			else
			{
				PauseMenuInstance->Setup(); // 显示并暂停
			}
		}
	}
}

第三步:制作 UMG 界面 (Widget Blueprint)

这是视觉部分,对应你提供的图片。

  1. 在 Unreal 编辑器中,右键 -> User Interface -> Widget Blueprint

  2. 命名为 WBP_PauseMenu。

  3. 打开它,点击右上角的 Graph,点击 Class Settings(类设置)。

  4. 在 Details 面板的 Parent Class 中,搜索并选择我们刚写的 C++ 类:MyPauseMenuWidget。

  5. 回到 Designer 视图:

    • 删除默认的 Canvas Panel 或者保留它。

    • 拖入一个 Border (或者 Image) 到屏幕中间,这就是你的菜单背景框。

    • 重要: 选中这个 Border,在 Details 面板的 Anchors (锚点) 选择正中间那个(Center)。将 Position X/Y 设为 0,Alignment X/Y 设为 0.5,这样它就会完美居中。

    • 在 Border 里面放一个 Vertical Box。

    • 在 Vertical Box 里拖入 3 个 Button。

    • 关键步骤: 将这 3 个 Button 的名称(在左侧 Hierarchy 面板中重命名)必须 改为:

      1. Btn_Restart

      2. Btn_MainMenu

      3. Btn_Resume

        (如果名字不匹配,C++代码里的 BindWidget 会报错或无法绑定)。

    • 在每个 Button 里面拖入 Text 控件,分别修改文字为 "重新开始"、"返回主菜单"、"返回游戏"。

第四步:配置游戏模式

最后一步是把这些连起来。

  1. 创建一个自定义的 GameMode Base (如果还没有)。

  2. 在 GameMode 的设置里,将 Player Controller Class 选为你刚才写的 MyPlayerController (或者基于它的蓝图)。

  3. 打开你的 MyPlayerController 的蓝图子类(如果没有,右键 C++ MyPlayerController 创建一个蓝图类)。

  4. 在蓝图的 Details 面板里找到 UI 分类下的 Pause Menu Class,选择你刚才做的 WBP_PauseMenu。

总结要点

  1. C++ 类 UserWidget:处理核心逻辑(Resume/Restart/LevelLoad)。

  2. BindWidget:这是连接 C++ 逻辑和 UMG 视觉的桥梁。

  3. PlayerController:处理输入(ESC键)。

  4. AddToViewport vs AddToPlayerScreen

    • AddToPlayerScreen 会把 UI 限制在分屏的那个小格子里。

    • AddToViewport 会把 UI 放在整个显示器屏幕上(无视分屏边界),这就是你想要的效果。

  5. SetGamePaused:确保游戏逻辑停止。

问题1:

为什么不把按键ESC触发菜单的逻辑写在Tank玩家类里?

这是一个非常好的架构问题。在虚幻引擎中,确实可以在 Pawn(坦克类)里写 ESC 逻辑,但将系统级按键(如暂停、菜单)写在 PlayerController 里是行业标准做法

主要原因有以下几点,其中第一点对你的坦克游戏尤为致命

1. "坦克炸了怎么办?"(生存周期问题)

这是最核心的原因。

  • Pawn (坦克) 是肉体。当坦克被摧毁(Destroyed)或者重生时,这个 C++ 对象通常会被销毁并在内存中清除。

  • PlayerController 是灵魂/大脑。无论坦克死多少次,PlayerController 一直存在,直到玩家退出关卡。

如果你把 ESC 逻辑写在 Tank 类里:

当玩家的坦克被打爆了,还没重生的时候,玩家想按 ESC 退出游戏或者重新开始------但他做不到。因为坦克对象没了,依附在上面的代码也就无法执行了。玩家会陷入"死机"状态,必须强制关闭窗口。

如果你写在 PlayerController 里:

即使坦克炸了,PlayerController 依然活着,玩家依然可以按 ESC 呼出菜单点击"重新开始"。

2. 逻辑分层(关注点分离)

  • Tank 类 (Pawn) 应该只关心和游戏世界内的交互:比如开火、移动、炮塔旋转、受到伤害。这是"角色行为"。

  • PlayerController 类 应该关心玩家与系统的交互:比如打开菜单、调整设置、鼠标显示隐藏、切换输入模式。这是"系统行为"。

让坦克去管理"UI显示"是不符合面向对象设计的。坦克只是一辆车,它不应该知道"主菜单"是什么。

3. 附身与载具切换 (Possession)

假设你的游戏以后加了一个功能:坦克坏了,驾驶员可以跳出来跑,或者驾驶员可以切换到一架无人机上。

  • 如果你把 ESC 写在坦克里,当你跳出坦克变成"驾驶员Pawn"时,ESC 键就失效了(除非你在驾驶员类里再写一遍)。

  • 如果你写在 PlayerController 里,无论你控制的是坦克、步兵还是无人机,ESC 键永远有效,因为控制器没变。

4. UI 与 输入模式的归属权

在代码中你需要调用 SetInputMode 和 SetShowMouseCursor。

这些函数本身就是 PlayerController 的成员函数。

  • 如果在 PlayerController 里写,直接 this->SetShowMouseCursor(true) 很顺手。

  • 如果在 Tank 里写,你还得先 GetController(),强转成 PlayerController,然后再调用。这从代码结构上就暗示了这些功能本来就不属于 Tank。

相关推荐
Howrun7771 天前
虚幻引擎_C++_游戏开始菜单
游戏·游戏引擎·虚幻
速冻鱼Kiel1 天前
虚幻状态树解析
ue5·游戏引擎·虚幻
Howrun7771 天前
虚幻引擎关卡相关的问题
虚幻
郁闷的网纹蟒2 天前
虚幻5---第12部分---蒙太奇
开发语言·c++·ue5·游戏引擎·虚幻
DoomGT4 天前
Physics Simulation - Hit Event的触发机制
ue5·游戏引擎·虚幻·虚幻引擎·unreal engine
Howrun7775 天前
虚幻引擎_核心框架
游戏引擎·虚幻
速冻鱼Kiel10 天前
Lyra的相机系统
笔记·ue5·游戏引擎·虚幻
妙为11 天前
unreal engine5角色把敌人 “挤飞”
游戏引擎·虚幻·ue·unrealengine5
北冥没有鱼啊13 天前
UE5 离谱问题,角色动画不播放
游戏·ue5·ue4·游戏开发·虚幻