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 {}
}
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
});
@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! 🌐