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

概述

本文对应Lecture 15, 61 - Console Variables for debugging and game balancing。

本文将会教你如何在C++中编辑控制台变量的逻辑,通过在游戏中打开控制台,以修改控制台变量的方式来修改游戏里的各种参数;此外,还会使用自定义静态函数库类,将部分常用的功能封装成静态函数以供使用。

另外,在这篇文章将会简单介绍UE中的碰撞规则,以及如何创建和使用碰撞通道。

对尸体施加冲击力

目录

  1. 控制台变量

    1. 是否自动生成AI角色
    2. 设置伤害倍率
    3. debug信息显示
  2. 静态函数库

控制台变量

喜欢玩游戏的朋友们一定有使用控制台对游戏进行设置的经历,还是拿Minecraft举例,常用的 time set 100 命令就是使用控制台来修改游戏中的时间,其内部原理往往就是调整控制台变量。在UE中同样也支持这种做法,允许我们在游戏运行的时候通过控制台变量来修改游戏中的各种参数,具体该怎么修改,让我们边做边说。

控制是否自动生成AI角色

我们的第一个控制台变量是一个bool类型的值,读者可以直接将下面一行代码复制到SurGameModeBase.cpp文件中,虽然看着很长,但是理解起来并不费劲。

在这行代码中,实际上是创建了一个TAutoConsoleVariable<bool>类型的static全局变量,并且在创建的时候调用了带参数的构造函数,我们将这个变量命名为CVarSpawnBots。由于在前面使用static关键字修饰,因此在其他cpp文件中是不能使用这个变量的。

其中,构造函数第一个参数是指令,第二个参数是默认值,第三个参数是指令描述,第四个参数是变量的标识,我们要做的就是依葫芦画瓢,将相关的参数填进去。

值得一提的是,这里的指令我们使用了su.***的格式,这是为了让我们在控制台中能够更方便的使用命令提示,只需要输入su.即可跳出我们定义的相关指令,这也算一个小技巧吧。

//SurGameModeBase.cpp
//第一个参数是指令,第二个参数是默认值,第三个参数是指令描述,第四个参数是变量的标识
//标识符为ECVF_cheat的作用是这个变量将不会在发行版本中生效,仅用于开发人员调试用
static TAutoConsoleVariable<bool> CVarSpawnBots(TEXT("su.SpawnBots"),true,TEXT("Enable spawning of bots via timer."), ECVF_Cheat);

可以点开类的声明看看他的源码,我们可以看到他的本质上就是一个模板类,类型T的主要的作用就是为了为这个控制台变量赋初值。

// TAutoConsoleVariable的源码
template <class T>
class TAutoConsoleVariable : public FAutoConsoleObject
{
public:
TAutoConsoleVariable(const TCHAR* Name, const T& DefaultValue, const TCHAR* Help, uint32 Flags = ECVF_Default);
}

如果要使用这个控制台变量,只需要在需要使用他的地方,像获取一个类的成员一样去获取他当前的值,如下所示

因为我们是在游戏里使用控制台,所以这里使用GetValueOnGameThread获取控制台变量。(顺带一提,编辑器也是有控制台的)

void ASurGameModeBase::SpawnBotTimerElapsed()
{
//从游戏线程中获取控制台变量
if(!CVarSpawnBots.GetValueOnGameThread())
{
UE_LOG(LogTemp, Warning, TEXT("Bot spawning disable via cvar 'CVarSpawnBots'."))
return;
}
....
}

设置伤害倍率

同样的,我们可以使用同样的方式设置子弹的伤害倍率。

当然,这里有一个小问题就是这个伤害倍率是不分敌我双方的,这里我想做个标注,因此我在注释里添加了@fixme这样的写法,为了让以后的自己能更快速的找到。我已经忘了这个写法是从哪里学来的了,但是这种写法在大项目中非常常见,而我们要找到也只需要对文件进行全局搜索即可。

