Godot游戏开发新手教程 #1
翻译自:Godot Game Development for Beginner #1
作者:Daniel Buckley
- 介绍
- 项目文件
- 安装Godot
- 创建新项目
- 探索编辑器
- Godot如何工作
- 创建第一个场景
- 创建Player
- 编写Player脚本
- 创建Tile
- 创建敌人
- 碰撞敌人
- 收集硬币
- 追踪摄像机
- UI
- 编写UI脚本
- 结论
着急开始制作你自己的游戏吗?
游戏开发从来没有像现在这样流行过 - 销售额达到数十亿美元,成千上万的独立开发者磨炼他们的技能,并将他们的创造力带入其中。
即使是一个从未编写过代码的超级新手,在大量引擎的帮助下,任何人都可以制作他们自己梦想的游戏。
在这个综合教程里,我们将学习如何使用Godot创建你的第一个游戏。Godot是一个免费开源的2D和3D游戏引擎。因其开源,用户可以随意的添加或删除引擎中的内容,因此迅速获得了广泛欢迎。它还有一个活跃的社区随时可以提供帮助,所以它是制作第一个游戏最理想的选择。
在安装好Godot并学习一些编辑器的基础后,我们将制作一款2D平台类游戏,所以请打起精神,准备开始开发游戏吧!
本项目,还需要一些角色,平台,硬币,敌人等等素材。你可以自己制作也可以使用教程提供的。素材可以在这里下载。
你也可以通过上面相同的连接下载完整的项目文件。
要下载安装Godot,打开https://godotengine.org/。在这里,可以查看Godot的特性,去论坛交流等等。点击Download按钮。
选择你需要的平台,然后下载64或32bit标准版本(取决于你的操作系统)。
下载包为.ZIP文件。里面是一个应用程序,你可以将它解压到你电脑的任何位置。然后就没有然后了,Godot安装完成。
运行下载的Godot应用程序,可以看到Project Manager。在这里,我们可以创建项目,查看其它内容和下载模板。
点击New Project按钮来创建新项目。这将弹出一个新窗口,如下图:
- 输入项目名称
- 点击Create Folder按钮来创建一个新的文件夹
- 点击Create & Edit按钮启动项目
当编辑器弹出时,是不是看不明白?没关系,让我们分解一下。
Godot有4个主要的面板,我们将用它们来创建游戏,每个面板都有特定的用途。
- Scene面板。它显示在当前场景下所有的节点及其层级结构。我们将在后面简单的讲一下场景和节点。
- FileSystem面板。它显示所有的文件和素材。精灵,模块,脚本,场景,文件夹,音频等等。
- 在这里可以看到并创造我们的游戏。四处移动,选择,编写脚本等。在此面板的上方有4个按钮,它们可以切换面板显示的内容。如2D,3D,脚本编辑器和外部素材库。
- Inspector面板。当选择一个节点时,它显示此节点所有的设置项。如:位置,旋转以及其它一些可以修改的属性。
上面讲到的场景和节点。它们都是什么?Godot游戏是有层级结构的节点组成的。一个节点可以是任何对象:一个角色,摄像机,3D模型,光,UI等等。节点组成了游戏中所有的实体,还可以成为一个节点子节点。
下面是游戏中一个角色示例。首先,我们有一个KinematicBody(运动体)节点,可以控制移动和一些物理交互。其次,它有两个子节点,一个CollisionShape2D(碰撞体)和一个Sprite(精灵)。当KinematicBody节点移动,旋转等其它行为时,它所有了子节点都会随之进行。甚至删除父节点时,所有子节点都同样会被删除。
Godot游戏由许多的父子节点为组成,它们创建各种各样不同的元素和系统。
由于游戏是由节点组件,所以在一个场景面板里,最终可能有上百甚至上千个节点。这会导致很难找到正确的节点,并且会让整个游戏开发变得混乱。要解决这个问题,我们可以将节点放入不同的场景里。
场景是可以做为节点放入其它场景的节点包。让我们将上面角色示例中的节点层级结构放入一个场景中。在文件系统中,它是一个保存的文件,我们可以将它拖入另一个场景中。
将游戏划分为节点的一个好处还在于,我们可以很轻松的将重复节点移除,与其在游戏里放入100个具有精灵,碰撞体的节点,不如将它们创建为一个场景,放在多个实体里。
在整个项目中,我们都将探索节点和场景使用。
那么,开始我们的2D平台游戏吧。首先,我们需要创建一个主场景,它是我们游戏的基础,它包含其它场景,如:角色,平台,硬币和敌人。
在scene面板里,点击2D Scene按钮来创建一个新的2D场景。
双击这个新节点,重命名为MainScene。
即时保存场景是个好习惯。可以使用Ctrl + S或Scene > Save Scene来保存它。点击enter后你将在FileSystem里看到它。
好了,我们有了主场景。接下来,我们继续创建角色场景,它将拥有所有需要的节点和对应的脚本。
要创建一个新场景,我们可以通过Scene > New Scene来进行。在场景面板里,选择Custom Node。这次会打开一个窗口,搜索并选择KinematicBody2D节点。
将节点重命名为Player,然后保存它。
在我们的平台游戏中,将需要一些sprites和其它的素材。本教程带有一个ZIP文件,包含了所有需要的素材。下载后将三个文件夹拖入Godot的项目目录里。
回到Godot,右键点击Player节点,选择Add Child Node。添加一个CollisionShape2D节点作为子节点。然后到FileSystem里找到Player_Idle图片并将它拖到场景里。重命名这个节点为Sprite。
我们想让我们的sprite在居中,所以选择它,然后在Inspector中重新设置它。
- 打开Transform下拉框
- 设置Position为0, 0
你可能已经注意到,CollisionShape2D节点的边上有一个错误的符号。它的意思是需要给它一个形状。选择CollisionShape2D节点,在Inspector中设置Shape为Capsule。要使其可见,需要调整节点的层级,将CollisionShape2D节点移动到Srpite节点的下方。
设置CollisionShape2D的属性,点击Inspector中shape的选项,它会展开更多的选项。
- 设置Radius到27
- 设置Height到12
- 设置Position到-0.5, 14.7
现在了解了节点结构设置,我们可以开始编写Player脚本了。这将涉及移动,跳跃和收集硬币。
要创建脚本,选择Player父节点,在它的Inspector中,选择创建新的脚本。这会打开一个新窗口,点击回车。
可以看到,主窗口已经从2D切换到脚本模式。
Godot使用它自己的脚本语言,叫做GDScript。它的语法类似于Python。我们不会讨论语言的各个方面或编程的基本概念,但想要进行下去,需要基础的了解一下。
现在在脚本中,我们有两件事情。
extends KinematicBody2D
Extends类似于使用或导入,表示继承于kinematic body 2D对象,因此我们可以直接使用它的属性。
func _ready ():
pass
这是一个可重用的基本的函数代码块。_ready函数是Godot内置函数,它会在节点初始化后被调用一次。
pass只有定义函数时的一个填充,没有任何意义。如果函数体里是空的,那将会产生一个错误。
接下来添加一些变量。
# stats
var score : int = 0
# physics
var speed : int = 200
var jumpForce : int = 600
var gravity : int = 800
var vel : Vector2 = Vector2()
var grounded : bool = false
Vector2定义了x和y两个值,它可使用于位置,比例,旋转,速度。我们将使用它来存储Player的当前速度。
我们还需要一个变量。它是一个子节点Sprite的引用。onready的意思是当节点初始化后,获取到Sprite节点。
# components
onready var sprite = $Sprite
现在,我们定义了变量,那么来让我们的Player移动起来吧。首先,我们需要定义一些按钮的输入。打开Project Settings窗口(Project -> Project Settings),打开Input Map标签。
在Action字段中,输入一个名称,点击Add。
我们需要创建3个action。
- move_left
- move_right
- jump
在每个action的后边有一个'+'小图标。点击它,选择Key。它可以让我们为action设置一个按键。
为3个action都设置一个按键。
都设置好后,关掉窗口继续编写脚本。
我们继续创建一个Godot的内置函数。_physics_process函数每秒会被调用60次,它是用来做物理计算的函数。一旦我们开始在函数里写代码,就可以移除pass了。
delta参数是每帧之前的时间。我们可以将移动乘上它,以便基于每秒像素移动,而不是每帧像素移动。
func _physics_process (delta):
pass
首先,我们需要重置水平速度。然后监听左和右移动按键,它们将改变水平速度。
# reset horizontal velocity
vel.x = 0
# movement inputs
if Input.is_action_pressed("move_left"):
vel.x -= speed
if Input.is_action_pressed("move_right"):
vel.x += speed
接着,我们使用move_and_slide函数来移动Player,该函数将以特定的速度移动,并检测碰撞或其它事情。第二个参数是地面线(地面朝哪个方向?)
# applying the velocity
vel = move_and_slide(vel, Vector2.UP)
之后,我们可以施加重力,并检测是否按下跳跃键并且在地面上。如果是,Player将跳起来。
# gravity
vel.y += gravity * delta
# jump input
if Input.is_action_pressed("jump") and is_on_floor():
vel.y -= jumpForce
最后,我们可以根据移动的方向翻转精灵(转身)。
# sprite direction
if vel.x < 0:
sprite.flip_h = true
elif vel.x > 0:
sprite.flip_h = false
现在我们完成了脚本,让我们切回到2D模式进入MainScene。将Player.tscn场景从FileSystem中拖进来。
现在要做的就是测试游戏。在屏幕的右上角,点击Play按钮。
一个窗口将弹出,提示未选择主场景。点击Select按钮,然后选择MainScene.tscn做为主场景。
游戏界面将会显示,但是在重力作用下,我们的Player很快就掉出了屏幕。我们需要做的是创建一个平台场景,然后在主场景里复制它。
创建一个新场景(Scene -> New Scene),选择Custom Node,搜索StaticBody2D节点。它是一个静态的物理节点,也就是说它无法移动或做任何事情,只是检测碰撞。
- 重命名节点为Tile
- 添加一个CollisionShape2D节点为子节点
- 设置Shape为Rectangle
- 设置Extents为32, 32
- 从FileSystem中将Tile图片拖入场景创建一个sprite节点
- 重命名它为Tile
- 设置Position为0, 0
- 保存场景为Tile.tscn
回到MainScene,我们可以将tile场景拖入。然后使用Ctrl + D复制它并在周围移动它。
为了放置tiles更容易,可以打开对齐。在场景面板的左上角,点击snap按钮选中它。然后点击它边上的三个点展开snap设置框。确保只选中Use Pixel Snap。现在移动tile,它将基于像素进行移动。
现在,按下play,测试看看。
以Area2D作为根节点创建一个新场景。使用CollisionShape2D节点创建一个子节点,将其shape设置为circle,然后将Enemy sprite拖入到场景里(确保它居中)。最后保存场景为Enemy.tscn。
选择根节点的Area2D节点,创建一个新的脚本Enemy。
首先,创建变量。
export var speed : int = 100
export var moveDist : int = 100
onready var startX : float = position.x
onready var targetX : float = position.x + moveDist
export意思是此变量将会显示在Inspector中,允许我们改变场景实例的属性值。
在**_physics_process**函数中,我们将在startX和targetX位置进行移动。move_to函数是自定义函数,我们将在后面创建。
func _physics_process (delta):
# move to the "targetX" position
position.x = move_to(position.x, targetX, speed * delta)
# if we're at our target, move in the other direction
if position.x == targetX:
if targetX == startX:
targetX = position.x + moveDist
else:
targetX = startX
move_to函数按步骤将给定值转换为目标值(移动过去)。
# moves "current" towards "to" in an increment of "step"
func move_to (current, to, step):
var new = current
# are we moving positive?
if new < to:
new += step
if new > to:
new = to
# are we moving negative?
else:
new -= step
if new < to:
new = to
return new
回到MainScene,将Enemy.tscn拖入场景里。注意敌人与你要移动到的位置的距离,并在Move Dist属性中进行设置。
现在可以按下play,看看enemy的动作。你可能注意到了,它移动得实在太慢了,所以,我们将Speed设置为500.
现在,player可以穿过敌人而不会发生任何事。而我们想要的是,如果我们碰到敌人,游戏将会重置。为此,我们需要处理信号。这些是当节点发生一些事情后可以被调用的事件。
在Enemy场景,选择根节点Area2D节点,在Inspector里,切换到Node标签。在这里,可以看到此节点可以发出的所有信号。在body_entered信号上双击。按下回车键将在Enemy脚本里创建一个新的函数。
在此函数中,我们将检测进入body的对象是否Player,如果是,调用那节点的die函数。
# called when we collide with a physics body
func _on_Enemy_body_entered (body):
if body.name == "Player":
body.die()
现在打开Player脚本,创建die函数。
# called when we hit an enemy
func die ():
get_tree().reload_current_scene()
这样,当player碰撞到enemy时,场景将会被重置。
我们希望player达到某个目标,所以,让我们来添加一些硬币。在创建场景前,让我们先在Player脚本中添加一些内容。
collect_coin函数将在我们碰撞到硬币时被调用,它会根据给定的参数值来增加我们的分数。
# called when we run into a coin
func collect_coin (value):
score += value
接下来,创建一个新场景,根节点使用Area2d。为它添加一个CollisionShape2D节点做为子节点,shape选择circle。然后将Coin sprite拖入场景并让它居中。
在Area2D的根节点上,创建一个新的脚本Coin。它将检测是否和player产生碰撞,如果是则调用collect_coin函数。
我们从硬币唯一的变量value开始。
export var value = 1
_process函数是Godot的内置函数,它会在每一帧被调用。我们将使用它来制作硬币旋转。
func _process (delta):
# rotate 90 degrees a second
rotation_degrees += 90 * delta
接下来,选择Area2D节点,在Inspector中选择Node标签,双击body_entered信号在脚本中添加函数。
回到脚本,在函数中填上检测进入body的对象是否是player,如果是,调用collect_coin函数,销毁硬币。
# called when something collides with us
func _on_Coin_body_entered (body):
# was it the player?
if body.name == "Player":
body.collect_coin(value)
queue_free()
在主场景中,现在可以拖入硬币来填充关卡了。
目前,我们的摄像机是静态的,因此让我们实现水平移动摄像机的功能。
摄像机是我们现在要创建的新节点,游戏将根据节点和像素的位置来渲染,而不是摄像机的位置。因此,在MainScene中,右键点击父节点创建一个新的子节点Camera2D。
- 勾选Current,摄像机将被激活
- 移动摄像机覆盖整个关卡
接着在摄像机节点添加一个新脚本CameraController。
首先,需要创建一个变量来存储player的引用,get_node会根据指定的路径来搜索节点。
onready var player = get_node("/root/MainScene/Player")
然后在**_process**函数里(每帧都会被调用),设置x position和player一样。
# tracks the player along the X axis
func _process (delta):
position.x = player.position.x
现在,当我们按下play,可以看到摄像机跟随着player的x position。但它有点怪异。选中摄像机,在Inspector里,设置Drag Margins为0, 0, 0, 0。这意味着我们无法自动移动,摄像机将会始终跟踪我们的x position。
按下play,可以看到它们已经正常工作了。
我们的游戏已经完成了许多内容,现在需要添加一些用户界面来展示我们当前的分数。
首先,使用control节点(User Interface)创建一个root节点。
- 重命名节点为UI
- 保存场景
这时可以看到一个有颜色的矩形。这是control节点的边界,我们的UI元素将会构建在里面。
此矩形覆盖在整个屏幕上,让我们添加两个元素。
- 一个硬币图标
- 一个文本框用于显示当前分数
让我们从图标开始。创建一个新的子节点,类型为TextureRect。这是一个control节点,用于显示图像。在我们的FileSystem里,找到硬币图片,将它拖入Inspector的Texture域里。
将Position设置为20, 20。
接下来处理文本,创建一个新的子节点,类型为Label。它是一个control节点,用于显示文本。
- 重命名为ScoreText
- 设置Position为90, 20
- 设置Size为100, 64
这时,我们还看不到我们的文本,因为还需要一个字体。这里有一个默认字体,但局限性太大,所以,我们将使用自己的字体。
在FileSystem里,打开字体文件夹,右键点击字体文件,选择New Resource...,找到DynamicFont,创建它。
选择我们新的资源,进入Inspector,将.ttf字体文件拖到Font Data属性里。
在Settings标签下,设置Size为40。
选择ScoreText,在Custom Fonts标签下,将我们新的dynamic font资源拖进去。
现在可以在Text属性里输入一个占位符数字,设置Valign属性为Center,让文字垂直居中。
这样,我们完成UI设置。打开MainScene,将UI场景拖入场景中,将它显示出来。
如果按下play,你可以看到,它没有跟随摄像机移动。要修正这个问题,我们需要创建一个新的节点叫做CanvasLayer,并将UI节点变成它的子节点。该节点定义哪些内容渲染为UI。
剩下要做的就是将UI连接到脚本。在UI场景里,创建一个新的脚本叫UI,并将它附到父节点上。
首先,需要创建一个变量,在游戏开始的时候获取score文本节点。
onready var scoreText = get_node("ScoreText")
然后在唯一的函数里,设置score text的值为参数的值。
func set_score_text (score):
scoreText.text = str(score)
在Player脚本里,创建一个UI节点的引用变量。
onready var ui = get_node("/root/MainScene/CanvasLayer/UI")
接着来到collect_coin函数,添加一行代码设置UI text。
ui.set_score_text(score)
按下play,现在当我们收集一个硬币时,score text将更新。我们还要进行一些设置,以便游戏开始时显示设置的文本。在UI脚本里...
func _ready ():
scoreText.text = "0"
现在,当游戏开始时,score text将显示为0。
如果你在Godot项目里点击Play,一切都是正常的。恭喜你完成了第一个游戏!
现在,我们有了一个具有功能的2D平台游戏,它具有可玩的角色,硬币,UI和一个危险的敌人来阻碍我们前进。你可以改进此项目添加更多的挑战,但毫无疑问,这里讲解的基础将是你进一步提高游戏开发技能的有用起点。天空确实有极限,你可以根据需要扩展以创建具有这些基本原理的新游戏项目。
无论怎样,你都迈出了关键的第一步,创建了第一个游戏!我们祝愿你好运,我们已经迫不急待的想看到你制作的各种游戏了!