Help us understand the problem. What is going on with this article?

Ebitenの個人的まとめ

Ebiten

EbitenとはGo言語のシンプルな2Dゲームライブラリです。
自動テクスチャアトラス化、自動バッチ処理などで大量のスプライトを高速に描画できます。
WindowsではCgoが不要。

テスト環境

Go 1.12.5
Ebiten 1.9.3
Windows10

ウィンドウ

ウィンドウを表示させる

main.go
package main

import (
    "log"

    "github.com/hajimehoshi/ebiten"
    "github.com/hajimehoshi/ebiten/ebitenutil"
)

func update(screen *ebiten.Image) error {
    if ebiten.IsDrawingSkipped() {
        return nil
    }
    ebitenutil.DebugPrint(screen, "Hello, World!")
    return nil
}

func main() {
    if err := ebiten.Run(update, 320, 240, 2, "Hello, World!"); err != nil {
        log.Fatal(err)
    }
}

func ebiten.Run

コールバック関数とウィンドウの幅、高さ、スケーリング、タイトルを設定している。
1秒間にあらかじめ設定されている回数update関数が呼び出されることになる。

ebiten.Run(update, 320, 240, 2, "Hello, World!")

第1引数のupdate関数はレシーバでもいい。

type Game struct {
    hoge Hoge
}

func (g *Game) update(screen *ebiten.Image) error {
    if ebiten.IsDrawingSkipped() {
        return nil
    }
    return nil
}

var game *Game

func main() {
    if err := ebiten.Run(game.update, 320, 240, 2, "Game"); err != nil {
        log.Fatal(err)
    }
}

func ebiten.IsDrawingSkipped

この関数より上に更新の度にしたい計算等の処理を書き。この関数より下に描画関係の処理を書きます。

    if ebiten.IsDrawingSkipped() {
        return nil
    }

更新処理が間に合わなくなると、描画をスキップして次回の更新に間に合わせようとする為にあるようです。

func ebitenutil.DebugPrint

デバッグ用テキスト表示。途中で改行もできる。日本語不可。

TPSとFPS

現在のTPSとFPSを表示させる。

func update(screen *ebiten.Image) error {
    if ebiten.IsDrawingSkipped() {
        return nil
    }

    msg := fmt.Sprintf("TPS = %0.2f\nFPS = %0.2f", ebiten.CurrentTPS(), ebiten.CurrentFPS())
    ebitenutil.DebugPrint(screen, msg)

    return nil
}

tps_and_fps.png

func ebiten.CurrentTPS

現在1秒間にupdate関数が呼ばれる回数を取得する。
TPS (ticks per second)

func ebiten.CurrentFPS

現在1秒間に描画されている回数を取得する。
FPS (frames per second)

例えば小さい画像を数万個描画すると、TPS=60, FPS=30という表示になり、計算2回に描画1回という感じで描画回数が少なくなります。

この描画スキップにより安定してupdate関数が呼び出されることで、固定フレームレートを前提とした計算をしても、ほぼほぼ問題は起こらないように思います。

ウィンドウアイコン

ウィンドウのアイコンを変更する。ウィンドウのアイコンはWindowsにしかない。Alt+Tabのウィンドウ切り替え時の表示や、タスクバーのアイコンも一緒に変更される。

main.go
package main

import (
    "image"
    _ "image/png"
    "log"
    "os"

    "github.com/hajimehoshi/ebiten"
)

var icon image.Image

func init() {
    fp, err := os.Open("ebiten.png")
    if err != nil {
        log.Fatal(err)
    }
    defer fp.Close()

    icon, _, err = image.Decode(fp)
    if err != nil {
        log.Fatal(err)
    }
}

func update(screen *ebiten.Image) error {
    if ebiten.IsDrawingSkipped() {
        return nil
    }
    return nil
}

func main() {
    ebiten.SetWindowIcon([]image.Image{icon})

    if err := ebiten.Run(update, 320, 240, 2, "Window Icon"); err != nil {
        log.Fatal(err)
    }
}

window_icon.png

画像の読み込み

pngファイルを読み込むので、インポートに_ "image/png"を記述する。

import (
    "image"
    _ "image/png"
    "log"
    "os"

    "github.com/hajimehoshi/ebiten"
)

os.Open()からのimage.Decode()image.Image構造体を取得。
ここまではEbitenに限らずgoで画像ファイルを扱う時と同じ。

func ebiten.SetWindowIcon

引数にはimage.Image構造体のスライスを渡す。ebiten.Imageではないことに気を付ける。
update内でも使用できるが1度実行すればいいようなのでmain関数内で実行している。
画像のサイズは16x16, 32x32, 64x64であることが望ましい。

ウィンドウ非アクティブ時の動作

デフォルトではウィンドウが非アクティブになるとプログラムが一時停止する。

動作を停止させない
ebiten.SetRunnableInBackground(true)

引数にtrueを指定すると非アクティブ状態でも動作し続ける。デフォルトはfalse
1度実行するだけでいい。
ブラウザでは意味ない。

マウスカーソルを非表示に

1度実行するだけでいい。

カーソル非表示
ebiten.SetCursorVisible(false)

グラフィック

座標の原点は左上で、右下方向に数字が大きくなる。

画像の描画

main.go
package main

import (
    "image"
    "image/color"
    _ "image/png"
    "log"
    "os"

    "github.com/hajimehoshi/ebiten"
)

var (
    uma *ebiten.Image
)

func init() {
    fp, err := os.Open("uma.png")
    if err != nil {
        log.Fatal(err)
    }

    img, _, err := image.Decode(fp)
    if err != nil {
        log.Fatal(err)
    }

    uma, err = ebiten.NewImageFromImage(img, ebiten.FilterDefault)
    if err != nil {
        log.Fatal(err)
    }
}

func update(screen *ebiten.Image) error {
    if ebiten.IsDrawingSkipped() {
        return nil
    }

    screen.Fill(color.RGBA{0x37, 0x94, 0x6E, 0xFF})

    op := &ebiten.DrawImageOptions{}
    op.GeoM.Translate(64, 64)
    screen.DrawImage(uma, op)

    return nil
}

func main() {
    if err := ebiten.Run(update, 320, 240, 2, "Image"); err != nil {
        log.Fatal(err)
    }
}

draw_image.png

func ebiten.NewImageFromImage

画像から*ebiten.Imageを作成する。
引数にimage.Image構造体とフィルタータイプを指定する。

func Fill

画像を指定した色で塗りつぶす関数。今回は画面全体を塗りつぶしている。
image/colorを使う。

単色のテクスチャを用意したいときにも使える。

img, _ := ebiten.NewImage(16, 16, ebiten.FilterDefault)
img.Fill(color.White)

フェードインアウト表現にも使える。

type ebiten.DrawImageOptions

描画の際の座標変換や色変換、描画モード等を設定するもの。

    op := &ebiten.DrawImageOptions{}
    op.GeoM.Translate(64, 64)
    screen.DrawImage(uma, op)

op.GeoM.Translate(64, 64)とはxとyを64移動させているということ。
ほかにScaleRotateもある。詳しくはGoDocを見る。

func DrawImage

画像に対してほかの画像を描画する関数。
今回はスクリーン画像に対してumaを描画している。

ポリゴン

3角形を描画する。自由な変形が可能。

main.go
package main

import (
    "image"
    "image/color"
    _ "image/png"
    "log"
    "os"

    "github.com/hajimehoshi/ebiten"
)

var (
    uma       *ebiten.Image
    vertecies []ebiten.Vertex
    indices   []uint16
)

func init() {
    fp, err := os.Open("uma.png")
    if err != nil {
        log.Fatal(err)
    }

    img, _, err := image.Decode(fp)
    if err != nil {
        log.Fatal(err)
    }

    uma, err = ebiten.NewImageFromImage(img, ebiten.FilterDefault)
    if err != nil {
        log.Fatal(err)
    }
}

func init() {
    const (
        x = 160
        y = 64
        w = 32
        h = 32
    )
    vertecies = []ebiten.Vertex{
        {
            DstX:   x - 16,
            DstY:   y,
            SrcX:   0,
            SrcY:   0,
            ColorR: 1,
            ColorG: 1,
            ColorB: 1,
            ColorA: 1,
        },
        {
            DstX:   x + w - 16,
            DstY:   y,
            SrcX:   0 + w,
            SrcY:   0,
            ColorR: 1,
            ColorG: 1,
            ColorB: 1,
            ColorA: 1,
        },
        {
            DstX:   x + w + 16,
            DstY:   y + h,
            SrcX:   0 + w,
            SrcY:   0 + h,
            ColorR: 1,
            ColorG: 1,
            ColorB: 1,
            ColorA: 1,
        },
        {
            DstX:   x + 16,
            DstY:   y + h,
            SrcX:   0,
            SrcY:   0 + h,
            ColorR: 1,
            ColorG: 1,
            ColorB: 1,
            ColorA: 1,
        },
    }
    indices = []uint16{0, 1, 2, 0, 2, 3}
}

func update(screen *ebiten.Image) error {
    if ebiten.IsDrawingSkipped() {
        return nil
    }

    screen.Fill(color.RGBA{0x37, 0x94, 0x6E, 0xFF})

    op := &ebiten.DrawImageOptions{}
    op.GeoM.Translate(64, 64)
    screen.DrawImage(uma, op)

    screen.DrawTriangles(vertecies, indices, uma, nil)

    return nil
}

func main() {
    if err := ebiten.Run(update, 320, 240, 2, "Polygon"); err != nil {
        log.Fatal(err)
    }
}

uma.png
polygon.png

右側がポリゴンで変形させて描画したもの。

type ebiten.Vertex

頂点座標、テクスチャ座標(画像の座標であってuvではない)、頂点カラー(maxが1)を設定する構造体。
これのスライスを使用する。

func DrawTriangles

画像に対してポリゴンを描画する。
引数に、頂点スライス、インデックススライス、テクスチャ、最後にtype DrawTrianglesOptionsなんだが、今回は不要なのでnilを渡している。

公式サンプルexamples/shapesでは、DrawTrianglesを使用してラインや矩形を描画している。ラインの場合は線の太さを求める処理が必要なようだ。

アニメーション

キャラクターアニメーション。

main.go
package main

import (
    "fmt"
    "image"
    "image/color"
    _ "image/png"
    "log"
    "os"

    "github.com/hajimehoshi/ebiten"
    "github.com/hajimehoshi/ebiten/ebitenutil"
)

var (
    umas  *ebiten.Image
    count int
    frame int
)

const duration = 10

func init() {
    fp, err := os.Open("uma_sheet.png")
    if err != nil {
        log.Fatal(err)
    }

    img, _, err := image.Decode(fp)
    if err != nil {
        log.Fatal(err)
    }

    umas, err = ebiten.NewImageFromImage(img, ebiten.FilterDefault)
    if err != nil {
        log.Fatal(err)
    }
}

func update(screen *ebiten.Image) error {
    count++
    if count > duration {
        count = 0
        frame++
        if frame >= 5 {
            frame = 0
        }
    }

    if ebiten.IsDrawingSkipped() {
        return nil
    }

    screen.Fill(color.RGBA{0x37, 0x94, 0x6E, 0xFF})

    x := frame * 32
    uma := umas.SubImage(image.Rect(x, 0, x+32, 32)).(*ebiten.Image)
    op := &ebiten.DrawImageOptions{}
    op.GeoM.Translate(64, 64)
    screen.DrawImage(uma, op)

    msg := fmt.Sprintf("frame = %d", frame)
    ebitenutil.DebugPrint(screen, msg)

    return nil
}

func main() {
    if err := ebiten.Run(update, 320, 240, 2, "Animation"); err != nil {
        log.Fatal(err)
    }
}

uma_sheet.png
animation.png

カウンタ計算はebiten.IsDrawingSkipped()より上に書く。

func SubImage

画像からimage.Rectangle構造体の範囲の画像を取得する。
ピクセルをコピーではなく共有している。

uma := umas.SubImage(image.Rect(x, 0, x+32, 32)).(*ebiten.Image)

上記はSubImageの戻り値を*ebiten.Imageに変換している。
型アサーション。

タイリング

SubImageを使ったタイルマップ表現。

main.go
package main

import (
    "image"
    _ "image/png"
    "log"
    "os"

    "github.com/hajimehoshi/ebiten"
)

var (
    tiles *ebiten.Image
)

var tilemap = [][]int{
    {0, 0, 0, 0, 3, 0, 0, 2, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0},
    {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 1, 0},
    {0, 0, 2, 0, 0, 1, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0},
    {0, 0, 0, 1, 0, 0, 4, 0, 0, 0, 0, 4, 0, 5, 0, 0, 1, 0, 0, 0},
    {0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 3, 0, 0, 2},
    {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
    {0, 1, 0, 0, 2, 0, 0, 0, 0, 0, 0, 2, 0, 1, 0, 0, 0, 1, 2, 0},
    {0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0},
    {0, 0, 0, 0, 0, 0, 0, 2, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 5, 0},
    {0, 4, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
    {2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0},
    {0, 0, 0, 0, 1, 0, 4, 0, 3, 0, 0, 0, 2, 0, 0, 0, 0, 4, 2, 0},
    {0, 0, 0, 0, 0, 0, 2, 0, 1, 0, 0, 0, 0, 0, 5, 0, 1, 0, 0, 0},
    {1, 0, 0, 2, 0, 0, 0, 0, 0, 0, 2, 0, 0, 1, 0, 0, 0, 0, 0, 0},
    {0, 2, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0},
}

func init() {
    fp, err := os.Open("grass_tiles.png")
    if err != nil {
        log.Fatal(err)
    }

    img, _, err := image.Decode(fp)
    if err != nil {
        log.Fatal(err)
    }

    tiles, err = ebiten.NewImageFromImage(img, ebiten.FilterDefault)
    if err != nil {
        log.Fatal(err)
    }
}

func update(screen *ebiten.Image) error {
    if ebiten.IsDrawingSkipped() {
        return nil
    }

    for col := 0; col < 15; col++ {
        for row := 0; row < 20; row++ {
            n := tilemap[col][row]
            x := (n % 4) * 16
            y := n / 4 * 16
            tile := tiles.SubImage(image.Rect(x, y, x+16, y+16)).(*ebiten.Image)
            op := &ebiten.DrawImageOptions{}
            op.GeoM.Translate(float64(row*16), float64(col*16))
            screen.DrawImage(tile, op)
        }
    }

    return nil
}

func main() {
    if err := ebiten.Run(update, 320, 240, 2, "Tilemap"); err != nil {
        log.Fatal(err)
    }
}

grass_tiles.png
tilemap.png

Geomety Matrix

行列を使った図形の変換。

移動してから回転することと、回転してから移動する事では結果が違うことに気を付ける。
順番が大事。
主に使うのは以下の3つだと思う。他はGoDoc見る。

func (g *GeoM) Rotate(theta float64)
func (g *GeoM) Scale(x, y float64)
func (g *GeoM) Translate(tx, ty float64)

Rotate

引数にはRadianを指定する。

ラジアンの求め方
r := float64(角度) * math.Pi / 180
op.GeoM.Rotate(r)

Scale

xとy軸で拡大縮小。

画像を左右反転させる場合にもこれを使う。

左右反転
op.GeoM.Scale(-1, 1)

反転時の中心は画像の原点なので、あらかじめTranslateで移動させるか、反転後に移動させるかすること。

Color Matrix

行列を使った色の変換。
公式サンプルexamples/flood, examples/hsv, examples/hueあたりを見る。

描画する画像を塗りつぶして点滅させる。

main.go
package main

import (
    "image"
    "image/color"
    _ "image/png"
    "log"
    "os"

    "github.com/hajimehoshi/ebiten"
)

var (
    uma   *ebiten.Image
    count int
    flush bool
)

func init() {
    fp, err := os.Open("uma.png")
    if err != nil {
        log.Fatal(err)
    }

    img, _, err := image.Decode(fp)
    if err != nil {
        log.Fatal(err)
    }

    uma, err = ebiten.NewImageFromImage(img, ebiten.FilterDefault)
    if err != nil {
        log.Fatal(err)
    }
}

func update(screen *ebiten.Image) error {
    count++
    if count > 10 {
        count = 0
        flush = !flush
    }
    if ebiten.IsDrawingSkipped() {
        return nil
    }

    screen.Fill(color.RGBA{0x37, 0x94, 0x6E, 0xFF})

    op := &ebiten.DrawImageOptions{}
    op.GeoM.Translate(64, 64)
    if flush {
        op.ColorM.Scale(0, 0, 0, 1)
        op.ColorM.Translate(1, 1, 0.8, 0)
    }
    screen.DrawImage(uma, op)

    return nil
}

func main() {
    if err := ebiten.Run(update, 320, 240, 2, "Flush"); err != nil {
        log.Fatal(err)
    }
}

uma (2).png
flush.png

透明度以外を操作

op.ColorM.Scale(0, 0, 0, 1)
op.ColorM.Translate(1, 1, 0.8, 0)

Scaleでアルファ以外の色要素を0にしてから、Translateで塗りつぶしている。
Translateで指定する値は0~1。

半透明に描画したいとき

透明度を半分に
op.ColorM.Scale(1, 1, 1, 0.5)

描画モード

CompositeMode描画方法を設定する。デフォルトはアルファブレンディング。
使い方はexample/additive, example/maskingを見る。

加算モード
op = &ebiten.DrawImageOptions{}
op.CompositeMode = ebiten.CompositeModeLighter
screen.DrawImage(img, op)

キーボード入力

キーが押されているか、押した瞬間、離した瞬間、押している長さを調べることができる。

サンプルではスペースキーの状態を調べている。

main.go
package main

import (
    "fmt"
    "log"

    "github.com/hajimehoshi/ebiten"
    "github.com/hajimehoshi/ebiten/ebitenutil"
    "github.com/hajimehoshi/ebiten/inpututil"
)

func update(screen *ebiten.Image) error {
    pressed := ebiten.IsKeyPressed(ebiten.KeySpace)
    justpressed := inpututil.IsKeyJustPressed(ebiten.KeySpace)
    justreleased := inpututil.IsKeyJustReleased(ebiten.KeySpace)
    duration := inpututil.KeyPressDuration(ebiten.KeySpace)

    if ebiten.IsDrawingSkipped() {
        return nil
    }

    if pressed {
        ebitenutil.DebugPrintAt(screen, "Space key pressed.", 0, 0)
    }
    if justpressed {
        ebitenutil.DebugPrintAt(screen, "Space key just pressed.", 0, 16)
    }
    if justreleased {
        ebitenutil.DebugPrintAt(screen, "Space key just pressed.", 0, 32)
    }
    msg := fmt.Sprintf("Space key duration = %d", duration)
    ebitenutil.DebugPrintAt(screen, msg, 0, 48)

    return nil
}

func main() {
    if err := ebiten.Run(update, 320, 240, 2, "Keyboard"); err != nil {
        log.Fatal(err)
    }
}

キーの状態判定はebiten.IsDrawingSkipped()より上に書くこと。

func ebiten.IsKeyPressed

引数に指定したキーが押されている状態かを返す。
引数にebiten.Key型の定数を指定する。戻り値はbool
定義されているキーはGoDocを見る。

ebiten.Key型は元はintなので型キャストできる。

k := ebiten.Key(0)

func inpututil.IsKeyJustPressed

キーが押された瞬間かどうかを返す。

func inpututil.IsKeyJustReleased

キーが離された瞬間かどうかを返す。

func inpututil.KeyPressDuration

キーの押されている時間を返す。戻り値はint

マウス入力

マウスカーソル座標、マウスホイールの移動量、各種ボタンの状態を調べることができる。

サンプルではカーソル座標、ホイール、マウス左ボタンの状態を見ている。

main.go
package main

import (
    "fmt"
    "log"

    "github.com/hajimehoshi/ebiten"
    "github.com/hajimehoshi/ebiten/ebitenutil"
)

var wheelx, wheely float64

func update(screen *ebiten.Image) error {
    x, y := ebiten.CursorPosition()
    xoff, yoff := ebiten.Wheel()
    wheelx += xoff
    wheely += yoff

    leftpressed := ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft)

    if ebiten.IsDrawingSkipped() {
        return nil
    }

    msg := fmt.Sprintf("Cursor x = %d, y = %d\nWheel x = %0.2f, y = %0.2f", x, y, wheelx, wheely)
    ebitenutil.DebugPrint(screen, msg)

    if leftpressed {
        ebitenutil.DebugPrintAt(screen, "Left mousebutton pressed.", 0, 32)
    }

    return nil
}

func main() {
    if err := ebiten.Run(update, 320, 240, 2, "Mouse"); err != nil {
        log.Fatal(err)
    }
}

func ebiten.CursorPosition

戻り値にマウスカーソル座標x,yが返る。
この座標にはウィンドウのスケールもかかる。

func ebiten.Wheel

戻り値にマウスホイールの移動した値が返る。

func ebiten.IsMouseButtonPressed

マウスのボタンが押されているか調べる。
引数には定数を指定する。MouseButtonLeft, MouseButtonRight, MouseButtonMiddleのいずれか。

func inpututil.IsMouseButtonJustPressed

押された瞬間かどうか。

func inpututil.IsMouseButtonJustReleased

離した瞬間かどうか。

func inpututil.MouseButtonPressDuration

押し続けている時間を取得。

ゲームパッド

テキスト

用意したフォントを使ってテキストを描画する。
日本語可。

main.go
package main

import (
    "image/color"
    "io/ioutil"
    "log"

    "github.com/golang/freetype/truetype"
    "golang.org/x/image/font"

    "github.com/hajimehoshi/ebiten"
    "github.com/hajimehoshi/ebiten/text"
)

var (
    mplusfont font.Face
    pixelfont font.Face
)

func init() {
    data, err := ioutil.ReadFile("mplus-1p-regular.ttf")
    if err != nil {
        log.Fatal(err)
    }

    ttf, err := truetype.Parse(data)
    if err != nil {
        log.Fatal(err)
    }

    op := truetype.Options{Size: 24, DPI: 72, Hinting: font.HintingFull}
    mplusfont = truetype.NewFace(ttf, &op)
}

func init() {
    data, err := ioutil.ReadFile("PixelMplus12-Regular.ttf")
    if err != nil {
        log.Fatal(err)
    }

    ttf, err := truetype.Parse(data)
    if err != nil {
        log.Fatal(err)
    }

    op := truetype.Options{Size: 12, DPI: 72, Hinting: font.HintingNone, SubPixelsX: 16}
    pixelfont = truetype.NewFace(ttf, &op)
}

func update(screen *ebiten.Image) error {
    if ebiten.IsDrawingSkipped() {
        return nil
    }
    screen.Fill(color.RGBA{0x37, 0x94, 0x6E, 0xff})

    text.Draw(screen, "はじめてのEbiten。", mplusfont, 32, 32, color.White)
    text.Draw(screen, "はじめてのEbiten。", pixelfont, 32, 128, color.White)

    return nil
}

func main() {
    if err := ebiten.Run(update, 320, 240, 2, "Text"); err != nil {
        log.Fatal(err)
    }
}

text_x2.png
下は等倍表示。
text_x1.png

こちらのフォントを使用させてもらいました。
M+FONTS
PixelMplus

フォント読み込み

インポートを追加
import (
    "github.com/golang/freetype/truetype"
    "golang.org/x/image/font"
)
ttfファイルからbyteデータを取得
    data, err := ioutil.ReadFile("mplus-1p-regular.ttf")
Fontに展開
    ttf, err := truetype.Parse(data)
オプションを設定してfont.Faceを作成
    op := truetype.Options{Size: 24, DPI: 72, Hinting: font.HintingFull}
    mplusfont = truetype.NewFace(ttf, &op)

truetype.Options構造体

Size: 0なら12が使われる
DPI: 0なら72が使われる
Hinting: 0なら無し
GlyphCacheEntries: 0なら512
SubPixelsX: 0なら4が使われる
SubPixelsY: 0なら1が使われる

詳細はfreetype/truetype/face.goを見る。

テキスト描画

指定する座標はテキストの左下。

func text.Draw

    text.Draw(screen, "はじめてのEbiten。", mplusfont, 32, 32, color.White)
    text.Draw(screen, "はじめてのEbiten。", pixelfont, 32, 128, color.White)

文字の縁取り

1ドットずつ上下左右にずらして、重ねて描画すると縁取りしたように見せられる。
同じ文字を複数回描画することは、コストの高い処理ではない。
outline_x2.png
下は等倍表示。
outline_x1.png

文字の幅を調べる

文章の折り返し位置を調べるために。

1文字ずつ文字の幅を調べていく。

advance, ok := mplusfont.GlyphAdvance(rune('あ'))
fmt.Println(advance.Floor())

戻り値が固定小数点なので、整数に変換する。

オーディオ

.wav, .ogg, .mp3形式の音声ファイルを再生する。

基本的な形

main.go
package main

import (
    "log"
    "os"

    "github.com/hajimehoshi/ebiten"
    "github.com/hajimehoshi/ebiten/audio"
    "github.com/hajimehoshi/ebiten/audio/vorbis"
    "github.com/hajimehoshi/ebiten/ebitenutil"
)

const (
    sampleRate = 44100
)

var (
    audioContext *audio.Context
)

func init() {
    var err error
    audioContext, err = audio.NewContext(sampleRate)
    if err != nil {
        log.Fatal(err)
    }

    f, err := os.Open("audio_xxx.ogg")
    if err != nil {
        log.Fatal(err)
    }

    s, err := vorbis.Decode(audioContext, f)
    if err != nil {
        log.Fatal(err)
    }

    p, err := audio.NewPlayer(audioContext, s)
    if err != nil {
        log.Fatal(err)
    }
    p.Play()
}

func update(screen *ebiten.Image) error {
    if ebiten.IsDrawingSkipped() {
        return nil
    }
    if !audioContext.IsReady() {
        ebitenutil.DebugPrint(screen, "audio context not ready")
    }
    return nil
}

func main() {
    if err := ebiten.Run(update, 320, 240, 2, "Audio play"); err != nil {
        log.Fatal(err)
    }
}

func audio.NewContext

オーディオコンテキストを作成する。
指定するサンプルレートは44100または48000がよいらしい。

Windows10ではプレイヤーを再生するまで、IsReady()falseを返す。

audioContext.IsReady()

func vorbis.Decode

oggから再生可能なストリームソースに変換する。
2ch16bit指定したサンプルレートに変換されるので、再生するファイルのサンプルレート等が違っても問題ない。

os.Close()でファイルを閉じてはいけない。GCに回収されるときに閉じられる。

func audio.NewPlayer

ストリームソースからプレイヤーを作成。
Play()で再生する。他にPause(), SetVolume()などがある。
1つのソースを複数のプレイヤーで共有できない。

ループ再生

違う部分だけを抜粋。

    s, err := vorbis.Decode(audioContext, f)

    l := audio.NewInfiniteLoop(s, s.Length())

    p, err := audio.NewPlayer(audioContext, l)

func audio.NewInfiniteLoop

無限ループストリームを作成。

func NewInfiniteLoop(src ReadSeekCloser, length int64) *InfiniteLoop

lengthはバイト数を設定する。
曲の最後でループさせる場合はソースのLength()を設定すればいい。

func audio.NewInfiniteLoopWithIntro

こちらは、イントロ部分を設定することができる。

func NewInfiniteLoopWithIntro(src ReadSeekCloser, introLength int64, loopLength int64) *InfiniteLoop

introLengthの求め方

sampleRate * イントロ部分の秒数 * 4

同じソースを重複して再生

短い効果音を重複して再生したい場合、NewPlayerFromBytesでプレイヤーを作成する。

以下サンプル。
スペースキーを連打すると、音が重なっているのがわかると思う。

main.go
package main

import (
    "io/ioutil"
    "log"
    "os"

    "github.com/hajimehoshi/ebiten"
    "github.com/hajimehoshi/ebiten/audio"
    "github.com/hajimehoshi/ebiten/audio/wav"
    "github.com/hajimehoshi/ebiten/ebitenutil"
    "github.com/hajimehoshi/ebiten/inpututil"
)

const (
    sampleRate = 44100
)

var (
    audioContext *audio.Context
    byteSource   []byte
)

func init() {
    var err error
    audioContext, err = audio.NewContext(sampleRate)
    if err != nil {
        log.Fatal(err)
    }

    f, err := os.Open("jab.wav")
    if err != nil {
        log.Fatal(err)
    }

    s, err := wav.Decode(audioContext, f)
    if err != nil {
        log.Fatal(err)
    }

    byteSource, err = ioutil.ReadAll(s)
    if err != nil {
        log.Fatal(err)
    }
}

func update(screen *ebiten.Image) error {
    if inpututil.IsKeyJustPressed(ebiten.KeySpace) {
        p, err := audio.NewPlayerFromBytes(audioContext, byteSource)
        if err != nil {
            return err
        }
        p.SetVolume(0.5)
        p.Play()
    }

    if ebiten.IsDrawingSkipped() {
        return nil
    }

    if !audioContext.IsReady() {
        ebitenutil.DebugPrint(screen, "audio context not ready")
    }

    return nil
}

func main() {
    if err := ebiten.Run(update, 320, 240, 2, "Audio byte source"); err != nil {
        log.Fatal(err)
    }
}

func audio.NewPlayerFromBytes

バイトソースからプレイヤーを作成する。
同じバイトソースを複数のプレイヤーで共有できる。

examples

audio, audioinfiniteloop, pcm, piano, sinewave, wavを見る。

リソースファイルの埋め込み

Ebitenのexamplesでは画像や音声ファイルを.goファイルに埋め込んでいるようです。

埋め込み方法

file2byteslice
こちらのプログラムを使います。
go getしてからgo installするとbinに配置してくれるので楽です。
使い方はexamples/resources/generate.goを見ると、go generateを使ってまとめて処理しているようです。

画像

埋め込んだbyteスライスからebiten.Imageを作成。

変更部分だけ抜粋
import "my/resources/images"

    img, _, err := image.Decode(bytes.NewReader(images.Hoge_png))
    hogeImage, _ = ebiten.NewImageFromImage(img, ebiten.FilterDefault)

変換した.goファイルのパッケージをインポートする。
Hoge_png(グローバル変数)というbyteスライスをリーダーに変換してからデコードする。

フォント

もともとbyteスライスをパースしているのであまり違いが無い。

変更部分だけ抜粋
import "my/resources/fonts"

    ttf, err := truetype.Parse(fonts.Hoge_ttf)

オーディオ

byteスライスをaudio.BytesReadSeekCloserに渡してからデコードする。

変更部分だけ抜粋
import raudio "my/resources/audio"

    s, err := wav.Decode(audioContext, audio.BytesReadSeekCloser(raudio.Hoge_wav))

ウェブブラウザ

Goのコードをブラウザで実行できる形に変換する。

GopherJS

GoのコードをJavaScript形式に変換する。

GopherJSのインストール

下記のコマンドでインストール。

go get -u github.com/gopherjs/gopherjs

ビルド

gopherjsはコードを生成するときにプラットフォームのデフォルトのGOOSを使用する。
サポートされているGOOSlinuxdarwin
windowsの場合は一時的に変更しなければならない。

プロジェクトディレクトリに移動して下記のコマンドを入力。

set GOOS=linux
gopherjs build

.js.js.mapファイルが生成される。

gopherjs serve

手軽にブラウザで動作テストできる便利な機能。

set GOOS=linux
gopherjs serve github.com/hoge/hogehoge

セキュリティの警告が出ても許可する。終了するときはCtrl+C

ブラウザを開き、アドレスバーにhttp://localhost:8080/と入力すると、js変換後ページを開く。
ページを開きなおすたびに一連の動作をする。

リソースファイルの問題

Windowsでは機能の制限があるのでリソースファイルはすべて埋め込む。
あまり大きなリソースファイルは変換後のサイズ肥大化にもなるので気を付ける。

.jsに変換する場合はbyteスライスよりbase64形式の方がサイズが小さくなるかも。

JavaScriptとデータのやり取りとか

詳細はサイトの方へ。
GopherJS

WebAssembly

Edge ×
Chrome 〇
Firefox 〇

その他

スクリーンショット

撮影するシュートカットキーを設定する。
一時的に環境変数を設定する。
コマンドプロンプトだと1行で書けない。

エスケープキーで撮影
set EBITEN_SCREENSHOT_KEY=escape
go run main.go

メインのフォルダに保存される。windowスケール1倍で保存される。

os.Setenv関数で設定しても大丈夫だった。

一時的な環境変数
func init() {
    err := os.Setenv("EBITEN_SCREENSHOT_KEY", "escape")
    if err != nil {
        log.Fatal(err)
    }
}

自前で保存する場合

キーボードが使えない環境とか。

内部イメージをダンプ

環境変数EBITEN_INTERNAL_IMAGES_KEYにキーを設定すると、内部イメージをすべてダンプすることができる。
これはビルドタグにebitendebugを設定している場合にのみ有効。

コマンドプロンプト
set EBITEN_INTERNAL_IMAGES_KEY=escape
go run -tags=ebitendebug main.go

コマンドプロンプトを非表示に

Windowsで起動時に一瞬だけ表示されるのを出てこないようにする。

go build -ldflags -H=windowsgui

Ebitenリンク

Ebiten
GitHub
Wiki
GoDoc

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away