赞
踩
游戏开发初期总体思路:
绘制出一个游戏窗口以及开始页
绘制出蛇的移动和食物随机生成
绘制结束页
想要用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 方法来渲染游戏界面。 这是游戏引擎的通用做法,开发者只用关注游戏的状态和图像就好
先运行这个框架
现在我们已经能够表示这个窗口了
为了增加代码复用度,引入func NewGame() *Game{}可以把一些刚定义的参数放在这里,然后在update和draw里调用和使用这些参数。便于以后对游戏维护时只用在构造函数中操作。
对游戏框架进行分类:
游戏初始计划架构:
snake
game
game
collision
bg
把复杂的代码全部拆分成单个表示;
main函数中只用写游戏启动方法即可,其他的会在之后讲解
小游戏贪吃蛇:
实现目标:
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)
进ebitengame/snake_demo · cyrex/go - 码云 - 开源中国 (gitee.com)查看源码
现在我们发现这个游戏框架已经建成,想要加入更多功能也变得非常简单。
第二次版本迭代: 本次更新加入了更多的功能:
道具,积分系统,背景图片,背景音乐自动响起,背景音乐主动触发,道具变色,蛇变色,游戏开始页,游戏结束页,点按空格暂停和继续游戏,增加任意形状大小的障碍物。
基于v1.0开发,容我慢慢道来
道具的添加:
模仿food文件新建item.go文件,在此基础上做修改,item可以使用时间函数做到每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)
运行截图:
开始:
游戏中:
结束:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。