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[]
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! 🎮