ebitengine 使ってみた: デプロイ編

作ったもの: https://ebiten-icecream-game.vercel.app/

スクショ↓

デプロイ編ってあるけど今のところこれだけ。あとでコード編も書くかも?

背景/お気持ち

Golang さわりたかったんだけど、TODO アプリ作るにはモチベが足りなかったので、とりあえず ebitengine 触ってみた。1個目は ChatGPT と Cursor に土台ほぼ書いてもらってこまかいところだけ修正してたんだけど、さすがに学んだ感がゼロだったので、ChatGPT の壁打ちだけで書いてみた。

画像系は ChatGPT (というか DALLE) と Figma でつくって、

  • アイス: Figmaフルスクラッチ
  • 炎: DALLE に作ってもらったやつを Figma でトレスして好みの感じに調整
  • 背景: DALLE に作ってもらって色合いだけ Figma で調整

iPad を開くのすら面倒だったので Figma で画像つくってみたけど、意外と行けた。

サウンドはフリーのやつをとってきた。

今回やれなかった系でいくと、ヒット時のエフェクトがあるとかっこよかったかも。あとクリア条件がなくて永遠に数字が上がっていくので、これは後で何か考える...予定。

言語面だと、 ChatGPT が「今後これ学んでいけばいいんじゃない」って言ってた。

(とりあえず) デプロイ

↓の記事を見ると Vercel にデプロイできるとあったので Vercel にした

ただ、とりあえずさっさと上げたくて express サーバを立てることすら面倒だったので、以下の手順でやった。

事前作業: 画像とかを embed する

まず、ファイルを os.OpenFile("/path/to/file") でやっている場合、ブラウザで動かないので embed パッケージを使ったやり方に書き換える。 embed を使うと、ファイルではなくデータが返ってくるので、周りの関数もちょっと書き換えが必要。

audio, image, font の書き方はだいたいこんな感じ。(既存コードを整形して動作確認してないのでガバあるかも)

package main

import (
    "embed"
    _ "embed"
    "log"

    "bytes"

    "github.com/hajimehoshi/ebiten/v2"
    "github.com/hajimehoshi/ebiten/v2/audio"
    "github.com/hajimehoshi/ebiten/v2/audio/mp3"
    "github.com/hajimehoshi/ebiten/v2/ebitenutil"
    "github.com/hajimehoshi/ebiten/v2/text/v2"
)

var (
    audioContext *audio.Context

    //go:embed assets/fonts
    fontsDir embed.FS
    //go:embed assets/images
    imagesDir embed.FS
    //go:embed assets/sounds
    soundsDir embed.FS
)

func loadImage(filepath string) *ebiten.Image {
    data, err := imagesDir.ReadFile(filepath)
    if err != nil {
        log.Fatal(err)
    }
    img, _, err := ebitenutil.NewImageFromReader(bytes.NewReader(data))
    if err != nil {
        log.Fatal(err)
    }
    return img
}

func loadAudio(filepath string) *audio.Player {
    data, err := soundsDir.ReadFile(filepath)
    if err != nil {
        log.Fatal(err)
    }

    // Decode the MP3 file
    stream, err := mp3.DecodeWithSampleRate(audioContext.SampleRate(), bytes.NewReader(data))
    if err != nil {
        log.Fatal(err)
    }

    // Create an audio player
    player, err := audioContext.NewPlayer(stream)
    if err != nil {
        log.Fatal(err)
    }
    return player
}

func loadFontSource(filepath string) *text.GoTextFaceSource {
    data, err := fontsDir.ReadFile(filepath)
    if err != nil {
        log.Fatal(err)
    }

    source, err := text.NewGoTextFaceSource(bytes.NewReader(data))
    if err != nil {
        log.Fatal(err)
    }
    return source
}

ローカルでビルド

(1) public/ ディレクトリをつくる

.
├── README.md
├── main.go
├── ... (other go files)
└── public

(2) Go ファイルのビルド

↓を見ながらやりつつ、 public/ フォルダに作られるようにする

# build the wasm file
env GOOS=js GOARCH=wasm go build -o public/icecream-game.wasm

# copy wasm_exec.js to execute the WebAssembly binary
cp $(go env GOROOT)/misc/wasm/wasm_exec.js public/

(3) HTML ファイルの作成

main.htmlindex.html の2つをつくる。ファイル名とか iFrame のサイズは適宜変えてね。

main.html

<!DOCTYPE html>
<style>
  canvas {
    width: 480px;
    height: 640px;
  }
</style>
<script src="wasm_exec.js"></script>
<script>
  const go = new Go();
  WebAssembly.instantiateStreaming(fetch("icecream-game.wasm"), go.importObject).then(result => {
    go.run(result.instance);
  });
</script>

index.html

(フォントとかは完全に趣味なのでよしなに変えてね)

<!DOCTYPE html>
<html>

<head>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Cherry+Bomb+One&display=swap" rel="stylesheet">
  <style>
    h1 {
      font-weight: normal;
      color: #174359;
    }

    h1,
    p {
      font-family: 'Cherry Bomb One';
    }

    .container {
      width: 100%;
      height: 100%;
      display: flex;
      justify-content: center;
      align-items: center;
      flex-direction: column;

    }
  </style>
</head>

<body>
  <div class="container">
    <h1>Ice Cream Game</h1>
    <p>tip: use <- and -> to move</p>
    <iframe src="main.html" width="480" height="640"></iframe>
  </div>
</body>

いちおう main.htmlindex.html として iframe に入れずにサーブしても動くんだが、ゲーム画面がブラウザの最大幅になっちゃって変え方がすぐわからなかったのと、docs に ↓のようにあったので iframe に入れた。

If you want to embed your game into your web page, using iframe is strongly recommended. The screen scale is automatically adjusted. If the above HTML's name is main.html, the host HTML will be like this: ...

(4) 動作確認

# run server
cd public
python3 -m http.server

(別に python のサーバじゃなくても OK)

(5) Git リポジトリに push

注意点として、通常の frontend 開発とは違って public はコミットするのと、ソースコードを変更したら毎回ビルドを走らせる必要がある。

git add public
git push origin main

Vercel の設定

(1) あたらしい project を作成 & Git リポジトリに紐づけ

(2) コマンドの設定

↓のスクショの通りだけど、

  • build command も install command もなし (true で設定)
  • output ディレクトリを public/ 二設定

(3) デプロイ

これで動けば OK

改善点

上でもちょろっと書いたけど、

  • ソースコードを変更したらローカルでビルドコマンドを走らせる必要がある
  • public ディレクトリをコミットする必要がある

あたりが課題。Vercel で go のコマンド動かせられるのか不明だったのでこのやり方にした。