This translation is community contributed and may not be up to date. We only maintain the English version of the documentation. Read this tutorial in English
このチュートリアルでは、空のプロジェクトから始めて、キャラクターのアニメーション、物理衝突、アイテムの収集、得点の機能を備えたランナーゲームを完成させます。
新しいゲームエンジンを学ぶときには、覚えることがたくさんあります。そこで、最初の一歩を踏み出せるように、このチュートリアルを用意しました。エンジンとエディターの仕組みを順に説明する、かなり充実したチュートリアルです。プログラミングの基礎知識があることを前提としています。
Lua プログラミングの入門が必要な場合は、Defold での Lua のマニュアルを参照してください。
最初に取り組むにはこのチュートリアルは少し難しすぎると感じた場合は、さまざまな難易度のチュートリアルを揃えたチュートリアルのページを参照してください。
動画のチュートリアルを見たい場合は、Youtube の動画版を参照してください。
ここでは、ほかの2つのチュートリアルのゲームアセットに少し変更を加えて使用します。チュートリアルはいくつかのステップに分かれており、各パートを進めるたびにゲームの完成に大きく近づきます。
最終的には、主人公のキャラクターを操作して、コインを集め、障害物を避けながらステージを走り抜けるゲームになります。主人公は一定の速度で走り、プレイヤーは1つのボタンを押すだけで(モバイルデバイスでは画面をタッチして)主人公のジャンプだけを操作します。レベルには、飛び乗るための足場と収集するコインが途切れることなく現れます。
このチュートリアルやゲーム作りの途中で行き詰まったら、遠慮なく Defold フォーラムで質問してください。フォーラムでは Defold について話し合ったり、Defold チームに助けを求めたり、ほかのゲーム開発者の問題解決方法を調べたり、新しいアイデアのヒントを見つけたりできます。さっそく利用してみましょう。
このチュートリアルでは、概念や特定の操作方法についての詳しい説明を、この段落のように示しています。詳しすぎると感じる場合は、読み飛ばしてください。
それでは始めましょう。このチュートリアルを楽しみながら進め、Defold を使い始めるきっかけにしていただければ幸いです。
このチュートリアルのアセットはこちらからダウンロードできます。
まず、次のファイルをダウンロードします。
Defold エディターをまだダウンロードしてインストールしていない場合は、ここで行います。
Defold のダウンロードページを開きます。macOS、Windows、Linux(Ubuntu)向けの Download ボタンがあります。

エディターを起動するには、「Applications」フォルダーを開き、ファイル「Defold」を ダブルクリックします。

D:\Defold)に移動します。Defold を C:\Program Files (x86)\ や C:\Program Files\ に移動すると、エディターを更新できなくなるため、これらの場所への移動は避けてください。エディターを起動するには、フォルダー「Defold」を開き、ファイル「Defold.exe」を ダブルクリックします。

ターミナルでアーカイブファイル「Defold-x86_64-linux.zip」を見つけ、「Defold」という名前の展開先ディレクトリに展開します。
$ unzip Defold-x86_64-linux.zip -d Defold
エディターを起動するには、アプリケーションを展開したディレクトリに移動して実行可能ファイル Defold を実行するか、デスクトップでそのファイルを ダブルクリックします。
$ cd Defold
$ ./Defold
Help > Create Desktop Entry メニューには、デスクトップエントリをインストールするための補助機能があります。
エディターの起動、プロジェクトを開く操作、Defold のゲームの実行で問題が発生した場合は、FAQ の Linux に関するセクションを参照してください。
Defold のすべてのベータ版と安定版は、GitHub からも入手できます。
エディターをインストールして起動したら、新しいプロジェクトを作成して準備します。「Empty Project」テンプレートから新しいプロジェクトを作成します。
このチュートリアルでは、Spine Extension が提供する Spine の機能を使います。game.project の依存関係のセクションに、この拡張を追加します。
初めてエディターを起動すると、プロジェクトが開かれていない空の状態になります。メニューから Open Project を選択し、先ほど作成したプロジェクトを選びます。プロジェクトの「ブランチ」の作成も求められます。
Assets pane には、プロジェクトに含まれるすべてのファイルが表示されます。「main/main.collection」ファイルをダブルクリックすると、中央のエディタービューにファイルが開きます。

