虚幻引擎构建光照失败的原因

by David Nadaski

大卫·纳达斯基(David Nadaski)

如何在虚幻引擎4中构建实时动态封面系统 (How to build a real-time dynamic cover system in Unreal Engine 4)

介绍 (Introduction)

A cover system enables A.I. units to avoid direct fire, taking cover behind various objects on the map. Using a cover system enhances a game’s level of realism and introduces essential tactical elements to any genre. Such systems are composed of two distinct modules: cover generation and cover finding. Cover generation is typically static and is done before the game starts, while finding cover happens in real-time during play.

掩护系统使AI单位能够避免直接射击,在地图上的各种物体后面掩护。 使用掩护系统可以提高游戏的真实感,并在各种游戏类型中引入必要的战术元素。 这样的系统由两个不同的模块组成:封面生成和封面查找。 封面的生成通常是静态的,并且是在游戏开始之前完成的,而封面的生成是在游戏过程中实时进行的。

In this article, I challenge the static nature of cover systems by describing how to build a fully dynamic, real-time cover generation module from scratch in Unreal Engine 4, as well as providing the implementation of a cover finding module.

在本文中,我将通过描述如何在虚幻引擎4中从头开始构建一个完全动态的实时封面生成模块,以及提供封面查找模块的实现,来挑战封面系统的​​静态性质。

Creating a robust cover system may seem daunting at first, but once you realize it’s just a set of simple techniques glued together, the task at hand will seem a lot less intimidating.

首先,创建一个强大的封面系统似乎很艰巨,但是一旦您意识到这只是将一系列简单的技术粘合在一起,那么眼前的任务似乎就不那么令人生畏了。

Whether you’re making a next-generation real-time strategy (RTS) game or want to use this in a first-person shooter (FPS), I hope you will find the information in this article helpful. I recommend downloading the demo project and checking out how it all works when put together.

无论您是要制作下一代实时战略(RTS)游戏还是想在第一人称射击游戏(FPS)中使用此游戏,我都希望本文中的信息对您有所帮助。 我建议下载演示项目,并检查它们放在一起时如何工作。

The project includes a fully functional implementation of all the techniques discussed above, complete with well-commented source code.

该项目包括上述所有技术的完整功能实现,并附有注释良好的源代码。

Download the demo project and source code.

下载演示项目和源代码。

工具和先决条件 (Tools & Prerequisites)

The tutorial uses Unreal Engine 4 (UE4) version 4.18, however it should work with older releases of the engine as well. The example project and source code were written in C++. You need a basic understanding of UE4, C++ and UE4’s Blueprints to understand the sample project and source code.

本教程使用虚幻引擎4(UE4)版本4.18,但是它也应与该引擎的较早发行版一起使用。 示例项目和源代码是用C ++编写的。 您需要对UE4,C ++和UE4的蓝图有基本的了解,以了解示例项目和源代码。

设计 (Design)

When designing a cover system, the three most important challenges you will face are as follows:

在设计封面系统时,您将面临的三个最重要的挑战如下:

  • Data generation资料产生
  • Data persistence数据持久性
  • Data usage数据使用

Since this article focuses on creating a real-time dynamic cover system where cover may become available or disappear completely at run-time, it is essential to apply an optimized approach to all three.

由于本文的重点是创建一个实时动态封面系统,其中封面可能会在运行时可用或完全消失,因此对这三种方法都应用优化方法至关重要。

I cover two data generation methods: navmesh edge-walking and 3D object scanning.

我介绍了两种数据生成方法: navmesh边缘遍历3D对象扫描

If your cover data is generated synchronously, it causes a noticeable hitch in your game’s performance, resulting in lags in gameplay. I demonstrate how to make use of the Unreal Engine’s excellent multi-threaded APIs to parallelize cover data generation, taking advantage of multi-core processing typically found in modern-day gaming hardware.

如果封面数据是同步生成的,则会导致游戏性能明显下降,从而导致游戏延迟。 我将演示如何利用虚幻引擎的出色多线程API来并行化封面数据生成,并利用现代游戏硬件中常见的多核处理优势。

Similarly, if access to cover data is too sluggish, your game slows down by a considerable amount, consuming large amounts of CPU and/or GPU cycles on cover queries. To avoid this, it is best to use a data structure that was meant for real-time concurrent lookup of spatial data: the octree. Using octrees appropriately also allows storing custom cover data, for example cover material (stone vs. hay), height, health, and so on with quick and efficient access.

同样,如果对掩护数据的访问速度太慢,您的游戏速度会大大降低,在掩护查询上会消耗大量CPU和/或GPU周期。 为了避免这种情况,最好是使用本来是对空间数据的实时并行查询的数据结构八叉树 。 适当地使用八叉树还可以存储自定义的封面数据 ,例如具有快速有效访问权限的封面材料(石头与干草),高度,健康状况等。

Data usage optimizations — for when your units are actively deciding which cover point to use in real-time — minimize the number of raycasts and ensure the availability of spatial lookup facilities (octrees) as well as support for direct fetch requests (arrays or maps).

数据使用优化-当您的设备正在主动决定实时使用哪个覆盖点时-尽量减少射线广播的数量并确保空间查找工具(八叉树)的可用性以及对直接获取请求(数组或地图)的支持。

In order to project how a unit may step out of cover to open fire, it is necessary to map out its peeking or leaning capabilities. A tank can’t peek out of cover — a foot soldier can. The best way I’ve found to accomplish this without using too many raycasts is to define “leaning offsets” on units. These are just simple floats that get added to the unit’s location upon hit-testing from cover.

