Ableye我们如何用Go和Ebiten实现Ably SDK的可视化

436 阅读14分钟

我最近开始为Ably工作,在2022年初。我接到的第一个任务是建立一个使用Ably SDK的演示项目。在过去的4年里,作为一个后端Go开发者,我对SDK的选择很容易。我肯定想使用ably-go

我发现自己想知道如何以可视化的方式探索、调试和测试一个SDK,就像Postman API客户端与API的互动一样。我知道Ably平台的功能是实时的,所以任何与Ably互动的工具也需要实时工作。根据我以前玩、测试和开发电脑游戏的经验,我也知道电脑游戏对用户输入的反应是实时的。

电脑游戏的核心是一个无限的循环,叫做游戏循环。这个循环负责在屏幕上绘制像素,播放声音和更新逻辑。时间和时机很重要,因为代码在发生时对按键和鼠标点击做出反应。

我的计划是建立一个图形用户界面,可以探索、测试和调试ably-go SDK,我打算通过使用一个游戏引擎来实现这一目标。我的主要限制是,因为我想与围棋SDK互动,我需要用围棋构建我的工具。

Ableye: How we visualized an Ably SDK with Go and Ebiten

为什么与Ebiten合作?

为了建立一个游戏,开发者基本上需要对音频、键盘、鼠标和图形进行低层次的访问。现有的能够实现这些功能的库大多是用C语言家族编写的,而不是Go语言。虽然有一个叫做SDL2的库,确实有Go的绑定,但是当我第一次尝试用它来构建游戏时,我发现学习曲线相当陡峭。主要是因为每当Go建立一个与C库对话的项目时,Go都要求开发者安装一个C编译器。另外,直接使用绑定意味着需要编写大量的低级代码来完成在屏幕上绘制一些文字等工作。

然而,有一个开源的2D游戏引擎叫Ebiten,它是用Go编写的。我知道Ebiten能够为我完成很多繁重的工作。Ebiten将原生的OpenGL函数映射到Go函数中,使得在屏幕上绘制图像变得快速而简单。Ebiten已经存在了好几年了,在GitHub上有超过6千颗星,并且被积极地开发、更新和维护,这真的很不错!我将向大家介绍我是如何使用Ebiten的。

我将解释我是如何开始使用Ebiten的,并包括每个开发阶段的一些示例代码。Ebiten的官方文档以及更多的例子可以在ebiten.org找到。

1.开始使用一个新的Ebiten项目

首先要做的是导入Ebiten并定义游戏对象。一个游戏对象必须满足Ebiten的Game 接口。它通过拥有与Ebiten的Game 接口相同的签名的方法Update,DrawLayout 来实现这一点。通过满足Game 接口,游戏对象被允许传递给Ebiten的RunGame 函数,从而启动游戏。

游戏的核心是一个无限的循环,称为游戏循环。这个循环的一个周期对应于一帧。如果游戏以每秒60帧的速度运行,这意味着每一秒循环将完成60个周期。游戏循环的每个周期,称为一帧,包括调用Update ,更新游戏的状态,然后调用Draw ,将游戏的当前状态绘制到屏幕上。

在一个新的文件夹中,创建一个包含以下代码的main.go 文件。完成后,用命令go mod init 来初始化项目中的Go模块,然后运行命令go mod tidy 。这将导致Go获取所有的项目依赖,并创建go.modgo.sum 文件来管理它们。要运行游戏go build -o myGame && ./myGame ,这将创建一个名为myGame 的可执行文件,然后开始运行该文件。你应该看到你的屏幕上出现一个空的黑色窗口。

你会注意到导入的ebiten包是github.com/hajimehoshi/ebiten/v2 。所有的示例代码都将使用Ebiten的v2 。如果你正在跟随并使用一个自动导入包的IDE,请注意不要自动使用v1 的导入。

示例代码1

2.在屏幕上绘制一个.png图像

Draw 方法接受作为参数的屏幕是图像类型。当我们想在屏幕上绘制一个.png图像时,Ebiten将该.png图像渲染在屏幕图像之上。我们也可以使用DrawImageOptions来转换图像。DrawImageOptions可以改变一个图像的位置、旋转、颜色或透明度。

在你的项目中创建一个名为images 的文件夹,并在那里保存一个.png 的图像。在示例代码中,我使用了这个世界的图像

为了处理.png 图像,我们需要导入标准库包image/png 。这个包的导入只是为了它的副作用(初始化),所以用一个空白的标识符作为明确的包名。例如:import _ image/png 。如果我们不这样做,我们会得到一个错误image: unknown format ,因为我们写的任何代码都不会理解图片的格式.png

