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 秒間にすべての画像を再生するよう意図しています。

ただ、 love.update で現在時刻に dt (最後のフレームからの経過時間として知られているデルタ・タイム) を加算します。これで以降は絶えず計算が行われます!

しかし、どのフレームを表示するか決定するために現在時間を使用します。それ自体は 0 と "duration" (継続時間) の値の間に発生することを望むでしょう。 "currentTime" が "duration" 以上かを確認して、そうならば "duration" を減算します。単に "duration" を減算するのではなく "currentTime" を 0 に設定することもできます。しかしながら、これでは秒の小数部がアニメーションの終了時に毎回失われてしまい、アニメーションの再生時間が少し崩れてしまいます。それは容易に避けることができますし、避けるべきです!

さて、次は本当に興味深い部分ですよ!

アニメーションの描画

これをどのように描画しますか?

よろしい。継続時間と現在時間があります。この情報で割合を計算することができます! これまでのアニメーションの経過時間はどのくらいですか?

これまで読者が適切に本チュートリアルを理解しているならば "currentTime / duration" では 0 と 1 の間にある数値を用意します。それは割合を表します。 0.25 はアニメーションが 25% 再生経過していることを意味します。

このことを考慮すれば使用する画像を正確に探し出すことができます! 既に 0 と 1 の間にある数値はありますので、この割合を画像の総枚数と掛けることで 0 と 6 の間の数値を得ることができます!

currentTime / duration * #quads

しかし、これでテーブルで取得しようとすると、これが整数ではない問題に出くわします。 ですが、画像は整数にて記録されています! そして "4.75" インデックスの画像を取得しようとしても何も得られません。なんてっこった、がっかりだ!

でも、大丈夫です。 解決方法はあまり難しくはありません。

"currentTime" の数値が 0 の間は "duration" はそれ以下になります (大きいか 等しい "duration" の場合は "currentTime" を減らすためです)。その結果は 0 と 5 の間の数値となります。

この値を小数点の値から整数の 1 と 6 の間の数値へ変換するには次の処理を行います:

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

math.floor は小数点以下を切り捨てて整数にした数値を提供します。この場合は 0 から 5 の間にある数値を意味します。 1 から 6 の間にある数値を広げるために 1 を加算します。これですべてのスプライトを扱えるようになります!

素晴らしい!

大丈夫です。残りは全て適切な Quad として描画します!

これは必要となる参照用の画像 (用意したスプライトシート) と使用したい Quad を love.graphics.draw へ渡すだけです。まあまあ簡単ですね。

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

そして、これで完了です! このコードを実行したときに、ウィンドウの左上の隅っこを歩いているアイツがいるはずです!

また、描画関数へ更なる引数を指定することにより描画位置と描画方法を変更することができます。

免責次項: このコードはゲーム製作用に設計されていません。アニメーションの裏側にある基本的な論理に関しての説明用です。ゲーム制作用のコードを探しているのでしたらライブラリの一覧が利用できます!

課題

このコードを改良してみましょう。更新と描画用の関数を書き直して複数のアニメーションを扱えるようにしてみましょう。

同じアニメーションを複数回読み込んでから、即時再生するために特別なテーブルへ格納することができます。

どのような方法で行えるか示唆をしましょう: アニメーションのテーブルへ描画用のロジック (論理) を同じように挿入することができます。そして animation:draw(x, y, r, sx, sy, [...]) を呼び出して画面上に描画できるようにするために、それ構築してください。

幸運をお祈りします。そして楽しい時間をお過ごしくださいませ!

完全なソースコード

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


そのほかの言語