What a Unity to Defold port involves
A port keeps the game the player knows: the rules, the levels, the balancing, the art and the sound. Everything underneath changes. C# becomes Lua, prefabs and scenes become Defold game objects and collections, and every third-party SDK is replaced with a Defold extension or a short piece of custom code.
Teams usually start a port because the Unity web build loads too slowly for portals like Poki or CrazyGames, or because the game stutters on low-end Android phones. Some want one small codebase for web and mobile. The Defold engine adds only a few megabytes to a build. Project files are plain text that diffs well in Git, and there is no Library folder to rebuild after a fresh checkout.
Treat the port as a rewrite with an unusually detailed specification. The Unity project is that specification. You can open it at any time to check how a jump feels or what a power-up does, which makes a port far more predictable than a new game.
How Unity concepts map to Defold
Defold has an official manual for Unity users. Below are the differences that cost the most time in real projects.
GameObjects, MonoBehaviours and components
In Unity a GameObject holds a Transform and any number of MonoBehaviours. A Defold game object has position, rotation and scale built in, and it holds components: sprites, labels, sounds, collision objects, factories and scripts. A script component is a .script file with lifecycle functions.
Awake,StartandOnEnableall map toinit(self).Updatemaps toupdate(self, dt),FixedUpdatetofixed_update(self, dt)andLateUpdatetolate_update(self, dt)in recent Defold versions.OnDisableandOnDestroymap tofinal(self).- Input arrives in
on_inputonce the script has sentacquire_input_focusto itself. Messages from other scripts arrive inon_message.
Serialized fields have a direct equivalent. A line like go.property("speed", 200) at the top of a script shows the value in the editor, and every instance can override it.
The habit to drop early is one script per object. A Unity scene with three hundred MonoBehaviours should not turn into three hundred Defold scripts that post messages to each other. Defold projects stay easier to follow with fewer scripts: one script that drives all enemies, with the logic in plain Lua modules, often replaces dozens of small components. Experienced Defold developers on the forum regularly give this advice to people coming from Unity.
Defold also does not guarantee the order in which components run their init or update. Code that depends on Script Execution Order needs another approach, usually a manager script that calls modules in a fixed order.
Prefabs: factories and collection factories
A prefab with one object maps to a game object file spawned by a factory component. Instantiate becomes factory.create("#enemy_factory", position), which returns the id of the new object. A prefab with children maps to a collection spawned by a collection factory, which returns a table with the ids of the whole hierarchy.
Hierarchies work differently. Unity lets you nest objects inside a prefab as deep as you like. A Defold game object file cannot contain other game objects, so parent and child links are set in a collection or at runtime with go.set_parent. Prefab variants have no direct counterpart. We usually replace them with one prototype plus script properties passed to factory.create.
With Load Dynamically ticked, a factory loads its resources when you first need them, which keeps rarely used content out of memory.
Scenes: collections and collection proxies
The bootstrap collection set in game.project is your first scene. Other scenes are collections loaded through collection proxies. You send async_load to the proxy, wait for proxy_loaded and then send enable. Each proxy loads a separate game world with its own physics, so data that moves between levels goes through a Lua module or a message.
Proxies give you one thing Unity scenes lack. With set_time_step you can slow down or freeze a whole world, so pausing the level while the pause menu keeps animating takes a single message.
Objects marked DontDestroyOnLoad usually end up in a main collection that stays loaded and swaps levels through proxies. Additive scene loading maps to several proxies enabled at the same time.
From C# to Lua
Rewriting C# in Lua is the bulk of the work, and it goes faster than most teams expect. The part of Lua that gameplay code needs can be learned in an afternoon. Most bugs in a fresh port come from a short list of differences:
- Tables start at index 1, and
#liststops counting at the first nil. - Lua 5.1 has no integer division.
7 / 2gives 3.5, so code that relied on C# int math needsmath.floor. - Lua has no classes. You build them from tables and metatables or a small class helper. Interfaces and generics disappear, and gameplay code rarely misses them.
- Values from
vmath.vector3are passed by reference. If two variables point to the same vector, changingv.xchanges both. Vector3 in C# is a struct, so code ported line by line can break here without any error message. - A typo in a variable name creates a global or reads nil, and no compiler stops you. Luacheck or the Lua language server catch most of these. If the team wants static types back, Teal compiles typed code to Lua.
On HTML5 the engine runs standard Lua 5.1 instead of LuaJIT, so profile heavy loops in the browser build too.
Coroutines and async code
Unity runs coroutines for you through StartCoroutine and yield return. Lua has coroutines in the language, but Defold does not resume them on its own. Most WaitForSeconds calls become timer.delay(seconds, false, callback). Tweens with a completion callback become go.animate or gui.animate.
For longer sequences such as a tutorial, a cutscene or a chain of HTTP requests, a coroutine wrapper keeps the code readable. You yield inside the coroutine and resume it from a timer or a callback. Write one small helper for this in the first week and use it everywhere, so the codebase does not end up with three styles of async code.
Events and message passing
C# events, UnityEvents and direct calls to methods on other components all turn into messages. msg.post("/player#script", "take_damage", { amount = 10 }) sends a Lua table to another script, which handles it in on_message. Addresses follow the pattern socket:/path#component. A message is delivered later in the frame, so the reply never arrives on the next line of your code.
There is no GetComponent. To read or change a value on another object you use go.get and go.set on script properties, and shared state can sit in a Lua module that several scripts require. For C# static events, a small event bus module with subscribe and publish functions is often the cleanest replacement, because listeners do not need to know each other's addresses.
uGUI and GUI scenes
Defold UI lives in GUI components, separate from the game world. A .gui file holds nodes such as boxes, text, pie nodes, templates and particle effects, and a gui_script controls them through the gui API. GUI is drawn in screen space and has its own layers and draw order.
Canvas Scaler settings map to the adjust mode of each node and to layouts per display profile, where you can arrange a screen differently for portrait and landscape. Layout groups and content size fitters have no equivalent, so lists and grids are positioned in code. Many teams use Druid, a community library with buttons, scroll areas, grids and input fields.
Rebuild each screen from Unity screenshots and the original sprites. Recreating uGUI hierarchies node by node takes longer than laying the screen out again in the GUI editor.
Animator, flipbooks, Spine and Rive
Frame animations go into an atlas or tile source as flipbook animations and play with sprite.play_flipbook. Property animations, the kind people make with DOTween or simple Animator clips, map to go.animate with easing and playback modes. Skeletal 2D animation has official Spine and Rive extensions, and skinned 3D models come in as glTF.
Defold has no Animator state machine. A short Lua state machine that picks the right flipbook or Spine animation does the same job, and in most 2D games it is shorter than the original Animator graph.
Physics
Defold uses Box2D for 2D and Bullet for 3D. Rigidbody and Collider merge into a collision object of type dynamic, kinematic, static or trigger, with collision groups and masks set by name. Collisions arrive as messages such as contact_point_response and trigger_response.
Check two things early. Defold works in pixels, so set the physics scale in game.project to keep Box2D bodies at sensible sizes in meters. Kinematic objects are not pushed out of walls for you, so a character controller written around CharacterController or Rigidbody2D.MovePosition needs its own separation code based on contact points.
Shaders and materials
A Defold material combines a GLSL vertex program and fragment program with constants, samplers and tags. In the render script you choose which tags are drawn in which pass, with which camera and blend mode.
Shader Graph and Amplify graphs are rewritten by hand in GLSL. A tint, hit flash, dissolve or UV scroll takes minutes. Post processing like bloom or color grading is built in the render script with render targets, and each effect costs frame time on weak GPUs, so keep the ones that matter to the look of the game. Many materials with different tags also break batching, which shows up as a higher draw call count in the profiler.
Sorting layers and order in layer become the Z position of sprites. In the default render script, sprites with a higher Z are drawn on top, within a range from -1 to 1.
ScriptableObjects, Lua modules and JSON
Game data from ScriptableObjects, such as item stats, level lists or enemy waves, has two good homes in Defold. A Lua module that returns a table suits data the programmers own. JSON files added as custom resources in game.project and read with sys.load_resource and json.decode suit data that designers edit in spreadsheets.
Export the data from Unity with a small editor script instead of retyping it. It is faster and it avoids copy mistakes in balancing values. Levels built from prefabs can be exported the same way, as a JSON list of objects with positions, and rebuilt at runtime with factories. Unity tilemaps can go to Defold tile maps.
Native plugins and native extensions
Unity native plugins (.aar, .framework, .jslib) map to Defold native extensions. An extension can contain C, C++, Objective-C, Java or JavaScript code plus prebuilt libraries, and the Defold build server compiles it together with the engine. For most projects there is no local Android or iOS toolchain to set up.
Before writing a new extension, search the Defold asset portal. Ads, purchases, analytics, push notifications, safe area handling, store reviews and most web portal SDKs are already covered.
Moving the assets
Assets carry over best, and the port is a good moment to clean them up.
- Atlases. Every sprite, GUI image and particle texture in Defold comes from an atlas or a tile source. Group images by the screen or level where they appear, so the first level does not pull in art for the whole game. Re-export images at the size the game shows them, since Unity projects often keep 2048 px sources and scale them down at runtime.
- Texture compression. Texture profiles set compression per path pattern. Basis Universal is transcoded at runtime to a format the GPU reads, such as ASTC, ETC2 or BC, so one build covers many phones. A profile can also cap the maximum texture size. Compression slows builds down, so many teams use it for release builds and leave it off while iterating.
- Audio. Defold plays WAV and Ogg Vorbis. Convert music to Ogg, make short effects mono and use sound groups for separate music and effects volumes. Sound is not positional out of the box, so 3D audio from Unity needs simple distance math in Lua or the FMOD extension.
- Fonts. TextMeshPro font assets do not carry over. Create Defold .font files from the original TTF or OTF files. Distance field fonts work for text that scales, and bitmap fonts suit small pixel text. Recent Defold versions can also generate glyphs at runtime, which helps with languages that have large character sets.
- Spine and 3D models. Spine exports work with the Spine extension when the runtime versions match. Defold reads glTF and Collada, so FBX files need a conversion pass, usually in Blender.
Replacing SDKs: ads, analytics, purchases and web portals
List every SDK in the Unity project with its version and the functions the game actually calls. Then find the Defold side:
- Ads. There are extensions for AdMob, AppLovin MAX and Unity LevelPlay. Rewarded and interstitial placements map closely. Banners often need layout changes.
- In-app purchases. extension-iap covers Google Play and the App Store. Server-side receipt validation can stay as it was.
- Analytics and crashes. Firebase Analytics and Crashlytics have Defold extensions, and GameAnalytics ships its own Defold SDK. Keep the event names from the Unity version so dashboards and funnels continue without a gap.
- Web portals. The Poki SDK extension, the CrazyGames extension, and Playgama Bridge or GamePush when one integration has to cover many portals. Mobile ad SDKs do not run in the browser, so the web build calls the portal's own ad functions.
Some SDKs have no Defold extension, for example a small ad network or a studio's own analytics. Plan a native extension for those or, on the web, a short JavaScript bridge. Each one adds time, which is why the SDK list is the first thing we read in an audit.
Keeping player progress
If the Defold version replaces a live Unity game, existing players expect their progress to survive the update. On mobile the new build ships with the same package name or bundle id, and a small native extension can read the old PlayerPrefs on first launch (SharedPreferences on Android, NSUserDefaults on iOS) and write them into the Defold save file.
On the web, Unity keeps PlayerPrefs in IndexedDB, and a JavaScript bridge can read them only when the Defold build is served from the same origin. Cloud saves tied to a player account are the easiest case, as long as the Defold version reads and writes the same data format. Decide on migration during the audit, because it shapes the save system from the first day.
Audit checklist before the first line of Lua
- Unity version, render pipeline (Built-in, URP or HDRP) and target platforms.
- Size of the current builds and loading time on a mid-range Android phone and in Safari on an iPhone.
- Every Asset Store package and plugin, with the features the game uses from each.
- Every SDK and its version.
- Custom shaders and post processing.
- Scenes, prefabs and scripts, and which scripts hold most of the logic.
- Where game data lives: ScriptableObjects, JSON, spreadsheets or values hardcoded in C#.
- Localization: languages, fonts and how texts get into the game.
- The save system and whether progress has to migrate.
- Who signs off on game feel, and how the two versions will be compared.
A step by step porting plan
- Audit and decisions. Go through the checklist. Write down what stays as it is, what gets simplified and what gets dropped.
- Project skeleton. Set up game.project, the bootstrap collection, the render script, input bindings, a save module and level loading through proxies. Settle folder structure and naming now, while it costs nothing.
- Data export. Write Unity editor scripts that export levels, items and balancing to JSON or Lua.
- Vertical slice. Port the core loop and one full level with final art and audio. Measure build size, loading time and frame rate on the weakest target phone, and play it side by side with the Unity version.
- Content and UI. Port the remaining levels, menus, the shop and meta features in milestones. Each milestone ends with a playable build.
- SDKs. Integrate ads, purchases, analytics and portal SDKs, and test each one on real devices.
- Optimization. Tune texture profiles and atlas layout, compress audio, move content that can wait into Live Update archives and switch off unused engine features in the app manifest.
- QA and side by side review. Compare with the original level by level and fix differences in feel: jump arcs, input timing, camera easing.
- Release and migration. Ship, watch crash reports and analytics, and keep the Unity project archived for reference.
Protect step 4. If the slice reaches your size and frame rate goals on a cheap phone, the rest of the port is mostly volume. If it misses them, you find out while changing direction is still cheap.
Testing on low-end phones and in the browser
Test on the hardware your players own from the vertical slice onward. A desktop with a good GPU hides almost every problem.
- An Android phone with 2 or 3 GB of RAM and an older GPU, plus an iPhone a few generations old.
- Chrome on Android and Safari on iOS for web builds. Safari has the strictest memory limits and reloads the tab when a game uses too much.
- Network throttling in the browser developer tools, to see the first load on mobile data.
- The Defold profiler in a debug build, for frame time, draw calls and memory.
- Sessions of 30 minutes and longer. Leaks from objects that are spawned and never deleted show up late.
- Audio that starts after the first tap, a pause when the player switches tabs, and a clean return after the phone locks.
More browser specific advice is in our HTML5 game optimization guide.
Common pitfalls
- A script on every object. Hundreds of scripts posting messages to each other are hard to debug and slower than a few manager scripts.
- Line by line translation. It drags in patterns Lua does not need, such as getters, interfaces and deep inheritance. Port the behaviour, check it against the Unity build and keep the Lua plain.
- Assumed execution order. Code that relied on Script Execution Order or on Awake running before Start fails in ways that look random.
- Physics that feels off. Box2D settings, physics scale and the fixed timestep all change how jumps and collisions feel. Tune them during the vertical slice, while there is still time.
- One huge atlas. All the art in a single atlas makes the first download and memory use bigger than they need to be.
- Forgetting the browser. Mobile ad SDKs, file access and threads behave differently in HTML5 builds or are missing. Check each against the web build early.
- Saves left for the end. Migrating player progress after release is much harder than designing for it at the start.
Port, rewrite or stay on Unity
After the audit there are three sensible outcomes.
- Port when the game is 2D or light 3D, the goal is the web or low-end phones, and the C# code is readable enough to serve as the specification.
- Rewrite when the design is changing anyway, or when the Unity code is so tangled that reading it costs more than building from the design document. The Unity build still serves as a reference for feel and content.
- Stay on Unity when the game depends on URP or HDRP lighting, terrain, heavy Shader Graph work or a large stack of Asset Store tools. The same goes for a web build that already meets portal limits and runs well. Optimizing the existing build is a better use of time in those cases.
Defold has no built-in terrain, light components, line or trail renderers or cloth. Each can be built, and the community has solutions for some of them. A game that relies on several of these features may still be better off in Unity.
How we approach Unity ports
Unity ports are a regular part of our work at Codeward, mostly for the web. Some of them were mobile titles from well known mobile game brands, built in Unity and moved to Defold so they could run in the browser.
Several people on our team worked in Unity for years and know C# in depth. Before we write any Lua, we read the original code and find where the feel of the game comes from. Often it is a few lines in a controller script or an animation curve, and those lines have to survive the port exactly.
If you have a Unity game and wonder whether a Defold port makes sense, send us a build and a short description through the contact form or to biuro@paweljarosz.pl. We will tell you what we would port, what we would change and where we see risk. For the product side of moving a mobile game to the browser, read our guide to porting a mobile game to the web, and for other engines see web porting from Unity, Godot and Construct.