UE4学习笔记:开发规范指南(半成品)

本随笔用于记录作者在实际开发中、公司、Epic Games建议、Unreal Engine用户等各种场合中使用蓝图C++类等资产时,总结出来的一套作者本人觉得最适合自己的开发规范。本随笔内容整理自官方文档官方WIKI等。

本随笔会根据作者开发过程中使用到的具体资产或文件夹而不定时更新,也有可能根据以后的开发经验将已有规范进行更改,因此本随笔仅做一个参考,而不应该当作唯一指南来使用

本随笔作者还处在学习当中,对UE4引擎的使用和理解还不是很透彻,难免出现书写上和技术上的问题,若出现类似问题,欢迎在评论区或私信里与我讨论

命名规范

蓝图文件夹命名格式

对于新项目来说,“Content”文件夹下最应该只存在一个文件夹

  • 我们的项目首先最应当在Content文件夹里面手动生成一个也是唯一一个文件夹(一般可以以项目的名字来命名该文件夹),并且将下面所有内容都放到该文件夹里而不是Content内,因为当我们从虚幻商城下载内容的时候大部分新内容也都会以商城内容的标题为文件夹放置到我们的项目里面,或者直接就是许多子文件夹。用这种方法可以很好的将我们的项目文件夹和其他内容的文件夹区分开来。

文件夹命名规范

  • Maps:存放持久性关卡,子关卡则被放置于子文件夹存放。
    • Sublevels。存放持久性关卡使用到的子关卡:
  • Blueprints:存放大部分蓝图,包括Actor、UI、角色蓝图甚至是组件(但是角色蓝图、组件属于子文件夹,见下文),但是不包括动画蓝图,因为动画蓝图应该被放到角色相关的文件夹里。该文件夹主要放置的是Actor及其非Gameplay框架资产类型的子类,Gameplay相关的资产需要放置到子文件夹里。
    • Components:用于保存所有组件,因为组件通常都是被用于蓝图,因此属于蓝图的子文件夹。
    • Gameplay:用于存放Gameplay相关的资产,例如Game Instance、Game Mode、Game State、Player Controller、Player State、Player Character等相关类型。
    • UIs:用户界面相关的资产,即所有从UserWidget类型继承而来的蓝图资产。
    • GAS: 游戏能力系统(Gameplay Ability System)相关的资产。不命名为“GameplayAbilitySystem”文件夹的原因是为了防止与引擎已有的文件夹产生冲突,同时还能缩小文件路径长度。
      • AbilitySystemComponents: 能力系统组件(Ability System Component)资产,如果以后需要自定义ASC的功能,可以将其蓝图放入该文件夹内。
      • GameplayAbilities: 游戏能力(Gameplay Ability)资产。
      • GameplayEffects: 游戏效果(Gameplay Effect)资产。
      • GameplayCues: 游戏特效(Gameplay Cue)资产。
  • Input:“增强输入(Enhanced Input)插件”资产所存放的文件夹。
    • InputActions: 用于存放“输入操作(Input Actions)”资产的文件夹。
    • Input Mapping Contexts: 用于存放“输入映射上下文(Input Mapping Context)”资产的文件夹。
  • Configs: 用于存放配置文件相关的代码,例如用于生成Data Table、Data Asset之类的结构体,以及用于读取这些数据表、数据资产的单例类等等。
    • Structures: 存放用于生成数据表(Data Table)、数据资产(Data Asset)类型时候需要指定的结构体类型。
  • Meshes:存放静态网格体,不包括骨骼网格体,骨骼网格体应该被放到角色相关的文件夹里
  • Materials主要用于保存材质实例(虽然文件夹名字是“材质”),而主材质(即材质实例的父材质)应该放到子文件夹,因为一个父材质会衍生出多个材质实例。
    • ParentMaterials:放置所有材质实例的父材质。
    • MaterialCollections: 放置材质参数集合。
    • MaterialFunctions:放置函数材质。
  • Textures:存放通常纹理,包括漫反射、法线、粗糙度、高光度等模型使用到的纹理。但是不包括一些经常复用的纹理如噪点纹理、变数纹理以及UI使用到的图片纹理等,这类纹理应该被放置于子文件夹。
    • Macros:存放噪点纹理、变数纹理等用于在原材质上添加随机特性的纹理。
    • UIs:存放UI使用到的图片纹理,例如按钮样式、背景图片等等。
  • Audio:存放音乐、音效等相关类型的资产。
    • Sources:存放Sound Wave类型的资产,因为这类资产是音频类资产最基础的资产类型,因此取名为“Source”。
  • Characters: 存放角色相关的骨骼、骨骼网格体、骨骼物理资产,不包括角色使用的材质纹理和蓝图,角色蓝图应该放到Gameplay文件夹下。该文件夹下以各个不同角色的名字命名数个子文件夹,这些子文件夹内部的子文件夹结构如下:
    • Skeletons: 存放骨骼Skeleton。
    • SkeletalMeshes: 存放骨骼网格体。
    • SkeletalPhysicsAssets:存放骨骼物理资产。
    • AnimationSequences:存放动画序列。
    • AnimationBlueprints: 存放动画蓝图。
    • BlendSpaces:存放混合空间。
  • Test: 测试用文件夹,用于存放哪些需要在UE编辑器里查看(比如说角色蓝图、Actor蓝图的默认设置是否正确),但是实际并不会被打包到项目里去的资产,因此特别需要注意的是,要在编辑器里面将该文件夹设置为不会被cook的文件夹。也因为该文件夹仅仅做测试用,因此文件夹内部的文件夹规范可以随意,也可以按照上方的顺序来整理文件夹。
  • NonAssets:非Asset资产。引擎使用到外部资源(例如.mp4、.excel)时,项目进行打包时默认不会将这些非asset资产也打进包内,如果就使用默认打包配置的话则在打完包之后并不会找到这些非asset资产,一般的解决方法是在项目设置(Project Settings)->打包(Packaging)->要打包的额外非资产目录(Additional Non-Asset Directories to Packages)里选择该文件夹,就可以在打包的时候把文件夹内的非Asset资产也打进去。因此我们可以用单独这样一个文件夹用来保存全部的非Asset资产,后续就不再需要设置其他的文件夹。

