LoginSignup
3
2

More than 1 year has passed since last update.

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

Posted at

Go Advent Calendar 2021 (カレンダー2) 22日目の記事です。

はじめに

「気軽にゲームを作る」をテーマに、2DゲームエンジンEbitenで1つ作品を作ります。

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

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

公式サイト

や別記事 production-ready なGo製2Dゲームライブラリ Ebiten の紹介 & リンク集(@eihighさん) にありますのでぜひご覧ください。
また、本カレンダー13日目にもEbitenの記事 はじめてのゲームプログラミング(Ebiten)(@hokita222さん)があります。

プロジェクト構成

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


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


作ったもの

作る

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

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

まずは、最初に呼び出される、gameディレクトリ内のゲーム本体をEbiten側にセットするmain.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)
    }
}

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

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はこんな感じです。

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

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

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を使って読み込みます。
フォントはさわらび明朝サブセットフォントメーカーでサブセットしたもの、効果音はDOVA-SYNDROMEからお借りしました。

最後はにmainScene.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で作ったゲームを紹介しているので見てくれたら嬉しいです。

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2