DEV Community

Ricardo Sanchez
Ricardo Sanchez

Posted on

I built a private MMORPG stack for a 3D farming game (Godot + Go)

Farm World 3D gameplay


Farm World started as a simple idea: a 3D multiplayer farm where you build, plant, drive machines, and meet people in town. I did not want to rent someone else's netcode forever. I wanted a stack I could own for years.

So I built one.

This is not a "look at my landing page" post. It is the messy middle: protocol design, Blender without being a 3D artist, SQLite under a live world loop, and the bugs that only show up when real players join.

What Farm World is

A multiplayer 3D farming game:

  1. Own a plot, grow crops, raise animals, store harvest.
  2. Buy vehicles, hitch field tools, work the land like a small farm sim.
  3. Walk into town, sell, craft, take quests, hang out with other players.
  4. Play in the browser or download a desktop build.

Client: Godot 4.6 (GL Compatibility, so web export stays realistic).

Server: Go, plain WebSocket JSON, SQLite persistence.

No Unity Netcode. No Photon. No "MMORPG framework" bought off a shelf.

Rough size today: about 29k lines of Go on the server (without tests), hundreds of GLB props and crops in the asset tree, plus a Next.js site for play and downloads.

Why a private framework

I kept hitting the same wall with ready-made multiplayer kits:

  1. Great for shooters or tiny lobbies, awkward for persistent farms.
  2. Your economy, plots, animals, and world events still need custom authority.
  3. Once the kit fights your game design, you pay twice: once in money, once in rewrites.

Farm World needs server authority on almost everything that matters: gold, inventory, crop growth, market prices, vehicle cargo, plot power. The client is a view. The server is the truth.

That decision became the spine of the project. The "framework" is not a product I sell. It is the habit of building:

  1. A small JSON op protocol over WebSocket.
  2. A single Go process that owns game rules and DB writes.
  3. A Godot autoload (game_net.gd) that speaks that protocol and nothing else.
  4. Migrations that grow with the live world instead of a big bang rewrite.

I plan to keep this shape for years. New systems plug into ops, handlers, and tables. That is the private stack.

The protocol is boring on purpose

Every message is basically:

{ "op": "action_sow", "slot_id": 12, "cell_x": 3, "cell_y": 7, "seed_type": "wheat" }
Enter fullscreen mode Exit fullscreen mode

Login, resume session, sow, harvest, place buildings, spawn vehicles, chat, friends… same envelope. On the Go side, it lands in one Message struct with many optional fields. On Godot, it is send_op({ "op": "..." }).

Boring is a feature. When a new system arrives (pets, craft pots, world events, voice signals), you add an op and a handler. You do not invent a second networking universe.

Client-side, it looks like this:

func request_sync() -> void:
    send_op({"op": "sync"})

func request_sow(slot_id: int, cell_x: int, cell_y: int, seed_type: String) -> void:
    send_op({
        "op": "action_sow",
        "slot_id": slot_id,
        "cell_x": cell_x,
        "cell_y": cell_y,
        "seed_type": seed_type,
    })
Enter fullscreen mode Exit fullscreen mode

Hard problem 1: an MMORPG server from zero

"From zero" means: accept sockets, authenticate, rate limit login, keep session tokens, load plot state, tick growth, broadcast presence, and never trust the client with gold.

A few lessons that cost real nights:

Full sync is a trap.

sync that rebuilds the whole player profile feels safe early on. It is also how you melt FPS. We once shipped a client loop that asked for full sync tens of times per second after restoring farm machines. The HUD ping lied. The real cost was JSON parsing on the main thread. Fix: stop the loop, prefer deltas, treat full sync as a rare reconnect tool.

The world loop is a product feature.

Crops do not grow because the client animates. Rain, market multipliers, and harvest yield live on the server clock. That makes events fair across players and keeps cheaters bored.

SQLite can carry an early MMO if you respect it.

One file DB, WAL, careful transactions, indexes on hot paths, and migrations that do not lock the world for minutes. I did not start with a cluster. I started with correctness and measurable queries. When something is slow, it is usually an N+1 load of tiles or a sync that got fat again.

Farm World custom telemetry dashboard tracking server metrics, funnel events, and SQLite performance
Farm World custom telemetry dashboard tracking server metrics, funnel events, and SQLite performance
Custom admin analytics dashboard built to monitor server health (Goroutines, Heap, SQL latency) and player onboarding friction.

Authority boundaries stay sacred.

If the client could invent a harvest, the economy dies. Every meaningful action is validated server side against inventory, plot ownership, and cooldowns.

Hard problem 2: hundreds of GLBs without being a Blender expert

I am not a professional 3D artist. I still needed a town, crops at multiple growth stages, kitchen props, vehicles, hitch tools, and vegetation.

The workflow that actually worked:

  1. Source packs and kitbash where the art direction allows it.
  2. Blender only for the boring jobs: scale, origin, export GLB, fix materials that Godot hates.
  3. Naming discipline so crops and props map cleanly to item IDs.
  4. Accept that "pretty enough and consistent" beats "perfect and never shipping".

The surprise cost was not sculpting. It was pipeline: import settings, LODs later, collision, and keeping the web build from drowning in megabytes. Art is a systems problem once you have hundreds of files.

Hard problem 3: a 16 km² map that has to run in the browser

The world is about 16 square kilometers. That sounds cool in a trailer. On the web, it is a constant fight: Terrain3D regions, props, vehicles, town collision, audio, and a WASM build that cannot pretend it is a fat desktop install.

Making that map playable in the browser has been one of the hardest product constraints of the whole project. GL Compatibility is not a preference; it is survival. Streaming attention, draw distance, and "do not load the entire planet at once" thinking matter more than fancy lighting.

With a small player count so far, the result is already good enough to ship and keep iterating: people drive, farm, and explore without the tab melting. The open question is scale. More concurrent players means more presence traffic, more vehicles in view, more pressure on the same web budget. The map size stays. The stack has to earn it.

Hard problem 4: 100 plots, 784 cells each, and ping spikes

We have 100 farm plots. Each one is a 28×28 grid, so 784 cells per plot. That is not a cute number on a whiteboard. It is tiles, soil, crops, trees, buildings, paint, and power state that can all want to travel over the wire.

The pain showed up when a plot got "real": something like 200 harvest cells plus a dense tree layer. Full property payloads got fat. The client spent frames merging and spawning. The server spent time building profiles and broadcasting to people who did not even need the detail. Ping spikes were not mysterious network weather. They were us shipping too much world to the wrong people.

I spent hours reworking both sides:

  1. Interest/distance. Nearby players and the plot owner get full detail. Far players get a thin stub: skyline bits you can see from the road, without the crop and tree arrays that would wipe or bloat their local cache.
  2. Fetch on approach. When you walk closer, the client asks for the real plot (fetch_plot style flow) instead of assuming every slot arrives complete on welcome.
  3. Deltas over dumps. Growth and harvest prefer tile patches and targeted property_updates. Re-sending 784 cells because one tomato ripened is how you invent lag.
  4. Presence is local too. Movement does not fan out to every connected socket on a 16 km² map. If you are far away, you should not pay for my tractor position every tick.
  5. Client LOD to match. Plot interiors, fences, and foliage already think in distance bands. Network stubs and scene LODs have to agree, or you optimize one layer, and the other still melts the frame.

This was not one clever PR. It was a loop of measure, cut payload, fix a merge bug, watch the spike come back under a busier plot, repeat. The goal was boring: keep ping peaks low even when farms look alive.

Things that moved the needle beyond plots and the big map:

  1. Stop chatty sync loops (the farm machine bug was a teacher).
  2. Send growth patches instead of re-sending the whole plot when possible.
  3. Treat export audio, caching, and pack size as first-class bugs on web.
  4. Measure. A "50 ms ping" HUD can hide a 70 ms JSON parse.

There is also the human side of shipping: version modals when server and client disagree, production bugs like crops that show in inventory but refuse to sell, audio that works in the editor and vanishes on web export. That is the real curriculum.

What I would tell myself on day one

  1. Design the op list before the UI screens.
  2. Assume full state sync will betray you.
  3. Put economy math on the server on day one, even if the formulas are naive.
  4. Learn just enough Blender to unblock export, then stop cosplaying as a studio.
  5. Write down pitfalls. Future you will step on the same rake.

The long game

Farm World is the game players see. Under it sits a private multiplayer habit I expect to reuse: Godot client, Go authority server, SQLite until proven otherwise, JSON ops, ruthless server validation.

I am still adding systems (crafting and town businesses, pets, mobile/export paths, better audio spaces). The stack is meant to survive that.

Try it

If the development side caught your attention, you can play the free demo in the browser at farms-world.com/play, grab a desktop build from the site, or join the Discord and tell me what broke.

Questions welcome in the comments: protocol design, Godot web export, or "why SQLite for an MMO" are all fair game.

Gameplay Screenshots

Farm World gameplay overview

Farm World gameplay overview

Farm World gameplay overview

Farm World gameplay overview

Farm World gameplay overview

Farm World gameplay overview

Farm World gameplay overview

Top comments (0)