Skip to content

Instantly share code, notes, and snippets.

@byJeevan
Created September 15, 2019 15:15
Show Gist options
  • Save byJeevan/23b13453dc8fb7522d8b2acfd3d33889 to your computer and use it in GitHub Desktop.
Save byJeevan/23b13453dc8fb7522d8b2acfd3d33889 to your computer and use it in GitHub Desktop.
Read me of DEFOLD
Colorslide tutorial
Welcome to the Colorslide game tutorial. It will take you through the process of adding a simple GUI flow to an existing multi level mobile game. It is assumed that you know your way around the editor. If you don't, please check out our manuals and beginner tutorials.
The starting point for this tutorial is this project. It contains everything you need:
A simple but fully playable game where the player slides colored bricks on a tiled board until every brick's color matches the tile they sit on.
4 example levels are included. They are of various difficulty. Each level is built in its own collection.
Assets are included so you can build any number of levels, using the built in tile editor.
After having finished the tutorial, you will have accomplished the following:
You have added a level selection screen from where the player can start any of the 4 levels.
You have added a level completion message allowing the player to continue to the next level.
You have added a start screen.
You have added buttons so the user can navigate between these screens.
Understanding the game setup
Before beginning the tutorial, try running the game, then open "main.collection" to see how the game is set up.
The whole game is contained in a subcollection called "level" inside "main.collection". Currently, "level" references the file "/main/level_2/level_2.collection". Opening the "level" collection file reveals two game objects:
One game object with id "board". This one contains the tilemap. Notice that there are two layers on the tilemap, one is the actual playfield (with layer id "board") and one contains the intitial setup for the bricks (with layer id "setup"). When the game starts, it looks at the setup layer and replaces the brick tiles with separate game objects that can be animated freely. It then clears the layer.
One game object with id "level". This one contains the game logic script ("level.script") and a factory used to spawn bricks on game start. This game object is stored in a separate file called "/main/level.go" so game objects of this blueprint file can be instantiated in each separate level collection.
Now try some of the other levels:
Open "main.collection".
Mark "level" change its reference in the Path property to "/main/level_3/level_3.collection".
Build and run the game again (Project ▸ Build). Do the same for levels 1 and 4.
Loading collections via proxy
What is needed for this game is a way to make the level loading automatic and depending on player choice. Defold contains two mechanisms for loading collections dynamically:
Collection factories. This is a good choice for spawning hierarchies of objects into a running game, like enemy units, effects or interactive objects. Any spawned object will be part of the startup main collection world and live until the game shuts down, unless you explicitly delete the objects yourself.
Collection proxies. This is a good choice when you want to load larger chunks of a game dynamically, for instance a game level. With a proxy you create a new "world" based on the collection. Any object that is spawned from the collection content will be part of the created world and be automatically destroyed when the collection is unloaded from the proxy. The new world that is created has an overhead cost attached to it so proxies are not a good fit for spawning large quantities of small collections simultaneously.
For this game, proxies will be the best choice.
Open "main.collection" and remove the "level" collection reference. Instead, add a new game object and give it id "loader".
Add a collection proxy component to the game object, name it "proxy_level_1" and set its Collection property to "/main/level_1/level_1.collection".
Add a new script file called "loader.script" and add it as a script component to the "loader" game object.
Open "loader.script" and change its content to the following:
function init(self)
msg.post("#proxy_level_1", "load") -- [1]
end
function on_message(self, message_id, message, sender)
if message_id == hash("proxy_loaded") then -- [2]
msg.post(sender, "init")
msg.post(sender, "enable")
end
end
Send a message to the proxy component telling it to start loading its collection.
When the proxy component is done loading, it sends a "proxy_loaded" message back. You can then send "init" and "enable" messages to the collection to initialize and enable the collection content. We can send these messages back to "sender" which is the proxy component.
Now try to run the game.
Unfortunately there is an instant error. The console says:
ERROR:GAMEOBJECT: The collection 'default' could not be created since there is
already a socket with the same name.
WARNING:RESOURCE: Unable to create resource: /main/level_1/level_1.collectionc
ERROR:GAMESYS: The collection /main/level_1/level_1.collectionc could not be loaded.
This error occurs because the proxy tries to create a new world (socket) with the name "default". But a world with that name already exists - the one created from "main.collection" at engine boot. The socket name is set in the properties of the collection root so it's very easy to fix:
Open the file "/main/level_1/level_1.collection", mark the root of the collection and set the Name property to "level_1". And, mark the level.go and set the Id property to "level_1". Save the file.
Try running the game again.
The level now shows up, but if you try to click on the board to move a tile, nothing happens. Why is that?
The problem is that the script that deals with input is now inside the proxied world. The input system works like this:
It sends input to all game objects in the bootstrap collection that has acquired input focus.
If one of these objects listening to input contains a proxy, input is directed to any object in the game world behind the proxy that has acquired input focus.
So in order to get input into the proxied collection, the game object that contains the proxy component must listen to input.
Open "loader.script" and add a line to the init() function:
function init(self)
msg.post("#proxy_level_1", "load")
msg.post(".", "acquire_input_focus") -- [1]
end
Since this game object holds the proxy for the collection that needs input, this game object needs to acquire input focus too.
Run the game again. Now everything should work as expected.
Because the game contains four levels you need to add proxy components for the remaining three levels. Don't forget to change the Id property to a unique name for each level collection so the socket names don't collide when a proxy loads.
Test that each level loads by altering the proxy component you send the "load" message:
msg.post("#proxy_level_1", "load")
msg.post("#proxy_level_2", "load")
msg.post("#proxy_level_3", "load")
msg.post("#proxy_level_4", "load")
The level selection screen
Now you have built the setup required to load any at any moment so it is time to construct an interface to the level loading.
Create a new GUI file and call it "level_select.gui".
Add the "headings" font to the Font section of the GUI (right click the Fonts item in the outline and select Add ▸ Fonts...).
Add the "bricks" atlas to the Textures section of the GUI (right click the Textures item in the outline and select Add ▸ Textures...).
Construct an interface with 4 buttons, one for each level. For each button:
Create one root Box node (right click Nodes and select Add ▸ Box).
Set the Id to "level_1".
Set the Size Mode to Manual and the Size to 100, 100, 0.
Set the Alpha to 0 so the node will be invisible.
Create a child Box node to "level_1" (right click "level_1" and select Add ▸ Box).
Set the Id of the child node to "1_bg".
Set the Texture of the node to bricks/button.
Uncheck Inherit Alpha on the node so it renders even if its parent is transparent.
Create a child Text node to "level_1" (right click "level_1" and select Add ▸ Text).
Set the Id of the child node to "1_text".
Set the Text of the node to "1".
Set the Font of the node to headings.
Uncheck Inherit Alpha on the node so it renders even if its parent is transparent.
If you change the size of your graphics make sure that each root node is big enough to cover the whole button graphics because the root node will be used to test input against.
Repeat the above steps for all 4 level buttons and move each root node into position:
Create a new GUI script file and call it "level_select.gui_script".
Open "level_select.gui_script" and change the script to the following:
function init(self)
msg.post(".", "acquire_input_focus")
msg.post("#", "show_level_select") -- [1]
self.active = false
end
function on_message(self, message_id, message, sender)
if message_id == hash("show_level_select") then -- [2]
msg.post("#", "enable")
self.active = true
elseif message_id == hash("hide_level_select") then -- [3]
msg.post("#", "disable")
self.active = false
end
end
function on_input(self, action_id, action)
if action_id == hash("touch") and action.pressed and self.active then
for n = 1,4 do -- [4]
local node = gui.get_node("level_" .. n)
if gui.pick_node(node, action.x, action.y) then -- [5]
msg.post("/loader#loader", "load_level", { level = n }) -- [6]
msg.post("#", "hide_level_select") -- [7]
end
end
end
end
Set up the GUI.
Showing and hiding the GUI is triggered via messaging so it can be done from other scripts.
React to the pressing of touch input (as already set up in the input bindings).
The button nodes are named "level_1" to "level_4" so they can be looped over.
Check if the touch action happens within the boundaries of node "level_n". This means that the click happenen on the button.
Send a message to the loader script to load level n. Notice that a "load" message is not sent directly to the proxy from here since this script does not deal with the rest of the proxy loading logic, as a reaction to "proxy_loaded".
Hide this GUI.
Open "level_select.gui" and set the Script property on the root node to the new script.
To finish off this step, the loader script needs a bit of new code to react to the "load_level" message, and the proxy loading on init should be removed.
Open "loader.script" and change the init() and on_message() functions:
function init(self)
msg.post(".", "acquire_input_focus")
end
function on_message(self, message_id, message, sender)
if message_id == hash("load_level") then
local proxy = "#proxy_level_" .. message.level -- [1]
msg.post(proxy, "load")
elseif message_id == hash("proxy_loaded") then
msg.post(sender, "init")
msg.post(sender, "enable")
end
end
Construct which proxy to load based on message data.
Open "main.collection" and add a new game object with id "guis".
Add "level_select.gui" as a GUI component to the new "guis" game object.
Run the game and test the level selector screen. You should be able to click any of the level buttons and the corresponding level will load and be playable.
In game GUI
You can now start and play a level but there is no way to go back. The next step is to add an in game GUI that allows you to navigate back to the level selection screen. It should also congratulate the player when the level is completed and allow moving directly to the next level:
Create a new GUI file and call it "level.gui".
Add "headings" to the Font section and the "bricks" atlas to the Textures section of the GUI.
Build one back-button at the top and one level number indicator at the top.
Build a level complete message with a "well done" message and a "next"-button. Child these to a panel (a colored box node), call it "done" and place it outside of the view so they can be slid into view when the level is completed:
Create a new GUI script file and call it "level.gui_script".
Open "level.gui_script" and change the script to the following:
function on_message(self, message_id, message, sender)
if message_id == hash("level_completed") then -- [1]
local done = gui.get_node("done")
gui.animate(done, "position.x", 320, gui.EASING_OUTSINE, 1, 1.5)
end
end
function on_input(self, action_id, action) -- [2]
if action_id == hash("touch") and action.pressed then
local back = gui.get_node("back")
if gui.pick_node(back, action.x, action.y) then
msg.post("default:/guis#level_select", "show_level_select") -- [3]
msg.post("default:/loader#loader", "unload_level")
end
local next = gui.get_node("next")
if gui.pick_node(next, action.x, action.y) then
msg.post("default:/loader#loader", "next_level") -- [4]
end
end
end
If message "level_complete" is received, slide the "done" panel with the "next" button into view.
This GUI will be put on the "level" game object which already acquires input focus (through "level.script") so this script should not do that.
If the player presses "back", tell the level selector to show itself and the loader to unload the level. Note that the socket name of the bootstrap collection is used in the address.
If the player presses "next", tell the loader to load the next level.
Open "level.gui" and set the Script property on the root node to the new script.
Open "loader.script" and change it to the following:
function init(self)
msg.post(".", "acquire_input_focus")
self.current_level = 0 -- [1]
end
function on_message(self, message_id, message, sender)
if message_id == hash("load_level") then
self.current_level = message.level
local proxy = "#proxy_level_" .. self.current_level
msg.post(proxy, "load")
elseif message_id == hash("next_level") then -- [2]
msg.post("#", "unload_level")
msg.post("#", "load_level", { level = self.current_level + 1 })
elseif message_id == hash("unload_level") then -- [3]
local proxy = "#proxy_level_" .. self.current_level
msg.post(proxy, "disable")
msg.post(proxy, "final")
msg.post(proxy, "unload")
elseif message_id == hash("proxy_loaded") then
msg.post(sender, "init")
msg.post(sender, "enable")
end
end
Keep track of the currently loaded level so it can be unloaded and it is possible to advance to the next one.
Load next level. Note that there is no check if there actually exists a next level.
Unload the currently loaded level.
Open "level.script" and add a message to the level gui when the game is finished at the end of on_input():
...
-- check if the board is solved
if all_correct(self.bricks) then
msg.post("#gui", "level_completed") -- [1]
self.completed = true
end
end
...
Tell the GUI to show the level completed panel.
Finally, open "level.go" and add "level.gui" as a GUI component to the game object. Make sure to set the Id property of the component to "gui".
Run the game. You should be able to select a game, go back to the level selection screen (with the "back" button) and also start the next level when one is finished.
Start screen
The final piece of the puzzle is the start screen:
Create a new GUI file and call it "start.gui".
Add "headings" to the Font section and the "bricks" atlas to the Textures section of the GUI.
Build the front screen. Add logo and a "start" button:
Create a new GUI script file and call it "start.gui_script".
Open "start.gui_script" and change the script to the following:
function init(self)
msg.post("#", "show_start") -- [1]
self.active = false
end
function on_message(self, message_id, message, sender)
if message_id == hash("show_start") then -- [2]
msg.post("#", "enable")
self.active = true
elseif message_id == hash("hide_start") then
msg.post("#", "disable")
self.active = false
end
end
function on_input(self, action_id, action)
if action_id == hash("touch") and action.pressed and self.active then
local start = gui.get_node("start")
if gui.pick_node(start, action.x, action.y) then -- [3]
msg.post("#", "hide_start")
msg.post("#level_select", "show_level_select")
end
end
end
Start by showing this screen.
Messages to show and hide this screen.
If the player presses the "start" button, hide this screen and tell the level selection GUI to show itself.
Open "start.gui" and set the Script property on the root node to the new script.
Open "main.collection" and add "start.gui" as a GUI component to the "guis" game object.
Now open "level_select.gui" and add a "back" button. You can copy and paste the one you made in "level.gui" if you want.
Open "level_select.gui_script" and add the code for returning to the start screen in on_input():
function on_input(self, action_id, action)
if action_id == hash("touch") and action.pressed and self.active then
for n = 1,4 do
local node = gui.get_node("level_" .. n)
if gui.pick_node(node, action.x, action.y) then
msg.post("/loader#loader", "load_level", { level = n })
msg.post("#level_select", "hide_level_select")
end
end
local back = gui.get_node("back") -- [1]
if gui.pick_node(back, action.x, action.y) then
msg.post("#level_select", "hide_level_select")
msg.post("#start", "show_start")
end
end
end
Check if the player clicks "back". If so, hide this GUI and show the start screen.
Also edit the init() function so the level select GUI is hidden on startup.
function init(self)
msg.post(".", "acquire_input_focus")
msg.post("#", "hide_level_select") -- [1]
self.active = false
end
Hide the GUI on startup
And that's it. You are done! Run the game and verify that everything works as expected.
What next?
This GUI implementation is pretty simple. Each screen deals with its own state and contains the code to hand over control to the next screen by sending messages to the other GUI component.
If your game does not feature advanced GUI flows this method is sufficient and clear enough. However, for advanced GUIs things can get hairy and in that case you might want to use some sort of screen manager that controls the flow from a central location. You can either roll your own or include an existing one as a library. Check out https://www.defold.com/community/assets/ for community written GUI libraries.
If you want to continue experimenting with this tutorial project, here are some exercise suggestions:
You may have noticed that the "Level 1" header while playing a level is static. Add functionality so the header text shows the correct level number.
Implement unlocking of levels. Start the game with all but the first level locked and unlock them one by one as the game progresses.
Implement saving of the level unlock progression state.
Fix the case where the player completes the last level and there is no "next" one.
Use GUI templates to create the buttons.
Make the buttons response visually (react to press) and with sound.
Add sound to the game.
Create a solution to when there are more levels than what fits the screen.
Check out the documentation pages for more examples, tutorials, manuals and API docs.
If you run into trouble, help is available in our forum.
Happy Defolding!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment