外观
Game(游戏)
game 插件(plugins/game)带有游戏在应用之外所需的东西:MyGo 是一个应用 框架,这些是选择性启用的,在它们自己的包里。它全部是 Go。原生 UI 绘制游戏 如同绘制其余部分:game 包在一个元素里运行它的循环,通过一个摄像机观察它的 世界,game/gamepad 包读取它的控制器,核心包绘制它的像素艺术(像素化的 位图和其中的一部分) 并播放它的声音(audio)。
examples/mayoi, 一款以动画《MyGO!!!!!》为题材的小型生存游戏(MyGo 的名字正来自它),由以上 全部构成,可在此处游玩,已编译为 WebAssembly。点击它,然后用 WASD 或方向键 行走:Tomori 因舞台恐惧独自唱歌,捡起石头,每升一级,一个乐队伙伴可能加入她, 音乐也是。
examples/farm是一 个更平静的游戏,一块要耕种和浇水的农场。
游戏循环
一个 game.Game 有两个方法:Update 用玩家的输入把它的世界推进一步, Draw 绘制它。game.View 在一个元素里运行它:
go
import "github.com/egoist/mygo/plugins/game"
type farm struct {
x, y float32 // 玩家,在世界坐标中
px, py float32 // 上一步时所在的位置
}
func (f *farm) Update(in *game.Input) {
f.px, f.py = f.x, f.y
if in.Down(ui.KeyD) {
f.x += 2
}
if in.Pressed(ui.KeySpace) {
f.jump()
}
}
func (f *farm) Draw(p *ui.Painter, r ui.Rect, alpha float64) {
// 在最后两个位置之间插值,按这一帧进入下一步的程度。
x := f.px + (f.x-f.px)*float32(alpha)
y := f.py + (f.y-f.py)*float32(alpha)
p.ImageRegion(sheet, hero, ui.Rect{X: r.X + x, Y: r.Y + y, W: 48, H: 48}, ui.ImageOptions{})
}
mygo.NewWindow(mygo.WindowOptions{
Title: "Farm",
Content: ui.View(func(c *ui.Context) { game.View(c, f).Fill() }),
})步是固定的,每秒 game.Rate(60)步,或游戏的一个 Rate() int 方法所说的, 无论显示屏的刷新率如何,如同 Ebitengine 和 MonoGame 运行游戏:一个游戏在 60 Hz 和 120 Hz 的显示屏上表现一致,它的物理保持稳定。Draw 每帧运行;它的 alpha,从 0 到 1,是这一帧进入下一步的程度,用于在它的最后两个位置之间绘制 移动中的东西。暂停之后(例如窗口不在视野内),步最多以四分之一秒的速度追赶。
输入
game.Input 是一步所看到的玩家输入:
| 方法 | 报告的内容 |
|---|---|
Down(k) | 键 k 被按住 |
Pressed(k)、Released(k) | k 自上次步进以来被按下或释放,即使发生在两步之间 |
Pointer() | 指针在哪里(从视图的角点起算),以及它是否在视图之上 |
PointerDown(b)、PointerPressed(b)、PointerReleased(b) | 指针按钮 b(0 为主按钮) |
Wheel() | 滚轮自上次步进以来滚动的距离 |
Size() | 视图的大小 |
Focused() | 视图拥有键盘:游戏在失去它时暂停 |
键按位置:KeyW 是任何布局上 W 所在的位置的键,正如 WASD 所希望的。视图在 它出现和被点击时获得键盘焦点,把带有 ⌘、Ctrl 或 Alt 的键留给菜单的快捷键, 并在窗口失去键盘时放开所有按住的键(它们的释放永远不会到来)。
测试
游戏的 Update 是普通的 Go:无需窗口即可测试它的世界。视图在一个 ui.Tester 里运行,像任何元素一样:KeyDown 和 KeyUp 在帧之间按住一个 键,一步在帧比上一步晚一步的时间到来时运行一次。
摄像机
一个 game.Camera 观察世界的点 X, Y,它显示在视图的中央,放大 Zoom 倍, 旋转 Rotation 度,如同 raylib 的 Camera2D。Draw 通过它绘制一个在自己的 坐标中的世界,裁剪到视图:
go
cam := game.Camera{X: player.x, Y: player.y, Zoom: 3}
cam.Draw(p, r, func() {
for _, t := range tiles {
p.ImageRegion(sheet, t.src, t.box, ui.ImageOptions{}) // culled where the camera does not look
}
})
wx, wy := cam.ToWorld(r, px, py) // the pointer, in the world不旋转时,世界按屏幕的整像素移动,以便按整单位绘制的瓦片保持它们的边缘在像素 上,而位图的部分(放置在变换之下的位置)平滑滑动。Visible 返回世界中在视图 内的部分,供自己迭代瓦片的游戏使用,Transform 返回摄像机的变换,供 Painter.Transform 使用。
整数倍的缩放给一个 Pixelated 位图的每个像素同样的整数个屏幕像素;其他缩放 保持它们是方的,边缘跨一个屏幕像素混合。要在任何窗口大小下得到整数倍,绘制到 一个画布(见下文)。
像素完美缩放
对于在任何窗口大小下都像素完美的像素艺术,如同 Godot 的视口拉伸和 Stardew Valley 的绘制方式,游戏把它的世界绘制到一个逻辑大小的 ui.Canvas,并以适合的最大整数倍显示它,加黑边 (game.IntegerFit):
go
world := ui.NewCanvas(480, 270)
world.Bitmap().Pixelated()
func (f *farm) Draw(p *ui.Painter, r ui.Rect, alpha float64) {
world.Draw(p, func(cp *ui.Painter) {
cam := game.Camera{X: f.x, Y: f.y}
cam.Draw(cp, ui.Rect{W: 480, H: 270}, f.drawWorld)
})
dst, _ := game.IntegerFit(p, 480, 270, r)
p.Image(world.Bitmap(), dst, ui.FillBox)
}游戏手柄
plugins/game/gamepad 包读取游戏手柄控制器,在原生 UI 或 Web 页面的应用中, 在 MyGo 运行的每个平台上,包括浏览器。每个手柄都按一种布局读取,W3C Gamepad API 和 SDL 游戏控制器数据库的标准布局,无论它是什么手柄:按钮按位置,两个摇杆, 模拟扳机和十字方向键。
go
import "github.com/egoist/mygo/plugins/game/gamepad"
var last gamepad.State
func update() {
pads := gamepad.Pads()
if len(pads) == 0 {
return
}
s := pads[0].State()
x, y := s.LeftStick() // -1 to 1, right and down, with a dead zone
player.move(x, y)
if s.JustPressed(last, gamepad.South) {
player.jump()
}
last = s
}手柄
Pads 返回已连接的手柄,按它们连接的顺序,并最多每秒寻找新手柄;游戏每帧调用 它一次。一个 Pad 从它连接到断开期间存在(Connected 变为 false,它离开 Pads);重新插入时,它是一个新的。Name 是制造商对它的称呼。
State 读取手柄此刻的控件,作为一个值:保留上一个值以区分刚按下 (JustPressed)或刚释放(JustReleased)的按钮和一直按住的按钮。
布局
| 按钮 | Xbox | PlayStation | Nintendo |
|---|---|---|---|
South | A | Cross | B |
East | B | Circle | A |
West | X | Square | Y |
North | Y | Triangle | X |
LeftShoulder、RightShoulder | LB, RB | L1, R1 | L, R |
LeftTrigger、RightTrigger | LT, RT | L2, R2 | ZL, ZR |
Back、Start | View, Menu | Create, Options | −, + |
LeftStickButton、RightStickButton | LS, RS | L3, R3 | |
Up、Down、Left、Right | 十字方向键 | 十字方向键 | 十字方向键 |
Guide | Xbox | PS | Home |
按钮读取从 0 到 1(Value),扳机按拉下的程度;Pressed 是四分之一程。轴 (Axis)读取从 -1 到 1,向右和向下为正,按手柄上报的;LeftStick 和 RightStick 应用一个径向死区(Deadzone,0.15),以便静止的摇杆什么都不读, 并在每个方向上倾斜到同样远。
平台
| 平台 | 来源 | 支持的手柄 |
|---|---|---|
| macOS | GameController.framework | Xbox、PlayStation、Switch Pro 和 MFi 控制器 |
| Windows | XInput | Xbox 控制器和充当 Xbox 控制器的控制器(Steam Input 使其他手柄如此);Guide 通过 XInputGetStateEx |
| Linux | evdev,/dev/input/event* | 被内核驱动报告为游戏手柄、且座位上用户可读的那些;Xbox 控制器的 X 和 Y(报告为 BTN_X 和 BTN_Y)按原位读取 |
| 浏览器 | Gamepad API | 带标准映射的那些;其他按浏览器给出的顺序 |
在 macOS 上,手柄的输入只到达前台应用:在后台它读不到任何按下,正如游戏所期望 的。浏览器只在页面看到手柄的某个按钮被按下后才报告一个手柄。Windows 在没有 Steam Input 或把它变成 XInput 手柄的驱动程序时,看不到 PlayStation 或 Nintendo 控制器。
examples/farm用手柄 和键盘同样游玩。