小游戏:飞机大战"/>
golang小游戏:飞机大战
目录
一、准备阶段:游戏引擎介绍准
安装ebiten游戏引擎
打开窗口
二、游戏开发阶段
(1)、添加我飞机
先创建一个Object类
创建一个Model类
将飞机的shipAdd()方法添加到main.go的Update()方法里,并在main.go方法里用ship调用objectDraw()方法
(2)使我方飞机去移动
在Model.go添加飞机移动的方法shipMove()
(3)添加敌方第一种飞机
enemy1Add 第一种敌方飞机生成的方法
enmy1Move() 第一种敌方飞机的移动方法
objectSliceDraw() 遍历enemy1List切片并将里面的敌方飞机原素画出
最后将enemy1Add()和enemy1Move()添加到main.go的Update方法里,将objectSliceDraw(screen,enemy1List)添加到Draw()方法里
(4)添加我方飞机的子弹
(5)添加碰撞检测的方法
是否发生碰撞的条件
(6)给游戏添加星空背景
运行效果
(7)添加敌方小boss及子弹
运行效果
(8)添加敌方大boss
运行效果
(9)添加我方子弹和飞机与敌方元素的碰撞
(10)游戏的暂停功能
三、游戏架构
文件目录
go在运行上有点特殊须在终端运行go run . 即运行Plane2中的所有文件
运行效果
四、总结
源码
一、准备阶段:游戏引擎介绍准
安装ebiten游戏引擎
制作游戏我们首先需要一个图形化界面,我使用的是ebiten,安装及其简单只需在自己终端运行以下代码,即可完成安装
go run -tags=example github/hajimehoshi/ebiten/v2/examples/2048@latest
打开窗口
创建main.go
package mainimport (/*这段代码首先导入了必要的包,其中包括 github/hajimehoshi/ebiten/v2 和github/hajimehoshi/ebiten/v2/ebitenutil。这些包提供了 Ebiten 游戏引擎的功能和实用工具。*/"log""github/hajimehoshi/ebiten/v2""github/hajimehoshi/ebiten/v2/ebitenutil"
)
//定义了一个Game结构体 实现了游戏引擎里的Game接口
type Game struct{}//下面实现了Game结构体里三个方法 Update() Draw() Layout()//Update()相当与MVC里面的C control 用于将来控制我们飞机元素的添加和移动
func (g *Game) Update() error {return nil
}//Draw()相当于MVC架构里面的V view 用于渲染显示图形画面
func (g *Game) Draw(screen *ebiten.Image) {ebitenutil.DebugPrint(screen, "Hello, Plane")//画出一句话 Hello, Plane
}//设置窗口大小 宽600 高800
func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {return 600, 800
}func main() {//设置窗口大小ebiten.SetWindowSize(600, 800)//设置窗口的标题ebiten.SetWindowTitle("飞机大战")/*接着调用 ebiten.RunGame(&Game{}) 启动游戏循环,并将 &Game{} 作为参数传递给 RunGame() 函数以每秒60帧循环执行Game接口的 Update()方法 Draw()方法,以创建并运行游戏对象。如果在运行游戏过程中出现错误,将通过 log.Fatal() 打印错误信息并终止程序的执行。*/if err := ebiten.RunGame(&Game{}); err != nil {log.Fatal(err)}
}
运行效果
二、游戏开发阶段
(1)、添加我飞机
首先我们要明白我们的需求:我方飞机有自己的坐标 、可以移动 、有自己的图象属性
先创建一个Object类
Object类里有名字 图片 X,Y坐标 HP生命值 宽高属性
init初始化方法 objectDraw()画出对象的方法
package mainimport (//"fmt""github/hajimehoshi/ebiten/v2""github/hajimehoshi/ebiten/v2/ebitenutil""log"
)
//planeHp 根据飞机名称映射飞机的生命值
var planeHp = map[string]int{"myplane":1,
}
type Object struct {name string //表示对象的名称image *ebiten.Image //表示对象对应的图片X float64//对象的X坐标Y float64//对象的Y坐标HP int//对象的生命值width int//对象的宽度 将来用于碰撞检测height int//对象的高度
}
func (object *Object) init(name string, X, Y float64) {//对象的初始化方法object.name = nameobject.X = Xobject.Y = Yobject.HP = planeHp[object.name]//从planeHp map集合里映射飞机对象的生命值object.getImg()//调用getImg方法初始化对象的图片}
func (object *Object) getImg() {//使用ebitenutil.NewImageFromFile函数从文件系统中加载图像,并将图像赋值给对象的image字段img, _, err1 := ebitenutil.NewImageFromFile("image/" + object.name+".png")object.width, object.height = img.Size()//初始化对象的宽高if err1 != nil {log.Fatal(err1)}object.image = img
}//对象的图片绘画方法
func (object *Object)ObjectDraw(screen *ebiten.Image){var op_objecte ebiten.DrawImageOptions//ebiten.DrawImageOptions变量,用于配置绘制图像的选项op_objecte.GeoM.Translate(object.X, object.Y)//设置图象的位置使用screen.DrawImage方法将对象的图像绘制到目标图像上,传递了对象的图像object.image和绘制选项&op_objectescreen.DrawImage(object.image, &op_objecte)
}
创建一个Model类
Model用于写飞机的模型 和后续飞机的移动
package main//声明我方飞机变量
var ship *Object=nil//初始化我方飞机对象
func shipAdd(){if ship==nil {var myplane Objectmyplane.init("myplane",267,750)ship=&myplane}
}
将飞机的shipAdd()方法添加到main.go的Update()方法里,并在main.go方法里用ship调用objectDraw()方法
package mainimport (/*这段代码首先导入了必要的包,其中包括 github/hajimehoshi/ebiten/v2 和github/hajimehoshi/ebiten/v2/ebitenutil。这些包提供了 Ebiten 游戏引擎的功能和实用工具。*/"log""github/hajimehoshi/ebiten/v2""github/hajimehoshi/ebiten/v2/ebitenutil"
)
//定义了一个Game结构体 实现了游戏引擎里的Game接口
type Game struct{}//下面实现了Game结构体里三个方法 Update() Draw() Layout()//Update()相当与MVC里面的C control 用于将来控制我们飞机元素的添加和移动
func (g *Game) Update() error {shipAdd()return nil
}//Draw()相当于MVC架构里面的V view 用于渲染显示图形画面
func (g *Game) Draw(screen *ebiten.Image) {ebitenutil.DebugPrint(screen, "Hello, Plane")//画出一句话 Hello, Planeship.ObjectDraw(screen)}//设置窗口大小 宽600 高800
func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {return 600, 800
}func main() {//设置窗口大小ebiten.SetWindowSize(600, 800)//设置窗口的标题ebiten.SetWindowTitle("飞机大战")/*接着调用 ebiten.RunGame(&Game{}) 启动游戏循环,并将 &Game{} 作为参数传递给 RunGame() 函数以每秒60帧循环执行Game接口的 Update()方法 Draw()方法,以创建并运行游戏对象。如果在运行游戏过程中出现错误,将通过 log.Fatal() 打印错误信息并终止程序的执行。*/if err := ebiten.RunGame(&Game{}); err != nil {log.Fatal(err)}
}
打开终端运行
go run main.go Model.go Object.go
运行效果
(2)使我方飞机去移动
在Model.go添加飞机移动的方法shipMove()
package mainimport ("github/hajimehoshi/ebiten/v2"
)//声明我方飞机变量
var ship *Object=nil//初始化我方飞机对象
func shipAdd(){if ship==nil {var myplane Objectmyplane.init("myplane",267,750)ship=&myplane}
}
//飞机根据键盘移动
func shipMove(){//如果按下键盘左方向键我方飞机X减小 画面显示向左移动 if ebiten.IsKeyPressed(ebiten.KeyLeft) {ship.X -= 8if ship.X < 0 {//防止飞机飞出窗口左边界ship.X = 0}//如果按下键盘右方向键我方飞机X增大 画面显示向右移动 } else if ebiten.IsKeyPressed(ebiten.KeyRight) {ship.X+= 8if ship.X >= 534 {//防止飞机飞出窗口右边界ship.X = 534}//如果按下键盘上方向键我方飞机Y坐标增大 画面显示向上移动 } else if ebiten.IsKeyPressed(ebiten.KeyUp) {ship.Y -= 8if ship.Y <= 0 {//防止飞机飞出窗口上边界ship.Y = 0}//如果按下键盘下方向键我方飞机Y减小 画面显示向下移动 } else if ebiten.IsKeyPressed(ebiten.KeyDown) {ship.Y += 8if ship.Y >= 750 {//防止飞机飞出窗口下边界ship.Y = 750}}
}
在main.go 的Update()中添加shipMove()
func (g *Game) Update() error {shipAdd()shipMove()return nil
}
(3)添加敌方第一种飞机
第一种敌方飞机x坐标位置随机初始化,且可以一直向下移动
在Model.go中新声明一个enemy1List 切片存储我们的第一种飞机的对象
var enemy1List []*Object
enemy1Add 第一种敌方飞机生成的方法
(用随机数控制敌方飞机生成的频率和生成的位置)
func enemy1Add() {Seed(time.Now().UnixNano())//是一个用于设置随机数种子random2 := Intn(200) // 生成0到200之间的随机数if (int)(random2) == 13 {//当生成的随机数为13时添加敌方飞机,也就是说有1/200的概率添加敌方飞机1
/*
上面提到Update方法每秒运行60次,我们将enemy1Add方法加入到Update方法后,一秒被调用60次,随机数random2是13的概率为1/200,也就是说大概三秒半添加一个敌方飞机
*/var enemy1 Objectrandom1 := Intn(550) // 生成0到550之间的随机数 确定敌机生成位置enemy1.init("enemy_new1",float64(random1),0)enemy1List = append(enemy1List,&enemy1 )}}
enmy1Move() 第一种敌方飞机的移动方法
func enemy1Move(){for _,enemy1 := range enemy1List {//遍历敌方第一钟飞机的切片enemy1.Y++//使敌方飞机的Y坐标增加表现为敌方飞机向下移动}
}
objectSliceDraw() 遍历enemy1List切片并将里面的敌方飞机原素画出
func objectSliceDraw(screen *ebiten.Image, sl []*Object){//当想画出第一种飞机时,sl参数传为ennemy1Listfor _, sli := range sl {//遍历切片var op_slice ebiten.DrawImageOptionsop_slice.GeoM.Translate(sli.X, sli.Y)//根据切片元素的x,y坐标来确定切片的位置screen.DrawImage(sli.image, &op_slice)//画出元素的图片}
}
最后将enemy1Add()和enemy1Move()添加到main.go的Update方法里,将objectSliceDraw(screen,enemy1List)添加到Draw()方法里
func (g *Game) Update() error {shipAdd()shipMove()enemy1Add()enemy1Move()return nil
}//Draw()相当于MVC架构里面的V view 用于渲染显示图形画面
func (g *Game) Draw(screen *ebiten.Image) {ebitenutil.DebugPrint(screen, "Hello, Plane")//画出一句话 Hello, Planeship.ObjectDraw(screen)objectSliceDraw(screen,enemy1List)}
运行效果
(4)添加我方飞机的子弹
在Model.go中定义切片bulletList []*Object 用于存储飞机的子弹
在Model.go中写bulletAdd() bullletMove()分别控制我方飞机子弹的增添和移动
最后将bullletAdd()和bulletMove()添加到main.go的Update方法里,将objectSliceDraw(screen,bulletList)添加到Draw()方法里
Model.go
var bulletList []*Object //定义存储子弹的切片
func bulletAdd() {if inpututil.IsKeyJustReleased(ebiten.KeySpace) {//如果按下空格键就进入产生子弹的代码块// 按下空格子弹根据飞机确定位置var bullet1 Objectif ship.HP >= 5 {//当我方飞机的血量大于等于5时 飞机一次发射两发子弹var bullet2 Objectbullet1.init("bulletplus", ship.X+16, ship.Y)bullet2.init("bulletplus", ship.X+1, ship.Y)bulletList = append(bulletList, &bullet1)bulletList = append(bulletList, &bullet2)} else {//反之就发射一发子弹Ybullet1.init("bulletplus", ship.X+1, ship.Y-80)//ship.X+1 ,ship.Y-80 是子弹的x y 坐标使子弹恰好在飞机中间生成//子弹的坐标为 (ship.X-bullet.X)/2 ship.Y-bullet.YbulletList = append(bulletList, &bullet1)//将新生成的飞机子弹添加到切片bulletList里}}}
func bulletMove(){for _,bullet := range bulletList{bullet.Y-=5}
}
main.go
func (g *Game) Update() error {shipAdd()shipMove()enemy1Add()enemy1Move()bulletAdd()bulletMove()return nil
}//Draw()相当于MVC架构里面的V view 用于渲染显示图形画面
func (g *Game) Draw(screen *ebiten.Image) {ebitenutil.DebugPrint(screen, "Hello, Plane")//画出一句话 Hello, Planeship.ObjectDraw(screen)objectSliceDraw(screen,enemy1List)objectSliceDraw(screen,bulletList)}
运行效果
(5)添加碰撞检测的方法
新建hit.go里面写hit()方法
package mainimport "math"
//两个物体碰撞的方法
func hit(MyList *[]*Object, enemyList *[]*Object) {//假如传的参数为&bulletList &enemy1Listfor i, my := range *MyList {//遍历我方子弹的切片for j, enemy := range *enemyList {//遍历敌方飞机1的切片/*(x1,y1)为某个子弹的中心坐标 (x1,y2)为某个敌方飞机的中心坐标*/x1 := my.X + float64(my.width)/2y1 := my.Y + float64(my.height)/2x2 := enemy.X + float64(enemy.width)/2y2 := enemy.Y + float64(enemy.height)/2//判断是否碰撞的条件if math.Abs(x2-x1) < float64(my.width+enemy.width)/2 && math.Abs(y2-y1) < float64(my.height+enemy.height)/2 {*enemyList = append((*enemyList)[:j], (*enemyList)[j+1:]...)//发生碰撞将该飞机从切片中移除*MyList = append((*MyList)[:i], (*MyList)[i+1:]...)//发生碰撞后将该子弹从切片中移除return}}}
}
是否发生碰撞的条件
在main.go的Update()中加入
hit(&bulletList,&enemy1List)//传入我方子弹切片的地址 敌方飞机1的切片地址
//Update()相当与MVC里面的C control 用于将来控制我们飞机元素的添加和移动
func (g *Game) Update() error {shipAdd()shipMove()enemy1Add()enemy1Move()bulletAdd()bulletMove()hit(&bulletList,&enemy1List)//传入我方子弹切片的地址 敌方飞机1的切片地址return nil
}
如果看到这一步基本上游戏开发的流程已经完成了90%,剩下的几乎都是重复利用上面的代码来写新的游戏元素。
(6)给游戏添加星空背景
在Model中添加背景的创造及移动方法
var bg *Object//定义背景的对象func bgInit(){//背景产生的方法if bg==nil {var object Objectobject.init("bg",0,-700)bg=&object}
}
func bgMove() {//背景循环向下移动 营造我方飞机一直向前飞的效果bg.Y++if bg.Y >= -10 {bg.Y = -790}}
更改之后的Update(0
/*
mian.go
*/
//Update()相当与MVC里面的C control 用于将来控制我们飞机元素的添加和移动
func (g *Game) Update() error {bgInit()bgMove()shipAdd()shipMove()enemy1Add()enemy1Move()bulletAdd()bulletMove()hit(&bulletList,&enemy1List)//传入我方子弹切片的地址 敌方飞机1的切片地址return nil
}//Draw()相当于MVC架构里面的V view 用于渲染显示图形画面
func (g *Game) Draw(screen *ebiten.Image) {bg.ObjectDraw(screen)//将背景的Draw方法写在Draw的最上面一行,因为在画图时最上面的代码图象在最里面一层ship.ObjectDraw(screen)objectSliceDraw(screen,enemy1List)objectSliceDraw(screen,bulletList)}
(7)添加第二种敌机
//Model.govar enemy2List []*Object//敌机2的切片
var enemy2Bullet []*Object//敌机2子弹的切片
var count=0//记录游戏的刷新次数
// 添加第二种敌机
func enemy2Add() {Seed(time.Now().UnixNano())random2 := Intn(600) // 生成0到1000之间的随机数if (int)(random2) == 13 {// fmt.Println("画出了敌机2")var e Objectrandom3 := Intn(300)//随机产生敌机2的x坐标e.init("enemy_new2", float64(random3), 120)enemy2List = append(enemy2List, &e)}}// 给第二种敌机添加子弹
func enemy2BulletAdd(){if count%60 == 0 {count++for _, e := range enemy2List {var enemybullet2 Objectenemybullet2.init("enemybullet3", e.X+15, e.Y+67)enemy2Bullet = append(enemy2Bullet, &enemybullet2)}}
}func enemy2BulletMove() {for _, b := range enemy2Bullet {b.Y += 1.5}
}
mian.go
//Update()相当与MVC里面的C control 用于将来控制我们飞机元素的添加和移动
func (g *Game) Update() error {count++//每执行一次Update()刷新次数加一bgInit()bgMove()shipAdd()shipMove()enemy1Add()enemy1Move()bulletAdd()bulletMove()enemy2Add()enemy2BulletAdd()enemy2BulletMove()hit(&bulletList,&enemy1List)//传入我方子弹切片的地址 敌方飞机1的切片地址return nil
}//Draw()相当于MVC架构里面的V view 用于渲染显示图形画面
func (g *Game) Draw(screen *ebiten.Image) {bg.ObjectDraw(screen)//将背景的Draw方法写在Draw的最上面一行,因为在画图时最上面的代码图象在最里面一层ship.ObjectDraw(screen)objectSliceDraw(screen,enemy1List)objectSliceDraw(screen,bulletList)objectSliceDraw(screen,enemy2List)//enemy2List 调用Draw方法objectSliceDraw(screen,enemy2Bullet)//enemy2Bullet 调用Draw方法}//设置窗口大小 宽600 高800
func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {return 600, 800
}
运行效果
(7)添加敌方小boss及子弹
小boss的子弹可以追踪我方飞机
当我方飞机移动到子弹的上方时可以把追踪子弹消除
//Model.govar boss2List []*Object//boss2的切片
var boss2Bullet []*Object //boss2的子弹切片
func boss2Add() {Seed(time.Now().UnixNano())if count%1000 == 0 &&count!=0{ //20秒左右产生一个boss2count++random4 := Intn(400) // 生成0到200之间的随机数var boss2 Objectboss2.init("boos_new4", float64(random4), 112)boss2List = append(boss2List, &boss2)}
}
func boss2BulletAdd() {if count%200 == 0 {//生成了子弹count++for _, v := range boss2List {var bs2_bullet Objectbs2_bullet.init("enemy_bullet2", v.X+75.5, v.Y+112)boss2Bullet = append(boss2Bullet, &bs2_bullet)}}
}func boss2BulletMove() {for i, b := range boss2Bullet {//改变子弹坐标if ship.Y >= b.Y {//子弹会追踪飞机b.Y += 2} else {b.Y -= 2}if ship.X+float64(ship.width/2) >= b.X {b.X += 2} else {b.X -= 2}if b.Y+float64(b.height)-20 > ship.Y {//如果子弹在飞机的下方 子弹消失boss2Bullet = append(boss2Bullet[:i], boss2Bullet[i+1:]...)break}}
}
//main.go
func (g *Game) Update() error {count++//每执行一次Update()刷新次数加一bgInit()bgMove()shipAdd()shipMove()enemy1Add()enemy1Move()bulletAdd()bulletMove()enemy2Add()enemy2BulletAdd()enemy2BulletMove()boss2Add()boss2BulletAdd()boss2BulletMove()hit(&bulletList,&enemy1List)//传入我方子弹切片的地址 敌方飞机1的切片地址return nil
}//Draw()相当于MVC架构里面的V view 用于渲染显示图形画面
func (g *Game) Draw(screen *ebiten.Image) {bg.ObjectDraw(screen)//将背景的Draw方法写在Draw的最上面一行,因为在画图时最上面的代码图象在最里面一层ship.ObjectDraw(screen)objectSliceDraw(screen,enemy1List)objectSliceDraw(screen,bulletList)objectSliceDraw(screen,enemy2List)//enemy2List 调用Draw方法objectSliceDraw(screen,enemy2Bullet)//enemy2Bullet 调用Draw方法objectSliceDraw(screen,boss2List)//boss2List 调用Draw方法objectSliceDraw(screen,boss2Bullet)//boss2Bullet 调用Draw方法
}
运行效果
(8)添加敌方大boss
//Model.go
var boss1 *Object//定义大boss对象
var boss1Bullet []*Object//定义boss1子弹的切片
var score=0 //定义游戏分数
var boss1_speed=3.0
func Boss1Add() {if score >= 30 && boss1 == nil {//当游戏分数大于10时boss产生Seed(time.Now().UnixNano())var boss Objectx := Intn(359) + 1boss.init("boos_new1", float64(x), 10)boss1 = &boss}if boss1 != nil {//改变boss1的坐标boss1.X += boss1_speedif boss1.X <= 0 || boss1.X >= 360 {boss1_speed = -boss1_speed}}
}// 添加boss1子弹
func boss1BulletAdd() {if count%70 == 0 && boss1 != nil {count++var bos1Bullet Objectbos1Bullet.init("bullet_new2", boss1.X+110, boss1.Y+174)boss1Bullet = append(boss1Bullet, &bos1Bullet)}
}
func boss1BulletMove() {for i, bos1 := range boss1Bullet {bos1.Y += 2.0if bos1.Y > 800 {boss1Bullet = append(boss1Bullet[:i], boss1Bullet[i+1:]...)break}}}
//main.go//Update()相当与MVC里面的C control 用于将来控制我们飞机元素的添加和移动
func (g *Game) Update() error {count++//每执行一次Update()刷新次数加一bgInit()bgMove()shipAdd()shipMove()enemy1Add()enemy1Move()bulletAdd()bulletMove()enemy2Add()enemy2BulletAdd()enemy2BulletMove()boss2Add()boss2BulletAdd()boss2BulletMove()Boss1Add()boss1BulletAdd()boss1BulletMove()hit(&bulletList,&enemy1List)//传入我方子弹切片的地址 敌方飞机1的切片地址return nil
}//Draw()相当于MVC架构里面的V view 用于渲染显示图形画面
func (g *Game) Draw(screen *ebiten.Image) {bg.ObjectDraw(screen)//将背景的Draw方法写在Draw的最上面一行,因为在画图时最上面的代码图象在最里面一层ship.ObjectDraw(screen)objectSliceDraw(screen,enemy1List)objectSliceDraw(screen,bulletList)objectSliceDraw(screen,enemy2List)//enemy2List 调用Draw方法objectSliceDraw(screen,enemy2Bullet)//enemy2Bullet 调用Draw方法objectSliceDraw(screen,boss2List)//boss2List 调用Draw方法objectSliceDraw(screen,boss2Bullet)//boss2Bullet 调用Draw方法objectSliceDraw(screen,boss1Bullet)if boss1!=nil{boss1.ObjectDraw(screen)}}
运行效果
(9)添加我方子弹和飞机与敌方元素的碰撞
在hit.go中再添加一个panleHit()方法用于我方飞机与敌方子弹的碰撞,我方子弹与boss1的碰撞
//写飞机(我方飞机或boss1)与其他子弹的碰撞func planeHit(plane *Object,slice *[]*Object){for j, sl := range *slice {//遍历敌方飞机1的切片/*(x1,y1)为某个子弹的中心坐标 (x1,y2)为某个敌方飞机的中心坐标*/x1 := plane.X + float64(plane.width)/2y1 := plane.Y + float64(plane.height)/2x2 := sl.X + float64(sl.width)/2y2 := sl.Y + float64(sl.height)/2//判断是否碰撞的条件if math.Abs(x2-x1) < float64(plane.width+sl.width)/2 && math.Abs(y2-y1) < float64(plane.height+sl.height)/2 {*slice = append((*slice)[:j], (*slice)[j+1:]...)//发生碰撞将该元素从切片中移除plane.HP--if plane.HP<=0 {start=!start}return}}}
然后直接再main.go Update()中调用hit 和planeHit方法
/*main.go Update()
*/hit(&bulletList,&enemy1List)//传入我方子弹切片的地址 敌方飞机1的切片地址hit(&bulletList,&enemy2List)hit(&bulletList,&boss2List)hit(&bulletList,&boss2Bullet)hit(&bulletList,&boss1Bullet)planeHit(ship,&enemy2Bullet)planeHit(ship,&boss2Bullet)planeHit(ship,&boss1Bullet)if boss1!=nil {planeHit(boss1,&bulletList)}
然后几乎所有功能就写完了
(10)游戏的暂停功能
先说游戏如何运行的,我看到的游戏开始就是我们一直在调用Draw()方法,如果让Draw()方法一直画这一瞬间的内容那么游戏就是暂停
而让游戏只画一瞬间便是让你游戏里的所有元素的位置都不再更新,因此我们就需要让Update()里的方法都不再调用
pause()if !state {//pause()return nil}count++//每执行一次Update()刷新次数加一bgInit()shipAdd()bgMove()
三、游戏架构
文件目录
image包存放的图片
hit.go 写的是两种碰撞方法
package mainimport ("math"
)
//两个物体碰撞的方法
func hit(MyList *[]*Object, enemyList *[]*Object) {//假如传的参数为&bulletList &enemy1Listfor i, my := range *MyList {//遍历我方子弹的切片for j, enemy := range *enemyList {//遍历敌方飞机1的切片/*(x1,y1)为某个子弹的中心坐标 (x1,y2)为某个敌方飞机的中心坐标*/x1 := my.X + float64(my.width)/2y1 := my.Y + float64(my.height)/2x2 := enemy.X + float64(enemy.width)/2y2 := enemy.Y + float64(enemy.height)/2//判断是否碰撞的条件if math.Abs(x2-x1) < float64(my.width+enemy.width)/2 && math.Abs(y2-y1) < float64(my.height+enemy.height)/2 {enemy.HP--if enemy.HP<=0{*enemyList = append((*enemyList)[:j], (*enemyList)[j+1:]...)//发生碰撞将该飞机从切片中移除}*MyList = append((*MyList)[:i], (*MyList)[i+1:]...)//发生碰撞后将该子弹从切片中移除score++return}}}
}
//写飞机(我方飞机或boss1)与其他子弹的碰撞func planeHit(plane *Object,slice *[]*Object){for j, sl := range *slice {//遍历敌方飞机1的切片/*(x1,y1)为某个子弹的中心坐标 (x1,y2)为某个敌方飞机的中心坐标*/x1 := plane.X + float64(plane.width)/2y1 := plane.Y + float64(plane.height)/2x2 := sl.X + float64(sl.width)/2y2 := sl.Y + float64(sl.height)/2//判断是否碰撞的条件if math.Abs(x2-x1) < float64(plane.width+sl.width)/2 && math.Abs(y2-y1) < float64(plane.height+sl.height)/2 {*slice = append((*slice)[:j], (*slice)[j+1:]...)//发生碰撞将该元素从切片中移除plane.HP--if plane.HP<=0 {start=!start}return}}}
mian.go 是游戏的主体,负责调用游戏元素的产生移动的方法
package mainimport ("github/hajimehoshi/ebiten/examples/resources/fonts""strconv"//"github/hajimehoshi/ebiten/ebitenutil"//"github/hajimehoshi/ebiten/examples/resources/fonts"//"github/hajimehoshi/ebiten/text"//"golang/x/image/font"//"golang/x/image/font/opentype"//// */"image/color"//. "strconv"//////"log"////"github/hajimehoshi/ebiten/v2"//"github/hajimehoshi/ebiten/v2"_ "github/hajimehoshi/ebiten/v2"_ "github/hajimehoshi/ebiten/v2/ebitenutil""github/hajimehoshi/ebiten/v2/text""golang/x/image/font"_ "golang/x/image/font""golang/x/image/font/opentype""image/color""log"_ "strconv"
)func init() {tt, err := opentype.Parse(fonts.MPlus1pRegular_ttf)if err != nil {log.Fatal(err)}const dpi = 72oneFont, err = opentype.NewFace(tt, &opentype.FaceOptions{Size: 24,DPI: dpi,Hinting: font.HintingVertical,})if err != nil {log.Fatal(err)}
}
//定义了一个Game结构体 实现了游戏引擎里的Game接口
type Game struct{}//下面实现了Game结构体里三个方法 Update() Draw() Layout()//Draw()相当于MVC架构里面的V view 用于渲染显示图形画面
func (g *Game) Draw(screen *ebiten.Image) {if !start { // start=false 游戏未开始 游戏失败 游戏胜利if ship!=nil&&ship.HP<=0 {gameOver(screen)} else if boss1!=nil&&boss1.HP<=0 {victory(screen)}else {startBg(screen)}}else {if bg!=nil {bg.ObjectDraw(screen) //将背景的Draw方法写在Draw的最上面一行,因为在画图时最上面的代码图象在最里面一层}if ship!=nil {ship.ObjectDraw(screen)
}objectSliceDraw(screen, enemy1List)objectSliceDraw(screen, bulletList)objectSliceDraw(screen, enemy2List) //enemy2List 调用Draw方法objectSliceDraw(screen, enemy2Bullet) //enemy2Bullet 调用Draw方法objectSliceDraw(screen, boss2List) //boss2List 调用Draw方法objectSliceDraw(screen, boss2Bullet) //boss2Bullet 调用Draw方法objectSliceDraw(screen, boss1Bullet)if boss1 != nil {boss1.ObjectDraw(screen)}//画出分数str := strconv.Itoa(score)text.Draw(screen, "score:"+str, oneFont, 10, 20, color.White)// text.Draw((screen),"score:"+str, oneFont, 10, 20, color.White)//text.Draw(screen, "score:"+str, oneFont, 10, 20, color.White)}
}
//Update()相当与MVC里面的C control 用于将来控制我们飞机元素的添加和移动
func (g *Game) Update() error {if !start {startGame()return nil}pause()if !state {//pause()return nil}count++//每执行一次Update()刷新次数加一bgInit()shipAdd()bgMove()shipMove()enemy1Add()enemy1Move()bulletAdd()bulletMove()enemy2Add()enemy2BulletAdd()enemy2BulletMove()boss2Add()boss2BulletAdd()boss2BulletMove()Boss1Add()boss1BulletAdd()boss1BulletMove()hit(&bulletList,&enemy1List)//传入我方子弹切片的地址 敌方飞机1的切片地址hit(&bulletList,&enemy2List)hit(&bulletList,&boss2List)hit(&bulletList,&boss2Bullet)hit(&bulletList,&boss1Bullet)planeHit(ship,&enemy2Bullet)planeHit(ship,&boss2Bullet)planeHit(ship,&boss1Bullet)if boss1!=nil {planeHit(boss1,&bulletList)}return nil
}//设置窗口大小 宽600 高800
func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {return 600, 800
}func main() {//设置窗口大小ebiten.SetWindowSize(600, 800)//设置窗口的标题ebiten.SetWindowTitle("飞机大战")/*接着调用 ebiten.RunGame(&Game{}) 启动游戏循环,并将 &Game{} 作为参数传递给 RunGame() 函数以每秒60帧循环执行Game接口的 Update()方法 Draw()方法,以创建并运行游戏对象。如果在运行游戏过程中出现错误,将通过 log.Fatal() 打印错误信息并终止程序的执行。*/if err := ebiten.RunGame(&Game{}); err != nil {log.Fatal(err)}
}
Model.go 写的是 游戏元素的产生移动的方法
package mainimport ("fmt""github/hajimehoshi/ebiten/v2""github/hajimehoshi/ebiten/v2/inpututil""golang/x/image/font"// "github/hajimehoshi/ebiten/v2/ebitenutil". "math/rand""time"
)//声明我方飞机变量
var ship *Object=nil
var oneFont font.Face//初始化我方飞机对象
func shipAdd(){if ship==nil {var myplane Objectmyplane.init("myplane",267,750)ship=&myplane}
}
//飞机根据键盘移动
func shipMove(){//如果按下键盘左方向键我方飞机X减小 画面显示向左移动if ebiten.IsKeyPressed(ebiten.KeyLeft) {ship.X -= 8if ship.X < 0 {//防止飞机飞出窗口左边界ship.X = 0}//如果按下键盘右方向键我方飞机X增大 画面显示向右移动} else if ebiten.IsKeyPressed(ebiten.KeyRight) {ship.X+= 8if ship.X >= 534 {//防止飞机飞出窗口右边界ship.X = 534}//如果按下键盘上方向键我方飞机Y坐标增大 画面显示向上移动} else if ebiten.IsKeyPressed(ebiten.KeyUp) {ship.Y -= 8if ship.Y <= 0 {//防止飞机飞出窗口上边界ship.Y = 0}//如果按下键盘下方向键我方飞机Y减小 画面显示向下移动} else if ebiten.IsKeyPressed(ebiten.KeyDown) {ship.Y += 8if ship.Y >= 750 {//防止飞机飞出窗口下边界ship.Y = 750}}
}
var enemy1List []*Object//声明敌机1的切片用于存储敌机1的对象
func enemy1Add() {Seed(time.Now().UnixNano())//是一个用于设置随机数种子random2 := Intn(200) // 生成0到200之间的随机数if (int)(random2) == 13 {//当生成的随机数为13时添加敌方飞机,也就是说有1/200的概率添加敌方飞机1var enemy1 Objectrandom1 := Intn(550) // 生成0到550之间的随机数 确定敌机生成位置enemy1.init("enemy_new1",float64(random1),0)enemy1List = append(enemy1List,&enemy1 )}}
func enemy1Move(){for _,enemy1 := range enemy1List {//遍历敌方第一钟飞机的切片enemy1.Y++//使敌方飞机的Y坐标增加表现为敌方飞机向下移动}
}
func objectSliceDraw(screen *ebiten.Image, sl []*Object){//当想画出第一种飞机时,sl参数传为ennemy1Listfor _, sli := range sl {//遍历切片var op_slice ebiten.DrawImageOptionsop_slice.GeoM.Translate(sli.X, sli.Y)//根据切片元素的x,y坐标来确定切片的位置screen.DrawImage(sli.image, &op_slice)//画出元素的图片}
}
var bulletList []*Object //定义存储子弹的切片
func bulletAdd() {if inpututil.IsKeyJustReleased(ebiten.KeySpace) {//如果按下空格键就进入产生子弹的代码块// 按下空格子弹根据飞机确定位置var bullet1 Objectif ship.HP >= 5 {//当我方飞机的血量大于等于5时 飞机一次发射两发子弹var bullet2 Objectbullet1.init("bulletplus", ship.X+16, ship.Y)bullet2.init("bulletplus", ship.X+1, ship.Y)bulletList = append(bulletList, &bullet1)bulletList = append(bulletList, &bullet2)} else {//反之就发射一发子弹Ybullet1.init("bulletplus", ship.X+1, ship.Y-80)//ship.X+1 ,ship.Y-80 是子弹的x y 坐标使子弹恰好在飞机中间生成//子弹的坐标为 (ship.X-bullet.X)/2 ship.Y-bullet.YbulletList = append(bulletList, &bullet1)//将新生成的飞机子弹添加到切片bulletList里}}}
func bulletMove(){for _,bullet := range bulletList{bullet.Y-=5}
}
var bg *Object//定义背景的对象func bgInit(){//背景产生的方法if bg==nil {var object Objectobject.init("bg",0,-700)bg=&object}
}
func bgMove() {//背景循环向下移动 营造我方飞机一直向前飞的效果bg.Y++if bg.Y >= -10 {bg.Y = -790}}
var enemy2List []*Object//敌机2的切片
var enemy2Bullet []*Object//敌机2子弹的切片
var count=0//记录游戏的刷新次数
// 添加第二种敌机
func enemy2Add() {Seed(time.Now().UnixNano())random2 := Intn(600) // 生成0到1000之间的随机数if (int)(random2) == 13 {// fmt.Println("画出了敌机2")var e Objectrandom3 := Intn(300)//随机产生敌机2的x坐标e.init("enemy_new2", float64(random3), 120)enemy2List = append(enemy2List, &e)}}// 给第二种敌机添加子弹
func enemy2BulletAdd(){if count%60 == 0 {count++for _, e := range enemy2List {var enemybullet2 Objectenemybullet2.init("enemybullet3", e.X+15, e.Y+67)enemy2Bullet = append(enemy2Bullet, &enemybullet2)}}
}func enemy2BulletMove() {for _, b := range enemy2Bullet {b.Y += 1.5}
}
var boss2List []*Object//boss2的切片
var boss2Bullet []*Object //boss2的子弹切片
func boss2Add() {Seed(time.Now().UnixNano())if count%1000 == 0 &&count!=0{ //20秒左右产生一个boss2count++random4 := Intn(400) // 生成0到200之间的随机数var boss2 Objectboss2.init("boos_new4", float64(random4), 112)boss2List = append(boss2List, &boss2)}
}
func boss2BulletAdd() {if count%200 == 0 {//生成了子弹count++for _, v := range boss2List {var bs2_bullet Objectbs2_bullet.init("enemy_bullet2", v.X+75.5, v.Y+112)boss2Bullet = append(boss2Bullet, &bs2_bullet)}}
}func boss2BulletMove() {for i, b := range boss2Bullet {//改变子弹坐标if ship.Y >= b.Y {//子弹会追踪飞机b.Y += 2} else {b.Y -= 2}if ship.X+float64(ship.width/2) >= b.X {b.X += 2} else {b.X -= 2}if b.Y+float64(b.height)-20 > ship.Y {//如果子弹在飞机的下方 子弹消失boss2Bullet = append(boss2Bullet[:i], boss2Bullet[i+1:]...)break}}
}
var boss1 *Object//定义大boss对象
var boss1Bullet []*Object//定义boss1子弹的切片
var score=0 //定义游戏分数
var boss1_speed=3.0
func Boss1Add() {if score >= 10 && boss1 == nil {Seed(time.Now().UnixNano())var boss Objectx := Intn(359) + 1boss.init("boos_new1", float64(x), 10)boss1 = &boss}if boss1 != nil {//改变boss1的坐标boss1.X += boss1_speedif boss1.X <= 0 || boss1.X >= 360 {boss1_speed = -boss1_speed}}
}// 添加boss1子弹
func boss1BulletAdd() {if count%70 == 0 && boss1 != nil {count++var bos1Bullet Objectbos1Bullet.init("bullet_new2", boss1.X+110, boss1.Y+174)boss1Bullet = append(boss1Bullet, &bos1Bullet)}
}
func boss1BulletMove() {for i, bos1 := range boss1Bullet {bos1.Y += 2.0if bos1.Y > 800 {boss1Bullet = append(boss1Bullet[:i], boss1Bullet[i+1:]...)break}}}
var state = true
var start = false
func pause() {if inpututil.IsKeyJustReleased(ebiten.KeyQ) {fmt.Println(state)state = !state}
}func startGame() {if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {start = trueship=nilshipAdd()score = 0count = 0boss2List = boss2List[:0]enemy2Bullet = enemy2Bullet[:0]boss2Bullet = boss2Bullet[:0]boss1 = nilenemy2List = enemy2List[:0]boss1Bullet = boss1Bullet[:0]enemy1List = enemy1List[:0]bulletList = bulletList[:0]}
}// 游戏开始界面
func startBg(screen *ebiten.Image) {var start Objectstart.init("start1", 0, 0)var op_bullet ebiten.DrawImageOptionsop_bullet.GeoM.Translate(start.X, start.Y)screen.DrawImage(start.image, &op_bullet)
}// 游戏结束
func gameOver(screen *ebiten.Image) {var over Objectover.init("失败01", 22, 150)var op_bullet ebiten.DrawImageOptionsop_bullet.GeoM.Translate(over.X, over.Y)screen.DrawImage(over.image, &op_bullet)}
func victory(screen *ebiten.Image) {var victory Objectvictory.init("奖杯01", 50, 115)var op ebiten.DrawImageOptionsop.GeoM.Translate(victory.X, victory.Y)screen.DrawImage(victory.image, &op)
}
Object.go 里面声明了一个结构体(所有的飞机元素都用这个结构体)里面有初始化,以及绘方法
package mainimport (//"fmt""github/hajimehoshi/ebiten/v2""github/hajimehoshi/ebiten/v2/ebitenutil""log"
)
//planeHp 根据飞机名称映射飞机的生命值
var planeHp = map[string]int{"myplane":3,"boos_new1":10,"boos_new4":5,
}
type Object struct {name string //表示对象的名称image *ebiten.Image //表示对象对应的图片X float64//对象的X坐标Y float64//对象的Y坐标HP int//对象的生命值width int//对象的宽度 将来用于碰撞检测height int//对象的高度
}
func (object *Object) init(name string, X, Y float64) {//对象的初始化方法object.name = nameobject.X = Xobject.Y = Yobject.HP = planeHp[object.name]//从planeHp map集合里映射飞机对象的生命值object.getImg()//调用getImg方法初始化对象的图片}
func (object *Object) getImg() {//使用ebitenutil.NewImageFromFile函数从文件系统中加载图像,并将图像赋值给对象的image字段img, _, err1 := ebitenutil.NewImageFromFile("image/" + object.name+".png")object.width, object.height = img.Size()//初始化对象的宽高if err1 != nil {log.Fatal(err1)}object.image = img
}//对象的图片绘画方法
func (object *Object)ObjectDraw(screen *ebiten.Image){var op_objecte ebiten.DrawImageOptions//ebiten.DrawImageOptions变量,用于配置绘制图像的选项op_objecte.GeoM.Translate(object.X, object.Y)//设置图象的位置使用screen.DrawImage方法将对象的图像绘制到目标图像上,传递了对象的图像object.image和绘制选项&op_objectescreen.DrawImage(object.image, &op_objecte)
}
go在运行上有点特殊须在终端运行go run . 即运行Plane2中的所有文件
运行效果
四、总结
通过做小游戏,熟悉了面向过程的开发,更加熟悉了切片指针的使用,学会了游戏引擎ebiten的使用。
go语言小白,如果有建议欢迎大佬评论
参考链接:一起用Go做一个小游戏(上) - 知乎
源码
飞机大战源码
更多推荐
golang小游戏:飞机大战
发布评论