在用RunGame 开始我们的游戏之前,我们需要将.png 图像加载到内存中。虽然可以将图像嵌入Go二进制文件中,但我们在本教程中要采取的方法是从文件中加载图像到内存中。Ebiten有一个实用程序包,我们可以用它来做这件事。加载图像应该在游戏的初始化阶段进行,在用RunGame

我们将使用一个类型为*ebiten.Image 的全局变量来保存内存中的图像。在游戏的Draw 方法中,我们创建一个新的DrawImageOptionsDrawImageOptions 可以包括绘制图像的位置、颜色选项、alpha选项和纹理过滤器选项。DrawImage 方法接收世界图像和DrawImageOptions 。现在我们只是把一些空的选项传给DrawImage ,它将在屏幕上以x,y0,0 的坐标绘制图像。在游戏的Draw 方法中调用screen.DrawImage ,建立并运行该项目。现在你应该看到的不是黑屏,而是你的图像。

示例代码2

3.状态、屏幕和转场

任何软件项目的挑战之一是管理复杂性。重要的是,要把代码组织起来,使其易于理解和推理。我对此采取的一种方法是将项目划分为不同的屏幕或状态。

如果你正在构建一个纸牌游戏,你可能有一个标题屏幕和一个游戏屏幕。然后你可以把标题屏幕的代码分到它自己的文件中。游戏屏幕也可以在它自己的文件中包含自己的代码。在纸牌游戏的例子中,你可以为游戏的不同阶段设置不同的状态,例如初始化牌、发牌、打牌、配牌、得分。那么这些阶段的代码就可以分开到自己的文件中。虽然这在一个不包含太多逻辑的新项目中似乎没有必要,但在项目开始时,在复杂性开始增加之前,对代码组织做出决定要容易得多。

我们将把这个项目分成三个屏幕,一个开始的标题屏幕和两个用户可以选择访问的可选屏幕。我们将接受用户的键盘输入,并利用它在不同的屏幕之间进行导航。

我们将首先创建一个名为state.go 的新文件,以包含我们项目可能处于的所有状态。为了本教程的目的,我们将把我们的标题屏幕上显示的世界划分为北半球的屏幕和南半球的屏幕。有时在代码中,当我们将事物相互分离时,我们会关心这些事物的名称。然而在这种情况下,我们并不真正关心屏幕的名称,只关心它们彼此之间的区别。

在Go中一个很好的习惯是使用iota 标识符来简化常量的定义。使用iota 的常量的值使用递增的整数来区分它们之间的区别。为可能的状态使用自定义类型也是一个好主意,这样我们就可以要求代码接受我们的自定义类型,而不是接受一个整数值。这样做的好处是,如果我们的代码中任何地方使用了整数而不是游戏状态,Go编译器会告诉我们。

main.go 文件中,我们将添加一个新的全局变量来保存游戏状态。我们还将添加一个init 函数,在项目初始化时将游戏状态设置为标题屏幕。

然后我们将创建三个新的文件:screen_title.goscreen_northern.goscreen_southern.go 。这些文件将包含每个屏幕的绘制和更新逻辑。每个文件都有自己的初始化函数、绘制函数和更新函数。在main.go ,主游戏循环的draw 方法将根据游戏的状态使用一个开关语句来调用当前屏幕的相应绘制函数。主游戏main.go 中的update 循环将使用同样的模式来调用当前屏幕的相应更新函数。

在北方屏幕上,我们将显示一个北半球的图像

在南半球屏幕上,我们将显示一个南半球的图像。

现在,我们有了这些不同的屏幕,这很好,但用户需要一种方法来在它们之间进行导航。一种方法是接受用户的键盘输入。用户将从标题屏幕开始,然后如果他们按键盘上的N 键或S 键,我们将根据他们按的键把他们带到北半球屏幕或南半球屏幕。当在北方或南方屏幕上时,如果用户按下Escape 键,他们将返回标题屏幕。

Ebiten有一个叫做inpututil 的软件包,可以用来检测按键。当检测到一个按键时,游戏状态就会被更新。每个屏幕的更新函数都会检测是否有按键被按下,并相应地改变游戏状态。

示例代码3

4.在屏幕上绘制文本

我们要开始考虑的下一件事是在屏幕上绘制文本。这样我们既可以与用户交流,也可以显示数据。为了显示文本,我们需要导入三样东西。首先,我们需要golang.org/x/image/font 包。这个包提供了字体的接口。我们还需要一个.ttf 字体文件。在本教程中,我们将使用一个.ttf 字体文件,该文件作为一种资源包含在Ebiten示例项目中,通过导入"github.com/hajimehoshi/ebiten/examples/resources/fonts" 。最后,我们需要一个字体光栅器。栅格化器是一个软件包,它可以将矢量图形转换为由像素组成的栅格图像,这些像素在一起显示时将与矢量图形相似。我们将使用的光栅化器叫做freetype。值得注意的是,freetype只支持真字体,所以要确保在你的项目中使用一个以.ttf 结尾的字体文件。

在添加了上述三个导入后,我们将声明一个类型为font.Face 的全局变量。然后,我们可以使用光栅器来解析.ttf ,并从字体中生成一个font.face 。这是通过指定字体的选项来完成的,比如说尺寸、每英寸点数(DPI)和提示。在初始化过程中,将字体预先加载到我们的全局变量中是很重要的,这样它就可以在游戏开始时立即使用。正是由于这个原因,生成font.Face 的代码需要住在一个init 函数里面。

我们现在已经接近能够在屏幕上画出一些文字了,但是我们还需要一件事,那就是用一种颜色来显示这些文字。颜色可以使用Go标准库包image/color ,通过定义一个color.NRGBA 。颜色中的四个值代表红、绿、蓝和α的数量。NRGBA 中的N ,代表不预乘的意思。下面是一些声明颜色的例子。

    White := &color.NRGBA{0xff, 0xff, 0xff, 0xff}               
    Black := &color.NRGBA{0x00, 0x00, 0x00, 0xff}       
    JazzyPink := &color.NRGBA{0xff, 0x17, 0xd2, 0xff}       

因此,现在我们已经定义了一个font.face 和一个NRGBA颜色,我们能够在屏幕上绘制文本,这是用包"github.com/hajimehoshi/ebiten/v2/text" 。在游戏循环的Draw 方法中,我们设置了DrawImageOptions ,并调用了text.Draw

在示例代码中,文字 "北半球 "已被添加到北方屏幕上,"南半球 "已被添加到南方屏幕上。

示例代码4

现在我们已经导入了图片,创建了屏幕,在屏幕之间添加了过渡,并显示了文本。示例代码在运行时应该看起来像这样。

Ableye: How we visualized an Ably SDK with Go and Ebiten

示例代码运行时的输出

把所有的东西放在一起,建立Ableye

我能够使用上面描述的Ebiten的一些功能来建立我自己的工具,叫做Ableye

Ableye本质上是一个直接位于ably-go SDK之上的可视化和图形化界面。

Ableye支持在一个窗口中创建多达四个Ably客户端。这些客户端可以是Ably实时客户端或Ably REST客户端。在创建一个实时客户端后,可以通过输入一个频道名称,然后用鼠标左键点击Set Channel ,来设置一个频道。一旦一个频道被初始化,可以通过点击Subscribe All 按钮来订阅它。订阅一个频道后,会出现一个窗口,实时显示事件。在订阅时,将显示一个Unsubscribe 按钮,可以用来取消订阅。也可以通过点击AttachDetach 按钮在任何时候附加或脱离该频道。可以用Enter,GetLeave 按钮与存在进行互动。信息名称和信息数据可以使用Publish 按钮输入并发布到频道上。

下面是Ableye的一些操作录像,显示了一个客户如何订阅一个频道,第二个客户可以向该频道发布消息,而第一个客户则实时接收消息。

Ableye: How we visualized an Ably SDK with Go and Ebiten

Ableye在运行中

这是一个很好的学习练习,因为我能够在探索过程中了解到ably-go SDK。我现在也有了一个工具,可以用来帮助调试问题。通过连接Delve调试器,我也能够设置断点,并在调试模式下通过SDK来诊断错误。

使用Ebiten来构建一个界面确实需要一点仔细的计划,但回报也很高。尽管在设计阶段有许多问题需要回答。界面会是什么样子的?用户将如何与我的工具互动?他们将使用鼠标、键盘还是同时使用?通过一点点的实验,我发现Ebiten足够灵活,可以解决各种各样的问题。在我的Ableye工具中,我能够建立接受用户输入的文本框,并使用这些值来调用SDK。

在建立Ableye的过程中我学到的东西

在设计界面时,要遵循的一个好的模式是不要把x,y 坐标明确化,例如不要硬编码其像素值。让x,y 坐标是相对的。如果一个元素需要绘制在600 x 800的屏幕中间,与其在300,400 ,不如将屏幕的宽度和高度定义为常数,然后在(screenWidth/2),(screenHeight/2) 。这意味着,如果屏幕尺寸需要改变,该元素将保持在屏幕的中间。

在开发过程中获得他人的反馈也是非常重要的。特别是如果你希望人们采用你的工具并开始使用它。我很幸运,能够在构建工具时向人们展示我的工具,并在整个构建过程中获得反馈。不要等到最后才和别人分享你的工作。

我希望你喜欢学习如何使用Go和Ebiten构建具有图形界面的实时工具。如果你正在寻找更多的项目想法,可以在这里找到一个关于如何使用ably-go构建自己的聊天应用程序的伟大教程。