Reading time: ~35 minutes
This guide covers advanced features and patterns in CORP for experienced developers.
Table of Contents
Spawn Management
The SpawnManager handles NetworkObject and NetworkBehavior spawning and synchronization.
Understanding Spawn Lifecycle
When a NetworkObject is added to a GameObject on the server:
addComponent(NetworkObject) is called
- NetworkObject automatically spawns and generates a unique network ID
- GameObject is replicated to all clients
- All NetworkBehaviors are registered
- Clients reconstruct the GameObject and components
onStart() is called on both server and clients
Note: You don’t need to manually call spawn() - NetworkObjects spawn automatically when added with addComponent(). Manual spawn() is only needed when using the low-level addComponentNoInit() method.
Working with SpawnManager
import { SpawnManager } from "CORP/shared/networking/spawn-manager";
// Get a NetworkBehavior by ID
const behavior = SpawnManager.getNetworkBehaviorById(behaviorId);
// Listen for NetworkBehavior spawns
SpawnManager.onNetworkBehaviorAdded.connect((behavior) => {
print(`NetworkBehavior spawned: ${behavior.getId()}`);
});
// Listen for NetworkBehavior removals
SpawnManager.onNetworkBehaviorRemoved.connect((behavior) => {
print(`NetworkBehavior removed: ${behavior.getId()}`);
});
Dynamic Spawning Patterns
Spawn on Demand
export class EnemySpawner extends Behavior {
private spawnedEnemies: GameObject[] = [];
public spawnEnemy(position: Vector3): GameObject {
const enemy = new GameObject("Enemy");
enemy.setTransform(new CFrame(position));
// NetworkObject automatically spawns when added
enemy.addComponent(NetworkObject);
enemy.addComponent(EnemyAI);
enemy.addComponent(HealthSystem);
if (RunService.IsServer()) {
this.spawnedEnemies.push(enemy);
}
return enemy;
}
public despawnAll(): void {
if (RunService.IsServer()) {
for (const enemy of this.spawnedEnemies) {
enemy.destroy();
}
this.spawnedEnemies = [];
}
}
protected getSourceScript(): ModuleScript {
return script as ModuleScript;
}
public onStart(): void {}
public willRemove(): void {}
}
Object Pooling
export class ProjectilePool extends NetworkBehavior {
private pool: GameObject[] = [];
private active: Set<GameObject> = new Set();
public onStart(): void {
if (RunService.IsServer()) {
// Pre-spawn projectiles
for (let i = 0; i < 10; i++) {
const projectile = this.createProjectile();
this.pool.push(projectile);
}
}
}
private createProjectile(): GameObject {
const proj = new GameObject("Projectile");
const netObj = proj.addComponent(NetworkObject);
proj.addComponent(Projectile);
// Start inactive (no parent)
proj.setParent(undefined);
return proj;
}
public spawn(position: Vector3, direction: Vector3): GameObject | undefined {
if (!RunService.IsServer()) return;
// Get from pool
const projectile = this.pool.pop();
if (!projectile) return undefined;
// Activate
projectile.setTransform(new CFrame(position));
projectile.setParent(SceneManager.currentScene);
const proj = projectile.getComponent(Projectile);
proj?.activate(direction);
this.active.add(projectile);
return projectile;
}
public despawn(projectile: GameObject): void {
if (!RunService.IsServer()) return;
// Deactivate - remove from scene
projectile.setParent(undefined);
this.active.delete(projectile);
this.pool.push(projectile);
}
protected getSourceScript(): ModuleScript {
return script as ModuleScript;
}
public willRemove(): void {}
}
Observable System
CORP uses an observable system for networked state management.
NetworkedObservable Base Class
All networked collections inherit from NetworkedObservable:
export class CustomNetworkedType<T> extends NetworkedObservable<T, InternalVersionOf<T>> {
protected encode(input: T): InternalVersionOf<T> {
// Convert to network-safe format
return input as InternalVersionOf<T>;
}
protected decode(input: InternalVersionOf<T>): T {
// Convert from network format
return input as T;
}
protected needsEncoding(value: T | InternalVersionOf<T>): value is T {
// Check if value needs encoding
return true;
}
}
Linking NetworkBehaviors
NetworkedVariables can reference NetworkBehaviors:
export class TargetingSystem extends NetworkBehavior {
// References another NetworkBehavior
public readonly currentTarget = new NetworkedVariable<EnemyAI | undefined>(undefined);
public setTarget(enemy: EnemyAI): void {
if (RunService.IsServer()) {
// Automatically creates a network reference
this.currentTarget.setValue(enemy);
}
}
public onStart(): void {
this.currentTarget.onValueChanged.connect((newTarget) => {
if (newTarget) {
print(`Now targeting: ${newTarget.getName()}`);
}
});
}
protected getSourceScript(): ModuleScript {
return script as ModuleScript;
}
public willRemove(): void {}
}
Custom Observable Events
Create custom events for observables:
export class InventorySystem extends NetworkBehavior {
public readonly items = new NetworkedList<string>();
public readonly onItemEquipped = new Signal<[string]>("itemEquipped");
public equipItem(itemName: string): void {
if (RunService.IsServer()) {
const index = this.items.getValue().indexOf(itemName);
if (index !== -1) {
// Move to front (equipped slot)
this.items.remove(index);
this.items.push(itemName);
// Fire custom event
this.onItemEquipped.fire(itemName);
}
}
}
protected getSourceScript(): ModuleScript {
return script as ModuleScript;
}
public onStart(): void {}
public willRemove(): void {}
}
Signals
CORP uses a powerful Signal system for event-driven programming. Signals provide type-safe, memory-efficient event handling.
Basic Signal Usage
import { Signal } from "CORP/shared/utilities/signal";
export class EventEmitter extends Behavior {
// Create a signal with typed parameters
public readonly onDamaged = new Signal<[number, Player]>("onDamaged");
public readonly onHealed = new Signal<[number]>("onHealed");
public takeDamage(amount: number, attacker: Player): void {
// Fire the signal
this.onDamaged.fire(amount, attacker);
}
public heal(amount: number): void {
this.onHealed.fire(amount);
}
protected getSourceScript(): ModuleScript {
return script as ModuleScript;
}
public onStart(): void {}
public willRemove(): void {}
}
Connecting to Signals
export class DamageListener extends Behavior {
public onStart(): void {
const emitter = this.getComponent(EventEmitter);
if (emitter) {
// Connect to signal
const connection = emitter.onDamaged.connect((amount, attacker) => {
print(`Took ${amount} damage from ${attacker.Name}`);
});
// Add to collector for automatic cleanup
this.collector.add(connection);
}
}
protected getSourceScript(): ModuleScript {
return script as ModuleScript;
}
public willRemove(): void {
// Connections in collector are automatically cleaned up
}
}
One-Time Connections
export class OneShotListener extends Behavior {
public onStart(): void {
const emitter = this.getComponent(EventEmitter);
if (emitter) {
// Connect once, then disconnect
const connection = emitter.onDamaged.connect((amount, attacker) => {
print("First damage taken!");
connection.disconnect();
});
}
}
protected getSourceScript(): ModuleScript {
return script as ModuleScript;
}
public willRemove(): void {}
}
Custom Signals
export class GameStateManager extends Behavior {
public readonly onGameStart = new Signal<[]>("onGameStart");
public readonly onGameEnd = new Signal<[winner: string]>("onGameEnd");
public readonly onRoundStart = new Signal<[roundNumber: number]>("onRoundStart");
public startGame(): void {
print("Game starting!");
this.onGameStart.fire();
}
public endGame(winner: string): void {
print(`Game ended! Winner: ${winner}`);
this.onGameEnd.fire(winner);
}
public startRound(roundNumber: number): void {
print(`Round ${roundNumber} starting!`);
this.onRoundStart.fire(roundNumber);
}
protected getSourceScript(): ModuleScript {
return script as ModuleScript;
}
public onStart(): void {}
public willRemove(): void {}
}
Custom Serialization
Extending Behavior Serialization
Override serialization methods for custom types:
export class CustomData extends Behavior {
@SerializeField
public complexData: ComplexType = new ComplexType();
protected override serializeProperty(datatable: dict, key: string): void {
const value = (this as dict)[key];
if (key === "complexData") {
// Custom serialization for ComplexType
datatable[key] = {
type: "ComplexType",
data: this.serializeComplexType(value as ComplexType)
};
} else {
super.serializeProperty(datatable, key);
}
}
private serializeComplexType(data: ComplexType): dict {
return {
// Serialize complex type to plain object
field1: data.field1,
field2: data.field2
};
}
protected getSourceScript(): ModuleScript {
return script as ModuleScript;
}
public onStart(): void {}
public willRemove(): void {}
}
Scene Serialization Hooks
export class SaveableComponent extends Behavior {
private runtimeData: Map<string, number> = new Map();
public saveToScene(): dict {
const state = this.serializeState();
// Add runtime data
state.runtimeData = Object.fromEntries(this.runtimeData);
return state;
}
public loadFromScene(data: dict): void {
// Restore runtime data
if (data.runtimeData) {
this.runtimeData = new Map(Object.entries(data.runtimeData as dict) as [string, number][]);
}
}
protected getSourceScript(): ModuleScript {
return script as ModuleScript;
}
public onStart(): void {}
public willRemove(): void {}
}
Batched Network Updates
Use batched replication (the default) for frequently changing values:
export class MovementSync extends NetworkBehavior {
// BATCHED is the default replication behavior
// Updates are sent on HeartBeat for efficiency
public readonly position = new NetworkedVariable<Vector3>(Vector3.zero, {
replicationBehavior: NetworkVariableReplicationBehavior.BATCHED // default
});
// For critical updates that must be sent immediately
public readonly healthCritical = new NetworkedVariable<number>(100, {
replicationBehavior: NetworkVariableReplicationBehavior.INSTANTANEOUS
});
public onStart(): void {
if (RunService.IsServer()) {
// Update position frequently - batched automatically
RunService.Heartbeat.Connect(() => {
this.position.setValue(this.getPosition());
});
}
}
protected getSourceScript(): ModuleScript {
return script as ModuleScript;
}
public willRemove(): void {}
}
Note: BATCHED is the default replication behavior. It groups updates and sends them on HeartBeat for efficiency. Only use INSTANTANEOUS for critical updates that must be sent immediately.
Execution Order Optimization
Control component initialization order:
// Initialize managers first
export class GameManager extends Behavior {
public readonly executionOrder = -100;
// ... implementation
}
// Initialize systems next
export class PhysicsSystem extends Behavior {
public readonly executionOrder = -50;
// ... implementation
}
// Initialize gameplay components last
export class PlayerController extends Behavior {
public readonly executionOrder = 0; // Default
// ... implementation
}
Lazy Component Initialization
Defer expensive initialization:
export class ExpensiveComponent extends Behavior {
private initialized: boolean = false;
public onStart(): void {
// Quick initialization
this.setupBasics();
}
private setupBasics(): void {
// Minimal setup
}
public performExpensiveInit(): void {
if (this.initialized) return;
// Expensive operations
task.spawn(() => {
this.loadAssets();
this.buildDataStructures();
this.initialized = true;
});
}
private loadAssets(): void {
// Load assets asynchronously
}
private buildDataStructures(): void {
// Build complex data structures
}
protected getSourceScript(): ModuleScript {
return script as ModuleScript;
}
public willRemove(): void {}
}
Advanced Networking Patterns
Request-Response Pattern
export class DataProvider extends NetworkBehavior {
private dataCache: Map<string, string> = new Map();
@RPC.Method({
endpoints: [RPC.Endpoint.CLIENT_TO_SERVER],
returnMode: RPC.ReturnMode.RETURNS
})
public requestData(key: string): string | undefined {
if (RunService.IsServer()) {
return this.dataCache.get(key);
}
return undefined;
}
// Client usage
public async getData(key: string): Promise<string | undefined> {
// Call RPC and wait for response
return this.requestData(key);
}
protected getSourceScript(): ModuleScript {
return script as ModuleScript;
}
public onStart(): void {}
public willRemove(): void {}
}
Broadcast Pattern
export class ChatSystem extends NetworkBehavior {
@RPC.Method({
endpoints: [RPC.Endpoint.CLIENT_TO_SERVER]
})
public sendMessage(message: string): void {
if (RunService.IsServer()) {
// Validate message
if (message.size() > 200) return;
// Broadcast to all clients
this.broadcastMessage(message);
}
}
@RPC.Method({
endpoints: [RPC.Endpoint.SERVER_TO_CLIENT]
})
private broadcastMessage(message: string): void {
if (RunService.IsClient()) {
print(`Chat: ${message}`);
// Update UI
}
}
protected getSourceScript(): ModuleScript {
return script as ModuleScript;
}
public onStart(): void {}
public willRemove(): void {}
}
Ownership Transfer Pattern
Transfer ownership of NetworkObjects between players:
export class TransferableObject extends NetworkBehavior {
public transferOwnership(newOwner: Player): void {
if (RunService.IsServer()) {
const netObj = this.getNetworkObject();
// Transfer ownership
netObj.changeOwnership(newOwner);
// Notify clients
this.notifyOwnershipChange(newOwner);
}
}
@RPC.Method({
endpoints: [RPC.Endpoint.SERVER_TO_CLIENT]
})
private notifyOwnershipChange(newOwner: Player): void {
if (RunService.IsClient()) {
print(`Ownership transferred to ${newOwner.Name}`);
// Update client-side logic
if (newOwner === Players.LocalPlayer) {
this.enableLocalControl();
} else {
this.disableLocalControl();
}
}
}
private enableLocalControl(): void {
// Enable input handling
}
private disableLocalControl(): void {
// Disable input handling
}
protected getSourceScript(): ModuleScript {
return script as ModuleScript;
}
public onStart(): void {}
public willRemove(): void {}
}
Note: The term “ownership” is used for NetworkObject ownership. “Authority” is reserved for the Unreal system’s authority model (client vs server authority).
CORP includes a Performance namespace with utilities for monitoring and optimization.
import { Performance } from "CORP/shared/utilities/performance";
export class PerformanceMonitor extends Behavior {
public onStart(): void {
// Monitor frame times
const frameTime = Performance.getFrameTime();
print(`Current frame time: ${frameTime}ms`);
// Check if performance is degraded
if (Performance.isPerformanceDegraded()) {
warn("Performance is degraded!");
this.reduceQuality();
}
// Measure operation performance
const startTime = Performance.now();
this.expensiveOperation();
const duration = Performance.now() - startTime;
print(`Operation took ${duration}ms`);
}
private expensiveOperation(): void {
// Expensive code
}
private reduceQuality(): void {
// Reduce graphics quality, particle effects, etc.
}
protected getSourceScript(): ModuleScript {
return script as ModuleScript;
}
public willRemove(): void {}
}
Debugging and Logging
Debug Utilities
CORP includes a debug system with log groups:
import { Debug, LogGroup } from "CORP/shared/utilities/logging";
const MyLogGroup: LogGroup = {
prefix: "MY_SYSTEM",
enabled: true,
parent: undefined // Optional parent group
};
export class DebuggableComponent extends Behavior {
public onStart(): void {
Debug.log(MyLogGroup, "Component started");
Debug.info(MyLogGroup, "Info message");
Debug.warning(MyLogGroup, "Warning message");
Debug.except(MyLogGroup, "Error message");
Debug.special(MyLogGroup, "Special message");
}
protected getSourceScript(): ModuleScript {
return script as ModuleScript;
}
public willRemove(): void {}
}
Network Debug Logging
const NetworkLogGroup: LogGroup = {
prefix: "NETWORK",
enabled: true
};
export class NetworkDebugger extends NetworkBehavior {
public onStart(): void {
const netObj = this.getNetworkObject();
netObj.ownerChanged.connect((owner) => {
Debug.log(NetworkLogGroup, `Ownership changed to ${owner?.Name ?? "server"}`);
});
this.health.onValueChanged.connect((newValue, oldValue) => {
Debug.log(NetworkLogGroup, `Health: ${oldValue} -> ${newValue}`);
});
}
public readonly health = new NetworkedVariable<number>(100);
protected getSourceScript(): ModuleScript {
return script as ModuleScript;
}
public willRemove(): void {}
}
export class ProfiledComponent extends Behavior {
public onStart(): void {
debug.profilebegin("MyExpensiveOperation");
this.expensiveOperation();
debug.profileend();
}
private expensiveOperation(): void {
// Expensive code
// Shows up in Roblox MicroProfiler
}
protected getSourceScript(): ModuleScript {
return script as ModuleScript;
}
public willRemove(): void {}
}
Best Practices Summary
1. Use Appropriate Replication
- Batched for frequent updates (position, rotation)
- Instantaneous only for critical, infrequent updates
2. Optimize Component Lifecycle
- Use
executionOrder for dependencies
- Defer expensive initialization
- Clean up resources in
willRemove()
3. Design for Network Authority
- Server validates all inputs
- Clients predict locally
- Server corrects client state when needed
4. Pool Network Objects
- Pre-spawn frequently used objects
- Reuse instead of destroying
- Reduces network overhead
5. Profile and Debug
- Use log groups for debugging
- Profile expensive operations
- Monitor network bandwidth
Next Steps
Master advanced CORP features to build sophisticated games! 🎯