2Dに特化した軽量ゲームエンジンDefoldでゲームを作る(後編)

defold-tutorial-seaquel-eyecatch

 

 本記事は「2Dに特化した軽量ゲームエンジンDefoldでゲームを作る(前編)の続きとなります。

 

 

本記事でできること

・Defoldを使ったサンプルゲームが作れる

 

 本記事ではキャラクターのアニメーションの設定、足場の配置、コインの出現までを実装していきます。

 

1.地面の速度を変更可能にする

 地面の速度を制御するために「level.collection」に新規スクリプトを作成します。

defold-tutorial-seaquel-1-1

 

 スクリプトに実装するのは、プロパティを使った「speed」変数の設定と「ground.script」へのメッセージ送信です。プロパティを使うと下図のように「speed」の値が設定できるようになります。

defold-tutorial-seaquel-1-2

 

 下記スクリプトでハマりそうな箇所としては、メッセージ送信部分の「”ground/controller#script”」というURL設定部分です。当然のことながらURLが間違っているとスクリプト実行時にエラーとなり動作しません。ハマると中々抜け出せなかったので、URLの指定方法は改善して欲しいところです。

go.property("speed", 6) 

function init(self)
    msg.post("ground/controller#script", "set_speed", { speed = self.speed })
end

 

 メッセージ送信でエラーが出る場合は一度「ID」を確認してみて下さい。

defold-tutorial-seaquel-1-3

 

 

 送信されたメッセージを受信しなければならないので「ground.script」も編集します。地面のスピードが調節できるようになったのでゲームを実行してみましょう。

function on_message(self, message_id, message, sender)
    if message_id == hash("set_speed") then 
        self.speed = message.speed 
    end
end

 

 

2.足場を実装する

2.1 画像追加

 地面の画像を追加した時と同様に「level.atlas」に足場の画像を追加しましょう。

defold-tutorial-seaquel-2-1

 

2.2 足場用のゲームオブジェクト追加

 足場用のゲームオブジェクトを新しく追加します。「Collision Object」はキャラクターが足場に乗れるように「Box」を配置して下さい(パラメータは下図参照)。足場は画面右端から左端にスクロールさせ、画面外に移動したら削除するのでスクリプトを追加しましょう。足場にもスピードがあるので、メッセージを受信する部分を実装しておきます。また、ゲームオーバになってゲームをリスタートする時に、全ての足場を削除しなければならないので「/main/Level/controller.script」で足場を管理します。そのため、足場の削除は足場のスクリプト内では実行していません。

defold-tutorial-seaquel-2-2

 

 

function init(self)
    self.speed = 9      -- Default speed
end

function update(self, dt)
    local pos = go.get_position()
    if pos.x < -500 then
        msg.post("level/controller#script", "delete_spawn", { id = go.get_id() }) 
    end
    pos.x = pos.x - self.speed
    go.set_position(pos)
end

function on_message(self, message_id, message, sender)
    if message_id == hash("set_speed") then
        self.speed = message.speed
    end
end

 

2.3 足場を一定時間毎に追加

 「/main/Level/controller.script」で足場を管理するので修正を加えます。まず「level.collection」に足場を作成するために使う「factory」コンポーネントを右クリックメニューで「Add Component」から追加します。「factory」に指定するプロパティは、スクリプトから作成したいゲームオブジェクトになります。今回は簡単のために、「platform_factory」と「platform_long_factory」の両方に「platform.go」を指定しました。

defold-tutorial-seaquel-2-3

 

 「/main/Level/controller.script」を修正します。上記で作成した「platform_factory」と「platform_long_factory」を「update」関数内の「factory.create」で指定することで、足場を動的に出現させることができます。足場の作成時に「table.insert」を実行することで全ての足場を管理しています。メッセージの受信部分で管理していた足場も同時に削除しています。想像のつく方もおられると思いますが、ゲームのリスタート時にはspawns内に格納されていた足場を全て削除することになります。

go.property("speed", 6) 

local grid = 460
local platform_heights = { 100, 200, 350 }

function init(self)
    msg.post("ground/controller#script", "set_speed", { speed = self.speed })
    self.gridw = 0
end

function update(self, dt)
    self.gridw = self.gridw + self.speed

    if self.gridw >= grid then
        self.gridw = 0

        -- Maybe spawn a platform at random height
        if math.random() > 0.2 then
            local h = platform_heights[math.random(#platform_heights)]
            local f = "#platform_factory"
            local coins = coins
            if math.random() > 0.5 then
                f = "#platform_long_factory"
                coins = coins * 2 -- Twice the number of coins on long platforms
            end

            local p = factory.create(f, vmath.vector3(1600, h, 0), nil, {}, 0.6)
            msg.post(p, "set_speed", { speed = self.speed })
            table.insert(self.spawns, p) 
        end
    end
end

function on_message(self, message_id, message, sender)
    if message_id == hash("delete_spawn") then 
        for i,p in ipairs(self.spawns) do
            if p == message.id then
                table.remove(self.spawns, i)
                go.delete(p)
            end
        end
    end
end

 

 ゲームを実行してみると足場がスクロールしてきます。

defold-tutorial-seaquel-2-4

 

 

3.キャラクターアニメーションを修正する

 現状、キャラクターが再生しているアニメーションは「run_right」のみですので、ジャンプの上昇中やジャンプの下降中でアニメーションを切り替えましょう。編集するスクリプトは「hero.script」です。luaは構文スコープを持っているので、下記のローカル関数の実装箇所には注意する必要があります。ローカル宣言された変数と関数のスコープは宣言された行の次の行から開始され、ローカル宣言された行が含まれているブロックの終わりまでです。従って、「update」関数より下部に「play_animation」と「update_animation」を配置するとエラーが出て正常に動作しないことが確認できます。

-- gravity pulling the player down in pixel units/sˆ2
local gravity = -20

-- take-off speed when jumping in pixel units/s
local jump_takeoff_speed = 900

local function play_animation(self, anim)
    -- only play animations which are not already playing
    if self.anim ~= anim then
        -- tell the spine model to play the animation
        spine.play("#spinemodel", anim, go.PLAYBACK_LOOP_FORWARD, 0.15)
        -- remember which animation is playing
        self.anim = anim
    end
end

local function update_animation(self)
    -- make sure the right animation is playing
    if self.ground_contact then
        play_animation(self, hash("run_right"))
    else
        if self.velocity.y > 0 then
            play_animation(self, hash("jump_right"))
        else
            play_animation(self, hash("fall_right"))
        end
    end
end

~略~

function update(self, dt)
    local gravity = vmath.vector3(0, gravity, 0)

    if not self.ground_contact then
        -- Apply gravity if there's no ground contact
        self.velocity = self.velocity + gravity
    end

    -- apply velocity to the player character
    go.set_position(go.get_position() + self.velocity * dt)

	-- update animation
	update_animation(self)

    -- reset volatile state
    self.correction = vmath.vector3()
    self.ground_contact = false
end

~略~

 

 キャラクターのアニメーションがジャンプによって切り替わるようになりました。

defold-tutorial-seaquel-3-1

 

 

4.ゲームオーバを作る

 もう少しゲーム性を出すために、足場にトゲを付けてキャラクターが触れたらゲームオーバーになるようにしましょう。

 

4.1 トゲ画像を追加

 トゲの画像を「level.atlas」に追加します。

defold-tutorial-seaquel-4-1

 

4.2 足場用のゲームオブジェクトを修正

 足場用のゲームオブジェクト「platform.go」を修正してトゲを追加します。トゲとの当たり判定には「Collision Object」の「Box」を使っています。プロパティに関しては下図を参照して下さい(GroupとMaskの値を間違えないように注意)。

defold-tutorial-seaquel-4-2

 

4.3 キャラクタースクリプト修正

 「hero.script」を4.2章で作成したトゲの衝突判定結果を受け取るために修正します。下記スクリプトの受信メッセージ「reset」はゲームがリスタートした時に実行するキャラクターの処理で、受信メッセージ「danger」はゲームオーバー時にキャラクターが実行する処理です。それぞれ実行している内容としては、キャラクターの位置や回転値の設定、衝突判定の有効、無効の切り替え、アニメーションの再生です。

function on_message(self, message_id, message, sender)
    if message_id == hash("reset") then
        self.velocity = vmath.vector3(0, 0, 0)
        self.correction = vmath.vector3()
        self.ground_contact = false
        self.anim = nil
        go.set(".", "euler.z", 0)
        go.set_position(self.position)
        msg.post("#collisionobject", "enable")
        
    elseif message_id == hash("contact_point_response") then
        if message.group == hash("danger") then
            -- Die and restart
            play_animation(self, hash("die_right"))
            msg.post("#collisionobject", "disable")
            go.animate(".", "euler.z", go.PLAYBACK_ONCE_FORWARD, 160, go.EASING_LINEAR, 0.7) 
            go.animate(".", "position.y", go.PLAYBACK_ONCE_FORWARD, go.get_position().y - 200, go.EASING_INSINE, 0.5, 0.2,
            function()
                msg.post("controller#script", "reset")
            end)
                
        -- check if we received a contact point message. One message for each contact point
        elseif message.group == hash("geometry") then
            handle_geometry_contact(self, message.normal, message.distance)
        end
    end
end

 

4.4 コントローラスクリプト修正

 4.3章で「hero.script」から「controller.script」にメッセージを送信しているので受信部分を実装します。下記スクリプトの9行目ではコントローラの座標をアニメーションによって動かすような処理が入っていますが、これ自体に意味はなくアニメーション終了後に実行される「msg.post」を遅延実行させることが目的です(遅延実行させないとキャラクターの挙動がおかしいため)。フォーラムを調べると今のところ関数を遅延実行させるようなものは無いとのことでした。

function on_message(self, message_id, message, sender)
    if message_id == hash("reset") then 
        -- Delete all platforms
        for i,p in ipairs(self.spawns) do
            go.delete(p)
        end
        self.spawns = {}

        go.animate(".", "position.x", go.PLAYBACK_ONCE_FORWARD, 0, go.EASING_INSINE, 0, 1.0,	
        function()
            msg.post("hero#script", "reset")
        end)
    elseif message_id == hash("delete_spawn") then 
        for i,p in ipairs(self.spawns) do
            if p == message.id then
                table.remove(self.spawns, i)
                go.delete(p)
            end
        end
    end
end

 

4.5 キャラクターのMaskを修正

 キャラクターと衝突判定するオブジェクトにトゲを追加しておきます。

defold-tutorial-seaquel-4-4

 

 以上の修正でキャラクターがトゲに衝突するとアニメーションが再生されて地面の下に落ちていきます。

defold-tutorial-seaquel-4-3

 

 

5.コインを足場に配置する

 コインを足場に配置してキャラクターと接触したら消えるようにします。コインを管理するのは足場に任せて、足場が画面外に移動した時に全てのコインを削除してもらう実装にします。

 

5.1 コイン画像追加

 コインの画像を「level.atlas」に追加します。

defold-tutorial-seaquel-5-1

 

5.2 コイン用のゲームオブジェクト追加

 コイン用のゲームオブジェクト「coin.go」を追加します。キャラクターとの衝突判定用に「Collision Object」の「Sphere」を使ってコイン全体を覆います。プロパティに関しては下図を参照して下さい(GroupとMaskの値を間違えないように注意)。

defold-tutorial-seaquel-5-2

 

5.3 コインスクリプト追加

 コインの管理は足場に任せるので、コインを非表示にするタイミングとコインを表示するタイミングは、足場からのメッセージを受信した時に処理することになっています。

function init(self)
    self.collected = false
end

function on_message(self, message_id, message, sender)
    if self.collected == false and message_id == hash("collision_response") then
        self.collected = true
        msg.post("#sprite", "disable")
    elseif message_id == hash("start_animation") then
        pos = go.get_position()
        go.animate(go.get_id(), "position.y", go.PLAYBACK_LOOP_PINGPONG, pos.y + 24, go.EASING_INOUTSINE, 0.75, message.delay)
    end
end

 

defold-tutorial-seaquel-5-3

 

5.4 足場用ゲームオブジェクト修正

 足場にコイン用のゲームオブジェクトを動的に作成するために「Add Component」から「Factory」を追加します。

defold-tutorial-seaquel-5-4

 

5.5 足場スクリプト修正

 足場はコインの管理をしますが、コインの作成要求をするのはコントローラスクリプトになります。従って、メッセージ「create_coins」を受信した時に初めて「create_coins」が実行され、コインの位置設定やアニメーションが開始されます。受信したメッセージの中には、足場に何枚のコインを配置するのかを表している値「coins」も含まれています。また、27行目の「set_parent」も重要な項目であり、ゲームオブジェクトの親子関係が設定できます。コインには足場を親として設定することで、足場の移動に合わせてにコインも移動する機能が実現できます。試しにコメントアウトして実行してみるとコインが表示されないことが確認できます(画面外にコインが作成されているため)。

function init(self)
    self.speed = 9      -- Default speed
    self.coins = {}
end

function final(self)
    for i,p in ipairs(self.coins) do
        go.delete(p)
    end
end

function update(self, dt)
    local pos = go.get_position()
    if pos.x < -500 then
        msg.post("level/controller#script", "delete_spawn", { id = go.get_id() }) 
    end
    pos.x = pos.x - self.speed
    go.set_position(pos)
end

function create_coins(self, params)
    local spacing = 56
    local pos = go.get_position()
    local x = pos.x - params.coins * (spacing*0.5) - 24
    for i = 1, params.coins do
        local coin = factory.create("#coin_factory", vmath.vector3(x + i * spacing , pos.y + 64, 1))
        msg.post(coin, "set_parent", { parent_id = go.get_id() }) 
        msg.post(coin, "start_animation", { delay = i/10 }) 
        table.insert(self.coins, coin)
    end
end

function on_message(self, message_id, message, sender)
    if message_id == hash("set_speed") then
        self.speed = message.speed
    elseif message_id == hash("create_coins") then
        create_coins(self, message)
    end
end

 

5.6 コントローラスクリプト修正

 足場に対してコインの作成要求をメッセージ経由で送ります。

go.property("speed", 6) 

local grid = 460
local platform_heights = { 100, 200, 350 }
local coins = 2

function init(self)
    msg.post("ground/controller#script", "set_speed", { speed = self.speed })
    self.gridw = 0
    self.spawns = {} 
end

function update(self, dt)
    self.gridw = self.gridw + self.speed

    if self.gridw >= grid then
        self.gridw = 0

        -- Maybe spawn a platform at random height
        if math.random() > 0.2 then
            local h = platform_heights[math.random(#platform_heights)]
            local f = "#platform_factory"
            local coins = coins
            if math.random() > 0.5 then
                f = "#platform_long_factory"
                coins = coins * 2 -- Twice the number of coins on long platforms
            end

            local p = factory.create(f, vmath.vector3(1600, h, 0), nil, {}, 0.6)
            msg.post(p, "set_speed", { speed = self.speed })
            msg.post(p, "create_coins", { coins = coins })
            table.insert(self.spawns, p) 
        end
    end
end

~略~

 

5.7 キャラクターのMaskを修正

 キャラクターと衝突判定するオブジェクトにコインを追加しておきます。

defold-tutorial-seaquel-5-6

 

 ゲームを実行すると足場にコインが表示されるようになりました!コインに衝突するとコインが消えます!

defold-tutorial-seaquel-5-5