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

概述

这篇文章对应课程13课, 50~54节。虽然标题是敲定AI,实际内容和AI关联并不大,主要工作是对游戏内各种细节做优化,涉及到的新知识并不多。本篇文章便出于记录的目的,对课程里进行的各种优化做下简单讲解。具体进行了哪些优化,让我们边做边说。

目录

  1. 设置死亡布娃娃效果
  2. AI被攻击时切换目标
  3. 使用静态成员函数优化代码架构
  4. 优化开火逻辑(随机散射、碰撞检测)

死亡布娃娃效果

布娃娃效果是什么?不知道读者们有没有见过断了线的木偶,四肢瘫软地趴在地上。顾名思义,就是使角色像布娃娃一样瘫软,常用在游戏角色死亡后的场景里,就像一个人被抽去了全部力气,只能受重力或者其他外力摆布。

在UE里,我们只需要对角色的骨架网格组件开启物理模拟,并取消控制器类对角色的控制权即可,让这个世界的物理法则作用于骨架,并抽去他的所有力量。

我们只需要在角色死亡的时候开启布娃娃效果,上篇文章对于角色死亡,我仅使其马上Destory。既然要开启布娃娃效果,那么需要延长它的生命好让我们观察它一会儿。具体的实现代码如下:

void ASurAiCharacter::OnHealthChanged(AActor* InstigatorActor, USurAttributeComponent* OwningComp, float NewHealth,
float Delta)
{
if(NewHealth <= 0.f && Delta < 0)
{
AAIController* AIC = Cast<AAIController>(GetController());
if(AIC)
{
AIC->GetBrainComponent()->StopLogic("Killed");
} //GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Yellow, TEXT("Killed an AI"));
GetMesh()->SetAllBodiesSimulatePhysics(true);
GetMesh()->SetCollisionEnabled(ECollisionEnabled::PhysicsOnly); SetLifeSpan(10.f);
}
}

让我解释一下这段代码发生了什么:

  1. 当生命值变化时,如果当前生命值小于0,则执行后面的逻辑
  2. 获取AI控制器,停止他的行为树。没错,获取行为树的方法是调用GetBrainComponent()函数,通常Brain组件指的是AI的决策部分,这里当然就是行为树了。停止逻辑时可以传入一段字符串,作为调试的信息。
  3. 获取AI角色的骨骼网格体。由于骨骼网格体在Character类中是私有成员,因此这里只能使用函数的方式获取。然后设置骨骼网格体的全部部位都为物理模拟。
  4. 设置为物理模拟还不够,还需要设置骨骼网格体的碰撞类型为PhysicsOnly或者QueryAndPhysics,对应编辑器中的仅物理已启用碰撞,这里设置成了仅物理,因为我不希望后续让他参与到碰撞查询中。
  5. 最后使用SetLifeSpan设置AI角色剩下的生命长度。这个函数原理很简单,就是设置了一个Destory的定时器,没有什么特别的。

对于第三步和第四步,我和课程中的做法有一点点出入,因为在测试的时候发生了许多有趣的事情。我在这里详细的说一说我的发现:

教程里一开始使用了这条语句试图做到布娃娃系统

GetMesh()->SetAllBodiesSimulatePhysics(true);

代码执行表现为人物确实像布娃娃一样瘫软了,但是却穿过脚下的地板直直坠落了下去。暂停查看发现,人物的胶囊体还在地上,网格体却掉了下去(直接从地板穿过去了,然后无限地坠落)。

小兵死后,尸体滑了下去

查询官方文档发现,这个函数的作用是为将骨骼网格体的所有部位都设置成了物理模拟,却不修改网格体组件的物理模拟标识,这是什么意思?意思是网格体仍没有启用物理模拟,但是物理模拟却实实在在的作用在了网格体的每一个部位上。

为什么骨骼体会掉下去呢?

这里必须提一点,网格体的默认碰撞设置是“仅查询”,意思是网格体组件不会与其他物体产生物理效应(碰撞阻挡之类的),在这种状态下是不允许物理模拟的(可能是编辑器害怕出现什么BUG,或者是这种设置没有任何意义),而这个函数绕开了这一点(因为它不会修改物理模拟的标识),让部位能够进行物理模拟。又由于网格体是碰撞设置是“仅查询”,它不会碰到这世上的所有东西,所以会直接掉下去。只要将网格体的碰撞设置设置为“仅物理”或者“启用碰撞”,就不会穿墙了。因此课程在这里,新加了一条代码:

GetMesh()->SetCollisionProfileName("Ragdoll");

他直接修改了骨骼网格体的碰撞预设为Ragdoll,在这个碰撞预设中,碰撞设置是已启用碰撞,即允许物理和查询,因此骨骼体可以与场景中的物体产生碰撞和一些物理效应,就不会穿过去了。

新的问题出现了,如果在编辑器中直接将网格体预设设置为Ragdoll, AI小兵在刚开始运行的时候就会直接变成布娃娃。按逻辑来说,要让小兵死后变成布娃娃只需要SetCollisionProfileName即可,不需要开启所有部位的物理模拟,然而在实测中,我执行了>SetCollisionProfileName("Ragdoll"),但是角色依然屹立不倒。这点让我百思不得其解,也许是UE编辑器中的设置与代码里的设置有些出入吧,最后我作出了妥协。

我在查询了SetCollisionProfileName函数发现,函数本身是建议在构造函数中使用的,在其他地方使用不保证能产生你想要的效果。好吧,既然我们想要的只是Ragdoll预设中的已启用碰撞,那我改碰撞设置就是了。最后我改成了SetCollisionEnabled(ECollisionEnabled::PhysicsOnly);同样可以达成效果,并且也不需要大刀阔斧地修改碰撞预设。


以上内容只是笔者实验过程中产生的疑惑,至今仍有一些问题没有解决。没有关系,跟着代码的步骤走,你仍然可以获得想要的结果。现在产生的问题,等以后有了一定的知识储备,那就不再是问题了。

看看最后达成的效果:

小兵死后,尸体像烂泥一样倒在了地上

AI被攻击时切换目标

添加切换目标的代码很简单,之前已经做过,就是获取黑板组件设置黑板键即可。注意到OnPawnSeen中的部分代码也有相同的逻辑,出于不复制粘贴代码的原则,将设置目标抽象为一个函数,定义SetTargetActor

void ASurAiCharacter::SetTargetActor(AActor* TargetActor)
{
AAIController* AIC = Cast<AAIController>(GetController());
if(AIC)
{
AIC->GetBlackboardComponent()->SetValueAsObject(TargetActorKey, TargetActor);
}
}

对应的OnPawnSeen也作简单修改:

void ASurAiCharacter::OnPawnSeen(APawn* Pawn)
{
SetTargetActor(Pawn);
DrawDebugString(GetWorld(), GetActorLocation(), "PLAYER SPOTTED", nullptr, FColor::White, 5);
}

实现切换目标的逻辑主要体现在OnHealthChanged函数里,我们只需要在原来的逻辑前加上设置目标的函数即可,这里还添加了不考虑自己攻击自己的情况:

void ASurAiCharacter::OnHealthChanged(AActor* InstigatorActor, USurAttributeComponent* OwningComp, float NewHealth,
float Delta)
{
if(InstigatorActor != this)
{
SetTargetActor(InstigatorActor);
}
//………后面是原本的逻辑
}

原先的属性组件中,广播的Instigator是nullptr。这里我们修改了ApplyHealthChanges的参数列表,添加了AActor* InstigatorActor,允许调用者传入自己的指针。然后将广播的Instigator修改为InstigatorActor。注意,其他用到ApplyHealthChanges的地方也需要修改。

当然,在实际开发并不建议这样改函数定义,因为如果很多地方用到了这个函数,多处修改会很让开发人员头疼的。

bool USurAttributeComponent::ApplyHealthChanges(AActor* InstigatorActor, float Delta)
{
health += Delta;
if(health > MaxHealth)
{
Delta = MaxHealth - health;
health = MaxHealth;
}
OnHealthChanged.Broadcast(InstigatorActor, this, health, Delta);
return true;
}

一点细节需要注意一下,,子弹打中人后传入的是自己的Instigator,也就是射出子弹的Character。因此在生成子弹的时候,注意在ActorSpawnParameters.Instigator中传入自己的指针,这样才能被子弹获取。

//在生成子弹的时候设置ActorSpawnParameters
FActorSpawnParameters params;
params.Instigator = MyPawn;

最终效果:

击中小兵后,小兵立刻锁定了玩家

如果AI小兵比较拥挤的话,你甚至可以看到他们互相攻击,真是一场好戏呀。

添加静态函数

如果要获取属性组件,我们需要写这么一行很长很长的代码来实现我们的想法。

USurAttributeComponent* AttributeComp = Cast<USurAttributeComponent>(Bot->GetComponentByClass(USurAttributeComponent::StaticClass()));

这里课程教了一个优化代码架构的小办法,就是使用静态函数的方式来优化代码的可读性。读者也可以学习和模仿UE中的GameplayStatics库中的写法,里面定义了很多获取游戏内常用资源的方法,比如获取控制器,获取所有Actor等。同样的,你也可以继承FunctionLibrary类来拓展UE的函数库,这里我就不展开叙述了。

现在我们想轻松地获得一个属性组件,不想再写这么长一串代码怎么办?我们可以定义一个获取属性组件的静态函数,将这个函数定义在SurAttributeComponent类中,因为功能和类是紧密相关的,所以在使用的时候可以减少部分记忆负担。

为了方便使用,在这里我定义了两个静态函数,实现也很简单。注意到我对IsActorAlive使用了meta关键字,meta为元数据的意思,这里使用了其中的DisplayName,即为该函数在蓝图中起了个别名,因此在蓝图搜素IsAlive也能搜到该静态函数。

//SurAttributeComponent.h
//获取Actor的属性组件
UFUNCTION(BlueprintCallable, Category = "Attributes")
static USurAttributeComponent* GetAttributes(AActor* TargetActor); //判断Actor是否还活着
UFUNCTION(BlueprintCallable, Category = "Attributes", meta = (DisplayName = "IsAlive"))
static bool IsActorAlive(AActor* TargetActor); //SurAttributeComponent.cpp
USurAttributeComponent* USurAttributeComponent::GetAttributes(AActor* TargetActor)
{
if(TargetActor)
{
return Cast<USurAttributeComponent>(TargetActor->GetComponentByClass(USurAttributeComponent::StaticClass()));
}
return nullptr;
} bool USurAttributeComponent::IsActorAlive(AActor* TargetActor)
{
USurAttributeComponent* AttrComp = GetAttributes(TargetActor);
if(AttrComp)
{
return AttrComp->IsAlive();
}
return false;
}

修改完后,现在只需要使用USAttributeComponent* AttributeComp = USAttributeComponent::GetAttributes(MyActor); 就可以获取属性了。

优化开火逻辑(随机散射、不打尸体)

目前AI小兵只要获取到玩家的角色对象,就会不断开火,即使玩家已经死亡倒地;另外的,AI小兵的准头似乎太好了,从来不马枪,对玩家躲闪子弹来说非常不友好。本节内容来优化这些细节。

我们主要修改的地方是SBTTask_RangeAttack类,我为这个类新加了一个float类型的MaxBulletSpread,并将其暴露在蓝图中,为的就是设定子弹偏移的范围。

在ExecuteTask函数的修改中,我新增了判断玩家死亡的逻辑,和使用FMath::RandRange生成随机偏移量,读者可以与之前的代码进行对比,自行测试。

有一个小细节需要注意一下,部分同学可能想在检测到玩家死亡的时候,就让他立刻更换目标,或者将目标对象设为nullptr。想法是可以,但是尽量别在ExecuteTask里实现。出于职责单一原则,行为树的Task尽量只做一件事情,更换目标不是当前这个发射子弹的Task所需要决定的。

EBTNodeResult::Type USBTTask_RangeAttack::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
AAIController* MyController = OwnerComp.GetAIOwner();
if(ensure(MyController))
{
ACharacter* MyPawn = Cast<ACharacter>(MyController->GetPawn());
if(MyPawn == nullptr)
{
return EBTNodeResult::Failed;
} AActor* TargetActor = Cast<AActor>(OwnerComp.GetBlackboardComponent()->GetValueAsObject("TargetActor"));
if(TargetActor == nullptr)
{
return EBTNodeResult::Failed;
} //如果目标死了,就不进行攻击。这里并不需要更换目标或者其他操作,因为这不是这个Task应该做的事情
if(!USurAttributeComponent::IsActorAlive(TargetActor))
{
return EBTNodeResult::Failed;
} FVector MuzzleLocation = MyPawn->GetMesh()->GetSocketLocation("Muzzle_01");
//方向向量=目标位置-当前位置
FVector Direction = TargetActor->GetActorLocation() - MuzzleLocation;
FRotator MuzzleRotation = Direction.Rotation(); //添加随机偏移
MuzzleRotation.Pitch += FMath::RandRange(0.f, MaxBulletSpread);
MuzzleRotation.Yaw += FMath::RandRange(-MaxBulletSpread, MaxBulletSpread); FActorSpawnParameters params;
params.Instigator = MyPawn;
params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; ensure(ProjectileClass);
AActor* NewProj = GetWorld()->SpawnActor<AActor>(ProjectileClass, MuzzleLocation, MuzzleRotation, params);
return NewProj ? EBTNodeResult::Succeeded : EBTNodeResult::Failed;
} return EBTNodeResult::Failed;
}

