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

概述

本文对应课程15章,60 - Refining Player Respawns。

在本篇文章中,将会为游戏新增加一个规则,即玩家可以自动产卵,呸,自动重生。

设定玩家重生

在之前的课程中,我们使用GameMode为游戏添加了第一个规则,即自动生成AI小兵。在本节课中,我们将为游戏添加第二个规则,让我们的角色在被打死后能够自动复活从而继续进行游戏。

要实现这个功能也非常简单,主要思路如下:当玩家受到伤害且血量归零时,调用我们在GameMode里定义的OnActorKilled函数,执行玩家死亡后的逻辑。我们想让玩家死亡后在一定时间后重生,因此我们需要设置一个重生时间和一个重生的定时器,并且在OnActorKilled函数中启用定时器,在重生时间结束后调用重生相关的函数,这里我将其定义为RespawnPlayerElapsed(AController* Controller)。函数声明如下:

//SurGameModeBase.h
protect:
UFUNCTION()
void RespawnPlayerElapsed(AController* Controller); UFUNCTION(BlueprintCallable)
virtual void OnActorKilled(AActor* VictimActor, AActor* Killer); UPROPERTY(EditDefaultsOnly)
float RespawnDelay; public:
ASurGameModeBase();

下面是OnActorKilled函数的定义,我们将在角色血量归零的时候调用这个函数。主要的工作就是启用了一个定时器,在重生时间结束后调用RespawnPlayerElapsed

值得一提的是,由于RespawnPlayerElapsed函数是带有参数的,所以我们不能像之前一样直接将函数名作为定时器的参数传进去,而是要定义一个Delegate,将函数名和参数绑定在一起。熟悉C++11的读者应该也见过类似的东西,没错,就是std::bind 函数.

void ASurGameModeBase::OnActorKilled(AActor* VictimActor, AActor* Killer)
{
AMyCharacter* Player = Cast<AMyCharacter>(VictimActor);
if(Player)
{
//没有必要持有Handle,且为了防止多人游戏中handle相互覆盖,这里做成局部变量
FTimerHandle TimerHandle_RespawnDelay;
//Delegate用于需要传参的情况,类比于C++11的Bind函数
FTimerDelegate Delegate;
Delegate.BindUFunction(this, "RespawnPlayerElapsed", Player->GetController()); GetWorldTimerManager().SetTimer(TimerHandle_RespawnDelay, Delegate, RespawnDelay, false);
}
UE_LOG(LogTemp, Log, TEXT("OnActorKilled:Victim:%s, Killer: %s"), *GetNameSafe(VictimActor), *GetNameSafe(Killer));
}

接下来是RespawnPlayerElapsed函数。我们想让控制死亡角色的控制器重新获得一个新的角色,并且控制器的生命周期往往长于角色的生命周期,因此这里需要传入控制器的指针,释放它所控制的角色,并重新生成一个。

//之所以传入Controller,是因为我们不能保证玩家角色是否在计时结束后已经被销毁
void ASurGameModeBase::RespawnPlayerElapsed(AController* Controller)
{
if(ensure(Controller))
{
//作用之一就是将pawn成员设为null
Controller->UnPossess();
//如果控制器拥有一个Pawn,则获取pawn的旋转作为控制器的新旋转
//如果控制器不拥有,则选择一个出生点,新生成一个pawn
RestartPlayer(Controller);
}
}

至于如何调用OnActorKilled函数,下面给出了使用的案例,并且对USurAttributeComponent::ApplyHealthChange作出了较多的修改,供读者参考。

当角色的血量归零时,就会获取当前游戏的GameMode,并调用GameMode类定义的OnActorKilled函数,将攻击双方的指针作为参数传进去。

值得一提的是,所有的带有USurAttributeComponent的Actor都有可能执行OnActorKilled,但是并非所有Actor都是玩家角色,因此才需要在OnActorKilled函数里对Actor进行类型转换并进行判断,随手判空绝对是一个好习惯。

bool USurAttributeComponent::ApplyHealthChanges(AActor* InstigatorActor, float Delta)
{
if(!GetOwner()->CanBeDamaged() && Delta < 0.f)
{
return false;
}
float OldHealth = Health;
Health = FMath::Clamp(Health + Delta, 0.f, MaxHealth);
float ActualDelta = Health - OldHealth; OnHealthChanged.Broadcast(InstigatorActor, this, Health, ActualDelta); if(ActualDelta < 0.f && Health == 0.f)
{
ASurGameModeBase* GM = Cast<ASurGameModeBase>(GetWorld()->GetAuthGameMode());
if(GM)
{
GM->OnActorKilled(this->GetOwner(), InstigatorActor);
} }
return true;
}

UMG的BUG

在进入游戏测试时,发现玩家是可以顺利重生的,但是左上角的血条出现了BUG。

玩家重生BUG(请忽略光照重建问题)

出现了以下几点问题:

  1. 血条自动回满,并且数字变成了文本的默认值(100),但是玩家角色的血量并不是100(这里我将玩家血量设置为3);
  2. 在玩家受到攻击掉血后,上方的血条(红条)不会产生任何变化;
  3. 由于玩家反复重生,就会反复创建UMG,因此我们看到的UMG是一层一层叠在一起的。

课程里讲解这个BUG省略了很多细节(可以说什么都没讲明白),在笔者反复Debug之后,大概有了些许眉目。现在让我们简单分析一下。

血条的构造函数

回到血条的构造函数图表中去。逐步调试发现,在玩家进行重生时,构造函数图表的获取玩家Pawn返回了空值,这导致了后面所有逻辑全部不被执行,这也是导致了血条不发生变化的直接原因,毕竟没有绑定事件,玩家受到攻击后血条自然不会发生任何变化。

为什么获取玩家Pawn会返回空值呢?这涉及到程序执行顺序的问题。当上一个玩家角色死亡的时候,控制器会将当前控制的角色释放掉,然后新建一个玩家角色,最后才将这个玩家角色赋给自己。

这就出现了一个问题。玩家角色的构造函数设置了创建UMG,玩家角色被创建的时候,血条也就在这时候被创建了。血条创建的时候,想要通过控制器获取玩家的Pawn,但是这时候新的玩家角色此时并没有被赋值到控制器中,因此就会返回空值。

讲完啦,总结一下执行的顺序:

  1. 上一个玩家角色死亡
  2. 控制器释放玩家角色(将自己的pawn成员赋值为nullptr)
  3. 控制器创建新的玩家角色
  4. 角色在构造中创建UMG,创建血条
  5. 血条的构造函数中想通过控制器获取玩家Pawn,但此时控制器的Pawn为nullptr,返回空值,后面的逻辑全部不执行
  6. 血条构造完毕,玩家角色构造完毕
  7. 控制器将pawn成员赋值为新创建的玩家角色

以上就是这个BUG产生的根本原因,目前课程里并没有提到解决方法,但既然我们知道了BUG产生的根本原因,那么我们就可以以此指定修复bug的策略,例如修改血条获取玩家角色的方法,添加带参数的构造函数,或者是玩家角色主动获取UMG进行赋值等。方法交由读者思考,因为修改BUG涉及到要修改的地方比较多,为了尽量不偏离课程,这里就不自作聪明了。

至少,我们可以解决前面提到的第三个BUG。

新建玩家控制器类

由于玩家控制器类的生命周期通常都是远远长于玩家的(一些游戏中甚至是一直存在的),因此, 相较于让玩家角色创建,让用户UI由玩家控制器来创建UMG往往是一个更好的选择。这里,我新创建了一个PlayerController蓝图类,在事件开始运行时创建Main HUD空间,并删掉了玩家角色中创建控件的蓝图节点。这样,只要玩家控制器不发生改变,那么HUD控件自始至终都只会创建一次了。

将玩家角色的创建空间功能移动到玩家控制器上

要启用我们自定义的玩家控制器类,只需要在GameMode中选定即可。

修改GameMode

总结

本篇文章为游戏新增了玩家重生的功能,并且尝试理解和优化了玩家重生所带来的BUG。

值得一提的是,笔者在看教程的时候十分不满意讲师对BUG的讲解,因此自己花费了很多时间去阅读源码和逐步调试,最后定位了BUG产生的原因,在这个过程中也接触到了自己之前从未想过的知识,在此我也希望看到这里的读者能够积极思考,尝试自己解决问题和阅读源码,这将会是非常好的学习方式。

斯坦福 UE4 C++ ActionRoguelike游戏实例教程 09.第二个游戏规则:玩家重生的更多相关文章

  1. 使用Html5+C#+微信 开发移动端游戏详细教程: (四)游戏中层的概念与设计

    众所周知,网站的前端页面结构一般是由div组成,父div包涵子div,子div包涵各种标签和项, 同理,游戏中我们也将若干游戏模块拆分成层,在后续的代码维护和游戏程序逻辑中将更加清晰和便于控制. We ...

  2. [libGDX游戏开发教程]使用libGDX进行游戏开发(1)-游戏设计

    声明:<使用Libgdx进行游戏开发>是一个系列,文章的原文是<Learning Libgdx Game Development>,大家请周知.后续的文章连接在这里 使用Lib ...

  3. [libGDX游戏开发教程]使用libGDX进行游戏开发(12)-Action动画

    前文章节列表:  使用libGDX进行游戏开发(11)-高级编程技巧   使用libGDX进行游戏开发(10)-音乐音效不求人,程序员也可以DIY   使用libGDX进行游戏开发(9)-场景过渡   ...

  4. [libgdx游戏开发教程]使用Libgdx进行游戏开发(11)-高级编程技巧 Box2d和Shader

    高级编程技巧只是相对的,其实主要是讲物理模拟和着色器程序的使用. 本章主要讲解利用Box2D并用它来实现萝卜雨,然后是使用单色着色器shader让画面呈现单色状态:http://files.cnblo ...

  5. 使用Html5+C#+微信 开发移动端游戏详细教程 :(五)游戏图像的加载与操作

    当我们进入游戏时,是不可能看到所有的图像的,很多图像都是随着游戏功能的打开而出现, 比如只有我打开了"宝石"菜单才会显示宝石的图像,如果是需要显示的时候才加载, 会对用户体验大打折 ...

  6. [libgdx游戏开发教程]使用Libgdx进行游戏开发(10)-音乐和音效

    本章音效文件都来自于公共许可: http://files.cnblogs.com/mignet/sounds.zip 在游戏中,播放背景音乐和音效是基本的功能. Libgdx提供了跨平台的声音播放功能 ...

  7. [libgdx游戏开发教程]使用Libgdx进行游戏开发(9)-场景过渡

    本章主要讲解场景过渡效果的使用.这里将用到Render to Texture(RTT)技术. Libgdx提供了一个类,实现了各种常见的插值算法,不仅适合过渡效果,也适合任意特定行为. 在本游戏里面, ...

  8. [libgdx游戏开发教程]使用Libgdx进行游戏开发(7)-屏幕布局的最佳实践

    管理多个屏幕 我们的菜单屏有2个按钮,一个play一个option.option里就是一些开关的设置,比如音乐音效等.这些设置将会保存到Preferences中. 多屏幕切换是游戏的基本机制,Libg ...

  9. [libgdx游戏开发教程]使用Libgdx进行游戏开发(6)-添加主角和道具

    如前所述,我们的主角是兔子头.接下来我们实现它. 首先对AbstractGameObject添加变量并初始化: public Vector2 velocity; public Vector2 term ...

  10. [libGDX游戏开发教程]使用Libgdx进行游戏开发(5)-关卡加载

    在上一章我们介绍了如何管理和利用素材,但是我们注意到,这些素材都是零散的,比如岩石的左部等,这一章,我们将利用这些零件拼合成完整的游戏对象. 回顾最开始的设计类图,注意Level类和所有Level中的 ...

随机推荐

  1. it 作形式主语:It's no good doing sth.

    It's no good doing sth. 这个 句型其实是一个省 略介词 in 的句型,完整形式是 It's no good in doing sth. 其中, good 是形容词,和介词 in ...

  2. windows平板的开发和选型

    今天谈一个老话题,windows系统的选型和开发.问题的起因是我们一个客户说,用安卓平板不安全,苹果系统不考虑,于是他们要用自认为安全的WIN7系统. 提到WINDOWS平台下的的平板系统,此事说来话 ...

  3. Swagger系列:SpringBoot3.x中使用Knife4j

    目录 一.简介 二.版本说明 三.使用 四.效果图 一.简介 官网:https://doc.xiaominfo.com/ Knife4j是一个集Swagger2 和 OpenAPI3 为一体的增强解决 ...

  4. sqlserver在设计表结构时,如何选择字段的数据类型

    在设计表结构时,选择适当的字段数据类型是非常重要的,它会直接影响数据库的性能.存储空间和数据的完整性.以下是在 SQL Server 中选择字段数据类型时的一些建议和理由: 1. 整数类型:在 SQL ...

  5. 前端CodeReivew实践

    把Code Review变成一种开发文化而不仅仅是一种制度 把Code Review 作为开发流程的必选项后,不代表Code Review这件事就可以执行的很好,因为Code Review 的执行,很 ...

  6. redis主从同步及redis哨兵机制

    1.主从和哨兵的作用: 角色 作用 主从 1.(提供)数据副本:多一份数据副本,保证redis高可用 2.  扩展(读)性能:如容量.QPS等 哨兵 1.监控: 监控redis主库及从库运行状态: 2 ...

  7. CF1854E Games Bundles 题解

    乱搞题 设个 \(dp[i]\) 表示和为 \(i\) 的子序列个数,那么转移是容易的, \(dp[j]+=dp[j-i]\) ,然后就判下 \(dp[60]+dp[60-i]\) 是否大于 \(m\ ...

  8. Visual Studio vs2010到2022各个版本的的永久激活密钥

    前言 以下密钥均收集于网络,但均可以正常激活 VS2022专业版和企业版的密钥 Visual Studio 2022 Pro(专业版) TD244-P4NB7-YQ6XK-Y8MMM-YWV2J Vi ...

  9. Docker学习资料集(从入门到实践)

    前言 昨天分享了一篇介绍Docker可视化管理工具的文章,然后在公众号后台收到了挺多同学的私信问:学习Docker有好的资料值得推荐的吗?想要学习Docker但是无从下手.其实之前我有断断续续的分享过 ...

  10. easyupload

    打开界面就是一个文件上传 的界面 然后在bp试了很多种方法都没有成功,还是看了wp 这里需要利用到.use.ini那为什么不用.heaccess?好像这种方法被过滤了,当时我用的时候没有成功 这里的话 ...