golang开发游戏:贪吃蛇

golang开发游戏:贪吃蛇

入门讲解:ebiten引擎

游戏开发初期总体思路:

  1. 绘制出一个游戏窗口以及开始页

  2. 绘制出蛇的移动和食物随机生成

  3. 绘制结束页

想要用go进行一些小游戏的开发,就绕不过ebiten的学习。

下边是引擎最基础的结构

 package main
 import (
     "github.com/hajimehoshi/ebiten/v2"
     "github.com/hajimehoshi/ebiten/v2/ebitenutil"
     "log"
 )
 // Game defines necessary functions for a game.
 //type Game interface {
 //  Update() error
 //  Draw(screen *Image)
 //  Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int)
 //}
 //这是一个内置的接口,意思是主函数必须实现这个接口,肯定要实现这三个方法
 type Game struct {
 }
 func (g *Game) Update() error {
 //作用是更新游戏状态每秒更新60次
     return nil
 }
 func (g *Game) Draw(screen *ebiten.Image) {
 //作用是渲染游戏内的东西(图片或者啥的)
     ebitenutil.DebugPrint(screen, "Hello world!")
 }
 func (g *Game) Layout(outsideWidth,outsideHeight int) (screenWidth, screenHeight int) {
 //作用是指定窗口大小,。
     return 320,240
 }
 func main()  {
     ebiten.SetWindowSize(640, 480)
     ebiten.SetWindowTitle("贪吃蛇")
     if err:=ebiten.RunGame(&Game{});
      err != nil {
        log.Fatal(err)
     }
 }
 主函数中并不需要调用这三个方法,只用运行ebiten.RunGame(&Game{})
 它接受一个实现了 ebiten.Game 接口的对象(这里是 &Game{})作为参数。Ebiten 会自动启动游戏循环,并在循环中自动调用 Update 和 Draw 方法。
 具体的工作流程如下:
 RunGame 启动游戏循环。
 在游戏循环中,它会以每秒 60 次的帧率自动调用 Update 方法,以更新游戏状态。
 然后,它会调用 Draw 方法来渲染游戏界面。
 这是游戏引擎的通用做法,开发者只用关注游戏的状态和图像就好

先运行这个框架

image-20231026102225428

现在我们已经能够表示这个窗口了

为了增加代码复用度,引入func NewGame() *Game{}可以把一些刚定义的参数放在这里,然后在update和draw里调用和使用这些参数。便于以后对游戏维护时只用在构造函数中操作。

对游戏框架进行分类:

游戏初始计划架构:

snake

main

game

snake

food

game

collision

bg

把复杂的代码全部拆分成单个表示;

main函数中只用写游戏启动方法即可,其他的会在之后讲解

小游戏贪吃蛇:

实现目标:

  • 使用ebiten引擎制作hello world的基本程序
  • 增加食物逻辑:在地图中自动生成
  • 增加蛇逻辑:可以上下左右控制蛇头,蛇会以固定速度向蛇头方向移动,原本的蛇身位置会慢慢消失
  • 增加碰撞逻辑:蛇碰到食物定义为:eat方法:食物消失,蛇尾加1,并运行一次食物生成方法,蛇碰到自身或边框,游戏结束。
  • 设置主game,调用背景文件,食物文件,蛇文件,逻辑文件中的方法 按照这些步骤基础功能基本完成,具体实现如下

main文件中只写启动游戏条件

 func main() {
     ebiten.SetWindowSize(game.ScreenWidth, game.ScreenHeight)
     ebiten.SetWindowTitle("Snake Game")
     ebiten.SetMaxTPS(10) // 设置帧率为每秒10帧
     if err := ebiten.RunGame(game.NewGame()); err != nil {
        log.Fatal(err)
     }
 }

food写食物大小,颜色,随机生成函数

 type Food struct {
     x, y int
 }
 func NewFood() *Food {
     food := &Food{}
     food.RandomizePosition()
     return food
 }
 func (f *Food) RandomizePosition() {//随机函数
     maxX := ScreenWidth/gridSize - 2 // -2是为了避免在边界生成
     maxY := ScreenHeight/gridSize - 2
     f.x = rand.Intn(maxX) + 1
     f.y = rand.Intn(maxY) + 1
 }
 func (f *Food) Draw(screen *ebiten.Image) {//图像绘制
     green := color.RGBA{0, 255, 0, 255}
     x, y := f.x*gridSize, f.y*gridSize
     ebitenutil.DrawRect(screen, float64(x), float64(y), gridSize, gridSize, green)
 ​
 }

snake写蛇的各种参数,比如蛇身体初始化,蛇吃到食物后身体的变化函数,蛇的头部移动方向函数,蛇身移动函数

蛇初始化:

 func NewSnake() *Snake {
     snake := &Snake{
        segments:  []Segment{{3, 1}, {2, 1}, {1, 1}},
        direction: Right,
     }
     return snake
 }

蛇吃到食物后身体变化函数

 func (s *Snake) Eat() {
     // 获取蛇的尾部
     tail := s.segments[len(s.segments)-1]
     // 创建一个新的段,位置与尾部相同
     newSegment := Segment{tail.x, tail.y}
     // 将新的段添加到蛇的尾部
     s.segments = append(s.segments, newSegment)
     s.foodEatenTime = time.Now()
 }

蛇移动方向函数

 func (s *Snake) SetDirection(newDirection Direction) {
     // 设置蛇的移动方向
     s.direction = newDirection
 }

蛇身移动函数

 func (s *Snake) Move() {
     // 保存原始头部坐标
     oldHead := s.segments[0]
     // 根据当前方向移动头部
     switch s.direction {
     case Up:
        s.segments[0].y--
     case Down:
        s.segments[0].y++
     case Left:
        s.segments[0].x--
     case Right:
        s.segments[0].x++
     }
     // 移动蛇的身体,每一节移到前一节的位置
     for i := 1; i < len(s.segments); i++ {
        oldX := s.segments[i].x
        oldY := s.segments[i].y
        s.segments[i].x = oldHead.x
        s.segments[i].y = oldHead.y
        oldHead.x = oldX
        oldHead.y = oldY
     }
 }

