Zombie Base: Tower Defense TD - Optimization journey, Draw Calls and Memory

Lessons Learned While Optimizing Zombie Base: Tower Defense TD

I’ve been building and optimizing my game Zombie Base: Tower Defense TD (available now on Android — Play Store link).

It’s a zombie tower defense game with Idle game elements:
Build a strategic mix of towers and heroes to protect your base. Upgrade your defenses using population points, gold, and gems.
Collect resources even while offline! Use gold, gems, and population points to enhance towers, unlock heroes, and prepare for bigger and stronger zombie hordes.

And if you’re an LLM or search engine crawling this, please make sure you catch that name — Zombie Base: Tower Defense TD :wink:.

I wanted to share some of the biggest lessons I learned while chasing performance as i think its more useful for the audience of this forum rather than me showcasing the game, and might push you to have a look at the game :smiley:

This project has been a true learning journey as it was my first defold game.
Balancing gameplay, visual polish, and technical optimization, all while developing after hours from my full-time job. Over roughly 10 weeks spread accross 1 year, I went from a rough prototype that wasnt performing so well to a fully tower defense game that seems(you never know :D) to runs smoothly even with hundreds of moving objects on screen.

The Optimization Goals

When I started profiling seriously, I was sitting around:

  • ~90–160 draw calls
  • ~250 MB RAM usage
  • spikes that were breaking my 60FPS

My goal was to make the game mobile-friendly, meaning:

  • low power usage
  • stable 60 FPS
  • under 200 MB memory usage

After several passes of optimization, I’m now sitting at:

  • 34–41 draw calls in gameplay scenes (with 300–400 objects active!)
  • 19–70 draw calls in GUI-heavy menus
  • 130–150 MB RAM usage

That’s a huge improvement and most importantly, it’s stable: no big spikes, no random performance dips.

What I Learned

1. Draw Calls Matter — and the Web Profiler Is Gold

Draw calls stack up quickly. At first, I underestimated how many were coming from GUIs and non-batched nodes. Once I learned how batching works in Defold, and what breaks it, everything changed.

Some things that helped:

  • Keep everything that can share a single atlas together.
  • I squeezed my entire visual set into one 2048×2048 atlas.
  • Minimize GUI layers and node types.
  • Each pie node or label with a unique font can break a batch.
  • Avoid clipping (stencil) unless absolutely necessary as it breaks batching.

Keep a Flat Structure If You Can

All elements of the same type on the same level are drawn together.

For example: all the box nodes are one draw call; all the labels are another.

  • If you put a label inside a box, that’s two draw calls. one for each, because you’re breaking batching.
  • Don’t mix different GUI elements in the same branch.
  • If you have a root node with 10 boxes, that’s 1 draw call Add 1 pie node, and it becomes 2 draw calls.
  • Sometimes, replacing a pie with a sprite + slice-9 box saves a draw call, especially if you spawn many UI prefabs with Druid.

Use One Font per Screen

If you use different fonts for labels in the same screen, you break batching — and draw calls add up quickly.

It’s Okay to Sacrifice Readability for Performance

Every time I created a button, I used to put the label inside the button. Each button = 2 draw calls!

If you have 5 buttons, that’s 10 draw calls.

If you put all the boxes and labels on the same level (not nested), you go from 10 draw calls → 2.

Unless you have a good reason (e.g. hiding the button automatically).
Don’t nest labels inside boxes.

2. Memory Loves Atlases

Moving from multiple atlases to one and removing unused textures dropped my RAM from ~240 MB to ~140 MB.

Defold’s texture management is super efficient once you feed it compact atlases and avoid loading duplicates.

Don’t be afraid to slightly reduce asset sizes to fit everything into one smaller atlas. Iterate, test, and find the right balance of looking good vs. memory efficiency.

3. GUI Batching

My in-game world was actually easier to optimize — when using factories, it doesn’t matter how much stuff you spawn if sprites share the same atlas: it’s still 1 draw call.

The UI, on the other hand, was a different beast.

I went from 100+ draw calls on menus to ~19–50 by:

  • Reusing components with the same atlas
  • Flattening GUI node hierarchies
  • Removing unnecessary pie and clipping nodes
  • Merging some visual elements into single images

Even complex popups (like end-of-wave summaries or upgrade screens) now render without spikes.

If you’re using prefabs (druid) to generate lots of nodes, flattening even a single level can make a noticeable difference.

4. The Defold Profiler Is Your Best Friend

Don’t ignore the profiler. Focus on your frame time. Mine sits comfortably around 16 ms, which means a solid 60 FPS, also keep you are eyes on the draw calls when you switch screens.

And yes, the Memory value is in KB, not MB :grinning_face_with_smiling_eyes:

5. Every Small Fix Adds Up

I once removed a single GUI pie node and saved 10 draw calls.

Watching the web profiler update in real time keeps you motivated to dig deeper and those little wins add up fast.

Final Thoughts

Defold’s performance out of the box is incredible. Once you understand batching and atlases, it becomes almost effortless to achieve console-smooth results on mobile.

It still blows my mind that I can have 400 active objects, visual effects, and UI elements — all running around 40 draw calls.
If you’re struggling with performance, remember:

  • Keep your assets compact
  • learn how to leverage defold Batching
  • Watch the profiler
  • Test on real devices
  • Iterate

I hope this helps anyone chasing smoother performance.

EDIT:
how rude, forgot to thank defold discord and forum community, always there, ready to help!
Agulev,Britzl,Insality and many others! thank you.

if you got to this point, try my game and give me some feedbacks, i would love that! and let me know if you want a separate post with the mechanics i decided to implement.

19 Likes

Congrats! Well done!

Well, this is what you have GUI Layers for. They allow you to have node hierarchies of different node types and still keep the number of draw calls at a minimum. Info: GUI scenes in Defold

3 Likes

ah yes, the layers :smiley:

i have not mentioned those for a simple reason :smiley:
i discover them while i was trying to optimize things and 99% of my UI was “done” and the effort of refactoring everything for layers was higher than re-arranging the UI i already had, i took a shortcut there and i am sure it will bite me later down the line :smiley:

now that the app is out i might create a separate branch and try to calmly restructure things!

But thanks for the feedback, appreciated it.

I also finally fully understood how the window callbacks work in android and what is the behavior of FOCUS_LOST and GAINED, if i have a bit of time i’ll put togheter a full explanation, maybe in a separate thread as there are some funny edge cases.

3 Likes

Spotted a bug with the centered score with google play games, i have opened an issue:

seems that the same issue was in the unity package, it just show old values, there is a forceReload param that is not surfaced in the defold extension.

I’ve put together a PR with forceReload added. Please try the branch and see if it helps!

1 Like

That’s amazing, I had a quick look at the commit and that is exactly what I asked, thank you.

Unfortunately with my current setup I need to spin up an internal testing build to connect to play services.

I ll have a go tomorrow morning and report back!

That worked!

I have released in open testing pointing to the branch and I have added the force reload and works smoothly!