Help us understand the problem. What is going on with this article?

EbitenでImage操作 ~キャラクターを動かす~

はじめに

フューチャー Advent Calendar 2019 (2) の5日目です。
Qiita初投稿です。入門編は書くよりむしろ読む側ですが、どうぞお手柔らかによろしくお願いします。
今回は、今まで全くゲーム開発を経験してこなかった自分が、業務の中でIoTの情報の可視化の手段としてゲームエンジンのEbitenの利用を検証するため、実際にゲーム開発に取り組む過程でつまずいた画像(キャラクター)の移動操作についてまとめていきます。

Ebitenとは

Hajime Hoshiさんの開発された、2Dゲーム開発のための洗練されたGo のゲームライブラリです。(詳しくはこちら)

ゲーム開発に必要なAPIが非常にシンプルにまとまっているの、自分のようなゲーム開発初心者でも簡単にゲームを作ることができます。

Ebiten自体のチュートリアルやサンプルは本家が非常に充実しているので、こちらではゲーム開発特有のテクニカルな部分をまとめられればと思っています。

ゲームエンジンについて

実際の実装を見ていく前に、ゲームを走らせているゲームエンジンとは何かについて少しまとめておきます。
ゲームエンジンは、ゲームを動かすのに必要なスクリーンの描画やサウンドなどの処理を効率化するソフトウェアです。

Ebitenもその一つで、func update(screen *ebiten.Image) errorで定義した画面の変化を、ebiten.Run()によって無限ループで走らせることにより、1秒につき数十回~数百回の頻度でスクリーンの描画が更新されます。
つまり、ゲーム開発者としては、ゲームエンジンのおかげで画面や音の変化をいかに実装していくかということに集中することができる訳です。

今回はその一例としてキャラクターの画像を画面内で動かす処理を紹介しますが、その処理を先述のfunc update(screen *ebiten.Image) error内で行うことにより、ゲームが動いていることも伝わると幸いです。

キャラクターを動かしたい

ゲーム開発でビジュアル的にもなかなか面白い部分です。
ゲームにおけるキャラクターの操作は、

  1. 座標を更新する
  2. 更新後の座標の位置に画像を描画する

の繰り返しで、2.の描画についてはebitenのおかげで簡単にできるので、基本的にはいかにそれぞれのキャラクターの座標更新を行うかという課題になるのですが、キャラクターの構造体があるとして、「構造体のリスト」としてデータを保持するか、「構造体のポインタのリスト」として持つかで座標更新処理は少しだけ変わってきます。詳しくは 2 時間経過で動くオブジェクトで説明します。
まずは簡単なユーザーインプットで動かす方法から見ていきます。

1. プレイヤーの入力で動かす

Ebitenには、様々なユーザーインプットを読むAPIがあります。今回は単純のため、矢印キーで単体の海老天の画像を動かしてみます。

Spritesのソースコードを参考に、単体の海老天を矢印キーで移動させるシンプルなプログラムです。

最後にはこの海老天が矢印キーで動きます。
capture

準備

package main

import (
    "bytes"
    "fmt"
    "image"
    _ "image/png"
    "log"

    "github.com/hajimehoshi/ebiten"
    "github.com/hajimehoshi/ebiten/ebitenutil"
    "github.com/hajimehoshi/ebiten/examples/resources/images"
)

const (
    screenWidth  = 320
    screenHeight = 240
    speed        = 3
)

インスタンス生成

インスタンスspriteをポインタで保持しています。
init関数内で画像の読み込みやインスタンスの生成をしています。
以下のような手順です。

  1. 画像ファイルをebiten.Image型に変換し、その(ポインタの)変数に受け取る(ebitenには様々な画像読み込みの関数があり、ほかのやり方でも可能)
  2. 画像のサイズを変数に受け取る(ebiten.size())
  3. スクリーンのサイズに合わせ中央にキャラクターの座標を設定
  4. インスタンス化して変数に保持。
// Sprite ; holds its positions and image sizes
type Sprite struct {
    imageWidth, imageHeight, x, y int
}

var (
    ebitenImage *ebiten.Image
    sprite      *Sprite
)

func init() {
    // 手順1
    img, _, err := image.Decode(bytes.NewReader(images.Ebiten_png))
    if err != nil {
        log.Fatal(err)
    }
    origEbitenImage, _ := ebiten.NewImageFromImage(img, ebiten.FilterDefault)

    w, h := origEbitenImage.Size()
    ebitenImage, _ = ebiten.NewImage(w, h, ebiten.FilterDefault)

    op := &ebiten.DrawImageOptions{}
    ebitenImage.DrawImage(origEbitenImage, op)

    // 手順2
    w, h = ebitenImage.Size()

    // 手順3
    x, y := screenWidth/2, screenHeight/2

    // 手順4
    sprite = &Sprite{w, h, x, y}
}

動かす

ゲームエンジンの説明で述べた通り、実際のキャラクターを動かす処理をfunc update(screen *ebiten.Image) errorに実装していきます。

今回は矢印キーの入力を受け取りたいので、ebiten.IsKeyPressed()を使います。
この関数は、引数に渡したキーが押されているときtrueを返すので、trueを返している(かつ操作後に画像が画面内に収まるとき、)キー入力に対応する分の座標変更を、(sprite.x, sprite.y)に対し行っています。シンプルです。

最後に、それぞれ4つの矢印キーについての更新が終わった後の座標にキャラクターの画像を描画し、終了します。
この全体の処理が、無限ループで回ることによりゲームが動きます。

func update(screen *ebiten.Image) error {
    if ebiten.IsKeyPressed(ebiten.KeyLeft) && sprite.x-speed >= 0 {
        sprite.x -= speed
    }
    if ebiten.IsKeyPressed(ebiten.KeyRight) && sprite.x+sprite.imageWidth+speed <= screenWidth {
            sprite.x += speed
    }
    if ebiten.IsKeyPressed(ebiten.KeyUp) && sprite.y-speed >= 0 {
        sprite.y -= speed
    }
    if ebiten.IsKeyPressed(ebiten.KeyDown) && sprite.y+sprite.imageHeight+speed < screenHeight {
        sprite.y += speed
    }

    // キャラクター画像の描画
    op := &ebiten.DrawImageOptions{}
    op.GeoM.Translate(float64(sprite.x), float64(sprite.y))
    screen.DrawImage(ebitenImage, op)

    return nil
}

func main() {
    if err := ebiten.Run(update, screenWidth, screenHeight, 2, "Sprites (Ebiten Demo)"); err != nil {
        log.Fatal(err)
    }
}

2. 時間経過で動くオブジェクト

公式のExampleであるSpritesはこの良い例かと思います。時間経過で海老天が画面内でバウンドしながら移動します。(ぜひ公式に飛んで動いているところを見てみてください。)
capture

このような時間経過でランダム(周辺でぶらつくNPCなど)、もしくは規則的に動き続けるケースは、ポインタで保持したオブジェクトに対し、ポインタレシーバの座標更新メソッドを定義してあげます。ここで、処理の実装が「構造体のリスト」か「構造体のポインタのリスト」かで少しだけ異なるので、そこから見てみます。

構造体のリスト

説明を簡単にするために、以下のようなケースを考えます。
以下のようなObjという構造体があるとし、その中の値vを更新するためにそのポインタをレシーバに持つUpdate()関数を以下のように定義します。
これにより、インスタンスについてUpdate()を呼ぶたびに中の変数vが1ずつ増えるという、1次元の座標更新と考えてみましょう。

package main

import "log"

type Obj struct {
    v int
}

func (o *Obj) Update() {
    o.v += 1
}

構造体Objのリスト(スライス)objsを使い、キャラクターを管理してみます。
まずv=0で初期化したObjインスタンスを4つ生成し、objsに追加します。
この4つのインスタンスについて、100回座標更新を行ってみましょう。
この際注意しなければならないのが

  • ポインタレシーバの関数は、ポインタに対してもインスタンスに対しても呼び出せる
  • Go言語の特性上、forループでアクセスしたオブジェクトのコピーが渡されるため、for文内のoはコピー。Updateを行った後の再代入を行う必要がある

といった点です。
こちら Go Playground で試せます。

func main() {
    // 空のスライスの生成
    objs := make([]Obj, 0)

    // 4つのインスタンスをスライスに追加
    for i := 0; i < 4; i++ { 
        new_o := Obj{0}
        objs = append(objs, new_o)
    }

    // それぞれのインスタンスを100回座標更新してみる
    for i := 0; i < 100; i++ { 
        for i, o := range objs {
            // (&o).Update()としなくてよい
            o.Update()
            log.Printf("%d ", o.v)

            // コピーに対してしか更新できてないので、再代入
            objs[i] = o
        }
    }   
}

構造体のポインタのリスト

同様のObjUpdate()に対し、Objのポインタのリスト(スライス)で管理すると以下のようになります。
見てわかる通り、ポインタのリストのほうがわかりやすいので、この記事ではこの方法を採用しています。

こちら Go Playground で試せます。

func main() {
    // 空のポインタのスライスを生成
    objs := make([]*Obj, 0)

    for i := 0; i < 4; i++ { 
        // インスタンス生成し、ポインタを受け取りスライスへappend
        new_o := &Obj{0}
        objs = append(objs, new_o)
    }

    for i := 0; i < 4; i++ { 
        for _, o := range objs {
            o.Update()
            log.Printf("%d ", o.v)
            // ポインタに対し処理したので再代入は不要
        }
    }


}

時間経過で動かす

あるキャラクターの x, y座標を持つ構造体Characterがあるとすると、下記のようにCharacter型のポインタをレシーバに持つfunc (c *Character) Update()メソッドを定義します。

type Character struct {
    x, y int
}

// ここでは複数のキャラクターを想定しているのでポインタのリストで管理
var characters []*Character

// Character型に対する座標更新
func (c *Character) Update() {
    // 座標更新の処理
    // 例
    c.x += 1
    x.y += 1
}

その後、ebitenのupdate内で各characterに対しCharacter.Update()を呼ぶことで、座標を更新します。(例のようなメソッドだと、毎回のupdateで画面上のキャラクターは右に1ピクセル、下に1ピクセル移動します。)

// ebitenのupdate関数内
func update(screen *ebiten.Image) error {
    // 任意の処理

    for _, c := range characters {
        c.Update()
    }
    // 任意の処理
}

これで時間経過でも(ユーザーインプットに依存しないような)動くキャラクターの処理をすることができます。

最後に

記事の内容について、よりよい実装方法などあればぜひコメントお待ちしております。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした