In this tutorial we start with an empty project and build a complete runner game with an animated character, physics collision, pickups and scoring.
There is a lot to take in when learning a new game engine, so we have created this tutorial to get you started. It is a fairly complete tutorial that walks through how the engine and the editor works. We assume that you have some familiarity with programming.
If you need an introduction to Lua programming, check out our Lua in Defold manual.
If you feel that this tutorial is a bit too much to begin with, please check out our tutorials page where we have a selection of tutorials of varied difficulty.
If you prefer to watch video tutorials, please check out the video version on Youtube.
We use game assets from two other tutorials, with some small modifications. The tutorial is divided into several steps, with each part taking us a significant step towards the final game.
The end result will be a game where you control a hero character that runs through an environments, collecting coins and avoiding obstacles. The hero character runs at fixed speed and the player controls only the hero character’s jumping by the press of a single button (or screen touch on a mobile device). The level consists of an endless stream of platforms to jump on - and of coins to collect.
If you at any point get stuck on this tutorial or when creating your game don’t hesitate to ask us for help at the Defold Forum. In the forum you can discuss Defold, ask for help from the Defold team, see how other game developers solved their problems and find new inspiration. Get started now.
Throughout the tutorial, detailed descriptions on concepts and how to do certain moments are marked like this paragraph. If you feel that these section go into too much detail, please skip them.
So let’s begin. We hope you will have a lot of fun going through this tutorial and that it helps you getting going with Defold.
Download the assets for this tutorial here.
The first step is to download the following files.
Now, if you haven’t already downloaded and installed the Defold editor, it’s time to do that:
Go to the Defold download page where you will find Download buttons for macOS, Windows and Linux (Ubuntu):
To start the editor, open your “Applications” folder and double click the file “Defold”.
D:\Defold
). You should not move Defold to C:\Program Files (x86)\
or C:\Program Files\
since this will prevent the editor from updating.To start the editor, open the folder “Defold” and double click the file “Defold.exe”.
From a terminal, locate the archive file “Defold-x86_64-linux.zip” unzip it to a target Directory called “Defold”.
$ unzip Defold-x86_64-linux.zip -d Defold
To start the editor, change directory to where you extracted the application, then run the Defold
executable, or double click it on your desktop.
$ cd Defold
$ ./Defold
There is a helper to install a desktop entry on the Help > Create Desktop Entry
menu.
If you run into any problems starting the editor, opening a project or running a Defold game please refer to the Linux section of the FAQ.
Every beta and stable version of Defold is also available on GitHub.
When the editor is installed and started it’s time to create a new project and getting it ready. Create a new project from the “Empty Project” template.
This tutorial uses Spine features, which has been moved to its own extension post Defold 1.2.188. If you are on a newer version, please add the Spine Extension to the dependencies section of game.project.
The first time you start the editor, the editor starts blank, without any project open so choose Open Project from the menu and select your newly created project. You will also be prompted to create a “branch” for the project.
Now, in the Assets pane you will see all files that are part of the project. If you double-click the file “main/main.collection” the file will open up in the editor view in the center:
The editor consists of the following main areas:
print()
and pprint()
debug messages from your scripts. If your app or game won’t start the console is the first thing to check. Behind the console are a set of tabs displaying error information as well as a curve editor that is used when building particle effects.The “Empty” project template actually is completely empty. Nonetheless, select Project ▸ Build to build the project and launch the game.
A black screen is perhaps not very exciting, but it’s a running Defold game application and we can easily modify it into something more interesting. So let’s do that.
The Defold editor works on files. By double-clicking a file in the Assets pane you open it in a suitable editor. You can then work with the contents of the file.
When you are done editing a file you have to save it. Select File ▸ Save in the main menu. The editor gives a hint by adding an asterisk ‘*’ to the filename in the tab for any file that contain unsaved changes.
Before we begin, let’s set up several settings for our projects. Open the game.project asset from the Assets Pane
and scroll down to the Display section. Set the width
and height
of the project to 1280
and 720
respectively.
You also need to add the Spine extension to the project so that we can animate the hero character. Add a version of the Spine extension which is compatible with the version of the Defold editor which you have installed. Available Spine versions can be seen here:
https://github.com/defold/extension-spine/releases
Right click on the link to the zip file for the release you want to use:
Add the link to the release to your list of game.project dependencies. When the Spine extension has been added you also need to restart the editor to activate the editor integration included with the Spine extension.
Let’s take the first baby steps and create an arena for our character, or rather a piece of scrolling ground. We do this in a few steps.
An Atlas is a file that combines a set of separate images into one larger image file. The reason for doing that is to save space and also to gain performance. You can read more about Atlases and other 2D graphics features in the 2D graphics documentation.
Why doesn’t it work!? A common problem people have when they starting with Defold is forgetting to save! After adding images to an atlas you need to save the file before you can access that image.
Create a collection file ground.collection for the ground and add 7 game objects to it (right-click the root of the collection in the Outline view and select Add Game Object). Name the objects “ground0”, “ground1”, “ground2” etc by changing the Id property in the Properties view. Note that Defold automatically assigns new game objects a unique id.
In each object, add a sprite component (right-click the game object in the Outline view and select Add Component, then select Sprite and click OK), set the Image property of the sprite component to the atlas you just created and set the default animation of the sprite to one of the two ground images. Set the X position of the sprite component (not the game object) to 190 and Y position to 40. Since the width of the image is 380 pixels and we shift it sideways half as many pixels, the pivot of the game object will be at the leftmost edge of the sprite image.
It’s probably easiest to create one complete scaled game object with a sprite component and then copy it. Mark it in the Outline view, then select Edit ▸ Copy and then Edit ▸ Paste.
It is worth noticing that if you want larger or smaller tiles you can just change the scaling. However, doing so will also require that you change the X positions of all ground game objects to multiples of the new width.
Save the file, then add ground.collection to the main.collection file: first double click the main.collection file, then right-click the root object in the Outline view and select Add Collection From File. In the dialog, select ground.collection and click OK. Make sure you place ground.collection in position 0, 0, 0 or it will be offset visually. Save it.
Start up the game (Project ▸ Build to see that everything is in place.
By now you might be confused and wonder about what all these things that we have been creating really are, so let us take a moment and look at the most basic building blocks in any Defold project:
For the time being these description probably suffices. However, a much more comprehensive dive through these things can be found in the Building blocks manual. It is a good idea to visit that manual at a later stage to get a deeper understanding on how things work in Defold.
Now that we have all ground pieces in place, it is rather simple to get them moving. The idea is this: move the pieces right-to-left and when a piece reach the leftmost edge outside of the screen, move it to the rightmost position. To move all these game objects requires a Lua script so let’s create one:
-- 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()
function is called when the game object comes to life in the game. We initiate a object local member variable that contains the speed of the ground.update()
is called once each frame, typically 60 times per second. dt
contains the number of seconds since the last call.dt
to get framerate independent speed in pixels/s.Defold is a fast engine core that manages your data and game objects. Any logic or behavior that you need for your game is created in the Lua language. Lua is a fast and light-weight programming language that is great for writing game logic. There are great resources available to learn the language, like the book http://www.lua.org/pil/[Programming in Lua] and the official http://www.lua.org/manual/5.3/[Lua reference manual].
Defold adds a set of APIs on top of Lua, as well as a message passing system that allows you to program communications between game objects. See the Message passing manual for details on how this works.
You can toggle the Assets Pane, Console and Outline sections of the editor using the F6, F7 and F8 keys respectively
Now that we have a script file, we should add a reference to it to a component in a game object. That way, the script will be executed as part of the game object lifecycle. We do this by creating a new game object in ground.collection and add a Script component to the object that refers to the Lua script file we just created:
Now when you run the game, the “controller” game object will run the script in its Script component, causing the ground to scroll smoothly across the screen.
The hero character will be a game object consisting of the following components:
Start by importing the body part images, then add them to a new atlas that we call hero.atlas:
We also need to import the Spine animation data and set up a Spine Scene for it:
The file hero.spinejson has been exported in Spine JSON format. You will need the Spine animation software to be able to create such files. If you want to use other animation software you can export your animations as sprite-sheets and use them as flip-book animations either from Tile Source or Atlas resources. See the manual on Animation for more information.
Now we can start constructing the hero gameobject:
Now it’s time to add physics for collision to work:
“Kinematic” collision means that we want collisions to register, but the physics engine won’t solve collisions automatically and simulate the objects. The physics engine supports a number of different types of collision objects. You can read more about them in the Physics documentation.
It is important that we specify what the collision object should interact with:
Finally, create a new hero.script file and add it to the game object.
handle_geometry_contact()
function.)The reason we are handling the collision ourselves is that if we instead set the type on the character’s collision object to dynamic, the engine will peform a Newtonian simulation of the bodies involved. For a game like this, such a simulation is far from optimal so instead of fighting the physics engine with various forces, we take full control.
Now, to do that and handle collision properly requires a little bit of vector mathematics. A thorough explanation on how to solve kinematic collisions is given in the Physics documentation.
-- 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
If you want you can now try and temporarily add the hero character to the main collection and run the game to see it fall through the world.
The last thing we need for the hero to be functional is input. The script above already contains an on_input()
function that responds to actions “jump” and “touch” (for touch screens). Let’s add input bindings for these actions.
Now that we have a hero character set up with collision and all, we need to also add collision to the ground so the hero character has got something to collide with (or run on). We’ll do that in a second, but first, we should do a little refactoring and put all level stuff in a separate collection and clean up the file structure a bit:
As you might have noticed by now, the file hierarchy seen in the Assets pane is decoupled from the content structure you build in your collections. Individual files are referenced from collection- and game object files, but their location is completely arbitrary.
If you want to move a file to a new location Defold helps by automatically updating references to the file (refactoring). When making a complex piece of software, like a game, it is extremely helpful to be able to change the structure of the project as it grows and changes. Defold encourages that and makes the process smooth so don’t be afraid to move your files around!
We should also add a controller game object with a script component to the level collection:
Open the script file, copy the following code into it and save the file:
-- controller.script
go.property("speed", 360) -- <1>
function init(self)
msg.post("ground/controller#ground", "set_speed", { speed = self.speed })
end
The “controller” game object does not exist in a file but is created in-place in the level collection. This means that the game object instance is created from the in-place data. That is fine for single purpose game objects like this one. If you need multiple instances of some game object and want to be able to modify the prototype/template used to create each instance, just create a game object file and add the game object from file to the collection. That creates a game object with a reference to the file as prototype/template.
Now, the purpose of this “controller” game object is to control everything that relates to the running level. Soon, this script will be in charge of spawning platforms and coins for the hero to interact with, but for now it will only set the speed of the level.
In the level controller script’s init()
function, it sends a message to the ground controller object’s script component, addressed by its id:
msg.post("ground/controller#controller", "set_speed", { speed = self.speed })
The id of the controller game object is set to "ground/controller"
since it lives in the “ground” collection. Then we add the component id "controller"
after the hash character "#"
that separates the object id from the component id. Note that the ground script does not yet have any code to react to the set_speed
message so we have to add an on_message()
function to ground.script and add logic for that.
-- 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
At this point we should add physics collision for the ground:
Now you should be able to try running the game (Project ▸ Build). The hero character should run on the ground and it should be possible to jump with the kbd:[Space] button. If you run the game on a mobile device, you can jump by tapping on the screen.
To make life in our game world a little less dull, we should add platforms to jump on.
Create a Script file platform.script (Right-click in the Assets pane then select New ▸ Script File) and put the following code in the file, then save it:
-- 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
Note that both platform.go and platform_long.go has Script components that refer to the same script file. This is a good thing since any script changes we make to the script file will affect the behavior of both regular and long platforms.
The idea with the game is that it should be a simple endless runner. This means that the platform game objects cannot be placed in a collection in the editor. Instead we must spawn them dynamically:
-- 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, {}, 0.6)
msg.post(p, "set_speed", { speed = self.speed })
end
end
end
1- Predefined values for the Y position to spawn platforms on.
2- The update()
function is called once every frame and we use that to decide whether to spawn a regular or long platform at certain intervals (to avoid overlaps) and heights. It’s easy to experiment with various spawning algorithms to create different gameplay.
Now run the game (Project ▸ Build).
Wow, this is starting to turn into something (almost) playable…
The first thing we’re gonna do is to bring life to the hero character. Right now the poor thing is stuck in a run-loop and does not respond well to jumps or anything. The spine file that we added from the asset package actually contains a set of animations for just that.
update()
function: -- 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()
and add a call to update_animation
: ...
-- apply it to the player character
go.set_position(go.get_position() + self.velocity * dt)
update_animation(self)
...
Lua has “lexical scope” for local variables and is sensitive about the order that you place local
functions in. The function update()
calls the local functions update_animation()
and play_animation()
which means that the runtime must have seen the local functions to be able to call it. That is why we must put the functions before update()
. If you switch order of the functions you will get an error. Note that this applies to local
variables only. You can read more about Lua’s scoping rules and local functions on http://www.lua.org/pil/6.2.html
That’s all that’s needed to add jump and fall animations to the hero. If you run the game you will notice that it feels much better to play. You might also realize that the platforms unfortunately can push the hero off the screen. That is a side-effect of the collision handling but the remedy is easy–add violence and make the edges of the platforms dangerous!
Save the file.
Open hero.go, mark the Collision Object and add the “danger” name to the Mask property. Then save the file.
Open hero.script and change the on_message()
function so we get a reaction if the hero character collides with a “danger” egde:
-- 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
Change the init()
function to send a “reset” message to initialize the object, then save the file:
-- 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
If you try the game now it quickly becomes apparent that the reset mechanism doesn’t work. The hero reset is fine, but you can easily reset into a situation where you will instantly fall onto a platform edge and die again. What we want to do is to properly reset the whole level on death. Since the level is just a series of spawned platforms, we just need to track all spawned platforms and then delete them at reset:
Open the controller.script file and edit the code to store the id’s of all spawned platforms:
-- 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, {}, 0.6)
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
Open platform.script and modify it so that instead of just deleting a platform that has reached the leftmost edge, send a message to the level controller asking to remove the platform:
-- 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)
...
And now the main restart-die loop is in place!
Next up - something to live for: coins!
The idea is to put coins in the level for the player to collect. The first questions to ask is how to put them into the level. We can, for instance, develop a spawning scheme that is somehow in tune with the platform spawning algorithm. However, we chose a much easier approach in the end and just have the platforms themselves spawn coins:
Create a new script file coin.script (right-click level in the Assets pane and select New ▸ Script File). Replace the template code with the following:
-- 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
Add the script file as a Script component to the coin object (right-click the root in Outline and select Add Component from File).
The plan is to spawn the coins from the platform objects so put factories for the coins in platform.go and platform_long.go.
Now we need to modify platform.script so it spawns and deletes the coins:
-- 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
Parent-child relations are strictly a modification of the scene graph. A child will be transformed (moved, scaled or rotated) along with its parent. If you need additional “ownership” relations between game objects, you need to track that specifically in code.
The last step in this tutorial is to add a couple of lines to 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)
...
And now we have a simple, but functional game! If you make it this far you might want to continue on your own and add the following:
Download the completed version of the project here
That concludes this introductory tutorial. Now go ahead and dive into Defold. We have lots of manuals and tutorials prepared to guide you, and if you get stuck, your’re welcome to the forum.
Happy Defolding!
Did you spot an error or do you have a suggestion? Please let us know on GitHub!
GITHUB