golang小游戏开发实战之飞翔的小鸟
作者:壹个人的火锅
游戏开发总体思路
首先要选取一个合适的图形化界面进行开发。该项目选取的是 ebiten 一个用于创建2D游戏和图形应用程序的游戏引擎,提供了一些简单的GUI功能。
其次明确游戏设计思路。飞翔的小鸟共分为三个场景。
第一个场景就是游戏开始前的准备阶段,让玩家点击屏幕确定游戏开始。
第二个场景就是让游戏正式开始,玩家可以操控小鸟进行游戏。
第三个场景就是游戏结束,显示分数的阶段。
一、先让窗口和背景绘制出来
先简单的让窗口显示出来,通过ebiten.RunGame(&game)启动游戏引擎,会开始初始化游戏并且开始循环执行游戏的更新和渲染逻辑。
游戏运行后 会自动调用Update函数进行屏幕刷新,然后再进行游戏绘制。
一般情况下Update函数是写游戏逻辑的,Draw函数进行游戏回话。
func main() { // 设置窗口大小是 ebiten.SetWindowSize(880, 520) // 设置窗口头部,显示 飞翔的小鸟 ebiten.SetWindowTitle("飞翔的小鸟") // 运行游戏 err := ebiten.RunGame(&game); if err != nil { log.Fatal(err) } } /* Layout()函数的返回值表示显示窗口里面逻辑上屏幕的大小 官网上说参数outsideWidth和outsideHeight是显示在桌面的窗口大小 这里是固定大小 */ func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) { return 880, 520 } func (g *Game) Update(screen *ebiten.Image) error { g.DrawBegin(screen) return nil } func (g *Game) DrawBegin(screen *ebiten.Image) { DrawBackGround(screen) } }
这里是进行游戏背景的渲染的,screen.Fill 是将整个画面都填充完一种颜色。对于地板来说,进行一张图片通过for循环来进行反复绘制。
大概就是这个样子,for循环,每次只用让图片的x坐标进行增加就可以了,这样就把背景给渲染出来了。
func DrawBackGround(screen *ebiten.Image){ // 背景颜色 screen.Fill(color.RGBA{78, 192, 202,255}) // 绘制地板 f1, err := os.Open("imgs/ground.png") if err != nil { log.Fatal(err) } img1, err := png.Decode(f1) if err != nil { log.Fatal(err) } filter1 := ebiten.FilterNearest // 把Image文件转成ebiten.Image文件,用于展示 eImg1, _ := ebiten.NewImageFromImage(img1,filter1) var groundX int = 0; var groundY int = 437; for i :=1;i<5;i++ { op1 := &ebiten.DrawImageOptions{} op1.GeoM.Translate(float64(groundX), float64(groundY)) // 图像坐标 groundX+=250 // 在屏幕上展示出图片 screen.DrawImage(eImg1, op1) }
这样一个带有背景图片的窗口就绘制完成了。
二、游戏准备阶段
该阶段制作相对容易,因为此时只用基于上面那个画面,添加一个鼠标点击事件,让玩家点击后即可进入到下一个场景。
先传入两个照片,一个是显示游戏名,一个是点击进入到游戏的图片。
然后对第二个图片添加一个鼠标点击事件,让玩家点击图片进入到游戏中!
ebiten.IsMouseButtonPressed是鼠标点击事件,关键要确定好鼠标点击的范围!
func (g *Game) DrawReady(screen *ebiten.Image) { g.DrawBegin(screen) imageObject1, _, _ := ebitenutil.NewImageFromFile("imgs/title.png",ebiten.FilterDefault) imageObject2, _, _ := ebitenutil.NewImageFromFile("imgs/start.png",ebiten.FilterDefault) gameReady := &GameReady{ imgReady1: imageObject1, img1X: 370, img1Y: 140, imgReady2: imageObject2, img2X: 333, img2Y: 200, } op := &ebiten.DrawImageOptions{} op.GeoM.Translate(float64(gameReady.img1X),float64(gameReady.img1Y)) screen.DrawImage(gameReady.imgReady1,op) op.GeoM.Reset() op.GeoM.Translate(float64(gameReady.img2X),float64(gameReady.img2Y)) screen.DrawImage(gameReady.imgReady2,op) if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) { x,y := ebiten.CursorPosition() rect := image.Rect(gameReady.img2X,gameReady.img2Y,gameReady.img2X+200,gameReady.img2Y+30) if (x >=rect.Min.X && x<=rect.Max.X && y>=rect.Min.Y && y<= rect.Max.Y ){ gameState = 1 } } }
三、游戏开始阶段
这个场景有两个重要的对象需要进行处理。小鸟和障碍物。
小鸟:需要进行绘制,并且给小鸟添加逻辑。每次跳跃修改坐标,并且碰到天空和地面进行over处理。
小鸟的制作
绘制
首先进行小鸟的绘制。绘制其实只用读取小鸟的图片,然后给小鸟设置一个初始坐标,然后展示在屏幕中。
f, err := os.Open(g.Bird) if err != nil { log.Fatal(err) } img, err := png.Decode(f) if err != nil { log.Fatal(err) } filter := ebiten.FilterNearest // 把Image文件转成ebiten.Image文件,用于展示 eImg, _ := ebiten.NewImageFromImage(img,filter) op := &ebiten.DrawImageOptions{} op.GeoM.Translate(g.LocationX,g.LocationY) // 在屏幕上展示出图片 screen.DrawImage(eImg, op)
逻辑处理
小鸟因为只用上下移动,它的逻辑处理起来还是比较简单的,只用通过按键响应监听空格,每按一下空格,小鸟的坐标就减少,松开空格后坐标就想加。(这是因为 这个图像左上角的坐标是0 0,向右和向下分别增加x和y的坐标值)。
同时也要对 小鸟状态进行判断,当触碰到天空和地面时,进行over处理,然后每次按
// 按空格 跳跃 if ebiten.IsKeyPressed(ebiten.KeySpace) && g.BirdState != 0 && !isSpace && gameState == 1 &&!isstop { isSpace = true locationTemp = -5 g.Bird = "imgs/up.png" } // 松开空格 if !ebiten.IsKeyPressed(ebiten.KeySpace) && gameState == 1{ locationTemp = 2 g.Bird = "imgs/down.png" isSpace = false } // 判断是否暂停,然后判断 小鸟此时 能否移动 if !isstop { // 暂停 g.LocationY = g.LocationY + float64(locationTemp) } // 碰到下边界 和 上边界 if (g.LocationY >= 409 || g.LocationY <= -4 ){ g.Bird = "imgs/die.png" g.BirdState = 0 } // 死亡不让超出下边界 if (g.LocationY >= 479){ g.LocationY = 479; } // 小鸟死亡 if (g.BirdState == 0){ g.Bird = "imgs/die.png" g.LocationY +=3 locationTemp = 3 }
障碍物的制作
障碍物的制作也分为两个方面,一个就是障碍物的绘制和逻辑处理。
障碍物的逻辑处理还是相对简单的,因为障碍物只需要一直向左边移动,所以只用改变障碍物的x坐标就可以了。
复杂的是障碍物的绘制,因为障碍物有三种形态,每种形态的绘制还是随机的,不能重复。
绘制
考虑到障碍物的长度不能一样,所以就想到了通过 随机数来确定障碍物的长度。并且也需要通过随机数来确定障碍物的哪一种形态的。
可以先获取一个随机数,来确定障碍物的长度。然后通过for循环来绘制障碍物。(通过for循环是因为障碍物 和 地方一样 它们的图片都是非常短的一截,确定长度后通过循环进行绘制)。
下面是三种障碍物的绘制方法,绘制后将其存入到切片中。
var obstacles []*NewBarrier // 切片存数组 // 下方障碍物 func (o *NewBarrier) DrawTop(screen *ebiten.Image) { for i := 1 ;i<o.TopLength;i++{ op := &ebiten.DrawImageOptions{} op.GeoM.Translate(float64(o.X),float64(o.TopY+(i-1)*20)) screen.DrawImage(o.Image,op) } op1 := &ebiten.DrawImageOptions{} op1.GeoM.Translate(float64(o.X-2),float64(o.TopLength*20-20)) screen.DrawImage(o.BottomImage,op1) } // 上方障碍物 func (o *NewBarrier) DrawBottom(screen *ebiten.Image) { for i := 1 ;i<19-o.TopLength;i++{ op := &ebiten.DrawImageOptions{} op.GeoM.Translate(float64(o.X),float64(o.BottomY-(i-1)*20)) screen.DrawImage(o.Image,op) } op1 := &ebiten.DrawImageOptions{} op1.GeoM.Translate(float64(o.X-2),float64(500-(19-o.TopLength)*20+20)) screen.DrawImage(o.UpImage,op1) } // 中间障碍物 func (o *NewBarrier) DrawMid(screen *ebiten.Image) { //fmt.Println(o.HoverUPY,o.TopLength,o.HoverBottomY) for i := 1 ;i<o.TopLength;i++{ op := &ebiten.DrawImageOptions{} op.GeoM.Translate(float64(o.X),float64(o.HoverUPY+i*20)) screen.DrawImage(o.Image,op) } op1 := &ebiten.DrawImageOptions{} op1.GeoM.Translate(float64(o.X-2),float64(o.HoverBottomY)) screen.DrawImage(o.BottomImage,op1) op2 := &ebiten.DrawImageOptions{} op2.GeoM.Translate(float64(o.X-2),float64(o.HoverUPY)) screen.DrawImage(o.UpImage,op2) } // 绘制障碍物 func (g *Game) DrawBarrier(screen *ebiten.Image){ if (gameState != 0){ for _, o := range obstacles { if (o.BarrierStates ==0 || o.BarrierStates==1){ o.DrawTop(screen) o.DrawBottom(screen) }else{ o.DrawMid(screen) } if (o.X<200){ if (!o.isScore){ o.isScore = true Score++ } } } } }
逻辑处理
障碍物的逻辑处理其实非常简单,只用遍历切片,让每个障碍物的坐标一直减小就可以了。
// 让障碍物移动起来 func obojsaasdf(obs []*NewBarrier){ for i:=0;i< len(obs);i++ { obs[i].X -- } }
障碍物对象池
因为障碍物的数量考虑到非常多,并且一直有障碍物会移出屏幕,移出屏幕之后便看不见了,如果不对其进行妥善处理,我感觉会对内存有很大的消耗。所以就引入了类似于java对象池一样的池子。将超出屏幕的障碍物 修改其部分属性 然后重新添加到切片末尾,这样便只用初始化几个障碍物,然后这些障碍物反复利用,便可大大节省内存消耗。
func deal() { if len(obstacles) > 0 { rand.Seed(time.Now().UnixNano()) for i := 0; i < len(obstacles); i++{ if obstacles[i].X < -50 { obstacles[i].TopLength = util.Random(15,2) // 移除超出屏幕的障碍物 del := obstacles[0] del.X = obstacles[len(obstacles)-1].X+180 ran := util.Random(15,2) ran1 := util.Random(180,80) del.TopLength = ran del.HoverUPY = ran1 del.HoverBottomY = ran1 + 20*ran del.BarrierStates = util.Random(3,0) del.isScore = false obstacles = append(obstacles[:i], obstacles[i+1:]...) // 添加新的障碍物到切片末尾 obstacles = append(obstacles, del) } } } }
碰撞检测处理
当小鸟和障碍物都绘制完了,就需要考虑他们之间的碰撞检测了!
碰撞检测采用的是获取小鸟四个角的坐标和障碍物的坐标,让他们的坐标没有交集。
绿色的是障碍物,蓝色的是小鸟。有一点抽象了。。。图中这几种情况是不会发生碰撞的情况。
所以,对于上下型障碍物来说,当小鸟的右边 < 障碍物的左边 或者 小鸟的左边 > 障碍物的右边 或者 小鸟的上边 > 障碍物的下面 并且小鸟的下面 < 障碍物的上面
对于悬浮障碍物来说,x 坐标考虑和上面一样,对于 y 坐标。小鸟的下面 < 障碍物的上面 或者 小鸟的上面 > 障碍物的下面
// 碰撞检测 func IsColliding(birdX, birdY float64, birdWidth, birdHeight float64, barrierX, barrierTopY, barrierBottomY float64, barrierWidth int,HoverUp float64,HoverBottom float64) bool { birdLeft := birdX birdTop := birdY birdRight := birdLeft + birdWidth birdBottom := birdTop + birdHeight barrierLeft := barrierX barrierTop := barrierTopY barrierRight := barrierLeft + float64(barrierWidth) barrierBottom := barrierBottomY if birdRight < barrierLeft || birdLeft > barrierRight || ((birdBottom < barrierBottom) && (birdTop > barrierTop)) || ((birdBottom < HoverUp) || (birdTop > HoverBottom+20)) { // 没有碰撞 return false } // 有碰撞 return true }
分数绘制
游戏中应该在添加一个记录成绩的文本。
小鸟每次过一个障碍物让其分数 +1,考虑到小鸟一直是原地上下移动,是障碍物在一直移动,所以可以考虑通过障碍物的坐标来记录分数。每当障碍物的 x 坐标 小于 小鸟的 x 坐标的时候,便让成绩+1即可。
func (g *Game) DrawBarrier(screen *ebiten.Image){ if (gameState != 0){ for _, o := range obstacles { if (o.X<200){ if (!o.isScore){ o.isScore = true Score++ } } } } } func (g *Game) DrawScore(screen *ebiten.Image) { ebitenutil.DebugPrintAt(screen , Itoa(Score),100,100) }
游戏暂停
这里是在游戏过程中,添加一个游戏暂停处理。
其实逻辑很简单,添加一个按键监听,当按下暂停键后,让 小鸟保持不动,障碍物不再移动即可。
那具体怎么处理呢? 小鸟的跳跃是通过按键响应来控制的,那么当调用这个按键响应的同时添加一个 判断即可,判断此时 是否暂停。
同样的,障碍物移动的时候,也添加一个判断,判断此时是否暂停。
var isstop bool = false // 游戏是否暂停 // 判断游戏是否暂停 if ebiten.IsKeyPressed(ebiten.KeyS) && isPressStop && gameState ==1 && g.BirdState != 0{ isPressStop = false if (!isstop){ isstop = true }else { isstop = false } } // 判断是否暂停,然后判断 小鸟此时 能否移动 if !isstop { // 暂停 g.LocationY = g.LocationY + float64(locationTemp) } // 判断游戏是否暂停 if ((g.LocationY != 482 || g.BirdState !=0) && !isstop && gameState != 0){ // 没有暂停 obojsaasdf(obstacles) }else if ((g.LocationY != 482 || g.BirdState != 0 ) && isstop) { // 游戏暂停 //fmt.Println("暂停了 ,障碍物不能动了") }
四、游戏结束阶段
游戏结束
界面绘制
游戏结束后,需要绘制的就是 “GameOver” 和 这一局的分数了。还有就是让障碍物不在移动,这个在下面的逻辑处理细讲。
其实很简单,就是获取字符串内容,然后显示在屏幕中间即可 - -
// 绘制 GameOver方法 func (g *Game) DrawGameOver(screen *ebiten.Image) { ebitenutil.DebugPrintAt(screen , "GameOver",400,210) ebitenutil.DebugPrintAt(screen , Itoa(Score),422,240) } // 绘制成绩 func (g *Game) DrawScore(screen *ebiten.Image) { ebitenutil.DebugPrintAt(screen , Itoa(Score),100,100) }
逻辑处理
当游戏暂停后,停止调用障碍物移动的方法就行了。
// 判断游戏是否结束 if ((g.LocationY != 482 || g.BirdState !=0) && !isstop && gameState != 0){ // 没有结束 obojsaasdf(obstacles) }else if ((g.LocationY != 482 || g.BirdState != 0 ) && isstop) { // 游戏暂停 //fmt.Println("暂停了 ,障碍物不能动了") }else if(g.LocationY == 482 || g.BirdState == 0 ) { // 游戏结束 gameState = 2 g.DrawBegin(screen) g.DrawGameOver(screen) }
游戏重开
逻辑处理
添加一个按键响应,当按下重开键后,重新开始游戏。
重点是:要将信息全部初始化:初始化障碍物的切片,初始化小鸟状态 和 坐标,初始化该局分数。
// 此时游戏结束 考虑是否重开 if (gameState == 2){ if ebiten.IsKeyPressed(ebiten.KeySpace) { // 按压空格后 重开 Score = 0 gameState = 1 g.BirdState = 1 g.LocationY = 200 obstacles = obstacles[:0] // 清空 原来切片中的障碍物 makeBarrier() } }
界面绘制
重新调用游戏开始阶段的函数即可。
五、总结感想
做完这个游戏熟悉了 go 的基础语法,增加了对代码的手感。
到此这篇关于golang小游戏开发实战之飞翔的小鸟的文章就介绍到这了,更多相关golang飞翔的小鸟小游戏内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!