Ebiten
EbitenとはGo言語のシンプルな2Dゲームライブラリです。
自動テクスチャアトラス化、自動バッチ処理などで大量のスプライトを高速に描画できます。
WindowsではCgoが不要。
テスト環境
Go 1.12.5
Ebiten 1.9.3
Windows10
ウィンドウ
ウィンドウを表示させる
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
}
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のウィンドウ切り替え時の表示や、タスクバーのアイコンも一緒に変更される。
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)
}
}
画像の読み込み
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)
グラフィック
座標の原点は左上で、右下方向に数字が大きくなる。
画像の描画
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)
}
}
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移動させているということ。
ほかにScale
、Rotate
もある。詳しくはGoDocを見る。
func DrawImage
画像に対してほかの画像を描画する関数。
今回はスクリーン画像に対してuma
を描画している。
ポリゴン
3角形を描画する。自由な変形が可能。
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)
}
}
右側がポリゴンで変形させて描画したもの。
type ebiten.Vertex
頂点座標、テクスチャ座標(画像の座標であってuvではない)、頂点カラー(maxが1)を設定する構造体。
これのスライスを使用する。
func DrawTriangles
画像に対してポリゴンを描画する。
引数に、頂点スライス、インデックススライス、テクスチャ、最後にtype DrawTrianglesOptions
なんだが、今回は不要なのでnil
を渡している。
公式サンプルexamples/shapes
では、DrawTriangles
を使用してラインや矩形を描画している。ラインの場合は線の太さを求める処理が必要なようだ。
アニメーション
キャラクターアニメーション。
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)
}
}
カウンタ計算はebiten.IsDrawingSkipped()
より上に書く。
func SubImage
画像からimage.Rectangle
構造体の範囲の画像を取得する。
ピクセルをコピーではなく共有している。
uma := umas.SubImage(image.Rect(x, 0, x+32, 32)).(*ebiten.Image)
上記はSubImage
の戻り値を*ebiten.Image
に変換している。
型アサーション。
タイリング
SubImage
を使ったタイルマップ表現。
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)
}
}
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
あたりを見る。
描画する画像を塗りつぶして点滅させる。
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)
}
}
透明度以外を操作
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)
キーボード入力
キーが押されているか、押した瞬間、離した瞬間、押している長さを調べることができる。
サンプルではスペースキーの状態を調べている。
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
。
マウス入力
マウスカーソル座標、マウスホイールの移動量、各種ボタンの状態を調べることができる。
サンプルではカーソル座標、ホイール、マウス左ボタンの状態を見ている。
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
押し続けている時間を取得。
ゲームパッド
テキスト
用意したフォントを使ってテキストを描画する。
日本語可。
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)
}
}
こちらのフォントを使用させてもらいました。
M+FONTS
PixelMplus
フォント読み込み
import (
"github.com/golang/freetype/truetype"
"golang.org/x/image/font"
)
data, err := ioutil.ReadFile("mplus-1p-regular.ttf")
ttf, err := truetype.Parse(data)
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ドットずつ上下左右にずらして、重ねて描画すると縁取りしたように見せられる。
同じ文字を複数回描画することは、コストの高い処理ではない。
下は等倍表示。
文字の幅を調べる
文章の折り返し位置を調べるために。
1文字ずつ文字の幅を調べていく。
advance, ok := mplusfont.GlyphAdvance(rune('あ'))
fmt.Println(advance.Floor())
戻り値が固定小数点なので、整数に変換する。
オーディオ
.wav
, .ogg
, .mp3
形式の音声ファイルを再生する。
基本的な形
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
でプレイヤーを作成する。
以下サンプル。
スペースキーを連打すると、音が重なっているのがわかると思う。
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
を使用する。
サポートされているGOOS
はlinux
とdarwin
。
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