Ebiten是一个使用Go语言编程库,用于2D游戏开发,可以跨平台。本届开始讲解官方实例,实例熟悉后会给大家讲解实战游戏课。 贪吃蛇实例
源码如下:
package main
import (
"fmt"
"image/color"
"log"
"math/rand/v2"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
"github.com/hajimehoshi/ebiten/v2/inpututil"
"github.com/hajimehoshi/ebiten/v2/vector"
)
const (
screenWidth = 640
screenHeight = 480
gridSize = 10
xGridCountInScreen = screenWidth / gridSize
yGridCountInScreen = screenHeight / gridSize
)
const (
dirNone = iota
dirLeft
dirRight
dirDown
dirUp
)
type Position struct {
X int
Y int
}
type Game struct {
moveDirection int
snakeBody []Position
apple Position
timer int
moveTime int
score int
bestScore int
level int
}
func (g *Game) collidesWithApple() bool {
return g.snakeBody[0].X == g.apple.X &&
g.snakeBody[0].Y == g.apple.Y
}
func (g *Game) collidesWithSelf() bool {
for _, v := range g.snakeBody[1:] {
if g.snakeBody[0].X == v.X &&
g.snakeBody[0].Y == v.Y {
return true
}
}
return false
}
func (g *Game) collidesWithWall() bool {
return g.snakeBody[0].X < 0 ||
g.snakeBody[0].Y < 0 ||
g.snakeBody[0].X >= xGridCountInScreen ||
g.snakeBody[0].Y >= yGridCountInScreen
}
func (g *Game) needsToMoveSnake() bool {
return g.timer%g.moveTime == 0
}
func (g *Game) reset() {
g.apple.X = 3 * gridSize
g.apple.Y = 3 * gridSize
g.moveTime = 4
g.snakeBody = g.snakeBody[:1]
g.snakeBody[0].X = xGridCountInScreen / 2
g.snakeBody[0].Y = yGridCountInScreen / 2
g.score = 0
g.level = 1
g.moveDirection = dirNone
}
func (g *Game) Update() error {
// Decide the snake's direction along with the user input.
// A U-turn is forbidden here (e.g. if the snake is moving in the left direction, the snake cannot go to the right direction immediately).
if inpututil.IsKeyJustPressed(ebiten.KeyArrowLeft) || inpututil.IsKeyJustPressed(ebiten.KeyA) {
if g.moveDirection != dirRight {
g.moveDirection = dirLeft
}
} else if inpututil.IsKeyJustPressed(ebiten.KeyArrowRight) || inpututil.IsKeyJustPressed(ebiten.KeyD) {
if g.moveDirection != dirLeft {
g.moveDirection = dirRight
}
} else if inpututil.IsKeyJustPressed(ebiten.KeyArrowDown) || inpututil.IsKeyJustPressed(ebiten.KeyS) {
if g.moveDirection != dirUp {
g.moveDirection = dirDown
}
} else if inpututil.IsKeyJustPressed(ebiten.KeyArrowUp) || inpututil.IsKeyJustPressed(ebiten.KeyW) {
if g.moveDirection != dirDown {
g.moveDirection = dirUp
}
} else if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
g.reset()
}
if g.needsToMoveSnake() {
if g.collidesWithWall() || g.collidesWithSelf() {
g.reset()
}
if g.collidesWithApple() {
g.apple.X = rand.IntN(xGridCountInScreen - 1)
g.apple.Y = rand.IntN(yGridCountInScreen - 1)
g.snakeBody = append(g.snakeBody, Position{
X: g.snakeBody[len(g.snakeBody)-1].X,
Y: g.snakeBody[len(g.snakeBody)-1].Y,
})
if len(g.snakeBody) > 10 && len(g.snakeBody) < 20 {
g.level = 2
g.moveTime = 3
} else if len(g.snakeBody) > 20 {
g.level = 3
g.moveTime = 2
} else {
g.level = 1
}
g.score++
if g.bestScore < g.score {
g.bestScore = g.score
}
}
for i := int64(len(g.snakeBody)) - 1; i > 0; i-- {
g.snakeBody[i].X = g.snakeBody[i-1].X
g.snakeBody[i].Y = g.snakeBody[i-1].Y
}
switch g.moveDirection {
case dirLeft:
g.snakeBody[0].X--
case dirRight:
g.snakeBody[0].X++
case dirDown:
g.snakeBody[0].Y++
case dirUp:
g.snakeBody[0].Y--
}
}
g.timer++
return nil
}
func (g *Game) Draw(screen *ebiten.Image) {
for _, v := range g.snakeBody {
vector.DrawFilledRect(screen, float32(v.X*gridSize), float32(v.Y*gridSize), gridSize, gridSize, color.RGBA{0x80, 0xa0, 0xc0, 0xff}, false)
}
vector.DrawFilledRect(screen, float32(g.apple.X*gridSize), float32(g.apple.Y*gridSize), gridSize, gridSize, color.RGBA{0xFF, 0x00, 0x00, 0xff}, false)
if g.moveDirection == dirNone {
ebitenutil.DebugPrint(screen, fmt.Sprintf("Press up/down/left/right to start"))
} else {
ebitenutil.DebugPrint(screen, fmt.Sprintf("FPS: %0.2f Level: %d Score: %d Best Score: %d", ebiten.ActualFPS(), g.level, g.score, g.bestScore))
}
}
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
return screenWidth, screenHeight
}
func newGame() *Game {
g := &Game{
apple: Position{X: 3 * gridSize, Y: 3 * gridSize},
moveTime: 4,
snakeBody: make([]Position, 1),
}
g.snakeBody[0].X = xGridCountInScreen / 2
g.snakeBody[0].Y = yGridCountInScreen / 2
return g
}
func main() {
ebiten.SetWindowSize(screenWidth, screenHeight)
ebiten.SetWindowTitle("Snake (Ebitengine Demo)")
if err := ebiten.RunGame(newGame()); err != nil {
log.Fatal(err)
}
}
在Ebiten框架中有一个3个函数: Layout函数:
func (g *Game) Layout(outsideWidth, outsideHeight int)
Draw函数:
func (g *Game) Draw(screen *ebiten.Image)
Update函数:
func (g *Game) Update() error
以上3个函数是通过框架函数调用:
ebiten.RunGame(newGame())
而ebiten.RunGame在框架底层:
// RunGame starts the main loop and runs the game.
// game's Update function is called every tick to update the game logic.
// game's Draw function is called every frame to draw the screen.
// game's Layout function is called when necessary, and you can specify the logical screen size by the function.
//
// If game implements FinalScreenDrawer, its DrawFinalScreen is called after Draw.
// The argument screen represents the final screen. The argument offscreen is an offscreen modified at Draw.
// If game does not implement FinalScreenDrawer, the default rendering for the final screen is used.
//
// game's functions are called on the same goroutine.
//
// On browsers, it is strongly recommended to use iframe if you embed an Ebitengine application in your website.
//
// RunGame must be called on the main thread.
// Note that Ebitengine bounds the main goroutine to the main OS thread by runtime.LockOSThread.
//
// Ebitengine tries to call game's Update function 60 times a second by default. In other words,
// TPS (ticks per second) is 60 by default.
// This is not related to framerate (display's refresh rate).
//
// RunGame returns error when 1) an error happens in the underlying graphics driver, 2) an audio error happens
// or 3) Update returns an error. In the case of 3), RunGame returns the same error so far, but it is recommended to
// use errors.Is when you check the returned error is the error you want, rather than comparing the values
// with == or != directly.
//
// If you want to terminate a game on desktops, it is recommended to return Termination at Update, which will halt
// execution without returning an error value from RunGame.
//
// The size unit is device-independent pixel.
//
// Don't call RunGame or RunGameWithOptions twice or more in one process.
func RunGame(game Game) error {
return RunGameWithOptions(game, nil)
}
同时:
func RunGame(game Game) error {
return RunGameWithOptions(game, nil)
}
中的函数参数:game Game:
// Game defines necessary functions for a game.
type Game interface {
// Update updates a game by one tick. The given argument represents a screen image.
//
// Update updates only the game logic and Draw draws the screen.
//
// You can assume that Update is always called TPS-times per second (60 by default), and you can assume
// that the time delta between two Updates is always 1 / TPS [s] (1/60[s] by default). As Ebitengine already
// adjusts the number of Update calls, you don't have to measure time deltas in Update by e.g. OS timers.
//
// An actual TPS is available by ActualTPS(), and the result might slightly differ from your expected TPS,
// but still, your game logic should stick to the fixed time delta and should not rely on ActualTPS() value.
// This API is for just measurement and/or debugging. In the long run, the number of Update calls should be
// adjusted based on the set TPS on average.
//
// An actual time delta between two Updates might be bigger than expected. In this case, your game's
// Update or Draw takes longer than they should. In this case, there is nothing other than optimizing
// your game implementation.
//
// In the first frame, it is ensured that Update is called at least once before Draw. You can use Update
// to initialize the game state.
//
// After the first frame, Update might not be called or might be called once
// or more for one frame. The frequency is determined by the current TPS (tick-per-second).
//
// If the error returned is nil, game execution proceeds normally.
// If the error returned is Termination, game execution halts, but does not return an error from RunGame.
// If the error returned is any other non-nil value, game execution halts and the error is returned from RunGame.
Update() error
// Draw draws the game screen by one frame.
//
// The give argument represents a screen image. The updated content is adopted as the game screen.
//
// The frequency of Draw calls depends on the user's environment, especially the monitors refresh rate.
// For portability, you should not put your game logic in Draw in general.
Draw(screen *Image)
// Layout accepts a native outside size in device-independent pixels and returns the game's logical screen
// size in pixels. The logical size is used for 1) the screen size given at Draw and 2) calculation of the
// scale from the screen to the final screen size.
//
// On desktops, the outside is a window or a monitor (fullscreen mode). On browsers, the outside is a body
// element. On mobiles, the outside is the view's size.
//
// Even though the outside size and the screen size differ, the rendering scale is automatically adjusted to
// fit with the outside.
//
// Layout is called almost every frame.
//
// It is ensured that Layout is invoked before Update is called in the first frame.
//
// If Layout returns non-positive numbers, the caller can panic.
//
// You can return a fixed screen size if you don't care, or you can also return a calculated screen size
// adjusted with the given outside size.
//
// If the game implements the interface LayoutFer, Layout is never called and LayoutF is called instead.
Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int)
}
以上就清楚了,和其他游戏引擎相似:update(),loaded(),start()等等,清楚了框架函数就知道贪吃蛇或者其他实例当中为什么有以上3个函数了。
定义结构信息:
const (
screenWidth = 640
screenHeight = 480
gridSize = 10
xGridCountInScreen = screenWidth / gridSize
yGridCountInScreen = screenHeight / gridSize
)
const (
dirNone = iota
dirLeft
dirRight
dirDown
dirUp
)
type Position struct {
X int
Y int
}
type Game struct {
moveDirection int
snakeBody []Position
apple Position
timer int
moveTime int
score int
bestScore int
level int
}
实例化贪吃蛇实例:
func newGame() *Game {
g := &Game{
apple: Position{X: 3 * gridSize, Y: 3 * gridSize},
moveTime: 4,
snakeBody: make([]Position, 1),
}
g.snakeBody[0].X = xGridCountInScreen / 2
g.snakeBody[0].Y = yGridCountInScreen / 2
return g
}
初始化贪吃蛇元素位置、设置相关信息:
func (g *Game) Draw(screen *ebiten.Image) {
for _, v := range g.snakeBody {
vector.DrawFilledRect(screen, float32(v.X*gridSize), float32(v.Y*gridSize), gridSize, gridSize, color.RGBA{0x80, 0xa0, 0xc0, 0xff}, false)
}
vector.DrawFilledRect(screen, float32(g.apple.X*gridSize), float32(g.apple.Y*gridSize), gridSize, gridSize, color.RGBA{0xFF, 0x00, 0x00, 0xff}, false)
if g.moveDirection == dirNone {
ebitenutil.DebugPrint(screen, fmt.Sprintf("Press up/down/left/right to start"))
} else {
ebitenutil.DebugPrint(screen, fmt.Sprintf("FPS: %0.2f Level: %d Score: %d Best Score: %d", ebiten.ActualFPS(), g.level, g.score, g.bestScore))
}
}
实时跟新游戏界面的元素和位置信息等:
func (g *Game) Update() error {
// Decide the snake's direction along with the user input.
// A U-turn is forbidden here (e.g. if the snake is moving in the left direction, the snake cannot go to the right direction immediately).
if inpututil.IsKeyJustPressed(ebiten.KeyArrowLeft) || inpututil.IsKeyJustPressed(ebiten.KeyA) {
if g.moveDirection != dirRight {
g.moveDirection = dirLeft
}
} else if inpututil.IsKeyJustPressed(ebiten.KeyArrowRight) || inpututil.IsKeyJustPressed(ebiten.KeyD) {
if g.moveDirection != dirLeft {
g.moveDirection = dirRight
}
} else if inpututil.IsKeyJustPressed(ebiten.KeyArrowDown) || inpututil.IsKeyJustPressed(ebiten.KeyS) {
if g.moveDirection != dirUp {
g.moveDirection = dirDown
}
} else if inpututil.IsKeyJustPressed(ebiten.KeyArrowUp) || inpututil.IsKeyJustPressed(ebiten.KeyW) {
if g.moveDirection != dirDown {
g.moveDirection = dirUp
}
} else if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
g.reset()
}
if g.needsToMoveSnake() {
if g.collidesWithWall() || g.collidesWithSelf() {
g.reset()
}
if g.collidesWithApple() {
g.apple.X = rand.IntN(xGridCountInScreen - 1)
g.apple.Y = rand.IntN(yGridCountInScreen - 1)
g.snakeBody = append(g.snakeBody, Position{
X: g.snakeBody[len(g.snakeBody)-1].X,
Y: g.snakeBody[len(g.snakeBody)-1].Y,
})
if len(g.snakeBody) > 10 && len(g.snakeBody) < 20 {
g.level = 2
g.moveTime = 3
} else if len(g.snakeBody) > 20 {
g.level = 3
g.moveTime = 2
} else {
g.level = 1
}
g.score++
if g.bestScore < g.score {
g.bestScore = g.score
}
}
for i := int64(len(g.snakeBody)) - 1; i > 0; i-- {
g.snakeBody[i].X = g.snakeBody[i-1].X
g.snakeBody[i].Y = g.snakeBody[i-1].Y
}
switch g.moveDirection {
case dirLeft:
g.snakeBody[0].X--
case dirRight:
g.snakeBody[0].X++
case dirDown:
g.snakeBody[0].Y++
case dirUp:
g.snakeBody[0].Y--
}
}
g.timer++
return nil
}
其他代码主要是游戏的逻辑,本实例都比较简单;暂时分析到这里;如果有不懂的可以留言,本期到这里,下期再见。
同学们,兴趣是最好的老师;只争朝夕,不负韶华!加油!
参考资料:
Go语言中文文档
http://www.golang.ltd/
Golang语言情怀
ID:wwwGolangLtd
www.Golang.Ltd