斯坦福 UE4 C++ ActionRoguelike游戏实例教程 10.5.作业五 为游戏添加一个积分系统,随机生成增益道具
斯坦福课程 UE4 C++ ActionRoguelike游戏实例教程 0.绪论
概述
本篇文章将解决作业五提出的问题,使用PlayerState,在原本游戏的基础上引入积分系统,实现击杀敌人得分,拾取道具扣分,以及创建一个积分道具(金币),使用EQS在游戏开始时随机生成数个回血和积分道具。

目录
- 前情提要
- 自定义PlayerState
- 修改击杀函数
- 血瓶添加积分花费
- 添加积分道具
- 绑定UI事件
- 设置环境查询(EQS)
- 游戏开始自动生成道具
前情提要
在跟随这篇文章实现作业要求前,要求你的游戏项目中拥有以下要素:
- 实现了交互接口(Interact GameInterface),允许角色与实现了交互接口的Actor进行交互。
- 能够实现场景增益道具(PowerUpBase),笔者已经实现了基类的定义和回血道具的功能。这部分内容可以参考作业三的实现。
- 简单实现了积分(Credit)的UMG控件,这将有助于观察实现的结果。
在作业五的要求中,我们要实现以下内容:
- 在玩家杀死敌对AI时获取一定积分
- 使用回血道具时扣除一定积分
- 实现“Coin”道具,在拾取时给予玩家一定积分
- 在UI中实时展现玩家目前拥有的积分
- 使用EQS在游戏开始时随机生成一定数量的增益道具(包括回血瓶和金币等)
本文仅对笔者认为值得的地方进行详细说明。一些略过的地方,比如一眼可以看明白的代码或者蓝图逻辑,笔者就不进行赘述了。
自定义PlayerState
我们将使用PlayerState来定义积分系统,包括玩家拥有的积分,获得、失去积分等功能。
PlayerState通常用于保存各个玩家的各种数据。如果查看其源码的话,可以发现里面很多功能都是为网络同步服务的。目前我们还处于单机阶段,只需要将其看做是一个专门记录玩家数据的类即可。
PlayerState拥有以下值得注意的特点:
- Pawn和Controller都有一个PlayerState成员
- PlayerState由Controller在InitPlayerState时创建,而Pawn中的PlayerState一般都是Controller创建Pawn的时候赋值给Pawn的。也就是说,PlayerState不归Pawn管,而是归Controller管。
- PlayerState 的getowner函数返回的是 controller
- 可以调用APawn或者AController的GetPlayerState获得PlayerState。
//.h
//定义一个三个参数的多播委托,将其命名为FOnCreditsChanged类型。
//当调用这个委托时,执行注册在委托里的所有函数
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnCreditsChanged, ASurPlayerState*, PlayerState, int32, NewCredits, int32, Delta);
UCLASS()
class FPSPROJECT_API ASurPlayerState : public APlayerState
{
GENERATED_BODY()
protected:
UPROPERTY(EditDefaultsOnly, Category = "Credits")
int32 Credits;
public:
UFUNCTION(BlueprintCallable, Category = "Credits")
int32 GetCredits() const;
UFUNCTION(BlueprintCallable, Category = "Credits")
bool AddCredits(int32 Delta);
UFUNCTION(BlueprintCallable, Category = "Credits")
bool RemoveCredits(int32 Delta);
//BlueprintAssignable 只能与多播委托共用。可以在蓝图中指定函数
UPROPERTY(BlueprintAssignable, Category = "Events")
FOnCreditsChanged OnCreditsChanged;
};
//.cpp
bool ASurPlayerState::AddCredits(int32 Delta)
{
if(!ensure(Delta >= 0))
{
return false;
}
Credits += Delta;
OnCreditsChanged.Broadcast(this, Credits, Delta);
return true;
}
bool ASurPlayerState::RemoveCredits(int32 Delta)
{
if(!ensure(Delta >= 0))
{
return false;
}
//如果积分少于要扣除的量,那么是不被允许的
if(Credits < Delta)
{
return false;
}
Credits -= Delta;
OnCreditsChanged.Broadcast(this, Credits, Delta);
return true;
}
修改击杀函数
之前在SurGameModeBase中我们定义了一个静态函数OnActorKilled,它将会在有角色被杀死时调用。
这里添加杀死其他角色时尝试增加杀手的积分。当受害者被杀手杀死时,会试图获取杀手的SurPlayerState,并增加其拥有的积分。
void ASurGameModeBase::OnActorKilled(AActor* VictimActor, AActor* Killer)
{
AMyCharacter* Player = Cast<AMyCharacter>(VictimActor);
if(Player)
{
//...重生相关逻辑
}
//尝试获取Killer的PlayerState,只有成功获取了才能添加积分
APawn* KillerPawn = Cast<APawn>(Killer);
if(KillerPawn)
{
ASurPlayerState* PS = Cast<ASurPlayerState>(KillerPawn->GetPlayerState());
if(PS)
{
PS->AddCredits(CreditsPerKill);
}
}
UE_LOG(LogTemp, Log, TEXT("OnActorKilled:Victim:%s, Killer: %s"), *GetNameSafe(VictimActor), *GetNameSafe(Killer));
}
血瓶添加积分花费
这部分修改很简单,在玩家与血瓶互动时会尝试扣除玩家的积分,只有扣除成功了才会将血瓶隐藏起来,表示血瓶已经使用了。
UCLASS()
class FPSPROJECT_API ASurPowerUp_HealthPotion : public ASurPowerUpBase
{
GENERATED_BODY()
public:
ASurPowerUp_HealthPotion();
protected:
UPROPERTY(EditAnywhere)
int32 CreditCost;
virtual void Interact_Implementation(APawn* InstigatorPawn) override;
};
void ASurPowerUp_HealthPotion::Interact_Implementation(APawn* InstigatorPawn)
{
AMyCharacter* MyCharacter = Cast<AMyCharacter>(InstigatorPawn);
if(!ensure(MyCharacter)) return;
//获得Attribute
USurAttributeComponent* AttributeComponent = Cast<USurAttributeComponent>(MyCharacter->GetComponentByClass(USurAttributeComponent::StaticClass()));
if(ensure(AttributeComponent))
{
if(ASurPlayerState* PS = Cast<ASurPlayerState>(MyCharacter->GetPlayerState()))
{
if(AttributeComponent->ApplyHealthChanges(this, HealingValue) && PS->RemoveCredits(CreditCost))
{
//只有使用成功了才暂时将血瓶隐藏起来
HideAndCooldownPowerup();
}
}
}
}
添加积分道具
积分道具的定义比血瓶的还简单,我将其命名为SurPowerUp_CreditsUp
//SurPowerUp_CreditsUp.h
class FPSPROJECT_API ASurPowerUp_CreditsUp : public ASurPowerUpBase
{
GENERATED_BODY()
public:
ASurPowerUp_CreditsUp();
virtual void Interact_Implementation(APawn* InstigatorPawn) override;
protected:
UPROPERTY(EditAnywhere, Category = "Credits")
int32 CreditsValue;
};
//.cpp
ASurPowerUp_CreditsUp::ASurPowerUp_CreditsUp()
{
CreditsValue = 100;
bOnlyOnce = false;
}
void ASurPowerUp_CreditsUp::Interact_Implementation(APawn* InstigatorPawn)
{
if(!ensure(InstigatorPawn))
{
return;
}
if(ASurPlayerState* PS = InstigatorPawn->GetPlayerState<ASurPlayerState>())
{
if(PS->AddCredits(CreditsValue))
{
HideAndCooldownPowerup();
}
}
}

绑定UI事件
接下来进入编辑器。为了检验刚才的成果,我们要实现UI中Credits控件的绑定,这样才能实时查看我们当前拥有的积分。
首先需要将我们的PlayerState添加到GameMode中,这样游戏自动创建的玩家控制器才会带有指定的玩家状态类:

进入Credit_Widget的事件图表中,将其绑定到SurPlayerState中的OnCreditsChanged委托上。蓝图如下

如果找不到如何绑定事件,可以在获取PlayetState后,从后面拉出一条线,找到分配事件即可绑定自定义事件:

调整好各个属性值,进入游戏查看效果。

设置环境查询(EQS)
接下来实现在游戏开始时随机生成数个道具。首先创建EQS,起名为Query_FindPowerUpSpawn。在测试内容中,选择PathExist,只有在导航网格中能够到达的区域才会被选为查询结果。

其中查询内容是我自定义的,这里根据玩家出生点来选择EQS查询的范围。

在编辑器中使用EQSTestPawn确认一下查询范围。

游戏开始自动生成道具
在之前的AI生成部分,我们已经做过类似的东西。最大的区别,不过是在游戏开始时一口气生成多个道具罢了。
简单讲讲OnSpawnPowerUpQueryCompleted函数。该函数的作用是在执行一个环境查询后,根据查询返回的位置结果生成一定数量的道具(PowerUp)物体。
具体来说,该函数首先检查查询的状态是否成功,如果不是,则会输出一个警告信息。然后,从查询实例中获取位置结果,并将其存储在Locations数组中。
接下来,使用while循环来生成道具。循环条件是需要生成的道具数小于指定的数量DesiredPowerupCount且位置数组Locations不为空。在每次迭代中,从Locations数组中随机选择一个位置,并将该位置存储在PickedLocation变量中。然后,从PowerupClasses数组中随机选择一个类别,并将其存储在RandomPowerupClass变量中。接着,创建一个SpawnParams对象并设置SpawnCollisionHandlingOverride参数以处理生成的物体的碰撞行为。最后调用GetWorld()方法生成一个ASurPowerUpBase对象,并将它放置在选定位置的稍微上方50个单位处。生成的道具计数器SpawnCounter加1,这样循环就可以继续进行,直到达到指定的生成数量为止。
当然,如果你有闲情的话,可以为道具生成添加更多个性化的设置,比如限制道具之间的生成距离之类的。
void ASurGameModeBase::StartPlay()
{
Super::StartPlay();
//循环调用定时器,生成AI小兵
GetWorldTimerManager().SetTimer(TimerHandle_SpawnBots, this, &ASurGameModeBase::SpawnBotTimerElapsed, SpawnTimerInterval, true);
//生成PowerUp对象
if(ensure(PowerupClasses.Num() > 0))
{
UEnvQueryInstanceBlueprintWrapper* QueryInstance = UEnvQueryManager::RunEQSQuery(this, SpawnPowerUpQuery, this, EEnvQueryRunMode::AllMatching, nullptr);
if(ensure(QueryInstance))
{
QueryInstance->GetOnQueryFinishedEvent().AddDynamic(this, &ASurGameModeBase::OnSpawnPowerUpQueryCompleted);
}
}
}
void ASurGameModeBase::OnSpawnPowerUpQueryCompleted(UEnvQueryInstanceBlueprintWrapper* QueryInstance,
EEnvQueryStatus::Type QueryStatus)
{
if(QueryStatus != EEnvQueryStatus::Success)
{
UE_LOG(LogTemp, Warning, TEXT("Spawn Powerup EQS Query Failed!"));
}
//环境查询得到的所有坐标
TArray<FVector> Locations = QueryInstance->GetResultsAsLocations();
//要生成增益道具的所有坐标
TArray<FVector> UseLocation;
int32 SpawnCounter = 0;
while(SpawnCounter < DesiredPowerupCount && Locations.Num() > 0)
{
//随机选取环境查询结果的一个坐标
int32 RandomLocationIndex = FMath::RandRange(0, Locations.Num() - 1);
FVector PickedLocation = Locations[RandomLocationIndex];
//为了防止重复选取,将选取的坐标从数组中删掉
Locations.RemoveAt(RandomLocationIndex);
//随机选择一个Powerup进行生成
int32 RandomClassIndex = FMath::RandRange(0, PowerupClasses.Num() - 1);
TSubclassOf<ASurPowerUpBase> RandomPowerupClass = PowerupClasses[RandomClassIndex];
FActorSpawnParameters SpawnParams;
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
GetWorld()->SpawnActor<ASurPowerUpBase>(RandomPowerupClass, PickedLocation + FVector(0, 0, 50.f), FRotator::ZeroRotator, SpawnParams);
SpawnCounter++;
}
}
最后是设置GameMode蓝图,这些成员都是在C++代码中添加的。

