こんにちは! 虎の穴ラボのA.Mです。
この記事は夏の連載企画の15日目の記事です。
前回はm.mさんによる「画像ファイルをまとめてWebPに変換しよう!」が投稿されました。
次回は原さんによる「もしも転生してWebアプリエンジニアになったら?インフラエンジニアの新たな挑戦」が投稿されます。こちらもぜひご覧ください。
本記事では、Go言語で実装されている2DゲームエンジンであるEbitengine
を使用して、何か作ってみたいと思います。
はじめに
以前の記事で、Go言語で作ったゲームを紹介しました。
このゲームに対して、以下の2つを実現したいと考えていました。
- BGMやSEなどの音を追加したい
- 手軽に遊べるようにしたい(フォントやターミナルのサイズに依存しているのを解消したい)
どうやって実現しようかと考えていたところ、GoはWebAssemblyをサポートしていること思い出し、「そうだ、ブラウザで動かそう!」と思い立ちました。
実際にやってみたところ、以下のようなゲームのコアな部分が完全にOSの機能に依存していたので、がっつり作り直す必要がありました。
- 画面の描写、操作入力が標準入出力を使用
- 画面のリフレッシュが
clear
コマンド - 文字装飾(色・背景色・太字の変更など)がエスケープシーケンス
全部自前で実装するのしんどいなぁと思っていたところ、Ebitengine
という2Dゲームエンジンを見つけました。
ただ、全部を一度に作り直すのはハードルが高いので、まずはEbitengine
に触れてみることにします。
というわけで、今回はEbitengine
で簡単な動くものを作ってみたいと思います。
Ebitengineとは
Ebitengineは、Go言語で実装されたオープンソースな2Dゲームエンジンです。 開発者は、星一さんです。
マルチプラットフォームに対応しているので、Webブラウザ(WebAssembly)だけでなく、デスクトップ(Windows, macOS ,Linux ,FreeBSD)、モバイル (Android, iOS) 、Nintendo Switch™もサポートされています。
商用のゲームで採用されている実績もあるので、安定していると言えます。
また、開発者が日本人なのもあり、日本語のドキュメントもある程度あります。
ブラウザ上で確認できるサンプルも多数あるので、詳細は公式ページを参照してください。
開発環境
- macOS Monterey
- Go 1.22.5
本記事では、Goがインストール済みである前提で説明します。 環境構築手順については説明しませんので、詳しくは公式ページを参照してください。
実行環境の確認
Ebitengineが正常に動作するか確認します。
go run github.com/hajimehoshi/ebiten/v2/examples/rotate@latest
ウィンドウが立ち上がり、回転するGopherくんが表示されればOKです。
まずはHello World
公式のツアーを見ながら、Hello Worldを表示してみます。
ディレクトリを作成して初期化します。
mkdir ebitengine-practice cd ebitengine-practice # Go Modulesの初期化 go mod init ebitengine-practice
次に、main.go
を作成します。
package main import ( "log" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/ebitenutil" ) type Game struct{} func (g *Game) Update() error { return nil } func (g *Game) Draw(screen *ebiten.Image) { ebitenutil.DebugPrint(screen, "Hello, World!") } func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) { return 320, 240 } func main() { ebiten.SetWindowSize(640, 480) ebiten.SetWindowTitle("Hello, World!") if err := ebiten.RunGame(&Game{}); err != nil { log.Fatal(err) } }
続いて、依存関係を解決します。
go mod tidy
実行すると、以下のように必要なライブラリが追加されます。
$ go mod tidy go: finding module for package github.com/hajimehoshi/ebiten/v2/ebitenutil go: finding module for package github.com/hajimehoshi/ebiten/v2 go: found github.com/hajimehoshi/ebiten/v2 in github.com/hajimehoshi/ebiten/v2 v2.7.7 go: found github.com/hajimehoshi/ebiten/v2/ebitenutil in github.com/hajimehoshi/ebiten/v2 v2.7.7
最後に、main.go
を実行します。
go run main.go
ウィンドウが立ち上がり、Hello, World!
と表示されれば成功です。
コードの説明
Game
type Game struct{}
Game
の構造体を定義します。
Game
はインターフェース(ebiten.Game)であり、ゲームに必要な関数Update
、Draw
、Layout
を持っています。
Update
func (g *Game) Update() error { return nil }
Update
は、ゲームの論理状態を更新するための関数です。
Update
はtick
ごとに呼び出されます。
tick
は論理更新の時間単位で、デフォルト値は1/60秒です。
つまり、Updateメソッドはデフォルトで1秒間に60回呼び出されます。
Hello Worldのサンプルコードでは状態を持っていないので、何もしていません。
Draw
func (g *Game) Draw(screen *ebiten.Image) { ebitenutil.DebugPrint(screen, "Hello, World!") }
Draw
は、画面を描画するための関数です。
フレームごとに呼び出されます。フレームはレンダリングの時間単位で、ディスプレイのリフレッシュレートに依存します。
モニターのリフレッシュレートが60Hzの場合、Draw
は1秒間に60回呼び出されます。
Hello Worldの例で呼び出しているebitenutil.DebugPrint
は、デバッグメッセージをレンダリングするユーティリティ関数です。
Draw
が呼び出される度に、screenはクリアされるため、DebugPrintは毎回実行する必要があります。
Layout
func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) { return 320, 240 }
Layout
は、ゲームの論理的な画面サイズを返す関数です。
このコードでは引数を無視して固定値を返しているので、ウィンドウサイズが変更されてもゲームの画面サイズは変わりません。
main関数の中身
ebiten.SetWindowSize(640, 480)
デスクトップ上のウィンドウサイズを設定しています。
これを呼び出さない場合は、デフォルトのウィンドウサイズが使用されます。
ebiten.SetWindowTitle("Hello, World!")
名前の通り、ウィンドウのタイトルを設定しています。
if err := ebiten.RunGame(&Game{}); err != nil { log.Fatal(err) }
ebiten.RunGame
はゲームのメインループを実行する関数です。
引数には ebiten.Game
を実装したGame
オブジェクトを渡します。
文字を使ってマップを表示してみる
マップのイメージは以下の通り。
これと同じ構成のダンジョン風のマップを文字(_
|
 ̄
)で実装してみます。コードは以下の通り。
package main import ( "log" "strings" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/ebitenutil" ) type Game struct { dungeon Dungeon player Player } type Player struct { RoomX int // プレイヤーがいる部屋のX座標 RoomY int // プレイヤーがいる部屋のY座標 } func (p *Player) SetPosition(x int, y int) { p.RoomX = x p.RoomY = y } func (p *Player) Move() { p.RoomX++ if p.RoomX > 6 { p.RoomX = 1 } } type Dungeon struct { player Player // ダンジョン内にいるプレイヤー } // ダンジョンマップ表示 func (d *Dungeon) PrintMap() string { var sb strings.Builder sb.WriteString("\n") for y := 4; y >= 1; y-- { // 1列目:壁 for x := 1; x <= 6; x++ { sb.WriteString("__________") } sb.WriteString("\n") // 2列目:見やすくするための空きスペース for x := 1; x <= 6; x++ { sb.WriteString("| |") } sb.WriteString("\n") // 3列目:プレイヤー for x := 1; x <= 6; x++ { sb.WriteString("| ") // 部屋にプレイヤーがいる場合 if x == d.player.RoomX && y == d.player.RoomY { sb.WriteString("● ") } sb.WriteString(" |") } sb.WriteString("\n") // 4列目:見やすくするための空きスペース for x := 1; x <= 6; x++ { sb.WriteString("| |") } sb.WriteString("\n") // 5列目:壁 for x := 1; x <= 6; x++ { sb.WriteString(" ̄ ̄ ̄ ̄ ̄") } sb.WriteString("\n") } return sb.String() } func (g *Game) Update() error { g.dungeon.player.Move() return nil } func (g *Game) Draw(screen *ebiten.Image) { ebitenutil.DebugPrint(screen, g.dungeon.PrintMap()) } func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) { return 320, 240 } func main() { ebiten.SetWindowSize(640, 480) ebiten.SetWindowTitle("Dungeon") p := Player{} p.SetPosition(1, 1) d := Dungeon{player: p} g := Game{dungeon: d} if err := ebiten.RunGame(&g); err != nil { log.Fatal(err) } }
これを実行すると......
あれ、想定していたものと違いますね...🤔
画面サイズやフォント等も含めて、もう少し手直しが必要そうです。(そもそもDebugPrintを使うべきではないような気もします)
というわけで、若干不完全燃焼ではありますが、今回はここまで。
触ってみた所感
- ドキュメントやサンプルコードも充実しているので、初心者でも取っ付きやすい
- Goが入っていればすぐに動かせるので、環境構築が楽
- ゲームループを自前で作らなくてもいいので、簡単なゲームならすぐ作れそう
- 以前作ったゲームでは、DrawやUpdateに相当する関数をオブジェクトごとに持っており、どう管理すべきか悩んでいたが、EbitengineではGameインターフェースを実装するだけでいいので、すっきり実装できそう
- 文字で画面描写するにはちょっとコツが必要そう
まとめ
今回は2DゲームエンジンであるEbitengine
を触ってみました。
自前でゲームループや画面描写、リフレッシュなど作る必要がないので、かなり楽にゲームを作れそうです。
本当はWebAssemblyで動かすところまでやってみる予定だったのですが、いろいろ試していたら時間がなくなったので、今回はここまでにしようと思います。
今後はWebAssemblyで動かしてみたり、以前作ったゲームの移植するのをやっていきたいと思います。
採用情報
虎の穴ラボでは一緒に働く仲間を募集中です!
この記事を読んで、興味を持っていただけた方はぜひ弊社の採用情報をご覧ください。
toranoana-lab.co.jp