为了预测一个单位可能如何走出掩体开火,必须规划其窥视或倾斜能力。 坦克不能偷偷看到-步兵可以。 我发现在不使用过多光线投射的情况下完成此操作的最佳方法是在设备上定义“倾斜偏移”。 这些只是简单的浮标,在从外壳进行命中测试时便会添加到设备的位置。

The final feature is real-time dynamic updates — whenever a new object is spawned in the game, we generate cover points around (and inside) it using Unreal’s event system via delegates. This ensures that we’re not wasting resources on Tick, which can slow the game down significantly, if care isn’t taken. We hook into Recast’s navmesh tile update events and update cover points in the corresponding tiles only when necessary.

最终功能是实时动态更新 -每当在游戏中生成一个新对象时,我们都会通过委托使用Unreal的事件系统在其周围(和内部)生成掩盖点。 这样可以确保我们不会浪费Tick上的资源,如果不注意的话,这会大大降低游戏的速度。 我们挂接到Recast的navmesh磁贴更新事件,并仅在必要时更新相应磁贴中的覆盖点。

Despite all the techno-speak, it’s actually wonderfully simple: a few trivial for-loops and a couple of missing pages from the UE4 documentation. So let’s get cracking!

尽管有很多技术方面的知识,但实际上却非常简单:一些琐碎的for循环和UE4文档中的一些缺失页面。 因此,让我们开始吧!

生成数据-两条大道 (Generating Data — Two Avenues)

There are multiple strategies for generating cover data, and I cover the two most prominent ones: first, a technique that’s similar to 3D scanning, and then a navmesh edge-walking approach.

生成封面数据有多种策略,我将介绍两种最突出的策略:首先是一种类似于3D扫描的技术,然后是导航网格边缘漫游方法。

3D object scanning relies on a 3D grid that’s created around an object. You typically have 3 main for-loops to do the bulk of the work, one for each axis. You iterate over the points on the grid that are a constant distance apart and check if you hit anything with a raycast.

3D对象扫描依赖于围绕对象创建的3D网格。 通常,您有3个主要的for循环来完成大部分工作,每个轴一个。 您遍历网格上相距恒定距离的点,并检查是否用射线投射击中了任何东西。

3D object scanning:

3D对象扫描:

  • Distributes cover points more uniformly than edge-walking分布覆盖点比边缘遍历更均匀
  • Supports objects that are incompatible with the navmesh, yet provide cover, for example, “force fields”支持与导航网格不兼容的对象,但提供掩护,例如“力场”
  • Has minimal chance for errors极少出错
  • Is slower (because of the sheer number of grid points)较慢(因为网格点的数量很大)
  • Copes poorly with landscapes应对景观差

The navmesh-based approach relies mostly on navmesh data and doesn’t deal with objects per se: if a point on the map is NOT covered by any navmesh polygons, then that means it is occupied by something large enough to provide cover.

基于导航网格的方法主要依赖于导航网格数据,并且本身不处理对象:如果地图上的某个点没有被任何导航网格多边形覆盖,则意味着该点被足以提供覆盖的物体所占据。

Navmesh edge-walking:

Navmesh边缘漫游:

  • Is considerably faster相当快
  • Handles rugged landscape topology easily轻松处理崎landscape的景观拓扑
  • Can’t deal with force fields and the like不能应付力场之类的东西
  • Is somewhat more error-prone: tile boundaries, mismatched points更容易出错:图块边界,不匹配的点
  • Does not distribute cover points as uniformly覆盖点分布不均匀

I cover these in more detail as we go on, so let’s dive into the nitty-gritty of 3D object scanning!

我们将继续详细介绍这些内容,因此让我们深入了解3D对象扫描的精髓!

基于对象的覆盖点生成 (Object-Based Cover Point Generation)

Rather than scan the navmesh for holes, we scan the objects themselves. Think of it like scanning an object in 3D: you slice it up along the X, Y, Z axes, divide the slices into a grid and use raycasts to determine where the object’s perimeter — or 3D circumference — meets the ground. This also works well for irregularly-shaped objects, like C-shapes, rings, castles — you name it.

而不是扫描导航网格中的Kong,而是扫描对象本身。 可以将其视为以3D形式扫描对象:沿X,Y,Z轴将其切片,将切片划分为网格,然后使用射线广播确定对象的周长(即3D圆周)与地面的交汇处。 这对于不规则形状的物体也很有效,例如C形,环形,城堡形-您可以将其命名。

A 3D scan grid looks like this:

3D扫描网格如下所示:

This is essentially accomplished by 3 very simple for-loops that just divide the actor’s bounding box into a 3D grid. But there’s a lot of points on the grid, and some of them are not even close to our object, so we have to filter them out.

这实际上是通过3个非常简单的for循环完成的,这些循环仅将actor的边界框划分为3D网格。 但是网格上有很多点,其中有些甚至不靠近我们的对象,因此我们必须将它们过滤掉。

And why not just use a 2D grid, you might ask? Because of two things:

您为什么会问为什么不只使用2D网格呢? 由于两件事:

  1. Multi-story objects (think of houses with multiple floors)多层物体(想想多层房屋)
  2. Rotated objects on slanted grounds倾斜地面上的旋转物体

To filter out invalid grid points, we cast a ray from each point downwards in the -Z direction to determine if it is close enough to the nearest ground plane. If it is, then we mark it as valid and continue on to the next one, eventually casting rays down from every single point.

为了滤除无效的网格点,我们从每个点沿-Z方向向下投射光线,以确定它是否足够接近最近的接地平面。 如果是,则将其标记为有效并继续进行下一个,最终从每个点向下投射射线。

Thankfully, raycasting is very cheap, so we don’t have to worry about single objects — it’s only when we have a multitude of them at run-time that we might start having problems, but we’re going to cross that bridge when we get there.

值得庆幸的是,光线投射非常便宜,因此我们不必担心单个对象—只有当我们在运行时拥有多个对象时,我们才可能开始遇到问题,但是当我们遇到问题时,我们将跨越那座桥梁到达那里。

Blue: closer to the ground than the grid point belowRed: farther from the ground than the grid point below

蓝色:离地面比下面的网格点更近 红色:离地面比下面的网格点更远

Since we’re only keeping the blue ones, we have many fewer points to worry about in the next pass: checking for minimum ground gap and minimum cover height.

由于我们只保留蓝色的,因此在下一遍中需要担心的问题要少得多:检查最小的接地间隙和最小的覆盖高度。

Red: too close to the objectGreen: far enough from the object for the smallest unit to fit under

红色:距离物体太近 绿色:距离物体足够远,无法容纳最小的物体

Red: too shortBlue: tall enough or empty

红色:太短 蓝色:足够高或空

This is how it looks like from the top orthographic view:

这是从顶部正交视图看的样子:

What we’re ultimately looking for are the closest spots to the red markers on the navmesh. These are represented by a subset of the white markers below:

我们最终要寻找的是最接近导航网格上红色标记的点。 这些由以下白色标记的子集表示:

The final cover points are represented by the purple markers below. We iterate over the red markers (above) and choose those white markers that are nearest to the red ones which fall on the navmesh:

最终的覆盖点由下面的紫色标记表示。 我们遍历红色标记(上方),然后选择最接近落在导航网格上的红色标记的那些白色标记:

Our final result looks like this:

我们的最终结果如下所示:

Here is how a scaled, rotated, multi-story actor on a slanted navmesh looks:

这是倾斜的导航网格上的缩放,旋转的多层角色的外观:

As you can see from the images above, this technique supports both rotation and scaling on the cover object, and on the ground plane as well. It is suitable to any type of geometry, and you don’t have to have level designers manually place a single marker in the scene anymore.

从上图可以看到,此技术既支持覆盖对象,也支持接地平面的旋转和缩放。 它适用于任何类型的几何图形,您不必再让关卡设计人员手动在场景中放置单个标记。

Since this kind of automated point generation sits well with multi-threaded execution, we’re going to lob all the logic inside an asynchronous task which we instruct UE4 to put into a thread pool. The best thing is, this is all supported right out of the box by the engine!

由于这种自动的点生成可以很好地与多线程执行配合使用,因此,我们将在异步任务中对所有逻辑进行遍历,并指示UE4将其放入线程池中。 最好的是,引擎开箱即用地支持所有功能!

To make it work with any type of actor, we create a custom UActorComponent and spawn our 3D scanning tasks from there. Let’s call it UCoverGeneratorComponent. Add this component to any force field-type actors. Don’t use it for regular objects — the navmesh-based generator that I outline next is our perfect all-purpose solution.

为了使其能够与任何类型的actor一起使用,我们创建了一个自定义UActorComponent并从此处生成3D扫描任务。 我们将其称为UCoverGeneratorComponent将此组件添加到任何力场类型的actor中 。 不要将其用于常规对象,我接下来概述的基于导航网格的生成器是我们完美的通用解决方案。

Navmesh边缘行走 (Navmesh Edge-Walking)

Time for the heavy-hitter, the generator that covers 90% of your cover system’s needs. So without further ado, let’s start walking the edge!

重击者的时间到了,发电机可以满足掩护系统90%的需求。 因此,事不宜迟,让我们开始努力!

Cover generation via edge-walking is actually a very simple process: take two vertices, cast a ray perpendicular to the resulting edge in both directions, see if the ray has hit something and if yes, then we’ve found cover.

实际上,通过边缘遍历生成覆盖是一个非常简单的过程:获取两个顶点,在两个方向上投射垂直于所得边缘的射线,查看该射线是否撞击到某物,如果是,则找到了覆盖。

We can complicate things further in terms of ray count by introducing ledge or cliff wall detection:

通过引入窗台或悬崖壁检测,我们可以使射线计数进一步复杂化:

This, however, adds at least 4 more rays per navmesh vertex, so now we are at 6 in total: 2 for the perpendicular ground rays, 4 for ledge detection. We can even go further and implement slope-tolerance for those nice cliff walls, so that rugged topology such as landscapes gets scanned properly:

但是,这每个导航网格顶点至少增加了4条射线,所以现在总共是6条:垂直地面射线2条,壁架检测4条。 我们甚至可以走得更远,对那些漂亮的悬崖壁实施坡度容限,以便正确地扫描地形等崎topology的拓扑:

But this is at least one more ray per side, which puts us to 8 total rays in the worst-case scenario. You decide whether this feature is worth the performance cost to your project or not — I tend to leave it on, as most of the generation happens fully asynchronously anyway, and the game can start even while the cover system is busy inspecting all those fancy ledges I’ve put down.

但是,这至少是每边多一束光线,在最坏的情况下,使我们总共有8条光线。 您可以决定此功能是否值得您的项目提高性能成本–我倾向于将其保留,因为大多数生成过程都是完全异步发生的,即使封面系统正忙于检查所有这些花哨的壁架,游戏也可以开始我放下了

速度 (Speed)

Walking a few edges is considerably cheaper than slicing even a relatively small object up into grid points. Imagine that most of the grid points cost you at least one raycast, and there could be thousands of them just on a simple mesh. The bigger the bounding box — the costlier it is to work with 3D scanning.

行走几条边缘要比将相对较小的物体切成网格点要便宜得多。 想象一下,大多数网格点至少要花费一个射线投射,而在一个简单的网格上可能有成千上万个。 边界框越大,使用3D扫描的成本就越高。

On the other hand, the number of navmesh polys on even a complex object won’t exceed a couple hundred, so the object can be as large as you want it to be. Its bounding box has no influence on its navmesh polycount in any way whatsoever. If you have lots of walkable space on your object, it will most likely get merged into a couple of polys. If you have many minute details on the surface, it might not even get navmesh on it at all. And even if you do manage to build a monster asset, its navmesh polycount most likely pales in comparison to the number of 3D grid points it would take to scan it.

另一方面,即使是一个复杂的对象,navmesh多边形的数量也不会超过几百个,因此该对象可以和您想要的一样大。 它的边界框无论如何都不会影响其导航网格数量。 如果您的对象上有很多可行走的空间,则很可能会合并成两个多边形。 如果您在表面上有很多细微的细节,它甚至可能根本就没有导航网格。 并且即使您确实设法构建了一个怪物资产,它的导航网格多数量与扫描它所需的3D网格点数相比也很可能会变白。

崎Landscape的景观拓扑 (Rugged Landscape Topology)

Landscapes are where navmeshes, and by extension Recast, the open-source pathfinding implementation integrated into UE4, shine.

风景是导航的地方,并且通过扩展Recast(集成到UE4中的开源寻路实现)也很出色。

The problem with landscapes and the 3D object scanning approach is that a lot of times, it misidentifies cover points as belonging to the landscape instead of their intended cover object. This is not a problem when using navmesh-based generation, and is the main reason — besides performance gains — why we use this technique wherever we can.

风景和3D对象扫描方法的问题在于,很多时候,它会错误地将Cover Point标识为风景,而不是其预期的Cover对象。 在使用基于导航网格的生成时,这不是问题,这是主要原因(除了性能提高以外),我们尽可能地使用这种技术。

力场(盾牌) (Force Fields (Shields))

Force fields are something that Recast does not traverse, and therefore are the only forte of the 3D object scanner. Since they’re dynamic objects that don’t affect the navmesh at all, I’ve created a boolean flag in the cover point data structure to indicate whether a point belongs to one of these. They are indicated by yellow markers in the demo project. Think of Reinhardt’s shield in Overwatch, but one that doesn’t move. This allows units to shoot through them while at the same time being protected from enemy fire.

力场是Recast不会遍历的东西,因此是3D对象扫描仪的唯一优点。 由于它们是根本不影响导航网格的动态对象,因此我在覆盖点数据结构中创建了一个布尔标志,以指示某个点是否属于其中之一。 在演示项目中,它们用黄色标记表示。 想想Reinhardt在《守望先锋》中的盾牌,但它不会动。 这使得单位可以射穿它们,同时免受敌人的射击。

失误 (Errors)

The navmesh-based approach is not without its drawbacks, and for the most part this manifests in unnecessary edges appearing on the tile boundaries of Recast. There’s not much we can do about them, except to cull them out during cover generation.

基于导航网格的方法并非没有缺点,并且在大多数情况下,这表现为在Recast的图块边界上出现不必要的边缘。 除了在封面生成过程中将它们剔除外,我们对它们无能为力。

As you can see, there are several excess vertices present, but most of them get discarded during cover point generation. The primary way of dealing with them is to cast rays in the two directions perpendicular to their edge’s XY axes and to do that from a fixed height. If nothing is hit, then our point is just a tile boundary vertex and can be safely discarded. These creep up on your objects as well, but the same culling technique applies.

如您所见,存在多个多余的顶点,但是大多数顶点在生成覆盖点时被丢弃。 处理它们的主要方法是在垂直于其边缘XY轴的两个方向上投射光线,并从固定高度进行投射。 如果什么都没有击中,那么我们的点就是一个图块边界顶点,可以安全地丢弃。 这些也会在您的对象上爬行,但是应用相同的剔除技术。

The other type of error comes from the fact that Recast doesn’t distinguish between closed and open spaces, meaning it generates a navmesh inside solid objects, too:

另一种类型的错误来自于Recast不能区分封闭空间和开放空间的事实,这意味着它也会在实体对象内部生成导航网格:

This is obviously no good, and the only way to get around it is to place nav modifier volumes in your map wherever you have larger solid meshes.

这显然是不好的,解决此问题的唯一方法是在您具有较大实体网格的位置将nav修改器体积放置在地图中。

This results in correct navmesh generation for the most part. But do note that there are cases where you just won’t be able to hide those inner navmeshes completely. That’s alright though, as our cover system filters out unreachable cover points automatically, so this will only result in some tiny loss of performance. Navmesh pathfinding queries tend to be relatively expensive when compared to the rest of our cover finding code, though, so you should still aim to minimize the number of unreachable navmesh islands.

这在大多数情况下会导致正确的导航网格生成。 但请注意,在某些情况下,您将无法完全隐藏这些内部导航。 没关系,因为我们的掩护系统会自动过滤出无法到达的掩护点,所以这只会导致性能的微小损失。 但是,与我们的其他掩护性查找代码相比,Navmesh寻路查询往往相对昂贵,因此,您仍应努力减少无法访问的navmesh孤岛的数量。

Next up, we look at how to store our cover points in a data structure that provides rapid and optimized access to spatial data: the octree.

接下来,我们研究如何将覆盖点存储在数据结构中,以快速,优化地访问空间数据:八叉树。

数据持久性— Octree (Data Persistence — The Octree)

It’s like crossing an octopus with a tree: easy to imagine, but difficult to climb. An octree is nothing more than a fancy way of saying “divide cube into 8 smaller cubes, rinse and repeat.” Likewise, a quadtree is just that — a square divided into four smaller squares that in turn are divided into four even smaller squares, and so on. By storing our entire map of cover points in an octree, we can ensure that our spatial queries are always as efficient as they can get.

就像章鱼越过树一样:容易想象,但难以攀登。 八叉树无非是说“将立方体分成8个较小的立方体,冲洗并重复”的奇特方法。 同样,四叉树就是这样-一个正方形分为四个较小的正方形,然后又分成四个甚至较小的正方形,依此类推。 通过将整个覆盖点地图存储在八叉树中,我们可以确保空间查询始终尽可能高效。

The good news is, most of the work has already been done for us by Epic, as UE4 features a fully functioning octree implementation. The bad news: almost no documentation. Fear not though, it won’t get too convoluted and we can always look at FNavigationOctree to see how Epic’s been using their monster.

好消息是,Epic已经为我们完成了大部分工作,因为UE4具有功能齐全的八叉树实现。 坏消息:几乎没有文档。 不用担心,它不会太复杂,我们可以随时查看FNavigationOctree来了解Epic如何使用他们的怪物。

One peculiarity of the octree is that whenever you want to delete existing data from it, you have to pass in an element id. But these ids aren’t stored in the octree — we must set up our own storage facility for them.

八叉树的一个独特之处是,每当要从中删除现有数据时,都必须传入一个元素ID。 但是这些ID不会存储在八叉树中-我们必须为它们设置自己的存储工具。

By following in FNavigationOctree’s steps, we use a simple TMap<const FVector, FOctreeElementId> ElementToID, where the FVector is the location of a cover point and FOctreeElementId is Epic’s built-in class that includes a reference to the node that the element is in, as well as its index. We also encapsulate all access calls to the map in thread-safe wrapper methods. Pretty standard stuff.

通过遵循FNavigationOctree的步骤,我们使用一个简单的TMap <const FVector,FOctreeElementId> Eleme ntToID, 而FV扇区是覆盖点的位置, 而FOctreeEle mentId是Epic的内置类,其中包括对Epic的引用。元素所在的节点及其索引。 我们还将所有对映射的访问调用封装在线程安全的包装器方法中。 很标准的东西。

The radius (size) of our octree should mimic that of our navmesh’s. But for simplicity’s sake, we just set it to 64000, which also happens to be the value that UNavigationSystem uses internally for the navmesh by default.

八叉树的半径(大小)应模仿导航网格的半径。 但是为了简单起见,我们只是将其设置为64000,这也恰好是UNavigationSystem在内部默认情况下对导航网格使用的值。

实时动态更新 (Real-Time Dynamic Updates)

One of the key features of our cover system is the ability to respond to changes in the environment at run-time, as well as to be able to process new geometry on-the-fly.

封面系统的​​主要功能之一是能够在运行时响应环境变化,并能够即时处理新的几何图形。

This is accomplished by hooking into Recast’s tile update event, which we do by subclassing ARecastNavMesh and overriding its OnNavMeshTilesUpdated method. The functionality inside the overridden method is very basic, yet indispensable: notify a custom dynamic multicast delegate whenever a tile was updated. We then subscribe to the delegate from our main cover system class, UCoverSystem (a singleton), and spawn cover point generator tasks accordingly.

这是通过挂钩Recast的tile更新事件来实现的,我们通过将ARecastNavMesh子类并覆盖其OnNavMeshTilesUpdated方法来实现。 覆盖方法中的功能非常基本,但必不可少:每当更新磁贴时,通知自定义动态多播委托。 然后,我们从主要掩护系统类UCoverSystem (单例)中订阅委托,并相应地生成掩护点生成器任务。

We call our subclass AChangeNotifyingRecastNavMesh and we hook it into the game via a custom game mode. The game mode overrides the PostActorCreated() method declared in AActor and uses SpawnActor() to instantiate our AChangeNotifyingRecastNavMesh, as follows:

我们将其子类称为AChangeNotifyingRecastNavMesh,并通过自定义游戏模式将其挂接到游戏中。 游戏模式将覆盖AActor宣布PostActorCreated()方法,并使用SpawnActor()来实例化AChangeNotifyingRecastNavMesh,具体如下:

void ACoverDemoGameModeBase::PostActorCreated(){ Super::PostActorCreated();  GetWorld()->SpawnActor<AChangeNotifyingRecastNavMesh>(AChangeNotifyingRecastNavMesh::StaticClass());}

Since there may be multiple tiles that get updated in a single event, and some of the tiles would also receive multiple updates in a relatively short time span, we time-slice our task-spawning logic so that we don’t update the same tile twice in rapid succession.

由于可能在单个事件中更新多个图块,并且某些图块还将在相对较短的时间段内收到多个更新,因此我们对任务产生逻辑进行了时间切片,以便我们不更新同一图块快速连续两次。

You also have to go to Project Settings ==> Navigation System ==> Agents ==> Supported Agents and add a new entry there whose Navigation Data Class and Preferred Nav Data should both be set to ChangeNotifyingRecastNavMesh, like so:

您还必须转到项目设置==>导航系统==>代理==>支持的代理,并添加一个新条目, 其导航数据和首选导航数据都应设置为ChangeNotifyingReca stNavMesh,例如所以:

You also need to uncheck Auto Create Navigation Data under Navigation System:

您还需要取消选中导航系统下的自动创建导航数据

You might have to restart the editor to see the new settings get applied.

您可能必须重新启动编辑器才能看到新设置被应用。

碰撞配置 (Collision Configuration)

Cover shouldn’t be generated around units like pawns and vehicles, so let’s exclude them by defining a custom trace channel. We can reference it in C++ as ECC_GameTraceChannel1.

不应在典当和车辆等单位周围生成掩体,因此让我们通过定义自定义跟踪通道来排除它们。 我们可以在C ++ 中将其引用为ECC_GameTraceChannel1

Go to Project Settings… ==> Engine ==> Collision and click on the New Trace Channel… button.Name: NonUnitsDefault Response: Ignore

转到项目设置…==>引擎==> Colcollion,然后单击“新建跟踪通道 ”按钮。 名称:无 nUnits 默认响应:忽略

Now expand the Preset section below Trace Channels and double-click on each of the following presets to set their response against our newly created NonUnits trace channel. Leave the ones not listed below intact — they’re already set to Ignore by default and that’s what we want there.

现在,展开“ 跟踪通道”下方的“ 预置”部分,然后双击以下每个预置,以针对我们新创建的NonUnits跟踪通道设置其响应。 保留未在下面列出的那些内容-默认情况下它们已经设置为“ 忽略” ,这就是我们想要的。

Check the checkbox on Block in the NonUnits row in all the following presets:

选中以下所有预设中“非单位”行中“ 阻止 ”复选框的复选框:

  • BlockAll全部封锁
  • BlockAllDynamic动态块
  • Destructible可破坏的
  • InvisibleWall隐形墙
  • InvisibleWallDynamic隐形墙动态

Next, check the checkbox on Overlap in the NonUnits row in all the following presets:

接下来,在以下所有预设中的“非单位”行中选中“ 重叠 ”复选框:

  • OverlapAll全部重叠
  • OverlapAllDynamicOverlapAllDynamic

Next, define a new Object Channel called “Shield” or “Force Field”:

接下来,定义一个新的对象通道,称为“ Shield ”或“ Force Field ”:

And finally, create a custom collision Preset named “NonBlockingShield” or “NonBlockingForceField”:

最后,创建一个自定义的碰撞预设,名称为“ NonBlockingShield ”或“ NonBlockingForceField ”:

在运行时查找封面 (Finding Cover at Run-Time)

So now you’ve got your cover points in your fancy little octree, and everything is efficient and multi-threaded with your custom navmesh passing in all the tile updates… All is good, so now’s the time to start making use of that data!

因此,现在您可以在自己喜欢的小八叉树中找到掩盖点,并且所有定制更新都传入所有瓦片更新中,一切变得高效且多线程……一切都很好,因此现在是时候开始使用这些数据了!

Your units want to look for cover, probably dozens of units at a time if you’re making an RTS — better yet, a tactics-heavy RTS (technically an RTT) — so how should they best approach that? Well it’s easy: just query the octree, pick a point that suits their needs, reserve the chosen spot, and move there.

您的部队想寻找掩护,如果要制造RTS,一次可能要找几十个部队-更好的是,战术上很繁重的RTS(技术上是RTT)-那么他们应该如何最好地解决呢? 好吧,这很容易:只需查询八叉树,选择一个适合他们需求的点,保留所选地点,然后移动到那里即可。

I recommend creating a CoverFinder service or task, the parent class being either UBTService or UBTTaskNode. If you go for a task, then you can add a Cooldown decorator to it so that it’s only invoked every x seconds, and doesn’t spam your octree and navmesh with queries, or PhysX with raycasts.

我建议创建一个CoverFinder服务或任务,其父类为UBTServiceUBTTaskNode 。 如果您执行某项任务,则可以向其添加一个Cooldown装饰器,以使其仅每x秒被调用一次,而不会对查询的八叉树和navmesh或raycast的PhysX进行垃圾邮件处理。

You can also create a UCoverFinder service of UBTService, instead. I’ve created both classes for you in the demo project, but you should note that I *do* spam the system with cover queries, so you will want to tweak the tick interval of UCoverFinder in your behavior tree so that it consumes fewer resources in your game.

您还可以创建UBTServiceUCoverFinder服务来代替。 我已经在演示项目中为您创建了两个类,但是您应该注意,我使用掩盖查询对系统进行垃圾邮件处理,因此您将需要在行为树中调整UCoverFinder的滴答间隔,从而减少资源消耗在您的游戏中。

封面评估 (Cover Evaluation)

The cover finder evaluates cover points that are between a set distance from the target enemy unit. In the cover demo project, I call these their minimum and maximum attack range, respectively. The finder queries the octree for points within a bounding box whose extent is that of max attack range, and then filters out any points that are closer to the enemy than min attack range. Let’s call this our unit’s optimal range.

掩体查找器评估与目标敌方单位之间设定距离之间的掩体点。 在封面演示项目中,我分别将它们称为最小和最大攻击范围。 查找器在八叉树中查询边界框内范围为最大攻击范围的点,然后筛选出比最小攻击范围更靠近敌人的点。 我们将此称为我们单位的最佳范围

It then iterates over cover points in its optimal range until it finds the first one where the following conditions hold true:

然后,它会在其最佳范围内遍历覆盖点,直到找到第一个满足以下条件的条件:

  • The unit can’t hit the enemy straight from cover部队无法从掩护直接击中敌人
  • The unit can hit the enemy by peeking or leaning out of cover该单位可以通过偷看或倾斜掩护来击中敌人
  • Line of sight to the enemy isn’t blocked by other units敌人的视线未被其他部队阻挡
  • The unit can get to the cover point via pathfinding on the navmesh单位可以通过导航网格上的寻路到达掩盖点

To check whether our unit can hit the enemy by peeking or leaning out of cover, we use two raycasts that are driven by the unit’s leaning (or peeking) capability parameter. This is just a simple float offset that gets added to the unit’s location in a direction perpendicular to where it’s facing.

为了检查我们的部队是否可以通过窥视或倾斜掩护来击中敌人,我们使用了两个射线投射,它们是由部队的倾斜(或窥视)能力参数驱动的。 这只是一个简单的浮动偏移量,它会在垂直于其所面对位置的方向上添加到单元的位置。

Light blue arrow: can’t hit the enemy by leaning outOrange arrow: can hit the enemy by leaning out

浅蓝色箭头:不能斜倚击中敌人 橙色箭头:不能斜倚击中敌人

In the screenshot above, a yellow unit has identified one spot where it can safely hit the blue unit from, so it moves to the corresponding cover point as blue scrambles for cover.

在上面的屏幕截图中,一个黄色单位确定了可以安全击中蓝色单位的一个位置,因此当蓝色争夺掩护时,它会移动到相应的掩护点。

Checking on both sides of the unit is done twice: once from a standing position, and if that fails then from a crouched one. This results in 4 raycasts in the worst-case scenario — standing: left, standing: right, crouched: left, crouched: right.

对设备的两面都进行两次检查:一次是从直立位置开始,如果失败,则从蹲下的位置进行一次检查。 在最坏的情况下,这将导致4次射线广播-站立:左,站立:右,蹲伏:左,蹲伏:右。

The same procedure is repeated for each cover point until a good one is found, resulting in <bad cover points&gt; x 5 raycasts overall.

对每个覆盖点重复相同的过程,直到找到一个好的覆盖点为止,从而导致<坏覆盖点&gt ;。 总共x 5个射线广播。

With force fields, it’s a little different: the only requirement there is that the unit must be able to hit its enemy from the cover point directly. In other words, no lean/peek checks are performed, but there is one extra check that is necessary: the unit must penetrate through the shield.

对于部队场,情况有所不同:唯一的要求是该部队必须能够直接从掩护点击中敌人。 换句话说,不执行倾斜/偷看检查,但是有必要进行额外的检查:设备必须穿透防护罩。

For this we have to use our custom NonBlockShield collision preset that uses our Shield object channel under the hood. So, this results in 2 raycasts overall: one against NonUnits and the other against Shields, and both must be successful for force field-type cover to be acceptable.

为此,我们必须使用自定义的NonBlockShield碰撞预设,该预设使用引擎盖下的Shield对象通道。 因此,这将导致总共2次光线投射:一个针对NonUnits ,另一个针对Shields ,并且两个都必须成功才能使力场类型的覆盖范围可接受。

统计资料 (Stats)

The cover system comes with its own stat group, aptly named STATGROUP_CoverSystem. It collects the following information:

封面系统带有其自己的统计信息组,恰当地命名为STATGROUP_CoverSystem 。 它收集以下信息:

  • Time spent finding cover (cycle counter)寻找封面的时间(周期计数器)
  • Total number of calls to FindCover, which equals the number of tasks spawned in total (dword)对FindCover的调用总数,等于调用总数(双字)
  • Total time spent finding cover in the game (float)在游戏中寻找封面的总时间(浮动)
  • Time spent generating cover (cycle counter)生成封面所需的时间(周期计数器)
  • Total number of cover generation calls (includes both edge-walking and 3d object scanning)封面生成调用的总数(包括边缘漫游和3d对象扫描)
  • Total time spent generating cover (includes both methods)产生掩护的总时间(包括两种方法)
  • Number of active tasks (dword)活动任务数(双字)

To see it in action, type stat CoverSystem in the console.

要查看其运行情况 ,请在控制台中键入stat CoverSystem

剖析 (Profiling)

Since custom stats are set up, it’s very easy to profile the cover system. Just write stat startfile and stat stopfile in the console and view the resulting log file using the Session Frontend under Window => Developer Tools.

由于设置了自定义统计信息,因此很容易对封面系统进行概要分析。 只需在控制台中写入stat startfilestat stopfile并使用Window => Developer To ols下的Session Frontend查看生成的日志文件。

结论 (Conclusion)

In summary, a robust cover system uses two separate techniques for cover generation: 3D object scanning and navmesh edge-walking. The former is best for force field type cover (static shields), while the latter works well for everything else (landscapes, objects, and so on).

总而言之,一个强大的封面系统使用两种单独的技术来生成封面:3D对象扫描和导航网格边缘漫游。 前者最适合用于力场类型的覆盖物(静态屏蔽),而后者则对其他所有条件(风景,物体等)都适用。

Object scanning involves slicing actors up into 3D grids, while navmesh edge-walking takes existing navmesh polys to traverse an area, with optional support for ledge-detection.

对象扫描涉及将角色划分为3D网格,而导航网格边缘漫游则使用现有的导航网格多边形遍历一个区域,并可选地支持窗台检测。

Both techniques store data in octrees, which provide efficient spatial lookup facilities.

两种技术都将数据存储在八叉树中,从而提供有效的空间查找功能。

Real-time dynamic updates are enabled by subscribing to and time-slicing Recast’s tile update events.

通过订阅Recast的磁贴更新事件并对其进行时间切片,可以启用实时动态更新。

Finding cover points at run-time is made more versatile by defining “peeking” or “leaning” offsets for units.

通过定义单位的“窥视”或“倾斜”偏移量,可以在运行时查找掩盖点变得更加通用。

You can increase cover generation performance by disabling ledge-detection, which reduces the number of raycasts.

您可以通过禁用边缘检测来提高封面生成性能,从而减少射线广播的数量。

You should take some extra steps for the navmesh-based technique to work best, like placing nav modifier volumes on the map wherever you have larger objects with navmeshes inside them. Some project set up is necessary as well, for example custom object channels, trace channels and collisions.

您应该采取一些额外的步骤,以使基于导航网格的技术发挥最佳作用,例如,将导航修改器体积放置在地图上的任何具有较大导航对象的地方。 也需要一些项目设置,例如自定义对象通道,跟踪通道和碰撞。

I’m sure you’ve had enough of me by now, so why not download the demo project and delve into the source code which includes a fully functional implementation of all the techniques discussed above. If something is unclear or you get stuck, feel free to let me know in the comments section below!

我确定您现在已经受够了,所以为什么不下载演示项目并深入研究源代码,其中包括上述所有技术的完整功能实现。 如果有不清楚或卡住的地方,请随时在下面的评论部分中告诉我!

Download the demo project and source code.

下载演示项目和源代码。

If you liked this tutorial, subscribe to our newsletter and get notified of new tutorials and articles.

如果您喜欢本教程,请订阅我们的新闻通讯 并获得有关新教程和文章的通知。

翻译自: https://www.freecodecamp.org/news/real-time-dynamic-cover-system-in-unreal-engine-4-eddb554eaefb/

虚幻引擎构建光照失败的原因

虚幻引擎构建光照失败的原因_如何在虚幻引擎4中构建实时动态封面系统相关推荐

  1. arcgis构建金字塔失败什么原因_天猫入驻为什么失败?知舟集团给出失败原因和解决办法...

    市场是有限的,竞争是无限的,前有京东.唯品会.拼多多.苏宁易购等多个平台争先恐后,后有云集.极有家等多个新平台如雨后春笋般起,但是在众多电商平台中,天猫依然独占鳌头,保持市场份额第一的趋势,这也是无数 ...

  2. ue4构建光照失败问题与解决

    ue4构建光照失败问题与解决 参考文章: (1)ue4构建光照失败问题与解决 (2)https://www.cnblogs.com/wzj998/p/6745184.html 备忘一下.

  3. 检索 COM 类工厂中 CLSID 为 {000209FF-0000-0000-C000-000000000046} 的组件失败,原因是出现以下错误: 8000401a 因为配置标识不正确,系统无法开

    我的服务器:windows server 2008(64位)+microsoft office 2007 企业版+windows服务应用程序 业务:调用msdn提供的SaveAsPDFandXPS.e ...

  4. devops失败的原因_为什么害怕失败是一种无声的DevOps病毒

    devops失败的原因 您认识以下情况吗? 我这样做是因为一位经理曾经扼杀了我的激情和创新,以至于我急于做出决定,冒险并专注于重要的事情:" 发现通过开发软件并帮助他人开发软件的更好方法 & ...

  5. devops失败的原因_如果没有这7个部门的支持,您的DevOps尝试将失败。

    devops失败的原因 当DevOps 由Andrew Shafer和Patrick Debois创造时 ,目标是使开发人员和运营商更加紧密地联系在一起,以实现客户价值. DevOps是一种不断学习和 ...

  6. eureka 集群失败的原因_对于注册中心,ZooKeeper、Eureka哪个更合适?

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试资料 来源:http://h5ip.cn/Kr8D 简介 Eureka本 ...

  7. 虚幻四中怎么保持导入模型坐标_[CG分享]|虚幻引擎5 技术解析

    今天还是跟大家聊一聊最近很火的虚拟引擎,Epic Game公司的虚幻引擎5惊艳了全球游戏业,其Nanite虚拟微多边形几何技术和Lumen动态全局光照技术带来了产业界的飞跃.Nanite虚拟几何技术的 ...

  8. 可口可乐市场调查失败的原因_可口可乐公司的市场调查为什么没有起到预期效果?...

    问答题请结合案例和所学知识回答问题. 20世纪70年代中期以前,可口可乐一直是美国饮料市场的霸主,市场占有率一度达到80%.然而,70年代中后期,它的老对手百事可乐迅速崛起.对手的步步紧逼让可口可乐感 ...

  9. 连接到mysql数据库失败的原因_连接MySQL数据库失败的原因

    欢迎进入Linux社区论坛,与200万技术人员互动交流 >>进入 连接mySQL数据库失败频繁,主要是什么原因造成的? 一年前,我开发了一个网站,租用的是linux下PHP+mySQL的虚 ...

最新文章

  1. Python中的类、模块和包究竟是什么?
  2. SpringDataJpa使用原生sql(EntityManager)动态拼接,分页查询
  3. 求二叉树第K层的节点个数+求二叉树叶子节点的个数
  4. iOS更改状态栏前景色背景色
  5. entity framework 6 我写了一个公用数据类
  6. cobaltstrike安装_CobaltStrike + Metasploit 组合安装
  7. TO C AND TO B IN TERMS OF CUSTOMER
  8. Windows计算机功能Java源码
  9. Java得到请求的IP地址
  10. 如何启动免安装版Tomcat并将Tomcat添加到服务中
  11. Perl opendir()函数
  12. Linux 查看磁盘的属性,Windows XP 查看磁盘属性(转)
  13. 【计算机组成原理】计算机系统概论
  14. R语言ETL工程:插入与合并(add/bind)
  15. Docker 基础 ( 二十 ) 部署Redis集群,问题记录
  16. MAC正确简单安装brew
  17. Facebook引流到独立站的三种技巧~附保姆级教程
  18. 电脑文件丢失你都是怎么找回来的?
  19. Android 欢迎引导页的魅力
  20. 网络广播mms直播地址

热门文章

  1. office中计算机剪贴画,Office 2010的剪贴画
  2. ibm笔记本修复计算机开机按,联想thinkpad重装系统按什么键_联想thinkpad电脑重装系统按哪个键-win7之家...
  3. 【海量数据学院】DBA的学习方法论系列—正确的学习方法
  4. activiti设置和使用启动人;activiti:initiator的作用及其使用
  5. 多边形的单边裁剪算法-JS
  6. 基于h5的航空订票系统的设计与实现
  7. ios开发者添加开发测试机
  8. 获取中国银行网页中外汇率
  9. Linux删除文件,df查看磁盘空间未减少
  10. WIFI学习一(socket介绍)