UE4开发C++沙盒游戏教程笔记(十四)(对应教程集数 43 ~ 45)

42. 合成台功能实现

这节课老师会将背包合成台的功能补全。

先在数据结构类里面添加合成配方相关的结构体。

结构体内加入如下两个方法:

  1. 检测合成九宫格里的输入物品能够输出什么物品以及输出数量的方法 DetectTable()。

  2. 加入检测输入物品的摆放是否符合配方摆放,并根据目标输出数量给出输入物品消耗数组 DetectExpend()。

(文字表述可能不太准确,读者可以先往后看,有大致思路了再回来详细阅读代码)

SlAiTypes.h

// 合成表结构体
struct CompoundTable
{
	// 合成图
	TArray<int> CompoundMap;
	// 构造函数
	CompoundTable(TArray<int> *InsertMap) {
		for (TArray<int>::TIterator It(*InsertMap); It; ++It) {
			CompoundMap.Add(*It);
		}
	}

	// 检测符合表的输出物品 ID 和数量
	void DetectTable(TArray<int>* IDMap, TArray<int>* NumMap, int& OutputID, int& OutputNum)
	{
		// 先默认设定输出 ID 为表输出 ID
		int TempID = CompoundMap[9];
		// 先设定输出数量为 64,一点一点减去
		int TempNum = 64;
		for (int i = 0; i < 9; ++i) {
			if ((*IDMap)[i] == CompoundMap[i]) {
				if ((*IDMap)[i] != 0) TempNum = (*NumMap)[i] < TempNum ? (*NumMap)[i] : TempNum;
			}
			else {
				TempID = TempNum = 0;
				break;
			}
		}
		// 如果输出 ID 不为空,更新 Output 数据
		if (TempID != 0 && TempNum != 0) {
			OutputID = TempID;
			OutputNum = TempNum;
		}
	}

	// 根据输入的物品 ID 和输出 ID 序列以及生产数量查询出是否匹配这个合成表并且输出消耗数组
	bool DetectExpend(TArray<int>* TableMap, int ProductNum, TArray<int>& ExpendMap)
	{
		// 是否匹配这个合成表,开始设置为 true
		bool IsMatch = true;
		for (int i = 0; i < 10; ++i) {
			// 只要有一个不符合,直接设置 false
			if ((*TableMap)[i] != CompoundMap[i]) {
				IsMatch = false;
				break;
			}
		}
		// 如果匹配
		if (IsMatch) {
			for (int i = 0; i < 9; ++i) {
				// 如果不为 0,直接 Add 生产的数量
				if (CompoundMap[i] != 0) {
					ExpendMap.Add(ProductNum);
				}
				else {
					ExpendMap.Add(0);
				}
			}
		}
		// 返回是否匹配
		return IsMatch;
	}

	// 字符打印
	FString ToString() {
		FString OutputString("");
		for (TArray<int>::TIterator It(CompoundMap); It; ++It) {
			OutputString += FString::FromInt(*It) + FString("__");
		}
		return OutputString;
	}
};

来到 Json 数据处理类,读取 Json 文件里面的合成表配方数据。

依旧是声明一个读取 Json 文件的方法和一个合成表数据的 Json 文件名。

SlAiJsonHandle.h

public:

	// 解析合成表
	void CompoundTableJsonRead(TArray<TSharedPtr<CompoundTable>>& CompoundTableMap);

private:

	// 合成表文件名
	FString CompoundTableFileName;
	

SlAiJsonHandle.cpp

SlAiJsonHandle::SlAiJsonHandle()
{

	// 指定合成表配方的 Json 文件名
	CompoundTableFileName = FString("CompoundTable.json");
	
}

void SlAiJsonHandle::CompoundTableJsonRead(TArray<TSharedPtr<CompoundTable>>& CompoundTableMap)
{
	FString JsonValue;
	LoadStringFromFile(CompoundTableFileName, RelativePath, JsonValue);

	TArray<TSharedPtr<FJsonValue>> JsonParsed;
	TSharedRef<TJsonReader<TCHAR>> JsonReader = TJsonReaderFactory<TCHAR>::Create(JsonValue);

	if (FJsonSerializer::Deserialize(JsonReader, JsonParsed))
	{
		for (int i = 0; i < JsonParsed.Num(); ++i) {
			TArray<TSharedPtr<FJsonValue>> ObjectAttr = JsonParsed[i]->AsObject()->GetArrayField(FString::FromInt(i));

			TArray<int> CompoundTableAttr;
			
			for (int j = 0; j < 10; ++j) {
				CompoundTableAttr.Add(ObjectAttr[j]->AsObject()->GetIntegerField(FString::FromInt(j)));
			}
			
			TSharedPtr<CompoundTable> NewTable = MakeShareable(new CompoundTable(&CompoundTableAttr));
			
			CompoundTableMap.Add(NewTable);
		}
	}
	else {
		SlAiHelper::Debug(FString("Deserialize Failed"));
	}
}	