进入游戏,可以看到在游戏一开始的时候就生成了多个增益道具。

碎碎念
笔者在写下本系列第一篇文章的时候,想的是将自己在UE4的学习过程中所学的知识梳理出来,并将学到的知识稍作拓展,希望能拓展成更加”通用“的东西,最后形成一套自己的体系,并将其输出为其他人所用。因此,我希望在每一篇文章中,能够尽可能的插入更多UE中的知识点,随着课程老师的进度逐步点亮沿途的”科技树“,并让每一篇文章都拥有自己的主题,即使抛开课程项目的背景,也能让读者从文章中尽可能的学到一点东西。
遗憾的是,越写到后面,越发觉得力不从心。一方面想事无巨细地展现当前课程所涉及到的知识点,一方面又想跟随课程的进度,完成当前应该掌握的内容。最近发现文章里复制粘贴代码的占比越来越大,体现UE理解的相关段落越来越少,应该说是部分课程内容越来越倾向于同质化呢,还是说笔者个人越来越松懈呢?目前还是希望能从大段代码和知识点之间找到一个平衡点,希望能够早日写出优质的文章。
有时候都想用AI来帮我写代码的逻辑了,感觉很多一眼能看明白的代码,我都不确定要不要对其详细展开,这可能就是知识的诅咒吧。
斯坦福 UE4 C++ ActionRoguelike游戏实例教程 10.5.作业五 为游戏添加一个积分系统,随机生成增益道具的更多相关文章
- XAML实例教程系列 - 事件(Event) 五
Kevin Fan分享开发经验,记录开发点滴 XAML实例教程系列 - 事件(Event) 2012-06-19 01:36 by jv9, 1727 阅读, 7 评论, 收藏, 编辑 Events, ...
- Cocos2d-x3.0游戏实例《不要救我》第十篇(结束)——使用Json配置数据类型的怪物
如今我们有2种类型的怪物,并且创建的时候是写死在代码里的,这是要作死的节奏~ 所以.必须可配置.不然会累死人的. ; i < size; ++i) { int id = root[i][&quo ...
- Cocos2d-x3.0游戏实例之《别救我》第八篇——TiledMap实现关卡编辑器
版权声明:本文为博主原创文章,未经博主同意不得转载. https://blog.csdn.net/musicvs/article/details/25368273 好吧.我真心全然搞不懂.我如今仅仅只 ...
- BPEL 实例教程
http://www.oracle.com/technetwork/cn/articles/matjaz-bpel1-090722-zhs.html BPEL 实例教程 作者:Matjaz Juric ...
- Delphi通过ADO链接数据库及对数据库的增加,删除,修改,读取操作实例教程4
ADO是一种程序对象,用于表示用户数据库中的数据结构和所包含的数据.ADO(ActiveXDataObjects,ActiveX数据对象)是Microsoft提出的应用程序接口(API)用以实现访问关 ...
- 【JavaScript】随机生成10个0~100的数字
随机生成10个0~100不重复的数字(包含0和100): 需要用到的知识点:随机数 去重 下面放代码 <!DOCTYPE html> <html> <head> & ...
- 如何练习python?有这五个游戏,实操经验就已经够了
现在学习python的人越来越多了,但仅仅只是学习理论怎么够呢,如何练习python?已经是python初学者比较要学会的技巧了! 其实,最好的实操练习,就是玩游戏. 也许你不会信,但这五个小游戏足够 ...
- Web 开发中应用 HTML5 技术的10个实例教程
HTML5 作为下一代网站开发技术,无论你是一个 Web 开发人员或者想探索新的平台的游戏开发者,都值得去研究.借助尖端功能,技术和 API,HTML5 允许你创建响应性.创新性.互动性以及令人惊叹的 ...
- Unreal Engine 4 系列教程 Part 10:制作简单FPS游戏
.katex { display: block; text-align: center; white-space: nowrap; } .katex-display > .katex > ...
- 《Genesis-3D开源游戏引擎完整实例教程-跑酷游戏篇:简介及目录》(附上完整工程文件)
跑酷游戏制作 游戏类型: 此游戏Demo,为跑酷类游戏. 框架简介: 游戏通常由程序代码和资源组成.如果说模型.贴图.声音之类的可以给游戏环境提供一个物理描述和设置,那么脚本和代码块会给游戏赋予生命, ...
随机推荐
- strcpy()函数详解
strcpy()函数是C语言中的一个复制字符串的库函数,以下将详细解释说明一下: · 函数声明以及实现代码 char *strcpy(char *dst, const char *src);char ...
- 10.4 认识Capstone反汇编引擎
Capstone 是一款开源的反汇编框架,目前该引擎支持的CPU架构包括x86.x64.ARM.MIPS.POWERPC.SPARC等,Capstone 的特点是快速.轻量级.易于使用,它可以良好地处 ...
- DFS(深度优先搜索)洛谷P1162
看大佬们dfs一遍就出结果,蒟蒻的我dfs了三遍,当然这题也可以用bfs做,但是dfs不用队列代码短一些. #include <iostream> #include <vector& ...
- c#组合模式详解
基础介绍: 组合模式用于表示部分-整体的层次结构.适用于希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象的情况. 顾名思义,什么叫部分-整体,比如常见的前端UI,一个 ...
- [vue]精宏技术部试用期学习笔记 III
精宏技术部试用期学习笔记(vue) 父子通信 什么是通信 / 为什么要通信 通信即在不同组件之间传输数据 当在 复用组件 时,需要传递不同数据达成不同的表现效果 能够根据其他组件的行动,响应式 的做出 ...
- websocket和ajax的区别(和http的区别)
websocket和ajax的区别(和http的区别) https://segmentfault.com/a/1190000021741131 1. 本质不同 ajax,即异步JavaScript和X ...
- [Python急救站课程]五角星的绘制
五角星的绘制 from turtle import * # 从turtle库中导入所有函数 fillcolor("red") # 表示填充红色 begin_fill() # 表示开 ...
- 2023 PolarD&N靶场通关笔记 Crypto
简单类: keyboard2.0 21 31 41 53 63 73 62 72 82 81 91 01 51 61 71 提示:解出来的内容转换为md5值后加上flag{} 数字被分成五组,每组包含 ...
- L3-002 特殊堆栈
#include <bits/stdc++.h> using namespace std; const int N = 1E5 + 10; int tr[N]; stack<int& ...
- 文心一言 VS 讯飞星火 VS chatgpt (139)-- 算法导论11.4 3题
三.用go语言,考虑一个采用均匀散列的开放寻址散列表.当装载因子为 3/4 和 7/8 时,试分别给出一次不成功查找和一次成功查找的探查期望数上界. 文心一言: 在开放寻址法中,当散列表的装载因子超过 ...