Godot3游戏引擎入门之八:添加可收集元素和子场景

2018-11-02 by Liuqingwen | Tags: Godot | Hits

一、前言

在前面的游戏地图基础上,我们已经实现了玩家的上下移动控制,也有了相应的碰撞体功能,一个小小的游戏世界已经打造好,不过对于一个完整的游戏来说还是缺少点什么,没有探索的乐趣就没有吸引力,因此,这也就是我们本篇要实现的目标——给游戏场景添加一些可爱的动画元素,比如金币,来供玩家探索吧!

除此之外,我还会介绍 Godot 中两个非常重要的概念或者实用技巧:子场景的创建和 Godot 中信号的使用。和之前的文章一样,本篇也是基于上一篇文章: Godot3 游戏引擎入门之七:地图添加碰撞体制作封闭的游戏世界

主要内容: 在游戏场景中添加互动元素
阅读时间: 10 分钟
永久链接:http://liuqingwen.me/blog/2018/11/02/introduction-of-godot-3-part-8-add-collectable-elements-and-sub-scenes/
系列主页:http://liuqingwen.me/blog/tags/Godot/

二、正文

本篇目标

  1. 创建子场景,实例化,并添加多个子场景
  2. 介绍 Area2D 节点的功能和应用
  3. Godot 中的观察者模式实现:信号的使用
  4. 创建和使用包含函数调用的复杂动画

创建玩家子场景

为什么需要子场景呢?这其实有点类似程序中的面向对象思想,如果你有使用 Unity 开发游戏的经验,那么你对 Unity 中深入人心的 Prefab 预制体概念肯定非常熟悉;同样地在 Apple 中开发 2D 游戏,使用 SpriteKit 也会创建很多的子场景: SKScene ,然后在主游戏中加以重复利用。 Godot 中也有类似的概念,想象一下,当你需要在场景中制作很多个功能类似的物体,比如多个相同的敌人,每个场景中数量还不一定一样,如果每个场景中都去单独制作一个个的敌人对象,那就显得非常地不优雅了,万一设计不合理,全部都需要修改呢?这个时候,你就可以把它制作成一个预制件,使用预制件来克隆多个敌人,当你需要修改某个功能的时候,你只需要修改这个预制件,那么所有的实例都能得到应用,方便高效,还能提高游戏性能。这就是 Godot 中所谓的 Sub-Scene 子场景概念了。

说的很多,实际上做起来很简单。首先,我又得做下比较了: Godot 中的子场景可比 Unity 中的预制体功能强大多了!子场景可以嵌套,可以覆盖,甚至还能单独运行,非常方便。其次,我们要了解到,什么情况下需要子场景:第一,独立的节点可以制作成子场景,方便开发、调试、合作;第二,重复利用的元素可以制作成子场景。最后,我们来使用子场景来改进一下我们当前的游戏结构。

在我们的游戏主场景中,玩家 Player 是一个五脏俱全的子节点,这里我们完全可以把它当做一个单独的场景进行开发利用,这样的好处在于可以单独修改 Player 节点,提高效率,而且当你有需求要在游戏的主场景中添加多个玩家的时候(这里不太可能,不过以后我们再谈多玩家局域网连线游戏),你会发现特别地方便!制作子场景一般有两种方式,这两种方式都非常简单,灵活采用。

我们先讲第一种方式:把场景中已有的节点转化为子场景。在我们的游戏主场景中,选择 Player 玩家节点,右键弹出菜单中,选择 Save Branch As Scene 即把该节点转化为场景,然后选择合适的位置,保存即可!现在 Player 节点变成了一个单独的子节点了,右边的 🎬 电影小标志说明该节点为一个子场景,你可以通过点击这个标志进入 Player 子场景进行编辑,非常简便、贴心。

godot_8_subscene.jpg

前面说过,子场景类似预制体,可以进行克隆创建出多个子场景的实例,接下来我们就通过制作金币子场景对此进行讨论。

制作金币场景

我们创建一些金币来丰富游戏的场景,供玩家探索发现。先构思一下金币在游戏世界中的表现:有一个金币,它闪耀在世界的某个角落,如果有幸被玩家拾取,将会播放一段动画,然后消失于人间!嗯,是时候把我们的想象力转化为实际操作了:我们来创建一个单独的金币子场景,包含有两个动画,一个是闪耀,另一个是消失动画,还要有碰撞反馈,最好能自我消失! grin

这就是我要讲的第二种子场景制作方式,首先我们点击场景编辑器上方的 + 号按钮,创建一个单独的场景,选择什么节点作为金币场景根节点呢?这里我要介绍一个新的节点: Area2D 区域节点。为什么要使用 Area2D 节点而非普通的 Node2D 或者之前我们多次接触过的具有碰撞属性的 StaticBody2D/KinematicBody2D 节点呢?原因在此:我们只需要一个能检测碰撞,但不需要有任何物理反馈的节点。 Area2D 在此非常合适,它可以用来制作一个区域,检测玩家进出该区域,相比 PhysicsBody2D 下的物理碰撞属性节点,它没有质量、弹性等属性,所以性能更高,另外有了 Area2D 作为根节点,我们没必要使用 Node2D 节点了。

选择 Area2D 作为根节点,改名为 Coin ,然后添加碰撞区域节点和图片、动画节点,调整相应设置,按 Ctrl+S 保存为 Coin.tscn 场景资源,场景结果如下图:

godot_8_new_scene.jpg

接下来需要给金币制作动画,按照前面的分析,需要两个动画:一个是没有被收集时的闪耀状态,一个是被收集后立刻消失的动画。第一个动画 rotate 非常简单,对于第二个消失动画 disappear 则稍微复杂点,但是只要把动画思路弄清楚,然后分多个轨道单独进行设计,调整,做出好看的效果也就非常简单了,动画分多个轨道:

  • 碰撞体禁用属性:玩家收集金币后碰撞体不再有效,启用 disabled 属性
  • 金币位置属性:金币从下往上漂浮,即 position 位置属性
  • 透明度属性:在颜色属性里让透明度变为 0 ,即 modulate 中的 alpha
  • 缩放属性:再添加一个缩放动画,在位置变化过程中不断缩小,即 scale 的值
  • 最后一个,金币需要回到第一帧,防止以某个侧面图片进行消失,设置 frame0 即可

godot_8_coin_animation.jpg

记得做动画过程中不断测试和调整播放时间。是不是感觉 Godot 中的 AnimationPlayer 简直是太强大了?嗯,甚至有点像 Adobe Animate ( Adobe Flash )动画工具啦!最后,提醒一点:由于金币会在玩家碰撞后立刻进行消失动画,这个时候我们要保证玩家不会再和金币继续产生二次碰撞,所以一定要在消失动画的第一帧就禁用碰撞体,同时注意运行游戏之前别因误勾选而禁用了碰撞体,这点特别重要,如果不明白怎么回事,又发生了金币不能被正常收集,那么你可以参考我之前的文章,使用 Godot 的碰撞体调试功能测试一下吧! sunglasses

连接信号

我们的场景已经准备完毕,现在需要添加一些操作来实现游戏的运行逻辑了。首先我们要做的是:当金币检测到与玩家有碰撞响应后立刻播放消失动画,表明已被收集。这个碰撞相当于一个触发器,而这个触发器在 Godot 中就是以 Signal 信号的方式传播出去的,我们收到信号之后立刻更改动画就可以了。那么,问题来了,这里涉及到一个非常重要的概念: Signal 信号,这又是什么鬼?别急,且听我慢慢解释。 smiley

编写过程序的朋友应该对程序设计模式中的观察者模式或多或少有所了解,观察者模式听上去很专业,高大上,实际上原理非常简单:有一个物体叫做事件源,也可叫被观察者,另外有一个物体叫订阅者,也叫观察者,或者事件侦听者,观察者订阅事件源的某个事件,当事件源发生了这个事件后,它并不需要知道谁订阅了它,只管把事件广播出去即可,然后那些订阅了这个事件的观察者们就能立刻侦听到这个事件,做出相应的处理,这就是所谓的观察者模式

举个例子,想象一下有这么几个主角:某指挥中心、某急救中心和某狙击手。他们之间的关系和事件,如下:

  • 狙击手作为被观察者,可随时发报
  • 指挥中心作为观察者,时刻等待信号到来
  • 急救中心同样订阅了狙击手的事件,作为观察者
  • 狙击手发现敌人,发出信号:“大量敌人出现”
  • 指挥中心收到信号,做出反应,立即派遣救援
  • 急救中心并没有订阅这个事件,或者订阅了也不处理
  • 狙击手被敌人干掉,发出信号:“ Help me! ”
  • 急救中心订阅了该事件,马上行动,开始救援

这就是观察者模式,如果还不清楚的话,可以看下图:

signal.gif

理解了观察者模式,就理解了 Godot 中的信号,回到金币场景中,当 Area2DCoin ) 发生碰撞的时候,立刻发出“碰撞”信号,所有的“感兴趣的订阅者”收到这个信号后作出各自相应的处理,这个处理就是订阅者们的“某个函数”。在 Godot 中订阅事件或者信号叫 Connect 连接,信号发出后,连接了该信号的订阅者的相应函数会被调用,也就是成功处理了该事件,完成一个流程。如何使用 Signal 信号呢?原理简单,操作也不难:

godot_8_connect_signal.png

按上图中的操作步骤:先给 Area2DCoin )添加一个空脚本,然后点击发出信号的节点 Area2DCoin ),在 Node 面板的 Signals 下显示了 Area2D 节点的所有信号种类,这里我们选择 body_entered(PhysicsBody2D body) 也就是碰撞体进入信号,双击它或者单击右下方的 Connect… 按钮,在弹出框中选择接收该信号的订阅者(这里订阅者仍然是金币节点本身,自己处理自己发出的信号),设置处理信号的方法函数,注意 Make Function 默认开启,如果关闭了则需要在脚本中手动编写该函数!连接后我们打开脚本文件,可以看到 Godot 自动帮我们添加了一个方法,同时在 Area2D 的信号面板中也有了变化: body_entered(PhysicsBody2D body) 信号下有了新建方法的连接提示。啰嗦了点,图片能理解的朋友直接跳过吧!

