用Go写一个中国象棋(三)| Ebiten加载资源
Ebiten加载资源的方式是通过把资源文件,例如png、mp3文件等转换成为byte数组,然后在程序中直接读取即可。
这样的好处是资源文件可以在任何目录下工作,包括浏览器。
file2byteslice
Ebiten官方已经出来一个工具专门用来转化资源文件。
项目地址:https://github.com/hajimehoshi/file2byteslice
使用命令把项目克隆下来:
git clone https://github.com/hajimehoshi/file2byteslice
然后进入/cmd/file2byteslice目录,在CMD下面执行命令:
go build -o file2byteslice.exe
生成exe之后就可以直接使用下面的命令把资源文件转化成对应的byte数组
file2byteslice -input INPUT_FILE -output OUTPUT_FILE -package PACKAGE_NAME -var VARIABLE_NAME
INPUT_FILE 输入的资源文件
OUTPUT_FILE 输入的Go文件
PACKAGE_NAME 包名
VARIABLE_NAME 变量名
还等什么!赶紧把exe文件拷到res目录下试一下,在CMD下面执行命令:
file2byteslice.exe -input ChessBoard.png -output ChessBoard.go -package res -var ImgChessBoard
执行完成之后就可以看到res目录下生成了一个ChessBoard.go的文件,里面定义了一个ImgChessBoard的变量,以后在代码中就直接用ImgChessBoard表示这张图片了。
自定义filetobyte
在使用file2byteslice的时候,会发现几个问题:
- 一个个文件去转换太麻烦
- 每个资源都会生成一个Go文件
- 能不能用一个map保存所有的资源呢?
带着这些问题,充分发挥了主观能动性造了一个轮子,代码如下:
//FileToByte 把文件转成Byte数组
func FileToByte(inPath, outPath string) error {
dir, err := ioutil.ReadDir(inPath)
if err != nil {
return err
}
fOut, err := os.Create(outPath + "/resources.go")
if err != nil {
return err
}
defer fOut.Close()
//写入包名
if _, err := fmt.Fprintf(fOut, "package chess\n\n"); err != nil {
return err
}
//初始化map
if _, err := fmt.Fprintf(fOut, "var resMap = map[int][]byte {\n"); err != nil {
return err
}
//目录下所有文件
for _, fIn := range dir {
//生成变量名
varName := ""
if ok := strings.HasSuffix(fIn.Name(), ".png"); ok {
varName = "Img" + strings.TrimSuffix(fIn.Name(), ".png")
} else if ok := strings.HasSuffix(fIn.Name(), ".wav"); ok {
varName = "Music" + strings.TrimSuffix(fIn.Name(), ".wav")
} else {
continue
}
//打开输入文件
f, err := os.Open(inPath + "/" + fIn.Name())
if err != nil {
return err
}
defer f.Close()
bs, err := ioutil.ReadAll(f)
if err != nil {
return err
}
//写入输出文件
if _, err := fmt.Fprintf(fOut, " %s : []byte(%q),\n", varName, bs); err != nil {
return err
}
}
if _, err := fmt.Fprintln(fOut, "}"); err != nil {
return err
}
return nil
}
下面来一步步分析下这个代码。
FileToByte分析
要解决上面三个问题,其实就是要定义一个resources.go文件,这个文件初始化了一个map[int][]byte,把每个资源文件转化后的byte数组用key-value存起来。
//ReadDir就是打开inPath目录,dir里面保存了目录下面所有文件的信息。 dir, err := ioutil.ReadDir(inPath)
//创建输出文件resources.go。 fOut, err := os.Create(outPath + "/resources.go")
//遍历目录下的资源文件。 for _, fIn := range dir
//这是字符串截取,用来获取文件名。 strings.HasSuffix(fIn.Name(), ".png")
//os.Open打开文件,ioutil.ReadAll读取文件里面的所有内容。 f, err := os.Open(inPath + "/" + fIn.Name()) bs, err := ioutil.ReadAll(f)
//把内容写入输出文件,即resources.go里面。 fmt.Fprintf(fOut, " %s : []byte(%q),\n", varName, bs)
生成resources.go
注释掉main函数下的代码,调用上面的FileToByte接口:
package main
import (
"ChineseChess/res"
)
func main() {
//chess.NewGame()
res.FileToByte("./res", "./chess")
}
F5执行之后,可以看到在chess目录下面生成了一个resources.go文件,里面定义了resMap。但是这个时候还有错误,因为我们还没有定义这些常量。
define.go
在chess目录下面新建一个define.go文件,这个文件主要用来放一些常量定义,通用接口等。
打开文件,复制下面的代码进去:
package chess
const (
//ImgChessBoard 棋盘
ImgChessBoard = 1
//ImgSelect 选中
ImgSelect = 2
//ImgRedShuai 红帅
ImgRedShuai = 8
//ImgRedShi 红士
ImgRedShi = 9
//ImgRedXiang 红相
ImgRedXiang = 10
//ImgRedMa 红马
ImgRedMa = 11
//ImgRedJu 红车
ImgRedJu = 12
//ImgRedPao 红炮
ImgRedPao = 13
//ImgRedBing 红兵
ImgRedBing = 14
//ImgBlackJiang 黑将
ImgBlackJiang = 16
//ImgBlackShi 黑士
ImgBlackShi = 17
//ImgBlackXiang 黑相
ImgBlackXiang = 18
//ImgBlackMa 黑马
ImgBlackMa = 19
//ImgBlackJu 黑车
ImgBlackJu = 20
//ImgBlackPao 黑炮
ImgBlackPao = 21
//ImgBlackBing 黑兵
ImgBlackBing = 22
)
const (
//MusicSelect 选子
MusicSelect = 100
//MusicPut 落子
MusicPut = 101
//MusicEat 吃子
MusicEat = 102
//MusicJiang 将军
MusicJiang = 103
//MusicGameWin 胜利
MusicGameWin = 104
//MusicGameLose 失败
MusicGameLose = 105
)
这样定义的好处是判断棋子的颜色非常简单:(ImgRedShi & 8) != 0 表示红方的棋子,(ImgBlackShi & 16) != 0 表示黑方的棋子。
loadResource
在Game的结构体中新增2个map保存图片和音效,代码如下:
type Game struct {
images map[int]*ebiten.Image //图片资源
audios map[int]*audio.Player //音效
audioContext *audio.Context //音效器
}
在game.go中新建函数loadResource,这个函数的作用就是把resMap中的二进制内容解码存入到Ebiten对应的结构体中,内容如下:
//loadResource 加载资源
func (g *Game) loadResource() bool {
for k, v := range resMap {
if k >= MusicSelect {
//加载音效
d, err := wav.Decode(g.audioContext, audio.BytesReadSeekCloser(v))
if err != nil {
fmt.Print(err)
return false
}
player, err := audio.NewPlayer(g.audioContext, d)
if err != nil {
fmt.Print(err)
return false
}
g.audios[k] = player
} else {
//加载图片
img, _, err := image.Decode(bytes.NewReader(v))
if err != nil {
fmt.Print(err)
return false
}
ebitenImage, _ := ebiten.NewImageFromImage(img, ebiten.FilterDefault)
g.images[k] = ebitenImage
}
}
return true
}
//playAudio 播放音效
func (g *Game) playAudio(value int) {
if player, ok := g.audios[value]; ok {
player.Rewind()
player.Play()
}
}
NewPlayerFromBytes 把二进制音效内容转换成Ebiten的player结构。
image.Decode 把二进制图片解码成Image。
ebiten.NewImageFromImage 把Image转换成Ebiten的图片结构。
在保存成功之后,编辑器会自动import所需要的包,但是这里需要注意:因为我们用的是png,所以需要手动添加png包:
import _ "image/png"
在下一节中,我们将学习如何定义棋局的基本数据结构。