找回密码
 立即注册
查看: 713|回复: 0

[简易教程] Unity Demo教程系列——Unity塔防游戏(五)情境(Waves of Enemies)

[复制链接]
发表于 2020-12-12 15:39 | 显示全部楼层 |阅读模式
200+篇教程总入口,欢迎收藏:
本文重点内容:
1、支持小中大三种类型的敌人
2、创建多波敌人
3、分类资产配置和游戏状态
4、开始、暂停、输赢以及加速游戏
5、重复情境,无尽模式
这是有关创建简单塔防游戏的系列教程的第五部分。我们创建各种各样敌人的游戏情境。
本教程是CatLikeCoding系列的一部分,原文地址见文章底部。
本教程是用Unity 2018.4.6f1制作的。
体验越来越好了
1 更多敌人
总是产生相同的蓝色立方体敌人不是很有意思。创建更有趣的游戏情境的第一步是支持不止一种敌人。
1.1 配置敌人
有很多方法可以使敌人变得独特,但我们将使用非常简单的方法:将它们分为小,中或大。创建一个EnemyType枚举来表明这一点。
调整EnemyFactory,使其支持这三种敌人类型,而不是单个类型。所有三个敌人都需要相同的配置字段,因此请添加一个包含它们的EnemyConfig类,然后将该类型的三个配置字段添加到工厂。由于此类仅用于配置,因此我们不会在其他任何地方使用它,我们只需将其字段公开即可,这样工厂就可以访问它们。EnemyConfig本身不必公开。
也让每个敌人的生命值可配置,因为大的敌人比小的敌人拥有更多的生命值是有意义的。
添加一个要获取的类型参数,这样就可以获得一个特定类型的敌人,默认设置为medium。使用该类型可以获得方便使用单独方法的正确配置,然后像前面一样创建和初始化敌人,并添加一个health参数。
将所需的health参数添加到Enemy.Initialize并使用它来设置其运行状况,而不是依赖size。
1.2 设计不同的敌人
如何设计三种敌人类型取决于你自己,但是对于本教程而言,我希望的是尽可能简单。我复制了原始敌人的预制件,并将其用于所有三种尺寸,只改变了它们的材质:黄色代表小,蓝色代表中,红色代表大。我没有更改立方体预制件的比例,而是使用工厂的比例配置来调整尺寸。我还分别给他们增加了生命值以及降低了速度。
三种不同的敌人在工厂中的配置
使所有类型出现在游戏中的最快方法是更改Game.SpawnEnemy,以使其获得随机的敌人类型,而不是总是中等类型。
混合的敌人类型
1.3 多工厂
敌人工厂现在定义了一组三个敌人。我们目前的工厂生产三种尺寸的立方体,但是没有什么可以阻止我们创建另一个生产其他尺寸的工厂,例如三种尺寸的球体。通过为游戏分配不同的工厂,从而切换到不同的主题,我们可以更改生成的敌人。
球形敌人
2 敌人波数
创建游戏情境的第二步是不再以固定的频率生成敌人。取而代之的是,应该在连续的波数中生成敌人,直到情境完成或游戏失败为止。
2.1 出生序列
单个敌方波由一群敌人组成,这些敌人一个接一个地产生,直到该波完成。每一波可能包含的敌人,并且连续生成之间的延迟会有所不同。为了使此操作简单易行,我们从一个基本的敌人生成序列开始,该序列以固定的频率产生相同的敌人类型。这样一来,一波就是这些生成序列的列表。
创建一个EnemySpawnSequence类来配置一个这样的序列。由于它相当复杂,因此将其放在自己的文件中。序列需要知道使用哪个工厂,产生哪种类型的敌人,多少个,以及多快。为了简化配置,我们将使用冷却时间来表示最后一个选项,表示在产生下一个敌人之前必须经过多少时间。请注意,这种方法可以将敌人的工厂混合在一起。
2.2 波数
波数只是生成序列的数组。为此创建一个EnemyWave资产类型,该资产类型以一个默认序列开头。
现在我们可以设计敌人的波数了。例如,我创建了一波,产生一堆立方体敌人,以每秒两个的速度从十个小敌人开始,然后每秒以五个中等的速度开始,最后是一个具有五秒钟冷却时间的大型敌人。
一波的立方体,尺寸在增长
我们可以在序列之间添加延迟吗?
可以间接地实现。例如,要在小型和中型立方体之间放置四秒钟的延迟,请将小型立方体的数量减少一个,并在单个小型立方体具有四秒钟的冷却时间之后插入一个序列。
三种不同的敌人在工厂中的配置
2.3 情境
游戏情境是由一系列的波数组成的。为此,使用单个波数组创建一个GameScenario资产类型,然后使用它来设计情境。
例如,我创建了一个包含两个中小波的情境,首先是立方体,然后是球体。
情境由2小波组成
2.4 序列进度
资产类型用于设计情境,但作为资产,它们意味着包含在游戏进行时不会更改的数据。但是要在整个情境中取得进展,我们需要以某种方式追踪其状态。一种实现方法是在游戏中使用时复制资产并让复制者追踪。但是我们不需要复制整个资产,我们只需要状态和对该资产的引用即可。因此,让我们首先为EnemySpawnSequence创建一个单独的State类。由于它仅适用于序列,因此使其成为嵌套类。它仅在引用其序列时才有效,因此请为它提供一个带有sequence参数的构造方法。
嵌套的状态,引用自己的序列
每当我们要开始处理序列时,就需要为其获取一个新的状态实例。将Begin方法添加到构造状态并返回状态的序列中。这就使被调用的任何人都有责任开始保持状态,而序列本身仍然是无状态的。甚至有可能并行地多次执行相同的序列。
为了使状态能够在编辑器中进行热重载,它需要可序列化。
这种方法的缺点是,每次启动序列时,我们都需要创建一个新的状态对象。可以通过将其作为结构而不是类来避免内存分配。只要状态保持较小就可以。请注意,状态是一个值类型。传递它会复制它,因此请在单个地方追踪它。
序列的状态仅由两部分组成:产生的敌人数量和冷却进度。添加一个Progress方法,该方法将冷却时间增加时间增量,如果达到配置的值,则将其回落,就像Game.Update中的生成进度一样。每次发生时增加计数。同样,冷却时间必须从最大值开始,这样就可以在没有初始延迟的情况下产生序列。
保持状态
我们可以在State中访问EnemySpawnSequence.cooldown吗?
是的,因为状态是在相同的作用域中定义的。因此,嵌套类型知道其包含类型的私有成员。
进度应该持续进行,直到产生所需数量的敌人并完成冷却为止。那时进度应该表示已完成,但很可能我们最终会稍微超出冷却时间。因此,我们必须在那一点上返回额外的时间,以用于进行下一个序列。为了完成这项工作,我们必须将时间增量转换为参数。我们还需要指出我们尚未完成,这可以通过返回负值来实现。
2.5 在任何地方生成敌人
为了使序列产生敌人成为可能,我们将把Game.SpawnEnemy转换为另一个公共静态方法。
由于Game将不再产生敌人本身,因此我们可以从Update中删除其敌人工厂,生成速度,生成进度以及生成代码。
增加计数后,在EnemySpawnSequence.State.Progress中调用Game.SpawnEnemy。
2.6 每一波的进度
我们使用相同的方法来完成序列,以完成整个波。给EnemyWave自己的Begin方法,该方法返回嵌套的State结构的新实例。在这种情况下,状态包含波索引和活动序列的状态,我们通过开始第一个序列进行初始化。
波状态,包含序列状态
给EnemyWave.State一个Progress方法,使用与以前相同的方法,并进行一些更改。从处理活动序列开始,然后用该调用的结果替换时间增量。只要有时间,请继续进行下一个序列(如果有)并继续进行。如果没有剩余序列,则返回剩余时间,否则返回负值。
2.7 情境的进度
给予GameScenario相同的待遇。在这种情况下,状态包含波索引和活动波状态。
由于处于最高级别,所以Progress方法不需要参数,我们可以直接使用Time.deltaTime。不需要返回任何剩余时间,但是需要指出情境是否已完成。当我们完成最后一波操作时,返回false,否则返回true,以指示情境仍然处于活动状态。
2.8 游玩情境
最后,要游玩情境,游戏需要情境的配置字段并跟踪其状态。当我们唤醒并在更新其他游戏状态之前在Update中对其进行处理时,我们将简单地开始该情境。
从现在开始,配置的场景将在游戏开始后立即运行。它会一直进行到完成为止,然后什么也不做。
两波,时间缩放为10
3 开始和结束游戏
我们可以只玩一个场景,但是一旦完成,就不会再有敌人出现了。为了使游戏继续进行,我们需要手动或由于玩家赢得或输掉游戏才能开始新的游戏。 也可以从多个游戏场景中进行选择,但本教程的不介绍此种情况。
3.1 开始新的游戏
理想情况下,我们可以随时开始新游戏。这需要清理整个游戏的当前状态,这意味着我们必须清除多个对象。首先,向GameBehaviorCollection添加一个Clear方法,以回收其所有behaviors。
假定所有行为都可以回收,而当前情况并非如此。要使其正常,请向GameBehavior中添加一个抽象的Recycle方法。
WarEntity的Recycle方法现在必须显式覆盖它。
敌人还没有回收方法,所以也给它一个。它所需要做的就是让工厂对其进行回收。然后在当前我们直接访问工厂的任何地方调用Recycle。
还需要清除GameBoard,因此请给它提供一个Clear方法,以清空所有瓦片,清除出生点并更新内容,并设置默认的目标和出生点。然后,我们可以在初始化结束时调用Clear,而不用重复代码。
现在,我们可以向游戏添加一个BeginNewGame方法,以清除敌人,非敌人和游戏板,然后开始一个新情境。
在进行场景之前,如果按下了B键,则在Update中调用此方法。
3.2 输掉游戏
游戏的目标是在过多敌人到达目的地之前击败所有敌人。需要多少敌人才能成功触发失败取决于玩家的初始health状况,为此我们将在Game中添加一个配置字段。在计算敌人时,我们使用整数而不是浮点数。
玩家一开始有10点血
唤醒或开始新游戏时,请将玩家的当前生命值设置为起始值。
添加一个公共静态EnemyReachedDestination方法,以便敌人可以通知Game他们到达了目的地。发生这种情况时,玩家的生命值就会降低。
在适当的时候调用Enemy.GameUpdate中的方法。
现在,我们可以检查Game.Update中是否失败。如果玩家的生命值等于或小于零,则触发失败。我们将简单记录此事实,并在进行场景之前立即开始新游戏。但是,只有在开始的生命值有效的情况下才这样做。这使我们可以使用零来启动运行状况,这样就不可能失败,这对于方案测试非常方便。
3.3 赢得游戏
失败的替代方法是获胜,这是在情境结束时玩家还存活时实现的。因此,如果GameScenario.Progess的结果是false则记录日志胜利,开始新游戏并立即进行游戏。
但是,即使面板上仍然有敌人,这也会在最后一次冷却完成后获得胜利。我们应该将胜利推迟到所有敌人都消失了,可以通过检查敌人集合是否为空(假设它具有IsEmpty属性)来做到这一点。
将所需的属性添加到GameBehaviorCollection。
3.4 掌控时间
我们还可以操纵时间,这不仅有助于测试,而且经常是游戏必备功能。首先,使Game.Update检查是否按下了空格键,并使用空格键暂停或取消暂停游戏。可以通过在零和1之间切换Time.timeScale来完成。这不会改变游戏逻辑,但是会冻结所有内容。另外,对于极慢的慢动作,你可以使用非常低的值(例如0.01)而不是零。
其次,在游戏中添加一个播放速度配置滑块,以便我们加快时间。
游玩速度
如果没有切换暂停,并且时间尺度未设置为暂停值,请使其等于播放速度。另外,在取消暂停时,请使用播放速度而不是1。
4 循环情境
在某些情况下,你可能想要多次经历所有波数。我们可以通过使情境重复出现,多次循环遍历所有波来支持这一点。但你可以进一步优化它,例如仅重复最后一波,但是在本教程中,我们将简单地重复整个情境。
4.1 循环波
向GameScenario中添加一个配置滑块,以获取周期数,默认情况下设置为1。将最小值设置为零,这将使方案无休止地重复。那将创造一个无法获胜的生存场景,重点是看玩家能持续多久。
2次循环
GameScenario.State现在需要追踪其周期值。
Progress 中,当所有波都结束时,增加周期,只有经过足够的周期才返回false。否则,将波指数设置回零并继续前进。
4.2 加速
如果玩家已经击败了一个周期,他们应该能够再次击败它而没有问题。我们必须增加难度以保持方案的挑战性。最简单的方法是减少连续周期中的所有冷却时间。这会使敌人显得更快,并且在生存情境中不可避免地会使玩家不知所措。
将配置滑块添加到GameScenario中以控制每个周期的加速。该值将在每个循环后添加到时间缩放中,仅用于提高冷却时间。例如,以0.5的加速比,第一个循环的冷却速度为×1,第二个循环的速度为×1.5,第三个循环的速度为×2,第四个循环的速度为×2.5,依此类推。
现在,我们还必须将时间缩放添加到GameScenario.State中。它始终从1开始,并在每个循环后通过配置的加速来增加。在前进波形之前,使用它缩放Time.deltaTime。
3次循环,增加出生速度,游玩速度为10
下一章,动画。
本文翻译自 Jasper Flick的系列教程
原文地址:
https://catlikecoding.com/unity/tutorials

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-23 11:42 , Processed in 0.064812 second(s), 23 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表