Thinking about Lua coding and defold

I am from java world and i am working as android developer. :slight_smile:
Gamedev is my hobby. When defold was released I try it, and I think it is realy awesome. :smile:
I try lua first time in defold. Lua is cool language. I make some rules for me to keep my code simple and readable.

1)Codestyle.

To make a code readable you need a codestyle.
As a start point I use http://lua-users.org/wiki/LuaStyleGuide.

For me it was hard to use underscore instead of CamelCase. But when I use lua, I also use lua libs and it is strange when in code you see underscore and CamelCase. It looks very bad.

2)DRY, KISS,YAGNI.

Cool ideas that you should use when you programming
DRY- don’t repear yourself
KISS – keep it simple stupid
YAGNI - you ain’t gonna need it

3)Classes.

Lua don’t have classes. But I use classes in my modules. Sometimes I need inheritance, but more often I need ability to create a different instances. Making class in lua is simple.

-- class example
local M = {}
M.__index = M
function M.new()
    local self = setmetatable({}, M)
    return self
end
return M

4)Modules.

I have three types of modules.They have different goal,and naming.

  • Constants. Only contains variables.Use caps when require.
HASHES = require "lib.hashes"
-- lib/hashes.lua
local M = {}
M.INPUT_TOUCH=hash("touch")
…
  • Сlasses.
MsgReceiver = require "lib.msg_receiver"
  • Usefull functions. Contains function and can have constants. Example lume.
 lume =  require "lib.lume"

5)Intro point.

In defold you have init function in script. But what if you need init some modules before anybody call it. I use render script for that case.

math.randomseed(os.time())
lock_mouse.lock_mouse()
MsgReceiver = require "lib.msg_receiver"
InputReceiver = require "lib.input_receiver"
HASHES = require "lib.hashes"
input =  require "lib.input"

function init(self)
    …

6)Globals

I use globals to share modules that I use often.
In render I init that globals

MsgReceiver = require "lib.msg_receiver"
InputReceiver = require "lib.input_receiver"
HASHES = require "lib.hashes"
input =  require "lib.input"

7)Perfomance.

1)Cache if you can. Don’t do same work if you can do it once. Hashes, table creations.
2)Avoid GC. I am from android, and before defold I use libgdx(java) for gamedev. And GC is one of the biggest problem in java on android. Looks like lua don’t have so much problem with it, but you should avoid it if you can.
3)https://springrts.com/wiki/Lua_Performance .Advices about lua perfomance.

8)It is all about data(MVC, Observables)

Looks like game can be split to data and view. The idea that you can change model and don’t broke view and vice versa.
For every entity(player,enemy) in my game I have a model. Model have data, and some functions to get data. MODEL CAN’T CHANGE DATA. Every model have a controller that can change data. Also every view(script, gui_script) can subscribe on changes.

local BaseMC = require("lib.controllers.base_mc")
local PlayerMC = BaseMC:subclass('PlayerMC')
local lume = require("lib.lume")
local weapons = require("pseudo.modules.models.weapons")

function PlayerMC:initialize(model)
    local events={
        HEALTH_CHANGED=hash("player_health_changed"),
    }
    BaseMC.initialize(self,model,events)
end

function PlayerMC:change_health(value)
    self.model.health = lume.clamp(self.model.health+value,0,self.model.max_health)
    self:notify(self.events.HEALTH_CHANGED)
end

return PlayerMC

In gui:

player.controller:registerObserver({events.HEALTH_CHANGED})

9)On message and on input.

BAD :rage:

function on_message(self, message_id, message, sender)
    if message_id == hashes.PHYSICS_MESSAGE_CONTACT and message.group ~= hashes.PHYSICS_GROUP_PICKUPS then
        if(vmath.length(normal * distance)<=0)then
                return
	end	
	if distance > 0 then
            local proj = vmath.project(self.correction, normal * distance)
            if proj < 1 then
            ...
	    end
	end
    elseif(message_id==hashes.PLAYER_CHANGE_POSITION)then
        go.set_position(player.pos)
    end	
end

BETTER :neutral_face:

function on_message(self, message_id, message, sender)
    if message_id == hashes.PHYSICS_MESSAGE_CONTACT and message.group ~= hashes.PHYSICS_GROUP_PICKUPS then
        handle_geometry(self,message.distance,message.normal)
    elseif(message_id==hashes.PLAYER_CHANGE_POSITION)then
        go.set_position(player.pos)
    end	
