虚幻引擎关卡相关的问题

关卡的默认游戏模式

虚幻引擎有一套自动寻找游戏模式的"兜底"机制

简单来说,当一个关卡(Level)被打开时,它必须有一个游戏模式。如果你没有在 OpenLevel 里指定,也没有在"世界场景设置"里指定,它就会去翻**项目设置(Project Settings)**里的账本。

1. 项目全局默认设置

  • 检查方法: 点击菜单栏 编辑 (Edit) -> 项目设置 (Project Settings) -> 地图和模式 (Maps & Modes)。

  • 看这里: 找到 Default Game Mode。

  • 原理: 只要你在这里选了你的那个类,那么全项目所有的关卡(只要没有手动在"世界场景设置"里改为别的模式),都会默认运行这个类。

2. 理解 OpenLevel 的参数逻辑

cpp 复制代码
UGameplayStatics::OpenLevel(
    GetWorld(), 
    TEXT("SinglePlayerLevel_1"), 
    true, 
    TEXT("Players=1")
);
  • TEXT("SinglePlayerLevel_1"):告诉引擎"去哪"。

  • TEXT("Players=1") :这只是一个"备注信息"(Options String)。它不会决定使用哪个游戏模式,它只是给已经运行的游戏模式发了一个短消息。

  • 你的游戏模式在启动后,会去读取这个 Players=1。即使你没传模式,引擎已经根据第 1 点或第 2 点加载了你的模式类,所以它能读到消息并生成角色。

如果你想在代码里动态指定游戏模式怎么办?

如果你想在点击"单人模式"时,强制指定一个特定的游戏模式类(而不是靠项目默认设置),你需要使用 ?game= 前缀:

cpp 复制代码
// 假设你的 C++ 游戏模式类在引擎里的路径是这个
// 注意:路径通常是 /Game/路径/类名.类名_C
UGameplayStatics::OpenLevel(
    GetWorld(), 
    TEXT("SinglePlayerLevel_1"), 
    true, 
    TEXT("game=/Script/BattleBlaster.SinglePlayerGameMode?Players=1")
);

总结:

你的代码能跑通,是因为:

  1. 引擎自动加载了默认模式:你的项目设置里已经把那个写了生成逻辑的类设为默认了。

  2. 模式一直都在:GameMode 不是通过 OpenLevel 传过去的物体,它是关卡(Level)本身的"规则管理器"。只要关卡开了,模式就自动创建了。

  3. 参数读取成功:虽然你只传了 Players=1,但只要模式类被加载了,它就会从 OptionsString 变量里把这个 1 读出来执行逻辑。

验证实验:

你可以尝试把 项目设置 -> 地图和模式 -> Default Game Mode 改回引擎自带的 GameModeBase。你会发现,这时候再点单人模式,你的坦克(角色)就不再生成了。

问题1:

OpenLevel()函数的功能和作用?

1. 函数签名(C++)

cpp 复制代码
static void OpenLevel(
    const UObject* WorldContextObject, 
    FName LevelName, 
    bool bAbsolute = true, 
    FString Options = TEXT("")
);
参数详解:
  1. WorldContextObject: 上下文对象。通常传入 this 或 GetWorld()。引擎需要通过它找到当前的执行环境。

  2. LevelName : 目标关卡的资产名称(不需要加 .umap 后缀)。例如关卡文件叫 L_BattleCity.umap,这里只需填 "L_BattleCity"。

  3. bAbsolute:

    • true (默认):绝对路径。这是最常用的。

    • false:相对路径。在现代开发中极少用到。

  4. Options (关键参数): 一个字符串,用于向新关卡传递配置信息。

2. Options 字符串:隐藏的"黑科技"

这是解决你之前"模式混乱"问题的终极武器。Options 的格式类似于网页的 URL 查询参数:?Key1=Value1?Key2=Value2。

(1) 强制指定 GameMode (?game=)

这是最强大的功能。无论目标关卡的"世界设置"里填了什么,你可以通过这个参数强行指定一个 GameMode 类。

  • 格式: ?game=/路径/蓝图名.蓝图名_C

  • 示例:

    cpp 复制代码
    FString MyOptions = 
    TEXT("?game=/Game/Blueprints/BP_SinglePlayerMode.BP_SinglePlayerMode_C");
    UGameplayStatics::OpenLevel(GetWorld(), "Level2", true, MyOptions);
(2) 开启监听服务器 (?listen)

如果你在做联机游戏,想让当前玩家作为主机(Host),必须加上这个。

  • 示例: UGameplayStatics::OpenLevel(GetWorld(), "Map_Lobby", true, "?listen");
(3) 传递自定义数据

你可以传递任何信息给新关卡的 GameMode(通过 GameMode::InitGame 接收)。

  • 示例: ?Difficulty=Hard?Weather=Rainy

  • 如何读取: 在 GameMode 中重写 InitGame:

    cpp 复制代码
    void AMyGameMode::InitGame(const FString& MapName, const FString& Options, FString& ErrorMessage) {
        Super::InitGame(MapName, Options, ErrorMessage);
        FString Diff = UGameplayStatics::ParseOption(Options, "Difficulty"); // 得到 "Hard"
    }

3. OpenLevel 执行时的生命周期(发生了什么?)

当你调用 OpenLevel 时,引擎会经历以下过程:

  1. 清理旧世界 (Cleanup):

    • 当前关卡的所有 Actor 会被 全部销毁 (Destroy)(包括玩家的坦克、子弹、GameMode 等)。

    • 唯一幸存者: GameInstance。它跨关卡存在,所以重要数据(如玩家分数、选定的模式枚举)必须存在这里。

  2. 加载新资产: 引擎从磁盘读取目标地图的数据。

  3. 初始化新世界:

    • 创建新的 GameMode(根据优先级:Options参数 > 世界设置 > 项目设置)。

    • GameMode 调用 InitGame 处理 Options 字符串。

    • GameMode 调用 PostLogin(创建 PlayerController)。

    • GameMode 调用 RestartPlayer(生成并 Possession 玩家的 Pawn)。

4. OpenLevel vs ServerTravel vs ClientTravel

这是很多初学者会混淆的概念:

  • OpenLevel: 最简单粗暴。通常用于单机游戏客户端自己跳地图。它会断开当前的服务器连接(如果有的话)。

  • ServerTravel (服务器跳转): 仅服务器调用。它会带着房间里所有的玩家一起跳到新地图。这是联机游戏(如吃鸡、开黑)关卡切换的正确方式。

  • ClientTravel: 客户端专用。通常用于客户端连接特定的服务器 IP(例如 ClientTravel("127.0.0.1", TRAVEL_Absolute))。

5. 常见陷阱与注意事项

  1. 同步加载导致卡顿:

    OpenLevel 是同步执行的。加载大地图时,主线程会阻塞,表现就是画面死掉(卡住)。

    • 解决方法: 如果地图很大,应使用 Seamless Travel(无缝旅游)或 Level Streaming(关卡流)。
  2. 大小写敏感: 虽然后台处理通常不敏感,但建议 LevelName 与资产名称完全一致,避免在某些打包平台(如 Linux)出现问题。

  3. 路径结尾的 _C:

    通过 Options 指定类(如 GameMode)时,蓝图类路径必须以 _C 结尾,否则引擎找不到该类。

问题2:

为什么我点击双人模式, 切换场景后, 我无法控制我的角色呢?

解决方案:

cpp 复制代码
//Tank.h
virtual void PossessedBy(AController* NewController) override;
//Tank.cpp
void ATank::PossessedBy(AController* NewController)
{
	Super::PossessedBy(NewController);
	// 1. 尝试获取本地玩家控制器
	APlayerController* PC = Cast<APlayerController>(NewController);
	if (PC && PC->IsLocalController())
	{
		// 2. 获取该玩家对应的 增强输入子系统
		ULocalPlayer* LocalPlayer = PC->GetLocalPlayer();
		if (LocalPlayer)
		{
			if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(LocalPlayer))
			{
				// 3. 关键:先清除旧的上下文(防止切换关卡残留),再添加新的
				Subsystem->ClearAllMappings();
				if (DefaultMappingContext)
				{
					Subsystem->AddMappingContext(DefaultMappingContext, MappingPriority);
				}
			}
		}
		// 4. 设置输入模式
		FInputModeGameOnly InputMode;
		PC->SetInputMode(InputMode);
		PC->bShowMouseCursor = false;
	}
}

这段代码能解决控制失效问题,是因为它在技术层面完成了以下四个关键操作:

1. 解决了指针有效性(执行时机)问题

在虚幻引擎的生命周期中,BeginPlay 执行时,Pawn 往往还没有被 Controller 控制。

  • 如果在 BeginPlay 中调用 GetController(),通常返回 nullptr,导致后续逻辑不执行。

  • PossessedBy 是引擎内置的回调函数 ,它在 Controller 正式控制 Pawn 的瞬间触发。此时 NewController 参数是一个绝对有效的指针,保证了逻辑能够 100% 运行。

2. 重新注册增强输入映射上下文(IMC)

UE5 的增强输入系统(Enhanced Input)是基于 LocalPlayerSubsystem 的。

  • 状态丢失: 当你切换关卡时,旧关卡的输入映射会被清除。虽然你的 Tank 存在,但系统不知道按键对应的 Action。

  • 重新绑定: Subsystem->AddMappingContext 将 DefaultMappingContext 资产重新注入到当前玩家的输入子系统中,建立了从硬件按键到游戏逻辑(IA)的映射关系。

3. 本地多玩家子系统的独立初始化

在双人模式下,玩家 0 和 1 拥有各自独立的 UEnhancedInputLocalPlayerSubsystem。

  • 这段代码运行在 ATank 类中,当玩家 1 的坦克被控制时,它为玩家 1 的子系统添加映射;当玩家 2 的坦克被控制时,它为玩家 2 的子系统添加映射。

  • 这确保了两个本地玩家都能获得独立的输入处理能力。

4. 强制切换输入模式(Input Mode)

由于你是从菜单(UI)点击进入游戏场景的,此时系统的输入焦点往往停留在 UI(Slate)层。

  • PC->SetInputMode(FInputModeGameOnly()) 强制修改了底层的输入路由。它告诉引擎:无视所有 UI 组件,将所有底层键盘/手柄信号直接发往 PlayerController 的 Input 处理逻辑

  • 如果不执行这一行,硬件输入可能会被残留的 UI 拦截,导致角色无法移动。

问题3:

关卡和其模式是强绑定的吗?如果关卡1选了模式1, 关卡2选了模式2, 关卡3没有选模式,我从关卡2切换到关卡1,会自动把模式切换成关卡1绑定的模式吗?切到关卡3会不会继续保持模式2?

简单直接的回答是:关卡和模式不是强绑定,而是"优先级绑定"。

虚幻引擎在切换关卡时,有一套非常严格的优先级逻辑来决定使用哪个 GameMode。

针对你的三个疑问,情况如下:

1. 从关卡 2 切换到关卡 1(有关卡覆盖的情况)

  • 结果: 会自动切换到模式 1。

  • 原理: 只要你在"世界设置(World Settings)"里设置了 GameMode Override,那么当你通过标准方式(如 OpenLevel 节点)进入该关卡时,引擎会检测到这个覆盖项,并强制实例化模式 1。

2. 从关卡 2 切换到关卡 3(关卡 3 没选模式的情况)

  • 结果: 不会保持模式 2,而是切换到"项目默认模式"。

  • 原理: GameMode 是不跨关卡的。 当你离开关卡 2 时,模式 2 会被完全销毁(Destroy)。进入关卡 3 时,引擎发现它没有"覆盖模式",就会去 项目设置(Project Settings)-> 映射和模式(Maps & Modes) 里找那个全项目通用的 Default GameMode。

  • 结论: 除非你的项目默认模式刚好就是模式 2,否则它会变回默认值。

3. 核心机制:虚幻是怎么选 GameMode 的?(优先级排序)

当你加载一个关卡时,引擎按以下顺序找 GameMode,谁先被找到就用谁

  1. URL 参数(最高优先级):

    如果你在切换关卡的代码里写了 ?game=/Path/To/BP_ModeX.BP_ModeX_C,引擎会无视所有设置,强行使用 ModeX。

  2. 世界设置(World Settings):

    就是你在关卡编辑器里手动填写的 GameMode Override。

  3. 项目设置(Project Settings):

    在 Maps & Modes 里设置的全局默认值。

4. 为什么你会有"关卡复用"的顾虑?

你可能担心:"如果我想在同一个关卡里,有时玩单人模式,有时玩双人模式,难道我要存两份一模一样的地图文件吗?"

完全不需要。 实现"关卡复用"的正确姿势有两条:

方案 A:通过代码动态指定(最灵活)

如果你想在关卡 3 里运行模式 2,你不需要在世界设置里改。你只需要在切换关卡时传参:

  • C++: UGameplayStatics::OpenLevel(GetWorld(), "Level_3", true, "game=/Game/Blueprints/Modes/BP_Mode2.BP_Mode2_C");

  • 蓝图: Open Level 节点的 Options 字符串填入 game=...(路径如上)。

方案 B:关卡组合(最专业)
  1. 把你的场景(地形、建筑、灯光)存成一个关卡,叫 L_Art_Main。

  2. 创建两个空关卡:L_SinglePlayer 和 L_MultiPlayer。

  3. 在 L_SinglePlayer 的世界设置里填单人模式,并在 Levels 窗口 里把 L_Art_Main 拖进去作为子关卡。

  4. 在 L_MultiPlayer 的世界设置里填双人模式,同样把 L_Art_Main 拖进去。

  • 结果: 你的美术资源只有一份(Level Streaming),但你有了两个入口,分别对应不同的模式。

问题4:

由于我的关卡切换相关的代码都写在了游戏实例类中, 导致我所有的游戏模式类都调用的是相同的关卡切换函数, 全部都乱了, 我是不是需要在游戏实例类中为不同的游戏模式类定义不同的关卡切换函数?

这是一个非常关键的架构问题。答案是不需要(也不建议)为每个模式定义不同的函数

如果为每个模式都写一个 LoadNextLevel_Single、LoadNextLevel_Multi,你的代码会变得极其难以维护(代码膨胀)。

出现"全乱了"的原因是因为你的 GameInstance 现在没有上下文意识。它只知道"加载下一关",但不知道"以什么身份(模式)加载下一关"。

解决方案:参数化与 URL 参数

正确的做法是:让 GameInstance 变成一个"服务者",由具体的 GameMode 告诉它要去哪里、用什么规则。

以下是分步改进方案:

1. 在 GameInstance 中定义一个通用的跳转函数

不要在 GameInstance 里硬编码跳转逻辑,而是接收参数。

cpp 复制代码
// BattleBlasterGameInstance.h
// 定义一个简单的枚举来区分模式
UENUM(BlueprintType)
enum class EGameModeType : uint8 {
    SinglePlayer,
    MultiPlayer
};

// ...
void LoadLevelWithMode(FName LevelName, EGameModeType ModeType);
cpp 复制代码
// BattleBlasterGameInstance.cpp
void UBattleBlasterGameInstance::LoadLevelWithMode(FName LevelName, EGameModeType ModeType)
{
    FString Options = "";
    
    // 根据传入的模式,构造 URL 参数
    if (ModeType == EGameModeType::SinglePlayer)
    {
        Options = TEXT("?game=/Game/Blueprints/Modes/BP_SinglePlayerGameMode.BP_SinglePlayerGameMode_C");
    }
    else if (ModeType == EGameModeType::MultiPlayer)
    {
        Options = TEXT("?game=/Game/Blueprints/Modes/BP_BattleBlasterGameMode.BP_BattleBlasterGameMode_C");
    }

    // 执行跳转
    UGameplayStatics::OpenLevel(GetWorld(), LevelName, true, Options);
}
2. 在不同的 GameMode 中调用时传入身份

在你的 SinglePlayerGameMode 里:

cpp 复制代码
// 胜利后加载下一关
GI->LoadLevelWithMode("Map_Level2", EGameModeType::SinglePlayer);

在你的 BattleBlasterGameMode 里:

cpp 复制代码
// 胜利后加载下一关
GI->LoadLevelWithMode("Map_Level2", EGameModeType::MultiPlayer);

为什么这样做能解决"乱了"的问题?

  1. 强制模式指定: 通过 URL 参数 ?game=...,你无视了地图(Level)原本在"世界设置"里填的 GameMode。这意味着你可以用同一张地图跑不同的模式。

  2. 职责明确:

    • GameMode 决定**"我是谁,我要去哪"**。

    • GameInstance 负责**"怎么去"**(处理 OpenLevel 的具体细节)。

  3. 防止"自动回滚": 之前你觉得乱,可能是因为进入关卡 2 后,它自动加载了关卡 2 默认的模式,而不是你从关卡 1 继承过来的模式。通过 Options 传参,你锁死了这个模式。

如果你的关卡是线性的(关卡 1 -> 关卡 2)

你可以让 GameInstance 存一个"当前是什么模式"的变量。

cpp 复制代码
// BattleBlasterGameInstance.h
EGameModeType CurrentMode; // 记录当前在玩单人还是双人

// 当用户在主菜单点击"单人模式"按钮时:
GI->CurrentMode = EGameModeType::SinglePlayer;
GI->LoadLevelWithMode("Map_Level1", EGameModeType::SinglePlayer);

然后在 LoadNextLevel 函数里,只需要读取这个 CurrentMode:

cpp 复制代码
void UBattleBlasterGameInstance::LoadNextLevel()
{
    // 假设你有一个获取下一关名字的逻辑
    FName NextLevelName = GetNextLevelName(); 
    
    // 自动带上当前的模式,这样单人永远是单人,双人永远是双人
    LoadLevelWithMode(NextLevelName, CurrentMode);
}

总结建议:

  • 不要写死函数: 游戏实例应该提供带参数的接口。

  • 利用 URL 参数: 使用 ?game=路径 是虚幻引擎实现"关卡复用"和"模式强制切换"的最强手段。

  • 保持状态: 在 GameInstance 里存一个枚举变量(Single 还是 Multi),这样在整个游戏流程中,你都能知道玩家当前处于哪种游戏状态。



相关推荐
郁闷的网纹蟒1 天前
虚幻5---第12部分---蒙太奇
开发语言·c++·ue5·游戏引擎·虚幻
DoomGT3 天前
Physics Simulation - Hit Event的触发机制
ue5·游戏引擎·虚幻·虚幻引擎·unreal engine
Howrun7774 天前
虚幻引擎_核心框架
游戏引擎·虚幻
速冻鱼Kiel9 天前
Lyra的相机系统
笔记·ue5·游戏引擎·虚幻
妙为10 天前
unreal engine5角色把敌人 “挤飞”
游戏引擎·虚幻·ue·unrealengine5
北冥没有鱼啊12 天前
UE5 离谱问题,角色动画不播放
游戏·ue5·ue4·游戏开发·虚幻
速冻鱼Kiel12 天前
GASP笔记03
笔记·ue5·游戏引擎·虚幻
Howrun77713 天前
虚幻引擎_创建组件
游戏引擎·虚幻
Howrun77716 天前
虚幻引擎_控制角色移动的三种方法
游戏引擎·虚幻