//SurAttributeComponent.cpp
//伤害倍率
static TAutoConsoleVariable<float> CVarDamageMultiplier(TEXT("su.DamageMutiplier"), 1.f, TEXT("Global Damage Motifier for Attrivute Component."), ECVF_Cheat); bool USurAttributeComponent::ApplyHealthChanges(AActor* InstigatorActor, float Delta)
{
if(!GetOwner()->CanBeDamaged())
{
return false;
} //@fixme: 这个伤害乘法不分敌我双方
if(Delta < 0.f)
{
float DamageMutiplier = CVarDamageMultiplier.GetValueOnGameThread();
Delta *= DamageMutiplier;
}
....
}

交互debug显示

控制台变量还有一个很常用的功能就是是否显示Debug信息,我们同样可以在代码里进行修改,让debug信息在我们想看到的时候出现,而不是每次都要重新编译才能切换显示。

//SurInteractionComponent.cpp

//是否开启交互debug显示
static TAutoConsoleVariable<bool> CVarDebugDrawInteraction(TEXT("su.InteractionDebugDeaw"), false, TEXT("Enable Debug Line for Interact Component."), ECVF_Cheat); void USurInteractionComponent::PrimaryInteract()
{ bool bDebugDraw = CVarDebugDrawInteraction.GetValueOnGameThread(); ....
if(bDebugDraw)
{
DrawDebugLine(GetWorld(), CtrlerLocation, End, FColor::Red, false, 3);
}
}

接下来展示一下游戏里的画面,按下·键,就可以唤出控制台,输入su.,就可以看到我们自己定义的控制台变量:

在后面输入值可以对控制台变量进行赋值

小TIPs:双击·键可以打开控制台详细面板,这里可以查看指令的输入历史以及最后一个执行的是谁

查看控制台历史指令

现在,我们就可以通过使用控制台的方式调整游戏里的各种参数了。

静态函数库

在很多时候,我们并不想反反复复地获取一个类的对象,然后再调用他的成员。而静态函数为我们编程提供了便利,例如说我们之前用过的UGamePlayStatic类中定义了相当多的静态函数供我们调用,我们可以轻松的使用相当多的功能。同样的,我们可以自定义我们所需要的静态函数,我们通常采取定义一个函数库类的做法。在这个类中,你应该把所有的成员函数声明为static类型以供项目其他地方所用。

如下图所示,新建一个C++蓝图函数库,将其命名为SurGameplayFunctionLibrary

这里我定义了两个静态函数。顾名思义,ApplyDamage的作用是使一个角色对另一个角色造成伤害,而 ApplyDirectionalDamage在前者的基础上添加了冲击力的设定。

UCLASS()
class FPSPROJECT_API USurGameplayFunctionLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
// 这个函数将被用于一个Actor试图伤害另一个Actor时
UFUNCTION(BlueprintCallable, Category = "Gameplay")
static bool ApplyDamage(AActor* DamageCauser, AActor* TargetActor, float DamageAmount); // 施加带有方向的伤害,目标角色将会受到一个冲击力
UFUNCTION(BlueprintCallable, Category = "Gameplay")
static bool ApplyDirectionalDamage(AActor* DamageCauser, AActor* TargetActor, float DamageAmount, const FHitResult& HitResult);
};

下面是函数的实现。相信ApplyDamage函数里面的内容大家都已经很熟悉了,就是获取属性组件然后调用ApplyHealthChanges那一套,我们将这些内容抽象成了一个静态函数,将来我们想做同样逻辑的时候可以直接调用它。

而ApplyDirectionalDamage函数在前者的基础上对目标Actor施加了冲击力。在这段代码中,他会尝试获取命中的组件,通常能够被碰撞的组件都是继承了UPrimitiveComponent类。之后判断命中的部位是否开启了物理模拟,由于我们想要命中的是骨骼组件,因此在判断的时候还传入了骨骼名作为参数。如果命中的是模拟了物理的骨骼组件,将会对这个组件施加一个冲击力。

