前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >Golang语言情怀--第131期 Go语言Ebiten引擎全栈游戏开发:第2节:Ebiten框架分析

Golang语言情怀--第131期 Go语言Ebiten引擎全栈游戏开发:第2节:Ebiten框架分析

作者头像
李海彬
发布2024-11-07 17:20:47
发布2024-11-07 17:20:47
13700
代码可运行
举报
文章被收录于专栏:Golang语言社区Golang语言社区
运行总次数:0
代码可运行

Ebiten实例分析:贪吃蛇

Ebiten是一个使用Go语言编程库,用于2D游戏开发,可以跨平台。本届开始讲解官方实例,实例熟悉后会给大家讲解实战游戏课。 贪吃蛇实例

源码如下:

代码语言:javascript
代码运行次数:0
复制
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函数:

代码语言:javascript
代码运行次数:0
复制
func (g *Game) Layout(outsideWidth, outsideHeight int) 

Draw函数:

代码语言:javascript
代码运行次数:0
复制
func (g *Game) Draw(screen *ebiten.Image) 

Update函数:

代码语言:javascript
代码运行次数:0
复制
func (g *Game) Update() error

以上3个函数是通过框架函数调用:

代码语言:javascript
代码运行次数:0
复制
ebiten.RunGame(newGame())

而ebiten.RunGame在框架底层:

代码语言:javascript
代码运行次数:0
复制
// 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)
}

同时:

代码语言:javascript
代码运行次数:0
复制
func RunGame(game Game) error {
    return RunGameWithOptions(game, nil)
}

中的函数参数:game Game:

代码语言:javascript
代码运行次数:0
复制
// 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个函数了。

贪吃蛇代码分析

定义结构信息:

代码语言:javascript
代码运行次数:0
复制
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
}

实例化贪吃蛇实例:

代码语言:javascript
代码运行次数:0
复制
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
}

初始化贪吃蛇元素位置、设置相关信息:

代码语言:javascript
代码运行次数:0
复制
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))
    }
}

实时跟新游戏界面的元素和位置信息等:

代码语言:javascript
代码运行次数:0
复制
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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-11-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Golang语言情怀 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Ebiten实例分析:贪吃蛇
  • 框架代码分析
  • 贪吃蛇代码分析
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档