Skip to main content
Reading time: ~25 minutes
CORP provides several powerful decorators to enhance your components with additional functionality and constraints. Decorators are applied using TypeScript’s @decorator syntax.

Table of Contents

@SerializeField

The @SerializeField decorator marks properties for serialization, allowing them to:
  • Be saved and restored with scenes
  • Be configured in scene descriptions
  • Be editable in future editor tools

Usage

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

export class GameSettings extends Behavior {
    @SerializeField
    public playerSpeed: number = 16;

    @SerializeField
    public maxPlayers: number = 10;

    @SerializeField
    public gameDuration: number = 300;

    // Not serialized
    private currentTime: number = 0;

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

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

Supported Types

@SerializeField supports the following types:
export class AllTypes extends Behavior {
    // Primitives
    @SerializeField
    public numberValue: number = 42;

    @SerializeField
    public stringValue: string = "hello";

    @SerializeField
    public booleanValue: boolean = true;

    // Roblox types
    @SerializeField
    public position: Vector3 = new Vector3(0, 0, 0);

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

    // Plain objects/tables
    @SerializeField
    public config: { [key: string]: number } = {
        health: 100,
        armor: 50
    };

    // Arrays
    @SerializeField
    public items: string[] = ["sword", "shield"];

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

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

Unsupported Types

The following cannot be serialized:
// ✗ Functions
@SerializeField
public callback: () => void = () => {}; // Error!

// ✗ Instances
@SerializeField
public part: Part; // Error!

// ✗ Components
@SerializeField
public otherComponent: OtherComponent; // Error!

// ✗ Complex objects with metatables
@SerializeField
public signal: Signal; // Error!

Scene Configuration

Serialized fields can be set in scene descriptions:
// Scene definition
const scene: SceneSerialization.SceneDescription = {
    children: [
        {
            name: "GameSettings",
            components: [
                {
                    path: ["src", "shared", "components", "GameSettings"],
                    data: {
                        // These values override the defaults
                        playerSpeed: 20,
                        maxPlayers: 16,
                        gameDuration: 600
                    }
                }
            ],
            children: []
        }
    ]
};

Getting Serialized State

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

    @SerializeField
    public activated: boolean = false;

    public save(): void {
        // Get all serialized fields as an object
        const state = this.serializeState();
        print(state); // { checkpointId: 1, activated: false }
        
        // Save to data store or similar
    }

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

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

@RequiresComponent

The @RequiresComponent decorator enforces that a component’s dependencies are present on the GameObject. If the required component is missing, it will be automatically added.

Usage

import RequiresComponent from "CORP/shared/componentization/decorators/requires-component";
import { NetworkObject } from "CORP/shared/networking/network-object";
import { NetworkBehavior } from "CORP/shared/networking/network-behavior";

@RequiresComponent(NetworkObject)
export class PlayerSync extends NetworkBehavior {
    public onStart(): void {
        // NetworkObject is guaranteed to exist
        const netObj = this.getComponent(NetworkObject)!;
        print(`Network ID: ${netObj.getId()}`);
    }

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

    public willRemove(): void {}
}

Multiple Requirements

You can stack multiple @RequiresComponent decorators:
import RequiresComponent from "CORP/shared/componentization/decorators/requires-component";

@RequiresComponent(Rigidbody)
@RequiresComponent(Collider)
export class PhysicsController extends Behavior {
    public onStart(): void {
        // Both Rigidbody and Collider are guaranteed
        const rb = this.getComponent(Rigidbody)!;
        const col = this.getComponent(Collider)!;
    }

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

    public willRemove(): void {}
}

Common Use Cases

Network Components

@RequiresComponent(NetworkObject)
export class NetworkedHealth extends NetworkBehavior {
    public readonly health = new NetworkedVariable<number>(100);
    
    // ... implementation
}

Physics Dependencies

@RequiresComponent(PhysicsBody)
export class Movement extends Behavior {
    private body!: PhysicsBody;

    public onStart(): void {
        this.body = this.getComponent(PhysicsBody)!;
    }

    // ... implementation
}

Component Chains

// Transform requires a base component
@RequiresComponent(BaseTransform)
export class AdvancedTransform extends Behavior {
    // ... implementation
}

// Character requires both Transform and Health
@RequiresComponent(AdvancedTransform)
@RequiresComponent(HealthSystem)
export class CharacterController extends Behavior {
    // ... implementation
}

Benefits

  1. Automatic Dependency Resolution: Required components are added automatically
  2. Type Safety: You can safely use non-null assertions (!)
  3. Documentation: Makes dependencies explicit in code
  4. Prevention: Avoids runtime errors from missing components
Note: Components are disallowed from having multiple instances by default in CORP. The @DisallowMultipleComponents decorator is reserved for future use.

@RPC.Method

The @RPC.Method decorator marks methods as Remote Procedure Calls, allowing them to be invoked across the network. See the Networking documentation for complete details.

Basic Usage

import { NetworkBehavior } from "CORP/shared/networking/network-behavior";
import { RPC } from "CORP/shared/networking/RPC";

export class PlayerActions extends NetworkBehavior {
    @RPC.Method({
        endpoints: [RPC.Endpoint.CLIENT_TO_SERVER],
        accessPolicy: RPC.AccessPolicy.OWNER
    })
    public jump(): void {
        if (RunService.IsServer()) {
            print("Player jumped!");
            // Apply jump force
        }
    }

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

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

Configuration Options

interface RPCConfig {
    // Where the RPC can be called from/to
    endpoints: RPC.Endpoint[];
    
    // Who can call the RPC
    accessPolicy?: RPC.AccessPolicy;
    
    // Whether it returns a value
    returnMode?: RPC.ReturnMode;
    
    // Transmission reliability
    reliability?: RPC.Reliability;
}

Examples by Use Case

Client Request to Server

@RPC.Method({
    endpoints: [RPC.Endpoint.CLIENT_TO_SERVER],
    accessPolicy: RPC.AccessPolicy.OWNER
})
public requestPickup(itemId: string): void {
    if (RunService.IsServer()) {
        // Server validates and processes
        this.giveItem(itemId);
    }
}

Server Notification to Clients

@RPC.Method({
    endpoints: [RPC.Endpoint.SERVER_TO_CLIENT],
    reliability: RPC.Reliability.UNRELIABLE
})
public updatePosition(position: Vector3): void {
    if (RunService.IsClient()) {
        // Update visual position
        this.setTransform(new CFrame(position));
    }
}

RPC with Return Value

@RPC.Method({
    endpoints: [RPC.Endpoint.CLIENT_TO_SERVER],
    returnMode: RPC.ReturnMode.RETURNS,
    accessPolicy: RPC.AccessPolicy.OWNER
})
public getInventory(): string[] {
    if (RunService.IsServer()) {
        return this.inventory;
    }
    return [];
}

Unreliable for Performance

@RPC.Method({
    endpoints: [RPC.Endpoint.CLIENT_TO_SERVER],
    reliability: RPC.Reliability.UNRELIABLE,
    accessPolicy: RPC.AccessPolicy.OWNER
})
public sendPosition(pos: Vector3): void {
    // Sent frequently, ok to drop some packets
}

Combining Decorators

Decorators can be combined for powerful component definitions:

Network Component with Dependencies

import RequiresComponent from "CORP/shared/componentization/decorators/requires-component";
import DisallowMultipleComponents from "CORP/shared/componentization/decorators/disallow-multiple-components";
import { SerializeField } from "CORP/shared/serialization/serialize-field";
import { NetworkObject } from "CORP/shared/networking/network-object";
import { NetworkBehavior } from "CORP/shared/networking/network-behavior";
import { RPC } from "CORP/shared/networking/RPC";

@DisallowMultipleComponents
@RequiresComponent(NetworkObject)
export class CombatSystem extends NetworkBehavior {
    @SerializeField
    public maxHealth: number = 100;

    @SerializeField
    public attackDamage: number = 25;

    private currentHealth: number = 100;

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

    @RPC.Method({
        endpoints: [RPC.Endpoint.CLIENT_TO_SERVER],
        accessPolicy: RPC.AccessPolicy.OWNER
    })
    public attack(targetId: string): void {
        if (RunService.IsServer()) {
            // Process attack
            print(`Attacking ${targetId} for ${this.attackDamage} damage`);
        }
    }

    @RPC.Method({
        endpoints: [RPC.Endpoint.SERVER_TO_CLIENT]
    })
    public takeDamage(amount: number): void {
        if (RunService.IsClient()) {
            // Show damage effect
            print(`Took ${amount} damage!`);
        }
    }

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

    public willRemove(): void {}
}

Complex Character Controller

@DisallowMultipleComponents
@RequiresComponent(NetworkObject)
@RequiresComponent(CharacterModel)
@RequiresComponent(AnimationController)
export class AdvancedCharacterController extends NetworkBehavior {
    @SerializeField
    public walkSpeed: number = 16;

    @SerializeField
    public runSpeed: number = 24;

    @SerializeField
    public jumpPower: number = 50;

    private model!: CharacterModel;
    private animator!: AnimationController;

    public onStart(): void {
        this.model = this.getComponent(CharacterModel)!;
        this.animator = this.getComponent(AnimationController)!;

        this.setupControls();
    }

    @RPC.Method({
        endpoints: [RPC.Endpoint.CLIENT_TO_SERVER],
        accessPolicy: RPC.AccessPolicy.OWNER,
        reliability: RPC.Reliability.UNRELIABLE
    })
    public move(direction: Vector3): void {
        if (RunService.IsServer()) {
            // Server validates and applies movement
            this.applyMovement(direction);
        }
    }

    @RPC.Method({
        endpoints: [RPC.Endpoint.CLIENT_TO_SERVER],
        accessPolicy: RPC.AccessPolicy.OWNER
    })
    public jump(): void {
        if (RunService.IsServer()) {
            this.applyJump();
        }
    }

    private setupControls(): void {
        if (this.getNetworkObject().getOwner() === Players.LocalPlayer) {
            // Setup input handling
        }
    }

    private applyMovement(direction: Vector3): void {
        // Movement logic
    }

    private applyJump(): void {
        // Jump logic
    }

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

    public willRemove(): void {}
}

Best Practices

1. Always Use @RequiresComponent for Dependencies

// ✓ Good - explicit dependencies
@RequiresComponent(NetworkObject)
export class MyNetworkComponent extends NetworkBehavior {}

// ✗ Bad - implicit dependencies, may fail at runtime
export class MyNetworkComponent extends NetworkBehavior {
    public onStart(): void {
        const netObj = this.getComponent(NetworkObject); // May be undefined!
    }
}

2. Serialize Configuration, Not State

// ✓ Good - serialize configuration
@SerializeField
public maxHealth: number = 100;

private currentHealth: number = 100; // Runtime state

// ✗ Avoid - serializing runtime state can cause issues
@SerializeField
public currentHealth: number = 100;

3. Use @DisallowMultipleComponents for Singletons

// ✓ Good - prevents duplicates
@DisallowMultipleComponents
export class PlayerController extends Behavior {}

// For components that can have multiples
export class Weapon extends Behavior {} // Can have multiple weapons

4. Configure RPCs Appropriately

// ✓ Good - appropriate reliability for use case
@RPC.Method({
    endpoints: [RPC.Endpoint.CLIENT_TO_SERVER],
    reliability: RPC.Reliability.UNRELIABLE // OK for frequent updates
})
public sendPosition(pos: Vector3): void {}

@RPC.Method({
    endpoints: [RPC.Endpoint.CLIENT_TO_SERVER],
    reliability: RPC.Reliability.RELIABLE // Important for critical actions
})
public purchaseItem(itemId: string): void {}

4. Document Decorator Choices

/**
 * Handles player combat actions.
 * Requires NetworkObject for replication.
 */
@RequiresComponent(NetworkObject)
export class CombatHandler extends NetworkBehavior {
    // ... implementation
}

Next Steps


Use decorators to create robust, well-structured components! 🎯