本系列笔记将会对梁迪老师的《UE4全反射零耦合框架开发坦克大战》教程进行个人的知识点梳理与总结,此课程可以在腾讯课堂上…额嗯,只能在网页端搜索到,并且已经超过报名截止时间了,无法通过正常方法观看。
不过有个神奇的方法就是通过手机浏览器(不同浏览器的效果有差别,有的会直接要求你登录,遇到这样的就换一个;还有可能点开网页会发现没有播放按钮,遇到这样的就换一个网页)搜索该课程后可以在课程预览界面观看,也可以在目录进行跳转,不过没有字幕。建议是在 PC 端的手机模拟器观看。
如果到了一些比较靠下的集数它会被登录按钮挡住,这时候需要到 PC 端的网页进入该课程页面,然后点开对应的集数,在 URL 地址
(举例:https://m.ke.qq.com/course/415159/3341342822782391?course_id=415159&taid=3616581506979255#term_id=100495286
)
上找到类似 3341342822782391
的这段数字并复制到手机模拟器的浏览器网页 URL 替换掉对应的数字就可以了。(希望官方不要修复这个小技巧,毕竟对于一些已经截止报名的免费课程,不能正常学习实在是太可惜了)
笔者用的引擎版本是 4.26.2,老师推荐的引擎版本是 4.19,不同的版本可能在代码上有所区别,笔者会通过注释标明。
本系列文章不允许转载。 希望 CSDN 不会让我失望吧,不然我自己的学习笔记也要遵守诺言,不能转到其他平台上了。
梁迪老师的这个课程相较于前一个沙盒游戏课程会更加要求学习者对 C++ 有一定的熟练度,并且也会更深入地探讨 UE4 C++ 实际开发过程中用到的语法与技术。本系列笔记可供读者学习后用于复习回顾或参考代码来解决一些敲错了代码导致的 Bug。并且笔者只会贴出对应集数修改的代码内容,已经有了的部分代码基本都不会贴出来,以免笔记篇幅过长。
这里是已经学完本课程的未来的笔者 : ) 这个课程并不会教你如何开发《坦克大战》游戏,只是讲解了梁迪老师的 DataDriven 框架,不过这个框架确实非常有用,有没有坦克大战已经不重要了 XD
教程主要分为三步:
第一步包括内容有:
第二步包括内容如下:
第三步则是运用上面的框架来开发坦克大战游戏,并且会打包出成品来验证这个框架是可行且实用的。
框架大体如下:
举个例子,倘若 UI 模组的 OptionWidget 要访问玩家模组的 Character,如果直接在 OptionWidget 的头文件包含 Character,这样的话耦合度太高。如果玩家模组有其他更多需要访问的对象,则要么一个个引入,要么直接引入整个玩家模组,前者比较麻烦,后者则容易引入不需要的对象。
如果按上图的绿色箭头来访问,虽然访问路线长了,但是耦合度大辐降低,也可以尽量地避免上面的问题。
对于单个模组:
值得一提的是消息模组库能够缩短树状框架中类似绿色箭头的访问流程,虽然会增加一点耦合度,但是也是值得的。
将老师提供的项目文件压缩包 FrameCourse.rar 下载下来并解压。如果版本不同的话则直接用自己的引擎新建一个同名的 C++ 项目,然后把老师准备的项目里面的 Resource 文件夹复制到新建项目的 Content 文件夹就好了。
打开项目,Content 目录下新建一个 Map 文件夹,保存当前打开的默认关卡到 Map 里面,取名 DefaultMap;然后再将它复制一份,取名 Lesson_I。
在项目设置里将开始地图和默认地图都设置为 Lesson_I。
在路径 Public/GameFrame 下创建如下 C++ 文件,都是 UE4 的 Gameplay 的一部分:
新建一个 GameModeBase 类,取名 FWGameMode,作为游戏模式。
新建一个 PlayerController 类,取名 FWPlayerController,作为玩家控制器。
新建一个 Character 类,取名 FWCharacter,作为角色。
新建一个 GameInstance 类,取名 FWGameInstance,作为游戏实例。
在 Content 目录下创建一个名为 Blueprint 的文件夹,再到里面创建一个叫 GameFrame 的文件夹,在里面分别创建 4 个以上面创建的 C++ 类为基类的蓝图文件,取名规则为在原名后面加上 “_BP”。
在项目设置里将 FWGameInstance_BP 设置为默认的 GameInstance。
随后在路径 Public/Common 下新建一个 C++ 的 Object 类,取名 FWCommon,里面会放置一些公用的方法。
在公共类里面添加一个命名空间,并且添加 Debug 方法。
FWCommon.h
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "Engine/GameEngine.h" // 引入头文件
#include "FWCommon.generated.h"
namespace FWHelper
{
FORCEINLINE void Debug(FString Message, float Duration)
{
if (GEngine) GEngine->AddOnScreenDebugMessage(-1, Duration, FColor::Yellow, Message);
}
}
UCLASS()
class FRAMECOURSE_API UFWCommon : public UObject
{
GENERATED_BODY()
};
在 FWCharacter_BP 里面创建如图 4 个变量,分别为红框里面的类型。
ParamOne 的类型是硬连接的引用,要为其赋值的时候,它就已经存在于内存里面了。
ParamTwo 是对象的类的类型的引用,也是一种硬连接。
ParamThree 是软连接的引用,它只有在用到的时候才会加载到内存。
ParamFour 也是软连接的引用,不过目标是对象的类的类型。
接下来在角色类里面添加这 4 种类型的变量的 C++ 版本。
FWCharacter.h
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "FrameWork")
AActor* CAOne; // 场景中实际存在的对象指针
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "FrameWork")
TSubclassOf<AActor> CATwo; // 对象的类的类型,允许使用类型安全传递 UClass 的模板
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "FrameWork")
TSoftObjectPtr<AActor> CAThree; // 对象的软引用,需要的时候再加载
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "FrameWork")
TSoftClassPtr<AActor> CAFour; // 对象的类的类型的软引用,需要的时候再加载
编译后,在蓝图脚本面板分别放出上面 8 个变量的 Set 节点,可以看见它们的类型是两两对应的。
还有两种类型如下:
FWCharacter.h
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "FrameWork")
TAssetPtr<AActor> CAFive; // 资源引用,是对 FStringAssetReference 的封装,包含对象路径与弱指针
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "FrameWork")
FStringAssetReference CASix; // 资源路径的引用
CAFive 指定了指针的目标类型为 AActor,可以按需要改变其指向类型。 目前它可以指向已经存在于场景内的 AActor 类型及其子类的对象。
此时,CASix 可以引用任何类型的资源,如果想要让其只对某个类型可引用,则可以更改其 UPROPERTY() 内容如下,即添加一个 meta 修饰符:
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (MetaClass = "Actor"), Category = "FrameWork")
FStringAssetReference CASix;
则现在 CASix 只能引用 AActor 类型及其子类的对象。
关于 UPROPERTY() 宏及其里面 meta 的使用,推荐阅读这篇文章:《UE4 C++学习 浅析UProperty属性说明符》
文章里面写到的一个网址 benui,里面有非常详细和齐全的 UPROPERTY() 宏以及其他宏的内容,不过是全英的,读者可以将其当作工具书一样,来查阅相关的宏与说明符的用法。
再来试试更多的 meta 修饰符:
FWCharacter.h
public:
// 使蓝图里变量显示的名称变成 AllowEdit,鼠标放置于其名称上时,会有写着 "I am a bool" 的提示框
// 并且提示框内文字正常情况下是不能显示中文的
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (DisplayName = "AllowEdit", ToolTip = "I am a bool"), Category = "FrameWork")
bool CAAllow;
// 将变量的值限制在 10~100 之间(但没设置初始值的话一开始为 0),并且其能否更改取决于 bool 类型的 CAAllow
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (ClampMin = 10, ClampMax = 100, EditCondition = CAAllow), Category = "FrameWork")
int32 CACount;
编译后,打开角色蓝图,在其细节面板可以改变一下上面两个变量的值进行测试。
接下来介绍与蓝图交互的方法:
FWCharacter.h
public:
UFUNCTION(BlueprintCallable, Category = "FrameWork")
void CAFuncOne(int32 Input, bool& Output);
UFUNCTION(BlueprintImplementableEvent, Category = "FrameWork")
void CAFuncTwo(int32 Input, bool& Output);
UFUNCTION(BlueprintNativeEvent, Category = "FrameWork")
void CAFuncThree(int32 Input, bool& Output);
UFUNCTION(Exec, Category = "FrameWork")
void CAFuncFour(FString Info, int32 Count);
BlueprintCallable:C++ 实现具体逻辑,蓝图可以通过节点调用。
BlueprintImplementableEvent:蓝图实现具体逻辑。
BlueprintNativeEvent:C++ 和蓝图都可以实现具体逻辑,但在 C++ 里实现的话,其具体方法的名字要加 _Implementation 的后缀。
Exec:带有这个说明符的方法可以在控制台执行。常用于 Debug。
FWCharacter.cpp
// 引入头文件
#include "Common/FWCommon.h"
void AFWCharacter::CAFuncOne(int32 Input, bool& Output)
{
}
void AFWCharacter::CAFuncThree_Implementation(int32 Input, bool& Output)
{
}
void AFWCharacter::CAFuncFour(FString Info, int32 Count)
{
FWHelper::Debug(Info + FString(" ") + FString::FromInt(Count), 120.f);
}
编译后,在角色蓝图脚本界面里可以看到前三种方法如下:
我们可以看到 CAFuncOne 的输入引脚有它的形参 Input,输出引脚有它的引用形参 Output,说明形参是否为引用决定了它存在于蓝图节点时是输入引脚还是输出引脚。
CAFuncTwo 和 CAFuncThree 都可以按照红框内的步骤进行蓝图内的逻辑编写。
我们可以来到 GameInstance 来测试一下带 Exec 说明符的方法。
FWGameInstance.h
public:
UFUNCTION(Exec, Category = "FrameWork")
void GIEcho(FString Info, int32 Count);
FWGameInstance.cpp
// 引入头文件
#include "Common/FWCommon.h"
void UFWGameInstance::GIEcho(FString Info, int32 Count)
{
FWHelper::Debug(Info + FString(" ") + FString::FromInt(Count), 120.f);
}
编译后,在 Lesson_I 地图的 WorldSettings 面板调整 GameMode 以及里面的内容如下:
运行游戏,按 " ~ " 键打开控制台,输入 GIEcho HAHA 123,可以看到左上角输出了 HAHA 123。
默认情况下,带有 Exec 说明符的方法只能在 GameInstance 或者 GameMode 这类比较上层的类里面才能这样直接在控制台调用。如果要让其他类里面的带 Exec 说明符的方法也能这样调用,则需要重写一下执行函数。
FWGameInstance.h
public:
virtual bool ProcessConsoleExec(const TCHAR* Cmd, FOutputDevice& Ar, UObject* Executor) override;
实际上就是通过获取场景内的该对象,让其调用这个控制台可用的方法。
FWGameInstance.cpp
// 引入头文件
#include "EngineUtils.h"
#include "GameFrame/FWCharacter.h"
#include "Kismet/GameplayStatics.h"
bool UFWGameInstance::ProcessConsoleExec(const TCHAR* Cmd, FOutputDevice& Ar, UObject* Executor)
{
bool Res = Super::ProcessConsoleExec(Cmd, Ar, Executor);
if (!Res) {
// 获取场景对象方法一:迭代器
for (TActorIterator<AFWCharacter> It(GetWorld()); It; ++It) {
Res = It->ProcessConsoleExec(Cmd, Ar, Executor);
}
// 获取场景对象方法二:获取所有指定类型的 Actor 对象
/*
TArray ActArray;
UGameplayStatics::GetAllActorsOfClass(GetWorld(), AFWCharacter::StaticClass(), ActArray);
for (AActor* Act : ActArray) Act->ProcessConsoleExec(Cmd, Ar, Executor);
*/
}
return Res;
}
编译后,在关卡的 World Settings 面板设置 GameMode 的 Default Pawn 为 FWCharacter_BP(使得运行游戏的时候生成它的实例到场景中)。运行游戏,通过控制台输入 CAFuncFour Hello 321,可见左上角可以正常输出 Debug 语句。
接下来是一个监视带有 UPROPERTY() 宏的变量的方法,当编辑器里变量的值被修改时,方法就会被触发。
FWCharacter.h
public:
#if WITH_EDITOR // 只在编辑器模式下生效
virtual void PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) override;
#endif
在里面添加逻辑,让 AFWCharacter 类的 CASix 变量被更改时则让 CACount 的值加 10。
FWCharacter.cpp
#if WITH_EDITOR
void AFWCharacter::PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent)
{
Super::PostEditChangeProperty(PropertyChangedEvent);
if (PropertyChangedEvent.Property && PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED(AFWCharacter, CASix)) {
CACount += 10;
}
}
#endif
编译后,打开 FWCharacter_BP,修改变量 CASix 的值,可见 CACount 的值就会加 10。(有趣的是此时 CACount 不会被限制,可以超过 100)
在 Public/UI/Widget 路径下新建两个 C++ 的 UserWidget 类: 一个取名 FWHUDWidget,作为用于测试的主界面;一个取名 FWAffectWidget,作为被修改的界面。
新建一个 C++ 的 HUD 类,路径为 Public/UI/HUD,命名为 FWHUD,作为管理 Widget 的一个类。
来到 FrameCourse.Build.cs,添加项目依赖模块。
FrameCourse.Build.cs
PublicDependencyModuleNames.AddRange(new string[] {
"Core", "CoreUObject",
"Engine", "InputCore", // 记得加逗号分隔
"UMG" // 添加模块
});
来到玩家控制器,重写 BeginPlay() 方法并且显示鼠标。
FWPlayerController.h
public:
virtual void BeginPlay() override;
FWPlayerController.cpp
void AFWPlayerController::BeginPlay()
{
Super::BeginPlay();
// 输入模式
FInputModeGameAndUI InputMode;
InputMode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
InputMode.SetHideCursorDuringCapture(false);
SetInputMode(InputMode);
// 显示鼠标
bShowMouseCursor = true;
}
来到 HUD,创建主界面 Widget,并添加到视窗。
FWHUD.h
public:
UPROPERTY(EditAnywhere, Category = "FrameWork")
TSubclassOf<class UFWHUDWidget> HUDWidgetClass;
protected:
virtual void BeginPlay() override;
FWHUD.cpp
// 引入头文件
#include "UI/Widget/FWHUDWidget.h"
void AFWHUD::BeginPlay()
{
Super::BeginPlay();
// 创建 Widget 方法一:
UFWHUDWidget* HUDWidget = CreateWidget<UFWHUDWidget>(GetWorld(), HUDWidgetClass);
HUDWidget->AddToViewport();
}
编译后打开项目,在 Content 目录下新建一个名为 UI 的文件夹,并在其中创建两个文件夹,分别叫 HUD 和 Widget。
在 Widget 文件夹里新建一个蓝图的 Widget Blueprint,取名 FWAffectWidget_BP。
在蓝图 Widget 里面修改其父类为 FWAffectWidget。过程:Graph(脚本界面) -> 左边上面中部 ClassSettings -> 左下角 ClassOption 的 Parent Class 选为 FWAffectWidget。
来到布局编辑界面,作如下修改:
再在同目录创建一个蓝图 Widget,取名 FWHUDWidget_BP。修改其父类为 FWHUDWidget,并调整布局如下:
在 UI/ HUD 路径下创建 FWHUD 的蓝图,取名 FWHUD_BP。
打开它,在右侧面板将其 HUDWidget Class 选为 FWHUDWidget_BP。
在关卡的 World Settings 把 GameMode 里的 HUD 选为 FWHUD_BP。运行后,可看见视窗的左上角有刚刚添加的图片背景。
打开 FWAffectWidget_BP,添加三个带文本的按钮:
来到 FWAffectWidget 类,分别给三个按钮声明各自的按钮响应方法。此外还会展示三种获取 UMG 控件的方法,以及三种绑定方法到按钮的方法。
FWAffectWidget.h
// 提前声明
class UImage;
class UCanvasPanel;
class UButton;
UCLASS()
class FRAMECOURSE_API UFWAffectWidget : public UUserWidget
{
GENERATED_BODY()
public:
// UserWidget 的构造函数,但一般不在这里面写东西
UFWAffectWidget(const FObjectInitializer& ObjectInitializer);
// UserWidget 的初始化函数
virtual bool Initialize() override;
// 这些 UFUNCTION() 宏都是必要的,毕竟要借助反射来实现 C++ 与蓝图交互
UFUNCTION(BlueprintCallable, Category = "FrameWork")
void ButtonThreeEvent();
UFUNCTION()
void ButtonOneEvent();
UFUNCTION()
void ButtonTwoEvent();
public:
// 获取控件方法三:反射绑定,变量名和 UMG 里的控件名要一致
UPROPERTY(Meta = (BindWidget))
UButton* ButtonOne;
private:
UImage* BGImage;
UCanvasPanel* RootPanel;
};
FWAffectWidget.cpp
// 引入头文件
#include "Components/CanvasPanel.h" // 4.26 需要添加上一级目录
#include "Components/Image.h"
#include "Components/Button.h"
#include "Common/FWCommon.h"
// 用的时候需要调用其父类,传入 ObjectInitializer
UFWAffectWidget::UFWAffectWidget(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{
}
bool UFWAffectWidget::Initialize()
{
if (!Super::Initialize()) return false;
// 获取控件方式一:强转子集
RootPanel = Cast<UCanvasPanel>(GetRootWidget());
if (RootPanel) {
BGImage = Cast<UImage>(RootPanel->GetChildAt(0));
}
// 获取控件方法二:GetWidgetFromName
UButton* ButtonTwo = (UButton*)GetWidgetFromName(TEXT("ButtonTwo"));
// 绑定按钮事件方法一:__Internal_AddDynamic
ButtonOne->OnClicked.__Internal_AddDynamic(this, &UFWAffectWidget::ButtonOneEvent, FName("ButtonOneEvent"));
// 绑定按钮事件方法二:FScriptDelegate
FScriptDelegate ButTwoDel;
ButTwoDel.BindUFunction(this, "ButtonTwoEvent");
ButtonTwo->OnReleased.Add(ButTwoDel);
return true;
}
void UFWAffectWidget::ButtonOneEvent()
{
FWHelper::Debug("ButtonOneEvent", 10.f);
}
void UFWAffectWidget::ButtonTwoEvent()
{
FWHelper::Debug("ButtonTwoEvent", 10.f);
}
void UFWAffectWidget::ButtonThreeEvent()
{
FWHelper::Debug("ButtonThreeEvent", 10.f);
}
编译后来到 FWAffectWidget_BP,将 ButtonThree 的 OnClicked 事件绑定到 ButtonThreeEvent(绑定按钮事件方法三)。运行游戏,单击按钮时左上角会输出对应的 Debug 文本。
绑定完了按钮,那接下来利用按钮事件改变一下 UMG 界面里的控件吧。
FWAffectWidget.h
private:
// 动态图片
UImage* DynImage;
};
FWAffectWidget.cpp
// 引入头文件
#include "Blueprint/WidgetTree.h" // 4.26 需要添加上一级目录
#include "Components/CanvasPanelSlot.h"
void UFWAffectWidget::ButtonOneEvent()
{
// 去掉 Debug 语句
// 获取贴图资源
UTexture2D* TarTex = LoadObject<UTexture2D>(NULL, TEXT("Texture2D'/Game/Resource/UI/Texture/MenuTex/book.book'"));
BGImage->SetBrushFromTexture(TarTex);
}
void UFWAffectWidget::ButtonTwoEvent()
{
// 去掉 Debug 语句
// 如果动态 Image 已经存在,直接返回
if (DynImage) return;
// 创建 Widget 方法二:通过 WidgetTree 创建组件
DynImage = WidgetTree->ConstructWidget<UImage>(UImage::StaticClass());
// 添加新控件到 Panel
UCanvasPanelSlot* DynImageSlot = RootPanel->AddChildToCanvas(DynImage);
// 设置变换
DynImageSlot->SetAnchors(FAnchors(0.f));
DynImageSlot->SetOffsets(FMargin(244.f, 268.f, 100.f, 100.f));
}
void UFWAffectWidget::ButtonThreeEvent()
{
// 去掉 Debug 语句
// 如果动态 Image 不存在,直接返回
if (!DynImage) return;
// 移除出父级
RootPanel->RemoveChild(DynImage);
// 移出父节点的另一种方法
//DynImage->RemoveFromParent();
// 释放资源
DynImage->ConditionalBeginDestroy();
// 设置为空
DynImage = NULL;
}
编译后运行游戏,点击按钮的效果如下:
这节课的内容不需要跟着老师去做,只需要弄明白其中的内容就可以了。如果读者需要跟着做的话最好备份代码,到时候方便恢复成原代码,因为本节课的代码在后面都是用不上的。
游戏里的世界(UWorld)可以包含一个或多个 ULevel,每个 ULevel 都有一个 ALevelScriptActor 作为这个关卡里面运行的脚本。我们探索 UE4 生命周期要利用到这个关卡脚本 Actor。
创建一个 C++ 类的 Level Script Actor,路径为 Public/GameFrame,取名为 FWLevelScriptActor。作为我们自己创建的关卡脚本。
然后在同样的路径下创建 C++ 类的 Actor,取名为 FWActor。
接下来就是给这俩 Actor 和 Character 类的四个方法:初始化控件、构造函数、BeginPlay()、Tick() 函数写上 Debug 语句,Tick()
函数需要一个布尔变量来控制它只输出一遍。
FWActor.h
public:
virtual void PostInitializeComponents() override;
protected:
bool IsOnceTick;
FWActor.cpp
#include "Common/FWCommon.h" // 引入头文件
AFWActor::AFWActor()
{
FWHelper::Debug("Actor --> Construct", 500.f);
}
void AFWActor::PostInitializeComponents()
{
Super::PostInitializeComponents();
FWHelper::Debug("Actor --> PostInitializeComponents", 500.f);
}
void AFWActor::BeginPlay()
{
Super::BeginPlay();
FWHelper::Debug("Actor --> BeginPlay", 500.f);
}
void AFWActor::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
if (!IsOnceTick) {
FWHelper::Debug("Actor --> Tick", 500.f);
IsOnceTick = true;
}
}
FWCharacter.h
public:
virtual void PostInitializeComponents() override;
protected:
bool IsOnceTick;
FWCharacter.cpp
AFWCharacter::AFWCharacter()
{
FWHelper::Debug("Character --> Construct", 500.f);
}
void AFWCharacter::PostInitializeComponents()
{
Super::PostInitializeComponents();
FWHelper::Debug("Character --> PostInitializeComponents", 500.f);
}
void AFWCharacter::BeginPlay()
{
Super::BeginPlay();
FWHelper::Debug("Character --> BeginPlay", 500.f);
}
void AFWActor::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
if (!IsOnceTick) {
FWHelper::Debug("Character --> Tick", 500.f);
IsOnceTick = true;
}
}
接下来给 FWGameMode、FWPlayerController、FWLevelScriptActor、FWHUD 和 FWHUDWidget 也作上述的操作,这里除了 FWHUDWidget 的结构有点不一样,其他的就不贴出来了。
FWHUDWidget.h
public:
UFWHUDWidget(const FObjectInitializer& ObjectInitializer);
// Widget 的初始化函数
virtual bool Initialize() override;
protected:
// 实际上也有 Tick() 这个函数,只不过是专门用在蓝图里的
virtual void NativeTick(const FGeometry& MyGeomeytry, float InDeltaTime) override;
protected:
bool IsOnceTick;
FWHUDWidget.cpp
#include "Common/FWCommon.h" // 引入头文件
UFWHUDWidget::UFWHUDWidget(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{
FWHelper::Debug("UserWidget --> Construct", 500.f);
}
bool UFWHUDWidget::Initialize()
{
// 这里一定要判断一下父类
if (!Super::Initialize()) return false;
FWHelper::Debug("UserWidget --> Initialize", 500.f);
return true;
}
void UFWHUDWidget::NativeTick(const FGeometry& MyGeometry, float InDeltaTime)
{
Super::NativeTick(MyGeometry, InDeltaTime);
if (!IsOnceTick) {
FWHelper::Debug("UserWidget --> Tick", 500.f);
IsOnceTick = true;
}
}
GameInstance 的作用是跨场景保存数据,它在整个世界运行之前就已经存在了。这里放出来只是给大家知道一下。
FWGameInstance.h
public:
virtual void Init() override;
FWGameInstance.cpp
void UFWGameInstance::Init()
{
Super::Init();
}
至于为何没有给销毁函数添加 Debug 语句是因为后面我们会自己写一套销毁流程,所以并不在此节课展示。
编译后打开项目,可以看到场景浏览界面的左上角已经输出了 各个对象调用 Construct 的 Debug 语句,因为这些对象即便没有运行游戏也会先在场景里构造好。
打开关卡蓝图,在类设置里将父类改成 FWLevelScriptActor。然后在 Blueprint/GameFrame 文件夹创建 FWActor 的蓝图,取名为 FWActor_BP,然后放进场景。然后把 FWHUDWidget_BP 里面的 FWAffectWidget_BP 移开,方便看输出内容。
运行游戏,可以看到左上的输出内容:
输出顺序是从下往上排列的,我们可以看到方法的运行顺序大致是:构造函数 -> 组件初始化 -> BeginPlay() -> Tick()。
并且可以通过构造函数的先后顺序看到,它们生成的先后顺序是:AScriptLevelActor -> Actor -> GameMode -> PlayerController -> HUD -> Character -> UserWidget,而 GameInstance 的 Init()
则比 AScriptLevelActor 的构造函数运行得还要早。这些对象以特定的顺序生成,然后共同组成了整个游戏世界。
如果读者跟着做完了,记得清理掉上面这些操作步骤。