资产命名格式

Epic Games建议

  • Epic Games官方建议虚幻引擎资产的命名方式按照如下格式进行命名:

[AssetTypePrefix]_[AssetName]_[Descriptor]_[OptionalVariantLetterOrNumber]

  • AssetTypePrefix 将表明资产的类型,详情请参阅下文。
  • AssetName 是资产的名称。
  • Descriptor 将提供资产的更多上下文,表明其用法。例如,纹理是正常贴图还是不透明度贴图、模型的大小等。
  • OptionalVariantLetterOrNumber 是可选的,用于区分资产的多个版本或变体。

资产类型前缀

地图(Level,又称“关卡”):MAP_
蓝图(Blueprint,特指继承自Actor类型的蓝图):BPA_
组件(Component):BPC_
数据表(Data Table):DT_
数据资产(Data Asset):DA_
材质(Material):M_
材质实例(Material Instance):MI_
材质参数集合(Material Paramter Collection):MPC_
材质函数(Material Function):MF_
材质图层(Material Layer):ML_
材质图层混合(Material Layer Blend):MLB_
纹理(Texture):T_
渲染目标(Render Target):RT_
静态网格体(Static Mesh):SM_
骨骼(Skeleton):SK_骨骼网格体(Skeletal Mesh)和骨骼物理资产(Skeletal Physics Asset)会在此基础上添加对应的后缀,详细请见资产类型后缀
音波(Sound Wave):S_
音效(Sound Cue):SC_
UI(Widget Blueprint):WBP_
游戏能力(Gameplay Ability):GA_
游戏效果(Gameplay Effect):GE_
游戏特效(Gameplay Cue):GC_
游戏效果修改器数值计算类(Gameplay Effect Modifier Magnitude Calculation,即类UGameplayModMagnitudeCalculation):MCC_
游戏效果执行类(Gameplay Effect Execution ,即类UGameplayEffectExecutionCalculation):GEE_
输入操作(Input Action):IA_
输入映射上下文(Input Mapping Context):IMC_
动画通知(Animation Notify):AN_
动画通知状态(Animation Notify State):ANS_
行为树(Behavior Tree):BT_
黑板(Blackboard):BB_
行为树任务(Behaviour Tree Task):BTT_
行为树装饰器(Behaviour Tree Decorate):BTD_
行为树服务(Behaviour Tree Server):BTS_

资产类型后缀

