はじめに

本課題は[Roblox ムービー演出の作り方(3) Scriptでの処理〜準備編〜]の続きとして、各ターゲットのCFrameのデータをフレーム単位に分解する処理を解説します。
MoonAnimator2でのアニメーションの作成方法等については、[Roblox ムービー演出の作り方(1) MoonAnimator2][Roblox ムービー演出の作り方(2) データ構造とエクスポート]をご確認ください。

下準備

今回は各ターゲット用のアニメーションのキーフレームのCFrameのデータを、1フレーム毎に分解するCreateAnimationFrames関数を用意します。
関数名については別名でも構いませんが、前回の記事[Roblox ムービー演出の作り方(3) Scriptでの処理〜準備編〜]で用意したLocalScriptのSetup関数内で呼び出されるので、LocalScript内のSetup関数より前に定義する必要があります。
パラメータとしては、キーフレーム情報が入った配列と、アニメーションの総フレーム数を受け取る形になります。

各ターゲットの位置データをフレーム単位に分解

まずパラメータで受け取ったキーフレーム情報が入った配列から、キーフレームでのターゲットのCFrameのデータを取得し、ソート用の配列に込めていきます。

今回のサンプルの場合、以下の画像にある様にRigに対応した1フォルダ配下のCFrameフォルダ内にキーフレームをあらわす数字のフォルダ(0、180、90)が複数存在し、その最深部にある0というCFrameオブジェクトに、このキーフレームでのRigのCFrameのデータが入っています。

この最深部にあるキーフレーム毎のCFrameのデータを元に1フレーム単位のCFrameのデータを計算していくのですが、CFrameフォルダからGetChildren関数でキーフレームフォルダを取得すると0、180、90の順番で取得されてしまうため、まずはキーフレームの順番にソートします。①②

次にソートされたキーフレーム毎のCFrameのデータを元に、キーフレーム同士の間の1フレーム毎のCFrameのデータを計算します。
CFrameの要素としては位置情報の他にターゲットの向いている方向も含まれているので、その要素毎に計算する必要があります。③

残りの作業として、最後のキーフレームとアニメーション全体の総フレーム数が異なる場合、その間を1フレームずつ補完する必要があります。
この作業は、ターゲットによってフレームの数が異なるとアニメーションを再生した際に終了を判断するために処理を入れなければいけなくなるため、アニメーション全体のフレーム数を合わせるためにも総フレーム数分はデータを作成します。
方法としては既存の最後の1フレームのCFrameのデータを、総フレーム数になるまでコピーしてあげるだけです。④

ここまでの手順のサンプルを以下に用意したので、コメントも含めて参考にしてみてください。

StartPlayer/StarterCharacterScripts/LocalScript

-- アニメーションのフレーム毎のデータを作成する関数
function CreateAnimationFrames(frameData, frameLength)
    local frameArray = {}

    -- キーフレーム毎のCFrameのデータをソートするための配列
    local sortFrames = {}
    -- ①キーフレーム毎にCFrameのデータを取得
    for i, data in pairs(frameData.CFrame:GetChildren()) do
        if tonumber(data.Name) ~= nil then
            local keyFrames = data.Values:GetChildren()
            sortFrames[#sortFrames + 1] = {
                order = tonumber(data.Name),
                cFrameValue = keyFrames[#keyFrames]
            }
        end
    end

    -- ②order順にソート
    table.sort(sortFrames, function(a, b)
        return a.order < b.order
    end)

    -- ③1フレーム毎のCFrameのデータを作成
    local frameIndex = 1
    for i = 1, #sortFrames do
        if sortFrames[i]["order"] == 0 then
            frameArray[0] = sortFrames[i]["cFrameValue"].Value
        end

        if i < #sortFrames then

            local currentFrameData = sortFrames[i]["cFrameValue"].Value
            local nextFrameData = sortFrames[i + 1]["cFrameValue"].Value

            -- 次のキーフレームとの差分を要素毎に取得
            local diff = {
                pos = nextFrameData.Position - currentFrameData.Position,
                xv = nextFrameData.XVector - currentFrameData.XVector,
                yv = nextFrameData.YVector - currentFrameData.YVector,
                zv = nextFrameData.ZVector - currentFrameData.ZVector,
            }

            local currentIndex = 1
            -- フレーム毎のCFrameを計算
            while true do
                -- 次のフレームまでの割合を計算
                local coefficient = currentIndex / (sortFrames[i + 1]["order"] - sortFrames[i]["order"])
                -- 現在のフレームのCFrameのデータを差分に対する割合で計算
                frameArray[frameIndex] = CFrame.fromMatrix(
                    currentFrameData.Position + diff["pos"] * coefficient,
                    currentFrameData.XVector + diff["xv"] * coefficient,
                    currentFrameData.YVector + diff["yv"] * coefficient,
                    currentFrameData.ZVector + diff["zv"] * coefficient
                )

                -- 現在のフレームが次のキーフレームと同じならループから抜ける
                if frameIndex == sortFrames[i + 1]["order"] then
                    frameIndex = frameIndex + 1
                    break
                end

                currentIndex = currentIndex + 1
                frameIndex = frameIndex + 1
            end
        end
    end

    -- ④最後のキーフレームが総フレーム数により小さい場合は最後のフレーム情報で補完
    if frameIndex < frameLength then
        local lastFrame = frameArray[#frameArray]
        for i = frameIndex, frameLength do
            frameArray[i] = lastFrame
        end
    end

    return frameArray
end

次の手順

次回の記事では、今回作成した1フレーム毎のデータを元にターゲットを移動する処理を解説します。