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
- Automatic Dependency Resolution: Required components are added automatically
- Type Safety: You can safely use non-null assertions (
!)
- Documentation: Makes dependencies explicit in code
- 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 [];
}
@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
// ✓ 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! 🎯