|
@@ -7,6 +7,37 @@ brief: In this tutorial you will create a small shooter game. This is a good sta
|
|
|
|
|
|
This tutorial goes through all the steps needed to create a small playable game in Defold. You do not need to have any prior experience with Defold, but if you have done some programming in Lua, Javascript, Python or similar, that will help.
|
|
|
|
|
|
+To get an idea about what you will build, you can try the result here:
|
|
|
+
|
|
|
+<div id="game-container" class="game-container">
|
|
|
+ <img id="game-preview" src="//storage.googleapis.com/defold-doc/assets/war-battles/preview.jpg"/>
|
|
|
+ <canvas id="game-canvas" tabindex="1" width="720" height="720">
|
|
|
+ </canvas>
|
|
|
+ <button id="game-button">
|
|
|
+ START GAME <span class="icon"></span>
|
|
|
+ </button>
|
|
|
+ <script src="//storage.googleapis.com/defold-doc/assets/dmloader.js"></script>
|
|
|
+ <script src="//storage.googleapis.com/defold-doc/assets/dmengine_1_2_106.js" async></script>
|
|
|
+ <script>
|
|
|
+ /* Load app on click in container. */
|
|
|
+ document.getElementById("game-button").onclick = function (e) {
|
|
|
+ var extra_params = {
|
|
|
+ archive_location_filter: function( path ) {
|
|
|
+ return ("//storage.googleapis.com/defold-doc/assets/war-battles" + path + "");
|
|
|
+ },
|
|
|
+ load_done: function() {},
|
|
|
+ game_start: function() {
|
|
|
+ var e = document.getElementById("game-preview");
|
|
|
+ e.parentElement.removeChild(e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ Module.runApp("game-canvas", extra_params);
|
|
|
+ document.getElementById("game-button").style.display = 'none';
|
|
|
+ document.getElementById("game-button").onclick = null;
|
|
|
+ };
|
|
|
+ </script>
|
|
|
+</div>
|
|
|
+
|
|
|
## Setting up the project
|
|
|
|
|
|
You need to create an empty project in Defold and download the asset package.
|
|
@@ -265,9 +296,9 @@ Consider the main collection for a second. Now it contains two game objects: the
|
|
|
|
|
|
1. <kbd>Right click</kbd> the folder *main* in the *Assets* view and select <kbd>New ▸ Game object</kbd>. Name this file *rocket.go*. Note that by creating this file, you do not create a game object but a file can be used as a *blueprint* when creating an actual game object.
|
|
|
|
|
|
-2. Drag the folder *buildings/turret-rocket* in the asset package to the *main* folder in the *Assets* view.
|
|
|
+2. Drag the folder *buildings/turret-rocket* from the asset package to the *main* folder in the *Assets* view.
|
|
|
|
|
|
-3. Open *sprites.atlas* and create a new animation group (right click the root node and select <kbd>New ▸ Animation group</kbd>).
|
|
|
+3. Open *sprites.atlas* and create a new animation group (right click the root node and select <kbd>New ▸ Animation group</kbd>). Name the animation "rocket".
|
|
|
|
|
|
4. Add the three rocket images to the animation group and set the *Fps* property to a value that makes the animation look good when you preview.
|
|
|
|
|
@@ -291,20 +322,373 @@ Now you have a basic rocket game object blueprint. The next step is to add funct
|
|
|
|
|
|

|
|
|
|
|
|
-5. Open *main/player.script* and scroll down to the `on_input()` function. Add a fourth `elseif` for the case where the function is called with the "fire" action:
|
|
|
+5. Open *main/player.script* and add a flag to track if the player is firing in the `init()` function:
|
|
|
+
|
|
|
+ ```lua
|
|
|
+ function init(self)
|
|
|
+ msg.post("#", "acquire_input_focus")
|
|
|
+
|
|
|
+ self.moving = false
|
|
|
+ self.firing = false -- <1>
|
|
|
+
|
|
|
+ self.input = vmath.vector3()
|
|
|
+ self.dir = vmath.vector3(0, 1, 0)
|
|
|
+ self.speed = 50
|
|
|
+ end
|
|
|
+ ```
|
|
|
+ 1. Whenever the player is firing this value will be set to `true`.
|
|
|
+
|
|
|
+6. Add what should happen when the flag is set in `update()`. The factory should create a new game object instance:
|
|
|
+
|
|
|
+ ```lua
|
|
|
+ function update(self, dt)
|
|
|
+ if self.moving then
|
|
|
+ local pos = go.get_position()
|
|
|
+ pos = pos + self.dir * self.speed * dt
|
|
|
+ go.set_position(pos)
|
|
|
+ end
|
|
|
+
|
|
|
+ if self.firing then
|
|
|
+ factory.create("#rocketfactory") -- <1>
|
|
|
+ end
|
|
|
+
|
|
|
+ self.input.x = 0
|
|
|
+ self.input.y = 0
|
|
|
+
|
|
|
+ self.moving = false
|
|
|
+ self.firing = false -- <2>
|
|
|
+ end
|
|
|
+ ```
|
|
|
+ 1. If the `firing` flag is true, tell the factory component called "rocketfactory" that you just created to spawn a new game object.
|
|
|
+ 2. Set the flag to false. This flag will be set in `on_input()` each frame the player presses the fire key.
|
|
|
+
|
|
|
+7. Scroll down to the `on_input()` function. Add a fourth `elseif` for the case where the function is called with the "fire" action:
|
|
|
|
|
|
```lua
|
|
|
...
|
|
|
elseif action_id == hash("right") then
|
|
|
self.input.x = 1
|
|
|
elseif action_id == hash("fire") and action.pressed then
|
|
|
- factory.create("#rocketfactory")
|
|
|
+ self.firing = true
|
|
|
+ end
|
|
|
+ ...
|
|
|
+ ```
|
|
|
+
|
|
|
+If you run the game now you should be able to move around and drop rockets all over the map by hammering the fire key. This is a good start, now you only need to fix three things:
|
|
|
+
|
|
|
+1. When the rocket is spawned, it should be oriented in the player's direction and it should move straight ahead.
|
|
|
+2. The rocket should explode after a second or so.
|
|
|
+
|
|
|
+Let's do these things one by one:
|
|
|
+
|
|
|
+## Setting the direction of the rocket
|
|
|
+
|
|
|
+1. Open *main/player.script* and scroll down to the `on_input()` function.
|
|
|
+
|
|
|
+ ```lua
|
|
|
+ ...
|
|
|
+ elseif action_id == hash("right") then
|
|
|
+ self.input.x = 1
|
|
|
+ elseif action_id == hash("fire") and action.pressed then
|
|
|
+ local angle = math.atan2(self.dir.y, self.dir.x) -- <1>
|
|
|
+ local rot = vmath.quat_rotation_z(angle) -- <2>
|
|
|
+ local props = { dir = self.dir } -- <3>
|
|
|
+ factory.create("#rocketfactory", nil, rot, props) -- <4>
|
|
|
end
|
|
|
|
|
|
...
|
|
|
```
|
|
|
+ 1. Compute the angle (in radians) of the player.
|
|
|
+ 2. Create a quaternion for that angular rotation around Z.
|
|
|
+ 3. Create a table containing property values to pass to the rocket. The player's direction is the only data the rocket needs.
|
|
|
+ 3. Add explicit position (`nil`, the rocket will spawn at the player's position), rotation (the calculated quaternion) and spawn property values.
|
|
|
+
|
|
|
+ Note that the rocket needs a movement direction in addition to the game object rotation (`rot`). It would be possible to make the rocket calculate its movement vector based on its rotation, but it is easier and more flexible to separate the two values. For instance, with a separate rotation it is possible to add rotation wobble to the rocket without it affecting the movement direction.
|
|
|
|
|
|
+3. <kbd>Right click</kbd> the folder *main* in the *Assets* view and select <kbd>New ▸ Script</kbd>. Name the new script file "rocket.script". Replace the code of the file with the following:
|
|
|
+
|
|
|
+ ```lua
|
|
|
+ go.property("dir", vmath.vector3()) -- <1>
|
|
|
+
|
|
|
+ function init(self)
|
|
|
+ self.speed = 200 -- <2>
|
|
|
+ end
|
|
|
+
|
|
|
+ function update(self, dt)
|
|
|
+ local pos = go.get_position() -- <3>
|
|
|
+ pos = pos + self.dir * self.speed * dt -- <4>
|
|
|
+ go.set_position(pos) -- <5>
|
|
|
+ end
|
|
|
+ ```
|
|
|
+ 1. Define a new script property named `dir` and initialize the property with a default empty vector (`vmath.vector3()`). The default value can be overrided by passing values to the `factory.create()` function. The current property value is accessed as `self.dir`.
|
|
|
+ 2. A rocket speed value, expressed in pixels per second.
|
|
|
+ 3. Get the current rocket position.
|
|
|
+ 4. Calculate a new position based on the old position, the movement direction (unit vector) and the speed.
|
|
|
+ 5. Set the new position.
|
|
|
+
|
|
|
+4. Open *rocket.go* and <kbd>Right click</kbd> the root in the *Outline* and select <kbd>Add component ▸ Script</kbd>. Select "rocket.script" for the component.
|
|
|
+
|
|
|
+5. Run the game and try the new mechanic. Notice that the rockets fly in the right direction but are oriented 180 degrees wrong.
|
|
|
+
|
|
|
+ 
|
|
|
+
|
|
|
+6. Open *sprites.atlas*, select the "rocket" animation and click the *Flip horizontal* property.
|
|
|
+
|
|
|
+ 
|
|
|
+
|
|
|
+7. Run the game again to verify that everything looks ok.
|
|
|
+
|
|
|
+ 
|
|
|
+
|
|
|
+Now you only need to make the rockets explode.
|
|
|
|
|
|
## Explosions
|
|
|
|
|
|
-##
|
|
|
+1. Drag the folder *fx/explosion* from the asset package to the main folder in the Assets view.
|
|
|
+
|
|
|
+2. Open *sprites.atlas* and create a new animation group (right click the root node and select <kbd>New ▸ Animation group</kbd>). Call the animation "explosion".
|
|
|
+
|
|
|
+3. Add the nine explosion images to the animation group and set the *Fps* property to a value that makes the animation look good when you preview. Also make sure that this animation has the *Playback* property set to `Once Forward`.
|
|
|
+
|
|
|
+ 
|
|
|
+
|
|
|
+4. Open *main/rocket.script* and scroll down to the `init()` function and change it to:
|
|
|
+
|
|
|
+ ```lua
|
|
|
+ function init(self)
|
|
|
+ self.speed = 200
|
|
|
+ self.life = 1 -- <1>
|
|
|
+ end
|
|
|
+ ```
|
|
|
+ 1. This value will act as a timer to track the lifetime of the rocket.
|
|
|
+
|
|
|
+5. Scroll down to the `update()` function and change it to:
|
|
|
+
|
|
|
+ ```lua
|
|
|
+ function update(self, dt)
|
|
|
+ local pos = go.get_position()
|
|
|
+ pos = pos + self.dir * self.speed * dt
|
|
|
+ go.set_position(pos)
|
|
|
+
|
|
|
+ self.life = self.life - dt -- <1>
|
|
|
+ if self.life < 0 then -- <2>
|
|
|
+ self.life = 1000 -- <3>
|
|
|
+ go.set_rotation(vmath.quat()) -- <4>
|
|
|
+ self.speed = 0 -- <5>
|
|
|
+ msg.post("#sprite", "play_animation", { id = hash("explosion") }) -- <6>
|
|
|
+ end
|
|
|
+ end
|
|
|
+ ```
|
|
|
+ 1. Decrease the life timer with delta time. It will decrease with 1.0 per second.
|
|
|
+ 2. When the life timer has reached zero.
|
|
|
+ 3. Set the life timer to a large value so this code won't run every subsequent update.
|
|
|
+ 4. Set the game object rotation to 0, otherwise the explosion graphics will be rotated.
|
|
|
+ 5. Set the movement speed to 0, otherwise the explosion graphics will move.
|
|
|
+ 6. Play the "explosion" animation on the game object's "sprite" component.
|
|
|
+
|
|
|
+6. Below the `update()` function, add a new `on_message()` function:
|
|
|
+
|
|
|
+ ```lua
|
|
|
+ function on_message(self, message_id, message, sender) -- <1>
|
|
|
+ if message_id == hash("animation_done") then -- <2>
|
|
|
+ go.delete() -- <3>
|
|
|
+ end
|
|
|
+ end
|
|
|
+ ```
|
|
|
+ 1. The function `on_message()` gets called whenever a message is posted to this script component.
|
|
|
+ 2. If the message posted has the hashed name (or id) "animation_done", then. The engine runtime sends back this message when a sprite animation initiated with "play_animation" has finished playing.
|
|
|
+ 3. When the animation is done, delete the current game object.
|
|
|
+
|
|
|
+Run the game. Now it starts feeling like the embryo for a nice little game, don't you think?
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+But now you need something to fire the rockets at. Enter tanks!
|
|
|
+
|
|
|
+## The tank game object
|
|
|
+
|
|
|
+1. <kbd>Right click</kbd> the folder *main* in the *Assets* view and select <kbd>New ▸ Game object</kbd>. Name this file *tank.go*. Like the rocket game object, this is a file that can be used as a *blueprint* when creating the actual tank game objects.
|
|
|
+
|
|
|
+2. Drag the folder *units/tank* from the asset package to the *main* folder in the *Assets* view.
|
|
|
+
|
|
|
+3. Open *sprites.atlas* and create a new animation group (right click the root node and select <kbd>New ▸ Animation group</kbd>). Name the animation "tank-down".
|
|
|
+
|
|
|
+4. Add the two downwards facing images (*/main/tank/down/1.png* and */main/tank/down/2.png*) to the animation and set it's *Fps* value to something that looks good.
|
|
|
+
|
|
|
+ 
|
|
|
+
|
|
|
+5. Open *tank.go* and <kbd>Right click</kbd> the root in the *Outline* and select <kbd>Add component ▸ Sprite</kbd>.
|
|
|
+
|
|
|
+6. Set the *Image* property of the sprite to "/main/sprites.atlas" and the *Default animation* to "tank-down".
|
|
|
+
|
|
|
+7. Open *main.collection*
|
|
|
+
|
|
|
+8. <kbd>Right click</kbd> the root node of the collection in the *Outline* and select <kbd>Add Game Object File</kbd>. Select *tank.go* as blueprint for the new game object.
|
|
|
+
|
|
|
+9. Create a few more tanks and position them on the map with the *Move Tool*. Make sure to set the Z position to 1.0 so they are rendered on top of the map.
|
|
|
+
|
|
|
+ 
|
|
|
+
|
|
|
+Run the game and check that the tanks look okay. You will notice that the rockets fly straight through the tanks so the next step is to add collision between the tanks and the rockets. To do this you need to add a new component to the tank and the rocket game object:
|
|
|
+
|
|
|
+## Adding collision objects
|
|
|
+
|
|
|
+1. Open *tank.go* and <kbd>Right click</kbd> the root in the *Outline* and select <kbd>Add component ▸ Collision Object</kbd>.
|
|
|
+
|
|
|
+2. Set the *Type* property to "Kinematic". This means that the physics engine will not simulate any gravity or collision on this object. Instead it will detect collisions and leave it to you to code the response.
|
|
|
+
|
|
|
+3. Set the *Group* property to "tanks" and *Mask* to "rockets". This causes this game object to detect collisions against object in the group "rockets" that has the mask set to "tanks".
|
|
|
+
|
|
|
+4. <kbd>Right click</kbd> the "collisionobject" component in the *Outline* and select <kbd>Add Shape ▸ Box</kbd>. Set the size of the box shape to match the tank graphics.
|
|
|
+
|
|
|
+ 
|
|
|
+
|
|
|
+6. Open *rocket.go* and <kbd>Right click</kbd> the root in the *Outline* and select <kbd>Add component ▸ Collision Object</kbd>.
|
|
|
+
|
|
|
+7. Set the *Type* property to "Kinematic".
|
|
|
+
|
|
|
+8. Set the *Group* property to "rockets" and *Mask* to "tanks". This causes this game object to detect collisions against object in the group "tanks" that has the mask set to "rockets". Since that is what you set the group and mask to in *tank.go* the rockets and tanks will interact physically.
|
|
|
+
|
|
|
+ 
|
|
|
+
|
|
|
+Now the engine will detect when these game objects intersect and send messages to them when that happens. The last piece of the puzzle is an addition to the rocket's script.
|
|
|
+
|
|
|
+## Reacting to collisions
|
|
|
+
|
|
|
+1. Open *rocket.script* and scroll down to the `update()` function. There are a couple of things to do here:
|
|
|
+
|
|
|
+ ```lua
|
|
|
+ local function explode(self) -- <1>
|
|
|
+ self.life = 1000
|
|
|
+ go.set_rotation(vmath.quat())
|
|
|
+ self.speed = 0
|
|
|
+ msg.post("#sprite", "play_animation", { id = hash("explosion") })
|
|
|
+ end
|
|
|
+
|
|
|
+ function update(self, dt)
|
|
|
+ local pos = go.get_position()
|
|
|
+ pos = pos + self.dir * self.speed * dt
|
|
|
+ go.set_position(pos)
|
|
|
+
|
|
|
+ self.life = self.life - dt
|
|
|
+ if self.life < 0 then
|
|
|
+ explode(self) -- <2>
|
|
|
+ end
|
|
|
+ end
|
|
|
+
|
|
|
+ function on_message(self, message_id, message, sender)
|
|
|
+ if message_id == hash("animation_done") then
|
|
|
+ go.delete()
|
|
|
+ elseif message_id == hash("collision_response") then -- <3>
|
|
|
+ explode(self) -- <4>
|
|
|
+ go.delete(message.other_id) -- <5>
|
|
|
+ end
|
|
|
+ end
|
|
|
+ ```
|
|
|
+ 1. Since you want the rocket to explode either when the timer runs out (in `update()`) or when the rocket hits a tank (in `on_message()`) you should break out that piece of code to avoid duplication. There are a few options, the easiest is probably to just create a local function. The function is declared `local` meaning it only exist for the rocket script. Therefore it must be placed before it is used, above `update()`, this is how Lua's scoping rules work. Also note that you should pass `self` as a parameter to the function to be able to access `self.life` etc.
|
|
|
+ 2. The code that used to live here has been moved to the `explode()` function.
|
|
|
+ 3. The engine sends a message called "collision_response" when the shapes collide, if the group and mask pairing is correct.
|
|
|
+ 4. Call the `explode()` function if there is a collision.
|
|
|
+ 5. Finally delete the tank. You get the id of the game object the rocket collided with in the `message.other_id` variable.
|
|
|
+
|
|
|
+And now you can run the game and try to destroy some tanks. The tanks aren't very interesting enemies, but they should nevertheless give you some score.
|
|
|
+
|
|
|
+## Scoring GUI
|
|
|
+
|
|
|
+1. Drag the the file *fonts/04B_03.TTF* from the asset pack folder to the *main* folder in the *Assets* view.
|
|
|
+
|
|
|
+2. <kbd>Right click</kbd> the folder *main* in the *Assets* view and select <kbd>New ▸ Font</kbd>. Name this file *text.font*.
|
|
|
+
|
|
|
+ 
|
|
|
+
|
|
|
+3. <kbd>Right click</kbd> the folder *main* in the *Assets* view and select <kbd>New ▸ Gui</kbd>. Name this file *ui.gui*. It will contain the user interface where you will place the score counter.
|
|
|
+
|
|
|
+4. Open *ui.gui*. <kbd>Right click</kbd> *Fonts* in the *Outline* view and select <kbd>Add ▸ Fonts</kbd>. Select the */main/text.font* file.
|
|
|
+
|
|
|
+5. <kbd>Right click</kbd> *Nodes* in the *Outline* view and select <kbd>Add ▸ Text</kbd>.
|
|
|
+
|
|
|
+6. Select the new text node in the outline and set its *Id* property to "score", its *Text* property to "SCORE: 0", its *Font* property to the font "text" and its *Pivot* property to "West".
|
|
|
+
|
|
|
+7. Place the text node in the top left corner of the screen.
|
|
|
+
|
|
|
+ 
|
|
|
+
|
|
|
+8. <kbd>Right click</kbd> the folder *main* in the *Assets* view and select <kbd>New ▸ Gui Script</kbd>. Name this file *ui.gui_script*.
|
|
|
+
|
|
|
+9. Go back to *ui.gui* and select the root node in the *Outline*. Set the *Script* property to the file */main/ui.gui_script* that you just created. Now if we add this Gui as a component to a game object the script will run for the component.
|
|
|
+
|
|
|
+10. Open *main.collection*.
|
|
|
+
|
|
|
+11. <kbd>Right click</kbd> the root node of the collection in the *Outline* and select <kbd>Add game object</kbd>.
|
|
|
+
|
|
|
+11. Set the id of the game object to "gui", then <kbd>Right click</kbd> it and select <kbd>Add Component File</kbd>. Select the file */main/ui.gui*.
|
|
|
+
|
|
|
+ 
|
|
|
+
|
|
|
+Now there is a score counter on screen. You only need to add functionality in the Gui script so the score can be updated.
|
|
|
+
|
|
|
+## Updating the score
|
|
|
+
|
|
|
+1. Open *ui.gui_script*
|
|
|
+
|
|
|
+2. Replace the template code with the following:
|
|
|
+
|
|
|
+ ```lua
|
|
|
+ function init(self)
|
|
|
+ self.score = 0 -- <1>
|
|
|
+ end
|
|
|
+
|
|
|
+ function on_message(self, message_id, message, sender)
|
|
|
+ if message_id == hash("add_score") then -- <2>
|
|
|
+ self.score = self.score + message.score -- <3>
|
|
|
+ local scorenode = gui.get_node("score") -- <4>
|
|
|
+ gui.set_text(scorenode, "SCORE: " .. self.score) -- <5>
|
|
|
+ end
|
|
|
+ end
|
|
|
+ ```
|
|
|
+ 1. Store the current score in `self`. Start from 0.
|
|
|
+ 2. Reaction to a message named "add_score".
|
|
|
+ 3. Increase the current score value in `self` with the value passed in the message.
|
|
|
+ 4. Get hold of the text node named "score" that you created in the Gui.
|
|
|
+ 5. Update the text of the node to the string "SCORE: " and the current score value concatenated to the end of the string.
|
|
|
+
|
|
|
+3. Open *rocket.script* and scroll down to the `on_message()` function where you need to add a line of code:
|
|
|
+
|
|
|
+ ```lua
|
|
|
+ function on_message(self, message_id, message, sender)
|
|
|
+ if message_id == hash("animation_done") then
|
|
|
+ go.delete()
|
|
|
+ elseif message_id == hash("collision_response") then
|
|
|
+ explode(self)
|
|
|
+ go.delete(message.other_id)
|
|
|
+ msg.post("/gui#gui", "add_score", {score = 100}) -- <1>
|
|
|
+ end
|
|
|
+ end
|
|
|
+ ```
|
|
|
+ 1. Post a message named "add_score" to the component "gui" in the game object named "gui" at the root of the main collection. Pass along a table where the entry `score` has been set to 100.
|
|
|
+
|
|
|
+4. Try the game!
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+Well done! We hope you enjoyed this tutorial and that it was helpful.
|
|
|
+
|
|
|
+## What next?
|
|
|
+
|
|
|
+To get to know Defold better, we suggest that you to continue working with this little game. Here are a few good exercises:
|
|
|
+
|
|
|
+1. Add directional animations for the player character. Tip, add a function called `update_animation(self)` to the `update()` function and change the animation depending on the value of the `self.dir` vector. It is also worth remembering that if you send a "play_animation" message each frame to a sprite, the animation will restart from the beginning, each frame---so you should only send "play_animation" when the animation should change.
|
|
|
+
|
|
|
+2. Add an "idle" state to the player character so it only plays a walking animation when moving.
|
|
|
+
|
|
|
+3. Make the tanks spawn dynamically. Look at how the rockets are spawned and do a similar setup for the tanks. You might want to create a new game object in the main collection with a script that controls the tank spawning.
|
|
|
+
|
|
|
+4. Make the tanks patrol the map. One simple option is to have the tank pick a random point on the map and move towards that point. When it is within a short distance of the point, it picks a new point.
|
|
|
+
|
|
|
+5. Make the tanks chase the player. One option is to add a new collision object to the tank with a spherical shape. If the player collides with the collision object, have the tank drive towards the player.
|
|
|
+
|
|
|
+6. Make the tanks fire at the player.
|
|
|
+
|
|
|
+7. Add sound effects.
|
|
|
+
|
|
|
+If you are stuck, please head over to the [Defold Forum](//forum.defold.com) where you can talk to the Defold developers and many friendly users.
|