bool USurGameplayFunctionLibrary::ApplyDamage(AActor* DamageCauser, AActor* TargetActor, float DamageAmount)
{
USurAttributeComponent* AttributeComponent = USurAttributeComponent::GetAttributes(TargetActor);
if(AttributeComponent)
{
return AttributeComponent->ApplyHealthChanges(DamageCauser, -DamageAmount);
}
return false;
} bool USurGameplayFunctionLibrary::ApplyDirectionalDamage(AActor* DamageCauser, AActor* TargetActor, float DamageAmount,
const FHitResult& HitResult)
{
if(ApplyDamage(DamageCauser, TargetActor, DamageAmount))
{
UPrimitiveComponent* HitComp = HitResult.GetComponent();
if(HitComp && HitComp->IsSimulatingPhysics(HitResult.BoneName))
{
HitComp->AddImpulseAtLocation(-HitResult.ImpactNormal * 300000.f, HitResult.ImpactPoint, HitResult.BoneName);
}
return true;
}
return false;
}

最后我们将以前写过的魔法子弹伤害部分的代码进行修改,直接调用静态函数库的ApplyDirectionalDamage,十分便捷。

void ASurMagicProjectile::OnOverlapBegin(UPrimitiveComponent* HitComp, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
if(OtherActor && OtherActor != GetInstigator())
{
if(USurGameplayFunctionLibrary::ApplyDirectionalDamage(GetInstigator(), OtherActor, DamageAmount, SweepResult))
{
Explode();
}
}
}

BUG的出现和解决:修改碰撞预设

实际运行的时候会发现子弹打到了角色身上,但是什么事情也没有发生。进行调试发现,子弹命中的是目标的胶囊网格体,并不是我们想要的骨骼网格体。胶囊网格体竟然为骨骼网格体挡枪,这我们自然是不能接受的。我们可以通过修改碰撞预设的方式,设置我们的子弹不会命中胶囊网格体,并且能够对骨骼网格体作出反应。

碰撞预设相关的知识点实际上是相当重要而且常见的,课程里针对子弹的碰撞类型修改了胶囊网格体的碰撞规则,实际上我们可以使用更聪明的办法该修改这个BUG,由于这部分内容会在课程后面提到,这里做一个概述,感兴趣的读者可以深入了解和学习。

在UE中,每一个能被“碰撞”的组件都有自己的通道(Object Channels),我们可以自定义不同的通道之间是如何进行交互的,交互模式分为三种,分别是忽略(ignore)、重叠(overlap)和阻挡(block)。

举个栗子,如果两个组件对对方的通道类型交互模式都是block,那么他们在接触的时候就会互相阻挡,并触发碰撞事件(OnHit),也就是类似我们现实中的物体碰撞;如果一个类型对另一个类型是ignore,那么他们就会互相穿过,并且不会产生任何事件。而重叠则是允许双方互相穿过,并且产生重叠事件。

在UE中已经自带了相当多的通道,比如我们常见的WorldStatic、pawn等。有时候我们也想自定义一个通道,比如说我们的魔法子弹,我们单独为他创建一个Object channel,起名为Projectile。只要将我们魔法子弹的球体组件设置为这个通道类型,那么这个球体组件就可以以projectile的身份与这个游戏里的其他碰撞类型进行交互。

创建通道

在创建通道下方有一个Preset,意思是预设,在这里可以编辑一个拥有碰撞属性的组件是应该拥有什么样的对象类型,并且如何对其他对象类型作出反应。如下图所示,我创建了一个预设,起名叫做Projectile。在这里,我规定使用了这个预设的组件的对象类型为Projectile,并且对WorldStatic、Vehicle、Destructible等类型为阻挡模式,对Pawn, WorldDynamic等类型为重叠模式。意思就是,当使用了这个预设的组件碰到了Pawn类型的组件后,会直接穿过,并触发重叠相关事件(前提是勾选触发重叠事件)。

设置碰撞预设

回到我们的项目中来,打开AICharacter的胶囊体设置,找到碰撞相关的菜单。我们不想让Projectile类型的组件与胶囊体产生交互,因此我们将Projectile的交互策略改为忽略。这样,类型为Projectile的魔法子弹(准确的说是魔法子弹的球体组件)可以穿过胶囊体组件,并且不触发任何事件。

