当前位置: 首页>编程语言>正文

go语言有什么游戏框架吗 go语言游戏引擎

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.代码构成

go语言有什么游戏框架吗 go语言游戏引擎,go语言有什么游戏框架吗 go语言游戏引擎_json,第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)
	}
}

效果图就是下面这样啦

go语言有什么游戏框架吗 go语言游戏引擎,go语言有什么游戏框架吗 go语言游戏引擎_go语言有什么游戏框架吗_02,第2张

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)

效果展示~~

go语言有什么游戏框架吗 go语言游戏引擎,go语言有什么游戏框架吗 go语言游戏引擎_go语言有什么游戏框架吗_03,第3张

同时,如果不想使用图片背景而想使用纯色背景,我们也可以直接使用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)
}
//省略其他不需要修改的代码

效果展示~~

go语言有什么游戏框架吗 go语言游戏引擎,go语言有什么游戏框架吗 go语言游戏引擎_json_04,第4张

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语言小白,如果有更好的建议欢迎评论区留言~~
参考:



https://www.xamrdz.com/lan/5vr1963809.html

相关文章: