2018-11-09 by Liuqingwen | Tags: Godot | Hits
本文开篇必须提到两个值得高兴的消息:
之前的文章里我已经申明过:我使用的是 Godot 3.1 预览版,如果要使用我所上传的 Github Demo 代码,那么务必到官网相应的版本哦!下面附上最新预览版下载地址:
工欲善其事必先利其器,好了,继续我们的 Godot 入门系列。依然基于上一篇文章,本篇我会给大家熟悉的“金币收集者骑士”小 Demo 划上一个句号,几个简单必要的任务是:添加常见的 UI 界面;然后再加一点料——游戏的音乐效果。再浏览之前,请务必参考上一篇文章: Godot3 游戏引擎入门之八:添加可收集元素和子场景。
主要内容: 创建 UI 界面以及添加一些音效 阅读时间: 8-10 分钟 永久链接:https://cloud.tencent.com/developer/article/1381581 系列主页: http://liuqingwen.me/blog/introduction-of-godot-series/
在添加 UI 控件显示金币收集数量之前,我们需要思考三个小问题,这三个问题解决好了界面就非常简单了,接下来我们一个一个解决。第一个问题就是:如何判断游戏场景中的金币已经被收集?
这个问题其实很好解决,在上一篇文章中我们已经在 AnimationPlayer 制作消失动画并结合代码实现的过程中已经解决了:使用 Signal 信号!金币在被采集的时刻,也就是玩家 Player 和金币 Coin ( Area2D )发生碰撞的那一刻,节点会发出 body_entered
的信号,我们通过连接这个信号做出处理并切换了金币的消失动画,同样的道理,我们可以利用这个信号在游戏主场景中加以利用,在信号订阅函数中进行计数处理,但与之前不同的是:
因为这几点不同,我们引出了第二个问题:既然金币数量不确定,我们要避免手动连接信号,那么如何在代码中连接信号呢?这个问题非常简单,一句代码解决: coin.connect('body_entered', target, 'your_method')
,代码种 connect
方法第一个参数为信号名称,第二个为目标即订阅者,第三个为处理信号的函数。这和我们之前使用编辑器连接信号是一样的效果,同样的,我们可以使用 disconnect
方法取消信号的连接。
两个问题都解决了,那么我们的模板代码大概是这样的:
# 这里的self指当前节点
$Coin1.connect('body_entered', self, '_on_Coin_collected')
$Coin2.connect('body_entered', self, '_on_Coin_collected')
$Coin3.connect('body_entered', self, '_on_Coin_collected')
# ……
# 碰撞处理函数
func _on_Coin_collected(body):
# 处理金币收集
pass
明显地这里引出了第三个问题:那么多金币,如何简便地、一次性地获取场景中所有金币呢?解决这个问题的核心在于使用 Godot 中的另一个重要概念: Group 分组!考虑一下分组的应用场景:游戏场景中有很多金币,他们同属于某个金币分组,我们通过 GDScrip 代码的某个方法,获取了这个分组的所有金币信息,然后使用一个循环就可以轻松解决上面的重复代码问题了。这就是 Group 的一个最简单的应用场景。理论结束,实践起来非常简单:在编辑器中创建分组,然后添加到金币子场景的节点即可!
如上图,我们创建了一个 coin
分组,之后我们并不需要在游戏主场景中对每一个金币实例进行分组的添加工作,只需在金币子场景中直接给根节点 Coin 添加 coin
分组就可以了。
接下来我们需要把金币收集数量显示到游戏场景中!也是第一次接触 Godot 中的 UI 控件吧,哈哈。在 Godot 中使用控件和节点没有任何区别。 Godot 中所有的控件都是继承于 Control 节点,我们只需要添加相应的 UI 节点就能在场景中显示,需要注意的是:控件的渲染和普通节点一样,后面的节点会覆盖前面节点的显示!在游戏中 UI 界面一般都会显示在主界面的最上层,那么我们添加控件的时候就需要把节点置为根节点 Game 的最后一个子节点。但是,这样做有个缺陷:一旦有新节点添加到游戏场景中,默认位置为最后,这就难免还要去修改 UI 元素。对于游戏开发者来说,时间就是金钱,那有没有办法让 UI 层忽略其他节点,一直显示在最顶层,达到一劳永逸的效果呢?那就有请“金钱节约者” CanvasLayer 隆重登场!
CanvasLayer 节点是一个特殊节点,它能确保渲染在最顶层,这正是我们所需要的。我们只要把所有控件节点设置为 CanvasLayer 层的子节点即可。说做就做,在主场景中添加一个 CanvasLayer 子节点,改名为 UI ,然后往它里面添加其他子节点:首先添加一个 HBoxContainer 控件节点,如同其名,这是一个内容水平排列的盒子容器;在该节点内部添加一个显示金币图片的控件 TextureRect 节点,以及一个计数文本标签节点: Label 控件。控件节点的属性设置如下:
texture
材质属性为金币图片 text
属性即文本内容为: Score: 0
如上图,这里的 Layout 属性是所有容器节点具有的属性, Top Wide 即顶宽,占据视窗顶部位置并拉伸宽度到最大。不过,现在有一个问题就是:文本标签中 Score 中的文字太小了!作为程序员,第一反应肯定是去找字体大小属性设置即可,不过在 Godot 中控件的文字大小并不能直接设置,我们必须先提供字体资源然后在此基础上设置字体大小!
这个字体资源就是 Custom Font 自定义字体,一般为 ttf
格式,准备好字体文件,点击 Label ( Score ) 标签,在 Custom Fonts 的 Font 属性标签下,选择 New DynamicFont 创建一个新的动态字体,点击新建的动态字体进入字体资源相关设置面板,把 ttf
格式的字体文件拖拽到面板的 Font Data 属性下,最后在属性面板里设置字体的大小,字体的轮廓、颜色等就可以了,操作稍微复杂,适应一下就好了。
注意:如上图,这里我把新建的字体资源保存成了单独的文件,该资源文件命名为 font.tres
,这些资源在后面可以重复利用,如果你不知道如何保存相关资源,可以翻一下我之前的文章。
金币分组已设置好, UI 界面也准备完毕,现在可以添加代码实现我们“梦寐以求”地计数功能了,哈哈。接下来,通过场景获取所有属于 coin
分组中的金币,然后把分组中的每个金币逐个连接到碰撞信号处理函数,最后在连接好的方法中实现计数功能,理论在前面已详述,在 Game 根节点代码基础上添加代码如下,可以参考我给的注释:
# 省略代码……
# 添加 UI 后的代码
onready var scoreLabel = $UI/HBoxContainer/Score
var score = 0 # 用于统计金币收集数量
func _ready():
# 从场景数中获取所有属于coin分组的节点
var coins = self.get_tree().get_nodes_in_group('coin')
for *Coin* in coins:
# 手动连接信号,用connect方法,第三个参数为信号处理函数名
coin.connect('body_entered', self, '_on_Coin_collected')
# 碰撞处理函数
func _on_Coin_collected(body):
score += 1
updateScore()
# 更新UI界面
func updateScore():
scoreLabel.text = 'Score: ' + str(score)
# 省略代码……
代码很简单,唯一值得注意的是 body_entered
信号处理函数需要传递一个参数。编写代码过程中如果遇到有任何问题,随时可以在 Godot 编辑器中按 F4 搜索查看相关说明。
运行我们的游戏,左上角,终于知道自己口袋里有多少 Money 了吧?!不过好像还是缺少点什么?嗯,缺少点声音——金币收集后的音效。和很多其他游戏引擎一样,在 Godot 中添加普通的音效非常简单,准备好我们需要的音乐素材,一个节点即可搞定: AudioStreamPlayer ,注意,你会发现 Godot 中有其他两个节点: AudioStreamPlayer2D 和 AudioStreamPlayer3D ,它们分别应用于 2D 世界和 3D 世界中的音特,比如声音传播立体感、传输的距离感等,不过这里我们不需要。
我们给游戏添加两个音效,一个是金币收集后消失的音效,一个是游戏的背景音乐。
金币收集音效:在金币子场景中再添加一个节点 AudioStreamPlayer 作为音乐流载体,音效是在 disappear
消失动画开始播放后才同时进行,所以我们需要把音效添加到相应的动画轨道上。首先打开动画面板,选择我们已经创建好的消失动画,然后添加一个音频轨道: Audio Playback Track ,在弹出的界面中选择刚才添加的 AudioStreamPlayer 节点,然后把准备好的音乐资源文件直接拖拽到新建的音频轨道上即可!简单,方便,又不失强大。
游戏背景音乐:同样地,在游戏主场景中添加一个 AudioStreamPlayer 节点,然后设置节点的 stream
音频流属性,只需要把准备好的背景音乐直接拖拽过去即可!另外,可以适当调整一下音乐的音量,这里我把 Volume Db
音量的分贝设置为了 -20
降低了背景音乐的音量,比较合适。
最后,添加一行代码,让场景加载完后自动播放背景音乐:
# 省略代码……
onready var audioPlayer = $AudioStreamPlayer
func _ready():
# 场景加载完毕后开启背景音乐
audioPlayer.play()
# 省略代码……
好了,运行游戏,收集几个金币,喝上几口凉茶,放松一下心情吧!骚年!
嗯,还没完!我们已经掌握了几个最基本的 UI 控件,在此基础上再把游戏打造的稍微完美一点。是时候介绍一波自己强大的游戏了!哈哈。和大部分游戏一样,我们给自己的 Demo 添加一个入口界面作为启动后的主界面,在这个界面的功能是突出显示游戏的名字,告诉玩家如何开始新的旅途,以及说明游戏体验是如何高大上,写明游戏的创作者有多牛逼……嗯,有点飘了,你继续,我来写。
这个界面并不复杂,两行文字即可,也恰如其分地体现了我们游戏的简陋,嘿嘿。首先新建一个子场景,因为主要是 UI 元素,使用Control节点作为根节点,改名为 StartMenu ,添加一个 CenterContainer 作为直接子节点,并在其下添加一个 VBoxContainer 垂直容器,容器内添加两个 Label 标签子节点。这几个节点的名字很好地解释了其功能: CenterContainer 是一个能把内容居中显示的容器, VBoxContainer 为一个内容垂直分布容器。这里我设置 CenterContainer 的 Layout 布局属性为 Full Rect 全屏显示,而两个文本标签都设置了 Align 对齐属性为 Center 居中,并写上几个“高大上”的文字。
给文本标签修改字体,这里我使用了之前保存的字体资源: font.tres
。不过,当我想在第二个标签中把字体放得更大、颜色更鲜艳、更突出表现的时候,你会发现一处修改,所有应用了该字体资源的文本标签都变了!为了标新立异,是不是又要重新创建一个独立的资源文件呢?别急,很显然, Godot 早已考虑到了这点,我们只需要让资源唯一化即可轻松达到目的!在标签属性面板中,选中我们的字体资源,然后打开属性面板上的选项,选择 Make Unique 就可以轻松搞定啦!
最后,给主场景也添加一个背景音乐,和之前的节点设置稍微有差别的是,这里我给 AudioStreamPlayer 节点上勾选了 AutoPlay
属性,也就是自动播放而无需使用代码进行控制了。我们的游戏界面做完了,保存好,按下 F5 启动游戏运行,这时候游戏还是会自动进入骑士收集金币的界面,这不是我们想要的,我们需要从 StartMenu 场景开始,所以要对主场景进行修改,在 Project -> Project Settings -> Application -> Run -> Main Scene 中,选择创建好的主界面 StartMenu.tscn
即可修改主界面为我们创建的菜单界面, OK ,一切准备就绪!
别忘了添加切换场景的代码,否则按 Enter 键或者空格键都不会有任何效果:
extends Control
# 游戏场景资源路径
var gameScene = 'res://Game.tscn'
func _input(event):
if event.is_action_released('ui_accept'):
# 当按下空格或者回车时切换场景到Game
self.get_tree().change_scene(gameScene)
大功告成:
总算结束了——这个“高大上”且“及其无聊”的“骑士满地找钱”游戏,哈哈。不知道大家看完后感觉怎样?不管如何,我们还是来总结一下本次学习到的一些 Godot 中的新鲜知识点吧:
最后的最后,我所要提醒的是, Godot 所支持的音频文件包括 OGG 和 WAV 格式,前者一般用于背景音乐,后者用于短音效,而不支持 MP3 格式的音频,另外我们的游戏也缺少很多很多普通游戏应有的一些机制,比如结束、暂停机制,没有怪物敌人、粒子特效,无关卡设计,不支持多人游戏等等,当然,这完全有待我们将来的开发啦!尽情期待吧!
本次代码已经上传到 Github ,还是那句话,原创非常不易,希望大家喜欢!