胶囊体的碰撞设置

打开AICharacter的骨骼网格体设置,注意到他的对象类型是Pawn,我们不需要修改它,只需要将生成重叠事件勾选上,这样在它与其他物体重叠的时候才可以触发重叠事件。

骨骼网格体的碰撞设置,注意要勾选生成重叠事件

此外,要记得将我们的魔法子弹的球体组件的预设设置为Projectile。

我们注意到球体组件对Pawn类型的策略是重叠,而骨骼网格体对Projectile类型的策略是阻挡,这两个策略相遇的话,会直接视为重叠,也就是会互相穿过,并触发重叠事件。

设置魔法子弹的碰撞预设

到这里为止,我们的设置就完成了,现在,我们的子弹可以穿过胶囊体组件,并命中网格体组件,触发重叠事件了。

碰撞设置这里的坑挺多的,理解起来也需要花费一定时间,希望读者可以多多实验和查阅资料。

Tips:控制台指令中自带一个God命令,使用了这个命令后,玩家控制的角色将会标记为不可被伤害。在代码中我们可以使用AActor::CanBeDamaged()来判断是否使用了god命令,然后进一步的编辑伤害的逻辑。

很有冲击力

还有一个小问题,注意到敌对小兵死后,虽然网格体飞出去很远,但是胶囊体组件还留在原地,如果我们试图经过的话,会被透明的胶囊体组件挡住(这也是因为双方的碰撞设置都为block)。

使用show collision控制台命令可以查看碰撞体。

解决这个问题很简单,只需要在敌对小兵死亡后,将其设置为无碰撞即可。从此,死去的小兵再也与世界无任何瓜葛,也再也没有人能触碰到它,令人唏嘘。

//ASurAiCharacter::OnHealthChanged
if(NewHealth <= 0.f)
{
......
//设置为无碰撞
GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
GetCharacterMovement()->DisableMovement();
SetLifeSpan(10.f);
}

总结

本篇文章揭秘了控制台变量是个什么玩意,作为游戏制作人的我们应该如何去使用它;介绍了静态函数库是个什么东西;最后结合产生的BUG介绍了游戏中碰撞通道和预设的概念。

关于碰撞这部分的内容,涉及的东西确实多,笔者确实有很多东西想讲,但精力确实也不太够,只能围绕案例做一点简单的解释,希望看到这里的读者踊跃发言,积极查阅资料。

最后的最后,这篇文章是课程第15课的最后一篇文章,这意味着我们已经学完了全部课程的一半,我们应该将这视为我们学习过程的一个里程碑,让我们小小的鼓(奖)励自己一下吧~

参考链接

UE4碰撞规则详解https://blog.csdn.net/zhangxsv123/article/details/79360025

斯坦福 UE4 C++ ActionRoguelike游戏实例教程 10.控制台变量的用法 & 静态函数库 & 使用对象通道对碰撞进行控制的更多相关文章

  1. Linux find命令实例教程 15个find命令用法

    除了在一个目录结构下查找文件这种基本的操作,你还可以用find命令实现一些实用的操作,使你的命令行之旅更加简易.本文将介绍15种无论是于新手还是老鸟都非常有用的Linux find命令.首先,在你的h ...

  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. Web 开发中应用 HTML5 技术的10个实例教程

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

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

    G-3D引擎2D射击类游戏制作教程 游戏类型: 打飞机游戏属于射击类游戏中的一种,可以划分为卷轴射击类游戏. 视觉表现类型为:2D 框架简介: Genesis-3D引擎不仅为开发者提供一个3D游戏制作 ...

  6. 值得 Web 开发人员收藏的20个 HTML5 实例教程

    当开始学习如何创建 Web 应用程序或网站的时候,最流行的建议之一就是阅读教程,并付诸实践.也有大量的 Web 开发的书,但光有理论没有实际行动是无用的.现在由于网络的发展,我们有很多的工具可以用于创 ...

  7. Unity-2017.3官方实例教程Space-Shooter(二)

    由于初学Unity,写下此文作为笔记,文中难免会有疏漏,不当之处还望指正. Unity-2017.3官方实例教程Space-Shooter(一) 章节列表: 一.创建小行星Prefab 二.创建敌机和 ...

  8. Unity-2017.3官方实例教程Space-Shooter(一)

    由于初学Unity,写下此文作为笔记,文中难免会有疏漏,不当之处还望指正. Unity-2017.3官方实例教程Space-Shooter(二) 章节列表: 一.从Asset Store中下载资源并导 ...

  9. 3Ds Max实例教程-制作女战士全过程

    3Ds Max制作“女战神” 作者:Diego Rodríguez 使用软件:3Ds Max,Photoshop 3Ds Max下载:http://wm.makeding.com/iclk/?zone ...

  10. Python导出Excel为Lua/Json/Xml实例教程(一):初识Python

    Python导出Excel为Lua/Json/Xml实例教程(一):初识Python 相关链接: Python导出Excel为Lua/Json/Xml实例教程(一):初识Python Python导出 ...

