Motivation

- todo

Keeping The Server Light

Player State and Cheat Detection

For an open world survival style game, for my purposes, I don't see the need for the server to be particularly heavy. The resource usage of maintaining a persistent, authoritative world state depends on what kind of state, exactly, you're calculating on the server side. If the game allows complex physics-based interactions that the server has authority over, then besides the high CPU cost of running physics, you may need detailed, memory-heavy meshes loaded in for participating objects, in order to do collision detection correctly.

I don't plan to allow this. Interactions such as movement, harvesting, and building can get by with box, capsule, or sphere colliders, together with checks against the terrain heighmap.

The server can do sanity checks with these, preventing some cheats and marking other suspicious ocurrences for manual review. Of course, cheat detection is complex and prone to countless false-positives due to bugs, latency, and general oversights, so the below are only vague outlines of the strategies to combat cheating.

  • Preventing wallhacks: Some can be prevented outright (such as going under the base terrain, using a simple heightmap check, or standing inside a tree's capsule collider), and others can be flagged as suspicious (being slighty, but too far, inside the bounding box of a mesh object in the world)
  • Verify object interactions: A cheap raycast against a collider can verify that the player is near enough and facing a resource they're harvesting, a building they're attacking, etc
  • In limited cases, particularly oddly shaped objects may need to use a very low detail mesh collider.
  • Some interactions don't need collision checks at all, such as harvesting bushes that can be walked through, or picking small objects up off the ground. Checking the distance between player and object (plus facing) is enough

I only plan to have basic cheat detection at first. Rampant cheating is a "big game" problem that you'd be lucky to have! By one estimate, you can expect 1 CCU (Concurrent User) for every 20 DAU, and 1 DAU for 20 MAU, therefore 1 CCU = 400 MAU.

By the time you have to worry about policing, say, 100 users at once, you have at least 40,000 paying customers. I need to keep reminding myself to resist the temptation to overdo cheat detection up front.

I'll block the lowest-effort, script-kiddie cheats up front, such as using Cheat Engine to just change the player's position vector to warp or fly, speedhack, or nullify cooldowns, and worry about actual mods built on decompilation much later.

World State

How about the tens of thousands of entities representing resource nodes (trees, rocks, bushes, etc) and thousands of entities for building structures? Isn't this guaranteed to be a massive memory hog?

No, not at all.

Without even looking under the hood at the CLR, just based on general knowledge of how language implementations work, we would expect that allocations have overhead only measured in dozens of bytes, storing various descriptors, pointers, and padding. Custom classes with large numbers of members, then, may be expected to stretch into the kilobytes of overhead.

If these common entities aren't designed wastefully, then each should fit into a few kilobytes, and multiplied by tens-of-thousands, this is only a few hundred megabytes e.g. 5K * 50,000 entities = 250 MB.

Furthermore, we can allocate all of the resource nodes up front (as they have fixed spawn locations) and keep them around for weeks or months at a time until the server restarts. This can make the memory usage more predictable by reducing heap fragmentation and reducing the chance the GC will expand the heap. We can also use object pooling in situations that you might not ordinarly bother with for a purely client-side game, such as allocating a large pool of buildable structure nodes on server start.

By keeping the resource usage light enough and predictable enough, we could host at least a dozen world instances on a single, moderately priced server. As before, infrastructure cost is a "big game" problem you only hope you'll be fortunate enough to worry about, but it's nice to have a ballpark idea, and build things at least somewhat judiciously up front.

Interest Management

Finally, what about message congestion? It should be obvious to implement some kind of interest management here. Most users don't need to know about most updates, because they aren't near each other at all. Users can receive the new state of resource nodes and structures incrementally as they move across the world toward them.

"It Doesn't Scale"

In this thread about Unity as a headless server, user "beheadedwarrior" declares definitively that Unity "will not work" as a server for "large scale open-world games" and that it can be expected not to scale well just based on "common sense"

beheadedwarrior:Depends on the game, small fps games? Sure it will work. large scale open world games will not.

Joe-Censored:What makes you think a large scale open world game can't work using a Unity headless server? I've been having good results.

beheadedwarrior:Common sense tells you that wont scale well. Maybe once ecs is stable yes but other than that you're better off writing your own server with a thin physics layer on top of it.

Beheadedwarrior doesn't provide any further explanation of why Unity is too inefficient, or which alternatives he's comparing to - it's just "common sense" after all!

Creating The Server

Creating A Server Scene

Unity allows setting server-only defines (Edit -> Project Settings -> Player -> Other Settings panel -> Scripting Define Symbols list) which could be used in C# #ifdefs to separate the client and server logic, but I wouldn't lean heavily on this technique. Instead, just create a separate scene for the server and re-use whatever logic is re-usable, and create separate server scripts when it's not. Pass this scene in the BuildPlayerOptions

For this game, I'll have an Editor Script that copies entities into the server scene, strips out unnecessary components, and attaches relevant ones (such as server-only logic scripts). It will also create a downsampled terrain heightmap for collision sanity-checking.

Triggering Builds Programmatically

The docs for BuildPipeline.BuildPlayer give a code sample that's almost sufficient, but needs the subtarget tweaks discussed below.

REMINDER: Editor scripts like this one must be placed in a folder named "Editor". I always forget this the first time.

Setting Headless Mode

The old way of setting headless mode, BuildOptions.EnableHeadlessMode, is marked as deprecated as of Unity 2021.2 — but will still work.

In the LTS release (Unity 2021.3) the recommended way is to set the subtarget property of BuildPlayerOptions. The valid subtargets are spread across multiple enums, but relevant here is StandaloneBuildSubtarget.Server. Remember to cast to an int

Description

Build a player that is optimized for running as a headless server.

He [sic] application output compiles the standalone player based on the following target platforms:

Mac player: Compiled as a standard console application.

Win Player: Compiled with /System:Console and runs as a standard windows console application.

Linux player: Compiled as a standard console application.

UNITY_SERVER will be defined when building managed assemblies.

To use the deprecated method anyway set buildPlayerOptions.options to BuildOptions.EnableHeadlessMode (or concatenate it with other options using a bitwise-or, as this is a bitfield)

The new way with subtargets requires that the Dedicated Server Build support is installed (via the "Add Modules" feature in Unity Hub), whereas the deprecated method does not.

Checklist

  • Remember to put the script in an Editor folder
  • Build into an empty subdirectory
  • Ensure Dedicated Server Build addons are installed for each platform
  • Make sure an appropriate renderless scene is listed in buildPlayerOptions.scenes