Skip to content

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)的按钮和一直按住的按钮。

布局 ​

按钮XboxPlayStationNintendo
SouthACrossB
EastBCircleA
WestXSquareY
NorthYTriangleX
LeftShoulder、RightShoulderLB, RBL1, R1L, R
LeftTrigger、RightTriggerLT, RTL2, R2ZL, ZR
Back、StartView, MenuCreate, Options−, +
LeftStickButton、RightStickButtonLS, RSL3, R3
Up、Down、Left、Right十字方向键十字方向键十字方向键
GuideXboxPSHome

按钮读取从 0 到 1(Value),扳机按拉下的程度;Pressed 是四分之一程。轴 (Axis)读取从 -1 到 1,向右和向下为正,按手柄上报的;LeftStick 和 RightStick 应用一个径向死区(Deadzone,0.15),以便静止的摇杆什么都不读, 并在每个方向上倾斜到同样远。

平台 ​

平台来源支持的手柄
macOSGameController.frameworkXbox、PlayStation、Switch Pro 和 MFi 控制器
WindowsXInputXbox 控制器和充当 Xbox 控制器的控制器(Steam Input 使其他手柄如此);Guide 通过 XInputGetStateEx
Linuxevdev,/dev/input/event*被内核驱动报告为游戏手柄、且座位上用户可读的那些;Xbox 控制器的 X 和 Y(报告为 BTN_X 和 BTN_Y)按原位读取
浏览器Gamepad API带标准映射的那些;其他按浏览器给出的顺序

在 macOS 上,手柄的输入只到达前台应用:在后台它读不到任何按下,正如游戏所期望 的。浏览器只在页面看到手柄的某个按钮被按下后才报告一个手柄。Windows 在没有 Steam Input 或把它变成 XInput 手柄的驱动程序时,看不到 PlayStation 或 Nintendo 控制器。

examples/farm用手柄 和键盘同样游玩。