Skip to main content
Reading time: ~35 minutes
CORP includes a powerful built-in networking system inspired by Unity’s Netcode for GameObjects. It handles automatic replication of GameObjects and their state across the client-server boundary.

Table of Contents

NetworkObject

NetworkObject is a special component that makes a GameObject replicate across the network. Any GameObject that needs to exist on both server and clients must have a NetworkObject component.

Adding a NetworkObject

import { GameObject } from "CORP/shared/componentization/game-object";
import { NetworkObject } from "CORP/shared/networking/network-object";

// Create a networked GameObject
const player = new GameObject("Player");
const netObj = player.addComponent(NetworkObject);

// NetworkObject automatically spawns when added with addComponent()
// You only need to call spawn() manually when using addComponentNoInit()
Note: When using addComponent(), the NetworkObject is automatically spawned and replicated. You only need to manually call spawn() when using the low-level addComponentNoInit() method, which is primarily for internal use.

NetworkObject Features

  • Automatic Replication: The GameObject and its transform are replicated to all clients
  • Ownership: Can be owned by a specific player or the server
  • Hierarchy Support: Child NetworkObjects are automatically managed
  • Network Events: Subscribe to ownership and state changes

NetworkObject Events

import { NetworkObject } from "CORP/shared/networking/network-object";

export class PlayerManager extends Behavior {
    public onStart(): void {
        const netObj = this.getComponent(NetworkObject)!;

        // Fired when ownership changes
        netObj.ownerChanged.connect((newOwner) => {
            if (newOwner) {
                print(`Now owned by ${newOwner.Name}`);
            } else {
                print("Now owned by server");
            }
        });

        // Fired when loaded state changes
        netObj.isLoadedChanged.connect((isLoaded) => {
            print(`Network loaded: ${isLoaded}`);
        });
    }

    protected getSourceScript(): ModuleScript {
        return script as ModuleScript;
    }

    public willRemove(): void {}
}

Getting Network Information

export class NetworkedComponent extends Behavior {
    public onStart(): void {
        const netObj = this.getComponent(NetworkObject);

        if (netObj) {
            // Get the owner (Player or undefined for server)
            const owner = netObj.getOwner();

            // Get the network ID
            const id = netObj.getId();

            // Check if this is owned by the local player
            const isLocallyOwned = owner === Players.LocalPlayer;
        }
    }

    protected getSourceScript(): ModuleScript {
        return script as ModuleScript;
    }

    public willRemove(): void {}
}

NetworkBehavior

NetworkBehavior is a specialized component that enables network replication of component state. It extends Behavior and requires a NetworkObject on the same GameObject.

Creating a NetworkBehavior

import { GameObject } from "CORP/shared/componentization/game-object";
import { NetworkBehavior } from "CORP/shared/networking/network-behavior";
import RequiresComponent from "CORP/shared/componentization/decorators/requires-component";
import { NetworkObject } from "CORP/shared/networking/network-object";

@RequiresComponent(NetworkObject)
export class PlayerController extends NetworkBehavior {
    public constructor(gameObject: GameObject) {
        super(gameObject);
    }

    public onStart(): void {
        if (RunService.IsServer()) {
            print("PlayerController started on server");
        } else {
            print("PlayerController started on client");
        }
    }

    public willRemove(): void {
        print("PlayerController removed");
    }

    protected getSourceScript(): ModuleScript {
        return script as ModuleScript;
    }
}

NetworkBehavior Features

  • Automatic Spawning: Replicated to clients when the NetworkObject spawns
  • Network Variables: Synchronize state using NetworkedVariables, including primitives, tables, and ScriptableObjects
  • RPCs: Call methods across the network
  • References: Can be referenced in NetworkedVariables
  • sendBehavior: Manually trigger replication of the behavior to specific clients

Getting the NetworkObject

export class HealthSystem extends NetworkBehavior {
    public onStart(): void {
        // Get the associated NetworkObject
        const netObj = this.getNetworkObject();

        // Use it to check ownership
        if (netObj.getOwner() === Players.LocalPlayer) {
            print("I own this!");
        }
    }

    protected getSourceScript(): ModuleScript {
        return script as ModuleScript;
    }

    public willRemove(): void {}
}

Networked Variables

NetworkedVariable allows you to synchronize data between server and clients automatically. When the value changes on the server, it’s automatically replicated to all clients.

Basic Usage

import { NetworkBehavior } from "CORP/shared/networking/network-behavior";
import { NetworkedVariable } from "CORP/shared/observables/networked-observables/networked-variable";
import { NetworkVariableReplicationBehavior } from "CORP/shared/observables/networked-observables";

export class PlayerHealth extends NetworkBehavior {
    // Create a networked variable with an initial value
    public readonly health = new NetworkedVariable<number>(100);

    // With configuration
    public readonly score = new NetworkedVariable<number>(0, {
        replicationBehavior: NetworkVariableReplicationBehavior.INSTANTANEOUS
    });

    public onStart(): void {
        if (RunService.IsServer()) {
            // Server can modify the value
            this.health.setValue(75);
            
            task.wait(2);
            this.health.setValue(50);
        } else {
            // Client can only read
            print(`Initial health: ${this.health.getValue()}`);

            // Listen for changes
            this.health.onValueChanged.connect((newValue) => {
                print(`Health changed to: ${newValue}`);
            });
        }
    }

    protected getSourceScript(): ModuleScript {
        return script as ModuleScript;
    }

    public willRemove(): void {}
}

Supported Types

NetworkedVariables support the following types:
// Primitives
new NetworkedVariable<number>(0);
new NetworkedVariable<string>("hello");
new NetworkedVariable<boolean>(true);

// Roblox types
new NetworkedVariable<Vector3>(new Vector3(0, 0, 0));
new NetworkedVariable<Color3>(new Color3(1, 1, 1));
new NetworkedVariable<CFrame>(new CFrame());

// Tables (plain objects)
new NetworkedVariable<{ x: number, y: number }>({ x: 0, y: 0 });

// ScriptableObjects
new NetworkedVariable<MyScriptableObject>(myAsset);

// NetworkBehavior references
new NetworkedVariable<PlayerController>(undefined);

// Collections
new NetworkedList<number>();
new NetworkedMap<string, number>();
new NetworkedSet<string>();

Replication Behavior

import { NetworkVariableReplicationBehavior } from "CORP/shared/observables/networked-observables";

export class GameState extends NetworkBehavior {
    // BATCHED: Changes are grouped and sent on HeartBeat (efficient)
    public readonly playerCount = new NetworkedVariable<number>(0, {
        replicationBehavior: NetworkVariableReplicationBehavior.BATCHED
    });

    // INSTANTANEOUS: Changes are sent immediately (expensive, use sparingly)
    public readonly urgentAlert = new NetworkedVariable<string>("", {
        replicationBehavior: NetworkVariableReplicationBehavior.INSTANTANEOUS
    });

    protected getSourceScript(): ModuleScript {
        return script as ModuleScript;
    }

    public onStart(): void {}
    public willRemove(): void {}
}

Networked Collections

CORP also provides networked collections for more complex data structures:

NetworkedList

import { NetworkedList } from "CORP/shared/observables/networked-observables/networked-list";

export class PlayerList extends NetworkBehavior {
    public readonly players = new NetworkedList<string>();

    public onStart(): void {
        if (RunService.IsServer()) {
            // Server modifies
            this.players.push("Alice");
            this.players.push("Bob");
            this.players.remove(0);
        } else {
            // Client observes
            this.players.onItemAdded.connect((item, index) => {
                print(`Player added: ${item} at ${index}`);
            });

            this.players.onItemRemoved.connect((item, index) => {
                print(`Player removed: ${item} from ${index}`);
            });

            // Called just before an item is removed
            this.players.onItemRemoving.connect((item, index) => {
                print(`Player removing: ${item} from ${index}`);
            });
        }
    }

    protected getSourceScript(): ModuleScript {
        return script as ModuleScript;
    }

    public willRemove(): void {}
}

NetworkedMap

import { NetworkedMap } from "CORP/shared/observables/networked-observables/networked-map";

export class ScoreBoard extends NetworkBehavior {
    public readonly scores = new NetworkedMap<string, number>();

    public onStart(): void {
        if (RunService.IsServer()) {
            this.scores.set("Player1", 100);
            this.scores.set("Player2", 50);
            this.scores.delete("Player1");
        } else {
            this.scores.onKeySet.connect((key, value) => {
                print(`${key} score: ${value}`);
            });

            this.scores.onKeyDeleted.connect((key) => {
                print(`${key} removed from scoreboard`);
            });
        }
    }

    protected getSourceScript(): ModuleScript {
        return script as ModuleScript;
    }

    public willRemove(): void {}
}
Note: NetworkedMap only supports primitive keys (string, number, boolean). Complex types like Vector3 or tables cannot be used as keys.

NetworkedSet

import { NetworkedSet } from "CORP/shared/observables/networked-observables/networked-set";

export class ActivePlayers extends NetworkBehavior {
    public readonly activePlayers = new NetworkedSet<string>();

    public onStart(): void {
        if (RunService.IsServer()) {
            this.activePlayers.add("Player1");
            this.activePlayers.add("Player2");
            this.activePlayers.delete("Player1");
        } else {
            this.activePlayers.onItemAdded.connect((item) => {
                print(`${item} became active`);
            });

            this.activePlayers.onItemDeleted.connect((item) => {
                print(`${item} became inactive`);
            });
        }
    }

    protected getSourceScript(): ModuleScript {
        return script as ModuleScript;
    }

    public willRemove(): void {}
}

Remote Procedure Calls (RPCs)

RPCs allow you to call methods across the network boundary. CORP provides a powerful decorator-based RPC system with extensive configuration options.

Basic RPC

import { NetworkBehavior } from "CORP/shared/networking/network-behavior";
import { RPC } from "CORP/shared/networking/RPC";

export class Weapon extends NetworkBehavior {
    @RPC.Method({
        endpoints: [RPC.Endpoint.CLIENT_TO_SERVER],
        accessPolicy: RPC.AccessPolicy.OWNER
    })
    public shoot(target: Vector3): void {
        print(`Player shot at ${target}`);
        // Server handles the shot
        this.dealDamage(target);
    }

    private dealDamage(target: Vector3): void {
        // Damage logic here
    }

    protected getSourceScript(): ModuleScript {
        return script as ModuleScript;
    }

    public onStart(): void {}
    public willRemove(): void {}
}

RPC Configuration

Endpoints

Define where the RPC can be called from and to:
// Client to Server (most common)
@RPC.Method({
    endpoints: [RPC.Endpoint.CLIENT_TO_SERVER]
})
public requestItem(): void {}

// Server to Client
@RPC.Method({
    endpoints: [RPC.Endpoint.SERVER_TO_CLIENT]
})
public showNotification(message: string): void {}

// Server local execution
@RPC.Method({
    endpoints: [RPC.Endpoint.SERVER_LOCALLY]
})
public processOnServer(): void {}

// Client local execution
@RPC.Method({
    endpoints: [RPC.Endpoint.CLIENT_LOCALLY]
})
public processOnClient(): void {}

// Client to Client (NOT YET SUPPORTED)
// Note: CLIENT_TO_CLIENT endpoint is reserved for future use
// @RPC.Method({
//     endpoints: [RPC.Endpoint.CLIENT_TO_CLIENT]
// })
// public syncLocalState(): void {}

// Multiple endpoints
@RPC.Method({
    endpoints: [RPC.Endpoint.CLIENT_TO_SERVER, RPC.Endpoint.SERVER_LOCALLY]
})
public flexibleMethod(): void {}

Access Policy

Control who can invoke the RPC:
// Only the owner can call
@RPC.Method({
    endpoints: [RPC.Endpoint.CLIENT_TO_SERVER],
    accessPolicy: RPC.AccessPolicy.OWNER
})
public ownerOnlyAction(): void {}

// Only non-owners can call
@RPC.Method({
    endpoints: [RPC.Endpoint.CLIENT_TO_SERVER],
    accessPolicy: RPC.AccessPolicy.NOT_OWNER
})
public otherPlayersAction(): void {}

// Anyone can call (default)
@RPC.Method({
    endpoints: [RPC.Endpoint.CLIENT_TO_SERVER],
    accessPolicy: RPC.AccessPolicy.ANYONE
})
public publicAction(): void {}

Return Mode

Specify if the RPC returns a value:
// Void (no return value)
@RPC.Method({
    endpoints: [RPC.Endpoint.CLIENT_TO_SERVER],
    returnMode: RPC.ReturnMode.VOID
})
public fireEvent(): void {}

// Returns a value (uses RemoteFunction)
@RPC.Method({
    endpoints: [RPC.Endpoint.CLIENT_TO_SERVER],
    returnMode: RPC.ReturnMode.RETURNS
})
public getData(): string {
    return "Server data";
}

Reliability

Control transmission reliability:
// Reliable (guaranteed delivery, ordered)
@RPC.Method({
    endpoints: [RPC.Endpoint.CLIENT_TO_SERVER],
    reliability: RPC.Reliability.RELIABLE
})
public importantAction(): void {}

// Unreliable (fast, may be dropped)
@RPC.Method({
    endpoints: [RPC.Endpoint.CLIENT_TO_SERVER],
    reliability: RPC.Reliability.UNRELIABLE
})
public frequentUpdate(position: Vector3): void {}

Complete RPC Example

import { NetworkBehavior } from "CORP/shared/networking/network-behavior";
import { RPC } from "CORP/shared/networking/RPC";
import { NetworkedVariable } from "CORP/shared/observables/networked-observables/networked-variable";

export class CombatSystem extends NetworkBehavior {
    public readonly health = new NetworkedVariable<number>(100);

    // Client requests to attack
    @RPC.Method({
        endpoints: [RPC.Endpoint.CLIENT_TO_SERVER],
        accessPolicy: RPC.AccessPolicy.OWNER,
        returnMode: RPC.ReturnMode.VOID,
        reliability: RPC.Reliability.RELIABLE
    })
    public attack(targetId: string, damage: number): void {
        print(`Player attacking target ${targetId} for ${damage} damage`);
        
        // Process attack on server
        const target = this.findTarget(targetId);
        if (target) {
            target.takeDamage(damage);
        }

        // Notify clients
        this.notifyAttack(targetId, damage);
    }

    // Server notifies clients of attacks
    @RPC.Method({
        endpoints: [RPC.Endpoint.SERVER_TO_CLIENT],
        accessPolicy: RPC.AccessPolicy.ANYONE,
        reliability: RPC.Reliability.UNRELIABLE
    })
    public notifyAttack(targetId: string, damage: number): void {
        print(`Attack notification: ${damage} damage to ${targetId}`);
        // Play visual effects
    }

    // Client requests current health (with return value)
    @RPC.Method({
        endpoints: [RPC.Endpoint.CLIENT_TO_SERVER],
        returnMode: RPC.ReturnMode.RETURNS
    })
    public getDetailedHealth(): { current: number, max: number } {
        return {
            current: this.health.getValue(),
            max: 100
        };
    }

    private findTarget(id: string): CombatSystem | undefined {
        // Implementation
        return undefined;
    }

    private takeDamage(damage: number): void {
        const newHealth = math.max(0, this.health.getValue() - damage);
        this.health.setValue(newHealth);
    }

    protected getSourceScript(): ModuleScript {
        return script as ModuleScript;
    }

    public onStart(): void {}
    public willRemove(): void {}
}

Network Ownership

NetworkObjects can be owned by specific players or the server. Ownership affects:
  • Who can modify NetworkedVariables
  • RPC access policies
  • Network authority

Changing Ownership (Server Only)

export class OwnershipManager extends Behavior {
    public onStart(): void {
        if (RunService.IsServer()) {
            const netObj = this.getComponent(NetworkObject)!;

            // Give ownership to a player
            const player = Players.GetPlayers()[0];
            netObj.changeOwnership(player);

            // Give ownership back to server
            netObj.changeOwnership(undefined);
        }
    }

    protected getSourceScript(): ModuleScript {
        return script as ModuleScript;
    }

    public willRemove(): void {}
}

Checking Ownership

export class PlayerControls extends NetworkBehavior {
    public onStart(): void {
        const netObj = this.getNetworkObject();
        
        // Check if owned by local player
        const isOurs = netObj.getOwner() === Players.LocalPlayer;

        if (isOurs) {
            // Enable controls
            this.enableControls();
        }

        // Listen for ownership changes
        netObj.ownerChanged.connect((newOwner) => {
            if (newOwner === Players.LocalPlayer) {
                this.enableControls();
            } else {
                this.disableControls();
            }
        });
    }

    private enableControls(): void {}
    private disableControls(): void {}

    protected getSourceScript(): ModuleScript {
        return script as ModuleScript;
    }

    public willRemove(): void {}
}

Spawning and Despawning

Spawning NetworkObjects

NetworkObjects are automatically spawned when you use addComponent():
if (RunService.IsServer()) {
    const enemy = new GameObject("Enemy");
    
    // NetworkObject automatically spawns during addComponent()
    const netObj = enemy.addComponent(NetworkObject);
    
    // Add other components
    enemy.addComponent(EnemyAI);
    
    // No need to call spawn() - it's already replicated!
}
Note: You only need to manually call spawn() when using addComponentNoInit(), which is a low-level method primarily for internal use.

Despawning NetworkObjects

Simply destroy the GameObject:
if (RunService.IsServer()) {
    // This automatically despawns on all clients
    gameObject.destroy();
}

Dynamic Spawning Example

export class EnemySpawner extends Behavior {
    public onStart(): void {
        if (RunService.IsServer()) {
            this.spawnEnemies();
        }
    }

    private spawnEnemies(): void {
        for (let i = 0; i < 5; i++) {
            const enemy = new GameObject(`Enemy_${i}`);
            
            // Position the enemy
            enemy.setTransform(new CFrame(i * 10, 0, 0));
            
            // NetworkObject automatically spawns on network when added
            enemy.addComponent(NetworkObject);
            enemy.addComponent(EnemyAI);
        }
    }

    protected getSourceScript(): ModuleScript {
        return script as ModuleScript;
    }

    public willRemove(): void {}
}

Best Practices

1. Always Use NetworkObject with NetworkBehavior

// ✓ Good
@RequiresComponent(NetworkObject)
export class MyNetworkBehavior extends NetworkBehavior {}

// ✗ Bad - will not work without NetworkObject
export class MyNetworkBehavior extends NetworkBehavior {}

2. Server Authority

Let the server make authoritative decisions:
export class HealthSystem extends NetworkBehavior {
    public readonly health = new NetworkedVariable<number>(100);

    // Client requests damage
    @RPC.Method({
        endpoints: [RPC.Endpoint.CLIENT_TO_SERVER],
        accessPolicy: RPC.AccessPolicy.OWNER
    })
    public requestDamage(amount: number): void {
        // Server validates and applies
        if (this.canTakeDamage(amount)) {
            this.health.setValue(this.health.getValue() - amount);
        }
    }

    private canTakeDamage(amount: number): boolean {
        // Server-side validation
        return amount > 0 && amount <= 100;
    }

    protected getSourceScript(): ModuleScript {
        return script as ModuleScript;
    }

    public onStart(): void {}
    public willRemove(): void {}
}

3. Use Batched Replication When Possible

// ✓ Good for frequently changing values
public readonly position = new NetworkedVariable<Vector3>(Vector3.zero, {
    replicationBehavior: NetworkVariableReplicationBehavior.BATCHED
});

// ✗ Avoid for frequent updates
public readonly position = new NetworkedVariable<Vector3>(Vector3.zero, {
    replicationBehavior: NetworkVariableReplicationBehavior.INSTANTANEOUS
});

4. Validate RPC Inputs

@RPC.Method({
    endpoints: [RPC.Endpoint.CLIENT_TO_SERVER],
    accessPolicy: RPC.AccessPolicy.OWNER
})
public purchaseItem(itemId: string, cost: number): void {
    // ✓ Validate inputs
    if (cost < 0 || cost > 10000) {
        warn("Invalid cost");
        return;
    }

    // Process purchase
}

5. Clean Up Connections

export class NetworkedComponent extends NetworkBehavior {
    public onStart(): void {
        const connection = this.health.onValueChanged.connect((value) => {
            print(value);
        });

        // ✓ Add to collector for automatic cleanup
        this.collector.add(connection);
    }

    public willRemove(): void {
        // Collector handles cleanup automatically
    }

    protected getSourceScript(): ModuleScript {
        return script as ModuleScript;
    }
}

Next Steps


Master networking to create seamless multiplayer experiences with CORP! 🌐