-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
整体模块module分析:
打开StrategyGame.uproject来看:
{ "FileVersion": 3, "Version": 0, "VersionName": "0.0", "EngineVersion": "0.0.0-0", "PackageFileUE4Version": 226, "PackageFileLicenseeUE4Version": 0, "EngineAssociation": "", "FriendlyName": "", "Description": "", "Category": "", "CreatedBy": "", "CreatedByURL": "", "Modules": [ { "Name": "StrategyGame", "Type": "Runtime", "LoadingPhase": "Default" }, { "Name": "StrategyGameLoadingScreen", //这个是额外增加的module "Type": "Runtime", "LoadingPhase": "PreLoadingScreen" //从代码中找到解释, 这个module必须设置成这种类型, 否则不会 “hook in time”, 从字面意思来看,是预加载一直存在于内存中的; } ], "EpicSampleNameHash": "0", "TargetPlatforms": [ "Android", "IOS", "MacNoEditor", "WindowsNoEditor" ] }
可以看到这里面定义了两个模块, 名字,类型,还有一个”LoadingPhase”的属性;
这个文件里面的内容是自己定义的, 还是自动生成的 ?
CONTINUE ... ...
这个demo从整体模块来讲可以看成有两个, 一个模块这里指的是生成dll的个数, 除了主模块StrategyGame之外,还有一个StrategyGameLoadingScreen:
- StrategyGameLoadingScreen: 作为子模块, 会生成一个dll, UE4Editor-StrategyGameLoadingScreen.dll(只是在editor里面编译运行过游戏); 具体实现的时候添加的内容有:
文件StrategyGameLoadingScreen.h/cpp
FStrategyGameLoadingScreenModule : IStrategyGameLoadingScreenModule : public IModuleInterface
利用这样的继承关系实现一个新的module;
其具有自己的build文件: StrategyGameLoadingScreen.Build.cs文件:
using UnrealBuildTool; // This module must be loaded "PreLoadingScreen" in the .uproject file, otherwise it will not hook in time! public class StrategyGameLoadingScreen : ModuleRules { public StrategyGameLoadingScreen(TargetInfo Target) { PrivateIncludePaths.Add("../../StrategyGame/Source/StrategyGameLoadingScreen/Private"); PublicDependencyModuleNames.AddRange( new string[] { "Core", "CoreUObject", "Engine" } ); PrivateDependencyModuleNames.AddRange( new string[] { "MoviePlayer", "Slate", "SlateCore", "InputCore" } ); } }
可以看到其内部定义了该模块的额外include path, 以及使用那些引擎的原生模块; 认为, 可能是: public为属性的模块可以和其他模块进行接口之间调用的交互,而private的只能是自己当前模块使用的(参考RulesCompiler.cs);
这个模块的主要功能是实现开始菜单的部分,并且一些UI元素如背景图等是代码写出来的(class SStrategyLoadingScreen 的Construct 函数 ), 在此发现SNew和SAssignNew是UI 进行new专用的;
但是对于这个demo的UI而言,这个module里面的内容还不够, 猜测主模块StrategyGame里面对于UI的处理是结合这个模块的;
- 主模块StrategyGame:
这个模块对应了一个UE4Editor-StrategyGame.dll; 其内部使用了上面那个StrategyGameLoadingScreen模块:
在StrategyGame.Build.cs文件里面除了增加其他module,还有:
PrivateDependencyModuleNames.AddRange( new string[] { "StrategyGameLoadingScreen" } );
- 主模块StrategyGame和StrategyGameLoadingScreen模块的交互:
这个demo中,主要是主模块调用子模块函数然后让其显示菜单背景图等, 需要包含子模块的头文件, 显式加载子模块, 调用子模块的具体实现:
void AStrategyMenuHUD::ShowLoadingScreen() { IStrategyGameLoadingScreenModule* LoadingScreenModule = FModuleManager::LoadModulePtr<IStrategyGameLoadingScreenModule>("StrategyGameLoadingScreen"); if( LoadingScreenModule != nullptr ) { LoadingScreenModule->StartInGameLoadingScreen(); } }
这个StrategyGameLoadingScreen模块作为具有PreLoadingScreen属性的模块,在游戏启动时候会专门先加载(LaunchEngineLoop.cpp, EnginePreInit(…)):
// Load up all modules that need to hook into the loading screen if (!IProjectManager::Get().LoadModulesForProject(ELoadingPhase::PreLoadingScreen) || !IPluginManager::Get().LoadModulesForEnabledPlugins(ELoadingPhase::PreLoadingScreen)) { return 1; }
关于模块加载函数LoadModulesForProject其参数是:
enum Type { /** Loaded before the engine is fully initialized, immediately after the config system has been initialized. Necessary only for very low-level hooks */ PostConfigInit, /** Loaded before the engine is fully initialized for modules that need to hook into the loading screen before it triggers */ PreLoadingScreen, /** Right before the default phase */ PreDefault, /** Loaded at the default loading point during startup (during engine init, after game modules are loaded.) */ Default, /** Right after the default phase */ PostDefault, /** After the engine has been initialized */ PostEngineInit, /** Do not automatically load this module */ None, // NOTE: If you add a new value, make sure to update the ToString() method below! Max };
所以可以知道,不同的module属性会在加载的时候具有不同的时机;
看后面可以发现会逐一加载后面几个属性的module(LaunchEngineLoop.cpp):
bool FEngineLoop::LoadStartupModules() { FScopedSlowTask SlowTask(3); SlowTask.EnterProgressFrame(1); // Load any modules that want to be loaded before default modules are loaded up. if (!IProjectManager::Get().LoadModulesForProject(ELoadingPhase::PreDefault) || !IPluginManager::Get().LoadModulesForEnabledPlugins(ELoadingPhase::PreDefault)) { return false; } SlowTask.EnterProgressFrame(1); // Load modules that are configured to load in the default phase if (!IProjectManager::Get().LoadModulesForProject(ELoadingPhase::Default) || !IPluginManager::Get().LoadModulesForEnabledPlugins(ELoadingPhase::Default)) { return false; } SlowTask.EnterProgressFrame(1); // Load any modules that want to be loaded after default modules are loaded up. if (!IProjectManager::Get().LoadModulesForProject(ELoadingPhase::PostDefault) || !IPluginManager::Get().LoadModulesForEnabledPlugins(ELoadingPhase::PostDefault)) { return false; } return true; }
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Blueprint和code布局总览:
比如,在game mode的代码和blueprint里面都定义当前的角色, 那么当前使用的角色到底是哪个?
想来,如果在代码的BeginPlay里面定义一些事情,再在blueprint里面的beginPlay节点后面连接定义一些事情,那么估计应该是先走代码里面的,再走blueprint里面的逻辑;
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
GameMode部分:
本demo中gamemode C++类没有创建对应的blueprint资源, 认为,对于一些不需要美术,或者不经常改动的变量,可以不暴露给editor,这样就不需要额外的blueprint, 纯C++类即可, 特别对于一些单件类可能更是如此;
GameMode主要是包含一些游戏性相关的接口,比如AllowCheats, InitGame, InitGameState, GetDefaultPawnClassForController, StartPlay, SetPause, ResetLevel, StartToLeaveMap, PreLogin,
CanSpectate(这个好像是是否freecamera)等, 在本demo中, 只是重新实现了InitGameState, RestartPlayer函数, 新增一些如ModifyDamage, ReturnToMenu, FinishGam, ExitGme这样的函数, 新增的函数如果允许blueprint来调用可以加上属性”BlueprintCallable”;
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
GameState部分:
GameStateBase is a class that manages the game's global state, and is spawned by GameModeBase.
这个demo里面这部分没有对这个类进行特定的blueprint资源;
有个类APlayerState:public AInfo 于文件PlayerState.h, 属于引擎原生文件, 如playername, playerID, starttime 等;
Demo中这个类添加了一些如敌人(这是个塔防游戏)个数(数组存储),OnCharPawn(供AI部分代码调用spawn出新的敌人); SetGamePaused供blueprint调用;
小地图指针也存储于此类: TWeakObjectPtr<AStrategyMiniMapCapture> MiniMapCamera;
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
MiniMap部分:
左下角的小地图有类: class AStrategyMiniMapCapture : public ASceneCapture2D[该父类自带一个DrawFrustum的component, 是个camera] 增加一些小地图宽高,AudioListener的FrontDir,RightDir[但在这里没用,应该只是存储,然后更新立体声时候从这里拿]等,以及辅助capture的变量; 在其BeginPlay函数里面存储this到GameState中; 根据tick函数来进行capture的更新; ASceneCapture2D自带GetCaptureComponent2D()->UpdateContent()[内部实现:{ CaptureSceneDeferred(); }];用于更新自身rendertarget的内容;
该类具有blueprint实例, 实例里面定义了当前使用哪个RenderTarget资源;
至于这个类里面的render target,在这个demo里面是自定义的成员变量:
在BeginPlay()里面将其给了父类中rendertarget:
MiniMapView = NewObject<UTextureRenderTarget2D>(); MiniMapView->InitAutoFormat(MiniMapWidth,MiniMapHeight); GetCaptureComponent2D()->TextureTarget = MiniMapView;
注意这里虽然是New出来的,但是没有显式析构; 其定义是 UPROPERTY() UTextureRenderTarget2D* MiniMapView; 应该是这样加上属性令UE4管理其析构的;
这里有点怀疑, 不使用额外的自定义的rendertarget应该也可以, 而在blueprint里面赋值的也只是这个TextureTarget, 而不是类中新增加的MiniMapView;
如此怀疑: blueprint与C++代码的交互是: 代码的BeginPlay()先走, 给TextureTarget赋值, 然后读取blueprint里面的值, 否则blueprint里面的值会被覆盖才对;
总感觉这个变量没什么用, 去掉应该也可以… … test … ….
该类内部专属editor代码,这部分代码关联着C++与blueprint之间交互的机制:https://docs.unrealengine.com/latest/CHN/Programming/Introduction/index.html
#if WITH_EDITOR void AStrategyMiniMapCapture::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) { Super::PostEditChangeProperty(PropertyChangedEvent); UProperty* PropertyThatChanged = PropertyChangedEvent.Property; FName PropertyName = PropertyThatChanged != nullptr ? PropertyThatChanged->GetFName() : NAME_None; if (PropertyName==FName(TEXT("RelativeRotation"))) { FRotator ChangedRotation = RootComponent->GetComponentRotation(); RootComponent->SetWorldRotation(FRotator(-90,0,ChangedRotation.Roll)); } } void AStrategyMiniMapCapture::EditorApplyRotation(const FRotator& DeltaRotation, bool bAltDown, bool bShiftDown, bool bCtrlDown) { FRotator FiltredRotation(0, DeltaRotation.Yaw, 0); Super::EditorApplyRotation(FiltredRotation, bAltDown, bShiftDown, bCtrlDown); } #endif
该类内部的GroundLevel定义: capture的camera Z值减去该值即为 [大约]距离地面的高度;
在StrategyPlayerController.Cpp里面发现代码用于坐标转换, 射线与平面检测(比较有用):
const FPlane GroundPlane = FPlane(FVector(0,0,GroundLevel), FVector::UpVector); FViewport* const Viewport = GEngine->GameViewport->ViewportFrame->GetViewport(); FVector2D const ScreenRes = Viewport->GetSizeXY(); FVector RayOrigin, RayDirection; FVector2D const ScreenCenterPoint = ScreenRes * 0.5f;//获取屏幕中心点 FStrategyHelpers::DeprojectScreenToWorld(ScreenCenterPoint, MyPlayer, RayOrigin, RayDirection);//屏幕中心点坐标转换到世界空间,传出世界空间中的射线始点与方向,其内部: FSceneViewProjectionData ProjectionData; if (Player->GetProjectionData(Player->ViewportClient->Viewport, eSSP_FULL, /*out*/ ProjectionData)) { const FMatrix ViewMatrix = FTranslationMatrix(-ProjectionData.ViewOrigin) * ProjectionData.ViewRotationMatrix; const FMatrix InvViewMatrix = ViewMatrix.InverseFast(); const FMatrix InvProjectionMatrix = ProjectionData.ProjectionMatrix.InverseFast(); FSceneView::DeprojectScreenToWorld(ScreenPosition, ProjectionData.GetConstrainedViewRect(), InvViewMatrix, InvProjectionMatrix, /*out*/ RayOrigin, /*out*/ RayDirection); return true; } FVector const WorldPoint = FStrategyHelpers::IntersectRayWithPlane(RayOrigin, RayDirection, GroundPlane);//在世界空间进行射线与平面检测
至于这个类及其render target与UI渲染的交互, 绘制到画面上: 是在class AStrategyHUD : public AHUD的函数DrawMiniMap()里面,该类重载了很多AHUD的函数,如DrawHUD; 默认的HUD可以在gamemode里面进行定义;关于小地图位置的设定也是在绘制的时候写死的,绘制:
FCanvasTileItem MapTileItem( FVector2D( 0.0f, 0.0f), FVector2D( 0.0f, 0.0f ), FLinearColor::White ); MapTileItem.Texture = MiniMapTexture->Resource; MapTileItem.Size = FVector2D( MapWidth, MapHeight ); MapTileItem.BlendMode = SE_BLEND_Opaque; Canvas->DrawItem( MapTileItem, FVector2D( MiniMapMargin * UIScale, Canvas->ClipY - MapHeight - MiniMapMargin * UIScale ) )
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
HUD部分:
HUD和菜单本来算是同一种实现方式,但是在本demo中是不一样的;
像上段落提到的,class AStrategyHUD : public AHUD;作为本demo默认的HUD类;
如何使用UE4的UMG进行UI设计? 这种方法可以纯粹使用blueprint,而不必使用代码,只是在按钮按下之类的事件的响应函数中可以利用代码实现(为了能够在blueprint里面在响应时候被调用,这种专门后台处理UI的类应该暴露给blueprint,而这个类可以作为一个成员变量存储到主角类里面,这样blueprint里面通过主角来获得该类,进而调用函数);如果一个C++类具有两个blueprint实例,那么这两个实例之间应该是没关系的,所以对于这种专门后台处理UI相应事件的逻辑(供blueprint调用)的类, 不必具有blueprint实例; 主要使用的资源类型是”Widget Blueprint”, 在这种资源里面进行UI设计,添加相应事件,调用响应函数; 游戏整体与这个资源进行交互的机制是, 一般可以在level blueprint(比如menu是个专门的level, 就具有专门的level blueprint)里面使用”Create Wedget”节点,使用这个资源(比如做弹出菜单的时候可以常用这种):
然后将这个”PauseMenuReference”作为节点”Add to Viewport”的target, 将菜单添加到Viewport中;
关于UI相关的资源在UE4中有四种:
1. Font: 字体;
本demo中没有新建字体;
2. Slate Brush: An asset describing how a texture can exist in slate’s DPI-aware environment and how this texture responds resizing, eg. Scale9-stretching? Tiling?
在blueprint里面可以使用节点“MakeSlateBrush”来创建一个SlateBrush;
一个brush可以包一个UTexture或者UMaterialInstance,一个brush应该可以理解为,设定一个贴图怎样绘制(大小,边缘怎样处理,对其方式);
在Widget Blueprint资源里面,如果拖进去一个image的控件,会发现属性里面有Brush一栏,里面的设定和一个Slate Brush的设定是一样的;
在本demo中,创建了一个Slate Brush资源,使用的方法是(看起来是作为备份的默认图片):
const FSlateBrush* SStrategyButtonWidget::GetButtonImage() const { if (ButtonImage.IsValid()) { return ButtonImage.Get(); } else { return FStrategyStyle::Get().GetBrush("DefaultActionImageBrush"); } }
可以看到这种slate brush资源“DefaultActionImageBrush”是做后备万一的;
3. Slate Widget Style: Just a wrapper for the struct with real data in it;
4. Widget Blueprint: The widget blueprint enables extending UUserWidget the user extensible UWidget; 其实可以完全不使用代码,仅仅通过这种资源来进行所有UI的设定,然后在blueprint里面通过节点“”“Create Widget”引用这个资源,然后“Add to Viewport”即可;只是本demo全是C++代码写的,类似于早期的纯代码写windows app的UI,而不是利用MFC拖控件的方式;
- 自定义Widget:
Widget可以理解为在UE4中即指代UI控件,本demo没有使用Widget Blueprint资源来涉及UI,所有都是代码写的,创建了很多继承自ScompoundWidget的类,作为一种新的自定义的Widget来作为UI的最小单元控件(后被用于组合成一个style);
如:class SStrategyButtonWidget : public SCompoundWidget : public SWidget
自定义Widget内部具体内容(如点击时的事件回调函数,button上的text等)需要以SLATE_BEGIN_ARGS(SStrategyButtonWidget)形式开始,以SLATE_END_ARGS()形式结束,内容例如:
SLATE_BEGIN_ARGS(SStrategyButtonWidget) {} /* Owning HUD for getting Game World */ SLATE_ARGUMENT(TWeakObjectPtr<AStrategyHUD>, OwnerHUD) SLATE_DEFAULT_SLOT(FArguments, Content) /** called when the button is clicked */ SLATE_EVENT(FOnClicked, OnClicked)//这个是自定义的新事件并自动被delegate,其实在SWidget里面有OnMouseButtonDown这样的虚函数已经托管好了,重载即可,这里拿来理解怎样新增自己事件以及绑定好回调函数 /** text on the button */ SLATE_ATTRIBUTE(FText, ButtonText) SLATE_END_ARGS()
又如:class SStrategyMiniMapWidget : public SCompoundWidget : public SWidget
这个是自定义小地图使用的widget,这部分和小地图的那个rendertarget渲染部分和事件响应(鼠标在小地图上点击,移动)是有关系的,该类还重载了OnPaint(…)函数, 这个函数内部只是绘制小地图上的白线的,通过FSlateDrawElement::MakeLines(…)函数;
至于小地图上的那个rendertarget的绘制,小地图部分已经提及;
一点比较有用的代码:
AStrategyPlayerController* const PC = Cast<AStrategyPlayerController>(GEngine->GetFirstLocalPlayerController(OwnerHUD.Get()->GetWorld())); AStrategyGameState const* const MyGameState = PC && PC->GetWorld() ? PC->GetWorld()->GetGameState<AStrategyGameState>() : NULL; AStrategyHUD* const HUD = PC ? Cast<AStrategyHUD>(PC->MyHUD) : NULL;
注意这里自定义的几种新的Widget,但是对于editor而言是没用的,也没有对应的资源之类的东西,只是逻辑代码上的东西;这些自定义的widget应该被用于后面实现class SStrategySlateHUDWidget : public SCompoundWidget;
自定义的widget在其构造函数中会初始化该widget的属性,如:(https://docs.unrealengine.com/latest/CHN/Programming/Slate/Overview/index.html )
void SStrategyButtonWidget::Construct(const FArguments& InArgs) { OwnerHUD = InArgs._OwnerHUD; ButtonText = InArgs._ButtonText; CenterText = InArgs._CenterText; CornerText = InArgs._CornerText; OnClicked = InArgs._OnClicked; OnClickedDisabled = InArgs._OnClickedDisabled; CoinIconVisible = InArgs._CoinIconVisible; TextHAlign = InArgs._TextHAlign; TextVAlign = InArgs._TextVAlign; TextMargin = InArgs._TextMargin; TextFont = InArgs._TextFont; Opacity = InArgs._Opacity; bIsUserActionRequired = false; bIsMouseButtonDown = false; bIsActiveAction = false; bIsActionAllowed = true; OnMouseEnterDel = InArgs._OnMouseEnterDel; OnMouseLeaveDel = InArgs._OnMouseLeaveDel; OpacityCurve = WidgetAnimation.AddCurve(0.0f, 0.2f, ECurveEaseFunction::QuadInOut); bMouseCursorVisible = true; ChildSlot .VAlign(VAlign_Fill) .HAlign(HAlign_Fill) [ SNew(SOverlay) +SOverlay::Slot() .HAlign(HAlign_Center) .VAlign(VAlign_Center) [ SNew(SImage) .Image(this, &SStrategyButtonWidget::GetButtonImage) .ColorAndOpacity(this,&SStrategyButtonWidget::GetImageColor) ] +SOverlay::Slot() .HAlign(HAlign_Center) .VAlign(VAlign_Center) [ SNew(SImage) .Image(this, &SStrategyButtonWidget::GetButtonImage) .ColorAndOpacity(this,&SStrategyButtonWidget::GetTintColor) ] +SOverlay::Slot() .HAlign(TextHAlign.Get().IsSet() ? TextHAlign.Get().GetValue() : EHorizontalAlignment::HAlign_Center) .VAlign(TextVAlign.Get().IsSet() ? TextVAlign.Get().GetValue() : EVerticalAlignment::VAlign_Bottom) .Padding(TAttribute<FMargin>(this, &SStrategyButtonWidget::GetTextMargin)) [ SNew(STextBlock) .ShadowColorAndOpacity(this,&SStrategyButtonWidget::GetTextShadowColor) .ColorAndOpacity(this,&SStrategyButtonWidget::GetTextColor) .ShadowOffset(FIntPoint(-1,1)) .Font(this, &SStrategyButtonWidget::GetTextFont) .Text(ButtonText) ] +SOverlay::Slot() .HAlign(EHorizontalAlignment::HAlign_Center) .VAlign(EVerticalAlignment::VAlign_Center) [ SNew(STextBlock) .ShadowColorAndOpacity(this,&SStrategyButtonWidget::GetTextShadowColor) .ColorAndOpacity(this,&SStrategyButtonWidget::GetTextColor) .ShadowOffset(FIntPoint(-1,1)) .Font(this, &SStrategyButtonWidget::GetTextFont) .Text(CenterText) ] +SOverlay::Slot() .HAlign(EHorizontalAlignment::HAlign_Right) .VAlign(EVerticalAlignment::VAlign_Top) [ SNew(STextBlock) .ShadowColorAndOpacity(this,&SStrategyButtonWidget::GetTextShadowColor) .ColorAndOpacity(this,&SStrategyButtonWidget::GetTextColor) .ShadowOffset(FIntPoint(-1,1)) .Text(CornerText) ] +SOverlay::Slot() [ InArgs._Content.Widget ] ]; }