Tutorial:Animation (日本語)

はじめる前に。これは中上級者向けのチュートリアルです。読者はテーブル、ループおよび LOVE による描画の基本事項に関して理解しているものと想定されております。もちろん LOVE でゲームを起動する方法も含まれます。

わかりました。どうすれば理解できますか?

筆者はスプライト技術によるアニメーションの基本事項を取り扱います。これは連続で表示される一連の画像があることを意味します。スプライトシートを自作すると決めているならば各スプライトの間に純粋な透過色のピクセル (αチャンネルで 255 の値) を最低でも 1 ピクセル配置してあるか確認してください。そうしなければ、次または前にある画像に一切不要かつ不自然な結果が見えてしまう場合があります!

アニメーションの読み込み

このチュートリアルでは無名の老英雄を使用します。 GrafxKid 製作のものであり、OpenGameArt.org によりみなさんのために提供されています。下記で画像をダウンロードして、 main.lua のある作業用フォルダへ画像を配置してください。

oldHero.png

複数のアニメーションの作成することは可能ですので、アニメーションと関連のあるすべてのものに対してテーブルを渡すことで使用できる再利用可能な関数が必要です。

新しい関数を定義してから作業用の画像に対して変数への格納を行い、新しい局所変数のテーブルを作成することにより、この作業を開始します。 (そして返値はテーブルです。あるアニメーション用のテーブルを思い付きで大域変数として定義してしまうと最悪の場合は関係のないその他のデータを上書きして破壊してしまうため、そのようなことは望ましくはありません!)

次の引数が必要です:

  • image

love.graphics.newImage(ファイルのパス) で作成される画像オブジェクトです。

  • width

個々のスプライトごとの幅です。すべてのスプライトは同一寸法であるものと想定しています。

  • height

個々のスプライトごとの高さです。

  • duration

アニメーションが最初のフレームまで折り返し再生 (ループバック) されるまでの長さ (全体の再生速度)。

function newAnimation(image, width, height, duration)
    local animation = {}
    animation.spriteSheet = image;

    return animation
end

これは love.graphics.draw(animation.spriteSheet) だけで描画しようとすることができます。しかしながら、全ての画像が互いに隣りで描画されるだけです。この結果は確かに望むものではありません。望む結果を得るには Quad が非常に役に立ちます!

そのために画像全体の代わりに描画される画像の一部を定義します。これこそ、まさしく望むものです!

さて、これで各 Quad を個別に定義することができました。けれども、いくつかのアニメーションで何百枚ものスプライトがある場合は、非常に面倒な作業になります! それを行うのではなく、読み込み時にループ内で画像内のスプライト枚数を検出する処理を行います。この配置では隙間が存在していけませんし、画像は正確な形式のアニメーションを有していなければいけません。

function newAnimation(image, width, height, duration)
    local animation = {}
    animation.spriteSheet = image;
    animation.quads = {};

    for y = 0, image:getHeight() - height, height do
        for x = 0, image:getWidth() - width, width do
            table.insert(animation.quads, love.graphics.newQuad(x, y, width, height, image:getDimensions()))
        end
    end

    return animation
end

for ループに対する引数は次の意味があります:

1. x または y の開始値。

2. 最大値 (この場合は参照する画像の全体の幅または高さ)

3. 1段階あたりの加算量。この場合のスプライトの寸法。

これにより y と x からスプライトシートの位置を取得できます!

なぜ、画像の高さを確認するのではなく image:getHeight() - height を行うのか疑問に思うでしょう。これは別のスプライトがスプライトシートへ常に適合するかどうかを確認したいからです。シート自体が必要とする寸法がない場合があるからです。1ピクセルあたりの寸法が多き過ぎる場合もあります。これでもう Quad の追加はしたくありませんので (何もレンダリングはしません)、なにもしないで無視します。

さて、テーブルのインデックス 1 ~ 6 の範囲から Quad は 6 個あります。すばらしい!

しかし、個別に描画できる6 個の画像には本質的な問題があります。けれども、それらを長い間次々描画する必要があります。 同様に、このアニメーションだけを再生したくありません。その再生速度を変更したいと思うかもしれません。

これを扱うためにアニメーションのテーブルへ二つ以上の変数を追加します。 継続時間である duration (引数として要求します) および現在時間である currentTime は経過時間を計測するために使用されます。

function newAnimation(image, width, height, duration)
    local animation = {}
    animation.spriteSheet = image;
    animation.quads = {};

    for y = 0, image:getHeight() - height, height do
        for x = 0, image:getWidth() - width, width do
            table.insert(animation.quads, love.graphics.newQuad(x, y, width, height, image:getDimensions()))
        end
    end

    animation.duration = duration or 1
    animation.currentTime = 0

    return animation
end

これでアニメーション生成関数は完了です!

アニメーションの更新

さて、次の話題として必要なものはアニメーションのテーブルへの読み込み (新規に作成した関数の呼び出し)、および現在時間による更新です。

function love.load()
    animation = newAnimation(love.graphics.newImage("oldHero.png"), 16, 18, 1)
end

function love.update(dt)
    animation.currentTime = animation.currentTime + dt
    if animation.currentTime >= animation.duration then
        animation.currentTime = animation.currentTime - animation.duration
    end
end

スプライトシートは幅 16 ピクセル、高さ 18 ピクセルの寸法によるスプライトから構成されています。 1 秒間にすべての画像を再生するよう意図しています。

In love.update we simply add dt (delta time aka the time since the last frame) to our current time. We now count upwards continuously!

But we will use the current time to determine which frame should be shown. As such we will want it to be between 0 and the value of "duration". The if simply checks if "currentTime" is more than "duration" in which case we subtract "duration". You could just set "currentTime" to 0 instead of subtracting "duration". However this will result in fractions of a second being lost every time the animation finishes and therefore slow down the animation playtime ever so slightly. Which can easily be avoided and should be avoided!

Now for the really interesting part!

アニメーションの描画

How do we draw this?

Well. We have the duration and current time. With this info we can calculate a percentage! How much of the animation has passed so far?

If you've followed this tutorial correctly so far "currentTime / duration" will provide you with a number between 0 and 1. Which represents the percentage. 0.25 means 25% of the animation has passed.

Considering this we can search for the correct image to use! Since we already have a number between 0 and 1 we can simply multiply this percentage with our total amount of images and get a number between 0 and 6!

currentTime / duration * #quads

However. If we try to get this from our table we will run into the issue that this is not a whole number. But our images are stored with whole numbers! So attempting to get the image at index "4.75" will give us nothing. Bummer!

Fear not. The solution is not too difficult.

"currentTime" will be a number between 0 and just below "duration" (because we reduce "currentTime" if it is larger or equal "duration"). Which would result in a number between 0 and 5.

To transform this value from our decimal point value to a whole number between 1 and 6 we do the following:

math.floor(currentTime / duration * #quads) + 1

"math.floor" provides us with the next lower number. Which means in our case a number between 0 and 5. We add one pushing it to a number between 1 and 6. All the sprites we have!

Lövely!

Alright. So all that's left is to draw the appropriate quad!

This simply requires us to provide "love.graphics.draw" with the image reference (our spriteSheet) and the quad we want to use. Simple enough!

    local spriteNum = math.floor(animation.currentTime / animation.duration * #animation.quads) + 1
    love.graphics.draw(animation.spriteSheet, animation.quads[spriteNum])

And we are done! You should have a walking dude in the top left corner of your window when you execute this code!

You can change where and how it is drawn by providing more parameters to the draw function.

Disclaimer: This code is not game ready! It is meant to explain the basic logic behind animations. If you are searching for game ready code take a look at the list of Libraries available!

課題

To improve this code. Try to rewrite the update and draw function to be able to handle multiple animations.

You can load the same animation multiple times and store them in a special table to get started right away!

As hint as to how it could be done: You could put the drawing logic in the animation table as well. And build it so you can call "animation:draw(x, y, r, sx, sy, [...])" to draw it on the screen.

Good luck and have fun!

完全なソースコード

function love.load()
    animation = newAnimation(love.graphics.newImage("oldHero.png"), 16, 18, 1)
end

function love.update(dt)
    animation.currentTime = animation.currentTime + dt
    if animation.currentTime >= animation.duration then
        animation.currentTime = animation.currentTime - animation.duration
    end
end

function love.draw()
    local spriteNum = math.floor(animation.currentTime / animation.duration * #animation.quads) + 1
    love.graphics.draw(animation.spriteSheet, animation.quads[spriteNum], 0, 0, 0, 4)
end

function newAnimation(image, width, height, duration)
    local animation = {}
    animation.spriteSheet = image;
    animation.quads = {};

    for y = 0, image:getHeight() - height, height do
        for x = 0, image:getWidth() - width, width do
            table.insert(animation.quads, love.graphics.newQuad(x, y, width, height, image:getDimensions()))
        end
    end

    animation.duration = duration or 1
    animation.currentTime = 0

    return animation
end


そのほかの言語