从零开始独立游戏开发学习笔记(七十一)--Godot 学习笔记(四)--指南(二)-输入

因为我是边做游戏边学的,所以其实感觉到最先难为我的其实是输入,怎样写出最佳的输入代码。所以我先看输入部分了。

1. InputEvent

通常来说,无论是 OS 还是平台,操作输入都是很复杂的事情。为了让这件事好做一些,一个 InputEvent 的内置类型因此而生。输入事件会穿过引擎,在游戏的各个部分都能够被接收,取决于需求。

这里先从一个小例子开始,这个例子处理输入 esc 退出游戏的情况:

js 复制代码
func _unhandled_input():
    if event is InputEventKey:
        if event.pressed and event.keycode == KEY.ESCAPE:
            get_tree().quit()

然而,这有点复杂,判断太多了。

使用内置的 InputMap 更好,这个就是在之前教程里讲过的,在项目设置里配置的,然后用 is_action_pressed 调用的,标准处理方式。

InputMap 可以让处理多个 key,并且也方便维护(比如更换触发键,只需要调整项目设置里的就行了)。此外,还能便利地在游戏中更换键位,which 几乎每个现代游戏都必须要有。

1.1 输入机制是如何工作的?

首先,用户给一个输入。然后 OS 获取到输入,传递给窗口,传到 Viewport(root),后面的比较复杂用一张图来说明吧:

其实这个图为了避免太长做了一些修正,下面几个事件是有顺序的而不是并行的。顺序如下:

  1. 如果 Viewport 是一个窗口,这个输入首先会被用于判断是否是对窗口进行的操作,比如移动窗口,改变窗口尺寸等。
  2. 好,上一步结束后,如果有一个窗口正处于 focused 状态。那么事件会被送往那个窗口,然后在该窗口中处理。如果没有窗口处于 focused 状态,那就送往当前 viewport 里的节点们,继续下一步。
  3. 首先,输入会被送往 Node._input() 函数(如果重写了, 且没有被 Node.set_process_input() 禁止的话)。可以在此通过 Viewport.set_input_as_handled() 来阻止事件进一步传播。这一步能够帮助你在进行 GUI 操作前处理输入。
  4. 接下来,这个输入会被送往 GUI 进行处理,叫做 Control._gui_input(),此外,gui 还会触发一个信号 gui_input。gui 里面通过 Control.accept_event() 来阻止事件进一步传播。gui 里还可以用 Control.mouse_filter 来控制 gui 是否接受鼠标信号以及是否进一步传播。
  5. 好的,如果到现在还没有被阻止。那么继续前往 Node._shortcut_input(),其他表现和 _input 基本一样。不过这个只会处理 InputEventKey,InputEventShortcut,InputEventJoypadButton 这三种事件。(像是鼠标事件这种就不行了)。正如名字所示,这个事件一般用于快捷键。
  6. 如果仍然没有处理完,那么就进入 _unhandled_input(),一般 gameplay 的输入会放在这里。
  7. 还没有处理完?接下来还有 Node._unhandled_key_input(),如名字所示,只处理键盘输入。
  8. 最终,事件到达最后。会触发 Object Picking。该功能可以在项目设置中配置。会在鼠标指着的对象上(如果有碰撞体)执行 _input_event()。这个如名字所示,用于鼠标事件。所以如果有这样的需求,就要给这些对象上都加上碰撞体。

那么,节点之间有没有顺序呢?有。输入事件在也会在节点上按照顺序传播。如下图所示:

也就是说从最下面开始,一直到上面,或者说反向深度优先(其实很合理,因为从 canvas 的视角来看,最下面的节点会覆盖上层的节点)。windows,viewport 不管这些 order(如前所示,它们最先跑)。此外,_gui_input 也不满足这些 order。gui 遵循一套自己的方式。

gui 的控制一般要求会很精确,所以只有事件目标的直接祖先会接受该事件。看下图(名字不是顺序,只是我自己测试用的): 如果在 5 上触发事件,只会接着触发 4,不会再传播到 1,2,3 上,因为这些都不是 4 的祖先。也就是说 gui 的更像是前端的冒泡。

需要注意的是,因为 Viewport 不会发送输入到别的 Subviewport,所以要用下面两种方法之一:

  1. 使用 SubViewportContainer。
  2. 根据需求,自己实现事件传播。

1.2 输入事件的剖析

输入事件代表的是一个事件,不是游戏场景中具体存在的某个东西。

1.3 action

action 可以把一个或者多个输入事件分组为一个容易理解的标题。这有利于你把输入抽象化。

这样可以:

  • 匹配不同的设备:键盘,手柄
  • 键位更换
  • 手动触发操作

如果要手动触发操作,可以这么弄:

js 复制代码
var ev = InputEventAction.new()
ev.action = "jump"
ev.pressed = true
Input.parse_input_event(ev)

1.4 输入映射

InputMap,就是你在项目设置里设置的那个。

2. 处理输入的案例

最佳实践来了,太棒了。

2.1 事件与轮询

有时候你希望游戏响应某个特定的时间,比如跳跃。有些时候你希望任何时候只要按下按键就会发生什么,比如移动。第一种情况可以用 _input 函数,第二种情况则可以使用 godot 专门提供的 Input 单例(Input.is_action_pressed() 这种)。我们会重点讨论第一种情况。

2.2 Input Event

注意是 Input Event 不是 InputEvent。Input Event 是 InputEvent 类型的对象(我觉得 Godot 起名真的需要改一下了,真的很混乱各种双关)。这个对象包含事件相关的信息。比如:

js 复制代码
extends Node
func _input(event):
    print(event.as_text())

我们用 InputEventMouseButton 为例,它继承了这么多类:

  • InputEvent:基类,不用说了。
  • InputEventWithModifiers: 添加了检查修饰符的功能,如 shift。
  • InputEventMouse: 添加了鼠标事件的属性与功能。
  • InputEventMouseButton:包含鼠标点击的功能与属性。

在使用 Input 的时候,最好文档就放在旁边。检查一下可用属性和方法,总会有惊喜。

如果你尝试使用不存在的属性,比如鼠标事件检查键盘属性。所以最好先测试一下事件的类型:

js 复制代码
func _input(event):
    if event is InputEventMouseButton:
        print("mouse button event at ", event.postion)

2.3 输入映射

Godot 提供了很多内置的 action,可以在项目设置->InputMap 里右上角打开显示内置类型。

2.4 捕捉 Action

定义操作后,可以用 is_action_pressed() 等来处理。

js 复制代码
func _input(event):
    if event.is_action_pressed("my_action"):
        print("my_action occurred!")

2.5 键盘事件

虽然建议使用 action,但偶尔还是会有检查键盘的时候,比如:

js 复制代码
func _input(event):
    if event is InputEventKey and event.pressed:
        if event.keycode == KEY_T:
            print("T was pressed")

2.6 键盘重影

键盘上一次性输入太多键会导致一些键位不会被电脑接收到。这个无法解决毕竟是硬件问题。除了高级玩家自己会去买防重影的游戏专用键盘以外没啥办法。当然,因为 wasd,space,enter,方向键这些用的太多了,大部分键盘会刻意在这些键上做防重影。所以把你的键位设置在这几个上面是有很大意义的。

2.7 修饰符

像是 InputEventKey 这种事件也都继承了 InputEventWithModifiers,所以可以用其属性检查修饰符是否按下。

2.8 鼠标事件

鼠标事件分为 InputEventMouseButton 和 InputEventMouseMotion。名字就能看出来区别。

2.8.1 鼠标按钮

鼠标按钮和按键处理很像,可以处理鼠标左右键中键以及额外鼠标按键(一些高级鼠标)。需要注意滚动也算按钮事件。

2.8.2 鼠标运动

鼠标移动就会触发运动事件。

js 复制代码
extends Node


var dragging = false
var click_radius = 32 # Size of the sprite.


func _input(event):
	if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
		if (event.position - $Sprite2D.position).length() < click_radius:
			# Start dragging if the click is on the sprite.
			if not dragging and event.pressed:
				dragging = true
		# Stop dragging if the button is released.
		if dragging and not event.pressed:
			dragging = false

	if event is InputEventMouseMotion and dragging:
		# While dragging, move the sprite with the mouse.
		$Sprite2D.position = event.position

3. 鼠标和输入坐标

这一节是为了纠正关于输入坐标,获取鼠标位置,屏幕分辨率等常见错误。

3.1 硬件显示坐标

如果 UI 非常复杂(编辑器,工具等),才会用到。除此之外没有意义。

3.2 视口 Viewport 显示坐标

Godot 使用 Viewport 中显示内容,并且可以缩放视口。

鼠标事件中的 position 指的就是视口的坐标。或者使用 Viewport.get_mouse_position() 直接获取鼠标位置。

4. 自定义鼠标指针

当然,切换鼠标指针是一个很常见的事。

切换鼠标指针两种方式,项目设置或者脚本,前者更方便但限制较多。

理论上来说,也可以用一个图片跟随鼠标位置切隐藏鼠标光标来做到,但这样会有延迟。还是用标准方法比较好。

项目设置就不说了,很简单。

4.1 脚本设置

脚本除了可以设置一个总体鼠标指针以外,还能设置指针的不同样子(比如链接上变成手型,等等)。如下:

js 复制代码
extends Node


# Load the custom images for the mouse cursor.
var arrow = load("res://arrow.png")
var beam = load("res://beam.png")


func _ready():
	# Changes only the arrow shape of the cursor.
	# This is similar to changing it in the project settings.
	Input.set_custom_mouse_cursor(arrow)

	# Changes a specific shape of the cursor (here, the I-beam shape).
	Input.set_custom_mouse_cursor(beam, Input.CURSOR_IBEAM)

5. 手柄,等各种游戏外设

因为有着 SDL ,所以 Godot 支持上百种不同的游戏外设。

不过其中方向盘,踏板等特别专业的设备测试比较少,可能会有 bug。

使用 action 来简化这一切。

5.1 振动

Input.start_joy_vibration 等函数可以控制振动效果。

5.2 deadzone

像是手柄这种,因为其物理特性,其实静止状态的位置并不是 0,0 ,而是有一点点偏离,如果再严重一点这个就叫做手柄飘移。项目设置内默认死区是 0.5,低于这个值不会被读取。

5.3 回声

按住控制器上的某个按钮并不会持续发送输入。键盘才会。要想达成这一效果,要通过代码生成输入事件 + pares_input_event() 来达成。

5.4 窗口焦点

控制器不像键盘,控制器没有窗口焦点,而是会发送到所有窗口。要想解决这个问题,godot 上文档有一串代码可以来控制焦点。。。(为啥不直接做进引擎?不懂。)

5.5 预防进入睡眠模式,屏保等

键盘持续输入会让电脑不进入休眠状态,但是手柄不会,玩着玩着进入睡眠模式或者屏保也太伤了。所以 Godot 默认启动了节能预防。项目设置中能看到。

5.6 其他问题

手柄问题确实很多,godot 上关于这个的 issue 有几十条。慢慢踩坑吧。。。

6. 处理退出

在 pc 端点叉叉会关闭程序。默认操作是退出游戏。

如果你想做点啥事(比如弹出提示框,或者保存啥的),可以按照如下代码:

js 复制代码
func _notification(what):
	if what == NOTIFICATION_WM_CLOSE_REQUEST:
		print(1233)

不过这并不会阻止其退出游戏,要想阻止退出游戏,使用 get_tree().set_auto_accept_quit(false)

6.1 发送自己的退出通知

虽然可以用 get_tree().quit() 来退出,但这样不会执行上面 notifiction 里的操作。我们希望发送 NOTIFICATION_WM_CLOSE_REQUEST。所以我们应该这样子写:

js 复制代码
get_tree().root.propogate_notification(NOTIFICATION_WM_CLOSE_REQUEST)

但是注意,4.0 版本开始,这个仅仅只会发送这个信息,但不会关闭程序,你还要再手动调用 get_tree().quit() 来关闭应用

相关推荐
DisonTangor1 天前
Ruffle 继续在开源软件中支持 Adobe Flash Player
游戏·adobe
VMOS云手机1 天前
《仙境传说RO:新启航》游戏攻略,VMOS云手机辅助高效挂机助攻!
游戏·云手机·游戏辅助·黑科技·免费云手机
PC端游爱好者1 天前
战神诸神黄昏9月19日登录PC端! 手机怎么玩战神诸神黄昏
游戏·智能手机·电脑·远程工作·玩游戏
疑惑的杰瑞2 天前
[C语言]连子棋游戏
c语言·开发语言·游戏
文宇炽筱2 天前
《黑神话:悟空》:中国游戏界的新篇章
游戏·游戏程序·玩游戏
星毅要努力2 天前
【C语言编程】【小游戏】【俄罗斯方块】
c语言·开发语言·学习·游戏
宇宙第一小趴菜2 天前
中秋节特别游戏:给玉兔投喂月饼
python·游戏·pygame·中秋节
这是我582 天前
C++掉血迷宫
c++·游戏·visual studio·陷阱·迷宫·生命·
zhooyu2 天前
C++和OpenGL实现3D游戏编程【目录】
开发语言·游戏·游戏程序
niaoma2 天前
剑灵服务端源码(c#版本+数据库+配套客户端+服务端)
游戏·c#·游戏程序·游戏策划