ebitenでキャラクターをアニメーションさせる時にちょっと楽できる構造体作ったん

golangでゲームが作れる素敵なライブラリ「[ebiten](https://hajimehoshi.github.io/ebiten/)」を使い始めました。楽しい！٩( 'ω' )و 
「[ねこむすめ道草日記](http://www.comic-ryu.jp/_nekomusume/)」の同人ゲーム「[黒菜んダッシュ（未完）](https://github.com/KemoKemo/KuronanDash)」を[ebitenに移植](https://github.com/KemoKemo/go-kuronandash)しつつ完成させる企みです。

# キャラクターのアニメーション

ゲームでキャラクターをアニメーション表示する際、以下のようなパターンがあるかと思います。

* キャラクターが一定歩数だけ動いたら次のフレームに切り替え
* 一定時間が経過したら次のフレームに切り替え

前者はアクションゲームの自キャラなどで多く見かけますし、後者はRPGの自キャラやアクションゲームのアイテムなどで多いように思います。いずれのパターンでも、歩数や時間をしきい値とくらべて現在表示すべきフレームを決める処理が必要になります。

今回ご紹介するのは、あらかじめしきい値となる値を決めておけば、この面倒な「**現在表示すべきフレームを決める処理**」をまるっとお任せできる構造体を作りました。もちろん**ebiten用**です。

# アニメーション構造体

## 歩数でアニメーションする版

![step_animation.gif](https://qiita-image-store.s3.amazonaws.com/0/166225/b0770c38-7015-7aae-f710-c1656ef2c9ac.gif)

動作サンプルということで、大好きな独楽ちゃんに動いてもらっています☆

```Go
// StepAnimation is an animation.
// This animates according to the number of steps.
type StepAnimation struct {
	ImagesPaths     []string
	DurationSteps   int
	once            sync.Once
	frames          []*ebiten.Image
	maxFrameNum     int
	currentFrameNum int
	walkedSteps     int
}

// Init loads images and initializes private parameters.
// If you call this function multiple times, it is only the
// first time to load the images.
func (s *StepAnimation) Init() (err error) {
	s.once.Do(func() {
		s.frames, err = loadImages(s.ImagesPaths)
		s.maxFrameNum = len(s.ImagesPaths)
	})
	if err != nil {
		return err
	}
	s.currentFrameNum = 0
	s.walkedSteps = 0
	return nil
}

func loadImages(paths []string) ([]*ebiten.Image, error) {
	if paths == nil || len(paths) == 0 {
		err := fmt.Errorf("paths is empty, please set valid path info of images")
		return nil, err
	}
	frames := []*ebiten.Image{}
	for _, path := range paths {
		image, _, err := ebitenutil.NewImageFromFile(path, ebiten.FilterNearest)
		if err != nil {
			return nil, err
		}
		frames = append(frames, image)
	}
	return frames, nil
}

// AddStep adds steps information. If your character moved, please
// call this function with steps information.
func (s *StepAnimation) AddStep(steps int) {
	s.walkedSteps += steps
}

// GetCurrentFrame returns a current frame image. This function determines
// the current frame based on the information on how far a character moved.
// If the sum of steps is grater than the DurationSteps, this function will
// return the next frame.
func (s *StepAnimation) GetCurrentFrame() *ebiten.Image {
	if s.walkedSteps > s.DurationSteps {
		s.currentFrameNum++
		s.walkedSteps = 0
	}
	if s.currentFrameNum < 0 || s.maxFrameNum-1 < s.currentFrameNum {
		s.currentFrameNum = 0
	}
	return s.frames[s.currentFrameNum]
}
```

### 初期化

まず以下のように初期化します。

```Go
imagePaths := []string{
	"images/game/koma_00.png",
	"images/game/koma_01.png",
	"images/game/koma_02.png",
	"images/game/koma_03.png",
}
animation := StepAnimation{
	ImagesPaths:   imagePaths, // 読み込む画像のパス配列
	DurationSteps: 5, // 5歩うごくとフレームが切り替わる設定
}
err := c.animation.Init()
if err != nil {
	return err
}
```

読み込む画像のパスと何歩うごけばフレームを切り替えるか、という情報を指定して`Init`メソッドで初期化します。
キャラクター構造体などを別途作って、その初期化処理の中であわせて処理してもらえるといい感じだと思います。

### キャラクターが動いた時の処理

以下のように、動いた歩数を引数に渡してAddStepメソッドを呼びます。

```Go
// Move moves the character regarding the user input.
func (c *Character) Move() {
	c.moved = false
	if ebiten.IsKeyPressed(ebiten.KeyLeft) || ebiten.GamepadAxis(0, 0) <= -0.5 {
		c.position.X--
		c.moved = true
	}
	if ebiten.IsKeyPressed(ebiten.KeyRight) || ebiten.GamepadAxis(0, 0) >= 0.5 {
		c.position.X++
		c.moved = true
	}
	if ebiten.IsKeyPressed(ebiten.KeyUp) || ebiten.GamepadAxis(0, 1) <= -0.5 {
		c.position.Y--
		c.moved = true
	}
	if ebiten.IsKeyPressed(ebiten.KeyDown) || ebiten.GamepadAxis(0, 1) >= 0.5 {
		c.position.Y++
		c.moved = true
	}

	if c.moved {
		c.animation.AddStep(1)
	}
}
```

### 描画するよ

単にGetCurrentFrameメソッドを呼ぶだけで現在表示すべき`*ebiten.Image`フレームが返って来ますので、
`screen`の`DrawImage`に渡して描画しましょう。

```Go
// Draw draws the character image.
func (c *Character) Draw(screen *ebiten.Image) error {
	op := &ebiten.DrawImageOptions{}
	op.GeoM.Translate(float64(c.position.X), float64(c.position.Y))
	return screen.DrawImage(c.animation.GetCurrentFrame(), op)
}
```

## 時間でアニメーションする版

![time_animation.gif](https://qiita-image-store.s3.amazonaws.com/0/166225/8e15de07-c873-26bb-0a3b-23902433f49b.gif)

`Init`メソッド内で呼んでいる`loadImages`メソッドは上述のものと共通の内容ですので省略します。

```Go
// TimeAnimation is an animation.
// This animates according to elapsed time.
type TimeAnimation struct {
	ImagesPaths     []string
	DurationSecond  float64
	once            sync.Once
	frames          []*ebiten.Image
	maxFrameNum     int
	currentFrameNum int
	switchedTime    time.Time
	elapsed         float64
}

// Init loads asset images and initializes private parameters.
func (t *TimeAnimation) Init() (err error) {
	t.once.Do(func() {
		t.frames, err = loadImages(t.ImagesPaths)
		t.maxFrameNum = len(t.ImagesPaths)
	})
	if err != nil {
		return err
	}
	t.currentFrameNum = 0
	t.switchedTime = time.Now()
	t.elapsed = 0.0
	return nil
}

// GetCurrentFrame returns a current frame image. This function determines
// the current frame according to elapsed time.
// If the elapsed time is grater than the DurationSecond, this function
// will return the next frame.
func (t *TimeAnimation) GetCurrentFrame() *ebiten.Image {
	t.elapsed = time.Now().Sub(t.switchedTime).Seconds()
	if t.elapsed >= t.DurationSecond {
		t.currentFrameNum++
		t.switchedTime = time.Now()
	}
	if t.currentFrameNum < 0 || t.maxFrameNum-1 < t.currentFrameNum {
		t.currentFrameNum = 0
	}
	return t.frames[t.currentFrameNum]
}
```

### 初期化

歩数で動く版と同じように初期化します。

```Go
imagePaths := []string{
	"images/game/koma_00.png",
	"images/game/koma_01.png",
	"images/game/koma_02.png",
	"images/game/koma_03.png",
}
animation = TimeAnimation{
	ImagesPaths:    imagePaths, // 読み込む画像のパス配列
	DurationSecond: 1.0, // 1.0 secごとにフレームが切り替わる設定
}
err := c.animation.Init()
if err != nil {
	return err
}
```

フレームを切り替える判断に用いるしきい値情報が、歩数ではなく経過時間（sec）になっています。

### あとは描画するのみ

経過時間はよしなに判断しますので、描画したいタイミングで以下のように`GetCurrentFrame`でフレームを取得して描画するだけです。楽ちんですねヽ(=´▽`=)ﾉ

```Go
func (c *Character) Draw(screen *ebiten.Image) error {
	op := &ebiten.DrawImageOptions{}
	op.GeoM.Translate(float64(c.position.X), float64(c.position.Y))
	return screen.DrawImage(c.animation.GetCurrentFrame(), op)
}
```

# さいごに

素晴らしいライブラリebitenを作り続けておられる@hajimehoshiさんに、そして数々の素敵な記事でebitenに出会わせてくれた@zenwerkさんに、この場をお借りしてお礼申し上げますm(_ _)m ありがとうございます！

よし、頑張ってゲーム作り上げますよ！

(追伸)
この記事で紹介しているコードはMITライセンスです。
ebitenライブラリと一緒に使うと楽しくなるコードを集めて、いつかライブラリにできたらいいなぁ～٩( 'ω' )و