shadowyRoblox developer
All work

Codebase architecture

The structure I set a game up in. Shared code in the middle, services on the server, controllers on the client, one bootstrap each side.

The explorer tree of the codebase, showing Shared, Server and Client folders with their modules

Overview

This is how I lay a game out before I write any of it. Shared code sits in ReplicatedStorage, the server lives in ServerScriptService, the client lives under StarterPlayerScripts, and each side has a bootstrap that starts everything in a set order.

The framework is Knit style. A feature on the server is a service, the same feature on the client is a controller, and both go through an Init pass and then a Start pass. Init is for grabbing references. Start is where things actually run. Nothing has to guess whether the thing it needs exists yet.

Most of it is written as classes. Signal and Trove sit in Shared, and anything that gets created a lot is an object with its own methods and its own cleanup instead of a pile of loose functions passing tables around.

It is not a game, it is the part I set up first. Every system in the other two projects sits inside a structure like this one.

What I built

01

Shared in the middle

Classes, Config, Framework, Network, Util and Types all live in ReplicatedStorage. The server and the client read the same code instead of two copies that slowly stop matching each other.

02

Services and controllers

Server features register as services, client features register as controllers, and the bootstrap on each side collects them and runs the two phases. Adding a feature is adding one module. The startup never gets touched.

03

Net

All the networking goes through one module. Both sides ask for a remote by name and get the same one, created if it is not there yet. No folder of hand made remotes to keep in sync, and renaming one is a single change.

04

Signal and Trove

Signal for events between systems that have no business being a remote. Trove for cleanup: whatever an object creates gets tracked, and destroying it takes one call. Forgetting a single connection is how you get leaks nobody can find later.

05

Config

GameConfig and TeleporterConfig hold the numbers. Tuning is editing a config, not going through scripts looking for the place a value was typed in.

06

Util and Types

A Logger instead of print calls left everywhere, TableUtil for the table work that keeps coming up, and a Types module so the shapes both sides pass around are written once.

Problems I hit

Problem

Before this, everything was scripts reaching into each other and hoping. Whether a session worked came down to what loaded first.

How I solved it

The bootstrap and the two phases. Every module gets Init, then every module gets Start, and nothing goes looking for anything else during Init. The waits and the retry loops came out and never went back in.

Problem

Remotes were made wherever somebody needed one, so the two sides drifted and half of them were named something nobody could remember.

How I solved it

Net. One module, get by name, made on demand. It sounds small and it removed a whole category of bug where one side was listening on a remote the other side never created.

Problem

Things that got destroyed left connections and instances behind.

How I solved it

Trove, and the rule that anything an object creates goes into its trove. Cleanup is one call at the end instead of a list of disconnects you have to remember to update.