[Godot3游戏引擎实践]平台跳跃小游戏(四)-玩家角色控制

欢迎阅读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 -= 1var local_var2 = param1 + 3 return local_var2# 内部类 class Something: var a = 10

可以发现有许多部分都像是从Python照搬过来的,比如动态类型、基于缩进的代码块、注释符号、函数定义、条件控制与循环等等。也有些不同的东西,比如类定义、类继承、内部类、变量定义等等。
我们就此打住,不详细展开讲解了,因为那会变得很枯燥。我们将在后续的游戏制作之旅中慢慢熟悉这门语言,对于新事物的学习,保持乐趣是十分重要的。
官方指南GDScript 基础几乎涵盖了所有语言方面的内容,脚本编写过程中可以作为参考文档参阅。
玩家角色脚本 创建脚本 打开Player场景,在Scene面板选中根节点,右键选择Attach Script,或者直接点击面板右上角的添加脚本按钮。
[Godot3游戏引擎实践]平台跳跃小游戏(四)-玩家角色控制
文章图片
添加脚本 在弹出的对话框中可以看到关于脚本语言、继承、模板与路径等相关设置,这里直接点击Create创建即可。
[Godot3游戏引擎实践]平台跳跃小游戏(四)-玩家角色控制
文章图片
新建脚本对话框 默认模板创建的脚本包含一些常用函数与指引,"#"后面的是注释:
[Godot3游戏引擎实践]平台跳跃小游戏(四)-玩家角色控制
文章图片
创建好的脚本 第一行表示脚本继承于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方向速度还需要累加上重力乘以每帧时间。
然后我们运行:
[Godot3游戏引擎实践]平台跳跃小游戏(四)-玩家角色控制
文章图片
没有人 (?Д?≡?Д?)人呢?到哪儿去了?看一下慢动作回放:
慢动作 什么原因呢,因为我们之前在制作地图时,还没有给地图加上碰撞体,角色的碰撞体没有跟其他碰撞体发生交互,所以就下地了。
给地图加上碰撞 来给地图加上碰撞体,回到Level1场景,在根节点下添加一个StaticBody2D节点,再在其下添加一个CollisionShape2D节点:
[Godot3游戏引擎实践]平台跳跃小游戏(四)-玩家角色控制
文章图片
添加静态体节点 可以发现StaticBody2D与构成玩家角色的KinematicBody2D有些类似,只不过它是静态的,不需要移动。
CollisionShape2D新建形状,这里选择矩形:
[Godot3游戏引擎实践]平台跳跃小游戏(四)-玩家角色控制
文章图片
新建形状 可以发现矩形出现在了工作区的原点位置:
[Godot3游戏引擎实践]平台跳跃小游戏(四)-玩家角色控制
文章图片
原点 调整它的位置与大小与地面相匹配,重复创建CollisionShape2D将地图要有碰撞的地方都画好:
[Godot3游戏引擎实践]平台跳跃小游戏(四)-玩家角色控制
文章图片
万恶的空气墙 然后运行,角色可以活碰乱跳了:
反复横跳 到现在为止的完整代码:
extends KinematicBody2Dvar 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,也没有根据状态播放对应的动画与音效,我们来一一完善。
地面判断 细心的朋友可能已经发现了上面的脚本中存在的问题,跳跃按键的检测没有限制,角色在空中时依然可以跳跃:
[Godot3游戏引擎实践]平台跳跃小游戏(四)-玩家角色控制
文章图片
梯云纵 需要对跳跃加上限制,假设我们的角色还没有掌握二段跳技能或者拥有特殊装备(比如泰拉瑞亚里的Fart in a jar),只能在地面上进行一次跳跃,那么这里的逻辑就很简单了,仅当角色处于地面时做跳跃按键处理即可。
如果要自己实现地面判断有些麻烦,说不定Godot已经提供了判断方法?Godot可以直接在编辑器里查看文档,我们来看看KinematicBody2D的说明文档,按住万能的Ctrl键(mac按?),鼠标左键点击脚本第一行的"KinematicBody2D",打开文档:
[Godot3游戏引擎实践]平台跳跃小游戏(四)-玩家角色控制
文章图片
查看文档 看来是有的,除了可以判断是否在地面,还可以判断是否在天花板和墙上。is_on_floor函数的说明:
bool is_on_floor() constReturns 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_forcevelocity.y += gravity * deltavelocity = 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函数中,通过AnimatedSpriteplay函数来播放对应动画,根据角色的朝向改变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 KinematicBody2Dconst FLOOR_NORMAL = Vector2.UPvar gravity = 3800 var jump_force = -1200 var max_speed = 400 var velocity = Vector2(0, 0)onready var AnimatedSprite = $AnimatedSprite onready var AudioStreamPlayer = $AudioStreamPlayerfunc _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 * deltavelocity = move_and_slide(velocity, FLOOR_NORMAL)

至此角色控制已经有了一个雏形,不算完美但基本能玩,下一篇文章将介绍摄像机的使用。
【[Godot3游戏引擎实践]平台跳跃小游戏(四)-玩家角色控制】下一篇:[Godot3游戏引擎实践]平台跳跃小游戏(五)-加入摄像机

    推荐阅读