修改后,小兵出现了明显的马枪,而且不再攻击死人

总结

本文根据课程实现了一系列效果,为我们的游戏添加了更多有趣的功能。本来还想把54节的受击闪光功能做出来的,由于我使用的是官方的《虚幻争霸:小兵》资源包,里面的材质和课程的大相径庭,功能也复杂的多。笔者经过一番尝试,由于材质相关知识的欠缺,闪光功能没有成功实现,留给读者自己研究,或者让我日后补充上去吧。

参考链接

UE4 C++:UPROPERTY宏、属性说明符、元数据说明符https://blog.csdn.net/Jason6620/article/details/126502800

斯坦福 UE4 C++ ActionRoguelike游戏实例教程 06.敲定AI——游戏框架拓展和细节优化的更多相关文章

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

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

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

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

  3. 《Genesis-3D开源游戏引擎--横版格斗游戏制作教程06:技能播放的逻辑关系》

    6.技能播放的逻辑关系 技能播放概述: 当完成对技能输入与检测之后,程序就该对输入在缓存器中的按键操作与程序读取的技能表信息进行匹配,根据匹配结果播放相应的连招技能. 技能播放原理: 按键缓存器中内容 ...

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

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

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

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

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

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

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

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

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

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

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

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

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

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

随机推荐

  1. 报错AttributeError: Attempted to set WANDB to False, but CfgNode is immutable

    问题  今天在跑代码的时候,使用到了wandb记录训练数据.  我在23服务器上跑的好好的,但将环境迁移到80服务器上重新开始跑时,却遇到了如下报错  看这个报错信息是由于wandb没有apis这个属 ...

  2. Linux系列教程——Linux磁盘管理、Linux进程管理、Linux系统服务、 Linux计划任务

    @ 目录 1 Linux磁盘管理 1.磁盘的基本概念 1.什么是磁盘 2.磁盘的基本结构 3.磁盘的预备知识 1.磁盘的接口类型 2.磁盘的基本术语 3.磁盘在系统上的命名方式 4.磁盘基本分区Fdi ...

  3. Go接口 - 构建可扩展Go应用

    本文深入探讨了Go语言中接口的概念和实际应用场景.从基础知识如接口的定义和实现,到更复杂的实战应用如解耦与抽象.多态.错误处理.插件架构以及资源管理,文章通过丰富的代码示例和详细的解释,展示了Go接口 ...

  4. 什么???CSS也能原子化!

    1.什么是原子化 CSS? Atomic CSS is the approach to CSS architecture that favors small, single-purpose class ...

  5. RL 基础 | Policy Iteration 的收敛性证明

    (其实是专业课作业 感觉算法岗面试可能会问,来存一下档) 目录 问题:证明 Policy Iteration 收敛性 0 Background - 背景 1 Policy Evaluation con ...

  6. Chromium VIZ架构详解

    1. VIZ的三个端 在设计层面上 viz 的架构如下图所示: 在设计上 viz 分了三个端,分别是 client 端, host 端和 service 端. client 端用于生成要显示的画面(C ...

  7. gametime

    这道题是动态调试的考点,看了wp才有思路 像这样的游戏题一定要搞清楚他的具体游戏流程才能更好的做出来,然后根据他的思路去改掉相关的判断就可以了 攻防世界逆向高手题之gametime_攻防世界 game ...

  8. BI 数据可视化平台建设(2)—筛选器组件升级实践

    作者:vivo 互联网大数据团队-Wang Lei 本文是vivo互联网大数据团队<BI数据可视化平台建设>系列文章第2篇 -筛选器组件. 本文主要介绍了BI数据可视化平台建设中比较核心的 ...

  9. DX后台截图C++实现代码

    DX后台截图C++实现代码 文章仅发布于https://www.cnblogs.com/Icys/p/DXGI.html和知乎上. 传统的GDI API (BitBlt)虽然可以完美的完成后台截图的任 ...

  10. mysql--基础管理

    1.docker环境登录mysql PS C:\WINDOWS\system32> docker ps -aCONTAINER ID IMAGE COMMAND CREATED STATUS P ...