斯坦福课程 UE4 C++ ActionRoguelike游戏实例教程 0.绪论

概述

本篇文章将解决作业五提出的问题,使用PlayerState,在原本游戏的基础上引入积分系统,实现击杀敌人得分,拾取道具扣分,以及创建一个积分道具(金币),使用EQS在游戏开始时随机生成数个回血和积分道具。

作业说明

目录

  1. 前情提要
  2. 自定义PlayerState
  3. 修改击杀函数
  4. 血瓶添加积分花费
  5. 添加积分道具
  6. 绑定UI事件
  7. 设置环境查询(EQS)
  8. 游戏开始自动生成道具

前情提要

在跟随这篇文章实现作业要求前,要求你的游戏项目中拥有以下要素:

  1. 实现了交互接口(Interact GameInterface),允许角色与实现了交互接口的Actor进行交互。
  2. 能够实现场景增益道具(PowerUpBase),笔者已经实现了基类的定义和回血道具的功能。这部分内容可以参考作业三的实现。
  3. 简单实现了积分(Credit)的UMG控件,这将有助于观察实现的结果。

在作业五的要求中,我们要实现以下内容:

  1. 在玩家杀死敌对AI时获取一定积分
  2. 使用回血道具时扣除一定积分
  3. 实现“Coin”道具,在拾取时给予玩家一定积分
  4. 在UI中实时展现玩家目前拥有的积分
  5. 使用EQS在游戏开始时随机生成一定数量的增益道具(包括回血瓶和金币等)

本文仅对笔者认为值得的地方进行详细说明。一些略过的地方,比如一眼可以看明白的代码或者蓝图逻辑,笔者就不进行赘述了。

自定义PlayerState

我们将使用PlayerState来定义积分系统,包括玩家拥有的积分,获得、失去积分等功能。

PlayerState通常用于保存各个玩家的各种数据。如果查看其源码的话,可以发现里面很多功能都是为网络同步服务的。目前我们还处于单机阶段,只需要将其看做是一个专门记录玩家数据的类即可。

PlayerState拥有以下值得注意的特点:

  1. Pawn和Controller都有一个PlayerState成员
  2. PlayerState由Controller在InitPlayerState时创建,而Pawn中的PlayerState一般都是Controller创建Pawn的时候赋值给Pawn的。也就是说,PlayerState不归Pawn管,而是归Controller管。
  3. PlayerState 的getowner函数返回的是 controller
  4. 可以调用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中,这样游戏自动创建的玩家控制器才会带有指定的玩家状态类:

千万别忘了修改PlayerState

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

蓝图节点

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

从PlayerState拉出一条线,这样才能找到分配事件

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

注意看左下角

设置环境查询(EQS)

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

Query_FindPowerUpSpawn 相关

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

QueryContext_PlayerStart 相关

在编辑器中使用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++代码中添加的。

设置GameMode

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

最终效果

碎碎念

笔者在写下本系列第一篇文章的时候,想的是将自己在UE4的学习过程中所学的知识梳理出来,并将学到的知识稍作拓展,希望能拓展成更加”通用“的东西,最后形成一套自己的体系,并将其输出为其他人所用。因此,我希望在每一篇文章中,能够尽可能的插入更多UE中的知识点,随着课程老师的进度逐步点亮沿途的”科技树“,并让每一篇文章都拥有自己的主题,即使抛开课程项目的背景,也能让读者从文章中尽可能的学到一点东西。

遗憾的是,越写到后面,越发觉得力不从心。一方面想事无巨细地展现当前课程所涉及到的知识点,一方面又想跟随课程的进度,完成当前应该掌握的内容。最近发现文章里复制粘贴代码的占比越来越大,体现UE理解的相关段落越来越少,应该说是部分课程内容越来越倾向于同质化呢,还是说笔者个人越来越松懈呢?目前还是希望能从大段代码和知识点之间找到一个平衡点,希望能够早日写出优质的文章。

有时候都想用AI来帮我写代码的逻辑了,感觉很多一眼能看明白的代码,我都不确定要不要对其详细展开,这可能就是知识的诅咒吧。

斯坦福 UE4 C++ ActionRoguelike游戏实例教程 10.5.作业五 为游戏添加一个积分系统,随机生成增益道具的更多相关文章

  1. XAML实例教程系列 - 事件(Event) 五

    Kevin Fan分享开发经验,记录开发点滴 XAML实例教程系列 - 事件(Event) 2012-06-19 01:36 by jv9, 1727 阅读, 7 评论, 收藏, 编辑 Events, ...

  2. Cocos2d-x3.0游戏实例《不要救我》第十篇(结束)——使用Json配置数据类型的怪物

    如今我们有2种类型的怪物,并且创建的时候是写死在代码里的,这是要作死的节奏~ 所以.必须可配置.不然会累死人的. ; i < size; ++i) { int id = root[i][&quo ...

  3. Cocos2d-x3.0游戏实例之《别救我》第八篇——TiledMap实现关卡编辑器

    版权声明:本文为博主原创文章,未经博主同意不得转载. https://blog.csdn.net/musicvs/article/details/25368273 好吧.我真心全然搞不懂.我如今仅仅只 ...

  4. BPEL 实例教程

    http://www.oracle.com/technetwork/cn/articles/matjaz-bpel1-090722-zhs.html BPEL 实例教程 作者:Matjaz Juric ...

  5. Delphi通过ADO链接数据库及对数据库的增加,删除,修改,读取操作实例教程4

    ADO是一种程序对象,用于表示用户数据库中的数据结构和所包含的数据.ADO(ActiveXDataObjects,ActiveX数据对象)是Microsoft提出的应用程序接口(API)用以实现访问关 ...

  6. 【JavaScript】随机生成10个0~100的数字

    随机生成10个0~100不重复的数字(包含0和100): 需要用到的知识点:随机数 去重 下面放代码 <!DOCTYPE html> <html> <head> & ...

  7. 如何练习python?有这五个游戏,实操经验就已经够了

    现在学习python的人越来越多了,但仅仅只是学习理论怎么够呢,如何练习python?已经是python初学者比较要学会的技巧了! 其实,最好的实操练习,就是玩游戏. 也许你不会信,但这五个小游戏足够 ...

  8. Web 开发中应用 HTML5 技术的10个实例教程

    HTML5 作为下一代网站开发技术,无论你是一个 Web 开发人员或者想探索新的平台的游戏开发者,都值得去研究.借助尖端功能,技术和 API,HTML5 允许你创建响应性.创新性.互动性以及令人惊叹的 ...

  9. Unreal Engine 4 系列教程 Part 10:制作简单FPS游戏

    .katex { display: block; text-align: center; white-space: nowrap; } .katex-display > .katex > ...

  10. 《Genesis-3D开源游戏引擎完整实例教程-跑酷游戏篇:简介及目录》(附上完整工程文件)

    跑酷游戏制作 游戏类型: 此游戏Demo,为跑酷类游戏. 框架简介: 游戏通常由程序代码和资源组成.如果说模型.贴图.声音之类的可以给游戏环境提供一个物理描述和设置,那么脚本和代码块会给游戏赋予生命, ...

随机推荐

  1. Django框架——模板层

    文章目录 1 模板层 一 模版简介 二 模版语法之变量 views.py html文件 三 模版之过滤器 语法: default length filesizeformat date slice tr ...

  2. Django框架——Web应用、基于SOCKET写一个web应用、 手撸简单web框架、http协议、Web框架(手撸自己的Web框架)、django简介以及简单使用

    文章目录 1 Web应用 一 Web应用程序是什么 1.1 Web应用程序的优点 1.2 Web应用程序的缺点 1.3 B/S架构优点 二 基于SOCKET写一个web应用 2.1 main.py 2 ...

  3. MySQL系列之——MySQL体系结构、基础管理(用户、权限管理、连接管理、多种启动方式介绍、初始化配置、多实例的应用)

    文章目录 一 体系结构 1.1 C/S(客户端/服务端)模型介绍 1.2 实例介绍 1.3 mysqld程序运行原理 1.3.1 mysqld程序结构 1.3.2 一条SQL语句的执行过程 1.3.2 ...

  4. ubuntu实时查看网速

    可以使用ifstat这个命令 安装 apt install ifstat   1 使用,直接打命令就行 ifstat

  5. My Code Style

    大家都在写,跟风. 头文件 万能头.因为我刚学 OI 的时候怎么都背不住 algorithm 怎么拼( 变量 数组开全局,一些前后重名/只在某一部分用的变量开局部. 尽量不使用指针/ stl 迭代器等 ...

  6. 装箱问题(lgP1049)

    01背包问题. 与模板不同,这道题要求的是最小剩余空间,也就是求背包里最多能放多少东西. 所以状态转移方程变为 fi= fi - w[i] + wi . 其中 fi 表示当背包容量为 i 时可放的最大 ...

  7. LooklessControl

    Lookless controls vs User Controls. Lookless controls usage patterns(LooklessControl与UserControl的比较. ...

  8. umich cv-6-1 循环神经网络基本知识

    这节课中介绍了循环神经网络的第一部分,主要介绍了循环神经网络的基本概念,vanilla循环网络架构,RNN的一些应用,vanilla架构的问题,更先进的rnn架构比如GRU和LSTM 循环神经网络基本 ...

  9. angular:响应式表单(Reactive Forms)和模板驱动表单(Template-Driven Forms)分别进行验证

    2022-01-18 响应式表单 响应式表单是围绕Observable的流构建的. 使用响应式表单时,FormControl类是最基本的构造类. 在使用响应式表单前,需要先导入 ReactiveFor ...

  10. Opencv中goodFeaturesToTrack函数(Harris角点、Shi-Tomasi角点检测)算子速度的进一步优化(1920*1080测试图11ms处理完成)。

    搜索到某个效果很好的视频去燥的算法,感觉效果比较牛逼,就是速度比较慢,如果能做到实时,那还是很有实用价值的.于是盲目的选择了这个课题,遇到的第一个函数就是角点检测,大概六七年用过C#实现过Harris ...