前言

一些前置的要求,虽然可以无视但建议看看

——qswwltn


在阅读本文前,比较推荐先对C++和虚幻引擎蓝图/使用的内容有比较好的了解后在继续阅读,因为下面的一些操作和语句除非是必要的,不然不会进行解释。

本文的内容为使用一个数据表来保存武器的信息,并在要使用的时候生成并附加到玩家模型上,因为代码和解释有点多所以分开来讲解。

本文的虚幻引擎版本为4.25.1,代码编辑器使用Visual Stdio 2017版本为15.9.26,代码提示工具是Visual Assist 版本为10.9.2248.0,因为虚幻引擎的API更新速度还是有点快,所以给出版本号。

创建数据表


那么废话少说,直接开始上手,创建项目的过程的话就不讲了,直接快进。

首先,我推荐把这个数据表放在一个公共文件夹中,因为可能后面会有很多其他的类调用这个。比如这样:

创建一个C++类,继承于蓝图函数库

创建好后引擎会自动编译一次,如果你自己写的某些蓝图或者C++类有错误的话虽然会不通过编译但是文件其实已经创建了,直接打开Visual Studio项目文件就可以看见你刚才创建的C++类了。比如这样:

打开刚才创建的类的头文件,引入UTableData的头文件

#include "Engine/DataTable.h"       //如果没有引入的话会报错

然后我的推荐是再在这个类里创建一个结构体来保存等下后面要导出的数据,当然如果你不喜欢也可以不创建。

UCLASS()
class CPP_GAMEPROJECT_API UArms_Table : public UBlueprintFunctionLibrary
{
	GENERATED_BODY()

public:
	struct ArmsInfo {
//因为等下导出的其实是一个字符串所以我推荐使用FString类型,这个类型可以转换成其他大多数基本类型。
		FString Arms;
   
		FString Sight;
		FString Vertgrip;
		FString Mag;
		int32 Now_Bullets;
		int32 Spare_Bullets;
		int32 Default_Bullets;
		int32 MaxRange;
		float Recoil;
		float Damage;
	};
};

创建完后再在这个下面创建一个蓝图的数据结构

USTRUCT(BlueprintType)
//FTableRowBase是必须要继承的,这个是数据表行的基础类型
struct FArmsInfo : public FTableRowBase {
    
	GENERATED_USTRUCT_BODY()
public:
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Arms_Table")
		UClass* Arms;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Arms_Table")
		UClass* Sight = nullptr;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Arms_Table")
		UClass* Vertgrip = nullptr;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Arms_Table")
		UClass* Mag = nullptr;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Arms_Table")
		int32 Now_Bullets;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Arms_Table")
		int32 Spare_Bullets;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Arms_Table")
		int32 Default_Bullets;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Arms_Table")
		int32 MaxRange;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Arms_Table")
		float Recoil;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Arms_Table")
		float Damage;
};

FArmsInfo是你自己取在UE4蓝图中使用的名称是去掉F,也就是ArmsInfo,这个数据名称前缀的话我看哪天也来介绍下算了;UPROPERTY()这个宏一定要使用,不然因为UE4的反射机制,将无法在蓝图中正常使用;UClass是为了保存这个武器的类,其实这个保存到数据表中是以字符串类型存储的,关于UClass和UObject这2个类型我以后会比较粗浅的介绍下,不过本文中我推荐使用这个类型来存储,在数据表的样子大概像这样:

现在到虚幻引擎中来,随便在你喜欢的位置创建一个数据表,行结构选择你刚才创建的,比如我刚才是FArmsInfo那就是ArmsInfo。

操作数据表


那么,前面已经是创建好了一个数据表了,那么现在准备开始操作了。

在你人物的C++类中声明一个UTableData类型

UDataTable* ArmsTable = nullptr;

注意,这里我比较推荐在头文件中声明,并且不要开放给蓝图,有可能是蓝图翻译成C++类的BUG,在蓝图中如果你使用C++中声明的这个的话有时候会出现无法编译的问题(就是变量突然失效的问题)。之后在你人物的构造函数中引入刚才创建的数据表蓝图。

AUear_Player::AUear_Player()
{	//获取数据表
	static ConstructorHelpers::FObjectFinder ArmsTable_BP(TEXT("DataTable'/Game/Arms/ArmsInfo_BP.ArmsInfo_BP'"));
	if (ArmsTable_BP.Succeeded()) {
		ArmsTable = ArmsTable_BP.Object;
	}
}

TEXT()中的内容是要到UE4中选中你刚才创建的数据表然后Ctrl+C复制来的,注意加””号,在这里我非常推荐在获取数据表后加一个检查是否存在的,如果表格不存在的话在后面使用的时候会直接崩溃。

引入数据表后现在准备开始读写操作了。

在你武器的C++类头文件中增加一个碰撞,比如USphereComponent(球形碰撞),当然你也可以使用其他的方式,不过我这边先使用这种办法来搞。

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Arms_BeasInfo")
		USphereComponent* CollisionBox;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Arms_BeasInfo")
		bool IsCreate = true;

这里的UPROPERTY()是为了在蓝图中调整大小,那个IsCreate是为了判断是不是从表中生成的不然会重复添加碰撞事件。

然后在你武器类的构造函数中添加这样的代码。

AArms_Beas::AArms_Beas()
{
        CollisionBox = CreateDefaultSubobject(TEXT("CollisionBox"));
	if (!IsCreate) {
		CollisionBox->OnComponentBeginOverlap.AddDynamic(this, &AArms_Beas::OnOverLap);		//添加开始重叠事件
	}
	CollisionBox->SetSphereRadius(32.0f);
   //设置半径
}

AArms_Beas::OnOverLap这个函数就是当产生碰撞后调用的函数,函数原型是

UFUNCTION(BlueprintCallable, Category = "Arms_BeasFunction")
		void OnOverLap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp,
			int32 OtherBodyIndex, bool bFromSweep, const FHitResult & SweepResult);

这个函数必须要使用UFUNCTION()这个宏,不然无法触发碰撞事件。

void AArms_Beas::OnOverLap(UPrimitiveComponent * OverlappedComponent, AActor * OtherActor,
	UPrimitiveComponent * OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult & SweepResult)
{
	AActor* Actor_Temp = OtherActor;
	if (!isPlayer(Actor_Temp) || Player->ArmsTable == nullptr) {
		return;
	}
	FName ArmsName = FName(FString::FromInt(Arms_Index));
	Player->ArmsTable->RemoveRow(ArmsName);			//移除相同的武器

	//创建数据表
	FArmsInfo ArmsInfo;
	ArmsInfo.Arms = this->GetClass();
	if (this->Sight != nullptr) {
		ArmsInfo.Sight = this->Sight->GetClass();
	}
	if (this->Vertgrip != nullptr) {
		ArmsInfo.Vertgrip = this->Vertgrip->GetClass();
	}
	if (this->Mag != nullptr) {
		ArmsInfo.Mag = this->Mag->GetClass();
	}
	ArmsInfo.Now_Bullets = this->Now_Bullets;
	ArmsInfo.Spare_Bullets = this->Spare_Bullets;
	ArmsInfo.Default_Bullets = this->Default_Bullets;
	ArmsInfo.MaxRange = this->MaxRange;
	ArmsInfo.Recoil = this->Recoil;
	ArmsInfo.Damage = this->Damage;

	Player->ArmsTable->AddRow(ArmsName, ArmsInfo);
	//GEngine->AddOnScreenDebugMessage(0, 5.0f, FColor::Yellow, TEXT("Set Table"));
	this->Destroy();		//把自己销毁
}

那个IsPlayer是我自己创建的代码如下:

bool AArms_Beas::isPlayer(AActor* OthActor) {
	AUear_Player* Temp = Cast(OthActor);
	if (Temp == nullptr) {
		return false;
	}
	return true;
}

那么我就开始解释下这里面的一些东西。在判断完是不是玩家后我为了一个武器不可以同时在一个表里所以要将相同的武器移除,Arms_Index是我自己创建的一个唯一标记的ID,这个的话你们可以自己发挥。如果没有相同的武器的话会直接无视并不会报错或者返回其他的标志。

FName ArmsName = FName(FString::FromInt(Arms_Index));
Player->ArmsTable->RemoveRow(ArmsName);			//移除相同的武器

创建数据表的这步就讲下那个GetClass()这个函数吧,这个函数会返回一个UClass*类型。注意啊,一定要判断指针是不是为空。

//创建数据表
	FArmsInfo ArmsInfo;
	ArmsInfo.Arms = this->GetClass();
	if (this->Sight != nullptr) {
		ArmsInfo.Sight = this->Sight->GetClass();
	}
	if (this->Vertgrip != nullptr) {
		ArmsInfo.Vertgrip = this->Vertgrip->GetClass();
	}
	if (this->Mag != nullptr) {
		ArmsInfo.Mag = this->Mag->GetClass();
	}
	ArmsInfo.Now_Bullets = this->Now_Bullets;
	ArmsInfo.Spare_Bullets = this->Spare_Bullets;
	ArmsInfo.Default_Bullets = this->Default_Bullets;
	ArmsInfo.MaxRange = this->MaxRange;
	ArmsInfo.Recoil = this->Recoil;
	ArmsInfo.Damage = this->Damage;

现在就是要添加到表中了

Player->ArmsTable->AddRow(ArmsName, ArmsInfo);
//GEngine->AddOnScreenDebugMessage(0, 5.0f, FColor::Yellow, TEXT("Set Table"));
this->Destroy();		//把自己销毁

这个AddRow()我觉得可以解释下

virtual void AddRow
(
    FName RowName,
    //这一行的名称

const FTableRowBase & RowData

//这个是这个数据表的数据行结构
)

现在,你武器的信息就已经保存到数据表中了。

待续


嗯….现在才写到大概1/3的位置就发现好像有点长了,那就先分章节来搞算了,后面的话就会讲怎么从表中取出数据了,读数据可是一个重点部分(指代码量比前面这几个还要高),而且会涉及到TSharedRef的内容(虚幻引擎的指针),不过放心,我会尽量解释的清楚的。那么,今天就到这里吧