Miniatures, Bandwidth, and Abstraction
A late-night blog post detailing a novel approach to abstract characters in Roblox.
ROBLOXTECHNICAL
Marceline Croyle
8/9/20243 min read
Yay, another late night blog post.
In today's talk, I will be jabbering about technical jargon and going over details about my latest pet project: the Intrepid Gray Miniature Model for networking and simulating thousands of independent characters in Roblox projects per second.
So, what do I mean when I say Miniature? What does this refer to?
To first understand the "miniature" model in Luau OOP (LOOP) terms, you need to understand the norm of character controllers: Roblox simulates and controls "characters" based on its proprietary "Humanoid" instance, with a replicated "state" and authority ceded to some client via "network ownership".
When characters exist outside the client-server model and are generated by the game state, the network owner of a character/humanoid instance is at the beck and call of Roblox's mystical physics engine. Additionally, characters in this state using traditional instance replication bear lots of overhead related to instances, humanoids, and various other things. The advantage of course, is that you have a generic and scalable solution that any 10 year old can pick up and code with, save for the odd error.
What happens when you need to push this slightly wonky, pushing 20 year old system to its absolute limit? The answer is, of course, disaster. Disaster strikes when you want thousands of Roblox characters moving around and simulating various simple actions. Humanoids are slow, janky, expensive, and don't scale particularly well when dealt with en masse. They are often times unpredictable, and are notoriously difficult to deal with when it comes to their "HumanoidState" API(s) [Yes, there are different endpoints for different types of states for Roblox Humanoids. It is a confounding and befuddling mess at times.]
Well, for modern gaming, disaster simply isn't tasteful. So, my solution to this issue is of course to create my own humanoid and character replication, and to do it in a way that it can support thousands of independent actors at once, in the same data model.
We first define the goal: Keeping with roughly 100kbps outbound data to the clients by volume (give or take, there is wiggle room), create a scalable and generic solution to some character who will have its "state" be truthfully represented on the server, without instance or replication overhead. In turn, each client will independently resolve a simulation according to their last known truthful representation.
To do this, I have introduced 2 LOOP classes: intMiniature, and intMiniatureVirtual. Client and server, respectively.
intMiniature (Intrepid Gray Miniature) resolves the simulation, based on the following parameters:
Miniature's Position (3 float32s for a total of 12 bytes)
Miniature's Rotation (1 unsigned 8 bit integer totaling 1 byte)
Miniature's Owner (1 unsigned 8 bit integer totaling 1 byte)
Miniature's State (1 unsigned 8 bit integer totaling 1 byte)
Miniature's Type (1 unsigned 16 bit integer totaling 2 bytes)
With this data, we can represent a whole miniature's dynamic state packet in just 17 bytes of data. That's pretty good, and it can (theoretically) rotate around, move, play animations, perform actions, and have a model representing it on the client. All of that behavior is client-driven, with it being simulated and computed based on the server's low-bandwidth high-throughput virtual representation of the miniature.
The miniature approach is designed to be re-usable for a broad variety of applications, because in my personal project, which is a tower defense style game, it will be applied as both the towers and the enemies.
With this solution, I have calculated that it would support nearly 6,000 independent miniatures in less than 100kbps client throughput, assuming each was being updated every second. That is a pretty good solution. Roblox Humanoids would likely be unable to keep up.
The downside of this, however, is that I will also need to rewrite or implement any tech that Roblox gives to its proprietary Humanoids for my own implementation. Thankfully, my project is quite simple, so that shouldn't be a big deal. But, for anyone who would be thinking about an approach like this in other projects, it will definitely be a larger concern.
Dealing in pure Luau, even with native code generation and strict typing compiler optimizations, and using all the latest performance gains like Parallel Luau, is still a tinge slow in comparison to a language like C++. But today, we can build some pretty impressive and blazing fast APIs like this Miniature Model in just Luau.
— Marceline Croyle