LoginSignup
4
5

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-06-12

golangでゲームが作れる素敵なライブラリ「ebiten」を使い始めました。楽しい!٩( 'ω' )و
ねこむすめ道草日記」の同人ゲーム「黒菜んダッシュ(未完)」をebitenに移植しつつ完成させる企みです。

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

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

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

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

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

アニメーション構造体

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

step_animation.gif

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

// 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]
}

初期化

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

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メソッドを呼びます。

// 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フレームが返って来ますので、
screenDrawImageに渡して描画しましょう。

// 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

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

// 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]
}

初期化

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

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でフレームを取得して描画するだけです。楽ちんですねヽ(=´▽`=)ノ

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ライブラリと一緒に使うと楽しくなるコードを集めて、いつかライブラリにできたらいいなぁ~٩( 'ω' )و

4
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
5