在数据处理类里添加一个数组变量存储合成配方结构体。然后声明一个方法来调用 Json 数据读取来初始化这个数组。

SlAiDataHandle.h

public:

	// 合成表图
	TArray<TSharedPtr<CompoundTable>> CompoundTableMap;

private:

	// 初始化合成表图
	void InitCompoundTableMap();

SlAiDataHandle.cpp

void SlAiDataHandle::InitializeGameData()
{

	// 初始化合成表图
	InitCompoundTableMap();
}

void SlAiDataHandle::InitCompoundTableMap()
{
	SlAiSingleton<SlAiJsonHandle>::Get()->CompoundTableJsonRead(CompoundTableMap);
}

背包管理类添加一个和格子基类一样的判断物品是否可以叠加的方法。

SlAiPackageManager.h

class SLAICOURSE_API SlAiPackageManager
{

private:


	// 获取是否可以叠加
	bool MultiplyAble(int ObjectID);
}

在 .cpp 文件补全合成的完整逻辑。并且把背包测试物品改成木头,方便后面测试。

输出逻辑可能会有些绕,但是结合前后代码来看,多看几遍就明白了。

SlAiPackageManager.cpp

// 添加头文件
#include "SlAiDataHandle.h"

 SlAiPackageManager::SlAiPackageManager()
{
	// 将背包测试物品改成木头
	ObjectIndex = 1;
	ObjectNum = 35;
}

void SlAiPackageManager::CompoundOutput(int ObjectID, int Num)
{
	// 如果生产为 0,直接return
	if (ObjectID == 0) return;
	// 合成表结构数组
	TArray<int> TableMap;
	for (TArray<TSharedPtr<SSlAiContainerBaseWidget>>::TIterator It(InputContainerList); It; ++It) {
		TableMap.Add((*It)->GetIndex());
	}
	TableMap.Add(ObjectID);
	// 消耗数量的数组
	TArray<int> ExpendMap;
	// 遍历找出符合的合成表并且拿到消耗数量的数组
	for (TArray<TSharedPtr<CompoundTable>>::TIterator It(SlAiDataHandle::Get()->CompoundTableMap); It; ++It) {
		// 如果找到符合的直接跳出循环
		if ((*It)->DetectExpend(&TableMap, Num, ExpendMap)) break;
	}
	// 如果消耗数组元素数量不是 9,直接 return
	if (ExpendMap.Num() != 9) return;
	// 循环设置合成输入表的属性
	for (int i = 0; i < 9; ++i) {
		// 如果原有数量减去消耗数量已经小于等于 0,直接把物品 ID 设置为 0
		int InputID = (InputContainerList[i]->GetNum() - ExpendMap[i] <= 0) ? 0 : InputContainerList[i]->GetIndex();
		int InputNum = (InputID == 0) ? 0 : (InputContainerList[i]->GetNum() - ExpendMap[i]);
		// 重置参数
		InputContainerList[i]->ResetContainerPara(InputID, InputNum);
	}
}

void SlAiPackageManager::CompoundInput()
{
	// 获取合成台 9 个容器的物品 id 和数量写进两个数组
	TArray<int> IDMap;
	TArray<int> NumMap;
	for (TArray<TSharedPtr<SSlAiContainerBaseWidget>>::TIterator It(InputContainerList); It; ++It) {
		IDMap.Add((*It)->GetIndex());
		NumMap.Add((*It)->GetNum());
	}
	// 定义检测出来输出框的 ID 和数量
	int OutputIndex = 0;
	int OutputNum = 0;
	// 迭代合成表进行检测
	for (TArray<TSharedPtr<CompoundTable>>::TIterator It(SlAiDataHandle::Get()->CompoundTableMap); It; ++It) {
		(*It)->DetectTable(&IDMap, &NumMap, OutputIndex, OutputNum);
		// 如果检测出来了,直接跳出循环
		if (OutputIndex != 0 && OutputNum != 0) break;
	}
	// 下面对 OutputContainer 进行赋值
	// 先判断是否可以叠加
	if (MultiplyAble(OutputIndex)) {
		// 可以叠加的话就直接赋值,包括 0,0
		OutputContainer->ResetContainerPara(OutputIndex, OutputNum);
	}
	else {
		// 不可以叠加的话只添加一个
		OutputContainer->ResetContainerPara(OutputIndex, 1);
	}
}

bool SlAiPackageManager::MultiplyAble(int ObjectID)
{
	// 获取物品属性
	TSharedPtr<ObjectAttribute> ObjectAttr = *SlAiDataHandle::Get()->ObjectAttrMap.Find(ObjectID);
	// 返回是否是武器或者工具
	return (ObjectAttr->ObjectType != EObjectType::Tool && ObjectAttr->ObjectType != EObjectType::Weapon);
}

运行游戏,可以用木头在九宫格摆个 “×” 来合成苹果。可以检查一下取走一个输入物品或者取走一半输出物品来检查逻辑是否正确。

合成

43. 添加物品到背包

拾取掉落物和可拾取物时,物品添加到背包

要添加物品到背包,最重要的就是判断背包有没有空间。我们先给格子基类添加 3 个方法,分别用于判断格子内是否为空、是否有物品但物品不足 64 个(如果物品不可叠加则算作没有剩余空间),以及给格子添加物品。

SSlAiContainerBaseWidget.h

class SLAICOURSE_API SSlAiContainerBaseWidget : public SCompoundWidget
{

public:

	// 是否为空
	bool IsEmpty();

	// 是否有空间
	bool RemainSpace(int ObjectID);

	// 添加一个元素
	void AddObject(int ObjectID);
	
};

SSlAiContainerBaseWidget.cpp

bool SSlAiContainerBaseWidget::IsEmpty()
{
	return ObjectIndex == 0;
}

// 不检测是否为空的情况,为空的情况由上方的方法检测
bool SSlAiContainerBaseWidget::RemainSpace(int ObjectID)
{
	if (ObjectIndex == ObjectID && ObjectNum < 64 && MultiplyAble(ObjectIndex)) return true;
	return false;
}

void SSlAiContainerBaseWidget::AddObject(int ObjectID)
{
	if (ObjectIndex == 0) {
		ResetContainerPara(ObjectID, 1);
		return;
	}
	if (ObjectIndex == ObjectID && ObjectNum < 64 && MultiplyAble(ObjectIndex)) {
		ResetContainerPara(ObjectIndex, ObjectNum + 1);
		return;
	}
}

添加一个 bool SearchFreeSpace() 来检测背包是否有空间可以放入物品。同时也声明一个同名的公有方法,在里面声明一个局部指针,就不用将其保存为类里面的指针变量,同时类的外部也不会访问到。

再声明一个添加物品的方法。

SlAiPackageManager.h

class SLAICOURSE_API SlAiPackageManager
{

public:

	// 是否有插入物品的空间,提供给外部访问
	bool SearchFreeSpace(int ObjectID);

	// 添加物品
	void AddObject(int ObjectID);

private:


	// 是否有插入物品的空间,每次只会插入一个,返回可以插入的那个容器
	bool SearchFreeSpace(int ObjectID, TSharedPtr<SSlAiContainerBaseWidget>& FreeContainer);
}

SlAiPackageManager.cpp

bool SlAiPackageManager::SearchFreeSpace(int ObjectID, TSharedPtr<SSlAiContainerBaseWidget>& FreeContainer)
{
	// 空容器引用
	TSharedPtr<SSlAiContainerBaseWidget> EmptyContainer;
	
	// 判断快捷栏有无空间
	for (TArray<TSharedPtr<SSlAiContainerBaseWidget>>::TIterator It(ShortcutContainerList); It; ++It) {
		// 如果空容器无引用
		if (!EmptyContainer.IsValid()) {
			if ((*It)->IsEmpty()) EmptyContainer = *It;
		}
		// 如果优先容器有没有引用
		if (!FreeContainer.IsValid()) {
			// 如果这个容器的物品和输入物品 ID 相同并且有空间,直接指定
			if ((*It)->RemainSpace(ObjectID)) {
				FreeContainer = *It;
				return true;
			}
		}
	}
	
	// 判断背包普通格子有无空间
	for (TArray<TSharedPtr<SSlAiContainerBaseWidget>>::TIterator It(NormalContainerList); It; ++It) {
		// 如果空容器无引用
		if (!EmptyContainer.IsValid()) {
			if ((*It)->IsEmpty()) EmptyContainer = *It;
		}
		// 如果优先容器有没有引用
		if (!FreeContainer.IsValid()) {
			// 如果这个容器的物品和输入物品 ID 相同并且有空间,直接指定
			if ((*It)->RemainSpace(ObjectID)) {
				FreeContainer = *It;
				return true;
			}
		}
	}
	
	// 如运行到这里说明需要指定空容器
	if (EmptyContainer.IsValid()) {
		FreeContainer = EmptyContainer;
		return true;
	}
	// 如果连空容器都没有,返回 false
	return false;
}

bool SlAiPackageManager::SearchFreeSpace(int ObjectID)
{
	TSharedPtr<SSlAiContainerBaseWidget> FreeContainer;
	return SearchFreeSpace(ObjectID, FreeContainer);
}

void SlAiPackageManager::AddObject(int ObjectID)
{
	TSharedPtr<SSlAiContainerBaseWidget> FreeContainer;
	if (SearchFreeSpace(ObjectID, FreeContainer)) {
		// 添加物品到容器
		FreeContainer->AddObject(ObjectID);
	}
}

上面背包管理器的方法由角色类来调用。

SlAiPlayerCharacter.h

public:

	// 背包是否有空间
	bool IsPackageFree(int ObjectID);

	// 添加物品到背包
	void AddPackageObject(int ObjectID);

SlAiPlayerCharacter.cpp

// 添加头文件
#include "SlAiPackageManager.h"

bool ASlAiPlayerCharacter::IsPackageFree(int ObjectID)
{
	return SlAiPackageManager::Get()->SearchFreeSpace(ObjectID);
}

void ASlAiPlayerCharacter::AddPackageObject(int ObjectID)
{
	SlAiPackageManager::Get()->AddObject(ObjectID);
}

此时到掉落物类把先前空着的物品添加到背包的逻辑补上。

这里顺便改变一下掉落物的自动销毁时间,之前的 10 秒销毁太快了。

SlAiFlobObject.cpp

void ASlAiFlobObject::BeginPlay()
{
	Super::BeginPlay();
	
	if (!GetWorld()) return;

	FTimerDelegate DetectPlayerDele;
	DetectPlayerDele.BindUObject(this, &ASlAiFlobObject::DetectPlayer);
	GetWorld()->GetTimerManager().SetTimer(DetectTimer, DetectPlayerDele, 1.f, true, 3.f);

	FTimerDelegate DestroyDele;
	DestroyDele.BindUObject(this, &ASlAiFlobObject::DestroyEvent);
	// 改回 30 秒销毁
	GetWorld()->GetTimerManager().SetTimer(DestroyTimer, DestroyDele, 30.f, false);

	SPCharacter = NULL;
}

void ASlAiFlobObject::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
	
	BaseMesh->AddLocalRotation(FRotator(DeltaTime * 60.f, 0.f, 0.f));

	if (SPCharacter) {
		SetActorLocation(FMath::VInterpTo(GetActorLocation(), SPCharacter->GetActorLocation() + FVector(0.f, 0.f, 40.f), DeltaTime, 5.f));
		if (FVector::Distance(GetActorLocation(), SPCharacter->GetActorLocation() + FVector(0.f, 0.f, 40.f)) < 10.f) {
			// 判断玩家背包是否有空间
			if (SPCharacter->IsPackageFree(ObjectIndex)) {
				// 添加对应的物品到背包
				SPCharacter->AddPackageObject(ObjectIndex);
				DestroyEvent();
			}
			else {
				SPCharacter = NULL;
				GetWorld()->GetTimerManager().UnPauseTimer(DetectTimer);
				GetWorld()->GetTimerManager().UnPauseTimer(DestroyTimer);
				BoxCollision->SetSimulatePhysics(true);
			}
		}
	}
}

void ASlAiFlobObject::DetectPlayer()
{
	// ... 省略

	if (GetWorld()->OverlapMultiByObjectType(Overlaps, GetActorLocation(), FQuat::Identity, ObjectParams, FCollisionShape::MakeSphere(200.f), Params)) {
		for (TArray<FOverlapResult>::TIterator It(Overlaps); It; ++It) {
			if (Cast<ASlAiPlayerCharacter>(It->GetActor())) {
				SPCharacter = Cast<ASlAiPlayerCharacter>(It->GetActor());
				// 如果背包有空间
				if (SPCharacter->IsPackageFree(ObjectIndex)) {	
					GetWorld()->GetTimerManager().PauseTimer(DetectTimer);
					GetWorld()->GetTimerManager().PauseTimer(DestroyTimer);
					BoxCollision->SetSimulatePhysics(false);
				}
				return;
			}
		}
	}
}

可拾取物的添加物品到背包的逻辑也要补充上。

SlAiPlayerController.cpp

void ASlAiPlayerController::StateMachine()
{
	// ... 省略
	
	if (Cast<ASlAiPickupObject>(RayActor) && FVector::Distance(RayActor->GetActorLocation(), SPCharacter->GetActorLocation()) < 300.f)
	{
		ChangePreUpperType(EUpperBody::PickUp);
		UpdatePointer.ExecuteIfBound(false, 0);
		// 如果右键按下
		if (IsRightButtonDown && SPCharacter->IsPackageFree(Cast<ASlAiPickupObject>(RayActor)->ObjectIndex)) {
			// 把物品捡起来
			SPCharacter->AddPackageObject(Cast<ASlAiPickupObject>(RayActor)->TakePickup());
		}
	}
}

运行游戏,掉落物和可拾取物添加到背包的逻辑已经完成了。(注意动图内快捷栏第一格的木材,必须是自己手动放的,第二格的快捷栏内的木材是先前为了测试,用代码放进去的。)

在这里插入图片描述

吃东西消耗物品

给背包管理器添加一个吃东西消耗食物的方法。

SlAiPackageManager.h

class SLAICOURSE_API SlAiPackageManager
{

public:

	// 吃东西,传入快捷栏的 ID,传回是否成功吃掉
	bool EatUpEvent(int ShortcutID);
	
}

SlAiPackageManager.cpp

bool SlAiPackageManager::EatUpEvent(int ShortcutID)
{
	// 获取物品属性
	TSharedPtr<ObjectAttribute> ObjectAttr = *SlAiDataHandle::Get()->ObjectAttrMap.Find(ShortcutContainerList[ShortcutID]->GetIndex());
	// 如果这个物品是食物
	if (ObjectAttr->ObjectType == EObjectType::Food) {
		// 新的物品数量
		int NewNum = (ShortcutContainerList[ShortcutID]->GetNum() - 1) < 0 ? 0 : (ShortcutContainerList[ShortcutID]->GetNum() - 1);
		// 新的物品 ID
		int NewIndex = NewNum == 0 ? 0 : ShortcutContainerList[ShortcutID]->GetIndex();
		// 更新这个容器属性
		ShortcutContainerList[ShortcutID]->ResetContainerPara(NewIndex, NewNum);
		// 返回成功吃掉食物
		return true;
	}
	return false;
}

同时给 PlayerState 添加一个补充饥饿度的方法,用于在吃食物后增加饥饿度。

SlAiPlayerState.h

public:
	
	// 提升饥饿度
	void PromoteHunger();

SlAiPlayerState.cpp

void ASlAiPlayerState::PromoteHunger()
{
	// 只要超过 500,马上设为 600
	if (Hunger + 100 >= 500.f) {
		Hunger = 600.f;
		return;
	}
	// 否则只加 100
	Hunger = FMath::Clamp<float>(Hunger + 100.f, 0, 600.f);
}

角色类添加一个玩家控制器的指针,在 BeginPlay() 里初始化,通过它来获取 PlayerState。

添加一个方法来让角色类调用上面的吃东西和消耗食物的方法。

SlAiPlayerCharacter.h

public:

	// 吃完东西调用的事件,由 Anim 进行调用
	void EatUpEvent();

public:

	UPROPERTY(VisibleDefaultsOnly, Category = "SlAi")
	UCameraComponent* FirstCamera;

	// 玩家控制器指针
	class ASlAiPlayerController* SPController;
	

SlAiPlayerCharacter.cpp

// 引入头文件
#include "Kismet/GameplayStatics.h"
#include "SlAiPlayerController.h"
#include "SlAiPlayerState.h"

void ASlAiPlayerCharacter::BeginPlay()
{
	

	// 如果控制器指针为空,添加引用
	SPController = Cast<ASlAiPlayerController>(UGameplayStatics::GetPlayerController(GetWorld(), 0));
}

void ASlAiPlayerCharacter::EatUpEvent()
{
	// 如果玩家用户状态为空,直接返回
	if (!SPController->SPState) return;
	// 告诉背包哪个快捷栏吃了东西,如果成功吃了东西
	if (SlAiPackageManager::Get()->EatUpEvent(SPController->SPState->CurrentShortcutIndex)) {
		// 告诉玩家状态类提升饥饿值
		SPController->SPState->PromoteHunger();
	}
}

那谁来调用角色类的这个方法呢?让动画类在吃东西的动画播放完的时候调用好了。

SlAiPlayerAnim.h

public:

	// 吃完东西时调用
	UFUNCTION(BlueprintCallable, Category = "PlayAnim")
	void EatUpEvent();

SlAiPlayerAnim.cpp

void USlAiPlayerAnim::EatUpEvent()
{
	if (!SPCharacter) return;
	// 告诉角色我吃完东西了
	SPCharacter->EatUpEvent();
}

最后调整下第一人称和第三人称的动画蓝图,让吃东西动画的 Notify (老师提前准备好的)调用 EatUpEvent()。

动画蓝图调整

运行后,等待一段时间后饥饿值的条缩短,控制角色吃苹果(要自己拿木材合成出来的才可以),可以看到饥饿值的条增加。

吃东西消耗

测试完成后,之前测试用的东西都可以去掉了。

先把背包控制器的初始化物品种类和数量设置为 0。

SlAiPackageManager.cpp

SlAiPackageManager::SlAiPackageManager()
{
	// 初始化物品和数量为 0
	ObjectIndex = 0;
	ObjectNum = 0;
}

PlayerState 的测试用物品也注释掉。

SlAiPlayerState.cpp

void ASlAiPlayerState::RegisterShortcutContainer(TArray<TSharedPtr<ShortcutContainer>>* ContainerList, 
	TSharedPtr<STextBlock> ShortcutInfoTextBlock)
{
	for (TArray<TSharedPtr<ShortcutContainer>>::TIterator It(*ContainerList); It; ++It) {
		ShortcutContainerList.Add(*It);
	}

	ShortcutInfoTextAttr.BindUObject(this, &ASlAiPlayerState::GetShortcutInfoText);
	ShortcutInfoTextBlock->SetText(ShortcutInfoTextAttr);

	// 临时测试代码,设置快捷栏的物品
	/*
	ShortcutContainerList[1]->SetObject(1)->SetObjectNum(5);
	ShortcutContainerList[2]->SetObject(2)->SetObjectNum(15);
	ShortcutContainerList[3]->SetObject(3)->SetObjectNum(1);
	ShortcutContainerList[4]->SetObject(4)->SetObjectNum(35);
	ShortcutContainerList[5]->SetObject(5)->SetObjectNum(45);
	ShortcutContainerList[6]->SetObject(6)->SetObjectNum(55);
	ShortcutContainerList[7]->SetObject(7)->SetObjectNum(64);
	*/
}

44. 敌人模型与动作

添加两个新的碰撞物体通道,一个取名 Enemy,默认回应为 Block。另一个取名 EnemyTool,默认回应为 Ignore。

添加两个碰撞预设如下。

碰撞预设

让 PlayerProfile 的预设对 EnemyTool 的回应为 Overlap。
ToolProfile 对 Enemy 和 EnemyTool 的回应为 Overlap。
FlobProfile 对 Enemy 的回应为 Ignore。

在 /Public/Enemy 路径下创建下面的 C++ 类:

添加一个 Character 类,取名 SlAiEnemyCharacter

添加一个 Anim Instance,取名 SlAiEnemyAnim

添加一个 AIController,取名 SlAiEnemyController

在 /Public/EnemyTool 路径下创建一个 C++ 的 Actor 类,取名叫 SlAiEnemyTool

再以这个类为基类创建两个子类。一个叫 SlAiEnemyWeapon,一个叫 SlAiEnemyShield。(老师将盾牌的英文名写成了 Sheild,笔者对于英语偏向于抠细节,所以自行改正了,读者看个人情况改不改)

在 Blueprint 下创建一个叫 Enemy 的文件夹,先在里面创建一个动画蓝图,以 SlAiEnemyAnim 为基类,骨骼使用 Enemy_Skeleton,取名叫 Enemy_Animation

首先写一下工具基类(其实跟玩家的手持物品基类差不多)。把 BeginPlay() 和 Tick() 方法都删掉,因为用不到。

给工具基类添加一个开关碰撞检测的方法;以及两个分别生成武器和盾牌的方法。

再重写一下碰撞检测的两个方法。剩下的就是补充组件,如网格体和盒子碰撞体。

SlAiEnemyTool.h

public:

	// 是否允许检测
	void ChangeOverlayDetect(bool IsOpen);

	static TSubclassOf<AActor> SpawnEnemyWeapon();
	
	static TSubclassOf<AActor> SpawnEnemyShield();

protected:

	UFUNCTION()
	virtual void OnOverlayBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);

	UFUNCTION()
	virtual void OnOverlayEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);

protected:

	// 根组件
	USceneComponent* RootScene;

	// 静态模型
	UPROPERTY(EditAnywhere, Category = "Mesh")
	UStaticMeshComponent* BaseMesh;

	// 盒子碰撞体
	UPROPERTY(EditAnywhere, Category = "Mesh")
	class UBoxComponent* AffectCollision;

SlAiEnemyTool.cpp

// 添加头文件
#include "Components/StaticMeshComponent.h"
#include "Components/BoxComponent.h"
#include "Components/PrimitiveComponent.h"

#include "SlAiEnemyWeapon.h"
#include "SlAiEnemyShield.h"

ASlAiEnemyTool::ASlAiEnemyTool()
{
	// 去掉允许 Tick 的代码
	// 实例化根节点
	RootScene = CreateDefaultSubobject<USceneComponent>("RootScene");
	RootComponent = RootScene;

	// 在这里实现模型组件但是不进行模型绑定
	BaseMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("BaseMesh"));
	BaseMesh->SetupAttachment(RootComponent);
	BaseMesh->SetCollisionProfileName(FName("NoCollision"));

	// 实现碰撞组件
	AffectCollision = CreateDefaultSubobject<UBoxComponent>(TEXT("AffectCollision"));
	AffectCollision->SetupAttachment(BaseMesh);
	AffectCollision->SetCollisionProfileName(FName("EnemyToolProfile"));
	// 初始时关闭 Overlay 检测
	AffectCollision->SetGenerateOverlapEvents(false);

	// 绑定检测方法到碰撞体
	FScriptDelegate OverlayBegin;
	OverlayBegin.BindUFunction(this, "OnOverlayBegin");
	AffectCollision->OnComponentBeginOverlap.Add(OverlayBegin);
	
	FScriptDelegate OverlayEnd;
	OverlayEnd.BindUFunction(this, "OnOverlayEnd");
	AffectCollision->OnComponentEndOverlap.Add(OverlayEnd);	
}

void ASlAiEnemyTool::ChangeOverlayDetect(bool IsOpen)
{
	AffectCollision->SetGenerateOverlapEvents(IsOpen);
}

TSubclassOf<AActor> ASlAiEnemyTool::SpawnEnemyWeapon()
{
	return ASlAiEnemyWeapon::StaticClass();
}
	
TSubclassOf<AActor> ASlAiEnemyTool::SpawnEnemyShield()
{
	return ASlAiEnemyShield::StaticClass();
}

void ASlAiEnemyTool::OnOverlayBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
}

void ASlAiEnemyTool::OnOverlayEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
}

给敌人武器类和敌人盾牌类完善一下。

SlAiEnemyWeapon.h

public:

	ASlAiEnemyWeapon();

SlAiEnemyWeapon.cpp

// 引入头文件
#include "ConstructorHelpers.h"
#include "Components/StaticMeshComponent.h"
#include "Components/BoxComponent.h"


ASlAiEnemyWeapon::ASlAiEnemyWeapon()
{
	static ConstructorHelpers::FObjectFinder<UStaticMesh> StaticBaseMesh(TEXT("StaticMesh'/Game/Res/PolygonAdventure/Meshes/SM_Wep_GreatAxe_01.SM_Wep_GreatAxe_01'"));
	BaseMesh->SetStaticMesh(StaticBaseMesh.Object);

	BaseMesh->SetRelativeLocation(FVector(-38.f, 9.6f, 9.8f));
	BaseMesh->SetRelativeRotation(FRotator(-10.f, 76.5f, -99.f));
	BaseMesh->SetRelativeScale3D(FVector(0.75f));

	AffectCollision->SetRelativeLocation(FVector(0.f, 0.f, 158.f));
	AffectCollision->SetRelativeScale3D(FVector(1.125f, 0.22f, 1.f));
}

SlAiEnemyShield.h

public:

	ASlAiEnemyShield();

SlAiEnemyShield.cpp

// 引入头文件
#include "ConstructorHelpers.h"
#include "Components/StaticMeshComponent.h"
#include "Components/BoxComponent.h"

ASlAiEnemyShield::ASlAiEnemyShield()
{
	// 这里没办法,要用这个地址的 Sheild,除非到项目里把资源名改了
	static ConstructorHelpers::FObjectFinder<UStaticMesh> StaticBaseMesh(TEXT("StaticMesh'/Game/Res/PolygonAdventure/Meshes/SM_Wep_Sheild_01.SM_Wep_Sheild_01'"));	
	BaseMesh->SetStaticMesh(StaticBaseMesh.Object);

	BaseMesh->SetRelativeLocation(FVector(53.f, -3.f, -9.f));
	BaseMesh->SetRelativeRotation(FRotator(0.f, 90.f, 90.f));

	AffectCollision->SetRelativeLocation(FVector(0.f, 0.f, 43.f));
	AffectCollision->SetRelativeScale3D(FVector(0.8125f, 0.156f, 1.344f));
}

完善一下敌人类。并且给敌人类添加两个子 Actor 组件,用于安放敌人使用的武器和盾牌的 Actor。

SlAiEnemyCharacter.h

class SLAICOURSE_API ASlAiEnemyCharacter : public ACharacter
{
	GENERATED_BODY()

public:

	ASlAiEnemyCharacter();
	
	// 将这俩方法移上来
	virtual void Tick(float DeltaTime) override;

	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

protected:

