Unreal Engine 5.8
The Unreal plugin
WorldPlane's game side is two Unreal Engine 5.8 plugins: WorldPlane, which every game needs, and WorldPlaneGAS, for games that use the Gameplay Ability System. The plugin knows nothing about any particular game: your game takes part through settings, a few hooks and components, and keeps its own rules in its own code.
What you need#
- Unreal Engine 5.8. A dedicated server target needs a C++ project and an engine built from source, as for any Unreal dedicated server.
- The Linux cross-compile toolchain for your engine version, to package the Linux server WorldPlane runs (
LINUX_MULTIARCH_ROOTset). - An active WorldPlane subscription: subscribers download the plugins from the panel.
- The
worldplaneCLI, to check the project and push builds (see CLI). Everything it does can also be done in the panel or the API.
Installing the plugins#
The download holds two plugin folders. Copy them into your project's Plugins folder, next to the .uproject:
MyGame/
MyGame.uproject
Plugins/
WorldPlane/ WorldPlane.uplugin, Source/, Binaries/
WorldPlaneGAS/ WorldPlaneGAS.uplugin, Source/ (only with the Gameplay Ability System)
Source/
Config/
Then enable them in the .uproject, or in the editor's Edit → Plugins (both are under Networking, marked experimental):
"Plugins": [
{ "Name": "WorldPlane", "Enabled": true },
{ "Name": "WorldPlaneGAS", "Enabled": true }
]
- WorldPlane has three runtime modules:
WorldPlaneCore(settings, entities, coordinator, telemetry),WorldPlaneIris(handoff, ghosts, owner calls) andWorldPlaneProxy(the gateway). It enables the engine plugins it needs:Iris,MultiServerReplicationandOnlineSubsystemUtils. - WorldPlaneGAS is off by default. It needs
WorldPlaneandGameplayAbilities, which it enables. - The server library. WorldPlane loads a native library,
wp_runtime, fromPlugins/WorldPlane/Binaries/ThirdParty/WPRuntime:Win64/wp_runtime.dllfor editors and Windows servers,Linux/x86_64-unknown-linux-gnu/libwp_runtime.sofor Linux servers. It is staged into packaged servers by itself. Client targets build without it. If the file is missing, a server or editor build stops withWorldPlane: ... is missing.
Regenerate project files and build the editor target once, so the plugin's modules compile with your project.
Project setup#
Four engine settings and one module line make a project ready. worldplane check --fix makes these changes for you (see checking a project).
Config/DefaultEngine.ini#
[ConsoleVariables]
net.Iris.UseIrisReplication=1
net.EnableSupportForNoPawnConnection=1
net.SubObjects.DefaultUseSubObjectReplicationList=1
[/Script/IrisCore.ObjectReplicationBridgeConfig]
NetRefHandleGeneratorClass=/Script/WorldPlaneIris.WPNetRefHandleGenerator
[/Script/Engine.Engine]
LocalPlayerClassName=/Script/WorldPlaneCore.WPLocalPlayer
| Setting | Why |
|---|---|
net.Iris.UseIrisReplication=1 | WorldPlane replicates through Iris. Under the legacy replication, each server's objects clash in the gateway. |
NetRefHandleGeneratorClass | An entity must have the same network handle on every server, or clients see it respawn at each handoff. WorldPlane's generator makes handles deterministic; nothing to write. |
net.EnableSupportForNoPawnConnection=1 | The gateway joins every server with every player, and only one server has the player's pawn. |
net.SubObjects.DefaultUseSubObjectReplicationList=1 | Iris replicates subobjects through registered lists. |
LocalPlayerClassName | On the gateway, each login it makes to a server gets the zone's session key and the options forwarded from the client's own login (ForwardedLoginOptions). A game with its own local player class calls WorldPlane::GetGatewayLoginOptions from it instead. |
Iris support in your modules#
Every module that declares replicated FFastArraySerializer properties, third-party plugins included, calls SetupIrisSupport in its Build.cs:
public MyGame(ReadOnlyTargetRules Target) : base(Target)
{
// ...
SetupIrisSupport(Target);
}
Project Settings → Plugins → WorldPlane#
The plugin's settings are saved in Config/DefaultGame.ini under [/Script/WorldPlaneCore.WPSettings]. The defaults suit most games; the ones you are most likely to touch:
| Setting | Default | What it does |
|---|---|---|
Scope | local/sandbox/dev | The zone this server belongs to, tenant/project/environment. A coordinator refuses a server of another scope. Hosted zones pass -WPScope, which overrides it. |
EntityClasses | pawns, player controllers, PlayerStates | Actors of these classes get a UWPEntityComponent on zone servers, which lets them move between servers as themselves. A class with its own entity component, in C++ or Blueprint, keeps it. |
NonEntityClasses | empty | Subclasses that stay on the server that spawned them, such as a turret. The actor tag WPNotEntity does the same. |
PlayerHysteresis, NpcHysteresis | 1000 cm, 500 cm | How far past a border a player or NPC goes before it is handed to the next server. |
MinResidenceSeconds | 3 s | How long an entity stays on a server it just arrived at before it can be handed on. |
PlacedSpawnerClasses | empty | Placed actors that spawn entities (a camp that tops up its creatures). Each runs on one server of the zone, and on another when that one stops. The tag WPSpawner does the same. |
PlacedActorsKeptEverywhere | empty | Placed replicated actors every server keeps, such as level managers. Other placed replicated actors exist only on the server whose area holds them. The tag WPKeepEverywhere does the same. |
ZoneMaxPlayers | 1000 | Zone servers raise their game session's MaxPlayers to at least this, since every player in the zone has a connection on every server. |
ForwardedLoginOptions | token | Client login options the gateway passes on when it joins the player to its first server, such as your game's login token. |
bRequirePlayerToken | off | Requires a WorldPlane player token on each join (see player accounts). |
GhostBandWidth | 15000 cm | Entities this close to another server's area are shown there as ghosts. |
bRealClassGhosts | on | Ghosts are non-authoritative copies of the entity's own class, so casts, interfaces and components work on them. |
GhostClass, StandInKinds | none | A stand-in ghost class of your own (an AWPGhostActor subclass) instead, for all kinds or the kinds listed. |
OwnerFunctions | empty | Functions whose calls on a ghost run on the entity's owner, by path, such as /Script/MyGame.MyInteractable:Interact. |
bForwardEngineCalls | on | Blueprint calls of ApplyDamage, ApplyPointDamage, LaunchCharacter, AddImpulse and AddForce on a ghost run on its owner. |
GameStateServerId | 1 | Only used without a coordinator: the one server whose GameState replicates. With a coordinator, the coordinator names it. 0 lets every server replicate its own. |
Checking a project#
worldplane check reads a project's config and C++ (no editor, no build) and lists what it needs before it runs on WorldPlane, each finding with a file and line. It exits 1 when there are errors.
worldplane check path/to/MyGame # or path/to/MyGame.uproject
worldplane check path/to/MyGame --fix # make the mechanical changes first
worldplane check path/to/MyGame --json
--fix makes the changes that need no judgment about the game, then reports what is left: it lists or enables the plugin in the .uproject, sets the four DefaultEngine.ini settings, adds SetupIrisSupport(Target) to modules with fast arrays, and adds the damage-forwarding line to pawns' TakeDamage (with the include and module dependency it needs). Files keep their byte-order mark and line endings; a file that isn't UTF-8 is left alone, with the reason.
| Check | Severity | What it looks for |
|---|---|---|
config | error (one warning) | The WorldPlane plugin, and the Iris, handle generator, no-pawn connection and subobject list settings. |
iris-module | error | A module with fast arrays whose Build.cs doesn't call SetupIrisSupport. |
multicast | warning | NetMulticast RPCs, which the gateway drops from actors no player owns. |
relevancy | warning or note | IsNetRelevantFor overrides, which Iris never calls. |
entity | warning | Pawns, player controllers and PlayerStates that your settings leave without a UWPEntityComponent. |
damage | warning or note | A pawn's TakeDamage that doesn't forward damage from a ghost to the owner. |
owner-function | error, warning or note | OwnerFunctions entries that return a value, name no function in the project, or are reached through reflection only. |
It parses C++ by its shape, not with a compiler, so it can miss things and, rarely, flag what is fine. Blueprints and levels are binary and are not read.
What game code needs#
Plain game code works across servers. Most of what a single-server game does needs nothing new; a few patterns that only hold on one server need a change.
What needs nothing#
- Entity identity. Zone servers give every pawn, player controller and PlayerState a
UWPEntityComponentas it spawns or loads. A pawn is a player's while a player controls it, otherwise an NPC. - Network handles are deterministic through the handle generator in the config.
- Moving players. Pawn, controller and PlayerState move with their
SaveGameproperties, movement mode and character movement timestamps. - Registering with the coordinator. A zone server started with
-WPServerIdand-WPCoordinatorregisters before its first world has actors. - NPCs. An arriving NPC gets its default AI controller.
- Plain references across a border. An
AActor*orUObject*in a client RPC or a replicated property that names another server's entity resolves to that server's ghost of it. Around each handoff of that entity (about 3 s) it still arrives as null. - Blueprint damage and movement on a ghost (
ApplyDamage,ApplyPointDamage,LaunchCharacter,AddImpulse,AddForce) runs on the owner. - Owner-only state.
COND_OwnerOnlyandCOND_SkipOwnerproperties reach the right clients through the gateway, as on one server. - Agones. On hosted zones each server and the gateway runs as an Agones GameServer; the plugin sends its health and ready signals.
What needs a change#
| Pattern | On WorldPlane |
|---|---|
| NetMulticast RPCs | The gateway drops multicasts from actors without an owning player. Use replicated state, or UWPEventsComponent: the owning server calls Emit(Name, Values) and clients bind OnEvent. It keeps the last 8 events, so it suits hit reactions, damage numbers and sounds. |
IsNetRelevantFor | Iris never calls it. Use WorldPlane::Relevancy::Update(Actor, IsRelevant) (WPGatewayClients.h), which works on zone servers and the gateway, and call it again when the answer can change. Use entity ids, not PlayerId, in rules. |
| C++ damage to a pawn | The engine refuses damage to a pawn without authority, so a pawn's TakeDamage starts with one line: if (WorldPlane::ForwardDamageToOwner(this, Damage, Event, By, Causer)) { return Damage; }. worldplane check --fix adds it. |
| Calls that change another server's entity | List the function in OwnerFunctions (calls through reflection: Blueprint, BlueprintNativeEvent, interface Execute_, delegates), or put WP_RUN_ON_OWNER(Params...) at the top of the UFUNCTION (WPOwnerCalls.h). Functions that return nothing only. A change made on a ghost and overwritten by its owner is logged as WP_LOST_WRITE, naming the property. |
| Gameplay effects on a ghost | With WorldPlaneGAS, one line at the top of your ability system component's ApplyGameplayEffectSpecToSelf: if (WorldPlane::GAS::ForwardToOwner(*this, Spec)) { return FActiveGameplayEffectHandle(); }. The owner rebuilds the effect from the instigator's ghost and applies it to the real entity. |
| Replicated actors placed in levels | Each is kept by the server whose area holds it at load, once per zone. Replicated actors are best spawned at runtime by the server that owns their location. |
| Placed spawners | Name the class in PlacedSpawnerClasses (or tag it WPSpawner) and spawn its entities with it as their Owner. Keep each entity's id with WorldPlane::GetEntityId; WorldPlane::IsSpawnedEntityAlive says whether it lives anywhere in the zone, and WorldPlane::GetSpawnedEntities lists what an earlier copy made. |
A GameState pointer kept from BeginPlay | Only one server's GameState replicates, and it can move to another server. Look it up each time with GetWorld()->GetGameState(). Zone-wide state kept there must be the same on every server, or live elsewhere. |
| Areas and ids | Use WorldPlane::GetAreas(), WorldPlane::FindOwnerServer(Location) and WorldPlane::AllocateEntityId() rather than reading settings or making ids. |
Carrying more state in a handoff#
FWPPlayerHandoffHooks lets a game take part in a player's handoff:
CanHandOff(Controller, Pawn): on the old server, each time the player is past the border; return false to wait (for example while a save is in flight).OnGather(Controller, Pawn, OutState): on the old server, before its actors stop.OnApply(Controller, Pawn, State): on the new server, after spawn and possession.RegisterRestoreProvider(Provider): once, at module startup. A restore provider carries one named, versioned piece of player state (skills, cooldowns, a quest log) from the old server to the new one, so the new server reads nothing from your backend.
FWPRestoreProvider Provider = FWPRestoreProvider::ForStruct<FMySkills>(TEXT("mygame.skills"), /*Version*/ 2,
[](const FWPRestoreContext& Context, FMySkills& Out) { /* old server: fill Out */ return true; },
[](const FWPRestoreContext& Context, const FMySkills& In, int32 SentVersion) { /* new server: apply In */ return true; });
Provider.MinVersion = 1; // still reads v1 from servers on the previous build
Provider.DependsOn = { TEXT("mygame.character") }; // applied after that provider
Provider.OnFailed = [](const FWPRestoreContext& Context, EWPRestoreFailure Reason) { /* read your backend instead */ };
FWPPlayerHandoffHooks::RegisterRestoreProvider(MoveTemp(Provider));
- Gather also runs every
wp.CheckpointSeconds(2 s) withContext.bCheckpointset, for crash recovery: keep it cheap and free of side effects. If a server stops, the server that now owns the player's position rebuilds them from the last checkpoint, and the gateway keeps the player connected meanwhile. - Apply runs before the arriving actors first replicate, and must not wait for anything.
- Failures are never silent: each is logged on the new server, and
OnFailedgets the reason (not_sent,version_mismatch,apply_failed,dependency_failed,scope_mismatch). - A game with persistence makes sure the old server writes nothing after the handoff.
WorldPlane player accounts (optional)#
A game without accounts of its own can use WorldPlane's, per environment:
- Your launcher or login screen signs the player in with
POST /v1/play/{tenant}/{project}/{env}/sessions(or/accountsto sign up) and gets a token, good for an hour. - The client connects to the gateway with
?token=<token>.tokenis inForwardedLoginOptionsby default. - Turn on
bRequirePlayerToken. The zone's coordinator checks the token on the player's first join, and refuses that player alone if it is missing, expired or for another environment. - Read who the player is from the login options
WPPlayer=(a stable id) andWPPlayerName=, inPreLogin,LoginorPostLogin.
Games with their own accounts leave it off and forward their own token through ForwardedLoginOptions. A server that should accept the gateway's own connection without the game's normal login check calls WorldPlane::IsTrustedGatewayLogin(World, Options) in its PreLogin.
Packaging a Linux server build#
WorldPlane runs the dedicated server for Linux x86-64. Servers and the gateway run the same build.
-
A server target. Add
Source/MyGameServer.Target.csif your project has none:using UnrealBuildTool; public class MyGameServerTarget : TargetRules { public MyGameServerTarget(TargetInfo Target) : base(Target) { Type = TargetType.Server; DefaultBuildSettings = BuildSettingsVersion.Latest; IncludeOrderVersion = EngineIncludeOrderVersion.Latest; ExtraModuleNames.Add("MyGame"); } } -
Build, cook and stage it for Linux with Unreal's automation tool, from your engine folder (or package Linux Server from the editor's Platforms menu):
Engine\Build\BatchFiles\RunUAT.bat BuildCookRun -project="C:\Path\MyGame\MyGame.uproject" -noP4 ^ -noclient -server -serverplatform=Linux -serverconfig=Development ^ -build -cook -stage -pak -map=/Game/Maps/World ^ -stagingdirectory="C:\Builds\MyGame" -unattended -utf8outputThe staged server is in
C:\Builds\MyGame\LinuxServer, withMyGameServer.shat its top. -
Zip the
LinuxServerfolder and check the archive as the control plane will:worldplane build check MyGameLinuxServer.zip -
Push it, or upload it in the panel's Builds:
worldplane build push --project mystudio/mygame --label 0.4.1 MyGameLinuxServer.zip
An archive is accepted when it has:
- exactly one
<Name>Server.sh; - a
<Name>Serverbinary underBinaries/Linuxthat is an x86-64 Linux executable; - the WorldPlane plugin, loose (
Plugins/WorldPlane/WorldPlane.uplugin) or listed in the staging manifestManifest_UFSFiles_*.txtthat packaging writes beside the pak, at pluginVersion1 or later; - no files that look like secrets:
.env,*.pem,*.key,id_rsa*,id_ed25519*. Secrets come from the environment, never from builds; - at most 20 GiB.
The first start runs your server alone. On app.worldplane.net a new build is imaged, then started once on a game node with none of your environment's arguments, and must keep running for a minute. A server that stops by itself there makes the build rejected, with its last log lines. If your server needs a token from its arguments, log that it is missing and keep running.
Server arguments#
You don't start servers yourself: the zone's coordinator does. Every zone server gets the map, then:
-log -unattended -nosteam -port={game_port} -WPServerId={index} -WPScope={tenant}/{project}/{env}
-WPCoordinator={coordinator} -WPTransportPort={transport_port} -WPSessionKey=...
The gateway is the same build, started with the map and:
-log -unattended -nosteam -port=7777 -ini:Engine:[ConsoleVariables]:wp.proxy.Enabled=1
-WPScope={tenant}/{project}/{env} -WPCoordinator={coordinator} -WPSessionKey=...
Your own arguments come after these, on every server and the gateway: the environment's extra_args, set with --server-arg in the CLI or Extra server arguments in the panel. They can use placeholders:
| Placeholder | Becomes |
|---|---|
{index} | The server's number. |
{role} | server or gateway. -ServerId=mygame-{role}-{index} names each process uniquely to your own backend. |
{env:NAME} | The environment's secret NAME. A deploy whose arguments use a secret the environment lacks is refused, naming it. |
worldplane env secret set mystudio/mygame/test API_TOKEN
worldplane env update mystudio/mygame/test "--server-arg=-ApiToken={env:API_TOKEN}"
worldplane deploy mystudio/mygame/test <build id>
Read it in your game as any command-line value:
FString Token;
FParse::Value(FCommandLine::Get(), TEXT("ApiToken="), Token);
--server-arg replaces the arguments, so give every one the servers need. Arguments and secrets take effect at the next deploy.
The plugin's own arguments#
| Argument | Meaning |
|---|---|
-WPServerId=N | This zone server's id. |
-WPCoordinator=URL | The zone's coordinator: the server registers there and takes its areas from it. |
-WPScope=tenant/project/environment | Overrides the Scope setting. A coordinator refuses a server of another scope, and the server exits rather than join the wrong zone. |
-WPTransportPort=N | The port servers use to talk to each other. |
-WPSessionKey=KEY | The zone's shared key. Servers accept the gateway's connection only with it. |
wp.proxy.Enabled=1 | Makes the process a gateway. With -WPCoordinator and -WPScope it takes its servers from the coordinator and follows them coming and going. |