Skip to main content
Reading time: ~25 minutes
This guide covers the fundamental building blocks of CORP: GameObjects, Components, and Behaviors. Understanding these concepts is essential for developing games with the framework.

GameObjects

GameObjects are the fundamental entities in your game world. Like Unity, every object in your game is represented by a GameObject. They act as containers for Components and form hierarchies through parent-child relationships.

Creating GameObjects

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

// Create a new GameObject
const player = new GameObject("Player");

// Create with a specific Roblox Instance
const actor = new Instance("Actor");
const npc = GameObject.fromInstance(actor, "NPC");

GameObject Hierarchy

GameObjects can be organized in parent-child hierarchies:
const parent = new GameObject("Parent");
const child = new GameObject("Child");

// Set parent
child.setParent(parent);

// Get parent
const parentObj = child.getParent(); // Returns GameObject | Scene | undefined

// Get children
const children = parent.getChildren(); // Returns GameObject[]

// Get all descendants
const descendants = parent.getDescendants(); // Returns GameObject[]

GameObject Transform

Each GameObject has a transform (CFrame) that defines its position and rotation:
// Get transform
const transform = gameObject.getTransform();

// Set transform
gameObject.setTransform(new CFrame(0, 10, 0));

// Get position
const position = gameObject.getPosition();

// Get the underlying Roblox Instance
const instance = gameObject.getInstance();

GameObject Lifecycle

const gameObject = new GameObject("MyObject");

// Add components
gameObject.addComponent(MyComponent);

// Get components
const component = gameObject.getComponent(MyComponent);

// Check if has component
if (gameObject.hasComponent(MyComponent)) {
    // Do something
}

// Remove a component
gameObject.removeComponent(MyComponent);

// Destroy the GameObject (removes all components and cleans up)
gameObject.destroy();

GameObject Events

GameObjects fire events during their lifecycle:
// Fired when a component is added
gameObject.componentAdded.connect((component) => {
    print(`Component added: ${component}`);
});

// Fired when a component is about to be removed
gameObject.componentRemoving.connect((component) => {
    print(`Component removing: ${component}`);
});

// Fired when a component spawns (for NetworkBehaviors)
gameObject.componentSpawned.connect((component) => {
    print(`Component spawned: ${component}`);
});

// Fired when GameObject is being removed
gameObject.removing.connect(() => {
    print("GameObject is being destroyed!");
});

// Fired when parent changes
gameObject.ancestryChanged.connect((gameObject, newParent) => {
    print(`Ancestry changed to: ${newParent}`);
});

Components

Components define the behavior and data of GameObjects. All functionality in CORP is built using Components.

The Component Base Class

The Component class is the abstract base for all components:
import { Component } from "CORP/shared/componentization/component";
import { GameObject } from "CORP/shared/componentization/game-object";

export class MyComponent extends Component {
    // Execution order (lower numbers run first)
    public readonly executionOrder = 0;

    public constructor(gameObject: GameObject) {
        super(gameObject);
    }

    // Required lifecycle methods
    public onStart(): void {
        // Initialization logic
    }

    public willRemove(): void {
        // Cleanup logic
    }

    // Required: return the ModuleScript that defines this component
    protected getSourceScript(): ModuleScript {
        return script as ModuleScript;
    }
}

Component Lifecycle

Components follow a strict lifecycle:

1. Construction

public constructor(gameObject: GameObject) {
    super(gameObject);
    // Component is instantiated
    // Don't access other components here!
}

2. willStart() (Optional)

public willStart(): void {
    // Called before onStart
    // Use for pre-initialization setup
}

3. initialize()

public initialize(): void {
    // Called internally by CORP
    // Calls onStart() by default
}

4. onStart() (Required)

public onStart(): void {
    // Main initialization
    // Safe to access other components here
    const other = this.getComponent(OtherComponent);
}

5. onPropertiesApplied() (Optional)

public onPropertiesApplied(): void {
    // Called after properties are set via serialization
    // Use to react to deserialized data
}

6. willRemove() (Required)

public willRemove(): void {
    // Called before component is destroyed
    // Clean up connections, instances, etc.
}

7. remove()

// Call to destroy the component
component.remove();

Accessing Other Components

export class PlayerController extends Behavior {
    private health: HealthComponent | undefined;

    public onStart(): void {
        // Get a component on the same GameObject
        this.health = this.getComponent(HealthComponent);

        // Get a component by string path
        const comp = this.getComponent("path.to.Component");

        // Check if component exists
        if (this.gameObject.hasComponent(HealthComponent)) {
            // Do something
        }
    }

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

    public willRemove(): void {}
}

Component Shortcuts

Components provide convenient shortcuts for common operations:
export class MyComponent extends Behavior {
    public onStart(): void {
        // Get transform
        const transform = this.getTransform();

        // Set transform
        this.setTransform(new CFrame(0, 10, 0));

        // Get position
        const pos = this.getPosition();

        // Get GameObject name
        const name = this.getName();

        // Get underlying Roblox Instance
        const instance = this.getInstance();

        // Get another component
        const other = this.getComponent(OtherComponent);

        // Destroy the entire GameObject
        this.destroy();
    }

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

    public willRemove(): void {}
}

Execution Order

Control the order in which components are initialized:
export class FirstComponent extends Behavior {
    public readonly executionOrder = 0; // Runs first

    // ... rest of component
}

export class SecondComponent extends Behavior {
    public readonly executionOrder = 10; // Runs second

    // ... rest of component
}

Behaviors

Behavior is a specialized subclass of Component that adds serialization support. Most of your components should extend Behavior rather than Component.

Why Use Behavior?

  • Serialization: Automatically serialize component state
  • State Management: Track and restore component data
  • Editor Support: Enable properties to be edited in the future editor

Creating a Behavior

import { Behavior } from "CORP/shared/componentization/behavior";
import { GameObject } from "CORP/shared/componentization/game-object";
import { SerializeField } from "CORP/shared/serialization/serialize-field";

export class PlayerStats extends Behavior {
    @SerializeField
    public maxHealth: number = 100;

    @SerializeField
    public speed: number = 16;

    private currentHealth: number = 100;

    public constructor(gameObject: GameObject) {
        super(gameObject);
    }

    public onStart(): void {
        this.currentHealth = this.maxHealth;
    }

    public takeDamage(amount: number): void {
        this.currentHealth = math.max(0, this.currentHealth - amount);
        
        if (this.currentHealth === 0) {
            this.onDeath();
        }
    }

    private onDeath(): void {
        print(`${this.getName()} has died!`);
        this.destroy();
    }

    public willRemove(): void {
        // Cleanup
    }

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

Serializable Properties

Use the @SerializeField decorator to mark properties for serialization:
import { SerializeField } from "CORP/shared/serialization/serialize-field";

export class ItemPickup extends Behavior {
    @SerializeField
    public itemName: string = "Health Potion";

    @SerializeField
    public value: number = 50;

    @SerializeField
    public respawnTime: number = 30;

    // This won't be serialized
    private timeSincePickup: number = 0;

    // ... rest of component
}

Serialization Support

Behavior supports serializing various types:
export class ComplexComponent extends Behavior {
    @SerializeField
    public position: Vector3 = new Vector3(0, 0, 0); // ✓ Supported

    @SerializeField
    public color: Color3 = new Color3(1, 1, 1); // ✓ Supported

    @SerializeField
    public enabled: boolean = true; // ✓ Supported

    @SerializeField
    public count: number = 0; // ✓ Supported

    @SerializeField
    public name: string = "Default"; // ✓ Supported

    @SerializeField
    public config: { [key: string]: number } = {}; // ✓ Supported (plain tables)

    // Note: Functions, threads, and userdata cannot be serialized
}

State Serialization

export class Checkpoint extends Behavior {
    @SerializeField
    public checkpointId: number = 1;

    @SerializeField
    public position: Vector3 = new Vector3(0, 5, 0);

    public saveState(): void {
        // Get serialized state
        const state = this.serializeState();
        print("Saved state:", state);
    }

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

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

Garbage Collection

CORP provides a Collector utility for managing resources:
import { Behavior } from "CORP/shared/componentization/behavior";
import { GameObject } from "CORP/shared/componentization/game-object";

export class ConnectionManager extends Behavior {
    // Collector is available in all Behaviors
    public onStart(): void {
        // Add connections to be cleaned up automatically
        const connection = someSignal.connect(() => {});
        this.collector.add(connection);

        // Add instances to be destroyed
        const part = new Instance("Part");
        this.collector.add(part);

        // Add custom cleanup functions
        this.collector.add(() => {
            print("Custom cleanup!");
        });
    }

    public willRemove(): void {
        // Collector.teardown() is called automatically
        // All added connections and instances are cleaned up
    }

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

Component Configuration

When adding components, you can configure their initial properties:
import { GameObject } from "CORP/shared/componentization/game-object";

const player = new GameObject("Player");

// Add component with configuration
player.addComponent(PlayerStats, {
    maxHealth: 150,
    speed: 20
});

// The component will have these values when onStart() is called

Component Dependencies

Use the @RequiresComponent decorator to enforce dependencies:
import RequiresComponent from "CORP/shared/componentization/decorators/requires-component";
import { NetworkObject } from "CORP/shared/networking/network-object";

@RequiresComponent(NetworkObject)
export class NetworkedPlayerController extends NetworkBehavior {
    public onStart(): void {
        // NetworkObject is guaranteed to exist
        const netObj = this.getComponent(NetworkObject)!;
    }

    // ... rest of component
}

Best Practices

1. Use Behaviors for Game Logic

// ✓ Good
export class GameLogic extends Behavior { }

// ✗ Avoid (unless you need the base Component)
export class GameLogic extends Component { }

2. Initialize in onStart(), Not Constructor

// ✓ Good
export class MyComponent extends Behavior {
    private other: OtherComponent | undefined;

    public onStart(): void {
        this.other = this.getComponent(OtherComponent);
    }
}

// ✗ Bad (other components may not exist yet)
export class MyComponent extends Behavior {
    private other: OtherComponent;

    public constructor(gameObject: GameObject) {
        super(gameObject);
        this.other = this.getComponent(OtherComponent)!; // May be undefined!
    }
}

3. Clean Up Resources

export class ResourceManager extends Behavior {
    public onStart(): void {
        const connection = myEvent.connect(() => {});
        this.collector.add(connection); // Will be cleaned up automatically
    }

    public willRemove(): void {
        // collector.teardown() is called automatically
    }
}

4. Use Execution Order When Needed

// Initialize game manager first
export class GameManager extends Behavior {
    public readonly executionOrder = -100;
}

// Initialize player after game manager
export class PlayerController extends Behavior {
    public readonly executionOrder = 0;
}

5. Mark Serializable Fields

export class Configuration extends Behavior {
    @SerializeField
    public playerSpeed: number = 16; // Can be edited and saved

    private runtime: number = 0; // Won't be saved
}

Next Steps


Understanding these core concepts will enable you to build complex games with CORP’s component-based architecture! 🎮