根据键盘控制蛇头方向函数

 func (s *Snake) Move1() {
     if ebiten.IsKeyPressed(ebiten.KeyArrowUp) {
        s.SetDirection(Up)
     } else if ebiten.IsKeyPressed(ebiten.KeyArrowDown) {
        s.SetDirection(Down)
     } else if ebiten.IsKeyPressed(ebiten.KeyArrowLeft) {
        s.SetDirection(Left)
     } else if ebiten.IsKeyPressed(ebiten.KeyArrowRight) {
        s.SetDirection(Right)
     }

collison逻辑文件写相关的逻辑:比如蛇碰到食物函数,蛇碰到自身函数,蛇碰到边界函数

 func (s *Snake) CollidesWithf(f *Food) bool {
     // 检测蛇头是否与食物碰撞
     head := s.segments[0]
     if head.x == f.x && head.y == f.y {
        s.Eat()
        return true
     }
     return false
 }
 func (s *Snake) CollidesWithItself() bool {
     // 检测蛇是否与自身碰撞
     head := s.segments[0]
     for i := 1; i < len(s.segments); i++ {
         if head.x == s.segments[i].x && head.y == s.segments[i].y {
             return true
         }
     }
     return false
 }
 func (s *Snake) CollidesWithBorder(screenWidth, screenHeight int) bool {
     // 检测蛇是否与边界碰撞
     head := s.segments[0]
     if head.x < 1 || head.x >= screenWidth/gridSize-1 || head.y < 1 || head.y >= screenHeight/gridSize-1 {
         return true
     }
     return false
 }

game.go文件中写游戏的大体框架

先进行结构体和方法初始化

然后在update函数中运行

 func (g *Game) Update() error {游戏流程
 g.snake.Move1()
 g.snake.Move()
 g.snake.Eat()
 g.food.RandomizePosition()
     if g.snake.CollidesWithItself() {
             g.gameOver = true
         }
         if g.snake.CollidesWithBorder(ScreenWidth, ScreenHeight) {
             g.gameOver = true
         }
             return nil
 ​
 }

在Draw函数中运行

 if g.gameOver { ebitenutil.DebugPrint(screen, fmt.Sprintf("Game over!!!,Score: %d", g.score))游戏结束,退出
 else {g.snake.Draw(screen)  游戏开始,绘制游戏相关参数
             g.food.Draw(screen)
             g.item.Draw(screen)

Layout函数

 func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
     return ScreenWidth, ScreenHeight //窗口大小
 }

大致框架就是这样

整体代码实现及效果:

ebitengame/snake_demo · cyrex/go - 码云 - 开源中国 (gitee.com)

image-20231020185401882

ebitengame/snake_demo · cyrex/go - 码云 - 开源中国 (gitee.com)查看源码

现在我们发现这个游戏框架已经建成,想要加入更多功能也变得非常简单。

第二次版本迭代: 本次更新加入了更多的功能:

道具,积分系统,背景图片,背景音乐自动响起,背景音乐主动触发,道具变色,蛇变色,游戏开始页,游戏结束页,点按空格暂停和继续游戏,增加任意形状大小的障碍物。

基于v1.0开发,容我慢慢道来

道具的添加:

模仿food文件新建item.go文件,在此基础上做修改,item可以使用时间函数做到每1秒变一次颜色:

示例:

  • 新增道具且变色:可以做到道具每1秒变色一次
 var (
     red             = color.RGBA{255, 0, 0, 255}            //颜色
     green           = color.RGBA{0, 255, 0, 255}
     blue            = color.RGBA{0, 0, 255, 255}
     colors          = []color.Color{red, green, blue}       //颜色数组  
     currentIndex    = 0                                         
     lastColorChange time.Time   //时间    

控制道具变色时间的函数:

 func (i *Item) Update(*ebiten.Image) error {
     // 获取当前时间
     currentTime := time.Now()
     // 如果距离上次颜色变化已经过去1秒
     if currentTime.Sub(lastColorChange) >= time.Second {
        currentIndex = (currentIndex + 1) % len(colors)
        lastColorChange = currentTime
     }

将Draw中的green改为:

 ebitenutil.DrawRect(screen, float64(x), float64(y), gridSize, gridSize, colors[currentIndex])

其他基本不变。

按照道具类可以再新建一个障碍物obstacle类,不同的是,障碍物坐标自己定义

 func (o *Obst) Obstacle() {
     maxX := ScreenWidth/gridSize - 2 // -2是为了避免在边界生成
     maxY := ScreenHeight/gridSize - 2
     o.x = maxX / 2
     o.y = maxY / 2
 }

障碍物大小和长度可以也自己定义

  • 积分系统

在game中直接定义积分函数:

 func (g *Game) IncreaseScore(points int) {
     g.score += points
 }
 并设置食物积分为10,道具积分为30
 在执行Eat函数后直接执行这个函数
 g.IncreaseScore(加的分数)
 并在Draw函数中运行:
 ebitenutil.DebugPrint(screen, fmt.Sprintf("Score: %d", g.score))
 把分数显示在屏幕上
  • 进入游戏背景音乐响起
 type Game struct {...
     audioContext  *audio.Context
     audioPlayer   *audio.Player
     audioPlayer2  *audio.Player
 }
 const (...
     sampleRate   = 48000
 )
 音乐相关代码:
     //初始化
     game.audioContext = audio.NewContext(sampleRate)
     //读取
     f, err := os.Open("F://huancun//goland//going//ebitengame//黄金矿工//images//1.wav")
     if err != nil {
         log.Fatal(err)
     }
     //转换格式
     d, err := wav.DecodeWithoutResampling(f)
     //使用
     game.audioPlayer, err = game.audioContext.NewPlayer(d)
      哪里需要音乐就放在哪里,吃东西音乐也一样,写到eat函数之后执行就行。
 ​
  • 蛇吃东西变色
 在snake中Draw文件中添加:
 green := color.RGBA{0, 255, 0, 255}
 foodEatenFlashDuration := 100 * time.Millisecond
 ​
 if time.Since(s.foodEatenTime) < foodEatenFlashDuration {
     //green = color.RGBA{255, 255, 255, 0}
     green = color.RGBA{0, 0, 255, 255}
 }
 意思是蛇吃到食物后变色1秒。
 ​
  • 游戏开始页:

设置鼠标点击事件,若不点击则不开始

     if !g.isGameStarted && ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
         // 处理点击事件,开始游戏
         g.isGameStarted = true
     }
  • 开始游戏时需先按空格才能移动
         if ebiten.IsKeyPressed(ebiten.KeySpace) {
             g.snake.Pause()
             } else {//开始有戏

背景进行了解耦,全部放到了bg中,使用时写

 g.bg.DrawOne(screen)
 g.bg.DrawTwo(screen)
 g.bg.DrawThree(screen)即可

可以看源码: ebitengame/snake · cyrex/go - 码云 - 开源中国 (gitee.com)

运行截图:

开始:

image-20231020205515022

游戏中:

image-20231020205451307

结束:

image-20231020205548962

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值