	virtual void BeginPlay() override;

protected:

	// 武器插槽
	UPROPERTY(VisibleDefaultsOnly, Category = "Mesh")
	class UChildActorComponent* WeaponSocket;

	// 盾牌插槽
	UPROPERTY(VisibleDefaultsOnly, Category = "Mesh")
	class UChildActorComponent* ShieldSocket;
}

在构造函数里指定敌人的 AIController。

SlAiEnemyCharacter.cpp

// 添加头文件
#include "ConstructorHelpers.h"
#include "Components/CapsuleComponent.h"
#include "Components/SkeletalMeshComponent.h"
#include "Components/BoxComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "Perception/PawnSensingComponent.h"	// 这个感知组件后面才会用到

#include "SlAiEnemyController.h"

#include "SlAiEnemyTool.h"

ASlAiEnemyCharacter::ASlAiEnemyCharacter()
{
	PrimaryActorTick.bCanEverTick = true;

	// 设置 AI 控制器
	AIControllerClass = ASlAiEnemyController::StaticClass();

	// 设置碰撞体属性文件
	GetCapsuleComponent()->SetCollisionProfileName(FName("EnemyProfile"));
	GetCapsuleComponent()->SetGenerateOverlapEvents(true);

	// 添加模型
	static ConstructorHelpers::FObjectFinder<USkeletalMesh> StaticEnemyMesh(TEXT("SkeletalMesh'/Game/Res/PolygonAdventure/Mannequin/Enemy/SkMesh/Enemy.Enemy'"));
	GetMesh()->SetSkeletalMesh(StaticEnemyMesh.Object);
	GetMesh()->SetCollisionObjectType(ECC_Pawn);
	GetMesh()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
	GetMesh()->SetCollisionResponseToAllChannels(ECR_Ignore);
	GetMesh()->SetRelativeLocation(FVector(0.f, 0.f, -95.f));
	GetMesh()->SetRelativeRotation(FQuat::MakeFromEuler(FVector(0.f, 0.f, -90.f)));

	// 添加动画蓝图
	static ConstructorHelpers::FClassFinder<UAnimInstance> StaticEnemyAnim(TEXT("AnimBlueprint'/Game/Blueprint/Enemy/Enemy_Animation.Enemy_Animation_C'"));
	GetMesh()->AnimClass = StaticEnemyAnim.Class;

	// 实例化插槽
	WeaponSocket = CreateDefaultSubobject<UChildActorComponent>(TEXT("WeaponSocket"));
	ShieldSocket = CreateDefaultSubobject<UChildActorComponent>(TEXT("ShieldSocket"));
}

void ASlAiEnemyCharacter::BeginPlay()
{
	Super::BeginPlay();

	// 绑定插槽
	WeaponSocket->AttachToComponent(GetMesh(), FAttachmentTransformRules::SnapToTargetNotIncludingScale, FName("RHSocket"));
	ShieldSocket->AttachToComponent(GetMesh(), FAttachmentTransformRules::SnapToTargetNotIncludingScale, FName("LHSocket"));

	// 给插槽添加物品
	WeaponSocket->SetChildActorClass(ASlAiEnemyTool::SpawnEnemyWeapon());
	ShieldSocket->SetChildActorClass(ASlAiEnemyTool::SpawnEnemyShield());
}

最后就是给敌人动画类写个构造函数,声明两个运动的参数,一个保存敌人角色的指针,以及重写更新参数的方法。

SlAiEnemyAnim.h

public:

	USlAiEnemyAnim();

	virtual void NativeUpdateAnimation(float DeltaSeconds) override;

public:

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "EnemyAnim")
	float Speed;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "EnemyAnim")
	float IdleType;

protected:

	// 保存角色
	class ASlAiEnemyCharacter* SECharacter;

SlAiEnemyAnim.cpp

// 添加头文件
#include "SlAiEnemyCharacter.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "Animation/AnimSequence.h"
#include "ConstructorHelpers.h"

USlAiEnemyAnim::USlAiEnemyAnim()
{
	// 初始化参数
	Speed = 0.f;
	IdleType = 0.f;
}

void USlAiEnemyAnim::NativeUpdateAnimation(float DeltaSeconds)
{
	// 初始化角色指针
	if (!SECharacter) SECharacter = Cast<ASlAiEnemyCharacter>(TryGetPawnOwner());
	if (!SECharacter) return;

	// 设置速度
	Speed = SECharacter->GetVelocity().Size();
}

动画蓝图调整如下:

敌人动画蓝图
此时将敌人类拖到场景,运行游戏,可看见敌人已经拿好了武器和盾牌,姿势正处于 Idle 动画。

敌人 Character

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值