前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Unity Demo教程系列——Unity塔防游戏(二)敌人(Moving Through a Maze)

Unity Demo教程系列——Unity塔防游戏(二)敌人(Moving Through a Maze)

作者头像
放牛的星星
发布2020-12-11 15:19:48
2.3K0
发布2020-12-11 15:19:48
举报
文章被收录于专栏:壹种念头

· 1 出生点

· 1.1 瓦片内容

· 1.2 切换出生点

· 1.3 访问出生点

· 2 生成敌人

· 2.1 工厂

· 2.2 预制体

· 2.3 放置敌人在游戏板上

· 3 移动敌人

· 3.1 敌人集合

· 3.2 跟随路径

· 3.3 从边到边

· 3.4 方向

· 3.5 改变方向

· 3.6 曲线运动

· 3.7 常量速度

· 3.8 Outro 状态

· 4 可变敌人

· 4.1 浮点随机

· 4.2 模型缩放

· 4.3 路径偏移

· 4.4 速度

本人重点内容: 1、放置出生点 2、让敌人出现并穿越面板 3、用常量的速度创建平滑的移动 4、让敌人的尺寸、速度和位置可变

这是有关创建简单塔防游戏的系列教程的第二部分。它涵盖了产生的敌人并将它们移动到最近的目的地。

本教程是CatLikeCoding系列的一部分,原文地址见文章底部。

本教程是用Unity 2018.3.0f2制作的。

(敌人沿着路径去目标点)

1 出生点

在产生敌人之前,我们需要确定将敌人放置在板上的哪个位置。所以需要创建一个出生点。

1.1 瓦片内容

生成点是瓦片内容的另一种类型,因此请将其条目添加到GameTileContentType。

然后创建一个预制件以使其可视化。我们可以复制目标预制件,更改其内容类型并为其提供其他材质即可。我把它弄成橙色。

(配置出生点)

将对出生点的支持添加到内容工厂,并为其提供对预制件的引用。

(工厂支持出生点)

1.2 切换出生点

与其他切换方法一样,添加一种将生成点切换到GameBoard的方法。但是生成点不会影响寻路,因此我们无需在更改后找到新路径。

游戏只有在有敌人的情况下才有意义,这就需要有出生点。因此,有效的游戏面板应至少包含一个出生点。添加敌人时,我们稍后还需要访问出生点,因此使用列表来跟踪所有带有出生点的瓦片。切换出生点时更新列表,并防止删除最后一个出生点。

现在,Initialize必须设置一个出生点以产生初始有效的面板状态。这里我们简单地切换第一个瓦片,它是左下角。

按住Shift键的左键(通过Input.GetKey方法检查),将切换为目的地,否则切换为出生点。

(带有出生点的面板)

1.3 访问出生点

游戏面板会照顾好自己的瓦片,但并不对敌人负责。我们将可以通过带有索引参数的公共GetSpawnPoint方法访问其出生点。

要知道哪些索引有效,就需要知道出生点的数量,因此请通过公共获取方法将其公开。

2 生成敌人

生成敌人有点像创建瓦片内容。我们通过工厂创建一个预制实例,然后将其放在板上。

2.1 工厂

我们将为敌人创建一个工厂,这会将其创建的所有内容放置在自己的场景中。该功能与我们已经拥有的工厂共享,因此让我们将其代码放入通用基类GameObjectFactory中。我们可以使用带有通用预制参数的单个CreateGameObjectInstance方法就足够了,该方法创建并返回一个实例,并负责所有场景管理。将方法设置为protected状态,这意味着该方法只能由类本身及其扩展的所有类型访问。这是基类所做的所有事情,并不打算用作功能齐全的工厂。因此,将其标记为抽象,就不可能创建它的对象实例。

调整GameTileContentFactory,以使其扩展此工厂类型,并在其Get方法中使用CreateGameObjectInstance,然后从中删除场景管理代码。

在这之后,创建一个新的EnemyFactory类型,通过Get方法实例化一个敌人预制件,以及一个相应的回收方法。

最初,新的敌人类型仅需要追踪其原始工厂。

2.2 预制体

敌人需要可视化,并且可以是任何东西。我们将使用机器人,蜘蛛,鬼魂或诸如立方体之类的简单对象。但总的来说,敌人拥有任意复杂的3D模型。为了便于支持,我们将为敌人的预制层使用一个根对象,该根对象仅附加了Enemy组件。

(预制体根节点)

给该对象一个子节点,即Model根。它应该具有transform组件。

(Model Root)

模型根的目的是相对于敌人的局部原点定位3D模型,因此将其视为其站立或悬停在其上方的枢轴点。在我们的案例中,模型将是默认比例的默认立方体,我将其设置为深蓝色。使它成为模型根的子节点,并将其Y位置设置为0.25,以便它位于地面上。

(立方体Model)

因此,敌人的预制件由三个嵌套对象组成:预制根,模型根和立方体。对于简单的立方体而言,这可以认为是过渡设计了,但它可以移动和设置任何敌人的动画而不用担心其细节。

(敌人预制体的层次)

创建一个敌人工厂并将预制件分配给它。

(工厂资产)

2.3 放置敌人在游戏板上

为了将敌人放在面板上,游戏需要引用敌人的工厂。由于我们将需要大量敌人,因此还添加了一个生成速度的配置选项,以每秒敌人数表示。0.1-10的范围似乎是合理的,默认值为1。

(enemy工厂,并且出生速度为4)

通过将速度乘以时间增量来跟踪Update中的生成进度。如果进度超过1,则递减并通过新的SpawnEnemy方法生成敌人。只要进度超过1,就继续执行此操作,以防速度过快且帧时间结束得太长,而产生多个敌人。

我们不应该在FixedUpdate中更新进度吗? 这是可以的,但我们的塔防游戏确实不需要这么精确的时间。取而代之的是,我们只需要每帧更新一次游戏状态,并确保它在任何时间增量内都能正常运行。

让SpawnEnemy从棋盘上随机获得一个生成点,并在该图块上生成一个敌人。我们将为敌人提供一个SpawnOn方法以正确定位自身。

现在,SpawnOn所需要做的就是将其自己的位置设置在瓦片的中心。因为预制模型的位置正确,所以敌方立方体最终位于瓦片上方。

(敌人出现在出生点上)

3 移动敌人

一旦敌人出现,它应该开始沿着路径移动到最近的目的地。我们必须为它们设置动画,以实现这一目标。我们首先简单地将它们在图块之间滑动,然后使它们的移动更加复杂。

3.1 敌人集合

我们将使用与“ 对象管理 ”系列中相同的方法来更新敌人。给Enemy一个公共的GameUpdate方法,该方法返回它是否还活着,此状态始终存在。现在,只需根据时间增量使其向前移动即可。

接下来,我们必须跟踪一个活着的敌人列表并更新所有敌人,从列表中删除死掉的敌人。可以将所有代码放在Game中,但是让我们隔离它并为此创建一个EnemyCollection类型。这是一个可序列化的类,不扩展任何内容。给它一个公共的方法来添加一个敌人,并给另一个方法来更新整个集合。

现在,游戏就可以创建一个这样的集合,在每个帧中对其进行更新,并向其中添加生成的敌人。在可能产生新敌人之后更新敌人,因此它们会立即更新。

(敌人向前移动)

3.2 跟随路径

我们的敌人正在前进,但他们还没有沿着路径前行。为了实现这一目标,敌人必须知道下一步要去哪里。给GameTile一个公共getter属性来检索路径上的下一个瓦片。

给定一个瓦片和一个向其移动的瓦片,敌人就可以确定单个瓦片的起点和终点。通过跟踪进度来在这两者之间进行插值。进度完成后,对下一个瓦片重复该过程。但是路径可以随时更改。我们将继续按照计划的路线行驶,并在到达下一个瓦片时重新评估,而不是找出正在进行的路线。

让敌人追踪两个瓦片,这样它就不会受到路径变化的影响。还要追踪位置,这样我们就不必在每一帧中检索它们。它也需要追踪进度。

在SpawnOn中初始化这些字段。给定的瓦片是从哪里过去的,目的地是路径上的下一个瓦片(假设存在) 。如果没有,我们就在目的地上的出生点,但这应该是不可能的。然后缓存瓦片的位置,并将进度设置为零。我们不必在这里设置敌人的位置,因为它的GameUpdate方法将在同一帧内被调用。

增加在GameUpdate中的进度。添加未修改的时间增量,使我们的敌人每秒移动一格。进度完成后,移动数据,使“ To”变为“ From”,而新的“ To”是路径上的下一个瓦片。然后递减进度。一旦数据更新,就可以在“from”和“to”之间插入敌人的位置。由于进度是我们的插值器,因此可以保证它位于0到1之间,因此我们可以使用Vector3.LerpUnclamped。

这会使敌人沿着路径前进,但在到达目标图块时会失败。因此,在调整“From”和“To”位置之前,请检查路径上的下一个瓦片是否为空。如果是,我们就到达了目的地,敌人也完成了。收回它并返回false。

(敌人沿着最短路径)

敌人现在从一个方块的中心移动到另一个方块的中心。请注意,由于它们仅在瓦片中心更改其移动状态,因此不会立即响应瓦片的更改。这意味着有时敌人会穿过刚放置的墙壁。一旦他们进入了方块,就不能再阻止他们。这就是为什么墙也需要有效的路径。

(敌人对路径变化做出反应)

3.3 从边到边

在瓦片中心之间移动和突然改变方向,对于一个敌人是滑动方块的抽象游戏来说还不错,但总体上来说更流畅的移动看起来更好。第一步是在瓦片边缘而不是中心之间移动。

可以通过平均相邻瓦片的位置来找到它们之间的边缘点。我们仅在路径更改时才在GameTile.GrowPathTo中计算它,而不是计算每个敌人的每一步。通过ExitPoint属性使其可用。

唯一的特殊情况是目标单元格,其出口点为其中心。

调整敌人,使其使用出口点而不是瓦片中心。

(敌人在边和边之间移动)

这种变化的副作用是,当敌人由于路径变化而转身时,它们会保持静止一秒钟。

(敌人转身的时候会静止)

3.4 方向

尽管敌人沿着道路前进,但他们目前从未改变方向。为了让他们看到他们要去的地方,他们必须知道他们所遵循的路径的方向。再一次,我们将在找到路径时定义它,这样敌人就不必计算它了。

我们有四个方向:北,东,南和西。为此定义一个枚举。

然后给GameTile一个路径方向的属性。

将方向参数添加到GrowTo,以设置属性。当我们向后生长路径时,方向与我们向其生长路径的方向相反。

我们需要将方向转换为旋转,以四元数表示。如果仅在一个方向上调用GetRotation,那将会很方便,因此让我们通过创建扩展方法来实现这一点。添加一个公共静态DirectionExtensions类,为其提供一个数组以缓存所需的四元数,再加上GetRotation方法以返回方向的适当值。在这种情况下,将扩展类与枚举类型放在同一文件中是有意义的。

什么是扩展方法? 扩展方法是静态类内部的静态方法,其行为类似于某种类型的实例方法。该类型可以是类,接口,结构,原始值或枚举。扩展方法的第一个参数需要具有this关键字。它定义了方法将要操作的类型和实例值。注意,这种方法意味着扩展属性是不可以的。 这允许我们向任何类型添加方法吗?是的,就像你可以编写具有任何类型作为参数的任何静态方法一样。

现在,我们可以在生成时以及每次输入新的瓦片时旋转敌人。更新数据后,“From”瓦片为我们提供方向。

3.5 改变方向

与其立即切换到新的方向,不如在旋转之间进行插值,就像在位置之间进行插值一样。要从一个方向转到另一个方向,我们需要知道我们必须改变的方向:不改变,向右转,向左转,还是向后转。为它添加一个枚举,它可以再次作为方向放在同一个文件中,因为它们很小而且密切相关。

添加另一个扩展方法,这里的情况是GetDirectionChangeTo,它将返回从当前方向到下一个方向的方向更改。如果方向相同,则没有方向。如果下一个比当前多一个,那么它是右转。但是随着方向的变化,如果下一个比当前小三个,情况也是如此。左转弯是相同的,但是加法和减法被翻转了。唯一的其他情况是转身。

我们仅在一维上旋转,因此线性角度插值就足够了。添加另一个扩展方法,以度为单位获取方向的角度。

敌人现在还必须跟踪其方向,方向变化以及必须在其间进行插值的角度。

SpawnOn变得越来越复杂,因此让我们将状态准备代码移至另一种方法。我们将敌人的初始状态指定为介绍状态,因此将其命名为PrepareIntro。在这种状态下,它会从起始瓦片的中心移动到其边缘,因此不会发生方向变化。从和到的角度是相同的。

此时,我们正在创建一个类似于小型状态机的东西。为了保持GameUpdate简单,将改变状态的代码移动到一个新的PrepareNextState方法中。只保留从瓦片到瓦片的调整,因为我们这里用它来检查敌人是否完成。

进入新状态时,我们总是需要调整位置,找到方向变化,更新当前方向,并将“ To”角度更改为“ From”。我们不再总是设置旋转角度。

我们还要做什么取决于方向变化。让我们为每种可能性添加一个方法。如果我们继续前进,“ To”角度将与当前单元格的路径方向匹配。我们还需要设置旋转角度,以使敌人指向前方。

万一转弯,我们不会立即旋转。相反,必须插值到另一个角度:向右转90°,向左转90°,转弯时多180°。To角度必须相对于当前方向,以防止由于缠绕角度而以错误的方式旋转。我们不必担心会低于0°或高于360°,因为四元数。Euler可以解决这个问题。

在PrepareNextState的末尾,我们可以使用方向更改上的开关来确定要调用四种方法中的哪一种。

现在,我们必须在GameUpdate的末尾检查是否有方向更改。如果是这样,请在两个角度之间插值并设置旋转角度。

(敌人旋转)

3.6 曲线运动

通过使敌人在转弯时沿着曲线移动,我们可以进一步改善运动。我们将使它们沿着四分之一圆移动,而不是从一端到另一端直线移动。该圆的中心位于“From”和“To”瓦片共享的角上,与敌人进入“From”瓦片的边缘相同。

(旋转1/4圆来向右转)

可以通过使用三角函数沿着弧线移动敌人,同时旋转它来实现这一目标。但是我们可以通过将敌人的本地原点暂时移动到圆心来简化为仅旋转。为了使之成为可能,我们需要调整敌人模型的位置,因此请给敌人一个通过配置字段公开的模型引用。

(Enemy带有模型引用)

当准备前进或转身时,应将模型设置为默认位置,位于敌人的本地位置。否则,模型必须从旋转点偏移半个单位(旋转圆的半径)。

接下来,敌人本身必须移动到旋转点。同样,这是半个单位,但是确切的偏移量取决于方向。为此,我们向Direction添加一个便捷的GetHalfVector扩展方法。

向右或向左转时,添加适当的向量。

而转弯时的位置应该是正常的起点。

同样,我们可以在计算出口点时使用GameTile.GrowPathTo中的半向量,因此我们不需要访问两个图块位置。

现在,当方向发生变化时,我们绝对不能在Enemy.GameUpdate中完全插入位置,因为移动是通过旋转来完成的。

(敌人在转角处转弯平滑)

3.7 常量速度

到目前为止,无论敌人在砖块内如何移动,敌人的速度始终为每秒一砖块。但是它们覆盖的距离取决于状态,因此以每秒单位表示的速度会有所不同。为了保持此速度恒定,我们必须根据状态调整进度速度。因此,添加进度因子字段,并使用它来缩放GameUpdate中的增量。

但是,如果进度随状态而变化,则剩余的进度不能直接应用到下一个状态。相反,在准备下一个状态之前,我们必须规范进度,并在进入新状态后应用新因子。

前进状态不需要任何改变,因此使用系数1。向右或向左转时,敌人覆盖了半径为½的四分之一圆,因此覆盖的距离为¼π。进度是需要被除以。转弯应该不会花费太长时间,所以让我们将进度翻倍以使其达到半秒。最后,Intro仅覆盖一半的瓦片,因此其进度也应加倍以保持速度恒定。

为什么距离为¼π? 圆周或圆等于其半径的2π倍。右转或左转仅覆盖该距离的四分之一,半径为½,因此为½π×½。

3.8 Outro 状态

由于我们有Intro状态,因此我们也要添加一个outro状态。目前,敌人一到达目的地便消失,但让我们将其延迟到到达目标瓦片的中心为止。为此创建一个PrepareOutro方法,设置向前移动,但仅向瓦片中心移动,并加倍进度以保持速度恒定。

为防止GameUpdate过早地消灭敌人,请从中移走瓦片。这将成为PrepareNextState的责任。这样,仅在结束完成后,空检查才会产生true。

在PrepareNextState中,首先移动瓦片。然后在设置“From”位置之后但在设置“To”位置之前,检查“To”瓦片是否为空。如果是,请准备outro,然后跳过其余方法。

(常量速度移动的敌人)

4 可变化敌人

我们有一群敌人,它们都是相同的立方体,以相同的速度移动。结果可能看起来像是一条长长的蛇,而不是单个敌人。让我们通过随机化它们的大小,偏移量和速度使它们更加独特。

4.1 浮点随机

我们将通过从一系列值中随机选择敌人的特征来对其进行调整。我们在“对象管理”中的“配置形状”中定义的FloatRange结构在这里很有用,因此让我们对其进行复制。唯一的变化是,我添加了一个带有单个参数的构造函数,并通过只读属性公开了最小值和最大值,以使范围不可变。

还要复制我们为其定义的属性,以限制其范围。

我们只需要滑块可视化,因此将FloatRangeSliderDrawer复制到Editor文件夹。

4.2 模型缩放

我们首先调整敌人的缩放。将比例配置选项添加到EnemyFactory。比例范围不应太大,但足以创建敌人的微型和巨型版本。类似于0.5–2,默认设置为1。在Get的此范围内选择一个随机比例,并通过新的Initialize方法将其传递给敌人。

Enemy.Initialize方法只是设置其模型的统一尺度。

(缩放的范围设置为0.5-1.5)

4.3 路径偏移

要进一步破坏敌人流的均匀性,我们可以调整它们在瓦片内的相对位置。它们向前移动,因此沿该方向偏移只会改变其移动时间,而不会增加太多。取而代之的是,我们将它们横向偏移,使其远离穿过瓦片中心的理想路径。将路径偏移范围添加到EnemyFactory,并将随机偏移传递给Initialize。偏移量可以是正值或负值,但不能超过½,因为这会使敌人移动到相邻的方块中。我们也不希望敌人延伸到他们正在穿过的地砖之外,因此实际范围应小于该范围,例如0.4,通过敌人的实际限制取决于敌人的大小。

由于路径偏移会影响所遵循的路径,因此敌人必须对其进行追踪。

直线向前移动时(在前奏,外奏或正常向前运动期间),我们只需将偏移量直接应用于模型即可。转身时也是如此。左转或右转时,我们已经偏移了模型,该模型现在相对于路径偏移。

由于路径偏移会在转弯时改变半径,因此我们必须调整如何计算进度系数。必须从½中减去路径偏移量才能获得右转弯的半径,并添加到左转弯的半径。

现在,我们在转180°时也会得到转弯半径。在这种情况下,我们将覆盖半径等于路径偏移量的半圆,因此距离仅是偏移量的π倍。但是,当偏移量为零时,这将不起作用,并且会导致极小偏移量的快速转弯。我们可以为速度计算强制使用最小半径,以防止瞬时转弯,例如0.2。

(路径偏移设置为-0.25~0.25)

请注意,即使转身,敌人也不会改变其相对路径偏移。因此,总路径长度因每个敌人而异。

还要注意,为防止敌人刺入相邻的瓦片中,必须考虑其最大可能的比例。我只是将最大大小设置为1,所以我们的立方体的最大允许偏移为0.25。如果最大尺寸为1.5,则最大偏移量应减小到0.125。

4.4 速度

我们要随机化的最后一件事是敌人的速度。为此,向EnemyFactory添加另一个范围,并将值传递给实例化的敌人。将其作为Initialize的第二个参数。敌人不应太慢也不能太快,因此游戏不会变得琐碎或不可能。让我们将范围限制为0.2-5。以每秒单位表示,仅当向前移动时才相对于于瓦片。

敌人现在还必须追踪其速度。

当我们不使用显式速度时,我们只是始终使用速度1。现在我们要做的就是基于速度来确定进度因子。

(速度设置为0.75~1.25)

下一章 塔。

欢迎扫描二维码,查看更多精彩内容。点击 阅读原文 可以跳转原教程。

本文翻译自 Jasper Flick的系列教程

原文地址:

https://catlikecoding.com/unity/tutorials

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-11-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 壹种念头 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档