end

THE BEST :grinning:

-- Msg_receiver
local M = {}
M.__index = M

local function ensure_hash(string_or_hash)
    return type(string_or_hash) == "string" and hash(string_or_hash) or string_or_hash
end

function M.new()
    local self = setmetatable({}, M)
    self.msg_funs = {}
    return self
end

function M:add(message_id,fun)
    local message_id = ensure_hash(message_id)
    assert(not self.msg_funs[message_id],message_id .. " already used")
    self.msg_funs[message_id] = fun
end	

function M:on_message(go_self, message_id, message, sender)
    local fun = self.msg_funs[message_id]
    if fun then
        fun(go_self, message_id, message, sender)
        return true
    end
    return false	
end	
return M

function init(self)
	self.msg_receiver = MsgReceiver.new()
	self.msg_receiver:add(hashes.PHYSICS_MESSAGE_CONTACT, handle_geometry)
	self.msg_receiver:add(hashes.PLAYER_CHANGE_POSITION , update_pos)
end

function on_message(self, message_id, message, sender)
	self.msg_receiver:on_message(self, message_id, message, sender)
end

I use this rules in my projects and looks like there working. If someone use other rules and practices, write about them.

16 Likes

Brilliant! Thanks for sharing!

I’m curious about this approach. Are you also doing some setup in the init() function of the render script or only outside the scope of the lifecycle functions? In any case, couldn’t you do this in a script attached to a game object in the bootstrap collection? Or perhaps have a thin “loader” collection that does setup and then loads the rest via a proxy?

3 Likes

Thanks for sharing! Definitely will try the tips in some of the parts for my current project, and in the future as well :slight_smile:

1 Like

I do it outside of scope. Looks like doing it in init, is the same idea.

If i have game objects in collection I don’t understand which script will be loaded first.

This is the best way. Also having simple main collection, and load other with proxy good idea. Because it is make simple to change screens in game. I am thinking about it, but not try yet.

1 Like

Yeah, that order isn’t guaranteed. But you can be sure that any code outside of the lifecycle functions (init, final, update etc) will be run before any lifecycle function is called.

2 Likes

Thank you for sharing good stuff.

About loader proxy:
Make “loader” collection with 1 GO and 1 loader script in bootstrap it good idea and you garantee loader collection script load first. My project have about 1.7GB unpacked textures and loading about 20-30 sec. I maked loader collection for async load proxy with levels graphics and one GO with spinner :slight_smile:

4 Likes

1.7 GB, that’s quite a lot. Is all of it used always or can you load/unload as needed?

It fullhd graphics with many anims. About 875 png files. It used always on one level, but load/unload between levels of course.

1 Like

Nice list, liked the approach for on_message, agreeing with every other thing too (not a huge fan of having MVC everywhere though).

Btw, @britzl, how about setting 2 spaces as a default setting in the editor, following the styleguide? :3

2 Likes

OH, NO! Two spaces, are you crazy!?!?!?!?! Nah, jokes aside, things such as tab vs spaces and indentation size should be configurable further down the road.

2 Likes

Yes, i was only proposing, that if there are customizable code formatting options for the editor, that the default option would be with 2 spaces, just as in the styleguide (and not tabs as right now, if i’m not mistaken).

2 spaces actually sound quite ridiculous at first, but we decided to use it and haven’t had any problems with that :slight_smile:

1 Like

Great post, thank you!

Only one thing… As I think, using globals this way - you lose more than win.

  1. It’s bad for encapsulation, code harder for reading and understanding and too easy make a shadowing of variable.
  2. Globals are bad for performance. More information about how to Lua works with globals here
3 Likes

Thx for post. Small remarks:
5) Enter point. I also use “loader” collection - here i setup sounds, pre init modules, also “loader” is a “scene selector” for me. I found “loader” functionality very usefull.
6) Globals. From perfomance reasons i use global in limited way - only sensetive datas stored as global to avoid agressive messaging in some cases. Yeah easy for data access. I configure globals in one place (“loader”) and prefix such var names as “global_” so sources more readable.
8) MVC. I think this pattern not usefull for small and middle size projects and add unneccessary complexity in code base.
Thx

1 Like

About MVC. I am a big fun of MVC,MVP and etc. But looks like it is hard and unnecessary to build game around this pattern. But for some tasks it is very useful. If you don’t build all game around MVC, it is add a few complexity, but make your logic very simple.

1 Like