目录

一、准备阶段:游戏引擎介绍准

安装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.com/hajimehoshi/ebiten/v2/examples/2048@latest

 打开窗口

创建main.go

package main

import (
	/*这段代码首先导入了必要的包,其中包括 github.com/hajimehoshi/ebiten/v2 和
	github.com/hajimehoshi/ebiten/v2/ebitenutil。这些包提供了 Ebiten 游戏引擎的功能和实用工具。
	 */

	"log"

	"github.com/hajimehoshi/ebiten/v2"
	"github.com/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 main

import (
	//"fmt"
	"github.com/hajimehoshi/ebiten/v2"
	"github.com/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 = name
	object.X = X
	object.Y = Y
	object.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_objecte
	screen.DrawImage(object.image, &op_objecte)
}

 创建一个Model类

Model用于写飞机的模型 和后续飞机的移动

package main

//声明我方飞机变量
var   ship *Object=nil


//初始化我方飞机对象
func shipAdd(){
	if ship==nil {
		var myplane Object
		myplane.init("myplane",267,750)
		ship=&myplane
	}
}

   将飞机的shipAdd()方法添加到main.go的Update()方法里,并在main.go方法里用ship调用objectDraw()方法

       

package main

import (
	/*这段代码首先导入了必要的包,其中包括 github.com/hajimehoshi/ebiten/v2 和
	github.com/hajimehoshi/ebiten/v2/ebitenutil。这些包提供了 Ebiten 游戏引擎的功能和实用工具。
	 */

	"log"

	"github.com/hajimehoshi/ebiten/v2"
	"github.com/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, Plane
    ship.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 main

import (
	"github.com/hajimehoshi/ebiten/v2"
)

//声明我方飞机变量
var   ship *Object=nil


//初始化我方飞机对象
func shipAdd(){
	if ship==nil {
		var myplane Object
		myplane.init("myplane",267,750)
		ship=&myplane
	}
}
//飞机根据键盘移动
func shipMove(){
	//如果按下键盘左方向键我方飞机X减小 画面显示向左移动 
	if ebiten.IsKeyPressed(ebiten.KeyLeft) {

		ship.X -= 8
		if ship.X < 0 {//防止飞机飞出窗口左边界
			ship.X = 0
		}//如果按下键盘右方向键我方飞机X增大 画面显示向右移动 
	} else if ebiten.IsKeyPressed(ebiten.KeyRight) {
		ship.X+= 8
		if ship.X >= 534 {//防止飞机飞出窗口右边界
			ship.X = 534
		}//如果按下键盘上方向键我方飞机Y坐标增大 画面显示向上移动 
	}  else if ebiten.IsKeyPressed(ebiten.KeyUp) {
		ship.Y -= 8
		if ship.Y <= 0 {//防止飞机飞出窗口上边界
			ship.Y = 0
		}//如果按下键盘下方向键我方飞机Y减小 画面显示向下移动 
	} else if ebiten.IsKeyPressed(ebiten.KeyDown) {
		ship.Y += 8
		if 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 Object
		random1 := 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参数传为ennemy1List
	for _, sli := range sl {//遍历切片
		var op_slice ebiten.DrawImageOptions
		op_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, Plane
    ship.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 Object

		if ship.HP >= 5 {//当我方飞机的血量大于等于5时 飞机一次发射两发子弹
			var bullet2 Object
			bullet1.init("bulletplus", ship.X+16, ship.Y)
			bullet2.init("bulletplus", ship.X+1, ship.Y)
			bulletList = append(bulletList, &bullet1)
			bulletList = append(bulletList, &bullet2)
		} else {//反之就发射一发子弹Y
			bullet1.init("bulletplus", ship.X+1, ship.Y-80)//ship.X+1  ,ship.Y-80 是子弹的x y 坐标使子弹恰好在飞机中间生成
			//子弹的坐标为 (ship.X-bullet.X)/2   ship.Y-bullet.Y
			bulletList = 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, Plane
    ship.ObjectDraw(screen)
	objectSliceDraw(screen,enemy1List)
	objectSliceDraw(screen,bulletList)

}

运行效果 

(5)添加碰撞检测的方法

新建hit.go里面写hit()方法

package main

import "math"
//两个物体碰撞的方法
func hit(MyList *[]*Object, enemyList *[]*Object) {//假如传的参数为&bulletList  &enemy1List
	for i, my := range *MyList {//遍历我方子弹的切片
		for j, enemy := range *enemyList {//遍历敌方飞机1的切片
			/*
			(x1,y1)为某个子弹的中心坐标  (x1,y2)为某个敌方飞机的中心坐标
			 */
			x1 := my.X + float64(my.width)/2
			y1 := my.Y + float64(my.height)/2
			x2 := enemy.X + float64(enemy.width)/2
			y2 := 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 Object
		object.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.go

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 Object
		random3 := 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 Object
			enemybullet2.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.go

var boss2List []*Object//boss2的切片
var boss2Bullet []*Object //boss2的子弹切片
func boss2Add() {
	Seed(time.Now().UnixNano())
	if count%1000 == 0 &&count!=0{ //20秒左右产生一个boss2
		count++
		random4 := Intn(400) // 生成0到200之间的随机数
		var boss2 Object
		boss2.init("boos_new4", float64(random4), 112)
		boss2List = append(boss2List, &boss2)
	}
}
func boss2BulletAdd() {
	if count%200 == 0 {
		//生成了子弹
		count++
		for _, v := range boss2List {
			var bs2_bullet Object
			bs2_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 Object
		x := Intn(359) + 1
		boss.init("boos_new1", float64(x), 10)
		boss1 = &boss
	}
	if boss1 != nil {
		//改变boss1的坐标
		boss1.X += boss1_speed
		if boss1.X <= 0 || boss1.X >= 360 {
			boss1_speed = -boss1_speed
		}

	}
}

// 添加boss1子弹
func boss1BulletAdd() {

	if count%70 == 0 && boss1 != nil {
		count++
		var bos1Bullet Object
		bos1Bullet.init("bullet_new2", boss1.X+110, boss1.Y+174)
		boss1Bullet = append(boss1Bullet, &bos1Bullet)
	}
}
func boss1BulletMove() {

	for i, bos1 := range boss1Bullet {
		bos1.Y += 2.0

		if 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)/2
		y1 := plane.Y + float64(plane.height)/2
		x2 := sl.X + float64(sl.width)/2
		y2 := 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 main

import (
	"math"
)
//两个物体碰撞的方法
func hit(MyList *[]*Object, enemyList *[]*Object) {//假如传的参数为&bulletList  &enemy1List
	for i, my := range *MyList {//遍历我方子弹的切片
		for j, enemy := range *enemyList {//遍历敌方飞机1的切片
			/*
			(x1,y1)为某个子弹的中心坐标  (x1,y2)为某个敌方飞机的中心坐标
			 */
			x1 := my.X + float64(my.width)/2
			y1 := my.Y + float64(my.height)/2
			x2 := enemy.X + float64(enemy.width)/2
			y2 := 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)/2
		y1 := plane.Y + float64(plane.height)/2
		x2 := sl.X + float64(sl.width)/2
		y2 := 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 main

import (
	"github.com/hajimehoshi/ebiten/examples/resources/fonts"
	"strconv"

	//"github.com/hajimehoshi/ebiten/ebitenutil"
	//"github.com/hajimehoshi/ebiten/examples/resources/fonts"
	//"github.com/hajimehoshi/ebiten/text"
	//"golang.org/x/image/font"
	//"golang.org/x/image/font/opentype"
	//
	// */"image/color"
	//. "strconv"
	//
	//
	//"log"
	//
	//"github.com/hajimehoshi/ebiten/v2"
	//
	"github.com/hajimehoshi/ebiten/v2"
	_ "github.com/hajimehoshi/ebiten/v2"
	_ "github.com/hajimehoshi/ebiten/v2/ebitenutil"
	"github.com/hajimehoshi/ebiten/v2/text"
	"golang.org/x/image/font"
	_ "golang.org/x/image/font"
	"golang.org/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 = 72
	oneFont, 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 main

import (
	"fmt"
	"github.com/hajimehoshi/ebiten/v2"
	"github.com/hajimehoshi/ebiten/v2/inpututil"
	"golang.org/x/image/font"

	// "github.com/hajimehoshi/ebiten/v2/ebitenutil"
	. "math/rand"
	"time"
)

//声明我方飞机变量
var   ship *Object=nil
var oneFont           font.Face

//初始化我方飞机对象
func shipAdd(){
	if ship==nil {
		var myplane Object
		myplane.init("myplane",267,750)
		ship=&myplane
	}
}
//飞机根据键盘移动
func shipMove(){
	//如果按下键盘左方向键我方飞机X减小 画面显示向左移动
	if ebiten.IsKeyPressed(ebiten.KeyLeft) {

		ship.X -= 8
		if ship.X < 0 {//防止飞机飞出窗口左边界
			ship.X = 0
		}//如果按下键盘右方向键我方飞机X增大 画面显示向右移动
	} else if ebiten.IsKeyPressed(ebiten.KeyRight) {
		ship.X+= 8
		if ship.X >= 534 {//防止飞机飞出窗口右边界
			ship.X = 534
		}//如果按下键盘上方向键我方飞机Y坐标增大 画面显示向上移动
	}  else if ebiten.IsKeyPressed(ebiten.KeyUp) {
		ship.Y -= 8
		if ship.Y <= 0 {//防止飞机飞出窗口上边界
			ship.Y = 0
		}//如果按下键盘下方向键我方飞机Y减小 画面显示向下移动
	} else if ebiten.IsKeyPressed(ebiten.KeyDown) {
		ship.Y += 8
		if 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的概率添加敌方飞机1
		var enemy1 Object
		random1 := 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参数传为ennemy1List
	for _, sli := range sl {//遍历切片
		var op_slice ebiten.DrawImageOptions
		op_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 Object

		if ship.HP >= 5 {//当我方飞机的血量大于等于5时 飞机一次发射两发子弹
			var bullet2 Object
			bullet1.init("bulletplus", ship.X+16, ship.Y)
			bullet2.init("bulletplus", ship.X+1, ship.Y)
			bulletList = append(bulletList, &bullet1)
			bulletList = append(bulletList, &bullet2)
		} else {//反之就发射一发子弹Y
			bullet1.init("bulletplus", ship.X+1, ship.Y-80)//ship.X+1  ,ship.Y-80 是子弹的x y 坐标使子弹恰好在飞机中间生成
			//子弹的坐标为 (ship.X-bullet.X)/2   ship.Y-bullet.Y
			bulletList = append(bulletList, &bullet1)//将新生成的飞机子弹添加到切片bulletList里
		}

	}

}
func bulletMove(){
	for _,bullet := range bulletList{
		bullet.Y-=5
	}
}
var bg *Object//定义背景的对象

func bgInit(){//背景产生的方法
	if bg==nil {
		var object Object
		object.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 Object
		random3 := 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 Object
			enemybullet2.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秒左右产生一个boss2
		count++
		random4 := Intn(400) // 生成0到200之间的随机数
		var boss2 Object
		boss2.init("boos_new4", float64(random4), 112)
		boss2List = append(boss2List, &boss2)
	}
}
func boss2BulletAdd() {
	if count%200 == 0 {
		//生成了子弹
		count++
		for _, v := range boss2List {
			var bs2_bullet Object
			bs2_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 Object
		x := Intn(359) + 1
		boss.init("boos_new1", float64(x), 10)
		boss1 = &boss
	}
	if boss1 != nil {
		//改变boss1的坐标
		boss1.X += boss1_speed
		if boss1.X <= 0 || boss1.X >= 360 {
			boss1_speed = -boss1_speed
		}

	}
}

// 添加boss1子弹
func boss1BulletAdd() {

	if count%70 == 0 && boss1 != nil {
		count++
		var bos1Bullet Object
		bos1Bullet.init("bullet_new2", boss1.X+110, boss1.Y+174)
		boss1Bullet = append(boss1Bullet, &bos1Bullet)
	}
}
func boss1BulletMove() {

	for i, bos1 := range boss1Bullet {
		bos1.Y += 2.0

		if 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 = true
		ship=nil
		shipAdd()
		score = 0
		count = 0
		boss2List = boss2List[:0]

		enemy2Bullet = enemy2Bullet[:0]
		boss2Bullet = boss2Bullet[:0]
		boss1 = nil

		enemy2List = enemy2List[:0]
		boss1Bullet = boss1Bullet[:0]
		enemy1List = enemy1List[:0]
		bulletList = bulletList[:0]
	}
}

// 游戏开始界面
func startBg(screen *ebiten.Image) {
	var start Object
	start.init("start1", 0, 0)
	var op_bullet ebiten.DrawImageOptions
	op_bullet.GeoM.Translate(start.X, start.Y)
	screen.DrawImage(start.image, &op_bullet)
}

// 游戏结束
func gameOver(screen *ebiten.Image) {
	var over Object
	over.init("失败01", 22, 150)
	var op_bullet ebiten.DrawImageOptions
	op_bullet.GeoM.Translate(over.X, over.Y)
	screen.DrawImage(over.image, &op_bullet)

}
func victory(screen *ebiten.Image) {
	var victory Object
	victory.init("奖杯01", 50, 115)
	var op ebiten.DrawImageOptions
	op.GeoM.Translate(victory.X, victory.Y)
	screen.DrawImage(victory.image, &op)
}

Object.go 里面声明了一个结构体(所有的飞机元素都用这个结构体)里面有初始化,以及绘方法

package main

import (
	//"fmt"
	"github.com/hajimehoshi/ebiten/v2"
	"github.com/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 = name
	object.X = X
	object.Y = Y
	object.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_objecte
	screen.DrawImage(object.image, &op_objecte)
}
go在运行上有点特殊须在终端运行go run .  即运行Plane2中的所有文件

 运行效果

四、总结

通过做小游戏,熟悉了面向过程的开发,更加熟悉了切片指针的使用,学会了游戏引擎ebiten的使用。

 go语言小白,如果有建议欢迎大佬评论

参考链接:一起用Go做一个小游戏(上) - 知乎

源码

飞机大战源码

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