持久性关卡(Persistent Level):_P
子关卡(Sub Level):_Sub
漫反射纹理(Diffuse Texture):_D
法线纹理(Normal Texture):_N
金属性纹理(Metallic Texture)或
骨骼网格体(Skeletal Mesh):_M
粗糙度纹理(Roughness):_R
环境光遮蔽纹理(Ambient Occlusion):_AO
高光度纹理(Specular):_SPC
自发光纹理(Emissive):_E
归并贴图(Mergemap),归并贴图指的是一张纹理里面不同的通道代表不同属性的贴图,例如把金属性贴图、粗糙度贴图合并到一张纹理里面):该类型的后缀应该以归并的贴图进行组合,例如说归并贴图是金属性(Metallic)、粗糙度(Rougness)和环境遮蔽(Ambient Occlusion)纹理的归并贴图,那么该贴图的后缀就应该是_MRAO
骨骼物理资产(Skeletal Physics Asset):_PA

资产缩写尽量不要与引擎使用的缩写重复

  • 例如说蓝图的缩写“BP”,引擎有时候会使用“BP_”作为前缀,但是在这里我们一般使用“BPA_”,原因是为了与引擎的资产进行区分。假如有引擎的“BP_Character”和我们的“BPA_Character”,我们只是继承BP_Character并更改了移动组件的部分功能,从命名上我们就能区分出后者是我们自己写的角色类,前者是引擎的角色类,也可以查看比起引擎的类我们自己的类进行了什么样的改动。

代码文件夹命名格式

“代码使用到的文件夹”指的是项目文件夹里“Source”文件夹下面的文件夹布局。一般来说这里的每个文件夹都表示一个模块

  • Gameplay: 用于存放游戏性相关类型代码的文件夹,同蓝图文件夹内的“Gameplay”文件夹内容相同或。
  • Input:“增强输入(Enhanced Input)插件”资产所存放的文件夹。
    • InputActions: 用于存放“输入操作(Input Actions)”资产的文件夹。
    • Input Mapping Contexts: 用于存放“输入映射上下文(Input Mapping Context)”资产的文件夹。
  • GAS: 用于存放项目中使用到游戏能力系统(Gameplay Ability System)类型的代码的文件夹,比如说继承了IAbilityInterface接口的Character、游戏能力(Gameplay Ability,简称“GA”,)、游戏效果(Gameplay Effect,简称“GE”)等等代码。
    • AbilitySystemComponents: 存放继承了GAS系统必须接口类“IAbilityInterface”的类的文件夹,可以是自定义的Pawn、Character或者Actor等等。
    • GameplayAbilities: 存放游戏能力类型代码的文件夹。
    • GameplayEffects: 存放游戏效果类型代码的文件夹。
      • ModifierMagnitudeCalculations: 存放游戏效果修改器数值计算类(即类UGameplayModMagnitudeCalculation)的文件夹。
      • Executions: 存放游戏效果执行类(即类UGameplayEffectExecutionCalculation)的文件夹。
      • Utilities: 存放GAS系统所需的一切工具类,比如说一些函数库。
  • Configs: 用于存放配置文件相关的代码,例如用于生成Data Table、Data Asset之类的结构体,以及用于读取这些数据表、数据资产的单例类等等。
    • Structures: 存放用于生成数据表(Data Table)、数据资产(Data Asset)类型时候需要指定的结构体类型。

蓝图资产(包括蓝图类、贴图、音效等)和代码资源(各种AActor子类等)命名规则之间的区别

  • 蓝图资产在缩写前缀结束后需要添加一个下划线“_”,而代码资源不需要,这个规则主要是便于同名资产之间区分蓝图类和代码类,例如“GA_Attack”和“GAAttack”,就能明显分辨出前者为蓝图类,而后者为代码类。
  • 对于某些唯一的类型(例如Character,游戏中所有角色的父类),我们在生成代码类的时候,可以通过直接命名“项目名+Character(例如“MyGameCharacter”)”而不用加前缀“BPA”变成“BPACharacter”,以此来达到让项目结构更加简洁的目的。

委托(Delegate)命名规范

  • 作为统一,委托声明宏的最后一律需要跟上;号作为结尾。
  • 对于委托的声明(Declaration),所有的委托类型命名都需要以“FOn”作为开头以“Delegate”作为结尾,然后中间写明该委托是什么时候会被调用的。例如我声明一个当数值改变时候会被调用的动态多播委托的话,则其声明应该如下:
    DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnValueChangedDelegate, int, ChangedValue);
    其中“FOnValueChangedDelegate”就是我们按照规范声明的委托类型名称。
  • 对于定义的委托实例命名,我们需要以“On”作为开头以“Delegate”作为结尾,中间作为什么时候会去调用该委托的说明。例如当我们定义上述委托的实例时,我们应编写如下代码:
    FOnValueChangedDelegate OnPlayerHealthValueChangedDelegate;
    其中“OnPlayerHealthValueChangedDelegate”即为我们规定的名称,既可以让用户知道这是一个委托,同时也可以让用户知道这是一个在玩家生命值改变时候会调用的委托。

C++类访问限定符顺序规范

在设计C++类的时候,对于访问限定符在类内的位置之所以需要做出规范,是为了方便在以后查看该类的时候能够更快速的定位类内成员的位置。

当我们生成继承自UObject的类的时候,引擎会自动帮我们生成该类,该类的模板如下(以继承自AActor且被命名为MyActorAMyActor类为示例):

UCLASS()
class MyProject_API AMyActor : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	AMyActor();

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;

};

其中会包含几个成员函数(BeginPlayTickAMyActor等)并且被各自的访问限定符所修饰,因此像是这些父类定义的函数并且有各自的访问限定符的函数,就按照原本的位置放置即可,也就是说不需要改变

当我们添加该类(AMyActor类)的成员变量和成员函数的时候,就需要遵守我们自己的访问限定符规范,该规范内容如下:

  1. 类内自定义的访问限定符必须按照从上到下依次为privateprotectedpublic的顺序定义
    按照这个顺序进行定义的原因是,如果你的类内需要private修饰的变量,那么该变量就不会被其他类访问到(假设没有友元),那么该变量就非常有可能被类内的函数访问到,因此将private限定符放到最上面同时也起到一个类似“前置声明”的作用。

  2. 同一个类型的访问限定符内,成员变量的定义要在成员函数的前面
    在设计类的过程中,大部分情况都是我们会定义一个成员变量,然后在某个成员函数里面使用该变量作为函数的形参,如果该函数的定义位于变量的前面,很可能会出现“未定义类型”的情况。

  3. 同一类型的访问限定符,成员变量只能使用一个,成员函数也只能使用一个
    只使用一个的原因是为了让类内看起来规范一些。

  4. 如果某个访问限定符没有被使用到,就将其省略即可

根据上述的访问限定符规范,之前我们定义的类在添加了我们自定义的数据之后,模板就会看起来如下:

UCLASS()
class MyProject_API AMyActor : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	AMyActor();

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;

// 上面的函数、访问限定符等因为是属于父类的,因此我们不需要进行更改。
// 下面的内容即是我们自定义的数据。
private:
	// 自定义的私有成员变量。
	
private:
	// 自定义的私有成员函数。

protected:
	// 自定义的保护成员变量。

protected:
	// 自定义的保护成员函数。

public:
	// 自定义的公有成员变量。

public:
	// 自定义的公有成员函数。

};

注释规范

代码注释在代码符号后不能有空格:

// int i = 0;	错误
//int i = 0;	正确

对于非代码注释(如说明性注释)应该在代码符号后添加一个空格:

// This is a comment.			正确
//This is a another comment.	错误

该规范用于区分项目代码里面的注释哪部分属于“说明性注释”,哪部分属于“临时代码注释”,区分的目的在于说明性注释是需要一直存在的,但是临时代码注释可能会在后续某个时间段内删除。

一个项目需要提前设计好的几个功能模块及开发步骤。

配置系统

需要根据项目的不同而设计不同类型的配置系统,例如可以使用Excel表格公式的强大计算能力搭配Excel表格转换csv表格的第三方库,再搭配上引擎读取CSV表格到数据表的功能,从而实现一套能够从Excel表格读取对应数据到数据表的配置系统来。也可以直接创建数据表对应的结构体并生成对应的数据表,然后创建数据表类型来直接通过数据表读取对应的数据信息并应用到项目里。

基类设计

对项目中会使用到各种对象类型抽象出来一些基类并设计这些共有的功能,最直接的例子就是设计一个“角色基类”,这个角色基类可以实现根据ID读取对应的数据表的功能,从而实现设置骨骼网格体、设置移动速度等属性,然后从该“角色基类”派生出一个“玩家角色基类”用来实现玩家输入控制和摄像机等属性,然后又可以从“角色基类”派生出一个“非玩家角色基类”用来实现行为树等NPC、敌人角色特有的属性。

代码审查

在将各个成员的代码合并到主要分支之前,需要进行一次“代码审查(Code Review)”,目的是为了检查各个成员提交的代码是否符合开发规范,同时也是为了避免代码中潜在的问题会影响到整个项目。代码审查的具体步骤我个人认为(因为还没有实践过)是在版本控制系统里面根据主分支的进度,随时更新一个“pre-main”分支,该分支基本上都会随着“main”分支的更新而更新,然后各个员工在编写完代码进行提交时,需要提交到的分支就是这个“pre-main”分支,然后固定一个时间点(比如说每天早上九点上班时候)由专门的审查人员对“pre-main”分支进行更新以获取到前一天员工的工作进度,当审查通过之后再由审查人员合并到“main”分支里,这样可以确保主分支“main”的代码是符合规范的。

测试流程

无论是程序员在开发过程中还是测试人员在测试过程中,指定一套测试流程是很有必要的,因为完整规范的测试流程有助于将个别程序员自己开发的模块整合到项目里面测试完整性,每个程序员都应该清楚自己开发的模块要用什么样的接口接入到项目里面。例如我要测试玩家角色技能的时候,最好是通过项目已经实现的功能(如移动功能、按键开火、按键使用技能、数据表读取配置功能等)来对新加的功能进行测试,而不是单纯地使用按键事件来直接调用对应的功能,因为直接调用的话很有可能会错过一些前置的检测、初始化等类似的功能,从而让我们正在测试的功能表现不正常或者没有正确读取到配置表里面的设置。

碰撞检测预设

涉及到碰撞检测的模块(例如武器击中Character,技能和技能、技能和玩家之间的交互)最好使用UE引擎自带的碰撞检测系统,为每个单独的类型(子弹类型、技能类型等)设立新的碰撞预设(Collision Preset),然后在碰撞属性里面设置当与其他物体进行物理交互时的响应(是忽视还是重叠还是碰撞)。而不应该是在一套碰撞预设里面响应所有类型的碰撞(无论是重叠还是阻挡),然后在对应的重叠事件或阻挡事件里面再用对象判断的方式来区分是否激活碰撞函数,这种方法实际上使用起来低效,而且不易于扩展,因为如果我们要检测十个对象的话,那么我们得在判断逻辑里面判断十次!这种方法显然没有使用碰撞预设设置来得方便。
但是需要注意的是,对于某些对象(例如项目里面的地形Landscape)如果启用了碰撞检测预设功能的话,很有可能因为地形Actor太大了导致碰撞检测性能损耗很大,而且会有部分的地形几乎永远不会被用到,这样也会造成性能浪费,因此遇到这种情况的时候就不能使用碰撞预设,而是使用下面的方法让其他会碰撞到地形的Actor使用射线检测来检测地形。

场景物体的碰撞检测预设

与其他物体不同,场景物体(如Landscape、Static Mesh Actor搭建的场景)一般情况下在场景里都是需要从生成出来之后持续存在到关卡结束甚至是游戏结束,如果让这些场景物体使用引擎自带的碰撞检测系统,那为了响应事件我们还需要开启“生成重叠事件(Generate Overlap Events)”“模拟生成撞击事件(Simulation Generate Hit Events)”属性,但很多时候我们并不需要全部的场景物体都开启这些事件,而且我们也并不知道哪些物体需要开启哪些物体不需要开启,这样当一个物体开启该属性,但是游戏全程都没有实际进行过碰撞检测的话,会产生性能浪费,因此最好的方法就是我们在需要进行场景物体碰撞检测的物体(例如玩家发射的子弹、怪物生成的炸弹等)使用Tick函数加射线检测(多对象直线射线、多对象球形射线等)对物体周围进行射线检测,然后对碰撞结果进行过滤来确定碰撞效果,然后把场景物体的生成时间全部关闭,这样就可以节省性能。这个时候我们也可以做一个约定,即让场景物体的碰撞对象类型全部设置为“世界静态(World Static)”的,这样在进行射线检测的时候可以只传一个该类型的对象从而过滤碰撞的结果。

开发周期

在开发一个功能模块的时候最建议的方法是用最快的时间搭建好一个可以使用的基础功能,例如使用配置系统+设计好的基类在场景里面生成一个对象,并对切换关卡、关闭关卡等项目运行过程中会遇的变化进行一系列的测试,当这些测试都通过的时候,再去进一步开发功能模块的细节,而不是一上手就把所有功能都实现好,到最后才来配置到项目里面去使用测试,因为这样的话可能会出现比预计多得多的问题,而且还需要把本来写好的东西都重新推翻编写,这种情况浪费了时间也浪费了精力,最好的办法就是通过“迭代开发”快速一个步骤一个步骤地实现功能并测试,测试通过之后再去开发下一个部分的新功能,直到该功能模块实现完毕。

设计良好的中间模块

例如我们有一个怪物,这个怪物会生成一个炸弹,从一般的角度考虑那我们肯定会创建一个怪物类,一个炸弹类,然后由怪物直接SpawnActor一个炸弹类,但是如果我们需要生成两个炸弹类,或者生成一个圆形阵型的炸弹,那这个时候我们肯定需要去更改怪物或炸弹的代码或蓝图脚本,这样的话迭代开发的效率就不会很高,这个时候可以通过设计一个中间类,比如说叫“技能生成器”,我们让怪物去生成一个“技能生成器”,然后在生成器里面用数据表配置我们要生成的类型,比如说生成一个炸弹,或者一个飞行子弹,并且提供额外设计:是否允许按照指定阵型去生成,这样的话后续的开发我们就可以通过配置数据表来进行迭代,而不需要再去更改代码。从最近的开发经验来看,有如下几个中间模块很合适用来实践:
技能生成器:用来配置需要实际生成的技能的类型、数量等,以及一些额外需求,比如说阵型。
阵型生成器:是要让数个技能按照直线生成还是圆形生成,该生成器应该返回的是可以生成的点。
技能生成机制:让项目可以很方便地去生成一个技能或一个技能生成器的机制,该机制完善了的话就可以很方便地去实现诸如技能结束之后再释放额外技能的机制,比如可以释放一个抛物线球的技能,然后让该抛物线球落到地面的时候生成一个范围轰炸技能。
选点系统:根据一定规则在场景上选择位置点(FVector),例如选择怪物周围的随机点,或者是怪物面对的扇形方向的随机点。可以考虑一下选点系统和EQS之间的取舍,或者说是否可以搭配使用。
池系统:用于信息池、技能池,例如当玩家需要发送多个消息(比如说本来拿到一个道具会有一个提示信息,然后你同时拿了五个,但是同屏只能显示三个),需要用池系统来收集全部需要发送的信息,以及处理发送间隔和处理暂未发送的信息。该部分功能可以通过引擎自带的队列模板“TQueue”作为框架来实现。
玩家状态系统:用于管理玩家当前处于的状态,并通过一个函数来判断传递给函数的状态是否可用。例如当玩家奔跑的时候是不能开火的,这个时候我们就只需要在编写开火逻辑的时候调用玩家状态系统,并且将当前的需要进行的状态当做枚举参数传递给函数,然后我们就可以通过事先在玩家状态系统里添加需要检测的状态并编写逻辑判断与其他状态之间的关系,最后再返回一个bool值来判断状态是否通过。可以考虑用引擎自带的GameplayTag系统来实现。

摄像机的位置、位移等设置

摄像机是玩家观察游戏世界最直接的窗口。对于第三人称游戏的玩家来说,视角的位置非常重要,如果玩家视角太接近角色模型的话,当模型往后移动时候玩家很可能看不到模型脚下的碰撞体导致玩家无法移动,但是玩家是视角看不到,会造成玩家体验不好的情况。

UMG设计

UI管理器

UI管理器(一般来说最好是UI管理子系统)专门用来处理UI的生成销毁以及显示的逻辑,但不仅于此:例如我们需要生成一个登录UI,然后在账号或密码错误的时候弹出一个对话框执行错误提示,这个时候我想要让登录UI里面的所有控件都不会与鼠标或手柄进行交互,这样的话我们就可以使用UI管理器生成登录UI并进行记录,然后生成弹窗之后对登录UI的Widget设置Non-Hittest属性即可。

悬停(Hover)和聚焦(Focus)需要分开写逻辑

在项目中我有时候会在控件的悬停事件上直接设置聚焦(使用“SetFocus”),这样写的好处是方便处理悬停逻辑和聚焦逻辑,但是会带来一个坏处:如果我们需要实现让一个按钮在某些情况下悬停不会触发聚焦(例如树形控件里面,每个父控件都不需要触发聚焦,但是子控件需要触发聚焦),这种情况下会不方便实现悬停事件和聚焦事件分开的逻辑。

UI的Z Order管理

UI在显示的时候需要设置好Z Order才不会在显示时被其他UI覆盖或覆盖其他应该需要显示的UI。可以在UI管理器里设计相关的功能,例如管理一个数据表,表的主键即为要创建的UI的类,然后对应一个Z Order值,这样在使用UI管理器生成UI时直接通过读表直接获取Z Order值,这样可以很方便管理每个UI的Z Order,并且在需要更改该值时只需要更改数据表,不需要更改代码即可实现。

多语言设计

TODO
现在的项目都支持多语言,UE引擎提供了一个多语言文本系统可以使用,不过目前我还没有使用过,所以该部分内容需要后续添加。

玩家输入

项目中玩家经常会在各种各样的场景里面使用输入,例如在游戏场景里面使用输入控制角色的移动、使用输入打开背包选择或使用道具、还有用输入进行设置等操作。这个时候管理一个好的输入是非常重要的,例如当我在打开背包使用鼠标左键选择背包里面的道具时,并不希望我们的角色响应左键执行对应的近战或射击操作,因此设计一个良好的输入系统是很有必要的,我们可以使用UE4引擎自带的“增强输入系统(Enhanced Input)”来实现该需求,我们也可以自己编写系统来实现需求,但无论使用哪一种方式,该输入系统的目的就是为了“让游戏响应我们需要响应的输入,而不需要响应其他的输入”。

音效

按键音效以及按键音效系统及配置

一般来说我们会在具体的交互UI上去播放按键音效,例如说UI有个确认按钮,我们需要在点击该按钮之后播放音效,因此我们会设计在“OnClicked”事件响应或按钮的Slate类里的“SlateHandleClicked”事件响应是播放按钮,但是如此会有一些问题:以后每实现一个UI,我们都需要进到对应的事件里面(Click、Hover、Focus等)播放音效;某些UI不需要按钮,但是也会被点击到(如一些背包界面的拖拽操作)等等。在按钮或具体的控件里实现音效的话会造成功能模块耦合度变高,而且需要实现每个UI里对应的事件,效率变低。因此我们可以使用“映射配置文件”的方法来实现按键音效的功能。该方法一共有两个步骤:

  1. 通过按键来播放音效。例如我们需要实现点击音效的话,就在当玩家按下“鼠标左键”或“手柄确认键”时直接播放音效,不需要关心该按键实现的功能,也就是从“通过响应逻辑播放音效”变成“通过响应按键播放音效”,在这个步骤需要实现玩家按下所有可能的按键(键盘、xbox手柄、ps手柄等)后直接播放音效的功能。如果只有这个功能肯定是不行的,如果玩家在一个只有一个选项的按钮上按下WASD按键,WASD按键也会播放音效,这显然不合理,这个回收就需要第二个步骤。
  2. 为按键实现“从按键到音效”的映射。就算是相同的按键,在不同的场景下也会有不同的音效:例如设置界面的确定键是一个音效,背包界面使用消耗品的确定键又是另外一个音效,如果生硬的把“确定键”绑定到“确定音效”的话,就不能实现同一个按键播放不同音效的需求,所以我们可以在“按键->按键音效”之间加一个中间层“按键音效配置”,让每个按键被响应之后首先去查看该配置文件,查询当前场景下该键要播放什么样的音效,如果查询得到的话则进行播放,查询不到的话就什么都不做,然后在配置文件里面我们只需要为需要播放音效的按键配置对应的音效(可以是SoundWave,也可以是一个字符串然后在额外的系统里去搜寻),例如“鼠标左键:左键点击音效”,没有配置的按键则没有对应的音效。
    当我们进入到该场景(甚至是某个控件,例如说一个列表的最后一个选项,该选项的下键音效不会和其他选项一致)之后只需要调用系统提供的API接口,应用我们配置好的按键音效配置文件,这样的话无论我们当前场景实现了什么交互功能,我们点击响应按键就能正确播放音效,同时把按键音效和按键功能实现了非常良好的解耦。这套系统也和虚幻引擎的“增强输入系统(Enhanced Input)”很类似:使用按键配置文件,然后通过应用该配置文件来应用功能。
posted @ 2022-07-14 15:58  U_N_Owen  阅读(1539)  评论(0编辑  收藏  举报