golang小游戏:飞翔的小鸟

2023-12-27 01:30
文章标签 golang 小游戏 飞翔 小鸟

本文主要是介绍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 = truelocationTemp = -5g.Bird = "imgs/up.png"}//  松开空格if !ebiten.IsKeyPressed(ebiten.KeySpace) && gameState == 1{locationTemp = 2g.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 +=3locationTemp = 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 = trueScore++}}}}
}
逻辑处理

障碍物的逻辑处理其实非常简单,只用遍历切片,让每个障碍物的坐标一直减小就可以了。

// 让障碍物移动起来
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+180ran := util.Random(15,2)ran1 := util.Random(180,80)del.TopLength = randel.HoverUPY = ran1del.HoverBottomY = ran1 + 20*randel.BarrierStates = util.Random(3,0)del.isScore = falseobstacles = 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 := birdXbirdTop := birdYbirdRight := birdLeft + birdWidthbirdBottom := birdTop + birdHeightbarrierLeft := barrierXbarrierTop := barrierTopYbarrierRight := barrierLeft + float64(barrierWidth)barrierBottom := barrierBottomYif 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 = trueScore++}}}}
}
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 = falseif (!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 = 2g.DrawBegin(screen)g.DrawGameOver(screen)}

在这里插入图片描述

游戏重开

逻辑处理

添加一个按键响应,当按下重开键后,重新开始游戏。

重点是:要将信息全部初始化:初始化障碍物的切片,初始化小鸟状态 和 坐标,初始化该局分数。

// 此时游戏结束 考虑是否重开if (gameState == 2){if ebiten.IsKeyPressed(ebiten.KeySpace) {   // 按压空格后 重开Score = 0gameState = 1g.BirdState = 1g.LocationY = 200obstacles = obstacles[:0] // 清空 原来切片中的障碍物makeBarrier()}}
界面绘制

重新调用游戏开始阶段的函数即可。

在这里插入图片描述

五、总结感想

做完这个游戏熟悉了 go 的基础语法,增加了对代码的手感。

这篇关于golang小游戏:飞翔的小鸟的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/541443

相关文章

golang字符串匹配算法解读

《golang字符串匹配算法解读》文章介绍了字符串匹配算法的原理,特别是Knuth-Morris-Pratt(KMP)算法,该算法通过构建模式串的前缀表来减少匹配时的不必要的字符比较,从而提高效率,在... 目录简介KMP实现代码总结简介字符串匹配算法主要用于在一个较长的文本串中查找一个较短的字符串(称为

golang内存对齐的项目实践

《golang内存对齐的项目实践》本文主要介绍了golang内存对齐的项目实践,内存对齐不仅有助于提高内存访问效率,还确保了与硬件接口的兼容性,是Go语言编程中不可忽视的重要优化手段,下面就来介绍一下... 目录一、结构体中的字段顺序与内存对齐二、内存对齐的原理与规则三、调整结构体字段顺序优化内存对齐四、内

Golang操作DuckDB实战案例分享

《Golang操作DuckDB实战案例分享》DuckDB是一个嵌入式SQL数据库引擎,它与众所周知的SQLite非常相似,但它是为olap风格的工作负载设计的,DuckDB支持各种数据类型和SQL特性... 目录DuckDB的主要优点环境准备初始化表和数据查询单行或多行错误处理和事务完整代码最后总结Duck

Golang的CSP模型简介(最新推荐)

《Golang的CSP模型简介(最新推荐)》Golang采用了CSP(CommunicatingSequentialProcesses,通信顺序进程)并发模型,通过goroutine和channe... 目录前言一、介绍1. 什么是 CSP 模型2. Goroutine3. Channel4. Channe

Golang使用minio替代文件系统的实战教程

《Golang使用minio替代文件系统的实战教程》本文讨论项目开发中直接文件系统的限制或不足,接着介绍Minio对象存储的优势,同时给出Golang的实际示例代码,包括初始化客户端、读取minio对... 目录文件系统 vs Minio文件系统不足:对象存储:miniogolang连接Minio配置Min

Golang使用etcd构建分布式锁的示例分享

《Golang使用etcd构建分布式锁的示例分享》在本教程中,我们将学习如何使用Go和etcd构建分布式锁系统,分布式锁系统对于管理对分布式系统中共享资源的并发访问至关重要,它有助于维护一致性,防止竞... 目录引言环境准备新建Go项目实现加锁和解锁功能测试分布式锁重构实现失败重试总结引言我们将使用Go作

Golang进程权限调度包runtime

关于 runtime 包几个方法: Gosched:让当前线程让出 cpu 以让其它线程运行,它不会挂起当前线程,因此当前线程未来会继续执行GOMAXPROCS:设置最大的可同时使用的 CPU 核数Goexit:退出当前 goroutine(但是defer语句会照常执行)NumGoroutine:返回正在执行和排队的任务总数GOOS:目标操作系统NumCPU:返回当前系统的 CPU 核数量 p

Golang 网络爬虫框架gocolly/colly(五)

gcocolly+goquery可以非常好地抓取HTML页面中的数据,但碰到页面是由Javascript动态生成时,用goquery就显得捉襟见肘了。解决方法有很多种: 一,最笨拙但有效的方法是字符串处理,go语言string底层对应字节数组,复制任何长度的字符串的开销都很低廉,搜索性能比较高; 二,利用正则表达式,要提取的数据往往有明显的特征,所以正则表达式写起来比较简单,不必非常严谨; 三,使

Golang网络爬虫框架gocolly/colly(四)

爬虫靠演技,表演得越像浏览器,抓取数据越容易,这是我多年爬虫经验的感悟。回顾下个人的爬虫经历,共分三个阶段:第一阶段,09年左右开始接触爬虫,那时由于项目需要,要访问各大国际社交网站,Facebook,myspace,filcker,youtube等等,国际上叫得上名字的社交网站都爬过,大部分网站提供restful api,有些功能没有api,就只能用http抓包工具分析协议,自己爬;国内的优酷、

Golang网络爬虫框架gocolly/colly(三)

熟悉了《Golang 网络爬虫框架gocolly/colly 一》和《Golang 网络爬虫框架gocolly/colly 二》之后就可以在网络上爬取大部分数据了。本文接下来将爬取中证指数有限公司提供的行业市盈率。(http://www.csindex.com.cn/zh-CN/downloads/industry-price-earnings-ratio) 定义数据结构体: type Zhj