Haxe + Defold = ❤️

Thanks, I’ll read that after work ^^

Just an typical example of game dev: I have a Player object, I need to decrease the HP by 1.

In Unity, in your player component, you do:

public void DecreaseHealth(int health)
{
    ...
}

In the caller, you do:

DecreaseHaelth(1);

And oops, I spell “health” wrong, the IDE will give me a red underline and I correct the error.

In Defold, in your player script, you do:

function on_message(self, id, message, sender)
    if message_id == hash("decrease_health") then
        ...
    end
end

In the caller, you do:

msg.post("player#controller", "decrease_haelth", { by: 1 })

And oops, I spell “health” wrong. The game runs fine though, until I realize the health is not decreased. After some debugging (assuming I can reproduce the trigger easily) I finally find out that I spell “health” wrong.

So yes, Hot Reload makes the feedback loop faster. However, static type languages will tell you this error as fast as you type it (assuming you are using a decent IDE). And you are much less likely to make this error since there’s IntelliSense.

Linter can help. However, my problem with linter is that it does not discover all error and especially in this case, since message passing is “string-based”, it is impossible for linters to spot the problem.

3 Likes

Usually, I have a module msgs (or smth like this) and it looks like:

local M ={}
...
M.HEALTH_DECREASE = hash("msgs.HEALTH_DECREASE")
...
return M

I require this module everywhere where I wanna use messages. And Atom helps me with autocompletion. Even without autocompletion if I write wrong I will receive a message about a nil value for message_id.

5 Likes

FWIW I made a helper for that in hxdefold: http://hxdefold.github.io/hxdefold/defold/support/MessageBuilder.html

I like the @:msg function idea still and probably will implement a quick prototype for that soon-ish :slight_smile:

3 Likes

I have similar workarounds but I don’t store hashed value. Storing hashed value should improve performance but how do you use it when sending messages?
You cannot do the following right?

msg.post("player#controller", M.HEALTH_DECREASE, { by: -1 })

Yes, it should improve performance a bit, I am using it next way:

local msgs = require "modules.msgs"
...
msg.post("player#controller", msgs.HEALTH_DECREASE, { by: -1 })
...

Also it possible to register your go’s in some module with paths and do not use “player#controller”, something like:

local paths = require "modules.paths"

function init(self)
  paths.register("player")
end

function final(self)
  paths.remove("player")
end

and path.lua:

local M = {}
...
function M.register(name)
   M[name] = msg.url()
end

function M.remove(name)
  M[name] = nil
end
...
return M

And now we can use :

local msgs = require "modules.msgs"
local paths = require "modules.paths" --we can use one module here
...
if paths.player then --now we can check "if player exist"
  msg.post(paths.player, msgs.HEALTH_DECREASE, { by: -1 })
end
...

Of course it is just a dirty example and of course, it’s possible to do better.
But what I wanna say, When I stared with Defold I hate Lua (I have exp. with AS3, C#, Haxe, Typescript before), but after some time I found it powerful and convenient for game development.
It is possible to avoid most of your concerns just using code architecture decisions. But it needs time to found them.
But… I like how Haxe help to avoid the same problems out of the box - it’s a really powerful language and Dan did a great job. If you like Haxe and don’t wanna spend your time to learn Lua, Haxe is a great solution for you, I think.

Also, it’s possible to use TypeScript, maybe it would be interesting for you.

1 Like

You could do something similar in Defold using a Lua module:

-- message_poster.lua
local M = {}

M.HEALTH_DECREASE = hash("msgs.HEALTH_DECREASE")

function M.decrease_health(amount)
	assert(amount)
	msg.post("player#controller", M.HEALTH_DECREASE, { by: amount })
end

return M

By putting all of the message posting in a module you’d at least get code completion for the different functions for sending messages. You wrap this into an even nicer module that takes care of both posting messages and register handlers for them.

3 Likes

BTW, in case you are not aware, hot reload does not work with hxdefold. It has something to do with the lua modules.

Yes. The Defold TypeScript wrapper is a thin layer. And the widely use of any type sort of makes it less exciting. I love TypeScript and I never look back since converting to TypeScript years ago.

And I agree there are lots of ways you can structure your Lua scripts to make it better, but still it never feels as good as a static type language. Maintaining Lua is much harder, especially you have lots of people touching the code. My Dota Mod was quite hard to maintain by myself already and quickly became impossible to maintain after I several contributors joined.

Also, as you mentioned, another nice point of static type language is that it works “out of the box”. And you always feel “safe” because of Type/IntelliSense. Also you can refactor with confidence.

Well the point is not having to write the these functions by hand :slight_smile:

Anyway, it is nice to have options for every taste and I totally should actually try creating something with Defold. :slight_smile:

2 Likes

True. I played around with a module that could do something like this:

local decrease_health = message.create("decrease_health", message.number("amount"), message.string("type"))
decrease_health(".", 11, "fire") -- would result in msg.post(".", "decrease_health", { amount = 11, type = "fire" })

Not sure if it’s an improvement though? You’d get automatic input validation and message creation at least.

1 Like

Yeah, some kind of run-time schema definition/validation is what people do all the time in dynamic languages to emulate types :slight_smile: I’m not against it, but you just don’t need any of this when you use a solid statically-typed language.

Sorry, should have been more clear: I asked not what type checking is, but why you relate it in a positive way to slower feedback loop you mentioned right afterwards.

That’s what I try to explain by detailing a typo feedback loop:

Static: Make a typo -> IDE underlines it -> Fix it (the loop is within seconds)
Dynamic: Make a typo -> IDE doesn’t complain -> Run the game -> Trigger the scenario -> Blow up -> Look at the code again -> Fix attempt #1 -> Hot reload -> Nope -> Look at the code -> Fix attempt #2 -> Hot reload -> Nope -> Look at the code -> Fix attempt #3 -> Ah-hah moment -> Hot reload -> It works!

But of course, if you talk about Normal feedback loop, then of course, hot reload is the faster than having to compile and re-run the game. That’s why I like Cocos Creator: they are JS focused with Typescript, so I get the best of both worlds.

2 Likes

OK, I think I made hot-reload working :slight_smile: Since there’s always a single lua module generated by Haxe we can as well just export everything in a single global variable and that seems to work well.

PS Also I updated the APIs to 1.2.148.

3 Likes

I wonder if there’s any way to trigger the hot-reload from outside? It would be cool to do hot-reload automatically after the recompilation.

1 Like

I quickly hacked up a message-handling macro before going to bed :slight_smile:

It processes this:

@:msg function damage(state:UnitData, amount:Int) {
	// handle damage
}

@:msg function addBuff(state:UnitData, buff:Buff, duration:Int) {
	// handle buffs
}

…and automatically generates this (an excerpt from the haxe/lua output):

Unit.prototype.on_message = function(self,state,message_id,message,sender) 
  if (message_id == Unit._hxdefoldmsg_addBuff) then 
    self:addBuff(state, message.buff, message.duration);
  else
    if (message_id == Unit._hxdefoldmsg_damage) then 
      self:damage(state, message.amount);
    end;
  end;
end

-- ... and later also, for performances :-)

Unit._hxdefoldmsg_damage = _G.hash("damage");
Unit._hxdefoldmsg_addBuff = _G.hash("addBuff");

So yeah, this is definitely possible and I think is pretty cool.

4 Likes

Cool! Wouldn’t it be faster to generate on_message as table lookup instead of sequential check?

I don’t know :slight_smile: I can generate whatever best practice is :slight_smile:

@dapetcu21 did lots of magic with his https://github.com/dapetcu21/atom-defold-ide
Defold currently does not offer a public API to trigger hot reload… But there are unsupported methods that Marius used, so perhaps he can share the tribal knowledge with you.