Skip to main content
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:
  1. addComponent(NetworkObject) is called
  2. NetworkObject automatically spawns and generates a unique network ID
  3. GameObject is replicated to all clients
  4. All NetworkBehaviors are registered
  5. Clients reconstruct the GameObject and components
  6. 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 {}
}

Performance Optimization

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).

Performance Namespace

CORP includes a Performance namespace with utilities for monitoring and optimization.

Using Performance Utilities

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 {}
}

Performance Profiling

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! 🎯