golangでゲームが作れる素敵なライブラリ「ebiten」を使い始めました。楽しい!٩( 'ω' )و
「ねこむすめ道草日記」の同人ゲーム「黒菜んダッシュ(未完)」をebitenに移植しつつ完成させる企みです。
キャラクターのアニメーション
ゲームでキャラクターをアニメーション表示する際、以下のようなパターンがあるかと思います。
- キャラクターが一定歩数だけ動いたら次のフレームに切り替え
- 一定時間が経過したら次のフレームに切り替え
前者はアクションゲームの自キャラなどで多く見かけますし、後者はRPGの自キャラやアクションゲームのアイテムなどで多いように思います。いずれのパターンでも、歩数や時間をしきい値とくらべて現在表示すべきフレームを決める処理が必要になります。
今回ご紹介するのは、あらかじめしきい値となる値を決めておけば、この面倒な「現在表示すべきフレームを決める処理」をまるっとお任せできる構造体を作りました。もちろんebiten用です。
アニメーション構造体
歩数でアニメーションする版
動作サンプルということで、大好きな独楽ちゃんに動いてもらっています☆
// 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
フレームが返って来ますので、
screen
のDrawImage
に渡して描画しましょう。
// 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)
}
時間でアニメーションする版
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ライブラリと一緒に使うと楽しくなるコードを集めて、いつかライブラリにできたらいいなぁ~٩( 'ω' )و