前言

It’s so cool that a loose community of “The People” have created things like Godot, OBS, and Blender. – seanw444(Hacker News)

之前做 游戏中的 AI Agent 的时候, 最初的选型过程, 调研过 Godot, 对它充满好感.

最近重新对 Godot 感兴趣, 是因为 John 在邮件里提到说, MicroBlocks 下一个版本的 IDE 将基于 Godot 构建.

为何要使用 Godot 构建 MicroBlocks IDE? 又要怎么做呢?

目前 John 还没有给出进一步的消息, 以下记录我的一些想法.

为何要使用 Godot 构建 MicroBlocks IDE?

目前, MicroBlocks 用户呼声最好的需求是支持移动端编程

MicroBlocks 当前的 IDE 基于 GP Blocks, 若要 MicroBlocks IDE 支持移动端, 需要在 GP Blocks 上进行许多工作, 可是 GP Blocks 已经不再维护.

因此, 更好的方向是使用一个成熟的 GUI 构建工具来重新制作 MicroBlocks IDE.

Godot 是一个非常强大的开源解决方案.

如何使用 Godot 构建 MicroBlocks IDE?

值得提醒的是, 并不是要使用 Godot 重新构建 MicroBlocks 而是使用 Godot 构建 MicroBlocks IDE

MicroBlocks 主要包含两大部分:

  • MicroBlocks VM
  • MicroBlocks IDE

MicroBlocks IDE 是 MicroBlocks VM 的图形化客户端, 它们之间通过消息进行通信(基于 MicroBlocks 协议). 要理解它们之间的关系可以参考 @邵悦 上周末的分享: MicroBlocks 能力图谱(视频)

使用 Godot 构建 MicroBlocks IDE 的含义是我们要制作一个图形化客户端, 它是一个 GUI 项目.

所以我们要做的事情是使用 Godot 制作一个 GUI 项目, 它能够支持移动端

Godot 非常擅长制作 GUI 项目:

兴趣点: GUI

Godot 主要是一个游戏引擎, 能做的事情非常多, 生态巨大, 文档极多. 这带来的一个问题是, 很难"学会"它.

好在, 我们并不需要"学会"它. 记住我们要做的事情是:

使用 Godot 制作一个 GUI 项目, 它能够支持移动端

我们并不需要学习如何使用 Godot 制作 2D 或 3D 游戏(这需要很多时间), 只需学习如何使用它制作 GUI 即可!

弄清楚目标, 需要学习的内容就变得很少且聚焦了, 我们只需要学习如何使用 Control 节点

以下是我喜欢的一些教学资源, 对于 Godot 这样**丰富(rich)**的计算环境, 强烈推荐通过观看视频来学习(尤其是入门阶段), 操作中的许多知识是隐性的, 很难通过文字描述学会:

Godot 入门

关键概念

Godot 非常适合快速制作原型。一旦你理解了核心概念,编辑器就非常快速且直观 – Hacker New 用户

Godot 关键概念概述

任何游戏引擎都是围绕着构建程序所用的抽象的。在 Godot 中,游戏就是一棵由节点构成的,树又可以结合起来构成场景。然后你还可以将这些节点连起来,让它们通过信号进行通信。

GDScript

GDScript 与 Python 高度相似.

有 Python 经验的用户快速过一下 GDScript 参考 即可.

如果你想从头开始学习, Learn to Code From Zero with Godot 或许是不错的选择

值得注的是, 使用 GDScript 编程通常具有事件驱动风格(UI/游戏领域通常采用的编程风格), 你经常使用 callback, 有 JavaScript 经验的用户可能对此熟悉.

REPL/Playground

学习一门新语言时, 我喜欢使用 REPL 进行支持交互式的实验和探索, 对于新手非常有帮助

Godot 没有提供 GDScript REPL. 有社区用户提供了类似的东西 GDScript Playground

开发

导出 Web 项目

MicroBlocks 用户最喜欢使用 Web 版本的 MicroBlocks IDE, 因其支持无线编程(BLE).

如果我们使用 Godot 制作 MicroBlocks IDE, 会将其导出为 Web 项目 并部署到线上.

在 Web 中环境测试

如果你使用的本机版本的 Godot, 点击远程调试按钮, 即可在浏览器上运行:

此外, 你可以将项目导出, 然后导入到Godot Web 开发环境里, 这样就可以实时调试它. Godot 的 Web 开发环境甚至支持数据持久化, 关闭浏览器再次打开, 之前所做的工作都还在.

与 JavaScript 互操作

我们可以在 Godot 中与 JavaScript 互操作

从 Godot 调用 JavaScript

以下是一个简单例子:

1
2
3
4
5
6
7
func _on_button_pressed():
    if OS.has_feature('web'):
        JavaScriptBridge.eval('''
            console.log('The JavaScriptBridge singleton is available')
        ''')
    else:
        print("The JavaScriptBridge singleton is NOT available")

扫描蓝牙设备:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func _on_button_pressed():
    # var console = JavaScriptBridge.get_interface("console")
    # var window = JavaScriptBridge.get_interface("window")
    if OS.has_feature('web'):
        var code = '''
        console.log("test");
        navigator.bluetooth.requestDevice({
            acceptAllDevices: true,
            optionalServices: []
        });
        '''
        JavaScriptBridge.eval(code)
    else:
        print("The JavaScriptBridge singleton is NOT available")

使用 Godot 制作 MicroBlocks 客户端

相关代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
extends Control


# Called when the node enters the scene tree for the first time.
func _ready():
    print("ready")
    if OS.has_feature('web'):
        var code = '''
class MicroBlocksClient {
    constructor(height, width) {
        this.MICROBLOCKS_SERVICE_UUID = "bb37a001-b922-4018-8e74-e14824b3a638"
        this.MICROBLOCKS_RX_CHAR_UUID = "bb37a002-b922-4018-8e74-e14824b3a638" // board receive characteristic
        this.MICROBLOCKS_TX_CHAR_UUID = "bb37a003-b922-4018-8e74-e14824b3a638" // board transmit characteristic
        this.bleDevice = null;
    }

    connect() {
        navigator.bluetooth.requestDevice({
            filters: [
                { services: [this.MICROBLOCKS_SERVICE_UUID] }
            ]
        }).then((device) => { this.bleDevice = device; return device.gatt.connect(); })
    }

    send(aString) {
        // console.log(aString);
        const data = new TextEncoder().encode(aString);
        let length = data.length + 1;
        let bytes = new Uint8Array([...(new Uint8Array([251, 27, 0, length % 256, parseInt((length / 256))])), ...data, ...(new Uint8Array([254]))])
        if (this.bleDevice) {
            this.bleDevice.gatt
                .getPrimaryService(this.MICROBLOCKS_SERVICE_UUID)
                .then((service) => service.getCharacteristic(this.MICROBLOCKS_RX_CHAR_UUID))
                .then((characteristic) => characteristic.writeValue(bytes))
                .catch((error) => {
                    console.log("error:" + error);
                });
        }
    }
}

window.MicroBlocksClient = MicroBlocksClient;
        '''

        JavaScriptBridge.eval(code)



# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
    pass


func _on_send_pressed():
    if OS.has_feature('web'):
        var aString = $LineEdit.text;
        var code = 'window.microblocks_client.send("%s")' % aString
        print(code)
        JavaScriptBridge.eval(code)

func _on_connect_pressed():
    if OS.has_feature('web'):
        var code = '''
window.microblocks_client = new window.MicroBlocksClient();
window.microblocks_client.connect();
        '''
        JavaScriptBridge.eval(code)
    else:
        print("The JavaScriptBridge is NOT available")

从这个例子很开始, 很容易使用 Godot 为 MicroBlocks 设备设计手机控制界面.

从 JavaScript 调用 Godot

使用 Callbacks, 可以从 JavaScript 调用 Godot.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
extends Node

# Here we create a reference to the `_my_callback` function (below).
# This reference will be kept until the node is freed.
var _callback_ref = JavaScriptBridge.create_callback(_my_callback)

func _ready():
    # Get the JavaScript `window` object.
    var window = JavaScriptBridge.get_interface("window")
    # Set the `window.onbeforeunload` DOM event listener.
    window.sendToGodot = _callback_ref

func _my_callback(arg):
    print(arg)

以上代码将为导出的 web 项目注册 window.sendToGodot 函数, 你可以在浏览器环境调用它.

Dynatalk

即然我们可以在 JavaScript 和 Godot 环境中进行双向通信, 我们便可以构建出 Dynatalk, 进而实现 Snap! 中的 Spline 库 的所有功能: 使用 Snap! 驱动任何 Godot 制作的游戏!

由于我们之前已经把 Python 解释器带入了 Snap! , 所以我们也可以让 Python 驱动任何 Godot 制作的游戏!

这带来许多有趣的可能性, 诸如我们通过 Snap! 把 GPT-4o 或者 mediapipe 的 AI 能力带到游戏里, 或者通过 Python 把 OpenCV 带入游戏里

传递 json

如果你想在 GDScript 和 JavaScript 中传递复杂的数据结构, 可以考虑传递 json 字符串(list 也有有效的 json 数据)

GDScript 有JSON 模块.

FAQ

Web 导出能否运行在移动设备上?

Web Export in 4.3 里提到

Apple 设备(macOS 和 iOS)在播放 Godot Web 导出时会出现问题。好吧,当你以单线程方式导出游戏时,这些问题幸运地消失了。

使用 4.3(目前是 beta 版) 导出的 web 项目能够运行在 iOS 设备上. 而 4.2 不行.

为了在设计上测试, 我在本地运行了服务器:

1
npx local-web-server --https --cors.embedder-policy "require-corp" --cors.opener-policy "same-origin" --directory "."

并使用 ngrok 将其暴露到公网:

1
ngrok http https://127.0.0.1:8000

在 Bluefy 浏览器中打开:

扫描 BLE 设备:

横屏显示:

支持虚拟键盘输入, 目前还是实验性的功能, 光标的位置有些问题, 但基本可用:

PWA

4.3 的 PWA 功能也能够正常工作:

如何将外部 JavaScript 库引入 Godot web 项目

将外部 JavaScript 库(如test.js)放入导出的 web 项目目录

在 GDScript 中引入 JavaScript 库

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func _ready():
    print("ready")
    if OS.has_feature('web'):
        var code = '''
        let url = "test.js";
        let scriptElement = document.createElement("script");
        document.head.appendChild(scriptElement);
        scriptElement.src = url;
        // scriptElement.crossorigin="anonymous";
        '''
        JavaScriptBridge.eval(code)

测试:

在 web 项目目录启动服务器:

1
npx local-web-server --https --cors.embedder-policy "require-corp" --cors.opener-policy "same-origin" --directory "."

参考