Hello!

In order to get more comfortable with Defold I have recently been thinking about what kind of project I should write and I thought it would be kinda fun to port over Heartbeasts "Godot Action RPG" series to Defold.

While I do plan to write my code alongside the videos (first video 1, then video 2, etc.) this is not supposed to be a guide per se and I do not plan to go super in depth about everything.

Either way lets go!

Setting up the project

First things first: I created a new empty Defold project and set up the resolution inside the game.project file

001-resolution

I do not know if there is a better way to implement proper zoom in Defold but just taking your target resolution which is 1280x720 for me and then dividing it by your intended zoom level (x2) will achieve the desired effect as the window is small and when you scale it up to the target resolution it will show the desired zoom! Godot used to have a feature where you could pick an actual window resolution but also the desired rendering resolution but they also removed this for reasons.

Also since this is pixel art do not forget to change your settings from LINEAR to NEAREST to ensure the pixels aren't distorted when scaling.

001-z-graphics

Next we set up some basic WASD controls:

002-input

This is probably one of my most disliked parts of Defold, if I want to bind an action to "W" I have to scroll through the input list in the search of "W" instead of having a search or even better a "click desired key now" type of thing.

And finally I setup all animations (and some other stuff) as tile sources. Defold handles spritesheets somewhat weird in a perfect world I would love to just add my spritesheet to a Defold atlas like any other texture but then you can't access the individual parts so you either have to work with a tile source or split the graphic up into individual parts, not sure why they chose this route as this seems kinda inconvenient.

003-animations

With this the basic setup is done next we can start working on the game!

Video 1 - Moving around the bush

In the first video Heartbeast implements a simple player controller which can move into one of 8 directions and a bush, simple enough.

First we set up a game object for the bush and the player

004-go

Then we create a simple player controller script (main/player/player_controller.script) like this:

speed = 150
 
---@class PlayerController
---@field velocity vector3
 
---@param self PlayerController
function init(self)
    msg.post(".", "acquire_input_focus")
    self.velocity = vmath.vector3(0, 0, 0)
end
 
---@param self PlayerController
---@param action_id userdata
---@param action table
function on_input(self, action_id, action)
    if action_id == hash("move_up") then
        self.velocity.y = speed
    elseif action_id == hash("move_down") then
        self.velocity.y = -speed
    elseif action_id == hash("move_left") then
        self.velocity.x = -speed
    elseif action_id == hash("move_right") then
        self.velocity.x = speed
    end
end
 
---@param self PlayerController
---@param dt float
function update(self, dt)
    local position = go.get_position()
 
    position = position + self.velocity * dt
    go.set_position(position)
 
    self.velocity = vmath.vector3(0, 0, 0)
end

and with that we have a moving player just like in video 1!

005-moving

Video 2 - Make it smooth

In video 2 Heartbeast makes the player controller move more smoothly (ramp up, friction, max speed) and not faster in the diagonals by normalizing the movement vector.

I realized I already partially implemented some of the things he does here out of habit whoops!

Well first lets change our implementation to be somewhat closer to his:

local max_speed = 150
 
---@class PlayerController
---@field velocity vector3
 
---@param self PlayerController
function init(self)
    msg.post(".", "acquire_input_focus")
    self.velocity = vmath.vector3(0, 0, 0)
end
 
---@param self PlayerController
---@param action_id userdata
---@param action table
function on_input(self, action_id, action)
    if action_id == hash("move_up") then
        self.velocity.y = 1
    elseif action_id == hash("move_down") then
        self.velocity.y = -1
    elseif action_id == hash("move_left") then
        self.velocity.x = -1
    elseif action_id == hash("move_right") then
        self.velocity.x = 1
    end
end
 
---@param self PlayerController
---@param dt float
function update(self, dt)
    move(self, dt)
end
 
---@param self PlayerController
---@param dt float
function move(self, dt)
    local movement = self.velocity * max_speed * dt
 
    local position = go.get_position()
    position = position + movement
    go.set_position(position)
 
    self.velocity = vmath.vector3(0, 0, 0)
end

To avoid the speed up problem in the diagonals we just simply normalize the velocity vector:

local movement = vmath.normalize(self.velocity) * max_speed * dt

Next I realized that I have drifted away further from Heartbeasts implementation than originally intended so I also refactored this using an input vector plus adding friction, acceleration and setting a limit to the max speed (yeah too lazy to write this part properly...)

local xvmath = require("modules.xvmath")
 
local ACCELERATION = 10
local MAX_SPEED = 100
local FRICTION = 10
 
---@class PlayerController
---@field velocity vector3
---@field input_vector vector3
 
---@param self PlayerController
function init(self)
    msg.post(".", "acquire_input_focus")
    self.velocity = vmath.vector3()
    self.input_vector = vmath.vector3()
end
 
---@param self PlayerController
---@param action_id userdata
---@param action table
function on_input(self, action_id, action)
    if action.pressed and action_id == hash("move_up") then
        self.input_vector.y = 1
    elseif action.pressed and action_id == hash("move_down") then
        self.input_vector.y = -1
    end
 
    if action.pressed and action_id == hash("move_left") then
        self.input_vector.x = -1
    elseif action.pressed and action_id == hash("move_right") then
        self.input_vector.x = 1
    end
 
    if action.released and (action_id == hash("move_up") or action_id == hash("move_down")) then
        self.input_vector.y = 0
    elseif action.released and (action_id == hash("move_left") or action_id == hash("move_right")) then
        self.input_vector.x = 0
    end
 
    if self.input_vector ~= vmath.vector3() then
        self.input_vector = vmath.normalize(self.input_vector)
    end
end
 
---@param self PlayerController
---@param dt float
function update(self, dt)
    if self.input_vector == vmath.vector3() then
        self.velocity = xvmath.move_toward(self.velocity, vmath.vector3(), FRICTION * dt)
    else
        self.velocity = self.velocity + (self.input_vector * ACCELERATION * dt)
        self.velocity = xvmath.limit_length(self.velocity, MAX_SPEED * dt)
    end
 
    move(self.velocity)
end
 
---@param movement vector3
function move(movement)
    local position = go.get_position()
    position = position + movement
    go.set_position(position)
end

as you might have noticed right away Godot has a lot of fancy math functions that Defold lacks so I just opted to reimplement them in Lua:

-- This file is modules/xvmath.lua
local xmath = require("modules.xmath")
 
local M = {}
 
---@param vec vector3
---@param vec_to vector3
---@param dt float
function M.move_toward(vec, vec_to, dt)
    local vd = vec_to - vec
    local length = vmath.length(vd)
 
    if length <= dt or length < xmath.CMP_EPSILON then
        return vec_to
    else
        return vec + vd / length * dt
    end
end
 
---@param vec vector3
---@param length number
---@return vector3
function M.limit_length(vec, length)
    local l = vmath.length(vec)
    local v = vec
 
    if l > 0 and length < l then
        v = (v / l) * length
    end
 
    return v
end
 
return M
 
-- This file is modules/xmath.lua
local M = {}
 
M.CMP_EPSILON = 0.00001
 
---@param value number
---@param min number
---@param max number
---@return number
function M.clamp(value, min, max)
    return math.min(math.max(value, min), max)
end
 
return M

And with this we have implemented everything from video 2!

This is probably enough for one blog post, see you in part 2!

GitHub is also available here: https://github.com/atomicptr/DefoldActionRPG

Read part 2 to continue