随机推荐

  1. 痞子衡嵌入式:MCUBootUtility v5.3发布,利用XMCD轻松使能外部RAM

    -- 痞子衡维护的 NXP-MCUBootUtility 工具距离上一个大版本(v5.0.0)发布过去4个多月了,期间痞子衡也做过三个小版本更新,但不足以单独介绍.这一次痞子衡为大家带来了全新重要版本 ...

  2. Thinking in Java 4th Edition Source Code

    Thinking in Java 4th Edition Source Code Instructions for downloading, installing and testing the so ...

  3. Oracle 高可用 阅读笔记

    1   个人理解概述 1.1  Oracle dg Oracle Data Guard通过从主数据库传输redo data,然后将apply redo到备用数据库,自动维护每个备用数据库.DG分为3个 ...

  4. POSIX 真的不适合对象存储吗?

    最近,留意到 MinIO 官方博客的一篇题为"在对象存储上实现 POSIX 访问接口是坏主意"的文章,作者以 S3FS-FUSE 为例分享了通过 POSIX 方式访问 MinIO ...

  5. InnoDB 存储引擎之 Buffer Pool

    Mysql 5.7 InnoDB 存储引擎整体逻辑架构图 一.Buffer Pool 概述 InnoDB 作为一个存储引擎,为了降低磁盘 IO,提升读写性能,必然有相应的缓冲池机制,这个缓冲池就是 B ...

  6. 【Spring】AOP实现原理

    注册AOP代理创建器 在平时开发过程中,如果想开启AOP,一般会使用@EnableAspectJAutoProxy注解,这样在启动时,它会向Spring容器注册一个代理创建器用于创建代理对象,AOP使 ...

  7. 简述几个我们对Redis 7开源社区所做的贡献

    Redis 7 已经于2022年4月28号正式发布,其中包括了将近50个新的命令,增加了许多新的特性,并且在整个Redis 6到Redis 7的开发过程中,我也对Redis 的开源社区贡献了一些微薄的 ...

  8. 递归+记忆化递归+DP:斐波那契数列

    递归:算法复杂度O(2^N) 1 int fib(int n) 2 { 3 if (n == 0) 4 { 5 return 0; 6 } 7 if (n == 1) 8 { 9 return 1; ...

  9. WPF --- 如何重写WPF原生控件样式

    引言 上一篇中 WPF --- 重写DataGrid样式,因新产品UI需要,重写了一下微软 WPF 原生的 DataGrid 的样式,包含如下内容: 基础设置,一些基本背景色,字体颜色等. 滚动条样式 ...

  10. C#使用SqlSugar操作MySQL数据库实现简单的增删改查

    公众号「DotNet学习交流」,分享学习DotNet的点滴. SqlSugar简介 SqlSugar 是一款 老牌 .NET 开源多库架构ORM框架(EF Core单库架构),由果糖大数据科技团队 维 ...