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
Beginner
このチュートリアルでは、再現に挑戦できる定番ゲームの1つを作る手順を説明します。このゲームには多くのバリエーションがありますが、ここでは「食べ物」を食べ、食べたときにだけ体が伸びるヘビが登場します。このヘビは、障害物のあるプレイフィールドを進みます。
![]()
このチュートリアルでは、次の方法を学びます。
このチュートリアルは初心者向けですが、Defold もゲーム開発もまったく初めての方には、まず入門マニュアル、特に Defold の基本構成要素と用語集を読むことをお勧めします。まだ Defold をダウンロードしていない場合は、インストールマニュアルを確認してください。エディター自体にすぐに慣れるために、エディターの概要も確認することをお勧めします。このチュートリアルでも、各手順のスクリーンショットを掲載しています。
Defold を起動し、次の操作をします。

完了!
まず、ゲームの解像度を定義します。
game.project ファイルを探します。ダブルクリックして開きます。game.project ファイルの Display セクションに移動します。Width と Height)を 768⨉768、またはほかの16の倍数に設定します。
この設定にするのは、各マスが 16x16 ピクセルのグリッド上にゲームを描画するためです。こうすることで、ゲーム画面の端でマスの一部が切れるのを防げます。game.project ファイルには、プロジェクトの重要な設定がすべて含まれています。各設定については、プロジェクト設定マニュアルで確認できます。
完了!
最小限のスネークゲームに必要なグラフィックスは、ごくわずかです。ヘビの体を構成する 16⨉16 の緑色のパーツ、障害物用の白いブロック、食べ物を表す少し小さな赤いブロックが、それぞれ1つあれば足ります。
まず、Defold エディターでアセット用のディレクトリを作成します。
main フォルダーを右クリックします。New Folder を選択します。assets と入力して Create Folder をクリックします。
完了!
必要なアセットは、次の画像だけです。


アセットのインポートについて詳しくは、こちらも参照してください。
完了!
Defold には組み込みのタイルマップコンポーネント(component)があり、これを使って、タイルをグリッド状に並べたプレイフィールドを作成します。タイルマップでは個々のタイルを設定したり読み取ったりできるため、このゲームにぴったりです。タイルマップのグラフィックスはタイルソース(tile source)から取得するので、タイルソースを作成する必要があります。
assets フォルダーを右クリックします。New ▸ Tile Source を選択します。snake.tilesource として保存します)。
タイルソースは、この種類のファイル専用のタイルソースエディターで開きます。タイルソースを使えるようにするため、画像の指定を求められます。右側に Properties ペインがあります。
Image プロパティに、先ほどインポートしたグラフィックスファイルを設定します。

Width と Height プロパティは、16(既定値)のままにします。これにより、32⨉32 ピクセルの画像が、1–4 の番号が付いた4つのタイルに分割されます。

Extrude Borders プロパティが2ピクセルに設定されていることに注目してください。これは、端までグラフィックスが描かれているタイルの周囲に、表示の乱れが生じるのを防ぐためです。
ファイルを変更すると、タブのファイル名の横にアスタリスク * が表示されます。File ▸ Save All を選択するか、ショートカットの Ctrl+S(Mac では ⌘Cmd + S)を使って、すべてのファイルを保存します。
完了!
タイルソースの準備ができたので、プレイフィールドのタイルマップコンポーネントを作成します。
main フォルダーを右クリックして、「Components」セクションの New ▸ Tile Map を選択します。新しいファイルに「grid」という名前を付けます(エディターは「grid.tilemap」として保存します)。

タイルマップエディターが開き、Tile Source が必要であることが強調表示されます。Tile Source プロパティに、先ほど作成した「snake.tilesource」を設定します。

完了!
Defold は、タイルマップの実際に使われている領域だけを保存します。そのため、画面の境界を埋めるだけのタイルを追加する必要があります。
Outline ペインで layer1 レイヤーを選択します。メニューの Edit ▸ Select Tile... またはショートカットの Space を選択してタイルパレットを表示し、描画に使うタイルをクリックします。


ゲーム画面を埋めるには、48x48 タイルのタイルマップが必要です(画面の大きさは 768、タイルの大きさは 16px なので、768/16 = 48 です)。
描き終えたら、タイルマップを保存します。
完了!
次に、タイルマップをゲームに追加する必要があります。Defold の基本構成要素をご存じなら、コンポーネントがゲームオブジェクト(game object)の一部であり、ゲームオブジェクトをコレクション(collection)内に定義できることをご存じでしょう。
Assets ペインで main.collection をダブルクリックして開きます。これは Empty Project テンプレートの既定のブートストラップコレクション(bootstrap collection)で、エンジンの起動時に読み込まれるコレクションです。
Outline のルートを右クリックし、Add Game Object を選択します。ゲーム開始時に読み込まれるコレクション内に、新しいゲームオブジェクトが作成されます。

新しいゲームオブジェクトを右クリックして、Add Component File を選択します。先ほど作成した「grid.tilemap」ファイルを選びます。

これで、ゲームのコレクションにタイルマップが追加されました。エディターからゲームを実行すると、表示されるはずです。
Project ▸ Build を選択するか、ショートカットの Ctrl + B(Mac では ⌘Cmd + B)を使います。
完了!
Assets ブラウザーで main フォルダーを右クリックし、Scripts セクションの New ▸ Script を選択します。新しいスクリプトファイルに「snake」という名前を付けます(「snake.script」として保存されます)。このファイルに、ゲームのすべてのロジックを記述します。

main.collection に戻り、タイルマップを持つゲームオブジェクトを右クリックします。Add Component File を選択し、「snake.script」ファイルを選びます。

これで、タイルマップコンポーネントとスクリプトが揃いました。
完了!
これから書くスクリプトで、ゲーム全体を動かします。機能を1つずつ追加していきます。
次のような仕組みで動かします。
snake.script を開き、init() 関数を探します。この関数は、ゲーム開始時にスクリプトが初期化されるときにエンジンから呼び出されます。コードを次のように変更します。
function init(self)
self.segments = { -- <1>
{x = 7, y = 24},
{x = 8, y = 24},
{x = 9, y = 24},
{x = 10, y = 24}
}
self.dir = {x = 1, y = 0} -- <2>
self.speed = 7.0 -- <3>
self.time = 0 -- <4>
end
このコードでは、次の処理をします。
self.segments という Lua テーブル(table)に保存します。このテーブルは、各パーツの X と Y の位置を保持するテーブルのリストです。self.dir というテーブルに保存します。self.speed に保存します。速度は1秒あたりのタイル数で表します。self.time に保存します。上のスクリプトコードは Lua 言語で書かれています。コードについて知っておきたいことがいくつかありますが、以下の内容がまだわからなくても心配はいりません。そのまま読み進め、試しながら時間をかければ、やがて理解できるようになります。今は、init() でこれから使う変数を初期化したことを覚えておけば十分です。
self パラメーターを通じて、現在のスクリプトコンポーネントのインスタンス(instance)への参照を渡します。self 参照は、インスタンスのデータを保存するために使います。self 参照は、データを保存できる Lua テーブルとして使えます。ほかのテーブルと同じように、self.data = "value" のようなドット記法を使うだけです。この参照は、スクリプトの生存期間を通して有効です。この場合は、ゲームの開始から終了までです。{} で囲んで記述します。{x = 10, y = 20})、入れ子になった Lua テーブル({ {a = 1}, {b = 2} })、またはほかのデータ型を使えます。完了!
init() 関数は、実行中のゲーム内にスクリプトコンポーネントのインスタンスが作成されるときに、ちょうど1回呼び出されます。一方、update() 関数は毎フレーム1回呼び出されます。そのため、この関数はリアルタイムのゲームロジックに適しています。
更新処理では、設定した間隔で次の処理をします。

:::sidenote ヘビの頭はテーブルの末尾にあり、尻尾は先頭にあることを覚えておいてください。 :::
update() 関数を探し、コードを次のように変更します。function update(self, dt)
self.time = self.time + dt -- <1>
if self.time >= 1.0 / self.speed then -- <2>
local head = self.segments[#self.segments] -- <3>
local newhead = {
x = head.x + self.dir.x,
y = head.y + self.dir.y
} -- <4>
table.insert(self.segments, newhead) -- <5>
local tail = table.remove(self.segments, 1) -- <6>
tilemap.set_tile("#grid", "layer1", tail.x, tail.y, 0) -- <7>
for i, s in ipairs(self.segments) do -- <8>
tilemap.set_tile("#grid", "layer1", s.x, s.y, 2) -- <9>
end
self.time = 0 -- <10>
end
end
このコードでは、次の処理をします。
update() が呼び出されてからの経過時間(delta time)、つまり dt の秒数だけタイマーを進めます。# は、テーブルを配列として使っている場合に、その長さを取得する演算子です。今回もこれに該当し、すべてのパーツがキーを指定しないテーブルの値になっています。self.dir)に基づいて、新しい頭のパーツを作成します。#grid には、layer1 というレイヤーが1つだけあります。i にはテーブル内の位置(1から始まります)が、s には現在のパーツが設定されます。ここでゲームを実行すると、4つのパーツからなるヘビがプレイフィールドを左から右へ進む様子が見えるはずです。

完了!
プレイヤーの入力に反応するコードを追加する前に、入力の対応付けを設定する必要があります。
input フォルダー内の game.input_binding ファイルを探し、ダブルクリックして開きます。
入力バインディングファイルは、実際のユーザー入力(キー、マウスの移動など)を、アクションの名前に対応付けます。この名前は、入力を要求したスクリプトに渡されます。
完了!
バインディングを設定したら、snake.script を開き、入力フォーカス(input focus)を取得するために、init() 関数の先頭に次の行を追加します。
function init(self)
msg.post(".", "acquire_input_focus") -- <1>
self.segments = {
{x = 7, y = 24},
{x = 8, y = 24},
{x = 9, y = 24},
{x = 10, y = 24}
}
self.dir = {x = 1, y = 0}
self.speed = 7.0
self.time = 0
end
追加した行では、次の処理をします。
次に on_input 関数を探し、次のコードを入力します。
function on_input(self, action_id, action)
if action_id == hash("up") and action.pressed then -- <1>
self.dir.x = 0 -- <2>
self.dir.y = 1
elseif action_id == hash("down") and action.pressed then
self.dir.x = 0
self.dir.y = -1
elseif action_id == hash("left") and action.pressed then
self.dir.x = -1
self.dir.y = 0
elseif action_id == hash("right") and action.pressed then
self.dir.x = 1
self.dir.y = 0
end
end
これらの if...elseif... 分岐では、次の処理をします。
action テーブルの pressed フィールドが true(プレイヤーがキーを押した)であれば、次の処理をします。ゲームをもう一度実行し、ヘビを操作できることを確認します。
完了!
2つのキーを同時に押すと、それぞれのキー入力に対して1回ずつ、合計2回 on_input() が呼び出されることに注目してください。上のコードでは、後から呼び出された on_input() が self.dir の値を上書きするため、最後の呼び出しだけがヘビの方向に影響します。
また、ヘビが左に進んでいるときに right キーを押すと、ヘビが自分の体に向かって進んでしまうことにも注意してください。一見わかりやすい解決策は、on_input() の if 節に追加の条件を入れることです。
if action_id == hash("up") and self.dir.y ~= -1 and action.pressed then
...
elseif action_id == hash("down") and self.dir.y ~= 1 and action.pressed then
...
しかし、ヘビが左に進んでいるときに、次の移動が起こる前にプレイヤーが up、続いて right を素早く押すと、right の入力だけが反映され、ヘビは自分の体に向かって進んでしまいます。上に示した条件を if 節に追加すると、その入力は無視されます。これでは困ります!
この問題を適切に解決するには、入力をキューに保存し、ヘビが移動するたびに、そのキューから要素を取り出します。
function init(self)
msg.post(".", "acquire_input_focus")
self.segments = {
{x = 7, y = 24},
{x = 8, y = 24},
{x = 9, y = 24},
{x = 10, y = 24}
}
self.dir = {x = 1, y = 0}
self.speed = 7.0
self.time = 0
self.dirqueue = {} -- <1>
end
今回は、次の変更を加えます。
self.dirqueue を追加します。update() 関数に次の処理を追加します。
function update(self, dt)
self.time = self.time + dt
if self.time >= 1.0 / self.speed then
local newdir = table.remove(self.dirqueue, 1) -- <1>
if newdir then
local opposite = newdir.x == -self.dir.x or newdir.y == -self.dir.y -- <2>
if not opposite then
self.dir = newdir -- <3>
end
end
local head = self.segments[#self.segments]
local newhead = {x = head.x + self.dir.x, y = head.y + self.dir.y}
table.insert(self.segments, newhead)
local tail = table.remove(self.segments, 1)
tilemap.set_tile("#grid", "layer1", tail.x, tail.y, 0)
for i, s in ipairs(self.segments) do
tilemap.set_tile("#grid", "layer1", s.x, s.y, 2)
end
self.time = 0
end
end
newdir が null でない場合)、newdir が self.dir と反対方向を指しているかどうかを確認します。そして、現在の入力をキューに保存するように on_input を変更します。
function on_input(self, action_id, action)
if action_id == hash("up") and action.pressed then
table.insert(self.dirqueue, {x = 0, y = 1}) -- <1>
elseif action_id == hash("down") and action.pressed then
table.insert(self.dirqueue, {x = 0, y = -1})
elseif action_id == hash("left") and action.pressed then
table.insert(self.dirqueue, {x = -1, y = 0})
elseif action_id == hash("right") and action.pressed then
table.insert(self.dirqueue, {x = 1, y = 0})
end
end
self.dir を直接設定する代わりに、入力された方向を方向のキューに追加します。ゲームを起動し、期待どおりにプレイできることを確認します。
完了!
ヘビの体が伸びて移動速度が上がるように、マップ上に食べ物が必要です。食べ物を追加しましょう!
init() 関数の上に、新しい関数を追加します。
local function put_food(self) -- <1>
self.food = {x = math.random(2, 47), y = math.random(2, 47)} -- <2>
tilemap.set_tile("#grid", "layer1", self.food.x, self.food.y, 3) -- <3>
end
この関数では、次の処理をします。
put_food() という新しい関数を宣言します。self.food という変数に保存します。次に、init() 関数の末尾でこの関数を呼び出します。
function init(self)
msg.post(".", "acquire_input_focus")
self.segments = {
{x = 7, y = 24},
{x = 8, y = 24},
{x = 9, y = 24},
{x = 10, y = 24}
}
self.dir = {x = 1, y = 0}
self.dirqueue = {}
self.speed = 7.0
self.time = 0
math.randomseed(socket.gettime()) -- <1>
put_food(self) -- <2>
end
math.random() で乱数の取得を始める前に、乱数のシードを設定します。設定しなければ、同じ乱数列が生成されます。このシードは1回だけ設定するようにします。put_food() 関数を呼び出し、マップ上に食べ物がある状態でプレイを始められるようにします。完了!
ヘビが何かに衝突したかどうかを検出するには、ヘビの進む先のタイルマップに何があるかを調べて、対応する処理をするだけです。
ヘビが生きているかどうかを管理する変数を追加します。
function init(self)
msg.post(".", "acquire_input_focus")
self.segments = {
{x = 7, y = 24},
{x = 8, y = 24},
{x = 9, y = 24},
{x = 10, y = 24}
}
self.dir = {x = 1, y = 0}
self.dirqueue = {}
self.speed = 7.0
self.time = 0
self.alive = true -- <1>
math.randomseed(socket.gettime())
put_food(self)
end
次に、壁や障害物、食べ物との衝突を判定するロジックを追加します。
function update(self, dt)
self.time = self.time + dt
if self.time >= 1.0 / self.speed and self.alive then -- <1>
local newdir = table.remove(self.dirqueue, 1)
if newdir then
local opposite = newdir.x == -self.dir.x or newdir.y == -self.dir.y
if not opposite then
self.dir = newdir
end
end
local head = self.segments[#self.segments]
local newhead = {x = head.x + self.dir.x, y = head.y + self.dir.y}
table.insert(self.segments, newhead)
local tile = tilemap.get_tile("#grid", "layer1", newhead.x, newhead.y) -- <2>
if tile == 2 or tile == 4 then
self.alive = false -- <3>
elseif tile == 3 then
self.speed = self.speed + 1 -- <4>
put_food(self)
else
local tail = table.remove(self.segments, 1) -- <5>
tilemap.set_tile("#grid", "layer1", tail.x, tail.y, 1)
end
for i, s in ipairs(self.segments) do
tilemap.set_tile("#grid", "layer1", s.x, s.y, 2)
end
self.time = 0
end
end
ゲームを試し、うまくプレイできることを確認しましょう!
チュートリアルはこれで終わりですが、引き続きゲームでいろいろ試し、以下の練習問題にも取り組んでみてください!
完了!
参考として、スクリプトのコード全体を示します。
local function put_food(self)
self.food = {x = math.random(2, 47), y = math.random(2, 47)}
tilemap.set_tile("#grid", "layer1", self.food.x, self.food.y, 3)
end
function init(self)
msg.post(".", "acquire_input_focus")
self.segments = {
{x = 7, y = 24},
{x = 8, y = 24},
{x = 9, y = 24},
{x = 10, y = 24}
}
self.dir = {x = 1, y = 0}
self.dirqueue = {}
self.speed = 7.0
self.time = 0
self.alive = true
math.randomseed(socket.gettime())
put_food(self)
end
function update(self, dt)
self.time = self.time + dt
if self.time >= 1.0 / self.speed and self.alive then
local newdir = table.remove(self.dirqueue, 1)
if newdir then
local opposite = newdir.x == -self.dir.x or newdir.y == -self.dir.y
if not opposite then
self.dir = newdir
end
end
local head = self.segments[#self.segments]
local newhead = {x = head.x + self.dir.x, y = head.y + self.dir.y}
table.insert(self.segments, newhead)
local tile = tilemap.get_tile("#grid", "layer1", newhead.x, newhead.y)
if tile == 2 or tile == 4 then
self.alive = false
elseif tile == 3 then
self.speed = self.speed + 1
put_food(self)
else
local tail = table.remove(self.segments, 1)
tilemap.set_tile("#grid", "layer1", tail.x, tail.y, 1)
end
for i, s in ipairs(self.segments) do
tilemap.set_tile("#grid", "layer1", s.x, s.y, 2)
end
self.time = 0
end
end
function on_input(self, action_id, action)
if action_id == hash("up") and action.pressed then
table.insert(self.dirqueue, {x = 0, y = 1})
elseif action_id == hash("down") and action.pressed then
table.insert(self.dirqueue, {x = 0, y = -1})
elseif action_id == hash("left") and action.pressed then
table.insert(self.dirqueue, {x = -1, y = 0})
elseif action_id == hash("right") and action.pressed then
table.insert(self.dirqueue, {x = 1, y = 0})
end
end
次の改良を実装してみると、よい練習になります。