当前位置:   article > 正文

【UE4】C++编程_ue4 c++

ue4 c++

一、工程目录

在这里插入图片描述

  • .vs:VS的缓存文件夹;
  • Binaries:存放UE的工程的可执行文件,以及编译的中间文件;
  • Biuld:存放一些编辑器相关的日志;
  • Config:存放游戏的默认配置文件;
  • Content:存放项目资产;
  • DerivedDataCache:主要存放DivX Descriptor File文件,应该是UE为制作影视视频准备的;
  • Plugins:存放项目中使用的插件;
  • Intermediate:存放UBT生成的文件,如:.generated.h文件;
  • Saved:缓存一些临时配置文件,在PIE模式下运行项目产生的日志以及Cook产生的数据文件;
  • Script:用来存放脚本语言,如python脚本等;
  • Source:存放项目的C++文件。

.uproject

右键.uproject会出现几个选项:

  • Open:使用默认的UE编辑器打开.uproject文件;
  • Launch game:以打包后的exe的形式直接运行项目;
  • Generate Visual Studio project files:生成VS相关文件;
  • Switch Unreal Engine version:选择默认的UE版本用以打开.uproject文件。

.uproject是UE的项目描述文件,采用json的格式来描述一个项目的版本信息、模块信息、插件信息等。

{
	"FileVersion": 3,
	"EngineAssociation": "4.26",
	"Category": "",
	"Description": "",
	"Modules": [
		{
			"Name": "MyProject",
			"Type": "Runtime",
			"LoadingPhase": "Default",
			"AdditionalDependencies": [
				"Engine"
			]
		}
	],
	"Plugins": [
		{
			"Name": "WebBrowserWidget",
			"Enabled": true
		}
	]
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

.uproject参数描述文件—ProjectDescriptor.h

struct PROJECTS_API FProjectDescriptor
{
	/** Descriptor version number. */
	EProjectDescriptorVersion::Type FileVersion;
	/** 
	 * The engine to open this project with. Set this value using IDesktopPlatform::SetEngineIdentifierForProject to ensure that
	 * the most portable value for this field is used.
	 * This field allows us to open the right version of the engine when you double-click on a .uproject file, and to detect when you 
	 * open a project with a different version of the editor and need the upgrade/downgrade UI flow. The normal engine 
	 * version doesn't work for those purposes, because you can have multiple 4.x branches in various states on one machine.
	 * For Launcher users, this field gets set to something stable like "4.7" or "4.8", so you can swap projects and game binaries 
	 * between users, and it'll automatically work on any platform or machine regardless of where the engine is installed. You 
	 * can only have one binary release of each major engine version installed at once.
	 * For Perforce or Git users that branch the engine along with their games, this field is left blank. You can sync the repository 
	 * down on any platform and machine, and it can figure out which engine a project should use by looking up the directory 
	 * hierarchy until it finds one.
	 * For other cases, where you have a source build of the engine but are working with a foreign project, we use a random identifier 
	 * for each local engine installation and use the registry to map it back to the engine directory. All bets are off as to which
	 * engine you should use to open it on a different machine, and using a random GUID ensures that every new machine triggers the
	 * engine selection UI when you open or attempt to generate project files for it. 
	 * For users which mount the engine through a Git submodule (where the engine is in a subdirectory of the project), this field 
	 * can be manually edited to be a relative path.
	 * @see IDesktopPlatform::GetEngineIdentifierForProject
	 * @see IDesktopPlatform::SetEngineIdentifierForProject
	 * @see IDesktopPlatform::GetEngineRootDirFromIdentifier
	 * @see IDesktopPlatform::GetEngineIdentifierFromRootDir
	 */
	FString EngineAssociation;
	/** Category to show under the project browser */
	FString Category;
	/** Description to show in the project browser */
	FString Description;
	/** List of all modules associated with this project */
	TArray<FModuleDescriptor> Modules;
	/** List of plugins for this project (may be enabled/disabled) */
	TArray<FPluginReferenceDescriptor> Plugins;
	/** Array of platforms that this project is targeting */
	TArray<FName> TargetPlatforms;
	/** A hash that is used to determine if the project was forked from a sample */
	uint32 EpicSampleNameHash;
	/** Custom steps to execute before building targets in this project */
	FCustomBuildSteps PreBuildSteps;
	/** Custom steps to execute after building targets in this project */
	FCustomBuildSteps PostBuildSteps;
	/** Indicates if this project is an Enterprise project */
	bool bIsEnterpriseProject;
	/** Indicates that enabled by default engine plugins should not be enabled unless explicitly enabled by the project or target files. */
	bool bDisableEnginePluginsByDefault;  
    ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • FileVersion:描述项目版本;

  • EngineAssociation:引擎版本;

  • Category:这个分类着实是不知道有什么卵用;

  • Description:项目描述;

  • Modules:模块信息,是一个FModuleDescriptor类型的数组,数组中一个元素代表一个模块,FModuleDescriptor的具体描述在ModuleDescriptor.h

    里面涉及很多模块的描述,最常使用的基本只有三个:

    Name:模块名称;

    Type:模块的使用类型,描述模块在什么时候能够使用;

    Type是一个EHostType::Type类型,描述文件也在ModuleDescriptor.h中

    namespace EHostType
    {
    	enum Type
    	{
    		// Loads on all targets, except programs.
    		Runtime,	
    		// Loads on all targets, except programs and the editor running commandlets.
    		RuntimeNoCommandlet,
    		// Loads on all targets, including supported programs.
    		RuntimeAndProgram,
    		// Loads only in cooked games.
    		CookedOnly,
    		// Only loads in uncooked games.
    		UncookedOnly,
    		// Deprecated due to ambiguities. Only loads in editor and program targets, but loads in any editor mode (eg. -game, -server).
    		// Use UncookedOnly for the same behavior (eg. for editor blueprint nodes needed in uncooked games), or DeveloperTool for modules
    		// that can also be loaded in cooked games but should not be shipped (eg. debugging utilities).
    		Developer,
    		// Loads on any targets where bBuildDeveloperTools is enabled.
    		DeveloperTool,
    		// Loads only when the editor is starting up.
    		Editor,		
    		// Loads only when the editor is starting up, but not in commandlet mode.
    		EditorNoCommandlet,
    		// Loads only on editor and program targets
    		EditorAndProgram,
    		// Only loads on program targets.
    		Program,	
    		// Loads on all targets except dedicated clients.
    		ServerOnly,		
    		// Loads on all targets except dedicated servers.
    		ClientOnly,
    		// Loads in editor and client but not in commandlets.
    		ClientOnlyNoCommandlet,		
    		//~ NOTE: If you add a new value, make sure to update the ToString() method below!
    		Max
    	};
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    LoadingPhase:模块的加载策略,控制模块的加载时机;

    LoadingPhase是一个ELoadingPhase::Type类型,描述文件也在ModuleDescriptor.h

    namespace ELoadingPhase
    {
    	enum Type
    	{
    		/** As soon as possible - in other words, uplugin files are loadable from a pak file (as well as right after PlatformFile is set up in case pak files aren't used) Used for plugins needed to read files (compression formats, etc) */
    		EarliestPossible,
    		/** Loaded before the engine is fully initialized, immediately after the config system has been initialized.  Necessary only for very low-level hooks */
    		PostConfigInit,
    		/** The first screen to be rendered after system splash screen */
    		PostSplashScreen,
    		/** Loaded before coreUObject for setting up manual loading screens, used for our chunk patching system */
    		PreEarlyLoadingScreen,
    		/** 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
    	};
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
  • Plugins:插件信息,是一个FPluginReferenceDescriptor数组,数组中一个元素代表一个插件,描述文件在PluginDescriptor.h和PluginReferenceDescriptor.h文件中

    其中也包含大量的描述参数,最常用的基本也只有Name:插件名称,Enabled:是否启用插件。

  • TargetPlatforms:描述项目的目标平台;

  • EpicSampleNameHash:也没有研究出是干什么用的;

  • PreBuildSteps和PostBuildSteps:根据源码描述是用来在当前项目构建的前后执行一些自定义操作用的,具体怎么使用着实是找不到案例,查看了FCustomBuildSteps是一个很简单的结构体,里面只有一个叫HostPlatformToCommands的与命令行相关的TMap,和几个简单函数。

  • bIsEnterpriseProject:描述当前项目是否为企业项目;

  • bDisableEnginePluginsByDefault:是否启用引擎默认启用的插件,这是在FProjectDescriptor结构体中的变量,所以默认值为0。

二、类

1.UE4中的预定义类

UE的预定义类,祥见GamePlay架构

2.C++类的创建

使用Unreal Editor创建C++类

当我们创建一个C++编程模板时,在内容浏览器中会生成一个C++类文件夹,同时目录下还会生成一个项目名称文件夹,我们可以在对应的文件夹下右键创建一个C++类,选择新类需要继承的父类和存储位置后确定,UE4会自动打开VS,并生成一个.cpp文件和一个.h文件

勾选Show All Classes可以看到引擎支持的所有可继承的类。

在Path处可以直接添加新的文件夹名称,UE会自动创建新的文件夹来存放新建的类。

我们在创建类时可以选择后面的公有与私有,或者都不选

  • 选择公有,UE4会在C++Class/ProjectName文件夹下创建一个Public文件夹存放我们创建的类,而在VS中则会创建一个public文件夹存放.h文件,创建一个private文件夹存放.cpp文件;
  • 选择私有,UE4会在C++Class/ProjectName文件夹下创建一个Public文件夹存放我们创建的类,而在VS中则将.h和.cpp都存放在private文件下;
  • 如果都不选,则我们创建的类直接存放在C++Class/ProjectName文件夹下,而VS中.cpp和.h文件都存放在Source文件夹下。
  • 如下图Unkown处是一个用于选择模块的下拉列表,可以选择当前创建的类应该放在哪个模块下,这个一般涉及到多模块时才使用,一般情况下都是直接选择当前项目,在UE的视角当前项目也是一个模块。

创建的C++类的.h文件的结构,以一个UObject为例

// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "PakExpand.generated.h"

UCLASS()
class UNKOWN_API UPakExpand : public UObject
{
	GENERATED_BODY()
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • CoreMinimal.h头文件中包含了一些UE预定义需要包含的头文件;

  • NoExportTypes.h头文件中包含了大量的引擎需要的状态量;

  • .generated.h文件则是由UBT生成的用于反射的代码;

  • UCLASS()表示这个类加入UE4的反射系统,使用UCLASS()就必须包含.generated.h头文件;

  • XXX_API这个宏由UBT生成,XXX会被模块名代替,如我当前的项目名为Unkown,则生成UNKOWN_API,对于引擎来说,整个项目就是一个模块,UNKOWN_API标识这个类属于UNKOWN模块。、;

  • GENERATED_BODY():

    这里GENERATED_BODY()宏处有两种情况,我们可以使用GENERATED_UCLASS_BODY()宏和GENERATED_BODY()宏,二者的区别是:

    使用了GENERATED_BODY()宏,我们的类中就不能直接使用父类中的声明,如果我们要去实现,我就必须在本类中声明。使用GENERATED_BODY()宏,我们必须手动实现一个无参构造函数。

    使用GENERATED_UCLASS_BODY()宏,我们就可以使用父类声明的构造函数,在本类中不需要再声明,而可以直接实现即可,且实现的构造函数必须带const FObjectInitializer&参数。

在VS中手动创建类

VS中的工程目录的Source目录下的目录结构有两种:

  • 一堆的.cpp、.h和.build.cs文件。
  • .h文件在public目录下,.cpp文件在private目录下,.biuld.cs文件在Source目录下.。

对于第一种目录结构,直接在Source文件夹下使用VS添加.cpp和.h文件即可。

对于第二种目录结构,我们需要在public下添加.h文件,在private下添加.cpp文件。

在VS中手动创建的类如果继承UObject,我们需要手动添加UCLASS()宏和GENERATED_BODY()或GENERATED_UCLASS_BODY()宏。

但是要注意的是手动创建的类系统不会自动为类名加前缀,所以手动创建的类定义类名时应该合乎UE4C++类的命名规范。

2.C++类的删除

UE4引擎自身不提供C++的删除功能,但是有时候我们需要删除一些类的时候怎么办呢?

唯一的办法就是建立在文件操作上了,步骤如下:

  • 删除项目目录下Source文件夹下需要删除类的.cpp.h文件;
  • 重新Generate Visual Studio project files,生成sln文件;
  • 双击.uproject文件,启动项目让引擎重新加载配置。

3.UE4类的命名规则

UE4为一些常用类的命名添加了一些命名前缀, 如果我们不写这些前缀,UE4会编译错误

前缀说明
F纯c++类
U继承自UObject,但不继承自Actor的类
A继承自Actor的类
SSlate控件相关类
HHitResult相关类

4.C++类的实例化

在UE4中实例化C++类稍显复杂,分为如下几种情况:

  • 如果是一个纯C++类型的类,即按UE4的命名规则F开头的类,符合C++的实例化条件,可以直接使用new运算符来实例化,或者直接使用构造函数在栈区中实例化;

  • 如果是一个继承自UObject的类,那么我们需要使用NewObject<T>()函数来实例化类对象;

  • 如果是一个继承自Actor的类,那么我们需要使用UWorld对象中的SpawnActor函数来实例化,调用方式为: GetWorld()->SpawnActor<T>()GetWorld()->SpawnActor<T>()不可以在构造函数中使用,如果直接在构造函数中使用UE4在编译时会直接崩溃。

  • 如果我们需要产出一个Slate类,那么我们需要使用 SNew()函数来实例化。

5.类的使用

继承自UObject类的C++类

UE中UObject类可以直接在C++中使用但是不能直接在蓝图中使用,如果想要在蓝图中使用一个继承自UObject的类,那么我们就可以使用UCLASS(Blueprintable)在类前说明,编译后就可以直接在蓝图中使用这个C++类,同时可以创建继承这个类的蓝图类,也可以在对应的类对象上右键创建蓝图类,否则创建蓝图类的按钮是非激活状态。

当然如果我们只需要在蓝图中使用,而不需要创建对应的蓝图类,我们可以使用UCLASS(BlueprintType)来描述。

如:

UCLASS(BlueprintType)
class ARP_04_API UTest : public UObject
{
	GENERATED_BODY()
	
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

除此之外,我们若想要让类里面的变量和函数也能被蓝图类使用,我同时还需要在变量前指定UPROPERTY(BlueprintReadWrite),在函数前指定UFUNCTION(BlueprintCallable),就如上面的实例代码一样。

UPROPERTY(BlueprintReadWrite)里的参数不是唯一的, BlueprintReadWrite表示成员变量在蓝图类里可读写,BlueprintReadOnly表示成员变量在蓝图类里只读,BlueprintWriteOnly表示成员变量在蓝图类里只写

经过以上步骤我们的继承自UObject类的类便可以通过对应的蓝图类在关卡蓝图中使用了,使用BeginPlay节点开始程序,使用Construct节点来实例化我们的蓝图类,通过实例化出来的对象便可调用类中的资源了。

继承自AActor类的C++类

由于AActor在UE中就是一个场景中的基本实体,所以继承自AActor类的C++类默认是可以直接被蓝图使用的。

纯C++类

纯C++类完全脱离UE的反射系统,无法被UCLASS,UPROPERTY等宏修饰,所以无法被蓝图使用。由于纯C++类无法使用UE的反射系统,所以也就不支持UE的GC系统,所以UE提供了共享指针来管理纯C++类。

6.抽象类

UE对C++的抽象类也进行了魔改,UE使用UCLASS(abstract)宏来标识一个类是抽象类,对于UObject类和AActor类的抽象类在实例化上又有所区别。

继承自UObject的抽象类

使用UCLASS(abstract)宏标识一个UObject类,那么这个UObject类就是一个于C++抽象类基本一致的抽象类了,由于在C++中抽象类是不可以实例化的,所以如果代码中去实例化了一个抽象类,编译阶段就会报错,但是由于编译器不完全支持UE魔改后的C++标准,所以即使我们在代码里去实例化了一个抽象类,编译也不会报错,只有在编辑器运行时,真正跑到这行代码时才会报错。

UCLASS(abstract)
class MYPROJECT_API UAbstractObject : public UObject
{
	GENERATED_BODY()
public:
    void Log();
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

之所以编译阶段不会报错是因为,UE是通过UCLASS(abstract)宏来标识抽象类的,所以即使一个类没有纯虚函数UE也可以识别它为抽象类,但这个类在C++层面其实不是抽象类,所以编译阶段不会出现错误,当然我们也可以给这个类创建一个纯虚函数,是这类成为一个真正意义上的抽象类。

继承自AActor的抽象类

继承自AActor的抽象类在实例化上和UObject有一些区别,一个不包含纯虚函数的AActor抽象类在运行时是可以被实例化的,UE编辑器仅仅是报出一个警告,标识为抽象类的Actor不可以被放入场景中,但是却可以在C++中使用SapwnActor函数实例化,并且实例是有效的,只是Actor不会被spawn到场景中,只能像UObject一样驻留在内存中。如:

UCLASS(abstract)
class MYPROJECT_API AAbstractBase : public AActor
{
	GENERATED_BODY()	
public:	
	AAbstractBase();

protected:
	virtual void BeginPlay() override;
public:	
	virtual void Tick(float DeltaTime) override;
    void Log()
    {
    	UE_LOG(LogTemp, Error, TEXT("AAbstractBase::Log()"));
    }
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

然后在另一个Actor—AAbstractOpreater的BeginPlay中去实例化:

void AAbstractOpreater::BeginPlay()
{
	Super::BeginPlay();
    AAbstractBase* AbstractBase = GWorld->SpawnActor<AAbstractBase>();    
    AbstractBase->Log();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

然后将AAbstractOpreater丢进场景中运行,看看结果:

可以看到AAbstractBase虽然被实例化了,并且Log函数也调用成功了,但是场景中并没有AAbstractBase这个Actor。

这估计是UE的bug吧,在使用时抽象类最好还是创建一个纯虚函数,以符合C++的标准。

7.接口

与C++的接口不同,UE对C++的接口进行了魔改,我们先创建一个UE接口来看一下,UE的接口长什么样。

我们要创建一个UE的接口,那么我们就需要使自己的接口继承自UInterface,在编辑器里则是选择:

创建出来的接口:

#pragma once

#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "UnkInterface.generated.h"

// This class does not need to be modified.
UINTERFACE(MinimalAPI)
class UUnkInterface : public UInterface
{
	GENERATED_BODY()
};

class MYPROJECT_API IUnkInterface
{
	GENERATED_BODY()	
	// Add interface functions to this class. This is the class that will be inherited to implement this interface.
public:

};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

可以看到.h文件里面有两个类,一个是继承自UInterface,一个为纯C++类,UE这样做的目的就是为了使接口也接入UE的对象系统,因为UInterface的顶层基类也是UObject,但是这就出现了另一个问题,即继承自UObject的其他类如果想要使用UE接口,就会出现菱形继承,为了使开发者避免自己使用虚继承来避开菱形继承问题,UE就直接魔改了C++的接口。

其中两部分 UInterface负责对象系统,即接口也能使用UE的反射系统和GC系统,而纯C++类部分则负责具体的接口内容,使用方式完全和C++一样,同时由于接口接入UE的反射系统,所以接口行数可以使用UFUNCTION来暴漏给蓝图。

C++使用接口

C++使用接口,和普通C++一样,直接继承并实现所有接口的纯虚函数,然后使用即可。

蓝图使用接口

虽然UE提供了纯蓝图的接口,但是纯蓝图接口限制比较多,不能设置成员变量,也不能在接口中对函数进行实现,所以很多时候还是会需要用到C++接口,比如要写一些通用接口的时候,但是C++接口可以声明成员变量却不能暴漏给蓝图,估计是UE接口的特殊实现方式决定的。

C++接口默认暴漏给蓝图,所以不需要再使用UCLASS(BlueprintCallable)或UCLASS(BlueprintType)去暴漏,但是蓝图只能看到接口暴漏给蓝图的函数和变量,蓝图类继承一个包含纯虚函数的接口,可以不实现,因为在UE中纯虚函数是不能暴漏给蓝图的。

所以如果蓝图要继承一个C++接口,那么C++接口中的函数就需要暴漏给蓝图,函数需要使用UFUNCTION(BlueprintNativeEvent)和UFUNCTION(BlueprintNativeEvent)暴漏给蓝图。

  • BlueprintNativeEvent:BlueprintNativeEvent可以使一个函数在C++中声明并在C++中实现,然后提供给蓝图可重写的能力,如果蓝图不重写,那么蓝图调用时就使用C++的实现,如果蓝图重写了,那么蓝图调用时就是用蓝图重写的实现。在UE C++接口中被BlueprintNativeEvent标识函数需要在C++中实现一个[FunctionName]_Implementation的函数体,并且必须要配合一个virtual [FunctionName]_Implementation函数声明,二者缺一都会导致程序编译不过,如:

    .h

    #pragma once
    #include "CoreMinimal.h"
    #include "UObject/Interface.h"
    #include "UnkInterface.generated.h"
    
    UINTERFACE(MinimalAPI)
    class UUnkInterface : public UInterface
    {
    	GENERATED_BODY()
    };
    
    class MYPROJECT_API IUnkInterface
    {
    	GENERATED_BODY()	
    public:	
        UFUNCTION(BlueprintNativeEvent)
    	void Log(const FString& msg);
        virtual void Log_Implementation(const FString& msg);
        UFUNCTION(BlueprintImplementableEvent)
        void Log2(const FString& msg);
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    .cpp

    #include "UnkInterface.h"
    
    void IUnkInterface::Log_Implementation(const FString& msg)
    {
        UE_LOG(LogTemp, Error, TEXT("IUnkInterface::Log_Implementation(%s)"), *msg);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    当然如果BlueprintNativeEvent标识的函数去掉virtual [FunctionName]_Implementation函数体声明和[FunctionName]_Implementation函数体定义也是可以编译过的,只是这样就失去了BlueprintNativeEvent的意义。如:

    .h

    #pragma once
    #include "CoreMinimal.h"
    #include "UObject/Interface.h"
    #include "UnkInterface.generated.h"
    
    UINTERFACE(MinimalAPI)
    class UUnkInterface : public UInterface
    {
    	GENERATED_BODY()
    };
    
    class MYPROJECT_API IUnkInterface
    {
    	GENERATED_BODY()	
    public:	
        UFUNCTION(BlueprintNativeEvent)
    	void Log(const FString& msg);
        UFUNCTION(BlueprintImplementableEvent)
        void Log2(const FString& msg);
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    .cpp

    #include "UnkInterface.h"
    
    • 1

    这里有一点是需要注意的,C++接口中使用BlueprintNativeEvent和在普通的类中使用BlueprintNativeEvent是有所不同的,如前面所说,接口中使用BlueprintNativeEvent必须配合一个virtual [FunctionName]_Implementation声明并且必须实现[FunctionName]_Implementation函数体,而在普通的UE类中使用BlueprintNativeEvent则不需要带virtual [FunctionName]_Implementation声明,只需要实现[FunctionName]_Implementation函数体即可。

    这可能是因为C++接口既要考虑蓝图实现,又要考虑C++实现的原因,virtual [FunctionName]_Implementation声明就是为了给C++实现这个函数留接口。

  • BlueprintImplementableEvent:BlueprintImplementableEvent标识的函数不能有C++实现,必须由蓝图来重写,BlueprintImplementableEvent标识的函数可以看成是相对于蓝图的纯虚函数。如上面代码中Log2函数,C++中是没有函数体实现的。

BlueprintNativeEvent和BlueprintImplementableEvent函数的蓝图实现

  • 参数中使用了FString类的时候有一个坑,即所有的FString参数都必须使用引用—FString&,否则编译时会报没有找到重载的成员函数,但是使用引用又会引出另一个问题,就是引用类型参数暴漏给蓝图是作为蓝图函数返回值来使用的,如果想要FString&作为输入参数,那么就必须使用const FString&。

  • BlueprintNativeEvent和BlueprintImplementableEvent标识的函数暴漏给蓝图,根据是否有返回值在蓝图中的表现是不一样的,不带返回值的函数在蓝图中以事件的形式存在,需要通过右键菜单调出重写,在接口栏表现为一个黄色的函数标识;带返回值的函数在蓝图中表现为普通的函数,可以直接双击接口栏的函数名重写,在接口栏表现为一个蓝色的函数标识。

C++调用蓝图的接口实现

UE接口的实现原理使得同一个UE接口既可以被C++继承也可以被蓝图继承,于是就会涉及到C++来调用蓝图的接口实现的情况,事实上C++调用蓝图的接口实现依旧是通过蓝图子类重写父类函数的方式实现的,只是说通过接口的方式调用可以做到比重写子类的方式更加灵活的多态。

比如我们现在创建6个类,分别是动物:Animal,狗狗:Dog,飞禽:Bird,母鸡:Chicken,动物园:Zoo以及接口IMove

Animal.h

UCLASS()
class MYPROJECT_API AAnimal : public AActor,public IMove
{
	GENERATED_BODY()
public:	
	AAnimal();
protected:
	virtual void BeginPlay() override;
public:	
	virtual void Tick(float DeltaTime) override;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

Dog.h

UCLASS()
class MYPROJECT_API ADog : public AAnimal
{
	GENERATED_BODY()
public:
    void Move_Implementation();
    {
        UE_LOG(LogTemp, Error, TEXT("Dog can creep"));
    }
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

Bird.h

UCLASS()
class MYPROJECT_API ABird : public AAnimal
{
	GENERATED_BODY()
public:
    void Move_Implementation()
    {
    	UE_LOG(LogTemp, Error, TEXT("Bird can fly"));
    }
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

Move.h

UINTERFACE(MinimalAPI)
class UMove : public UInterface
{
	GENERATED_BODY()
};
class MYPROJECT_API IMove
{
	GENERATED_BODY()
public:
    UFUNCTION(BlueprintNativeEvent,Category="IMove")
    void Move();
    virtual void Move_Implementation();
    UFUNCTION(BlueprintImplementableEvent, Category = "IMove")
    void Shout(const FString& Cry);
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

Move.cpp

#include "Interface/Move.h"
void IMove::Move_Implementation()
{
    UE_LOG(LogTemp, Error, TEXT("Animal can move"));
}
  • 1
  • 2
  • 3
  • 4
  • 5

Zoo.h

UCLASS()
class MYPROJECT_API AZoo : public AActor
{
	GENERATED_BODY()	
public:	
	AZoo();

protected:
	virtual void BeginPlay() override;
public:	
	virtual void Tick(float DeltaTime) override;
    void AnimalObservationDiary();
    void AnimalAction(AAnimal* Animal);
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

Zoo.cpp

#include "Interface/Zoo.h"
//...
void AZoo::AnimalObservationDiary()
{
    AAnimal* Animal;
    UE_LOG(LogTemp, Error, TEXT("Animal Action:"));
    Animal = GWorld->SpawnActor<AAnimal>();
    AnimalAction(Animal);
    UE_LOG(LogTemp, Error, TEXT("Dog Action:"));
    Animal = GWorld->SpawnActor<ADog>();
    AnimalAction(Animal);
    UE_LOG(LogTemp, Error, TEXT("Bird Action:"));
    Animal = GWorld->SpawnActor<ABird>();
    AnimalAction(Animal);
    UE_LOG(LogTemp, Error, TEXT("Bird Action:"));
    UClass* ChickenClass = StaticLoadClass(ABird::StaticClass(), nullptr, TEXT("Blueprint'/Game/CPlus/Chicken.Chicken_C'"));
    if (ChickenClass != nullptr)
    {
        Animal = GWorld->SpawnActor <ABird>(ChickenClass);
        AnimalAction(Animal);
    }
}
void AZoo::AnimalAction(AAnimal* Animal)
{
    UClass* AnimalClass = Animal->GetClass();
    if (AnimalClass->ImplementsInterface(UMove::StaticClass()))
    {
        IMove* Move = CastChecked<IMove>(Animal);
        IMove::Execute_Move(Animal);
        IMove::Execute_Shout(Animal, " ");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

以及继承自Bird的蓝图类Chicken

  • ImplementsInterface:检测一个类是否实现了接口;
  • IMove* Move = CastChecked<IMove>(Animal):通过接口指针指向子类,实现多态;
  • IMove::Execute_Shout(Animal, " "):UE4C++调用蓝图的接口实现,由于蓝图属于C++的上层,所以UE使用了Execute_XX的反射调用方式,第一个参数必须为要调用的对象,后面用空格隔开要传入的参数。

全部实现之后我们把Zoo类丢进场景里,运行下看看:

可以看到C++通过接口调用到了蓝图函数。

这里面有一个坑,就是如果蓝图类是一个纯蓝图类直接去继承IMove接口,而不是像上面一样通过一个Animal父类去继承IMove接口,那么Zoo::AnimalAction在调用蓝图的接口实现的时候将会产生断言。

三、字符串

1.FString

FString是UE4C++编程中极其常用的一个UE4字符串封装类型,是UE4自带字符串类型中唯一可以进行各种字符串操作的字符串类型,同时FString的资源消耗也是最大的。FString的底层事实上是一个TCHAR的数组。

FString初始化

  • 方法1:
FString fStr = FString(TEXT("str")); 
  • 1
  • 方法2:
FString fStr = FString("str"); 
  • 1
  • 方法3:
FString fStr = "str"; 
  • 1
  • 方法4:
FString fStr = TEXT("str"); 
  • 1

FString的中文乱码

默认情况下我们用FString存储一个中文字符串,在UE里使用,如使用UE_LOG打印是会出现乱码的情况的,这是因为VS在国区默认使用的文件编码为GB2313和UE使用的UTF-8编码不一致导的,我们需要把cpp文件的编码格式换成UTF-8即可。

默认情况下VS把设置文件编码的入口隐藏了起来,所以要想设置文件编码我们还需要把设置入口给显示出来。

然后高级保存选项就出来了,然后设置文件编码格式。

然后我们来测试一下

void AAbstractOpreater::FStringConvert()
{
    FString fStr = TEXT("中文");
    UE_LOG(LogTemp, Error, TEXT("fStr:%s"),*fStr);
}
  • 1
  • 2
  • 3
  • 4
  • 5

结果:

2.FName

FName也是UE4自带的字符串类型,FName是不区分大小写的,赋予FName的字符串会被存放到UE4的数据表中,多个FName赋予相同的字符串时都会指向同一个数据表地址。FName被赋值之后不可改变也不能被操作,FName的不可改变的性质和C++的string类很相似,std::string在修改时事实上是创建了一个新的字符串,而不是修改原字符串,因为FName的这些性质使得FName的查找和访问非常快。

  • FName的初始化
FName fn1 = FName(TEXT("str"));
FName fn2 = FName("str");
FName fn3 = "str";
  • 1
  • 2
  • 3

3.FText

FText是一个FString的升级版字符串,存储容量比FString要大很多,主要用于UE4的文本存储与处理。FText主要涉及UE的文本本地化处理。

FText不可以像FString和FName一样从构造函数通过TCHAR构建,FText从构造函数只能构建一个空FText,要想构建一个有内容的FText,则需要使用FText::FromString,FText::FromName,FText::AsCultureInvariant。

FString fStr = TEXT("fStr");
FName fn = FName(TEXT("fn"));
FText ft1 = FText();
FText ft2 = FText::FromString(fStr);
FText ft3 = FText::AsCultureInvariant(fStr);
  • 1
  • 2
  • 3
  • 4
  • 5

4.TCHAR

TCHAR就是UE层面的char类型了,TCHAR是UE对C++的char和wchar_t的封装,C++的char和wchar_t适用于不同的平台,而TCHAR则将二者的操作进行了统一,使得TCHAR具备可以移植性。

事实上TCHAR不是对char和wchar_t的直接封装,UE将char封装成了ANSICHAR,将wchar_t封装成了WIDECHAR,而TCHAR则是对ANSICHAR和WIDECHAR的再封装。

UE对C++类型的封装都在GenericPlatform.h文件里。

7.TChar

TChar是一个针对ASCII编码的字符串操作的封装泛型结构体,提供一系列方法操作字符串。

其中UE还对专门将TChar<TCHAR>封装成了FChar,将TChar<WIDECHAR>封装成了FCharWide,将TChar<ANSICHAR>封装成了FCharAnsi

8.TCString

TCString和TChar类似,是UE专门封装的用于处理C字符串的泛型结构体,其中对ANSICHAR和WIDECHAR字符做了专门实现,提供一系列方法操作C字符串。

和TChar一样,TCString也对一些常用的类型做了封装,TCString<TCHAR>封装成FCStringTCString<ANSICHAR>封装成FCStringAnsiTCString<WIDECHAR>封装成FCStringWide

9.TStringView

TStringView和FString用法基本上是一样的,底层也是对TCHAR的封装,只是TStringView有着自己的特殊的使用场景,按照源码的说明就是:

/**
 * String View
 *
 * A string view is implicitly constructible from const char* style strings and from compatible character ranges such as FString and TStringBuilderBase.
 *
 * A string view does not own any data nor does it attempt to control any lifetimes, it merely points at a subrange of characters in some other string. It's up to the user to ensure the underlying string stays valid for the lifetime of the string view.
 *
 * A string view is cheap to copy and is intended to be passed by value.
 *
 * A string view does not represent a NUL terminated string and therefore you should never pass in the pointer returned by GetData() into a C-string API accepting only a pointer. You must either use a string builder to make a properly terminated string, or use an API that accepts a length argument in addition to the C-string.
 *
 * String views are a good fit for arguments to functions which don't wish to care which style of string construction is used by the caller. If you accept strings via string views then the caller is free to use FString, FStringBuilder, raw C strings, or any other type which can be converted into a string view.
 **/
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

谷歌翻译过来就是:

/**
 * 字符串视图
 *
 * 字符串视图可以从 const char* 样式字符串和兼容的字符范围(例如 FString 和 TStringBuilderBase)隐式构造。
 *
 * 字符串视图不拥有任何数据,也不尝试控制任何生命周期,它仅指向其他字符串中的字符子范围。由用户确保底层字符串在字符串视图的生命周期内保持有效。
 *
 * 字符串视图复制起来很便宜,并且旨在按值传递。
 *
 * 字符串视图不代表 NUL 终止的字符串,因此您永远不应将 GetData() 返回的指针传递给仅接受指针的 C 字符串 API。您必须使用字符串生成器来生成正确终止的字符串,或者使用除了 C 字符串之外还接受长度参数的 API。
 *
 * 字符串视图非常适合不希望关心调用者使用哪种字符串构造方式的函数的参数。如果您通过字符串视图接受字符串,则调用者可以自由使用 FString、FStringBuilder、原始 C 字符串或任何其他可以转换为字符串视图的类型。
 **/
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

和TChar,TCString一样,TStringView也将TStringView<TCHAR>封装成了FStringViewTStringView<ANSICHAR>封装成了FAnsiStringViewTStringView<WIDECHAR>封装成了FWideStringView

不过TStringView实际使用过程中好像也没什么用

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/不正经/article/detail/275464
推荐阅读
相关标签