Liberent-Dev’s blog

株式会社リベル・エンタテインメントのテックブログです。

ebitengineを触ってみる


こんにちは!
システム開発部のK.Mです。

はじめに

最近やっと落ち着いてきましたが、UnityのUnity Runtime Feeの件でこのままUnityを使い続けて大丈夫なのかという感じになった開発会社さんは多いかと思います。
一応、紆余曲折あり無難なところに落ち着いた感じはしますが、またUnity側でどういった変更が行われるか分からないのもあり、他のゲームエンジンに移行しようかと検討している方も居るかと思われます。

代替えとして、UnrealEngine(以降、UE)・GodotEngine・GameMaker・Cocosなどあったりしますが、今回個人的な理由でGo言語で作られた2D用ゲームエンジンebitengineを使って簡単なゲームを作るために諸々確認をしていきます。

ただし、このebitengine、UnityやUEのような専用のエディターは存在しないので、昔ながらのDirectXAPIを叩いて使う形になるので、その点注意が必要です。

インストール

今回はMacOS(M1Mac)の環境で、ebitengineをインストール・実行していきます。
ほぼebitengineのインストールページに沿った内容で確認可能ですが、少し補足を載せておきます。

goのインストール

こちらからOSに合った1.18以上のバージョンをDLします。
今回は記事記載時の最新バージョン(1.21.1)をDL&インストールしています。

DL後にpkgファイルを実行して、インストーラーに沿ってインストールしていきます。
インストール後、/usr/local/goにファイルがあることを確認して、/usr/local/go/binをPATHに登録していきます。

まずは、ターミナルでecho $SHELLを実行して表示された文字列が/bin/zshであれば.zshrc/bin/bashであれば.bash_profileのファイルにPATHを設定します。
ファイルに関しては、cd ~で自身のホームディレクトリに移動して、上記のファイルが存在しなければtouchコマンドでファイルを作成します。

作成後、または既に存在する場合はファイルを開いて、PATHを記述します。もしファイルが存在している場合は、既にPATHが設定されている可能性があるので、:/usr/local/go/bin部分を追加するのみとなる場合もあります。

export PATH=$PATH:/usr/local/go/bin

上記追加して、.zshrcを保存しターミナル一度終了して再度起動します。
go versionでバージョン表示されていればgo自体のインストールは完了です。

% go version
go version go1.21.1 darwin/arm64

Cコンパイラのインストール

% clang 
clang: error: no input files

clangコマンドをターミナルで実行して上記のログが表示されている状態であれば既にCはインストールされています。
XcodeやVisualStudioをインストールしている環境であれば、Cもインストールされている。)

インストールされていない場合は、clangコマンドの後に表示される内容に沿ってインストールするか、Xcodeをインストールしましょう。

環境の確認

インストールページの記載通りに実行して、別ウィンドウが起動して回転するGopherの絵が表示されれば、動作環境として問題無い状態です。

初めてGoを実行した場合、ホームディレクトリ内にgoフォルダが作成されていますが、環境の確認時に使用したソースコードコンパイルされたファイルなどが入っている箇所になりますので削除しないようにしましょう。
(この辺りの出力先に関しては、GOPATHの設定で変更可能になっており、デフォルトがホームディレクトリになっている形となります。)

HelloWorld

諸々の設定・確認が終わったら、こちらに記載されている内容でHelloWorldを表示するウィンドウを実装しましょう。

設定に問題が無ければ、サンプル通り簡単にHelloWorldの表示が出来ます。

この後、入出力でよく使用しそうなebitengineのAPIを記載していきます。

出力系

  • Window関連
    SetWindow~のAPIが沢山ありますが、macOS上で動かすことを考えるとHelloWorldサンプルでも使用しているSetWindowSize()とSetWindowTitle()でWindowの設定は事足りそうです。

  • 2D描画
    Image構造体のメソッド各種が2D画像を表示するための処理になっています。

    • ファイルロード
      go標準のimageデータを渡す形になっています。下記の例はpngファイルをオープンする場合です。
var drawImage *ebiten.Image
// ファイルオープン
file, _ := os.Open('pngファイルのパス')
defer file.Close()
// デコード
img, err := png.Decode(file)
if err != nil {
    panic(err)
}
drawImage = ebiten.NewImmageFromImage(img)
  • 表示
func (g *Game) Draw(screen *ebiten.Image) {
    // Draw()内でロードしたファイルを表示する
    op := &ebiten.DrawImageOption{}
    screen.DrawImage(drawImage, op)
}
  • 回転
    DrawImageOption{}GeoMで回転を設定する
func (g *Game) Draw(screen *ebiten.Image) {
    // Draw()内でロードしたファイルを表示する
    op := &ebiten.DrawImageOption{}
    op.GeoM.Rotate(0.5)
    screen.DrawImage(drawImage, op)
}
  • 拡大・縮小
    DrawImageOption{}GeoMで表示スケールを設定する
func (g *Game) Draw(screen *ebiten.Image) {
    // Draw()内でロードしたファイルを表示する
    op := &ebiten.DrawImageOption{}
    op.GeoM.Scale(0.5, 0.5) //縮小
    op.GeoM.Scale(1.5, 1.5) //拡大
    screen.DrawImage(drawImage, op)
}
  • 3D描画
    ebitengine自体が2D特化したものなので、ebitengineのAPIには3Dを描画するものは無いのですが、Goの良いところでライブラリとして3Dが使えるものは存在します。

  • サウンド
    今回は音を出す予定は無いのですが、audio.goに存在する、type Player structサウンドを再生するメソッドとtype Context structに再生するサウンドファイルを指定するメソッドが存在します。
    詳しくはaudioサンプルを確認してください。

入力系

  • キーボード
    • IsKeyPressed() キーが押されているかの判断するAPI
if ebiten.IsKeyPressed(ebiten.KeyA) {
      // キーボードのAが押された場合の処理
}
  • AppendInputChars()
    押されたキーの表示可能な文字情報を渡してくれるAPI
var runes []rune
runes = ebiten.AppendInputChars(runes)
// runesに入力された文字情報があるので下記のように出力することが可能
ebiten.DebugPrint(screen, string(runes))
  • inpututil.IsKeyJustPressed()inpututil.IsKeyJustReleased()
    現tick中(現フレーム中)に押したか、離したかを判断。
    Just系はマウスやゲームパッドにもあり、inpututil側に存在しています。
    厳密にキー入力を取りたい場合はJust系を使う必要がありそうです。
import (
      "github.com/hajimehoshi/ebiten/v2"
      "github.com/hajimehoshi/ebiten/v2/inpututil"
 )
〜略〜
if inpututil.IsKeyJustPressed(ebiten.KeyA){
      // キーボードのAが押された場合の処理
}
if inpututil.IsKeyJustReleased(ebiten.KeyA){
      // キーボードのAが離された場合の処理
}

キーアサインの定義はkeys.goのtype Keyに定義があります。
またはこちらを確認してください。

  • マウス/タッチ
    タッチ操作に関しては今回はmacOS対象なので使わないですが、モバイル向けになるとタッチは必須となりますが、恐らくTouchPosition()がタッチ操作のものなので、デスクトップ/モバイル両対応するのであれば、MouseとTouch関連のAPI両方実装する必要があります。
    • IsMouseButtonPressed()
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
      // マウスの左ボタンが押された場合の処理
}
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonRight) {
      // マウスの右ボタンが押された場合の処理
}
  • inpututil.IsMouseButtonJustPressed()inpututil.IsMouseButtonJustReleased()
import (
      "github.com/hajimehoshi/ebiten/v2"
      "github.com/hajimehoshi/ebiten/v2/inpututil"
)
〜略〜
if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft){
      // マウスの左が押された場合の処理
 }
if inpututil.IsMouseButtonJustReleased(ebiten.MouseButtonLeft){
      // マウスの左が離された場合の処理
}
  • CursorPosition()
var posX int = 0
var posY int = 0
〜略〜
// posXにカーソルのX座標、posYにカーソルのY座標が入る(原点はwindowの左上)
posX, posY = ebiten.CursorPosition()
  • TouchPosition()AppendTouchIDs()
var touchX int = 0
var touchY int = 0
var touchIDs []ebiten.TouchID
〜略〜
touchIDs = ebiten.AppendTouchIDs(touchIDs[:0])
for _, id := range touchIDs {
      touchX, touchY = ebiten.TouchPosition(id)
}

キーボードと同じようにinpututil.JustPressedTouchIDs()も存在しています。
Touch関連はAPI仕様書にも記載がありますが、AppendTouchIDs()がdesktopでは動かないので、今回のmaxOSでは動作の確認が出来ませんでした。ちなみに、ebiten.TouchPosition(0)でマウスでクリックしても反応ない状態でした。
恐らくですが、マルチタッチのためにAppendTouchIDs()で複数のTouchIDが付与されるような仕組みになっているのかと考えられます。

マウスのボタンアサインの定義はmousebutton.goのtype MouseButtonに定義があります。
またはこちらを確認してください。

  • ゲームパッド
    func Gamepad〜func StandardGamepad〜という2系統ありますが、標準的なゲームパッド用のリリースノートを見ると後からStandardGamepad系が用意されたようです。
    ゲームパッドに関しては今回は使用する予定は無いので、こちらを参考にしてください。
    サンプルだと、func Gamepad〜func StandardGamepad〜の両方対応しているので、少し大変ですが両方対応するのが無難そうです。
    キーアサインの定義はgamepad.goのtype GamepadButtonに定義があります。
    またはこちらを確認してください。

ゲームループ

入出力の方法が分かれば、ゲームループから実装していく形になります。
サンプルなどを見ていると、

    ebiten.SetWindowSize(ScreenWidth,ScreenHeight)
    ebiten.SetWindowTitle("window title")
    if err := ebiten.RunGame(game); err != nil {
        log.Fatal(err)
    }

という形でWindow周りの設定をして、RunGame()を呼び出すことでゲームループが始まり、HelloWorldのサンプルなどを見る分かるのですが、Game構造体に定義されている、Update()Draw()Layout()のメソッド実装を行うことで、ゲームループが動くようになります。

  • Update()
    ゲームロジックを記載するメソッドで、初回フレーム時にDraw()の前に必ず1度呼び出されるので、初期化処理はこちらで書く必要があります。

  • Draw()
    描画周りの処理を記載するメソッド。

  • Layout()
    ほぼ毎フレームUpdate()の前に呼ばれる、スクリーン関連の処理。 ebiten.SetWindowSize()で設定するのは、表示するWindowの縦横サイズになりますが、Layout()で返却する縦横サイズが実際に表示する縦横サイズとなります。 例えば、ebiten.SetWindowSize(640, 480)Layout()で返却する値が320, 240の場合は下記のような表示になり、

    Layout()で返却する値が640, 480の場合は下記のような表示になり、

    Layout()で返却する値が1280, 960の場合は下記のような表示になり、

    Layout()で返却する値が160, 120の場合は下記のような表示になります。

まとめ

簡単に入出力の確認が出来たので、3Dは使用しない単純な2Dゲームであればebitengineで作るのも悪くなさそうです。次回以降、何かしら簡単な2Dゲームを作っていきます。

やはり、ebitengineを触ってみて、UnityやUEなどの開発環境に慣れた人であるとGUIが存在しないので、UIや画面周りの実装は大変かもしれません。


リベル・エンタテインメントでは、このような最新技術などの取り組みに興味のある方を募集しています。
もしご興味を持たれましたら下記サイトにアクセスしてみてください。
https://liberent.co.jp/recruit/