ebitenでキャラクターが歩くモーションを作ってみる


はじめに

ゲームを作ろうと思ったら、やっぱりキャラクターに動きは必要ですよね。
とりあえず歩くモーションだけでも作りたいですよね。
私は作りたいので、作ってみました。

サンプルコードのanimationを見てみる

ebitenインストール→サンプルのanimationをみる→めっちゃヌルヌル動くやん。。。
まずはこのサンプルを解析しましょう。

必要なところだけ抽出して解説します。

const (
    screenWidth  = 320
    screenHeight = 240

    frameOX     = 0         // フレーム開始時点のX座標
    frameOY     = 32        // フレーム開始時点のY座標
    frameWidth  = 32        // 1フレームで表示する横幅
    frameHeight = 32        // 1フレームで表示する縦幅
    frameNum    = 8         // 表示させる画像の数
)

var (
    // イメージの宣言(今回省略したmain関数の中で画像自体の読み込みをしています)
    runnerImage *ebiten.Image
)

type Game struct {
    count int
}

func (g *Game) Update() error {
    // マイフレーム毎にgを増加させていく
    g.count++
    return nil
}

func (g *Game) Draw(screen *ebiten.Image) {
    // 画像のオプションを宣言
    op := &ebiten.DrawImageOptions{}

    // 画像をスクリーンの中心点へ移動
    op.GeoM.Translate(-float64(frameWidth)/2, -float64(frameHeight)/2)
    op.GeoM.Translate(screenWidth/2, screenHeight/2)

    // 5フレーム毎に1つの絵が切り替わるようにする
    // もし 5 ではなく60 で割った時、 g.count / 60 が 1になるのは 1秒後(60FPS)
    // 1 % frameNum は 1となる
    // 2秒後には 120 / 60 となるので、 2 % 8 = 2
    // つまりここの数字は何フレーム経過後に変数iが変わっていくか?を制御している
    i := (g.count / 5) % frameNum

    // sx は基準となるframeOX(=0)にframeWidth(=32)をiの数だけ足したもの
    // つまりsxはi*32の数になり、基準点が右に動いていく
    // sy は常にframeOY(=32)
    sx, sy := frameOX+i*frameWidth, frameOY

    // 表示させる画像を切り出す
    // Xの範囲は sx から sx+frameWidth
    // Yの範囲は常に sy(=32)
    // つまり、画像の中段を、5フレーム毎に右にズレながら切り出していく
    var this_frame_img *ebiten.Image = runnerImage.SubImage(image.Rect(sx, sy, sx+frameWidth, sy+frameHeight)).(*ebiten.Image)

    // 画像を表示させる
    screen.DrawImage(this_frame_img, op)
}

中段の画像とは、赤で囲んだ部分のことです。

5フレーム毎に32ピクセルずつ右にズレていきながら抜き出して表示しているため、あんな風にヌルヌル動いていたんですね。

歩行モーションを作ってみる

画像の用意

兎にも角にも、画像が必要です。
ぴぽや倉庫さんの素材と「キャラクターなんとかJ」で作りました。

モーションについて

さて、基本的な歩行モーションの付け方はサンプルと同じなのですが、1つ違うのが
「右手→ニュートラル→左手→ニュートラル→右手→...」という動き方になる点です。
左から右にループさせるだけだと
「右手→ニュートラル→左手→右手→ニュートラル→左手→...」となってしまうので、ここを考慮しながら作ります。

完成したコード

前回の記事で作った画像表示のオリジナルパッケージ使って画像表示させてます

package main

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

    "github.com/hajimehoshi/ebiten/v2"
    "github.com/hajimehoshi/ebiten/v2/ebitenutil"
    "github.com/username/projectname/src/picture"  // 画像表示のオリジナルパッケージ
)

const (
    frameOX = 0             // フレーム開始時点のX座標
    frameOY = 0             // フレーム開始時点のY座標
    frameWidth = 32     // 1フレームで表示する横幅
    frameHeight = 32    // 1フレームで表示する縦幅
    frameNum = 4            // 表示させる画像の数
)


var character *ebiten.Image

// 最初に画像を読み込む
func init() {
    var err error
    character, _, err = ebitenutil.NewImageFromFile("character.png")
    if err != nil {
        log.Fatal(err)
    }
}

type Game struct{
    count int
}

func (g *Game) Update() error{
    // 毎フレーム毎にgを増加させていく
    g.count++
    return nil
}

func(g *Game) Draw(screen *ebiten.Image){
    // 13フレームに一度画像を更新する
    i := (g.count / 13) % frameNum

    // i が3 すなわち右手→ニュートラル→左手→ここ の時にニュートラルを表示させる
    if i == 3 {
        i = 1
    }

    sx, sy := frameOX + i * frameWidth, frameOY

    // 表示する画像を切り出す
    var this_frame_img *ebiten.Image = character.SubImage(image.Rect(sx, sy, sx + frameWidth, sy + frameHeight)).(*ebiten.Image)

    // 画像を表示させる
    picture.Show(screen, this_frame_img, 1, 100,100,0)
}



func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
    return 320, 240
}

func main() {
    ebiten.SetWindowSize(640, 480)
    ebiten.SetWindowTitle("walk")
    if err := ebiten.RunGame(&Game{}); err != nil {
        log.Fatal(err)
    }
}

完成

次は十字キーで動かしたいと思います。