斯坦福 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,为跑酷类游戏. 框架简介: 游戏通常由程序代码和资源组成.如果说模型.贴图.声音之类的可以给游戏环境提供一个物理描述和设置,那么脚本和代码块会给游戏赋予生命, ...
随机推荐
- 圆柱坐标系(Cylindrical Coordinate System)
参考:维基百科 圆柱坐标系(英语:cylindrical coordinate system)是一种三维坐标系统.它是二维极坐标系往 z-轴的延伸.添加的第三个坐标 \(z\) 专门用来表示 P 点离 ...
- 【Cucumber】关于BDD自然语言自动化测试的语法总结
1.关键字 - Feature 每一个.feature文件必须以关键字Feature开始,Feature关键字之后可以添加该feature的描述,其作用类似于注释,仅仅为了便于理解沟通交流,描述内容中 ...
- oracle 命令4 热备份
热备份:不停数据库,一定范围内(内存写满后,用户操作挂起),不影响用户使用.数据库开着,时刻变化,因此热备份通过以下手段保持一致性:1.开启归档2.备份前,通知数据库,把文件锁定,在备份未完成前,不操 ...
- 合并果子(lgP1090)
贪心. 每次取最小的两堆合并,最后即为正确答案.(我也不会证明/wq) 所以说主要问题就是怎么找最小的两堆. 由于中间不断有插入和删除,所以用优先队列. 扯不下去了 直接看代码吧. #include& ...
- 天上掉 Pizza
实在不知道错哪了... 对着 std 检查了好几遍了QAQ 题解见注释(不过估计题解也是错的,不然为什么写错啊QAQ #include<bits/stdc++.h> using names ...
- Python 轻松生成PDF文档
PDF(Portable Document Format)是一种常用的文档格式,具有跨平台兼容性.保真性.安全性和交互性等特点.我们日常生活工作中的合同.报告.论文等通常都采用PDF格式,以确保文档在 ...
- 红瞳瞳CRUD Avue各参数作用
常用的两个avue文档: avue 开发文档: https://www.bookstack.cn/read/avue-2.x/3c22e1c01099c1f1.md avue开发指南:https: ...
- 猜单词游戏(c++)
#include<iostream> #include<string> #include<fstream> #include<cstdlib> #inc ...
- 使用 redis 实现分布式接口限流注解 RedisLimit
前言 很多时候,由于种种不可描述的原因,我们需要针对单个接口实现接口限流,防止访问次数过于频繁.这里就用 redis+aop 实现一个限流接口注解 @RedisLimit 代码 点击查看RedisLi ...
- LabVIEW基于机器视觉的实验室设备管理系统(3)
目录 行动计划 创建用户信息数据库 后面板连线 初始化 确认修改 确认id 判断旧密码是否正确 判断两次输入的新密码是否相同 修改用户数据库中的密码 结尾 效果演示 上一期我们完成了欢迎登录和信息查询 ...