2021SC@SDUSC
一.概述
本文将以ebiten源码中自带的游戏2048为例子(位于examples/2048文件夹内),逐步介绍在游戏开发过程中所使用的相关函数工具。 首先考虑游戏实现过程中所需要的功能,例如: 界面窗口大小,界面窗口位置,窗口标题,游戏的初始化,游戏运行过程中的更新界面等。
下面将依次介绍实现这些功能的具体代码。
二.代码分析
func main() {
game, err := twenty48.NewGame()
if err != nil {
log.Fatal(err)
}
ebiten.SetWindowSize(twenty48.ScreenWidth, twenty48.ScreenHeight)
ebiten.SetWindowTitle("2048 (Ebiten Demo)")
if err := ebiten.RunGame(game); err != nil {
log.Fatal(err)
}
}
首先是游戏2048的main.go文件,调用了ebiten包的SetWindowSize()方法,SetWindowTiTle()方法和RunGame()方法。
1.SetWindowSize()方法
此方法位于ebiten源码的根目录下的window.go文件内。
func SetWindowSize(width, height int) {
if width <= 0 || height <= 0 {
panic("ebiten: width and height must be positive")
}
if w := uiDriver().Window(); w != nil {
w.SetSize(width, height)
}
}
当传入的宽高参数为负数时,抛出异常:宽高必须为正。 接下来调用uiDriver().Window()来获取当前的窗口对象。
其中uiDriver()方法位于根目录下的 uidriver_glfw.go 文件内:
func uiDriver() driver.UI {
return glfw.Get()
}
作用是获取用户接口类型的结构体 UserInterface ,其中包含如下属性:
type UserInterface struct {
context driver.UIContext
title string
window *glfw.Window
windowWidth int
windowHeight int
minWindowWidthInDP int
minWindowHeightInDP int
maxWindowWidthInDP int
maxWindowHeightInDP int
running uint32
toChangeSize bool
origPosX int
origPosY int
runnableOnUnfocused bool
fpsMode driver.FPSMode
iconImages []image.Image
cursorShape driver.CursorShape
windowClosingHandled bool
windowBeingClosed bool
setSizeCallbackEnabled bool
err error
lastDeviceScaleFactor float64
initMonitor *glfw.Monitor
initFullscreenWidthInDP int
initFullscreenHeightInDP int
initTitle string
initFPSMode driver.FPSMode
initFullscreen bool
initCursorMode driver.CursorMode
initWindowDecorated bool
initWindowResizable bool
initWindowPositionXInDP int
initWindowPositionYInDP int
initWindowWidthInDP int
initWindowHeightInDP int
initWindowFloating bool
initWindowMaximized bool
initScreenTransparent bool
initFocused bool
fpsModeInited bool
input Input
iwindow window
sizeCallback glfw.SizeCallback
closeCallback glfw.CloseCallback
framebufferSizeCallback glfw.FramebufferSizeCallback
framebufferSizeCallbackCh chan struct{}
t thread.Thread
m sync.RWMutex
}
上述属性通过如下方式进行初始化:
var (
theUI = &UserInterface{
runnableOnUnfocused: true,
minWindowWidthInDP: glfw.DontCare,
minWindowHeightInDP: glfw.DontCare,
maxWindowWidthInDP: glfw.DontCare,
maxWindowHeightInDP: glfw.DontCare,
origPosX: invalidPos,
origPosY: invalidPos,
initFPSMode: driver.FPSModeVsyncOn,
initCursorMode: driver.CursorModeVisible,
initWindowDecorated: true,
initWindowPositionXInDP: invalidPos,
initWindowPositionYInDP: invalidPos,
initWindowWidthInDP: 640,
initWindowHeightInDP: 480,
initFocused: true,
fpsMode: driver.FPSModeVsyncOn,
}
)
而之前uiDriver()方法中的glfw.Get()方法就是获取上述的 theUI 对象,方法定义在 internal/glfw/ui.go 文件内:
func Get() *UserInterface {
return theUI
}
且此文件内同样定义了uiDriver().Window()的Window()方法:
func (u *UserInterface) Window() driver.Window {
return &u.iwindow
}
返回上述theUI对象中的iwindow属性。
至此,已经成功获取到了当前游戏的窗口对象,下面是设置窗口大小 SetSize() 方法,定义在 internal/glfw/glfw_windows.go 文件内:
func (w *Window) SetSize(width, height int) {
glfwDLL.call("glfwSetWindowSize", w.w, uintptr(width), uintptr(height))
panicError()
}
其中call函数是定义在 internal/glfw/load_windows.go 文件内的:
func (d *dll) call(name string, args ...uintptr) uintptr {
if d.procs == nil {
d.procs = map[string]*windows.LazyProc{}
}
if _, ok := d.procs[name]; !ok {
d.procs[name] = d.d.NewProc(name)
}
r, _, _ := d.procs[name].Call(args...)
return r
}
相当于传入的第一个参数是指令的名称,根据这个名称即 “glfwSetWindowSize” 再执行下一级Call函数:
func (p *LazyProc) Call(a ...uintptr) (r1, r2 uintptr, lastErr error) {
p.mustFind()
return p.proc.Call(a...)
}
该方法是go语言自带的方法,在此就不过多赘述了,此方法的参数列表对应着之前的window对象以及需要改变的width和height,此时的Call方法定义如下:
func (p *Proc) Call(a ...uintptr) (r1, r2 uintptr, lastErr error) {
switch len(a) {
case 0:
return syscall.Syscall(p.Addr(), uintptr(len(a)), 0, 0, 0)
case 1:
return syscall.Syscall(p.Addr(), uintptr(len(a)), a[0], 0, 0)
case 2:
return syscall.Syscall(p.Addr(), uintptr(len(a)), a[0], a[1], 0)
case 3:
return syscall.Syscall(p.Addr(), uintptr(len(a)), a[0], a[1], a[2])
case 4:
return syscall.Syscall6(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], 0, 0)
case 5:
return syscall.Syscall6(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], 0)
case 6:
return syscall.Syscall6(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5])
case 7:
return syscall.Syscall9(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5], a[6], 0, 0)
case 8:
return syscall.Syscall9(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], 0)
case 9:
return syscall.Syscall9(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8])
case 10:
return syscall.Syscall12(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9], 0, 0)
case 11:
return syscall.Syscall12(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9], a[10], 0)
case 12:
return syscall.Syscall12(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9], a[10], a[11])
case 13:
return syscall.Syscall15(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9], a[10], a[11], a[12], 0, 0)
case 14:
return syscall.Syscall15(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9], a[10], a[11], a[12], a[13], 0)
case 15:
return syscall.Syscall15(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9], a[10], a[11], a[12], a[13], a[14])
default:
panic("Call " + p.Name + " with too many arguments " + itoa(len(a)) + ".")
}
}
可见此时已经是在执行系统调用了。
总结,ebiten的修改窗口大小的方法步骤大致如下:输入目的窗口宽高,获取当前窗口对象,利用glfw的方法多次传参,通过系统调用最终修改游戏窗口的大小。
2.SetWindowTiTle()方法
同理,与设置窗口大小方法相似,不同之处在于设置标题方法需要将字符串类型的title参数转化成byte类型的数组:
func (w *Window) SetTitle(title string) {
s := []byte(title)
s = append(s, 0)
defer runtime.KeepAlive(s)
glfwDLL.call("glfwSetWindowTitle", w.w, uintptr(unsafe.Pointer(&s[0])))
panicError()
}
然后依旧是传递参数,通过系统调用设置游戏窗口的标题。
3.RunGame()方法
RunGame()方法的作用是启动主循环并运行游戏。是每个游戏最重要的功能,需要一个Game类型的参数,位于根目录下的 run.go 文件中,其中Game类型的定义如下:
type Game interface {
Update() error
Draw(screen *Image)
Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int)
}
Game定义了游戏的必备功能。 其中Update()函数每帧执行一次用来更新游戏的逻辑,Draw()函数同样是每帧执行一次用来更新游戏的画面,Layout()方法是游戏界面的布局方式,几乎每一帧都会调用。
RunGame()定义如下:
func RunGame(game Game) error {
defer atomic.StoreInt32(&isRunGameEnded_, 1)
initializeWindowPositionIfNeeded(WindowSize())
theUIContext.set(&imageDumperGame{
game: game,
})
if err := uiDriver().Run(theUIContext); err != nil {
if err == driver.RegularTermination {
return nil
}
return err
}
return nil
}
关键方法是 uiDriver().Run(theUIContext) 即运行方法,位于 internal/uidriver/glfw/run_notsinglethread.go 文件内,定义如下:
func (u *UserInterface) Run(uicontext driver.UIContext) error {
u.context = uicontext
u.t = thread.NewOSThread()
graphicscommand.SetMainThread(u.t)
ch := make(chan error, 1)
go func() {
defer func() {
_ = u.t.Call(func() error {
return thread.BreakLoop
})
}()
defer close(ch)
if err := u.t.Call(func() error {
return u.init()
}); err != nil {
ch <- err
return
}
if err := u.loop(); err != nil {
ch <- err
return
}
}()
u.setRunning(true)
u.t.Loop()
u.setRunning(false)
return <-ch
}
其中关键是最后三行函数调用,首先是setRunning(true)设置游戏的状态是运行中,然后启动Loop()线程循环,可以开始重复调用Update,Draw,Layout函数了。
其中函数定义如下: 位于internal/uidriver/glfw/ui.go文件内
func (u *UserInterface) setRunning(running bool) {
if running {
atomic.StoreUint32(&u.running, 1)
} else {
atomic.StoreUint32(&u.running, 0)
}
}
位于internal/thread/thread.go文件内
func (t *OSThread) Loop() {
for f := range t.funcs {
err := f()
if err == BreakLoop {
t.results <- nil
return
}
t.results <- err
}
}
总结,RunGame()方法是通过开启一个主循环线程反复调用game对象的Update方法,Draw方法和Layout方法,来保证游戏的正常运行的。
|