Multiplayer & Dedicated Server Testing
Automating multiplayer games in Unreal Engine has traditionally been one of game development's hardest challenges. Specter leverages Playwright's multi-context architecture to let you drive a Dedicated Server and multiple game clients simultaneously inside a single test file.
Defining Multi-Client Fixtures with createWorld
In Specter, the default world fixture connects to the authoritative Server (or host PIE instance). Using baseTest.extend() and the createWorld helper, you can spin up additional client connection fixtures:
import { test as baseTest, SpecterWorld } from '@specter-automation/test';
// Extend baseTest: 'world' is the default server, use createWorld for additional clients
export const test = baseTest.extend<{
client1: SpecterWorld;
client2: SpecterWorld;
}>({
client1: async ({ createWorld }, use) => {
const client = await createWorld({
port: 8182,
name: 'Client 1',
autoJoinHost: true,
hostAddress: '127.0.0.1:7777'
});
await use(client);
},
client2: async ({ createWorld }, use) => {
const client = await createWorld({
port: 8183,
name: 'Client 2',
autoJoinHost: true,
hostAddress: '127.0.0.1:7777'
});
await use(client);
},
});Writing Multi-Client Tests
Once defined, inject your multi-client fixtures into your test parameters. Each client instance controls its own PlayerController and UMG viewport independently:
import { test, expect } from './multiplayer-fixtures';
// 'world' is the default server world; client1 & client2 are injected fixtures
test('Player 1 ability damage replicates to Player 2', async ({
world,
client1,
client2
}) => {
// 1. Client 1 (Steel) triggers an ability targeting Player 2
await client1.actor('BP_Hero_Steel').pressKey('Q');
// 2. Assert that Client 2 receives the replicated health reduction on their character
await expect(client2.actor('BP_Hero_Enemy')).toHavePropertyValue('Health', 80);
// 3. Verify server authority ground truth directly on the default server world
await expect(world.actor('BP_Hero_Enemy')).toHavePropertyValue('Health', 80);
});Simulating Network Conditions (Ping & Packet Loss)
Don't let your multiplayer game break under high ping or bad connections. Specter provides built-in network condition emulation so you can test client prediction and server reconciliation under laggy conditions:
test('Replication under 150ms latency and 5% packet loss', async ({ world, client1, client2 }) => {
// 1. Simulate high ping & packet loss on Client 1's network connection
await client1.network.setEmulatedLatency(150); // 150ms ping
await client1.network.setPacketLoss(5); // 5% packet loss
// 2. Client 1 triggers an ability targeting Player 2
await client1.actor('BP_Hero_Steel').pressKey('Q');
// 3. Assert Client 2 receives the replicated health reduction on their character
await expect(client2.actor('BP_Hero_Enemy')).toHavePropertyValue('Health', 80);
// 4. Verify authoritative server ground truth on the world fixture
await expect(world.actor('BP_Hero_Enemy')).toHavePropertyValue('Health', 80);
});⚡ Multi-PIE & Standalone Process Support
Specter seamlessly connects to Unreal Engine Play-In-Editor (PIE) with multiple viewports enabled, or to standalone dedicated server and client processes running across separate ports in headless CI/CD builds!
Multiplayer Network Role Tagging
In networked automated tests, understanding which peer executed an action is critical for resolving desyncs. Specter dynamically resolves engine networking state and prefixes all telemetry, logs, and events with the originating network role:
| Prefix | Network Context |
|---|---|
| [Server] | Authoritative dedicated server or listen server host instance. |
| [Client_1], [Client_2] | Specific connected multiplayer client instances in PIE or standalone. |
| [DedicatedServer] | Standalone dedicated server process without local player viewports. |
| [Standalone] | Single-player game session with no networking active. |
Where Network Roles Appear
- Engine Logs (
UE_LOG): Prefixes messages with peer tags (e.g.LogSpecter: [Server] Ability activated...). - WebSocket JSON Payloads: Serialized under
event.networkRolefor test assertions and filtering. - C++ & TypeScript SDK: Stored on
FSpecterTimelineEvent::NetworkRoleandSpecterTimelineEvent.networkRole. - GAS Causality Timelines: Displayed alongside timestamps (e.g.
• 🎯 [Server] [1.02s] Ability Activated).
// WebSocket JSON Event Packet with Network Role Tagging:
{
"type": "GLOBAL_timeline-event",
"networkRole": "Server",
"timestamp": 1.45,
"eventName": "GameplayEffectApplied",
"target": "BP_Hero_Steel",
"instigator": "BP_Hero_Howitzer",
"effect": "GE_Howitzer_BasicAttack"
}