カジュアルゲームをEbitenで作る

[Go Advent Calendar 2021](https://qiita.com/advent-calendar/2021/go) (カレンダー2) 22日目の記事です。
#はじめに
「気軽にゲームを作る」をテーマに、2DゲームエンジンEbitenで1つ作品を作ります。

EbitenはシンプルなAPIが特徴の、クロスプラットフォームで動く2Dゲームライブラリです。

詳しい機能や学習に役立つサイトなどは、

公式サイト

https://ebiten.org/

や別記事 [production-ready なGo製2Dゲームライブラリ Ebiten の紹介 & リンク集](https://qiita.com/eihigh/items/1fd6077327f99245b807)(@eihighさん) にありますのでぜひご覧ください。
また、本カレンダー13日目にもEbitenの記事　[はじめてのゲームプログラミング（Ebiten）](https://zenn.dev/hokita/articles/66cc118b227ae3)(@hokita222さん)があります。


#プロジェクト構成

go-inovationの構成に準じたものにします。

https://github.com/hajimehoshi/go-inovation


```

　├ game
　│　├── fonts
　│　│     └── mincho.ttf
　│　└── se
　│　│     └── kane.mp3
　│　└── main.go
　│　└── mainScene.go
　│　└── asset.go
　├ main.go
　├ go.mod
　└ go.sum


```
#作ったもの

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">無限年越しそばを作りました。<a href="https://twitter.com/hashtag/golang?src=hash&amp;ref_src=twsrc%5Etfw">#golang</a> <a href="https://t.co/0KGOdJlPeP">pic.twitter.com/0KGOdJlPeP</a></p>&mdash; Ichiban Kunio (@ku20298) <a href="https://twitter.com/ku20298/status/1473646640628375553?ref_src=twsrc%5Etfw">December 22, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

#作る

Ebitenは最低限のAPIを持つので、APIをラップするような形で自分好みに便利な関数を作っていくのが楽しいです。

今回は自前のライブラリ(?) urushiを使います。

https://github.com/ichibankunio/urushi


まずは、最初に呼び出される、`game`ディレクトリ内のゲーム本体をEbiten側にセットする`main.go`です。

```go:main.go
package main

import (
	"log"

	"github.com/hajimehoshi/ebiten/v2"
	"github.com/ku20298/soba/game"	//ゲームの本体のgameディレクトリをインポート
	"github.com/ichibankunio/urushi"

)

func main() {
	ebiten.SetWindowSize(360, 640)

	mainGame := &game.Game {
		UrushiGame: &urushi.Game {	//urushiを使うために初期化
			State: 0,
		},
	}
	if err := ebiten.RunGame(mainGame); err != nil {	//gameのGameインターフェースをebitenにセット
		log.Fatal(err)
	}
}

```

次にゲーム本体を作っていきます。

```go:game/main.go
package game

import (
	"math/rand"
	"time"
	"sync"

	"github.com/hajimehoshi/ebiten/v2"
	"github.com/ichibankunio/urushi"
)

const (
	SCREEN_WIDTH  = 720
	SCREEN_HEIGHT = 1280

	SAMPLE_RATE = 44100
)

type Game struct{
	UrushiGame *urushi.Game
}

const (
	SCENE_MAIN urushi.SceneID = iota
)

func init() {
	rand.Seed(time.Now().UnixNano())
}

var once sync.Once

var mainScene *urushi.Scene

func (g *Game) Update() error {
	once.Do(func() {//一回だけ呼ばれて、それ以降はスルーされる
		g.UrushiGame.AddScene(mainScene)
    })

	err := g.UrushiGame.Update()
	if err != nil {
		return err
	}
	
	return nil
}

func (g *Game) Draw(screen *ebiten.Image) {
	g.UrushiGame.Draw(screen)
}

func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
	return SCREEN_WIDTH, SCREEN_HEIGHT
}
```

`mainScene`を作って`UrushiGame`にセットしています。シーンをセットすると、各シーンのUpdate,Drawをurushi側が実行します。これでシーンごとに別のUpdate, Drawを書けるような仕組みにしています。

urushi.Sceneはこんな感じです。

>```go
type Scene struct {
	Update func(*Game) error
	Draw func(*ebiten.Image)
	ID SceneID
}
```


mainSceneの中身を書く前に、フォントと効果音を読み込みます。

```go:game/asset.go
package game

import (
	"embed"
	"log"
	"bytes"

	"github.com/hajimehoshi/ebiten/v2/audio/mp3"
	"github.com/hajimehoshi/ebiten/v2/audio"
	"github.com/ichibankunio/urushi"
	"golang.org/x/image/font"
)

//go:embed fonts
var fontsDir embed.FS

//go:embed se
var seDir embed.FS

var (
	mainFont font.Face
	smallFont font.Face

	audioContext *audio.Context
	kaneSEByte []byte
)

func init() {
	fontByte, err := fontsDir.ReadFile("fonts/mincho.ttf")
	if err != nil {
		log.Fatal(err)
	}

	mainFont = urushi.NewFontFromBytes(fontByte, 96)
	smallFont = urushi.NewFontFromBytes(fontByte, 48)
}

func init() {
	audioContext = audio.NewContext(SAMPLE_RATE)

	var err error
	kaneSEByte, err = seDir.ReadFile("se/kane.mp3")
	if err != nil {
		log.Fatal(err)
	}
}

func playSE(b []byte) {
	s, err := mp3.DecodeWithSampleRate(SAMPLE_RATE, bytes.NewReader(b))
	if err != nil {
		log.Fatal(err)
	}

	p, err := audioContext.NewPlayer(s)

	if err != nil {
		log.Fatal(err)
	}

	p.Play()
}
```

Go:embedを使って読み込みます。
フォントは[さわらび明朝](https://sawarabi-fonts.osdn.jp/index.html)を[サブセットフォントメーカー](https://opentype.jp/subsetfontmk.htm)でサブセットしたもの、効果音は[DOVA-SYNDROME](https://dova-s.jp/se/play1136.html)からお借りしました。

最後はに`mainScene.go`です。

```go:game/mainScene.go
package game

import (
	"image/color"
	"math/rand"


	"github.com/hajimehoshi/ebiten/v2"
	"github.com/ichibankunio/urushi"
)

var score int

type soba struct {
	bg *urushi.Sprite
	fg *urushi.Sprite
}

func init() {
	var sobas [16]*soba
	var dots [50]*urushi.Sprite

	bgImg := ebiten.NewImage(16, SCREEN_HEIGHT)//まずはebiten.Imageを作る
	bgImg.Fill(color.RGBA{150, 150, 150, 255})
	fgImg := ebiten.NewImage(12, SCREEN_HEIGHT)
	fgImg.Fill(color.RGBA{180, 180, 180, 255})

	for i := 0; i < 16; i++ {//そばが横に並ぶように座標を変えながらSpriteを作って代入
		sobas[i] = &soba{
			// func NewSprite(img *ebiten.Image, x, y float64) *urushi.Sprite
			bg: urushi.NewSprite(bgImg, float64(SCREEN_WIDTH/2 + (i-8)*16), 0),
			fg: urushi.NewSprite(fgImg, float64(SCREEN_WIDTH/2 + (i-8)*16 + 2), 0),
		}
	}

	dotImg := ebiten.NewImage(4, 4)
	dotImg.Fill(color.RGBA{80, 80, 80, 255})

	// 麺の上に黒っぽい粒を配置　ランダムに座標を決める
	for i := 0; i< 50; i++ {
		dots[i] = urushi.NewSprite(dotImg, float64(rand.Intn(len(sobas) * bgImg.Bounds().Dx()) + SCREEN_WIDTH / 2 - len(sobas)/2 * bgImg.Bounds().Dx()), float64(rand.Intn(SCREEN_HEIGHT)))
	}

	// func NewTxtSpr(txt string, x, y float64, clr color.Color, font font.Face, padUp, padLeft int, hidden bool) *urushi.TxtSpr
	titleText := urushi.NewTxtSpr("無限　　　そば", 0, 500, color.White, mainFont, 0, 0, false)
	titleText.SetCenter(SCREEN_WIDTH / 2)

	titleText2 := urushi.NewTxtSpr("年越し", 0, 500, color.Black, mainFont, 0, 0, false)
	titleText2.SetCenter(SCREEN_WIDTH / 2)

	instructionText := urushi.NewTxtSpr("長押しですする", 0, 800, color.White, smallFont, 0, 0, false)
	instructionText.SetCenter(SCREEN_WIDTH / 2)


	mainScene = &urushi.Scene{
		Update: func(g *urushi.Game) error {
			if len(ebiten.TouchIDs()) > 0 || ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {//押されている間は粒を上に動かす
				for i := range dots {
					dots[i].Y -= 2
					if dots[i].Y < 0 {
						dots[i].Y = SCREEN_HEIGHT
					}
				}
				if score % 1000 == 0 {
					playSE(kaneSEByte)
				}

				score ++			
				
				instructionText.Hidden = true

			}else {
				instructionText.Hidden = false

			}
			
			return nil
		},
		Draw: func(screen *ebiten.Image) {
			for _, v := range sobas {
				v.bg.Draw(screen)
				v.fg.Draw(screen)
			}
			
			for _, v := range dots {
				v.Draw(screen)
			}

			titleText.Draw(screen)
			titleText2.Draw(screen)
			instructionText.Draw(screen)
		},
		ID: SCENE_MAIN,
	}
}
```

init関数のみのファイルですが、mainSceneの挙動を記述しています。

画像と座標をセットにして、タッチされたかどうかなどを取得できる`urushi.Sprite`
ebitenのテキスト描画機能に座標の情報やタッチイベントなどを加えた(Text + Spriteな)`urushi.TxtSpr`

を利用しています。

#まとめ
記事を書いてみることで自分のGoやゲームプログラミングの理解がまだまだだと痛感しました。しかもゲームではない。

Ebitenをラップする一例として見ていただければ幸いです。

クソアプリカレンダーでもEbitenで作ったゲームを紹介しているので見てくれたら嬉しいです。

https://qiita.com/ichiban_kunio/items/9be28bbf19c77db826d6