golang学习成果检验,一个简单的星际大战小游戏~~
文章目录
- 一,游戏简介
- 1.功能介绍
- 2.游戏规则
- 3.游戏引擎----ebiten
- 二,部分功能代码详解
- 1.代码构成
- 2.显示窗口
- 3.添加背景图片
- 4.设置配置文件
- 5.添加我方飞机
- 6.使飞船可以移动起来
- 7.添加外星飞船
- 8.让外星飞船动起来
- 9.添加更多的外星飞船
- 10.将移出屏幕的外星飞船删除掉
- 11.我方飞机发射子弹
- 12.添加外星飞船和子弹的碰撞检测方法
- 总结
一,游戏简介
1.功能介绍
在游戏中总共设计了两种人物,分别是我方飞机和敌方外星人飞船。
2.游戏规则
我方飞机可移动并可发射子弹,且最多一次发射20颗;敌方外星人飞船可移动且不断生成,当我方飞机讲屏幕上的所有飞船全部射击即游戏胜利,我方飞机被飞船触碰到即游戏失败
3.游戏引擎----ebiten
要创建游戏界面,需要使用一个图形化界面,这里我使用到的是ebiten,可以直接在终端下载相关包。
go get -u github.com/hajimehoshi/ebiten/v2
二,部分功能代码详解
1.代码构成
在总的工程里面创建一个名为Fighting的包,每个物体对象创建一个file放进包里,game文件中存放游戏的核心逻辑,object文件中存放各种类的初始化结构体;另外为了后期修改方便,将所有可变项单独存放在配置文件中,这里使用json作为配置文件的格式
2.显示窗口
我们的第一步就是需要创建一个游戏窗口,并将其显示出来
- 运行ebiten引擎时需要传入一个对象,并且该对象必须实现ebiten.Game接口
- ebiten.Game接口内定义了三个方法,分别是Draw()、Update()、Layout()
- Draw()方法用于渲染界面,界面上要出现的东西都要在Draw()方法中进行渲染绘画;Draw方法中接收一个screen *ebiten.Image参数,该参数表示GUI窗口显示的对象
- Update()方法用于实时更新界面,界面每个周期都会调用一次Update()进行更新,更新的周期是1/60秒
- Layout()方法的返回值表示显示窗口里面逻辑上屏幕的大小,在本游戏中直接return窗口的长和宽即可
- 以上三个方法都是自调用函数,不需要再手动调用了
因此我们只需要创建一个Game对象,并使该Game对象实现ebiten.Game接口,就可以创建一个游戏窗口啦。接下来就是将要显示在屏幕上的东西放在Draw()方法中即可。
game文件中
package Fighting
import (
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
)
type Game struct {
}
func (g *Game) Draw(screen *ebiten.Image) {
ebitenutil.DebugPrint(screen, "Hello, World")
}
func (g *Game) Update() error {
return nil
}
func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
return 640, 480
}
func NewGame() *Game {
return &Game{}
}
main文件中
package main
import (
"awesomeProject/Fighting"
"log"
"github.com/hajimehoshi/ebiten/v2"
)
func main() {
ebiten.SetWindowSize(640, 480)
ebiten.SetWindowTitle("星际大战")
game := Fighting.NewGame()
if err := ebiten.RunGame(game); err != nil {
log.Fatal(err)
}
}
效果图就是下面这样啦
3.添加背景图片
创建好窗口之后可以逐步向窗口中绘制各种图形了,首先我们先来创建一个背景图片
- 在工程中创建一个image文件夹,存放我们要用到的图片,我们可以将找好的背景图片放入image文件夹中
- 将绘制背景图片的代码放到game.go中的Draw()方法中,即可将背景图片绘制到窗口
- 要绘制图片在窗口上,需要用到ebitenutil.NewImageFromFile()方法
image, _, err := ebitenutil.NewImageFromFile("image/background1.png")
if err != nil {
fmt.Println("背景图片找不到")
}
op := &ebiten.DrawImageOptions{}
screen.DrawImage(image, op)
效果展示~~
同时,如果不想使用图片背景而想使用纯色背景,我们也可以直接使用screen.Fill()方法来直接改变窗口的背景颜色
screen.Fill(color.RGBA{R: 200, G: 200, B: 200, A: 255}
4.设置配置文件
为了方便后期修改代码,我们将代码中的变量拿出来作为配置存在文件中
- 创建config.go文件
- 创建config.json文件
- 填入相应的代码
- 在game文件中定义对象结构
//config.go
package Fighting
import (
"encoding/json"
"image/color"
"log"
"os"
)
type Config struct {
ScreenWidth int `json:"screenWidth"`
ScreenHeight int `json:"screenHeight"`
Title string `json:"title"`
BgColor color.RGBA `json:"bgColor"`
}
func loadConfig() *Config {
f, err := os.Open("./config.json")
if err != nil {
log.Fatalf("os.Open failed: %v\n", err)
}
var cfg Config
err = json.NewDecoder(f).Decode(&cfg)
if err != nil {
log.Fatalf("json.Decode failed: %v\n", err)
}
return &cfg
}
//config.json
{
"screenWidth": 640,
"screenHeight": 480,
"title": "星际大战",
"bgColor": {
"r": 230,
"g": 230,
"b": 230,
"a": 255
}
}
//game.go
type Game struct {
cfg *Config
}
func NewGame() *Game {
cfg := loadConfig()
return &Game{
cfg: cfg,
}
}
//省略其他不需要修改的代码
5.添加我方飞机
- 定义飞船结构体
- 编写绘制自身的方法
- 把飞船添加到游戏对象中
- 绘制飞船到窗口上
新建一个plane.go,写入相关代码
package Fighting
import (
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
_ "golang.org/x/image/bmp"
"log"
)
type Ship struct {
image *ebiten.Image
width int
height int
}
func NewShip() *Ship {
img, _, err := ebitenutil.NewImageFromFile("image/plane1.png")
if err != nil {
log.Fatal(err)
}
width, height := img.Size()
ship := &Ship{
image: img,
width: width,
height: height,
}
return ship
}
//飞船绘制自身的方法
func (ship *Ship) Draw(screen *ebiten.Image, cfg *Config) {
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(float64(cfg.ScreenWidth-ship.width)/2, float64(cfg.ScreenHeight-ship.height))
screen.DrawImage(ship.image, op)
}
在游戏对象中添加飞船类型字段
type Game struct {
cfg *Config
ship *Ship
}
func NewGame() *Game {
cfg := loadConfig()
return &Game{
cfg: cfg,
ship: NewShip(),
}
}
func (g *Game) Draw(screen *ebiten.Image) {
image, _, err := ebitenutil.NewImageFromFile("image/background1.png")
if err != nil {
fmt.Println("背景图片找不到")
}
op := &ebiten.DrawImageOptions{}
screen.DrawImage(image, op)
g.ship.Draw(screen, g.cfg)
}
//省略其他不需要修改的代码
效果展示~~
6.使飞船可以移动起来
将整个窗口看成一个二维坐标,屏幕的左下角即为坐标原点;给飞机添加上左边(x,y值)来表示飞机的位置,通过改变x,y的值来实现飞机移动
- 给飞机添加变量x,y,将飞机的初始位置赋值给x,y
- 添加飞船移动速度变量,控制飞船的移动速度
- 添加键盘监听事件,通过键盘中的左,右键来控制飞船移动
- 控制飞船移动范围,使飞船不能移出屏幕外
type Ship struct{
//省略其他不需要修改的代码
x float64
y float64
}
func NewShip() *Ship {
//省略其他不需要修改的代码
ship := &Ship{
image: img,
width: width,
height: height,
x: float64(screenWidth-width) / 2,
y: float64(screenHeight - height),
}
在config.json中添加飞船速度配置项
//config.json
{
"screenWidth": 640,
"screenHeight": 480,
"title": "星际大战",
"shipSpeedFactor": 3
}
//config.go
type Config struct {
// 省略。。
ShipSpeedFactor float64 `json:"shipSpeedFactor"`
}
创建一个input.go,将以下代码放入,并在Game.Update方法中调用
type Input struct{}
func (i *Input) Update(ship *Ship) {
if ebiten.IsKeyPressed(ebiten.KeyLeft) {
ship.x -= cfg.ShipSpeedFactor
} else if ebiten.IsKeyPressed(ebiten.KeyRight) {
ship.x += cfg.ShipSpeedFactor
}else if ebiten.IsKeyPressed(ebiten.KeyUp) {
g.ship.y -= g.cfg.ShipSpeedFactor
} else if ebiten.IsKeyPressed(ebiten.KeyDown) {
g.ship.y += g.cfg.ShipSpeedFactor
}
}
func (g *Game) Update() error {
g.input.Update(g.ship)
return nil
}
还需要添加一个判断方法,控制飞船的移动范围,这里我是使飞船左,右,下不能移出窗口外,上方不能超过窗口的1/2
修改input中的Update方法
func (i *Input) Update(g *Game) {
//通过键盘的输入控制飞船移动
if ebiten.IsKeyPressed(ebiten.KeyLeft) {
g.ship.x -= g.cfg.ShipSpeedFactor
if g.ship.x < -float64(g.ship.width)/2 {
g.ship.x = -float64(g.ship.width) / 2
}
} else if ebiten.IsKeyPressed(ebiten.KeyRight) {
g.ship.x += g.cfg.ShipSpeedFactor
if g.ship.x > float64(g.cfg.ScreenWidth)-float64(g.ship.width)/2 {
g.ship.x = float64(g.cfg.ScreenWidth) - float64(g.ship.width)/2
}
} else if ebiten.IsKeyPressed(ebiten.KeyUp) {
g.ship.y -= g.cfg.ShipSpeedFactor
if g.ship.y < float64(g.cfg.ScreenHeight)/2 {
g.ship.y = float64(g.cfg.ScreenHeight) / 2
}
} else if ebiten.IsKeyPressed(ebiten.KeyDown) {
g.ship.y += g.cfg.ShipSpeedFactor
if g.ship.y > float64(g.cfg.BulletHeight) {
g.ship.y = float64(g.cfg.ScreenHeight) - float64(g.ship.width)
}
}
}
7.添加外星飞船
- 编写Alien类,添加绘制方法
- 创建一个map,用来存储生成的外星飞船
- 使外星人能够移动
同ship一样,创建外星飞船的类
type Alien struct {
image *ebiten.Image
width int
height int
x float64
y float64
speedFactor float64
}
func NewAlien(cfg *Config) *Alien {
img, _, err := ebitenutil.NewImageFromFile("../images/alien.png")
if err != nil {
log.Fatal(err)
}
width, height := img.Size()
return &Alien{
image: img,
width: width,
height: height,
x: 0,
y: 0,
speedFactor: cfg.AlienSpeedFactor,
}
}
func (alien *Alien) Draw(screen *ebiten.Image) {
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(alien.x, alien.y)
screen.DrawImage(alien.image, op)
}
在游戏开始时先创建两排外星飞船,每个外星飞船之间要留出一部分空位
通过计算判断一行可以容纳多少外星飞船,将他们绘制出来
type Game struct {
// Game结构中的map用来存储外星人对象
aliens map[*Alien]struct{}
}
func NewGame() *Game {
g := &Game{
// 创建map
aliens: make(map[*Alien]struct{}),
}
// 调用 CreateAliens 创建一组外星人
g.CreateAliens()
return g
}
func (g *Game) CreateAliens() {
alien := NewAlien(g.cfg)
availableSpaceX := g.cfg.ScreenWidth - 2*alien.width
numAliens := availableSpaceX / (2 * alien.width)
for row := 0; row < 2; row++ {
for i := 0; i < numAliens; i++ {
alien = NewAlien(g.cfg)
alien.x = float64(alien.width + 2*alien.width*i)
alien.y = float64(alien.height*row) * 1.5
g.addAlien(alien)
}
}
}
创建添加飞船的方法
//alien.go
//省略其他不需要修改的代码
func (g *Game) addAlien(alien *Alien) {
g.aliens[alien] = struct{}{}
}
在Game.Draw方法中添加绘制外星飞船的方法
func (g *Game) Draw(screen *ebiten.Image) {
//省略其他不需要修改的代码
for alien := range g.aliens {
alien.Draw(screen)
}
}
8.让外星飞船动起来
让外星飞船动起来,在Game.Update方法中更新位置
首先需要在config.json中添加外星飞船的移动速度,这里移动速度分为x方向和y方向.
//config.json
{
"alienSpeedFactor" : 2,//y方向的速度
"speedX": 1,//x方向的速度
}
//config.go
type Config struct {
AlienSpeedFactor float64 `json:"alienSpeedFactor"`
SpeedX float64 `json:"speedX"`
}
func (g *Game) Update() error {
//省略其他不需要修改的代码
for alien := range g.aliens {
alien.y += alien.speedFactor
alien.x += alien.speedX
if alien.x >= 460 {
alien.speedX = -alien.speedX
}
}
//省略其他不需要修改的代码
}
9.添加更多的外星飞船
如果只在游戏开始时创建两行飞船的话游戏就会结束的过早,为了更好的游戏体验,我们可以在后期添加更多的外星人。我的想法是每过1秒钟就添加一个新的外星飞船。
- 在alien.go中创建一个Check方法,用来生成更多的外星飞船。
- 在随机位置生成新的外星人
- 要实现在随机位置生成,这里使用的方法是随机数。在指定范围内生成两个随机数,将这两个随机数分别赋值给alien.x和alien.y,就可以实现这个功能。
- 接下来就是调用上面写好的addAlien()方法,传入alien对象即可
//alien.go
//省略其他不需要修改的代码
func (g *Game) Check() {
//生成两个随机数
rand.Seed(time.Now().UnixNano())
randomNumber1 := rand.Intn(580)
randomNumber2 := rand.Intn(211)
alien := NewAlien(g.cfg)
alien.x = float64(randomNumber1)
alien.y = float64(randomNumber2)
g.addAlien(alien)
}
- 添加定时器,将Check方法定时调用
- 这里定的时间是1秒,即1*time.Second
timer := time.NewTicker(time.Second)
tickerChan := timer.C
go func() {
for {
select {
case <-tickerChan:
game.Check()
}
}
}()
10.将移出屏幕的外星飞船删除掉
在alien.go中创建outOfScreen()方法,判断飞船是否移除了屏幕
//省略其他不需要修改的代码
func (alien *Alien) outOfScreen(cfg *Config) bool {
return alien.y > float64(cfg.ScreenHeight)
}
在game.Update()方法中调用该方法进行判断,满足条件的进行删除
//省略其他不需要修改的代码
for alien := range g.aliens {
alien.y += alien.speedFactor
alien.x += alien.speedX
if alien.x >= 460 {
alien.speedX = -alien.speedX
}
if alien.outOfScreen(g.cfg) {
delete(g.aliens, alien)
continue
}
}
11.我方飞机发射子弹
- 设置子弹的相关配置
- 创建bullet.go,编写Bullet类,添加绘制方法
- 创建一个map存储生成的子弹
- 将子弹绘制到窗口
- 删除移出屏幕外的子弹
这里子弹是直接画一个矩形来表示
{
"bulletWidth": 3,
"bulletHeight": 10,
"bulletSpeedFactor": 4,
"bulletColor": {
"r": 30,
"g": 31,
"b": 34,
"a": 255
}
}
定义一个子弹的结构类型,由于子弹在飞机头部发射,子弹的左标直接通过飞机坐标和飞机宽高来表示
type Bullet struct {
image *ebiten.Image
width int
height int
x float64
y float64
speedFactor float64
}
func NewBullet(cfg *Config, ship *Ship) *Bullet {
rect := image.Rect(0, 0, cfg.BulletWidth, cfg.BulletHeight)
img := ebiten.NewImageWithOptions(rect, nil)
img.Fill(cfg.BulletColor)
return &Bullet{
image: img,
width: cfg.BulletWidth,
height: cfg.BulletHeight,
x: ship.x + float64(ship.width/2),
y: ship.y - float64(ship.height/2),
speedFactor: cfg.BulletSpeedFactor,
}
}
func (bullet *Bullet) Draw(screen *ebiten.Image) {
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(bullet.x, bullet.y)
screen.DrawImage(bullet.image, op)
}
创建一个Bullet类型的map
type Game struct {
//省略
bullets map[*Bullet]struct{}
}
func NewGame() *Game {
return &Game{
// 省略
bullets: make(map[*Bullet]struct{}),
}
}
创建一个添加子弹的方法
func (g *Game) addBullet(bullet *Bullet) {
g.bullets[bullet] = struct{}{}
}
要想在窗口上显示子弹,需要添加空格键的监听事件,当按下空格键就将子弹绘制出来
同时需要控制两个子弹发射之间的时间间隔,在.json文件中添加bulletInterval字段进行控制
func (i *Input) Update(g *Game) {
//省略其他不需要修改的代码
if ebiten.IsKeyPressed(ebiten.KeySpace) {
bullet := NewBullet(g.cfg, g.ship)
g.addBullet(bullet)
}
}
{
"bulletInterval": 50
}
type Config struct {
BulletInterval int64 `json:"bulletInterval"`
}
添加lastBulletTime来记录上一次发射子弹的时间,通过判断,当间隔时间大于bulletInterval时才能再一次发射子弹
type Input struct {
lastBulletTime time.Time
}
func (i *Input) Update(g *Game) {
//省略其他不需要修改的代码
if ebiten.IsKeyPressed(ebiten.KeySpace) {
if len(g.bullets) < g.cfg.MaxBulletNum &&
time.Now().Sub(i.lastBulletTime).Milliseconds() > g.cfg.BulletInterval {
bullet := NewBullet(g.cfg, g.ship)
g.addBullet(bullet)
i.lastBulletTime = time.Now()
}
}
}
删除移出屏幕的子弹,与移除外星人方法一样,这里就不多叙述了
12.添加外星飞船和子弹的碰撞检测方法
创建一个collision.go来存放碰撞检测方法
// CheckCollision 检查子弹和外星人之间是否有碰撞
func CheckCollision(bullet *Bullet, alien *Alien) bool {
alienTop, alienLeft := alien.y, alien.x
alienBottom, alienRight := alien.y+float64(alien.height), alien.x+float64(alien.width)
// 左上角
x, y := bullet.x, bullet.y
if y > alienTop && y < alienBottom && x > alienLeft && x < alienRight {
return true
}
// 右上角
x, y = bullet.x+float64(bullet.width), bullet.y
if y > alienTop && y < alienBottom && x > alienLeft && x < alienRight {
return true
}
// 左下角
x, y = bullet.x, bullet.y+float64(bullet.height)
if y > alienTop && y < alienBottom && x > alienLeft && x < alienRight {
return true
}
// 右下角
x, y = bullet.x+float64(bullet.width), bullet.y+float64(bullet.height)
if y > alienTop && y < alienBottom && x > alienLeft && x < alienRight {
return true
}
return false
}
接着我们在Game.Update方法中调用这个方法,并且将碰撞的子弹和外星人删除。
func (g *Game) CheckCollision() {
for alien := range g.aliens {
for bullet := range g.bullets {
if CheckCollision(bullet, alien) {
delete(g.aliens, alien)
delete(g.bullets, bullet)
}
}
}
}
func (g *Game) Update() error {
// -------省略-------
g.CheckCollision()
// -------省略-------
return nil
}
总结
至此,游戏的基本功能都已介绍完毕,在此基础上还可以添加游戏状态等其他功能
go语言小白,如果有更好的建议欢迎评论区留言~~
参考: