欢迎阅读Godot3平台跳跃游戏实践系列文章,本系列将从创建工程开始,记录一个平台跳跃小游戏的制作过程,文章中如有错误或不妥之处欢迎指出。
上一篇文章中,我们了解了Godot中运动学物体2D与动画精灵的初步使用、碰撞体的创建、音频相关等,本篇文章将介绍如何通过脚本响应玩家的输入并控制角色的动作,本篇完成后的效果:
本篇涉及以下内容:
- 初识Godot脚本-GDScript
- 控制角色的移动与跳跃
- 地图碰撞与StaticBody2D(静态体)使用
- 角色动画与音效播放
初识Godot脚本-GDScript
终于要写脚本了!相信各位编程大佬们已经按捺不住手中的24k钛合金键盘了,而对于设计师朋友或萌新(本义)来说,脚本可能是比较头疼的部分。
Godot的脚本支持以下几种形式:
- GDScript:默认的脚本语言
- 可视化脚本:面向设计师或新手,简单但更为耗时,功能上存在局限性
- C#:当前(3.1.2)支持较为初级,日后会愈加完善
- C/C++:Godot本身使用C++编写,自然支持用C++编写脚本,但使用与编译较麻烦
Godot首推的是与引擎紧密集成的GDScript,用起来感觉如何呢?以我目前的理解来说,用GDScript写脚本简单够用。对于不太熟悉编程的人,GDScript的学习曲线比较平缓(可能要先去看看Python语法);对于大佬,GDScript能基本满足平常的功能需求。
如果你用过Python,那么恭喜你,GDScript跟Python没有太大区别,来看一段官方的代码示例(有删减):
# 一个文件便是一个类
# 继承
extends BaseClass
# (可选) 定义类名,与它的图标
class_name MyClass, "res://path/to/optional/icon.svg"
# 成员变量
var a = 5 # 数值
var s = "Hello" # 字符串
var arr = [1, 2, 3] # 数组
var dict = {"key": "value", 2: 3} # 字典
var typed_var: int # 指定变量类型
var inferred_type := "String" # 指定变量类型赋值
# 常量
const ANSWER = 42
const THE_NAME = "Charly"
# 枚举
enum {UNIT_NEUTRAL, UNIT_ENEMY, UNIT_ALLY}
enum Named {THING_1, THING_2, ANOTHER_THING = -1}
# 内置的矢量类
var v2 = Vector2(1, 2)
var v3 = Vector3(1, 2, 3)
# 函数
func some_function(param1, param2):
var local_var = 5 # 局部变量
# 条件控制
if param1 < local_var:
print(param1)
elif param2 > 5:
print(param2)
else:
print("Fail!")
# 循环
for i in range(20):
print(i)
while param2 != 0:
param2 -= 1
var local_var2 = param1 + 3
return local_var2
# 内部类
class Something:
var a = 10
可以发现有许多部分都像是从Python照搬过来的,比如动态类型、基于缩进的代码块、注释符号、函数定义、条件控制与循环等等。也有些不同的东西,比如类定义、类继承、内部类、变量定义等等。
我们就此打住,不详细展开讲解了,因为那会变得很枯燥。我们将在后续的游戏制作之旅中慢慢熟悉这门语言,对于新事物的学习,保持乐趣是十分重要的。
官方指南GDScript 基础几乎涵盖了所有语言方面的内容,脚本编写过程中可以作为参考文档参阅。
玩家角色脚本
创建脚本
打开Player场景,在Scene面板选中根节点,右键选择Attach Script
,或者直接点击面板右上角的添加脚本按钮。
在弹出的对话框中可以看到关于脚本语言、继承、模板与路径等相关设置,这里直接点击Create创建即可。
默认模板创建的脚本包含一些常用函数与指引,"#"后面的是注释:
第一行表示脚本继承于KinematicBody2D。创建的新脚本默认继承自当前节点的类型,这里Player节点是KinematicBody2D类型,则脚本也继承这个类型,这将方便我们直接在脚本中对当前节点进行操作。
变量需要用var
关键字声明,整洁起见,成员变量通常放在脚本的前面。
接下来是_ready
函数,func
关键字用于函数声明。_ready
函数将在节点准备好时被调用,这通常是当前节点进入到活动场景时。我们可以在这里做一些初始化的工作。函数里的pass
表示什么都不做。
最后是被注释掉了的_process(delta)
函数,它将在绘制每一帧时被调用,参数delta
是上次调用_process函数后所经过的时间,单位秒。我们可以在这里进行每一帧的与物理无关的处理。
然后就可以把除了第一行之外的代码删掉了,我们来编写自己的代码。
编写脚本
水平方向的运动
先来处理水平方向的运动。使用键盘时,我们希望通过方向键来控制角色左右移动,使用手柄时是十字键或摇杆,在触控设备上可能还会有一个虚拟控制器,万幸的是,我们可以对这些输入进行统一处理。
有输入后,角色需要跟随输入动起来。运动自然会有速度,在Godot中,二维空间的速度可以用内置的Vector2
类表示。我们在脚本前面(第一行之后)定义角色的当前速度:
var velocity = Vector2(0, 0)
这表示当前速度的初始值在x方向与y方向上均为0。
然后我们来处理输入:
func _physics_process(delta):
# 获取水平方向输入
var direction = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
velocity.x = direction * 400 # 计算x方向速度
velocity = move_and_slide(velocity) # 调用移动函数
这里添加了一个_physics_process
函数,与之前提到的_process
函数相比,_physics_process
将始终以固定的时间间隔调用,更适用于物理相关处理。
Input.get_action_strength
将获得指定按键的输入力度,范围0~1。什么,它还能知道我按键盘多用力吗?然鹅并不能,这个函数仅能获取手柄摇杆等有力度输入的控制器数值,对于键盘按下始终返回1.
在Godot的2D坐标系中,x轴向右为正,令右方向输入力度减去左方向输入力度,则可以得出角色的朝向数值。然后我们用朝向数值乘以角色的最大速度即可得出角色的当前速度,这里随便指定了一个最大速度。
之后调用神奇的move_and_slide
函数,传入当前速度。函数执行时,让角色以当前速度运动,如果发生碰撞,当前速度将根据碰撞发生改变,并返回改变后的速度,这里返回值用速度变量接收。
运行,角色可以动起来了:
竖直方向的运动
学习过抛物运动就知道,物体首先会有y方向上的初速度,接着由于重力的影响,速度将减少至0,随后向反方向增加。
当跳跃按键按下时,我们需要给角色一个竖直朝上的初速度,同时需要给角色施加重力,不然角色就上天了(物理)。
在脚本前面加上重力与跳跃初速度,数值先随便写,之后可以慢慢调整:
var gravity = 3800
var jump_force = -1200
在Godot的2D坐标系中,y轴向下为正,向上为负。
在_physics_process
函数中增加对竖直方向的处理:
func _physics_process(delta):
# 水平方向运动
# 获取水平方向输入
var direction = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
velocity.x = direction * 400
# 竖直方向运动
if Input.is_action_just_pressed("ui_up"):
velocity.y = jump_force # 赋予初速度
velocity.y += gravity * delta # 计算重力影响
velocity = move_and_slide(velocity)
Input.is_action_just_pressed
可以判断某个按键是否刚刚按下,这里判断上方向按键,当按键按下时,y方向的速度加上向上的初速度。由于y方向上受到重力,y方向速度还需要累加上重力乘以每帧时间。
然后我们运行:
(゚Д゚≡゚Д゚)人呢?到哪儿去了?看一下慢动作回放:
什么原因呢,因为我们之前在制作地图时,还没有给地图加上碰撞体,角色的碰撞体没有跟其他碰撞体发生交互,所以就下地了。
给地图加上碰撞
来给地图加上碰撞体,回到Level1场景,在根节点下添加一个StaticBody2D
节点,再在其下添加一个CollisionShape2D
节点:
可以发现StaticBody2D
与构成玩家角色的KinematicBody2D
有些类似,只不过它是静态的,不需要移动。
给CollisionShape2D
新建形状,这里选择矩形:
可以发现矩形出现在了工作区的原点位置:
调整它的位置与大小与地面相匹配,重复创建CollisionShape2D
将地图要有碰撞的地方都画好:
然后运行,角色可以活碰乱跳了:
到现在为止的完整代码:
extends KinematicBody2D
var gravity = 3800
var jump_force = -1200
var velocity = Vector2(0, 0)
func _physics_process(delta):
# 水平方向运动
# 获取水平方向输入
var direction = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
velocity.x = direction * 400 # 计算x方向速度
# 竖直方向运动
if Input.is_action_just_pressed("ui_up"):
velocity.y = jump_force # 赋予初速度
velocity.y += gravity * delta # 计算重力影响
velocity = move_and_slide(velocity) # 调用移动函数
完善角色脚本
现在角色的操控感觉十分梆硬,还存在着bug,也没有根据状态播放对应的动画与音效,我们来一一完善。
地面判断
细心的朋友可能已经发现了上面的脚本中存在的问题,跳跃按键的检测没有限制,角色在空中时依然可以跳跃:
需要对跳跃加上限制,假设我们的角色还没有掌握二段跳技能或者拥有特殊装备(比如泰拉瑞亚里的Fart in a jar),只能在地面上进行一次跳跃,那么这里的逻辑就很简单了,仅当角色处于地面时做跳跃按键处理即可。
如果要自己实现地面判断有些麻烦,说不定Godot已经提供了判断方法?Godot可以直接在编辑器里查看文档,我们来看看KinematicBody2D的说明文档,按住万能的Ctrl
键(mac按⌘
),鼠标左键点击脚本第一行的"KinematicBody2D",打开文档:
看来是有的,除了可以判断是否在地面,还可以判断是否在天花板和墙上。is_on_floor
函数的说明:
bool is_on_floor() const
Returns true if the body is on the floor. Only updates when calling move_and_slide().
仅在使用move_and_slide
函数时生效,接着阅读move_and_slide
函数的说明可知,还需要向函数中多传递一个floor_normal
参数,表示地面的法向量,这样Godot就能知道哪个是地板哪个是天花板了。
点击面板左侧的"Player.gd"回到角色脚本,先来定义一下地面的法向量,一个方向指向正上方的的单位向量:
const FLOOR_NORMAL = Vector2.UP
const
关键字用来声明一个常量。接着修改_physics_process
函数,在竖直方向的处理中加入地面判断:
func _physics_process(delta):
...
# 竖直方向
if is_on_floor(): # 判断是否处于地面
if Input.is_action_just_pressed("ui_up"):
velocity.y = jump_force
velocity.y += gravity * delta
velocity = move_and_slide(velocity, FLOOR_NORMAL)
保存并运行,角色已经不能施展梯云纵了。
加速与减速
角色运动没有加速与减速过程是手感梆硬的原因之一。在大部分平台跳跃游戏里,角色需要前进一段时间才能达到最大速度,松开方向键时,角色需要刹车一段时间才能停下,也就是有一定的加速度与减速度,这将让角色运动显得更加平滑。
Godot中最简单的实现方式是使用lerp
线性插值函数,我们先把最大速度定义为成员变量方便以后修改:
var max_speed = 400
接着修改_physics_process
函数:
func _physics_process(delta):
# 水平方向
var direction = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
# 使用线性插值计算速度
velocity.x = lerp(velocity.x, direction * max_speed, 0.2)
...
lerp(from, to, weight)
函数将在from与to的值之间按weight取一个插值,例如lerp(0, 4, 0.75)
将返回3。
这里from参数传入x方向的速度,to参数传入乘以方向后的最大速度,weight取一个你喜欢的值(0~1之间),水平速度便会平滑变化了。当角色前进时,需要花费数帧或数十帧时间(取决于各项数值)来逐渐达到最大速度,停下时也是如此。
保存并运行,可以感觉到角色的运动变得更平滑了。
最简单的方式不一定最好的方式,这里的实现将会得到一个比较奇怪的速度曲线。如果希望打造良好的角色操纵手感,不妨看看《蔚蓝》的手感为何迷人?中对角色运动的介绍。
动画切换
现在角色不论是静还是动都是显示行走动画,需要根据角色状态切换动画显示。
在上一篇文章中,我们创建了一个AnimatedSprite
节点来管理角色的动画,并且创建了"idle"、"walk"、"jump"三个状态的动画。现在我们需要在脚本中获取到AnimatedSprite
节点,并通过它来播放对应的动画。使用get_node("节点名")
或者$节点名
即可获取到节点:
var node = get_node("AnimatedSprite")
或者
var node = $AnimatedSprite
$
是get_node
的一种简写,这很jQuery.
需要注意的是,仅在节点准备好之后才能获取到节点,如果一开始便要获取到节点,可以在_ready
函数中获取,或者使用onready
关键字将节点获取为成员变量,这里我们使用第二种方式获取AnimatedSprite
节点:
onready var AnimatedSprite = $AnimatedSprite
在_physics_process
函数中,通过AnimatedSprite
的play
函数来播放对应动画,根据角色的朝向改变flip_h
的值来水平翻转动画:
func _physics_process(delta):
# 水平方向运动
...
# 动画
if direction != 0:
AnimatedSprite.flip_h = direction < 0 # 方向为负则翻转
AnimatedSprite.play("walk") # 播放行走动画
else:
AnimatedSprite.play("idle") # 播放空闲动画
# 竖直方向运动
if is_on_floor():
# 位于地面,获取跳跃输入
if Input.is_action_just_pressed("ui_up"):
velocity.y = jump_force
else:
AnimatedSprite.play("jump") # 播放跳跃动画
...
保存并运行,角色静止时将播放"idle"动画,行走时播放"walk"动画,跳跃时播放"jump"动画,且动画朝向与角色朝向一致。
音效播放
知道了动画如何播放,依葫芦画瓢就能写出音效播放了。上一篇文章中,我们创建了一个AudioStreamPlayer
节点来播放跳跃音效,同样将它获取为一个成员变量:
onready var AudioStreamPlayer = $AudioStreamPlayer
在_physics_process
函数中,跳跃按键按下时,播放音效:
func _physics_process(delta):
...
# 竖直方向运动
if is_on_floor():
# 位于地面,获取跳跃输入
if Input.is_action_just_pressed("ui_up"):
velocity.y = jump_force
AudioStreamPlayer.play() # 播放跳跃音效
...
可以在播放之前通过rand_range
随机范围函数改变一下音高,这样每次跳跃听起来会有点不一样:
AudioStreamPlayer.pitch_scale = rand_range(0.7, 1)
最终的运行效果(那个碍事的平台被我删掉了):
完整代码:
extends KinematicBody2D
const FLOOR_NORMAL = Vector2.UP
var gravity = 3800
var jump_force = -1200
var max_speed = 400
var velocity = Vector2(0, 0)
onready var AnimatedSprite = $AnimatedSprite
onready var AudioStreamPlayer = $AudioStreamPlayer
func _physics_process(delta):
# 水平方向运动
# 获取水平方向输入
var direction = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
# 使用插值计算速度
velocity.x = lerp(velocity.x, direction * max_speed, 0.2)
# 动画
if direction != 0:
AnimatedSprite.flip_h = direction < 0 # 方向为负则翻转
AnimatedSprite.play("walk") # 播放行走动画
else:
AnimatedSprite.play("idle") # 播放空闲动画
# 竖直方向运动
if is_on_floor():
# 位于地面,获取跳跃输入
if Input.is_action_just_pressed("ui_up"):
velocity.y = jump_force
# 随机范围音高
AudioStreamPlayer.pitch_scale = rand_range(0.7, 1)
AudioStreamPlayer.play() # 播放跳跃音效
else:
AnimatedSprite.play("jump") # 播放跳跃动画
# 施加重力
velocity.y += gravity * delta
velocity = move_and_slide(velocity, FLOOR_NORMAL)
至此角色控制已经有了一个雏形,不算完美但基本能玩,下一篇文章将介绍摄像机的使用。
下一篇:[Godot3游戏引擎实践]平台跳跃小游戏(五)-加入摄像机