文章 | 机核 GCORES ( ) • 2024-04-11 12:41
游戏不一定要有敌人才有挑战性,但是我们现在来挑战制作一个敌人。
利用素材包里的素材,我们来做一个简单的敌人。素材中有一个叫oposum的spritesheet。opossum是负鼠,不过这里的文件名少了一个s,不知道是作者刻意为之还是单纯地打错了。
这个负鼠没有太多的动作,就只有一个奔跑的动作。那么我们将就手中的素材,做一个游戏中常见的那种无脑跑过去但是会伤害玩家的敌人。
有了前面的基础,相信你已经有思路了。先想一想怎么做再往下看。再次提示,你可以看我的做法,但是你永远有其它的思路来达成同样的效果。
我们新建一个负鼠的场景。为了简化移动的实现,所以我们给它一个CharacterBody2D作为根节点。动画配置部分略去,因为它只有这么一段动画所以配置好了直接播放就行了。总之大概就是这样一个结构:
给它添加一个脚本。由于我们这次一开始就给了它一个CharacterBody2D作为根节点,所以新建脚本的时候它会自动选择CharacterBody2D的脚本模板,为我们的新脚本文件自动添加一些基础代码。可以简单看一下模板代码都做了些什么,然后再来修剪。
为了让它能自动从平台落下,我们保留重力加速度的定义。当然我个人比较喜欢把它改成重力加速度的常用简称g。模板代码中已经包含了用重力控制垂直方向上速度的代码,所以这里我们可以保留。不过关于SPEED和JUMP_VELOCITY的定义它是定义的两个常量,我们的speed考虑暴露出来便于修改,而这个角色不涉及跳跃所以这两个常量一个改一个删。
我们现阶段的设计就是让它只会朝一个方向跑,那么直接在ready中给它一个速度(velocity)就行了。此外再定义一个变量用来控制它跑的方向(左或者右)。由于素材的方向是向左,所以我们需要以向左为基准。我们可以用一个布尔类型的变量来表示是否向左跑。不过我这里还是选择定义一个简单的枚举然后暴露给检视面板以方便设置初始方向:
当然我这样做只是为了看起来更规矩一点,由于只有简单两个方向,你完全可以用一个布尔值来表示。
现在把它放到场景中。它就可以自己跑了。这里为了后续测试可以先暂时把主场景设置为之前的Main场景。

解决撞到平台后速度为0的问题

如果你的平台之间不够宽,负鼠就会撞到边上,但是它莫名其妙地停止了移动:
当然这里最简单的解决办法就是把平台之间的距离拉开一点或者降低速度。
默认情况下CharacterBody2D碰到墙壁(is_on_wall)就会把水平速度降到0。不过其它Body类型又没有这么简单的移动方法。所以我们这里还是将就CharacterBody。当然我们可以写代码来解决问题(比如提前检测墙壁),但是考虑到这里的敌人比较简单我们就偷个懒。
首先修改应用重力的条件,删掉对地板的检测:
然后在负鼠的CharacterBody2D节点的检视面板中找到Floor部分,修改属性如下:
现在负鼠遇到这种情况就会滑下去了:
下面解释一下为什么这样可以。Max Angle属性定义了“在哪个角度内的斜面会被视作地板”。超过该值的斜面就会被视作墙壁,进而导致角色碰到时水平速度为0。这里把默认的45改成了90,也就是说现在垂直的面也会被当成地板。CharacterBody2D的move_and_slide方法默认只会在被视为地板的面上滑动。
另一处改动是启用了Constant Speed(固定速率)。这样一来负鼠在滑动时会保持速率不变。你可以试一下不勾选它,此时负鼠在平台右侧上滑动时的速度会比较慢。

穿过玩家

在Godot中,两个Body会发生接触而不会穿过对方。现在这个负鼠如果和玩家碰到一起很可能不会和你预想的一样。聪明的你可能已经想到可以设置它们的碰撞层来让负鼠穿过玩家!这正是碰撞层的合理用法。
我们先好好理一下目前需要用到的层。我们目前发生碰撞的东西主要分三类,一是环境,而是玩家,三是敌人。对于环境来说,无论是玩家还是敌人都需要与之发生接触(至少对于目前仅有的一种敌人来说是这样)。但是我们不希望玩家和敌人之间相互接触。
因此我们现在设置三个层,上述三类对象各自分属一个层。然后设置不同的扫描层。由于我们以后可能还会有其它的层加入,因此我们可以给各个层进行合理的命名而不是试图记忆它们的对应关系。
编辑碰撞层时切记要在各个场景内部进行调整,而不是在主场景或者其他场景中的场景实例节点中调整。不然的话这些编辑不会反映为场景的默认值!
编辑层时点击右侧的三个点,弹出的菜单中选择Edit Layer Names即可编辑层的名字。
当然如你所见这里打开的是项目设置窗口中的一个页面,你也可以直接到项目设置中编辑。
以我自己的操作为例,我把1层设为了Player,2层为Environment,3层为Enemy。为了和我的命名保持一致,我把TileMap的第0物理层设为了2层,但是让它可以扫描1层(玩家层)。
然后我让负鼠在3层,但是只扫描环境层:
这样一来负鼠就会直接穿过玩家。而不会影响移动。

感知玩家

现在可以穿过玩家,但是我们还不能杀死玩家。CharacterBody2D上实际上没有暴露和碰撞相关的信号,即使它有,由于我们已经设置负鼠不会扫描玩家所在层,所以也不起作用。Godot目前无法单独设置某层对某层究竟是穿过还是阻挡(实际上Ureal中提供了类似的功能),所以我们必须再给它加一个Area2D来完成对玩家的检测。
为负鼠添加一个Area2D并设置CollisionShape。由于我们只用它来检测玩家,所以我们只设置它检测玩家层。
为了节省一些不必要的消耗,我们直接让它不位于任何层(这样就不会被其它节点检测到),且只检测玩家层。
然后连接Area2D的body_entered信号,如果这个body是Player,那么我们就直接杀死它:
现在负鼠作为一个敌人就可以正常杀死玩家了!

重复利用Spawner场景

我们现在是直接把负鼠放在场景中,但是我们可能需要根据设计的时机来生成负鼠。这里我们可以重复利用之前用来生成玩家的Spawner。
由于我们之前已经在Spawner上定export了一个场景变量,所以现在可以很方便地给另一个Spawner场景实例设置上负鼠的场景。
由于我们之前对Spawner已经进行了多次改进,现在Spawner本身没有和任何具体场景绑定的代码,所以用来生成任何东西都是合适的。这就是消除藕盒,不,耦合(decouple)的好处。
我们在主场景中添加对负鼠生成器的引用,然后适时生成负鼠:
现在我们可以删除之前直接丢到主场景中的负鼠了。
但是现在又有一个问题,我们之前在负鼠场景上暴露的direction变量现在没法直接在检视面板中设置了。
这个好说。我们在Spawner中定义一个信号,告知外部我们刚刚实例化了一个场景,你可以给它进行一些初始化设置:
调用instantiate之后我们立即发送这个信号。比如要是你想让负鼠一出来的时候向右跑就可以在脚本某处连接上这个信号然后把它调整为向右。

进入画面时再开始工作

不过现在的Spawner还是有点死板——可能说不那么“智能”更合适。我们必须自己在需要的时候调用spawn方法。
一种典型的设计是,当玩家走到某个东西刚好出现在画面中时才触发相应机关。比如我们想要Spawner进入画面时自动生成一个负鼠。在Godot中要实现这种功能非常简单。
Godot提供了一个叫VisibleOnScreenNotifier2D(“在屏幕中可见时发出通知的东西2D”)的节点。它定义了一个矩形区域,当它进入画面时就会发送相关信号。
我们把它添加到Spawner场景上。连接它的screen_entered的信号。现在为了我们的Spawner更好用,我们export一个布尔类型的变量(比如叫spawn_when_visible),让我们可以控制具体的spwaner是否仅当出现在画面中时自动spawn。然后响应信号的方法中我们就可以这样写:
就这么简单。接下来修改主场景中的代码,不再在ready中生成负鼠。同时在场景中的OpossumSpawner上勾选可见时生成。
太棒了,我们不仅有了一个敌人,还不断完善了Spawner的功能!

等等…是不是忘了什么

确实。负鼠实例化之后会一直存在。尽管一个负鼠被无视掉不会导致电脑卡死,但是长期看来如果不注意这种情况不是什么好事。
这里介绍一种调试技巧(准确来说应该是基本技能)。启动游戏后左侧的场景树面板其实发生了变化:
这里分成了Remote(远端)和Local(本地)两个条目。简单来说这里的远端就是指的从编辑器启动正在运行的游戏中的场景树。
从中我们可以看到即使负鼠离开了画面它也依然存在。
当然根据设计的不同我们可能会在不同的时机来销毁负鼠。比如你可能想在碰到玩家之后就销毁它。不过这里我们也可以简单地通过刚学习的VisibleOnScreenNotifier2D的screen_exited信号让负鼠在离开画面后销毁。这里就不重复了,留作练习吧!