Godot3游戏引擎入门之十:介绍一些常用的节点并开发一个小游戏(上)

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

一、前言

时间飞快,我有一段时间没有发表博客了,这段时间并不忙,一方面我自己也在不断学习,另一方面暂时不知写哪方面的内容了,感觉 Godot 中一些基础的部分我都或多或少谈到了,所以我打算使用我们学习过的知识来做一个小游戏吧。

这个游戏非常简单,但是对于完全“门外汉”的初学者来时还算有一定难度,不过别急,我会把我制作这个小游戏的一些思路以及常用的技巧娓娓道来,而且源代码我于上周就已经上传到 Github 啦: https://github.com/spkingr/Godot-Demos ,另外这个游戏来源于一本书:《 Godot Engine Game Development Projects 》,官网也有这个 Demo(Coin Dash) 以及其他示例的代码,我的思路和代码和官方有点不同,也实现了一些其他功能比如游戏暂停、金币数量显示等,强烈建议大家去围观。 smiley

result_1.gif

本文分上下两篇,第一篇,也就是在进入“金币”小游戏的开发制作讲解之前,我先把之前文章里没有遇到过的一些非常重要的节点介绍一下,还有一个提醒:最好的学习方法应该是先尝试一遍或者边思考边把代码浏览一下,然后再来看我的文章,这样效果会比较好。嗯,废话不多说,我们开始吧!

主要内容:认识一些新的节点和代码学习
阅读时间: 10 分钟
永久链接: http://liuqingwen.me/blog/2018/11/30/introduction-of-godot-3-part-10-introduce-some-node-types-and-make-a-new-game-part-1/
系列主页: http://liuqingwen.me/blog/tags/Godot/

二、正文

本篇目标

  1. 学习使用一些新的 Godot 节点
  2. 最基本的游戏开发规则
  3. 编写代码的规范

Godot 中常用节点

1. Timer 节点

看名字就知道这是一个“计时器”。在 Godot 中一切皆节点,所以看到这种纯功能性的节点不要觉得奇怪,同时,我们完全可以不使用节点,直接使用代码 Timer.new() 动态创建一个计时器也是没任何问题的;甚至我们完全可以通过设置变量,利用 _process(delta) 方法来计算时间,不过显然没有 Timer 节点来得方便简洁!

godot_10_timer_node.png

Timer 时间计时器节点的属性非常简单,根据需求可以设置其等待时间、重复计时以及是否自动开始,这些属性我们也可以在 GDScript 脚本中使用代码修改:

  • wait_time :等待时间,即计时时长,结束触发 timeout 信号
  • one_shot :是否是一次性,如果是,只会触发一次 timeout 信号
  • autostart :自动开始,载入场景后计时,也可以使用 start 方法手动开启

游戏中计时功能使用非常频繁,不过,有部分计时场合我们还可以使用 yield 关键字代替,这样会省去节点的创建和信号的连接等繁琐、重复代码,这是分使用场合的,后面我会详述。 smile

2. Tween 节点

在游戏开发过程中,我们一般使用 AnimationPlayer 节点来实现移动、缩放、颜色渐变等动画效果,但实际上,在有些场景中我们可能会直接使用 AnimatedSprite 节点,再结合一系列图片来实现动画特效,这个时候由于图片的限制(比如我们只做了金币的闪耀图片,并没有做金币的消失图片),我们并不能添加实现其他普通动画,那是不是没有其他办法呢?——办法当然有,这就需要 Tween 节点的隆重登场了!

godot_10_tween_node.png

Tween 即渐进/过渡的意思,从一种状态在一定时间内变化到另一种状态,从而产生一种视觉动画。渐变节点使用非常简单方便,可以对一个物体的任意属性进行动画控制,当然,也可以同时处理多个动画对象。其主要方法有以下几个:

  • repeat :是否重复
  • start() :开始渐变,结束后触发 tween_completed 信号
  • interpolate_property() :设置进行动画的节点属性以及时长等,需要传递属性名称、开始结束值、时长等参数

这里最重要的方法是 interpolate_property() ,可以在 Godot 编辑器中按 F4 搜索 Tween 类进行查看。当然,和 Timer 节点一样,我们完全可以在代码中动态创建 Tween 对象。

3. Path2D 节点

