斯坦福课程 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. Strimzi Kafka Bridge(桥接)实战之二:生产和发送消息

    欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos 本篇概览 本文是<Strimzi Kafka B ...

  2. destoon根据目录下的html文件生成地图索引

    因为项目需要,destoon根据目录下的html文件生成地图索引,操作方法,代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ...

  3. 【matplotlib 实战】--柱状图

    柱状图,是一种使用矩形条,对不同类别进行数值比较的统计图表.在柱状图上,分类变量的每个实体都被表示为一个矩形(通俗讲即为"柱子"),而数值则决定了柱子的高度. 1. 主要元素 柱状 ...

  4. Go 项目代码布局

    Go 项目代码布局 目录 Go 项目代码布局 一.Go 语言"创世项目"结构 1.1 src 目录结构三个特点 二.Go 项目布局演进 2.1 演进一:Go 1.4 版本删除 pk ...

  5. Python开发之Django框架

    一. Django框架 01.网络软件开发架构演变过程 02.HTTP协议讲解 03.web应用与框架介绍及手撸web框架 04.Django入门项目创建与必会三板斧 05.Django静态文件配置与 ...

  6. es针对nested类型数据无法进行过滤查询的问题记录

    问题描述 es中存在有一个名为task_data_1的索引,其字段映射关系如下所示: { "task_data_1" : { "mappings" : { &q ...

  7. July 2023 (version 1.81)

    July 2023 (version 1.81) 更新后显示发行说明 Update 1.81.1: The update addresses these issues. Welcome to the ...

  8. 自己把源码生成jar,在android项目中调用

    项目源码下载地址 看了很多,找了很多,都是没有自己想要的效果的,不容易啊,备注下吧. 1.自己的源码 ,java文件,里边有各种方法,生成jar,可以分享给别人使用. 2.目前主要验证在android ...

  9. JDK对于Java的作用

    JDK是Java Development Kit的缩写,是Java的开发工具包(SDK).JDK 是整个 Java 的核心,包括 Java 运行环境(Java Runtime Envirnment,简 ...

  10. Linux常用命令(包含学习资源)

    目录 (0)学习资源 (一)查看系统信息 (二)文件和目录 (三)文件搜索 (四)挂载一个文件系统 (五)磁盘空间 (六)用户和群组 (七)文件的权限 - 使用 "+" 设置权限, ...