エディターは、主に次の領域で構成されています。
print()、pprint() によるデバッグメッセージが表示されます。アプリケーションやゲームが起動しないときは、まずコンソールを確認してください。コンソールの背後には、エラー情報を表示するタブや、パーティクルエフェクトの作成に使うカーブエディターのタブがあります。「Empty」プロジェクトテンプレートは、文字どおり完全に空です。それでも、Project ▸ Build を選択すれば、プロジェクトをビルドしてゲームを起動できます。

黒い画面だけではあまり面白くないかもしれませんが、これは実際に動作している Defold のゲームアプリケーションです。少し変更するだけで、もっと面白いものにできます。さっそく取り組みましょう。
Defold エディターはファイルを対象に作業します。Assets pane でファイルをダブルクリックすると、適切なエディターで開き、ファイルの内容を編集できます。
ファイルの編集が終わったら、保存する必要があります。メインメニューで File ▸ Save を選択します。保存していない変更があるファイルは、タブのファイル名にアスタリスク「*」が付くので見分けられます。

作業を始める前に、プロジェクトの設定をいくつか行います。Assets Pane から game.project アセットを開き、Display セクションまで下にスクロールします。プロジェクトの width と height を、それぞれ 1280 と 720 に設定します。
主人公をアニメーションさせるために、プロジェクトに Spine 拡張を追加する必要もあります。インストールした Defold エディターのバージョンと互換性がある Spine 拡張のバージョンを追加してください。利用できる Spine のバージョンは、次のページで確認できます。
https://github.com/defold/extension-spine/releases
使いたいリリースの zip ファイルへのリンクを右クリックします。

リリースへのリンクを game.project の依存関係のリストに追加します。Spine 拡張を追加したら、拡張に含まれるエディター連携を有効にするため、エディターを再起動する必要があります。
最初の小さな一歩として、キャラクターの舞台となる場所、具体的にはスクロールする地面を作成しましょう。いくつかの手順で進めます。
アトラス(atlas) は、複数の個別の画像を1つの大きな画像ファイルにまとめるファイルです。これにより、容量を節約し、パフォーマンスを向上させます。アトラスやほかの 2D グラフィックス機能について詳しくは、2D グラフィックスのドキュメントを参照してください。


どうして動かないのでしょう!? Defold を使い始めた人によくあるのが、保存のし忘れです!アトラスに画像を追加したら、その画像を利用する前にファイルを保存する必要があります。
地面用のコレクション(collection)ファイル ground.collection を作成し、7つのゲームオブジェクト(game object)を追加します(Outline ビューでコレクションのルートを右クリックし、Add Game Object を選択します)。Properties ビューで Id プロパティを変更し、オブジェクトの名前を「ground0」「ground1」「ground2」などにします。Defold は、新しいゲームオブジェクトに一意の識別子を自動的に割り当てます。
各オブジェクトにスプライトコンポーネントを追加します(Outline ビューでゲームオブジェクトを右クリックして Add Component を選択し、Sprite を選んで OK をクリックします)。スプライトコンポーネントの Image プロパティを、先ほど作成したアトラスに設定し、スプライトの既定のアニメーションを2つの地面画像のどちらかに設定します。ゲームオブジェクトではなく、スプライトコンポーネント の X 位置を190、Y 位置を40に設定します。画像の幅は380ピクセルで、その半分だけ横にずらすため、ゲームオブジェクトのピボットはスプライト画像の左端に位置します。


スプライトコンポーネントを持ち、スケールの調整も済んだゲームオブジェクトを1つ作成してからコピーするのがおそらく最も簡単です。Outline ビューで選択し、Edit ▸ Copy、続いて Edit ▸ Paste を選択します。
タイルを大きくしたり小さくしたりするには、スケールを変更するだけで済みます。ただし、その場合は地面のすべてのゲームオブジェクトの X 位置も、新しい幅の倍数に変更する必要があります。
ファイルを保存し、ground.collection を main.collection ファイルに追加します。まず main.collection ファイルをダブルクリックし、Outline ビューでルートオブジェクトを右クリックして Add Collection From File を選択します。ダイアログで ground.collection を選択し、OK をクリックします。ground.collection は位置0, 0, 0に配置してください。そうしないと表示位置がずれます。ファイルを保存します。
ゲームを起動して(Project ▸ Build)、すべてが正しく配置されていることを確認します。

ここまでに作成したものが何なのか、少し混乱しているかもしれません。ここで一度、Defold のすべてのプロジェクトに共通する、最も基本的な構成要素を見てみましょう。
今のところは、この説明で十分でしょう。さらに詳しい説明は、基本構成要素のマニュアルにあります。後でこのマニュアルを読んで、Defold の仕組みへの理解を深めることをお勧めします。
地面のパーツをすべて配置できたので、動かすのは比較的簡単です。パーツを右から左へ移動し、画面の左端の外側に達したパーツを、一番右の位置へ移します。これらのゲームオブジェクトをすべて動かすには Lua スクリプトが必要なので、作成しましょう。
-- ground.script
local pieces = { "ground0", "ground1", "ground2", "ground3",
"ground4", "ground5", "ground6" } -- <1>
function init(self) -- <2>
self.speed = 360 -- Speed in pixels/s
end
function update(self, dt) -- <3>
for i, p in ipairs(pieces) do -- <4>
local pos = go.get_position(p)
if pos.x <= -228 then -- <5>
pos.x = 1368 + (pos.x + 228)
end
pos.x = pos.x - self.speed * dt -- <6>
go.set_position(pos, p) -- <7>
end
end
init() 関数は、ゲームオブジェクトがゲーム内で動作し始めるときに呼び出されます。ここでは、地面の速度を格納する、そのオブジェクト固有のメンバー変数を初期化します。update() は毎フレーム1回、通常は毎秒60回呼び出されます。dt には、前回の呼び出しから経過した秒数が格納されています。dt を掛けることで、フレームレートに依存しない、ピクセル/秒での速度にします。Defold は、データとゲームオブジェクトを管理する高速なエンジンコアです。ゲームに必要なロジックや振る舞いは、すべて Lua 言語で作成します。Lua は高速で軽量なプログラミング言語で、ゲームロジックの記述に適しています。書籍 Programming in Lua や公式の Lua リファレンスマニュアルなど、言語を学ぶための優れた資料があります。
Defold は Lua に各種 API を追加し、ゲームオブジェクト間の通信をプログラムできる メッセージパッシング の仕組みも提供します。仕組みの詳細は、メッセージパッシングのマニュアルを参照してください。
エディターの Assets Pane、Console、Outline は、それぞれ F6、F7、F8 キーで表示と非表示を切り替えられます。
スクリプトファイルを作成できたので、ゲームオブジェクト内のコンポーネントに、そのファイルへの参照を追加します。これにより、スクリプトはゲームオブジェクトのライフサイクルの一部として実行されます。ground.collection に新しいゲームオブジェクトを作成し、先ほど作成した Lua スクリプトファイルを参照する Script コンポーネントを追加します。

ゲームを実行すると、「controller」ゲームオブジェクトが Script コンポーネントのスクリプトを実行し、地面が画面を滑らかにスクロールします。
主人公は、次のコンポーネントで構成されるゲームオブジェクトになります。
まず、体の各パーツの画像をインポートし、hero.atlas という新しいアトラスに追加します。

Spine のアニメーションデータをインポートし、それに対応する Spine Scene を設定する必要もあります。

hero.spinejson ファイルは、Spine JSON 形式でエクスポートされています。このようなファイルを作成するには、アニメーションソフトウェアの Spine が必要です。ほかのアニメーションソフトウェアを使いたい場合は、アニメーションをスプライトシートとしてエクスポートし、Tile Source または Atlas リソースから、連続した画像を切り替えるフリップブックアニメーション(flip-book animation)として使えます。詳しくは、アニメーションのマニュアルを参照してください。
これで、主人公のゲームオブジェクトを組み立てられます。

次に、衝突が機能するように物理を追加します。
「Kinematic」の衝突では、衝突は検出されますが、物理エンジンによる自動的な衝突の解決やオブジェクトのシミュレーションは行われません。物理エンジンは、さまざまな種類のコリジョンオブジェクトをサポートしています。詳しくは、物理のドキュメントを参照してください。
このコリジョンオブジェクトが、何と相互作用するかを指定することが重要です。
最後に、新しい hero.script ファイルを作成し、ゲームオブジェクトに追加します。
handle_geometry_contact() 関数で行います)。
衝突を自分で処理するのには理由があります。キャラクターのコリジョンオブジェクトの種類を動的オブジェクトにすると、エンジンは関係する物体に対してニュートン力学に基づくシミュレーションを行います。このようなゲームでは、そのシミュレーションは最適とは言えません。さまざまな力を加えて物理エンジンと格闘する代わりに、すべての制御を自分で行います。
そのために衝突を正しく処理するには、少しだけベクトルの数学が必要です。キネマティックな衝突を解決する方法については、物理のドキュメントで詳しく説明しています。
-- 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
function init(self)
-- this tells the engine to send input to on_input() in this script
msg.post(".", "acquire_input_focus")
-- save the starting position
self.position = go.get_position()
-- keep track of movement vector and if there is ground contact
self.velocity = vmath.vector3(0, 0, 0)
self.ground_contact = false
end
function final(self)
-- Return input focus when the object is deleted
msg.post(".", "release_input_focus")
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)
-- reset volatile state
self.correction = vmath.vector3()
self.ground_contact = false
end
local function handle_geometry_contact(self, normal, distance)
-- project the correction vector onto the contact normal
-- (the correction vector is the 0-vector for the first contact point)
local proj = vmath.dot(self.correction, normal)
-- calculate the compensation we need to make for this contact point
local comp = (distance - proj) * normal
-- add it to the correction vector
self.correction = self.correction + comp
-- apply the compensation to the player character
go.set_position(go.get_position() + comp)
-- check if the normal points enough up to consider the player standing on the ground
-- (0.7 is roughly equal to 45 degrees deviation from pure vertical direction)
if normal.y > 0.7 then
self.ground_contact = true
end
-- project the velocity onto the normal
proj = vmath.dot(self.velocity, normal)
-- if the projection is negative, it means that some of the velocity points towards the contact point
if proj < 0 then
-- remove that component in that case
self.velocity = self.velocity - proj * normal
end
end
function on_message(self, message_id, message, sender)
if message_id == hash("contact_point_response") then
-- check if we received a contact point message. One message for each contact point
if message.group == hash("geometry") then
handle_geometry_contact(self, message.normal, message.distance)
end
end
end
local function jump(self)
-- only allow jump from ground
if self.ground_contact then
-- set take-off speed
self.velocity.y = jump_takeoff_speed
end
end
local function abort_jump(self)
-- cut the jump short if we are still going up
if self.velocity.y > 0 then
-- scale down the upwards speed
self.velocity.y = self.velocity.y * 0.5
end
end
function on_input(self, action_id, action)
if action_id == hash("jump") or action_id == hash("touch") then
if action.pressed then
jump(self)
elseif action.released then
abort_jump(self)
end
end
end
試してみたい場合は、主人公を一時的にメインコレクションに追加してゲームを実行すると、ワールドをすり抜けて落ちていく様子を確認できます。
主人公を機能させるために最後に必要なのは入力です。上のスクリプトには、「jump」と「touch」(タッチスクリーン用)のアクションに応答する on_input() 関数がすでに含まれています。これらのアクションに入力バインディング(input binding)を追加しましょう。

衝突を含む主人公の設定ができたので、主人公が衝突する相手(つまり、走る場所)となるように、地面にも衝突を追加する必要があります。その作業の前に、少しリファクタリングを行い、レベルに関するものをすべて別のコレクションにまとめて、ファイル構成を整理しましょう。
すでに気付いたかもしれませんが、Assets pane に表示されるファイル階層と、コレクション内で構築するコンテンツの構造は独立しています。各ファイルはコレクションファイルやゲームオブジェクトファイルから参照されますが、ファイルの配置場所は自由です。
ファイルを別の場所へ移動すると、Defold はそのファイルへの参照を自動的に更新して支援します(リファクタリング)。ゲームのような複雑なソフトウェアを作る際には、規模の拡大や内容の変化に応じてプロジェクトの構造を変更できることが大いに役立ちます。Defold はその変更を勧め、作業が円滑に進むようにしています。ためらわずにファイルを移動してください!
レベルのコレクションに、スクリプトコンポーネントを持つコントローラー用ゲームオブジェクトも追加します。
スクリプトファイルを開き、次のコードをコピーして、ファイルを保存します。
-- controller.script
go.property("speed", 360) -- <1>
function init(self)
msg.post("ground/controller#ground", "set_speed", { speed = self.speed })
end

「controller」ゲームオブジェクトは、独立したファイルとしては存在せず、レベルのコレクション内にその場で追加(in-place)されています。つまり、ゲームオブジェクトのインスタンスは、その場で定義したデータから作成されます。このように用途が1つのゲームオブジェクトには、それで問題ありません。あるゲームオブジェクトのインスタンスが複数必要で、各インスタンスの作成に使うプロトタイプ/テンプレートを変更できるようにしたい場合は、ゲームオブジェクトファイルを作成し、そのファイルからゲームオブジェクトをコレクションに追加します。これにより、ファイルをプロトタイプ/テンプレートとして参照するゲームオブジェクトが作成されます。
この「controller」ゲームオブジェクトの目的は、実行中のレベルに関するすべてを制御することです。このスクリプトは、後ほど主人公が触れる足場やコインの生成を担当しますが、今のところはレベルの速度を設定するだけです。
レベルのコントローラースクリプトの init() 関数では、地面のコントローラーオブジェクトのスクリプトコンポーネントに、その識別子を宛先としてメッセージを送信します。
msg.post("ground/controller#controller", "set_speed", { speed = self.speed })
コントローラーのゲームオブジェクトは「ground」コレクション内にあるため、識別子は "ground/controller" になります。コンポーネントの識別子 "controller" を、オブジェクトの識別子とコンポーネントの識別子を区切るハッシュ記号 "#" の後に追加します。地面のスクリプトには、まだ set_speed メッセージに応答するコードがありません。ground.script に on_message() 関数を追加し、そのためのロジックを記述する必要があります。
-- ground.script
function on_message(self, message_id, message, sender)
if message_id == hash("set_speed") then -- <1>
self.speed = message.speed -- <2>
end
end

ここで、地面に物理衝突を追加します。

ここでゲームを実行してみましょう(Project ▸ Build)。主人公が地面の上を走り、Space キーでジャンプできるはずです。モバイルデバイスでゲームを実行する場合は、画面をタップするとジャンプできます。
ゲームワールドをもう少し面白くするため、飛び乗れる足場を追加しましょう。
Script ファイル platform.script を作成します(Assets pane で右クリックし、New ▸ Script File を選択します)。次のコードをファイルに記述し、保存します。
-- platform.script
function init(self)
self.speed = 540 -- Default speed in pixels/s
end
function update(self, dt)
local pos = go.get_position()
if pos.x < -500 then
go.delete() -- <1>
end
pos.x = pos.x - self.speed * dt
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

platform.go と platform_long.go の Script コンポーネントは、同じスクリプトファイルを参照しています。スクリプトファイルへの変更が、通常の足場と長い足場の両方の動作に反映されるため、便利です。
このゲームは、単純なエンドレスランナーを目指しています。そのため、足場のゲームオブジェクトをエディターでコレクション内に配置しておくことはできません。代わりに、動的に生成する必要があります。
-- controller.script
go.property("speed", 360)
local grid = 460
local platform_heights = { 100, 200, 350 } -- <1>
function init(self)
msg.post("ground/controller#controller", "set_speed", { speed = self.speed })
self.gridw = 0
end
function update(self, dt) -- <2>
self.gridw = self.gridw + self.speed * dt
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"
if math.random() > 0.5 then
f = "#platform_long_factory"
end
local p = factory.create(f, vmath.vector3(1600, h, 0), nil, {}, vmath.vector3(0.6, 0.6, 1))
msg.post(p, "set_speed", { speed = self.speed })
end
end
end
update() 関数は毎フレーム1回呼び出されます。これを使い、一定の間隔(重なりを避けるため)と高さで、通常の足場または長い足場を生成するかどうかを決めます。さまざまな生成アルゴリズムを試して、異なるゲームプレイを作るのは簡単です。ゲームを実行します(Project ▸ Build)。
おお、(ほとんど)遊べるゲームになってきました……。

まず、主人公に動きの変化を与えましょう。今の主人公は走る動作を繰り返すだけで、ジャンプなどにうまく反応できません。アセットパッケージから追加した Spine ファイルには、実はそうした動作に使うアニメーションが含まれています。
update() 関数の 前 に、次の関数を追加します。 -- hero.script
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
local anim_props = { blend_duration = 0.15 }
spine.play_anim("#spinemodel", anim, go.PLAYBACK_LOOP_FORWARD, anim_props)
-- 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"))
else
play_animation(self, hash("jump"))
end
end
update() 関数を探し、update_animation の呼び出しを追加します。 ...
-- apply it to the player character
go.set_position(go.get_position() + self.velocity * dt)
update_animation(self)
...

Lua のローカル変数には「レキシカルスコープ」があり、local 関数を配置する順序が影響します。update() 関数はローカル関数 update_animation() と play_animation() を呼び出すため、それらを呼び出す前に、ランタイムがローカル関数の定義を読み込んでいる必要があります。そのため、これらの関数を update() の前に配置しなければなりません。関数の順序を入れ替えるとエラーになります。これは local 変数にだけ当てはまります。Lua のスコープの規則とローカル関数について詳しくは、http://www.lua.org/pil/6.2.html を参照してください。
主人公にジャンプと落下のアニメーションを追加する作業は、これだけです。ゲームを実行すると、操作感がかなりよくなったことが分かります。ただし、足場に押されて主人公が画面の外へ出てしまうことにも気付くかもしれません。これは衝突処理の副作用ですが、解決方法は簡単です。少し過激にして、足場の端を危険な場所にしましょう!
ファイルを保存します。

hero.go を開き、Collision Object を選択して、Mask プロパティに「danger」を追加します。ファイルを保存します。

hero.script を開き、主人公が「danger」の端に衝突したときに反応するように、on_message() 関数を変更します。
-- hero.script
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
-- check if we received a contact point message
if message.group == hash("danger") then
-- Die and restart
play_animation(self, hash("death"))
msg.post("#collisionobject", "disable")
-- <1>
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("#", "reset")
end)
elseif message.group == hash("geometry") then
handle_geometry_contact(self, message.normal, message.distance)
end
end
end
オブジェクトを初期化するために「reset」メッセージを送るよう、init() 関数を変更し、ファイルを保存します。
-- hero.script
function init(self)
-- this lets us handle input in this script
msg.post(".", "acquire_input_focus")
-- save position
self.position = go.get_position()
msg.post("#", "reset")
end
ここでゲームを試すと、リセットの仕組みがうまく動かないことがすぐに分かります。主人公自体は正しくリセットされますが、リセット直後に足場の端へ落ち、また死亡してしまう状況が起こりやすくなっています。実現したいのは、死亡時にレベル全体を正しくリセットすることです。レベルは生成された一連の足場で構成されているだけなので、生成したすべての足場を管理し、リセット時に削除すれば済みます。
controller.script ファイルを開き、生成したすべての足場の識別子を格納するようにコードを編集します。
-- controller.script
go.property("speed", 360)
local grid = 460
local platform_heights = { 100, 200, 350 }
function init(self)
msg.post("ground/controller#controller", "set_speed", { speed = self.speed })
self.gridw = 0
self.spawns = {} -- <1>
end
function update(self, dt)
self.gridw = self.gridw + self.speed * dt
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"
if math.random() > 0.5 then
f = "#platform_long_factory"
end
local p = factory.create(f, vmath.vector3(1600, h, 0), nil, {}, vmath.vector3(0.6, 0.6, 1))
msg.post(p, "set_speed", { speed = self.speed })
table.insert(self.spawns, p) -- <1>
end
end
end
function on_message(self, message_id, message, sender)
if message_id == hash("reset") then -- <2>
-- Tell the hero to reset.
msg.post("hero#hero", "reset")
-- Delete all platforms
for i,p in ipairs(self.spawns) do
go.delete(p)
end
self.spawns = {}
elseif message_id == hash("delete_spawn") then -- <3>
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
platform.script を開き、左端に達した足場をそのまま削除する代わりに、レベルのコントローラーに足場の削除を依頼するメッセージを送るように変更します。
-- platform.script
...
if pos.x < -500 then
msg.post("/level/controller#controller", "delete_spawn", { id = go.get_id() })
end
...

-- hero.script
...
go.animate(".", "position.y", go.PLAYBACK_ONCE_FORWARD, go.get_position().y - 200, go.EASING_INSINE, 0.5, 0.2,
function()
msg.post("controller#controller", "reset")
end)
...

これで、再開と死亡を繰り返す基本的なループができました!
次は、生き残る目的になるもの、コインです!
プレイヤーが収集できるコインをレベルに配置します。まず考えるのは、レベルにどう配置するかです。たとえば、足場の生成アルゴリズムに何らかの形で合わせた生成方法を作れます。ただし、ここでは最終的にもっと簡単な方法を選び、足場自体からコインを生成することにしました。
新しいスクリプトファイル coin.script を作成します(Assets pane で level を右クリックし、New ▸ Script File を選択します)。テンプレートのコードを、次の内容に置き換えます。
-- coin.script
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
スクリプトファイルを Script コンポーネントとして、コインのオブジェクトに追加します(Outline でルートを右クリックし、Add Component from File を選択します)。

足場のオブジェクトからコインを生成するため、platform.go と platform_long.go にコイン用のファクトリーを配置します。

次に、コインを生成、削除するように platform.script を変更します。
-- platform.script
function init(self)
self.speed = 540 -- Default speed in pixels/s
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#controller", "delete_spawn", { id = go.get_id() })
end
pos.x = pos.x - self.speed * dt
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() }) -- <1>
msg.post(coin, "start_animation", { delay = i/10 }) -- <2>
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
親子関係による変更は、あくまでも シーングラフ(scene graph) に対するものです。子は、親と一緒に変換(移動、拡大縮小、回転)されます。ゲームオブジェクト間に別の「所有」関係が必要な場合は、コードでその関係を明示的に管理する必要があります。
このチュートリアルの最後の手順として、controller.script に数行追加します。
-- controller.script
...
local platform_heights = { 100, 200, 350 }
local coins = 3 -- <1>
...
-- controller.script
...
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
...
-- controller.script
...
msg.post(p, "set_speed", { speed = self.speed })
msg.post(p, "create_coins", { coins = coins })
table.insert(self.spawns, p)
...

これで、単純ですが機能するゲームができました!ここまで進められたら、自分で次の機能を追加してみたくなるかもしれません。
完成版のプロジェクトはこちらからダウンロードできます。
入門チュートリアルはこれで終わりです。ここからは、Defold をさらに使ってみましょう。学習を支援するマニュアルとチュートリアルを多数用意しています。行き詰まったら、フォーラムへどうぞ。
Defold を楽しんでください!