Path2D 是一个路径节点,由很多位置点组成,这个路径可以是曲线,也可以是直线。实际上 Path2D 一般是与 PathFollow2D 配合使用,关于 Path2D 的使用,我推荐去看看官方的一个例子: Your first game

Your first game

在我要讲解的这个小 Demo 中,我使用 Path2D 路径节点绘制了一些点来保存需要用到的位置,后续我会详述。 smiley

godot_10_path2d_node.png

GDScript 几个重要关键字

1. export(PackedScene)/export(AudioStream)

在之前的文章中我们使用过 export(int) var speed = 10 来定义一个可以在编辑器中修改设置的整数值,以表示速度,同样地,我们可以使用 export 关键字来定义可以在编辑器中编辑的其他类型变量,比如:子场景、音频流等。

export(AudioStream) 用于定义一个音频流变量, export(PackedScene) 用于定义一个子场景变量,想象一下,游戏中我们制作了 3 种不同颜色的金币,每个关卡使用的金币可能不一样,这里我们就可以在关卡中定义一个 PackedScene 变量,然后直接在编辑器中选择对应的金币进行设置就可以了,非常方便。有点抽象,不过在后面的游戏代码中我们会应用到。

2. preload(‘res://resource.tscn’)

preload 方法可以在代码中动态加载场景、文字、图片、音频等资源,比如我们可以预加载制作好的金币子场景,然后在代码中实例化,生成多个金币节点并添加到舞台中,实现动态添加金币的效果。 preload 是一个常用方法,不过在这个游戏中我并没有使用到,暂时提一下,以后讲 Singleton 单例再详述吧。

3. ProjectSettings.get(‘display/window/size/width’)

在游戏创建的时候,我们都会对项目相关属性进行设置,比如游戏屏幕显示尺寸大小等,那么如何在代码中动态获取这些参数值呢?我们可以直接使用 ProjectSettings 这个单例,通过传入属性的路径,比如窗口大小的高度: display/window/size/height 即可获取相对应的配置值,这样能避免硬编码,即使修改了配置游戏依然能正常运行!

4. rand_range/randomize/randi

很多游戏中都会大量使用随机值,比如金币数量随机、金币品类随机、出现时机随机等等,在 GDScript 脚本中使用随机同样非常简单直接,一个方法 randi() 即可生成一个随机整数,不过这个整数的范围很大,需要生成范围限制的随机数则可以用 rand_range() 方法,接收两个参数,一个最小值,一个最大值。

除了这两个方法,还有一个 randomize() 方法,这个方法有什么用呢?如果你在游戏中使用随机数,你会发现每次运行游戏,这个随机数都是相同的,这是因为生成随机数需要一个 seed 也就是名为种子的整数,因为种子并没有随机,所以根据这颗种子生成的随机数自然也就不会变化了,如何做到真正的随机呢?——在使用随机方法前,调用一下 randomize() 方法就可以啦!

5. get_tree().paused

我在游戏中添加了暂停的功能,相信大部分游戏都有这个功能吧。在 Godot 中暂停功能非常容易实现!直接调用 get_tree().paused = true 这一行代码就可以了,是不是感觉非常轻松直接?哈哈,不过记住:一旦运行这行代码后,我们的游戏会完全处于暂停状态,也就是说不论游戏本身、还有输入、甚至弹出的 UI 界面等都一律等闲视之——后果就是你不能继续游戏了!

当然,解决这个问题是非常简单的,我们只需要把那些不被默认暂停的元素(暂停状态下依然可用)Pause Mode 暂停模式设置由 inherit 属性改成 process 就可以了:

godot_10_pause_mode.png

6. yield()

这可以算是 GDScript 脚本的一个高级功能,它和 Python 中的 yield 关键字如出一辙,如果你熟悉协程的概念,像 Unity C# 中的 StartCoroutine() 方法, Kotlin 中的 Coroutine 协程, Dart/JavaScript 语言中的 await/async 关键字,那么 yield 的工作原理是很好理解的。

对于新手来说,我觉得可以把协程简单地理解为:程序运行到该位置( yield ),暂停挂起在当前位置,继续执行其他代码,当时机到来,回到刚才挂起的位置继续执行。

嗯,听起来有点玄乎,不过在代码中使用起来非常简洁,参考运行下面的代码吧:

1
2
3
print('开始运行程序……')
yield(get_tree().create_time(1.0), 'timeout') # 挂起 1 秒钟
print('1秒钟后输出:结束运行。')

游戏开发的几个小 Tips

几个实用的小技巧或者说开发规则,也是我自己在开发实践中、他人的书籍里、一些博客文章中学到的,总结的不多,不过对于初学者来说还是比较重要的,可以先按部就班,之后再发展处自己的风格思路吧! grin

1. 文件夹的管理

在我之前的文章里,对于小项目我都没有做特殊的文件管理,但是当游戏项目越来越大的时候,我们需要引起足够的重视,因为这会影响开发速度、以及团队合作的效率。其实,你完全可以按照自己的风格去管理资源文件,但是更推荐官方的一些做法和建议: Project organization

1
2
3
4
5
6
7
8
9
10
11
12
13
/project.godot
/docs/.gdignore
/docs/learning.html
/models/town/house/house.dae
/models/town/house/window.png
/models/town/house/door.png
/characters/player/cubio.dae
/characters/player/cubio.png
/characters/enemies/goblin/goblin.dae
/characters/enemies/goblin/goblin.png
/characters/npcs/suzanne/suzanne.dae
/characters/npcs/suzanne/suzanne.png
/levels/riverdale/riverdale.scn

这里我简单地比较了 Unity 和 Godot 中文件管理的风格样式,我个人更倾向于 Godot 的文件组织方式,因为等会我还会讨论一条重要的开发原则:尽量保持每个子场景的独立性

file_orgnization_unity_vs_godot.png

2. 保持场景独立

嗯,我认为这是 Godot 中开发游戏最重要的一条原则了!它能明显地提升开发效率,提高团队合作,更利于 Debug 调试。因为 Godot 中一切基于场景,场景中可以包含多个子场景,子场景依然可以由多个其他子场景组成,而且每个子场景是可以单独运行的!打开子场景,按 F6 来单独运行、测试,及早发现问题,提高程序的健壮性。

如何保持场景独立?这就需要我们去仔细思考了,具有独立功能的部分我们都可以抽离出来作为一个单独的子场景,通用、具有类似功能的节点也可以抽离出来以继承关系实现,需要说明的是:独立并不意味着不与其他场景发生任何关系了,独立只是让它能单独运行,能单独测试一部分功能,这是很重要的。

3. 代码编写规范

代码构成了游戏的灵魂,代码编写不规范带来的直接后果就是:

  • 自己看不懂,遇到 BUG 后越改越乱
  • 团队里其他开发者看不懂,很难或者无法 DEBUG
  • 不利于后续功能的开发、重构等

和文件组织管理方式一样,其实代码编写规范也会因人而异,在 Godot 中官方所推荐的方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 枚举、常量等变量命名
enum State{INIT, IDLE, PLAYING, DEAD}
const CONST_GRAVITY = 98

# 普通变量、私有变量命名
var player_sprite = 1
var _walk_speed = 2

# 私有方法命名
func _private_method():
get_tree().paused = true
pass

# 公有方法命名
func public_method():
_private_method()
pass

注意,在 GDScript 中是没有 private/public/protected 等关键字来规范访问限制的,类似 Python ,这也正是我们需要保持一定的编码规范的原因之一。不过,你会发现我的命名方式会有所不同!我比较习惯 Java/C#/Dart 等语言的命名规则,采用驼峰式,同时利用 _ 下横线来标记私有变量或者方法,而且调用内部方法的时候我都会显式使用 self 关键字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 枚举、常量等变量命名
enum State{INIT, IDLE, PLAYING, DEAD}
const CONST_GRAVITY = 98

var playerSprite = 1 # 公有变量
var _walkSpeed = 2 # 私有变量

# 私有方法命名
func _privateMethod():
self.get_tree().paused = true
pass

# 公有方法命名
func publicMethod():
_private_method()
pass

至于选哪种,我觉得只要保持规范,符合个人或者团队共识就好啦! smiley

三、总结

本篇文章算是一个经验小总结吧,也是为了更好地解释我们后面要出场的游戏项目,林林总总地列举了一些不成文的条条列列,不知道大家看后的感受是怎样的呢?

嗯,有两周没有写文章了,因为最近有其他的事情和同学在忙乎,不过我一定会坚持下去的,还是那句话,原创非常不易,希望大家喜欢! smile

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


Comments: