はじめてのゲームプログラミング(Ebiten)
はじめに
こんにちは、@hokita222 です。この記事はGo Advent Calendar 2021の13日目の記事です。
2021年の今年は某プログラミングゲームが発売されエンジニア界隈でも大流行しました。今回はせっかくなので go でゲームを作ってみたいと思い Ebiten に挑戦してみました。
筆者のスペック
- 業務では Ruby on Rails で web アプリを開発
- go は現状趣味(仕事で書きたい!)
- ゲームプログラミング経験ゼロ
Ebiten
Ebiten とは go 言語で作成できる OSS のゲームライブラリです。特徴のひとつでとてもシンプルということでゲームプログラミング初心者の自分でもできるかなと思いチャレンジしてみました。
学習
ドキュメントもありますが、サンプルコードがいくつか用意されており、pull してくれば手元の環境でもgo run main.go
で動かすことができるので、実際に動かしながらコード読みつつ学習することができました。
作ったゲーム
できるだけシンプルで簡単なゲームがいいなと考えていたのですが、
「おぼろげながら浮かんできたんです、恐竜が障害物をジャンプするというゲームが」
ということで、ワンボタンでできる横スクロールのゲームを作成してみました。
完成品
コードの概要
Ebiten はebiten.Game
インターフェースを満たす構造体を用意してfunc RunGame(game Game) error
を呼ぶだけでゲームを実行することができます。
ebiten.Game
インターフェースには3つのメソッドを持ちます。それぞれのメソッドにゲームに必要な処理を記載していけば OK です。
Update() error
Draw(screen *Image)
Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int)
cf. https://ebiten.org/documents/cheatsheet.html#General
今回作ったゲームに当てはめますと下記のようになります。
-
Update
- 各値の更新、判定、ボタン押下時の処理、画面切り替えなど
-
Draw
- 恐竜、障害物などの画像やスコアなどの文字列の表示、非表示
-
Layout
- スクリーンサイズを設定
Update
func (g *Game) Update() error {
switch g.mode {
// タイトル画面
case modeTitle:
if g.isKeyJustPressed() {
g.mode = modeGame
}
// ゲーム画面
case modeGame:
g.count++
g.score = g.count / 5
// ボタン押下時
if !g.jumpFlg && g.isKeyJustPressed() {
g.jumpFlg = true
g.gy = -jumpingPower
}
// ジャンプ中
if g.jumpFlg {
g.dinosaurY += g.gy
g.gy += gravity
}
// 着地
if g.dinosaurY >= groundY-dinosaurHeight {
g.jumpFlg = false
}
// 障害物の動作
for _, t := range g.trees {
if t.visible {
t.move(speed)
if t.isOutOfScreen() {
t.hide()
}
} else {
if g.count-g.lastTreeX > minTreeDist && g.count%interval == 0 && rand.Intn(10) == 0 {
g.lastTreeX = g.count
t.show()
break
}
}
}
// 地面の動作
g.ground.move(speed)
if g.hit() {
g.mode = modeGameover
}
// ゲームオーバー画面
case modeGameover:
if g.isKeyJustPressed() {
g.init()
g.mode = modeGame
}
}
return nil
}
func (g *Game) isKeyJustPressed() bool {
// ボタン押下を検知
if inpututil.IsKeyJustPressed(ebiten.KeySpace) {
return true
}
return false
}
Update
では各値の更新処理を記載していきます。例えばゲーム画面でボタンを押下した際はジャンプフラグをオンにして、y 軸方向への力をセットします。また、障害物の出現条件を満たす場合は出現フラブをオンにしたり、地面を x 軸へスクロールさせるためにxの値を変化させたりしております。ボタン押下を検知するメソッドはプラグインで用意されているので簡単に実装することができます。
Draw
func (g *Game) Draw(screen *ebiten.Image) {
// 背景
screen.Fill(color.White)
// スコアの描画
text.Draw(screen, fmt.Sprintf("Hisore: %d", g.hiscore), arcadeFont, 300, 20, color.Black)
text.Draw(screen, fmt.Sprintf("Score: %d", g.score), arcadeFont, 500, 20, color.Black)
g.drawGround(screen)
g.drawTrees(screen)
g.drawDinosaur(screen)
// タイトル画面、ゲームオーバー画面で描画する文字列を変えている
switch g.mode {
case modeTitle:
text.Draw(screen, "PRESS SPACE KEY", arcadeFont, 245, 240, color.Black)
case modeGameover:
text.Draw(screen, "GAME OVER", arcadeFont, 275, 240, color.Black)
}
}
// 恐竜の描画
func (g *Game) drawDinosaur(screen *ebiten.Image) {
op := &ebiten.DrawImageOptions{}
// 表示位置
op.GeoM.Translate(baseX, float64(g.dinosaurY))
op.Filter = ebiten.FilterLinear
// 走っているように見えるように2枚の画像を交互に表示している
if (g.count/5)%2 == 0 {
screen.DrawImage(dinosaur1Img, op)
return
}
screen.DrawImage(dinosaur2Img, op)
}
Draw
では画面に描画する内容を記載します。Update
で更新された値を元に恐竜や障害物などの画像を描画しています。プラグインで用意されているメソッド名がわかりやすいのでソースコードを読んだだけでも何を描画しているのか予測できるかと思います。
Layout
const (
screenX = 640
screenY = 480
)
// Layout method
func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
return screenX, screenY
}
今回はスクリーンのサイズを 640x480 にセットしました。
main
func main() {
ebiten.SetWindowSize(screenX, screenY)
ebiten.SetWindowTitle("Dinosaur Jump")
if err := ebiten.RunGame(NewGame()); err != nil {
log.Fatal(err)
}
}
main 関数ではウィンドウサイズ、ウィンドウに表示されるタイトルを指定し、func RunGame(game Game) error
を呼び出しているだけです。
以上が主な処理です。まだ修正が必要な箇所も多いと思いますが、現状約400行程度のソースコードでこのレベルのゲームなら作れてしまいます。
最後に
私のようなゲームプログラミング初心者でも Ebiten でゲームを作ることができました。go 言語は 誰が書いても同じ形式になる ことで有名だと思いますが、サンプルコードなども迷わず読むことができました。皆さんもこれを機にトライしてみていただけたら嬉しいです。
Discussion