暂时丢下代码,我们转到主场景中添加我们制作好的金币子场景。在主场景中,点击 🔗 链接按钮,然后选择我们保存的金币场景资源 Coin.tscn 文件,即可实例化一个金币到主场景中,重复这个操作,多添加几个金币,放置到不同的位置,充分发挥你的想象吧!

godot_8_instance_subscene.jpg

工作基本完成,第二种子场景制作方式也介绍了,信号的原理、使用、添加也了解清楚了,最后就是逻辑处理啦。

逻辑代码

回到金币子场景,打开 GDScript 脚本,添加代码:

1
2
3
4
5
6
extends Area2D

func _on_Coin_body_entered(body):
$AnimationPlayer.current_animation = 'disappear'
# 打印文字到控制台,作为测试用
print('Coin collected!')

代码再简单不过!当金币被玩家收集后,也就是发生碰撞的时刻,金币发出信号,在代码中处理信号让金币消失——运行消失动画。运行游戏,测试!

貌似一切 OK ,实际上这里潜伏了一个大问题:硬币被收集后虽然表面上看不见,但实际上并没从场景中消失!如果你开启碰撞体调试就能清楚地看到这个问题的存在,这可能会引起一个运行 Bug :如果金币一直存在,游戏占用内存越来越多不能及时释放,以至于可能发生内存溢出而导致游戏崩溃!如何处理呢?会不会添加很多逻辑?哈哈,完全没必要,只需再添加一个简单的信号函数就可以轻松搞定!

我们已经在上一节做到了金币收集这个动作,接下来要处理的事情是:当金币的消失动画运行到最后一帧,要把它从游戏中真正的移除!这有涉及到信号的处理,当 AnimationPlayer 播放到最后一帧的时候也会发出一个信号: animation_finished(String anim_name) 动画结束事件,和 Area2D 的碰撞事件类似,选择 AnimationPlayer 节点下的相应信号,把这个信号连接到金币根节点 Coin 上,在方法处理中把该金币从游戏场景中移除!

1
2
3
4
5
6
7
8
9
10
extends Area2D

func _on_Coin_body_entered(body):
$AnimationPlayer.current_animation = 'disappear'
print('Coin collected!')

func _on_AnimationPlayer_animation_finished(anim_name):
if anim_name == 'disappear':
# queue_free方法将出该节点
self.queue_free()

唯一要注意的地方在于代码中的一个判断条件: if anim_name == 'disappear' ,这是因为其他动画播放结束的时候也会发出该信号,而我们只想在消失动画结束时候做相应处理。

大功告成,运行查看效果!

result_1.gif

Bonus: 函数动画

嗯,并没有结束,学无止境!我们再学习一个 Godot 中动画节点 AnimationPlayer 的新特性:函数调用关键帧!试想一下,如果我们可以在消失动画 disappear 的最后一帧自动调用金币根节点的 queue_free() 方法,那么不就可以实现场景中删除金币而无需连接信号、编写方法、处理逻辑了吗? Godot 3.1 就是这么强大,如你所愿!

首先,我们为了不重复处理同一个事件,我们需要取消动画播放结束的信号。只需要在已连接好的信号下方,点击 Disconnect 按钮取消关联即可。

godot_8_disconnect_signal.png

其次,需要稍微修改消失动画。在动画面板中,插入一个新的轨道: Call Method Track 即方法调用轨道,然后选择目标为 Coin 根节点;创建轨道后,在动画的最后插入一个新的关键帧,弹出 Select Method 方法选择框;搜索 void queue_free() 方法,在 Node 类下,点击确定,完成方法关键帧!大致步骤如下图:

godot_8_animation_with_function.png

OK ,总算结束了,高高兴兴地去全世界收集金币吧,骚年! sunglasses

三、总结

本章文字偏多,内容并不多,主要介绍了 Godot 中的两个关键特性,希望大家能理解并应用到自己的小游戏中。本篇代码已经上传到 Github ,最后总结一下本次学习到的知识点:

  1. 创建子场景并实例化子场景
  2. 连接订阅事件信号,处理信号
  3. 学习使用 Godot 3.1 动画中的方法调用特性
  4. 其他: Area2D 节点简介,碰撞处理,多轨道动画设计

够啰嗦了,还是那句话,原创实属不易,希望大家喜欢! smile

PS: 图片有一个单词写错 disappear -> disapear ,已经在源代码中更改,注意注意。 smiley

我的博客地址: http://liuqingwen.me ,欢迎关注我的微信公众号:
IT自学不成才


Comments: