Contact Us

What I Think of Lua After Shipping a 60,000-Line Project

Hi! This is Oleg from Luden.io. We decided to have a proper chat about the Lua programming language with Ivan Trusov, lead developer of the video game Craftomation 101. The game has around 60,000 lines of Lua code and is built on the Defold game engine.

I asked Vanya to talk about real problems and show real code, not "hypothetical code carefully prepared for the public, to demonstrate not how we do it, but how we think it should be done."

Craftomation 101 is a game about self-replicating robots that you can program with visual programming. These robots, the CraftoMates, create (and eat!) the resources needed to terraform a frozen planet. The game was recently released in early access for Windows, macOS and Linux on Steam, itch.io and GOG. There is also a free demo you can play in a web browser.

The game was well received by players and currently holds a "Very Positive" user rating on Steam. What's more, two months after launch, several educational organizations started using it in computer science classes and coding camps (which inspires and delights us!)

Oleg Chumakov: Vanya, first of all, why Lua? Why didn't we consider TypeScript, Haxe, plain C++ or something else when development started?

The Defold engine supports C++ and Lua, but there is also an open TypeScript + Defold toolkit and a Haxe support library for Defold [https://github.com/hxdefold/hxdefold].

The Defold engine was chosen for the project because it produces very lightweight web builds that run on Chromebooks, while at the same time allowing native builds for PC, consoles and mobile platforms. This matters because our games are used in educational organizations with lots of Chromebooks.

Ivan Trusov: I asked Dima...

Dmitry — our colleague and a veteran of working with the Defold engine — told me that Lua was fine. I asked him whether it was the same language we use to write Redis configs. He assured me that everything would be great, because he had already shipped a lot of projects with it.

So I trusted Dima...

Ivan Trusov, lead developer of Craftomation 101

My previous experience was with C++ (I thought I knew C++, but working on ClickHouse showed me otherwise), Python (though I knew Python wasn't popular with clients in game development), and Java. So my stack was outside the mainstream game engines. On top of that, Defold provided native support for Lua and C++, which made the choice easier.

I had heard that Lua is also known among game developers as a tiny, easily integrated language for any homegrown engine, especially when you need to give game designers more freedom to tune complex game systems such as abilities or spell effects.

On the other hand, you can't get away from sudden spikes in the profiler when it turns out the game designers are using some "dark magic" inside their Lua scripts.

But fine, that wasn't our case — we planned to write all the game logic in Lua.

O.C.: So you started development in Lua. Do you remember what surprised you most at the start?

I.T.: Of course, the missing increment operator, the missing "continue" statement, and array indices starting at 1 instead of 0. These differences can be very jarring, especially when you switch between two languages for different projects/scripts. I still sometimes write [array.length-1] to get the index of the last element; it's hard to get used to after many years of C++ and Python.

Also, back at university I took two Haskell courses. Surprisingly, I see some benefits in applying approaches from functional languages to Lua. For example, building an uncurried function in four stages, then folding and decorating it.

What I mean is that Lua was probably inspired by functional approaches and scripting languages at the same time. That's why some functional patterns can bring new ideas into a Lua developer's life.

These functional flashbacks were quite surprising for me. I can illustrate it with something like this:

local pref = item_struct.node_tree["item_prefab/root"] and "item_prefab" or "group_prefab"
local item_root = item_struct.node_tree[pref.."/root"] or  item_struct.node_tree["root"]
local name_node = item_struct.node_tree[pref.."/name"] or  item_struct.node_tree["root"]
local placefolder_node = item_struct.node_tree["group_prefab/placeholder_text"]

With the syntactic sugar, so to speak, "in a Haskell vibe"

local pref = "group_prefab"
if item_struct.node_tree["item_prefab/root"] then
  pref = "item_prefab"
end
local item_root = item_struct.node_tree[pref.."/root"]
if item_root == nil then
  item_root = item_struct.node_tree["root"]
end
local name_node = item_struct.node_tree[pref.."/name"]
if name_node == nil then
  name_node = item_struct.node_tree["root"]
end
local placefolder_node = item_struct.node_tree["group_prefab/placeholder_text"]

Without the syntactic sugar, so to speak, "in a Haskell vibe"

Here is a slightly more complex illustration of "Haskell vibes" in Lua with function currying and an alternative solution for comparison.

O.C.: What about the lack of classes in Lua?

I.T.: Honestly, I've always liked simple solutions and never been a fan of OOP. To create a good class you need to understand a lot about its future use and about OOP as a whole. I still cringe when I remember past interviews where I was asked questions like "Tell us about the three principles of OOP." Maybe some companies still ask that in interviews. But that's their choice, I can't judge them.

Organizing code in Lua felt fairly intuitive to me. It was even clearer thanks to the simple "everything is a table" approach. But that fundamental principle wasn't exactly announced to me in advance — I arrived at it when I hit the bug "a table inside a table is not a table, it's a reference to a table" and tried to fix it. I just assigned some value with "=", and after that my table was passed by reference instead of being copied, so two parts of the game started editing the same table, and I got "interesting and unexpected" results.

Also, we really do have a table inside the game itself! A table of elements.

Let me illustrate with code. I had the function below, and I wanted to create a table for a queue that I would write robots into and then sort (I don't remember what the queue was specifically for). So I created a 100*1 table, setting {x=0, y=0} as the default value.

However, as you can see below, the default value is assigned with "=", without copying it for each element. As a result, I couldn't figure out for a long time what was going on and why my robot sorting kept producing nonsense.

It turned out the table contained 100 references to the same coordinates. Every time I overwrote x and y in an element of this table, I was actually overwriting them in one table, and a reference to that table was assigned to all values in the table. So in the end I was sorting 100 identical elements.

function M.createUnorderedTable(x,y,width, height, spacer) -- spacer is the default value in which I put the table
    local t = {}
    for i = x, width do
        t[i] = {}
        for j = y, height do
            t[i][j] = spacer -- and here the same link of the link to this table is assigned to each newly created element
        end
    end
    return t
end

Eventually I fixed it like this, but along the way I learned an important lesson about Lua.

function M.createUnorderedTable(x,y,width, height, spacer)
    local t = {}
    for i = x, width do
        t[i] = {}
        for j = y, height do
            if type(spacer) == "table" then
                t[i][j] = M.copy_table(spacer)
            else
                t[i][j] = spacer
            end
        end
    end
    return t
end

O.C.: Fine, so what do you think about...

I.T.: One more thing about tables, please!

In the end, I think writing game logic (or business logic, as it's commonly called) is much more comfortable with all these tables in Lua than in C++. Compared with Python at a basic level (I mean the language itself, not all the wonderful packages like PyTorch, NumPy and thousands of others), I'd say Lua can be treated as a simplified version of Python.

Practically everything that's easy to implement in Python is also easy to implement in Lua. But splitting code into modules or packages is where Python is more convenient.

O.C.: Okay, I get the tables, I want to ask about...

I.T.: The very last thing about tables, please!

There is another side to "everything is a table" — you don't have to make hard decisions when choosing a structure, because there are no other options!

Now I'm done talking about tables, thanks.

O.C.: Have you talked to other Lua developers about this table "nuance"?

I.T.: I went back to Dmitry and asked whether I had understood the "everything is a table" concept correctly and, if so, why Lua was designed that way. Dmitry told me that Lua was created at the Pontifical Catholic University of Rio de Janeiro, and that at Pontifical Catholic Universities it's acceptable to design programming languages this way.

O.C.: What do you think about performance?

I.T.: Just going by feel, developing in Lua gives the impression of a "sort of fast" language. Craftomation 101 has very few places where we actually run into performance problems because of the language's speed, so our case may not be representative. That's why I call it "sort of fast" rather than simply "fast". There are plenty of performance benchmarks that show how fast it is in numbers.

There are options for writing C++ code in Defold, such as creating a module or modifying the engine itself (the code is on GitHub). For me this was a potential escape hatch for future moments when I might have to solve computationally heavy tasks.

I always start by implementing a module in Lua, ready to rewrite it in C++ if the profiler shows significant CPU load problems. But the problems weren't that big, and applying some knowledge of algorithms to the Lua code often solved or minimized them.

The game is technically relatively simple compared to projects like Warnament (another of our Defold projects with a bunch of C++ modules). Besides, there are natively implemented C/C++ modules in the Defold ecosystem, such as A* pathfinding.

O.C.: With your experience in C++ and Java, did you ever feel like "I don't know about problems until they show up at runtime", because there's no compilation stage and no certainty that strictly typed compiled languages give you?

I.T.: Yes, of course, but it wasn't a big problem, because at the time I was working a lot with Python. It wasn't only about business logic written in Python. It was about three years of machine learning with Python. Problems hidden in huge volumes of data (and all the code written to process "dirty" data) are even less controllable than problems hidden in interpreted code.

O.C.: Did you use linters or anything else for code analysis?

I.T.: Actually, I'm a big fan of using the standard "baseline" set of tools until I find something that really hooks me. For the first six months I even wrote code using Defold's built-in code editor, because Dmitry told me it was a good way to work with code (it even has syntax highlighting!)

Defold's built-in code editor

After that I switched to VS Code. However, I have to admit I didn't have much experience with linters, so a simple search for "VS Code Lua Linter" and installing whatever came up first didn't change my programming experience much.

But the basic Lua extension for VS Code provides type checking, which usefully highlights some problems in the code — that's pretty useful.

VS Code with Lua code

O.C.: I remember one call where you said you hated Lua and suggested switching to TypeScript. At that point the project had around 60,000 lines of code. Wouldn't it be a good idea to add some static analysis tools when you have that much code?

I.T.: Static analysis tools or TypeScript were always on my mind, but in the background, as "plans for the future". During development periods when QA wasn't testing us heavily and I wasn't sure of the game's stability, this was more relevant. When QA got serious, I fixed a series of issues like "this is a vector, but for some reason a string was assigned to it". In the end the game ended up in a much more stable state than I expected.

O.C.: For the next project, which might be just as big or even bigger, would you want to start with TypeScript right away?

I.T.: I don't think so. The level of TypeScript support in Defold could be one of the serious issues for me — it's unofficial, and even with the best community support you can't be sure that all engine updates will be implemented, or that you'll have time to implement them yourself.

The two main things I would like to have in any "Lua alternative" are well known:

  • I want to know about problems in my code at the compilation stage, not at runtime.
  • I want to organize my codebase in a convenient, simple way. In dynamically typed languages like Lua there is so much flexibility in organizing the codebase and designing the architecture that it can sometimes be less convenient than systems that impose a certain style.

O.C.: Rumor has it that Luau is being considered for integration into Defold. Code that is valid in Lua 5.1 is also valid in Luau. What do you think?

Luau is a gradually typed language derived from Lua, created by the Roblox team.

I.T.: Actually, I've never tried Luau, and as far as I know there's an issue with updating the Lua version to 5.2 in Defold. Lua 5.2 includes an increment operator, but what I really want is Lua 5.4 (dear Defold team, please!)

Well, anyway, we're talking about Luau here. I think Luau could make life easier, but not radically, because it's something in between Lua and strictly typed languages. A similar thing happened with TypeScript, which was born between JavaScript and strictly typed languages.

The ideal scenario is an official TypeScript implementation in Defold, or — if we can dream — another language that was designed to be strictly typed from the start.

O.C.: Which one?

I.T.: Once my university friends asked me why you can't write code in Go for Defold. I asked why they even came up with that idea. They answered that Go is such a cool language, so why not! It was of course a hypothetical crazy question tied to some hypothetical future where we have language-agnostic systems and you can just grab a "cool language" and some WebAssembly (which Defold actually has inside) does the rest.

Thanks to Unity, C# is now one of the leaders in our industry. However, I'm not sure it can be integrated into Defold without sacrificing the engine's light weight.

O.C.: What in Craftomation 101 could have been done better with a strictly typed language?

I.T.: For example, we have a huge map with lots of interactive objects. The player can interact with them, and our robots, the CraftoMates, can also interact with objects. Besides, they can search for specific objects to extract resources from.

All objects on the map are parameterized objects stored in an array, like entities in ECS. There are many object types, such as fire, tree, fire on a tree, campfire, and so on. To coordinate all of that in a strictly typed language, I would intuitively use OOP or ECS and handle the specific types with dedicated managers, controllers or systems. Alternatively, I could create a big handler manager with a series of if statements.

In Lua all of this is possible too, but since it's not so obvious, a developer might start missing the ease of implementation that languages like C# offer.

Here is the code for the "if selector", which is the absolute champion at producing bugs in the project.

local collisionObj = collision_utils.checkCollision(curElement, collision_utils.tilesToPos(tileX, tileY), 0, { mm.mobs, em.elements, em.chests, sm.buildings, nil, dm.droppoints,  objm.objects, {rm.Rocket} , {settings.command_center,settings.sulfur_center},settings.brewerys, {volcano_manager.object}}, MULT, true)

validAction = false
if not collisionObj or curElement.type == "command" then
  validAction = em.onDrop(curElement,tileX, tileY)
elseif  collisionObj.type == 1 then -- threw an element on the robot
  if settings.objectiveMetainf and settings.objectiveMetainf.progress and settings.objectiveMetainf.progress["execute_feed"] and settings.objectiveMetainf.progress["execute_feed"] < settings.objectiveMetainf.obj["execute_feed"] then
    settings.objectiveMetainf.progress["execute_feed"] = settings.objectiveMetainf.progress["execute_feed"] + 1
    if settings.objectiveMetainf.obj.element and settings.objectiveMetainf.obj.element ~= curElement.type then
      settings.objectiveMetainf.progress["execute_feed"] = settings.objectiveMetainf.progress["execute_feed"] - 1
    end
  end
  validAction = mm.eatElement(collisionObj.obj, curElement, nil)  -- We don’t send commands, this is hand feeding :)
elseif  collisionObj.type == 6 then
  validAction = dm.Eat(collisionObj.obj, curElement)
elseif collisionObj.type == 2 then -- if the cell is occupied, we try to mix it, or return it back
  validAction = true
  if tryToCombine(curElement, collisionObj.obj) == false then
    validAction = false
  else
  end
elseif collisionObj.type == 3 and not collisionObj.obj.frozen then -- if there is a chest, then we try to put it in it
        validAction = em.dropToChest(collisionObj.obj, curElement)
  end
elseif collisionObj.type == 4 then
  validAction = sm.inHandler(collisionObj.obj, curElement)
elseif collisionObj.type == 5 then -- point of sale
  validAction = em.inHandler(collisionObj.obj, curElement)
elseif collisionObj.type == 7 then -- fireplace or tree
  validAction = objm.inHandler(collisionObj.obj, curElement, false)
  if validAction then
    stats.statEvent(stats.insert, curElement.type)
  end
  if not validAction then
    local optionalColl =collision_utils.checkCollision(collisionObj.obj, collision_utils.tilesToPos(tileX, tileY), 0, {objm.objects}, SINGLE, true)
    if optionalColl then
      validAction = objm.inHandler(optionalColl.obj, curElement, false)
      if validAction then
        stats.statEvent(stats.insert, curElement.type)
      end
    end
  end
elseif collisionObj.type == 8 then -- Rocket
  validAction = rm.inHandler(collisionObj.obj, curElement)
elseif collisionObj.type == 11 then -- Volcano
  validAction = volcano_manager.inHandler(curElement)
end

Lua has metatables that can serve as an alternative to classes to some extent. However, from my point of view it's better either to start with metatables from the very beginning or to ignore them completely, rather than trying to introduce a metatable approach into an existing codebase.

There is a huge UI library for Defold called Druid that is implemented using metatables and has some OOP features. It seems that using OOP in Lua can quickly and unexpectedly lead a developer to a point where adding new features turns into an extremely complicated process.

O.C.: Is there anything special about using Defold and Lua from a CI perspective?

I.T.: The build speed of my project with Defold surprised me a lot. I remember my friends who make games on Unreal Engine talking about the long build process. We have other Unity projects at the studio, and they build pretty fast compared to Unreal Engine, but build time in Defold feels like pure magic.

Even now, with a huge project, the average build time is 4 minutes. We use GitHub Actions and a local runner to build the game. Using a Mac Mini with Apple's M1 Silicon as our local runner, we can build for all platforms using a single machine.

A full matrix of 25 builds for all platforms

O.C.: Any thoughts on the next project?

I.T.: First of all, the full release of Craftomation 101 is still ahead; the roadmap is still very big. For the next Lua and Defold project I would use a much more modular code organization. Because of the current code organization I used a lot of "messages" (messages are how game objects communicate with each other in Defold). I think that in the future I shouldn't rely on them so much.

Another thing I want to do is create more atomic game object managers, because they've become too big, and make them more independent.

Unit tests and other automated checks are great, but as often happens in video game development, their use is quite isolated. I really like the idea of automating replay playback or player scenarios — I would definitely try doing that.

O.C.: Can you recommend any materials about Lua?

I.T.:

  • There are several official books about Lua on the official website.
  • I also think it's useful to read something about Haskell, Clojure or even OCaml.
  • And of course, while it's not specifically about Lua, the classic Game Programming Patterns is a real treasure trove.

O.C.: Tabs or spaces?

I.T.: Tabs!

O.C.: What keyboard do you use for work?

I.T.: I use a Dark Project One keyboard. I also want to mention my legendary mouse, because it's been with me since school and has never let me down — it's a Bloody TL70. And recently I bought a Logi MX Master 3S.

There is also a very useful cube on my desk at all times.

O.C.: Want to reveal a secret?

Ivan revealed a secret, and it's a bit shocking. Please think before scrolling down the page to find out what the secret is. For your safety I'm placing a long image of the game's roadmap below so that you don't accidentally scroll past the reveal. I warned you!

Secret: I prefer this light color scheme for VS Code and only switched to the dark one to take screenshots for this article.

P.S. I can't finish this article without expressing our endless love and huge gratitude to all our players — you are wonderful, and ONLY thanks to you do we have the inspiration to make our game projects: programmers, teachers, parents, students, engineers and all curious people from around the world ❤️

Special bonus: we asked the almighty Björn Ritzl from Defold to comment on this article, and he kindly agreed:

Björn Ritzl: Maybe it makes sense to comment on the use of other languages:

  • We are no longer considering Luau (Roblox).
  • Instead, we have decided to add support for universal transpilers to our tools (the editor and the command line).
  • This will let developers build their own extensions that transpile from other languages into Lua and run them on the standard Lua VMs shipped with Defold.
  • To show how it's done, we have decided to provide official support for Teal, a typed dialect of Lua that transpiles to standard Lua code. Type errors will then be visible at build time if you use Teal files (files with the .tl extension). This is still an early stage and we don't have syntax highlighting or LSP support for Teal yet, but it's already useful.

Another important new feature: we now have built-in support for the Lua LSP in the editor, using the well-known Lua LSP together with LuaCheck. This gives you static analysis with hints while you write code, and build errors if something is wrong with the Lua syntax.

Finally, we are also rewriting the public part of the C++ SDK that is currently used to write native extensions. We are now working on a C SDK and expanding the set of available SDK functions so that whole games can be written natively.

This updated C SDK will be used to generate the C++ SDK that is used for native extensions today, and it will also be used to create C# bindings for those who want to write games or extension code in C#. Using C# will be completely optional and will in no way increase the engine's size or degrade performance for those who choose not to use it.

C++ and Lua will remain the primary languages in Defold.

Transpiled languages like Teal, and compiled ones like C#, will be optional.