ebitengine-game-dev
Go の 2D ゲームエンジン Ebitengine (v2) を使ったゲーム開発スキル。プロジェクト構築、スプライト描画、アニメーション、オーディオ(BGM/SE)、キーボード&タッチ入力、シーン管理、WASM ビルド、GitHub Pages デプロイまでを網羅。Use when building 2D games with Go and Ebitengine, adding sprites/audio/touch controls, or deploying Go games to the web via WASM.
Works with
Agent Skills format with YAML frontmatter. Claude Code reads it as-is.
---
name: "ebitengine-game-dev"
description: "Go の 2D ゲームエンジン Ebitengine (v2) を使ったゲーム開発スキル。プロジェクト構築、スプライト描画、アニメーション、オーディオ(BGM/SE)、キーボード&タッチ入力、シーン管理、WASM ビルド、GitHub Pages デプロイまでを網羅。Use when building 2D games with Go and Ebitengine, adding sprites/audio/touch controls, or deploying Go games to the web via WASM."
license: "MIT"
---
# Ebitengine Game Development Skill
Go の 2D ゲームエンジン [Ebitengine](https://ebitengine.org/) (v2) を使ったゲーム開発の包括的なガイド。
プロジェクトのゼロからの構築、スプライト描画、オーディオ、入力処理、WASM デプロイまでをカバーする。
## When to Use This Skill
- Go で 2D ゲームを開発する
- Ebitengine のプロジェクトをセットアップする
- スプライト画像をゲームに統合する
- BGM / SE(効果音)を実装する
- キーボード&タッチ(スマホ・タブレット)入力を実装する
- シーン管理(タイトル画面・プレイ・ゲームオーバー)を実装する
- Go ゲームを WASM にビルドして GitHub Pages にデプロイする
## Ebitengine の基本アーキテクチャ
### `ebiten.Game` インターフェース(3つの必須メソッド)
```go
type Game struct { /* ゲーム状態 */ }
// Update: ゲームロジック。毎秒60回(60TPS)呼ばれる。
func (g *Game) Update() error { return nil }
// Draw: 画面の描画。毎フレーム呼ばれる。
func (g *Game) Draw(screen *ebiten.Image) {}
// Layout: 論理画面サイズを返す。ウィンドウサイズと独立。
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
return screenWidth, screenHeight
}
```
### ゲームの起動
```go
func main() {
ebiten.SetWindowSize(640, 480)
ebiten.SetWindowTitle("My Game")
if err := ebiten.RunGame(&Game{}); err != nil {
log.Fatal(err)
}
}
```
### 座標系
- 原点: 左上 (0, 0)
- X軸: 右が正
- Y軸: **下が正**(上に移動 = Y が減る)
## プロジェクトセットアップ
```bash
go mod init <module-name>
go get github.com/hajimehoshi/ebiten/v2
```
### 推奨ディレクトリ構成
```
project/
├── assets/ # 画像・音声ファイル
├── main.go # ゲーム本体(小規模なら1ファイルで十分)
├── go.mod
├── index.html # ブラウザ版(WASM デプロイ時)
├── wasm_exec.js # Go WASM ランタイム(WASM デプロイ時)
└── .github/workflows/deploy.yml # GitHub Pages 自動デプロイ
```
## アセットの埋め込み(embed)
Go の `embed` パッケージでアセットをバイナリに埋め込む。
WASM ビルドでも外部ファイルアクセス不要になるため **必須**。
```go
import "embed"
//go:embed assets/*.png assets/*.mp3
var assetsFS embed.FS
```
### 画像の読み込み
```go
import "image/png"
func loadImage(name string) *ebiten.Image {
f, err := assetsFS.Open("assets/" + name + ".png")
if err != nil {
log.Fatal(err)
}
defer f.Close()
img, _ := png.Decode(f)
return ebiten.NewImageFromImage(img)
}
```
## 描画 API
### 背景の塗りつぶし
```go
screen.Fill(color.RGBA{0, 0, 0, 255})
```
### スプライト描画(スケーリング付き)
```go
func drawSprite(screen, sprite *ebiten.Image, x, y, scale float64) {
op := &ebiten.DrawImageOptions{}
op.GeoM.Scale(scale, scale)
op.GeoM.Translate(x, y)
screen.DrawImage(sprite, op)
}
```
### 色の調整(減光・フィルタ)
```go
op := &ebiten.DrawImageOptions{}
op.ColorScale.Scale(0.3, 0.3, 0.3, 1) // RGB を 30% に減光
screen.DrawImage(img, op)
```
### デバッグテキスト
```go
ebitenutil.DebugPrint(screen, "左上に表示")
ebitenutil.DebugPrintAt(screen, "指定位置", x, y)
```
### 矩形の描画
```go
ebitenutil.DrawRect(screen, x, y, width, height, color.RGBA{...})
```
### 半透明オーバーレイ
```go
overlay := ebiten.NewImage(screenWidth, screenHeight)
overlay.Fill(color.RGBA{0, 0, 0, 160}) // 半透明の黒
screen.DrawImage(overlay, nil)
```
## 入力処理
### キーボード
```go
// キーが押されている間ずっと true
if ebiten.IsKeyPressed(ebiten.KeyArrowUp) { ... }
// キーが押された瞬間だけ true(メニュー操作等に最適)
if inpututil.IsKeyJustPressed(ebiten.KeySpace) { ... }
```
### タッチ入力(スマホ・タブレット)
```go
// 新しいタッチの検出
touchIDs := inpututil.AppendJustPressedTouchIDs(nil)
if len(touchIDs) > 0 {
id := touchIDs[0]
x, y := ebiten.TouchPosition(id)
}
// タッチの終了検出
if inpututil.IsTouchJustReleased(id) { ... }
```
### スワイプ検出パターン
```go
const swipeThreshold = 16 // 最小スワイプ距離
type Game struct {
touchID ebiten.TouchID
touchStartX int
touchStartY int
touchTracking bool
}
func (g *Game) handleSwipe() {
// 1. 新しいタッチ → touchStart を記録、touchTracking = true
// 2. タッチ中 → 現在位置と開始位置の差分を計算
// 3. 差分が swipeThreshold を超えたら方向を確定
// 4. 確定後、touchStart を現在位置に更新(連続スワイプ対応)
// 5. タッチ終了 → touchTracking = false
}
```
### タッチ入力のUI判定(ボタンタップ)
```go
// ボタン矩形との当たり判定
touchIDs := inpututil.AppendJustPressedTouchIDs(nil)
for _, id := range touchIDs {
tx, ty := ebiten.TouchPosition(id)
if tx >= btnX && tx <= btnX+btnW && ty >= btnY && ty <= btnY+btnH {
// ボタンがタップされた
}
}
```
## オーディオ
### 必要なインポート
```go
import (
"github.com/hajimehoshi/ebiten/v2/audio"
"github.com/hajimehoshi/ebiten/v2/audio/mp3"
)
```
### BGM(無限ループ再生)
```go
var audioCtx = audio.NewContext(48000)
func initBGM() {
data, _ := assetsFS.ReadFile("assets/bgm.mp3")
stream, _ := mp3.DecodeWithoutResampling(bytes.NewReader(data))
loop := audio.NewInfiniteLoop(stream, stream.Length())
player, _ := audioCtx.NewPlayer(loop)
player.SetVolume(0.2)
player.Play()
}
```
### SE(効果音)— デコード済みデータをメモリ保持
```go
// 起動時に一度だけデコード
func decodeSE(name string) []byte {
data, _ := assetsFS.ReadFile("assets/" + name + ".mp3")
stream, _ := mp3.DecodeWithoutResampling(bytes.NewReader(data))
decoded, _ := io.ReadAll(stream)
return decoded
}
// 再生時: 毎回新しいプレイヤーを作成(重複再生可能)
func playSE(data []byte) {
player, _ := audioCtx.NewPlayer(bytes.NewReader(data))
player.SetVolume(0.33)
player.Play()
}
```
### 音量制御(ミュート対応)
```go
// BGM の音量をミュート状態に応じて切り替え
func (g *Game) applyVolume() {
if g.muted {
bgmPlayer.SetVolume(0)
} else {
bgmPlayer.SetVolume(bgmVolume)
}
}
// SE はミュート中なら再生しない
func playSE(data []byte, muted bool) {
if muted { return }
// ...
}
```
## シーン管理パターン
タイトル画面・プレイ中・ゲームオーバーなど、複数の画面を管理する設計パターン。
```go
const (
sceneTitle = iota
scenePlaying
sceneGameOver
)
type Game struct {
scene int
// ... 各シーンの状態
}
func (g *Game) Update() error {
switch g.scene {
case sceneTitle:
g.updateTitle()
case scenePlaying:
g.updatePlaying()
case sceneGameOver:
g.updateGameOver()
}
return nil
}
func (g *Game) Draw(screen *ebiten.Image) {
switch g.scene {
case sceneTitle:
g.drawTitle(screen)
case scenePlaying:
g.drawPlaying(screen)
case sceneGameOver:
g.drawPlaying(screen) // ゲーム画面を背景に
g.drawGameOver(screen) // オーバーレイ
}
}
```
## 移動速度の制御
`Update()` は毎秒60回呼ばれるため、ゲームオブジェクトの移動はフレームカウンタで間引く。
```go
const moveInterval = 15 // 15フレームごと = 4回/秒
func (g *Game) Update() error {
g.tickCount++
if g.tickCount < moveInterval {
return nil // まだ移動タイミングでない
}
g.tickCount = 0
// ここで移動処理
}
```
### 難易度による速度変更
```go
var difficultySpeed = [3]int{20, 12, 7} // Easy, Normal, Hard
// moveInterval の代わりに difficultySpeed[g.difficulty] を使う
if g.tickCount < difficultySpeed[g.difficulty] {
return nil
}
```
## UI パターン
### ボタン描画(矩形 + 枠線 + テキスト)
```go
// ボタン背景
ebitenutil.DrawRect(screen, x, y, w, h, bgColor)
// 枠線
drawBorder(screen, x, y, w, h, borderColor)
// テキスト(中央揃え)
ebitenutil.DebugPrintAt(screen, label, x+w/2-len(label)*3, y+h/2-4)
func drawBorder(screen *ebiten.Image, x, y, w, h int, c color.RGBA) {
fx, fy, fw, fh := float64(x), float64(y), float64(w), float64(h)
t := 2.0
ebitenutil.DrawRect(screen, fx, fy, fw, t, c)
ebitenutil.DrawRect(screen, fx, fy+fh-t, fw, t, c)
ebitenutil.DrawRect(screen, fx, fy, t, fh, c)
ebitenutil.DrawRect(screen, fx+fw-t, fy, t, fh, c)
}
```
### ポーズボタン(⏸ アイコン)
```go
// 右上に常時表示する小さなポーズボタン
px, py, pw, ph := screenWidth-40, 8, 32, 32
ebitenutil.DrawRect(screen, px, py, pw, ph, color.RGBA{0, 0, 0, 100})
// 2本の縦線で⏸アイコン
barW, barH, gap := 4.0, 22.0, 6.0
cx := float64(px) + float64(pw)/2
cy := float64(py) + 5
ebitenutil.DrawRect(screen, cx-gap/2-barW, cy, barW, barH, color.RGBA{200, 200, 200, 255})
ebitenutil.DrawRect(screen, cx+gap/2, cy, barW, barH, color.RGBA{200, 200, 200, 255})
```
## WASM ビルド & GitHub Pages デプロイ
### ビルド確認
```bash
GOOS=js GOARCH=wasm go build -o game.wasm .
```
### 必要ファイル
1. **`wasm_exec.js`**: Go ランタイムからコピー
```bash
cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" .
```
2. **`index.html`**: ゲーム埋め込み用 HTML
- `<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">`
- `touch-action: none` でタッチのスクロール・ズームを無効化
- canvas に `aspect-ratio` を設定して Layout の縦横比と一致させる
- `WebAssembly.instantiateStreaming(fetch("game.wasm"), go.importObject)`
### GitHub Actions ワークフロー
```yaml
name: Deploy to GitHub Pages
on:
push:
branches: [main]
permissions:
contents: read
pages: write
id-token: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- run: GOOS=js GOARCH=wasm go build -o game.wasm .
- run: |
mkdir -p _site
cp game.wasm wasm_exec.js index.html _site/
- uses: actions/upload-pages-artifact@v3
with:
path: _site
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v4
```
### GitHub Pages の有効化(CLI)
```bash
# リポジトリを Public に(無料プランで必要)
gh repo edit <owner/repo> --visibility public --accept-visibility-change-consequences
# Pages を GitHub Actions ソースで有効化
gh api repos/<owner/repo>/pages -X POST -f build_type=workflow
```
## 重要な注意点
### 180度反転防止(スネークゲーム等)
入力を即座に適用せず、`nextDir` バッファに保存して移動タイミングで適用する。
```go
// 入力時: nextDir に保存(逆方向は無視)
if ebiten.IsKeyPressed(ebiten.KeyArrowUp) && g.direction != dirDown {
g.nextDir = dirUp
}
// 移動時: nextDir を direction に適用
g.direction = g.nextDir
```
### ゲームリスタートのパターン
```go
// ポインタの中身を差し替え。Ebitengine が保持するポインタはそのまま。
*g = *NewGame()
```
### embed と WASM の互換性
`embed` で埋め込んだアセットは WASM ビルドでもそのまま動作する。
`os.Open` はブラウザでは使えないため、常に `embed.FS` を使うこと。
## 参考リンク
- [Ebitengine 公式サイト](https://ebitengine.org/)
- [Ebitengine サンプル集](https://ebitengine.org/en/examples/)
- [API リファレンス (pkg.go.dev)](https://pkg.go.dev/github.com/hajimehoshi/ebiten/v2)
- [wasmgame テンプレート](https://github.com/eihigh/wasmgame)More General & Other skills
find-skills
vercel-labs/skills
Helps users discover and install agent skills when they ask questions like "how do I do X", "find a skill for X", "is there a skill that can...", or express interest in extending capabilities. This skill should be used when the user is looking for functionality that might exist as an installable skill.
grill-me
mattpocock/skills
A relentless interview to sharpen a plan or design.
grill-with-docs
mattpocock/skills
A relentless interview to sharpen a plan or design, which also creates docs (ADR's and glossary) as we go.

