[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 -= 1

    var local_var2 = param1 + 3
    return local_var2

# 内部类
class Something:
    var a = 10

可以发现有许多部分都像是从Python照搬过来的,比如动态类型、基于缩进的代码块、注释符号、函数定义、条件控制与循环等等。也有些不同的东西,比如类定义、类继承、内部类、变量定义等等。

我们就此打住,不详细展开讲解了,因为那会变得很枯燥。我们将在后续的游戏制作之旅中慢慢熟悉这门语言,对于新事物的学习,保持乐趣是十分重要的。

官方指南GDScript 基础几乎涵盖了所有语言方面的内容,脚本编写过程中可以作为参考文档参阅。

玩家角色脚本

创建脚本

打开Player场景,在Scene面板选中根节点,右键选择Attach Script,或者直接点击面板右上角的添加脚本按钮。

[Godot3游戏引擎实践]平台跳跃小游戏(四)-玩家角色控制_第1张图片
添加脚本

在弹出的对话框中可以看到关于脚本语言、继承、模板与路径等相关设置,这里直接点击Create创建即可。

[Godot3游戏引擎实践]平台跳跃小游戏(四)-玩家角色控制_第2张图片
新建脚本对话框

默认模板创建的脚本包含一些常用函数与指引,"#"后面的是注释:

[Godot3游戏引擎实践]平台跳跃小游戏(四)-玩家角色控制_第3张图片
创建好的脚本

第一行表示脚本继承于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游戏引擎实践]平台跳跃小游戏(四)-玩家角色控制_第4张图片
没有人

(゚Д゚≡゚Д゚)人呢?到哪儿去了?看一下慢动作回放:

慢动作

什么原因呢,因为我们之前在制作地图时,还没有给地图加上碰撞体,角色的碰撞体没有跟其他碰撞体发生交互,所以就下地了。

给地图加上碰撞

来给地图加上碰撞体,回到Level1场景,在根节点下添加一个StaticBody2D节点,再在其下添加一个CollisionShape2D节点:

[Godot3游戏引擎实践]平台跳跃小游戏(四)-玩家角色控制_第5张图片
添加静态体节点

可以发现StaticBody2D与构成玩家角色的KinematicBody2D有些类似,只不过它是静态的,不需要移动。

CollisionShape2D新建形状,这里选择矩形:

[Godot3游戏引擎实践]平台跳跃小游戏(四)-玩家角色控制_第6张图片
新建形状

可以发现矩形出现在了工作区的原点位置:

[Godot3游戏引擎实践]平台跳跃小游戏(四)-玩家角色控制_第7张图片
原点

调整它的位置与大小与地面相匹配,重复创建CollisionShape2D将地图要有碰撞的地方都画好:

[Godot3游戏引擎实践]平台跳跃小游戏(四)-玩家角色控制_第8张图片
万恶的空气墙

然后运行,角色可以活碰乱跳了:

反复横跳

到现在为止的完整代码:

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,也没有根据状态播放对应的动画与音效,我们来一一完善。

地面判断

细心的朋友可能已经发现了上面的脚本中存在的问题,跳跃按键的检测没有限制,角色在空中时依然可以跳跃:

[Godot3游戏引擎实践]平台跳跃小游戏(四)-玩家角色控制_第9张图片
梯云纵

需要对跳跃加上限制,假设我们的角色还没有掌握二段跳技能或者拥有特殊装备(比如泰拉瑞亚里的Fart in a jar),只能在地面上进行一次跳跃,那么这里的逻辑就很简单了,仅当角色处于地面时做跳跃按键处理即可。

如果要自己实现地面判断有些麻烦,说不定Godot已经提供了判断方法?Godot可以直接在编辑器里查看文档,我们来看看KinematicBody2D的说明文档,按住万能的Ctrl键(mac按),鼠标左键点击脚本第一行的"KinematicBody2D",打开文档:

[Godot3游戏引擎实践]平台跳跃小游戏(四)-玩家角色控制_第10张图片
查看文档

看来是有的,除了可以判断是否在地面,还可以判断是否在天花板和墙上。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函数中,通过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 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游戏引擎实践]平台跳跃小游戏(五)-加入摄像机

你可能感兴趣的:([Godot3游戏引擎实践]平台跳跃小游戏(四)-玩家角色控制)