I am from java world and i am working as android developer.
Gamedev is my hobby. When defold was released I try it, and I think it is realy awesome.
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
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
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
-- 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.