Skip to main content
Reading time: ~35 minutes
This guide provides practical, real-world examples of building game systems with CORP.

Table of Contents

Simple Player Controller

A basic player controller with movement and jumping.

PlayerController Component

import { Behavior } from "CORP/shared/componentization/behavior";
import { GameObject } from "CORP/shared/componentization/game-object";
import { SerializeField } from "CORP/shared/serialization/serialize-field";
import { UserInputService, RunService } from "@rbxts/services";

export class PlayerController extends Behavior {
    @SerializeField
    public walkSpeed: number = 16;

    @SerializeField
    public jumpPower: number = 50;

    private humanoid: Humanoid | undefined;

    // Note: Avoid implementing constructors in component classes unless absolutely necessary.
    // CORP may update component constructors in future versions, and custom constructors
    // make upgrades more difficult. Use onStart() for initialization instead.

    public onStart(): void {
        // Find humanoid in the instance
        const instance = this.getInstance();
        this.humanoid = instance.FindFirstChildOfClass("Humanoid");

        if (!this.humanoid) {
            warn("PlayerController requires a Humanoid!");
            return;
        }

        // Setup controls
        this.setupMovement();
        this.setupJump();
    }

    private setupMovement(): void {
        if (!this.humanoid) return;

        this.humanoid.WalkSpeed = this.walkSpeed;

        // Listen for input
        const connection = RunService.Heartbeat.Connect(() => {
            this.handleMovement();
        });

        this.collector.add(connection);
    }

    private handleMovement(): void {
        // Get camera direction
        const camera = game.Workspace.CurrentCamera;
        if (!camera || !this.humanoid) return;

        const moveDirection = new Vector3(0, 0, 0);

        // Check WASD keys
        if (UserInputService.IsKeyDown(Enum.KeyCode.W)) {
            moveDirection.add(camera.CFrame.LookVector);
        }
        if (UserInputService.IsKeyDown(Enum.KeyCode.S)) {
            moveDirection.sub(camera.CFrame.LookVector);
        }
        if (UserInputService.IsKeyDown(Enum.KeyCode.A)) {
            moveDirection.sub(camera.CFrame.RightVector);
        }
        if (UserInputService.IsKeyDown(Enum.KeyCode.D)) {
            moveDirection.add(camera.CFrame.RightVector);
        }

        // Move humanoid
        this.humanoid.Move(moveDirection);
    }

    private setupJump(): void {
        if (!this.humanoid) return;

        this.humanoid.JumpPower = this.jumpPower;

        const connection = UserInputService.InputBegan.Connect((input, processed) => {
            if (processed) return;
            
            if (input.KeyCode === Enum.KeyCode.Space) {
                this.humanoid?.ChangeState(Enum.HumanoidStateType.Jumping);
            }
        });

        this.collector.add(connection);
    }

    public willRemove(): void {
        // Collector handles cleanup
    }

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

Scene Setup

const PlayerScene: SceneSerialization.SceneDescription = {
    children: [
        {
            name: "Player",
            transform: {
                position: { x: 0, y: 5, z: 0 },
                rotation: { x: 0, y: 0, z: 0 }
            },
            components: [
                {
                    path: ["src", "shared", "components", "PlayerController"],
                    data: {
                        walkSpeed: 16,
                        jumpPower: 50
                    }
                }
            ],
            children: []
        }
    ]
};

Health System

A complete health system with damage, healing, and death.

HealthSystem Component

import { NetworkBehavior } from "CORP/shared/networking/network-behavior";
import { GameObject } from "CORP/shared/componentization/game-object";
import { NetworkedVariable } from "CORP/shared/observables/networked-observables/networked-variable";
import { SerializeField } from "CORP/shared/serialization/serialize-field";
import { RPC } from "CORP/shared/networking/RPC";
import { Signal } from "CORP/shared/utilities/signal";
import RequiresComponent from "CORP/shared/componentization/decorators/requires-component";
import { NetworkObject } from "CORP/shared/networking/network-object";

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

    public readonly health = new NetworkedVariable<number>(100);
    public readonly died = new Signal<[]>("died");
    public readonly damaged = new Signal<[number]>("damaged");
    public readonly healed = new Signal<[number]>("healed");

    public onStart(): void {
        // Initialize health
        if (RunService.IsServer()) {
            this.health.setValue(this.maxHealth);
        }

        // Listen for health changes
        this.health.onValueChanged.connect((newHealth, oldHealth) => {
            if (newHealth < oldHealth) {
                this.damaged.fire(oldHealth - newHealth);
            } else if (newHealth > oldHealth) {
                this.healed.fire(newHealth - oldHealth);
            }

            if (newHealth <= 0 && oldHealth > 0) {
                this.onDeath();
            }
        });

        // Visual feedback on client
        if (RunService.IsClient()) {
            this.setupHealthUI();
        }
    }

    @RPC.Method({
        endpoints: [RPC.Endpoint.CLIENT_TO_SERVER],
        accessPolicy: RPC.AccessPolicy.ANYONE
    })
    public takeDamage(amount: number): void {
        if (RunService.IsServer()) {
            // Validate damage
            if (amount < 0 || amount > 1000) return;

            const current = this.health.getValue();
            const newHealth = math.max(0, current - amount);
            this.health.setValue(newHealth);

            print(`${this.getName()} took ${amount} damage. Health: ${newHealth}/${this.maxHealth}`);
        }
    }

    @RPC.Method({
        endpoints: [RPC.Endpoint.CLIENT_TO_SERVER, RPC.Endpoint.SERVER_LOCALLY]
    })
    public heal(amount: number): void {
        if (RunService.IsServer()) {
            // Validate healing
            if (amount < 0 || amount > 1000) return;

            const current = this.health.getValue();
            const newHealth = math.min(this.maxHealth, current + amount);
            this.health.setValue(newHealth);

            print(`${this.getName()} healed ${amount}. Health: ${newHealth}/${this.maxHealth}`);
        }
    }

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

        if (RunService.IsServer()) {
            // Respawn after delay
            task.wait(3);
            this.respawn();
        }
    }

    private respawn(): void {
        if (RunService.IsServer()) {
            this.health.setValue(this.maxHealth);
            print(`${this.getName()} respawned!`);
        }
    }

    private setupHealthUI(): void {
        // Create health bar UI
        this.health.onValueChanged.connect((newHealth) => {
            const percentage = (newHealth / this.maxHealth) * 100;
            print(`Health: ${percentage}%`);
            // Update UI here
        });
    }

    public willRemove(): void {
        this.died.destroy();
        this.damaged.destroy();
        this.healed.destroy();
    }

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

Weapon System

A flexible weapon system with shooting and reloading.

Weapon Component

import { NetworkBehavior } from "CORP/shared/networking/network-behavior";
import { GameObject } from "CORP/shared/componentization/game-object";
import { NetworkedVariable } from "CORP/shared/observables/networked-observables/networked-variable";
import { SerializeField } from "CORP/shared/serialization/serialize-field";
import { RPC } from "CORP/shared/networking/RPC";
import RequiresComponent from "CORP/shared/componentization/decorators/requires-component";
import { NetworkObject } from "CORP/shared/networking/network-object";
import { HealthSystem } from "./HealthSystem";

@RequiresComponent(NetworkObject)
export class Weapon extends NetworkBehavior {
    @SerializeField
    public damage: number = 25;

    @SerializeField
    public fireRate: number = 0.2; // Seconds between shots

    @SerializeField
    public maxAmmo: number = 30;

    @SerializeField
    public reloadTime: number = 2;

    @SerializeField
    public range: number = 300;

    public readonly ammo = new NetworkedVariable<number>(30);
    public readonly isReloading = new NetworkedVariable<boolean>(false);

    private lastFireTime: number = 0;

    public onStart(): void {
        if (RunService.IsServer()) {
            this.ammo.setValue(this.maxAmmo);
        }

        // Setup client input if we own this
        if (RunService.IsClient() && this.getNetworkObject().getOwner() === Players.LocalPlayer) {
            this.setupInput();
        }
    }

    private setupInput(): void {
        const connection = UserInputService.InputBegan.Connect((input, processed) => {
            if (processed) return;

            if (input.UserInputType === Enum.UserInputType.MouseButton1) {
                this.requestFire();
            } else if (input.KeyCode === Enum.KeyCode.R) {
                this.requestReload();
            }
        });

        this.collector.add(connection);
    }

    private requestFire(): void {
        // Get mouse target
        const mouse = Players.LocalPlayer.GetMouse();
        const target = mouse.Hit.Position;

        this.fire(target);
    }

    @RPC.Method({
        endpoints: [RPC.Endpoint.CLIENT_TO_SERVER],
        accessPolicy: RPC.AccessPolicy.OWNER,
        reliability: RPC.Reliability.RELIABLE
    })
    public fire(targetPosition: Vector3): void {
        if (RunService.IsServer()) {
            // Check fire rate
            const now = tick();
            if (now - this.lastFireTime < this.fireRate) return;

            // Check if reloading
            if (this.isReloading.getValue()) return;

            // Check ammo
            if (this.ammo.getValue() <= 0) {
                this.requestReload();
                return;
            }

            this.lastFireTime = now;

            // Consume ammo
            this.ammo.setValue(this.ammo.getValue() - 1);

            // Raycast
            const origin = this.getPosition();
            const direction = targetPosition.sub(origin).Unit;

            const raycastParams = new RaycastParams();
            raycastParams.FilterType = Enum.RaycastFilterType.Blacklist;
            raycastParams.FilterDescendantsInstances = [this.getInstance()];

            const result = game.Workspace.Raycast(origin, direction.mul(this.range), raycastParams);

            if (result) {
                // Check if hit has HealthSystem
                const hitInstance = result.Instance;
                const hitGameObject = GameObject.instanceMap.get(hitInstance as Actor);

                if (hitGameObject && hitGameObject.hasComponent(HealthSystem)) {
                    const health = hitGameObject.getComponent(HealthSystem)!;
                    health.takeDamage(this.damage);
                }

                // Notify clients of shot
                this.showFireEffect(origin, result.Position);
            } else {
                this.showFireEffect(origin, origin.add(direction.mul(this.range)));
            }
        }
    }

    @RPC.Method({
        endpoints: [RPC.Endpoint.CLIENT_TO_SERVER],
        accessPolicy: RPC.AccessPolicy.OWNER
    })
    public requestReload(): void {
        if (RunService.IsServer()) {
            if (this.isReloading.getValue()) return;
            if (this.ammo.getValue() === this.maxAmmo) return;

            this.isReloading.setValue(true);

            task.spawn(() => {
                task.wait(this.reloadTime);
                this.ammo.setValue(this.maxAmmo);
                this.isReloading.setValue(false);
                print("Reloaded!");
            });
        }
    }

    @RPC.Method({
        endpoints: [RPC.Endpoint.SERVER_TO_CLIENT],
        reliability: RPC.Reliability.UNRELIABLE
    })
    public showFireEffect(origin: Vector3, target: Vector3): void {
        if (RunService.IsClient()) {
            // Create visual effect
            const beam = new Instance("Part");
            beam.Size = new Vector3(0.2, 0.2, origin.sub(target).Magnitude);
            beam.CFrame = new CFrame(origin.add(target).div(2), target);
            beam.Anchored = true;
            beam.CanCollide = false;
            beam.BrickColor = new BrickColor("Bright yellow");
            beam.Parent = game.Workspace;

            // Remove after short delay
            task.wait(0.1);
            beam.Destroy();
        }
    }

    public willRemove(): void {}

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

Enemy AI

A simple AI that chases and attacks the player.

EnemyAI Component

import { NetworkBehavior } from "CORP/shared/networking/network-behavior";
import { GameObject } from "CORP/shared/componentization/game-object";
import { SerializeField } from "CORP/shared/serialization/serialize-field";
import RequiresComponent from "CORP/shared/componentization/decorators/requires-component";
import { NetworkObject } from "CORP/shared/networking/network-object";
import { HealthSystem } from "./HealthSystem";
import { RunService } from "@rbxts/services";

@RequiresComponent(NetworkObject)
@RequiresComponent(HealthSystem)
export class EnemyAI extends NetworkBehavior {
    @SerializeField
    public detectionRange: number = 50;

    @SerializeField
    public attackRange: number = 5;

    @SerializeField
    public attackDamage: number = 10;

    @SerializeField
    public attackCooldown: number = 1.5;

    @SerializeField
    public moveSpeed: number = 10;

    private target: GameObject | undefined;
    private lastAttackTime: number = 0;
    private updateConnection: RBXScriptConnection | undefined;

    public onStart(): void {
        // Only run AI on server
        if (RunService.IsServer()) {
            this.startAI();
        }
    }

    private startAI(): void {
        this.updateConnection = RunService.Heartbeat.Connect(() => {
            this.update();
        });

        this.collector.add(this.updateConnection);
    }

    private update(): void {
        // Find target if we don't have one
        if (!this.target || !this.target.getInstance().Parent) {
            this.target = this.findNearestPlayer();
        }

        if (!this.target) return;

        const targetPos = this.target.getPosition();
        const myPos = this.getPosition();
        const distance = targetPos.sub(myPos).Magnitude;

        if (distance > this.detectionRange) {
            // Target too far, give up
            this.target = undefined;
            return;
        }

        if (distance > this.attackRange) {
            // Move towards target
            this.moveTowards(targetPos);
        } else {
            // In attack range
            this.attackTarget();
        }
    }

    private findNearestPlayer(): GameObject | undefined {
        const myPos = this.getPosition();
        let nearest: GameObject | undefined;
        let nearestDist = this.detectionRange;

        // Find all GameObjects with "Player" tag or similar
        // This is a simple implementation
        for (const [instance, gameObject] of GameObject.instanceMap) {
            if (gameObject.getName().match("Player")[0]) {
                const dist = gameObject.getPosition().sub(myPos).Magnitude;
                if (dist < nearestDist) {
                    nearest = gameObject;
                    nearestDist = dist;
                }
            }
        }

        return nearest;
    }

    private moveTowards(targetPos: Vector3): void {
        const myPos = this.getPosition();
        const direction = targetPos.sub(myPos).Unit;

        // Simple movement (you'd want proper pathfinding here)
        const newPos = myPos.add(direction.mul(this.moveSpeed * RunService.Heartbeat.Wait()));
        this.setTransform(new CFrame(newPos));
    }

    private attackTarget(): void {
        if (!this.target) return;

        const now = tick();
        if (now - this.lastAttackTime < this.attackCooldown) return;

        this.lastAttackTime = now;

        // Deal damage
        const health = this.target.getComponent(HealthSystem);
        if (health) {
            health.takeDamage(this.attackDamage);
            print(`Enemy attacked for ${this.attackDamage} damage!`);
        }
    }

    public willRemove(): void {}

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

Pickup Items

A collectable item system.

Pickup Component

import { NetworkBehavior } from "CORP/shared/networking/network-behavior";
import { GameObject } from "CORP/shared/componentization/game-object";
import { SerializeField } from "CORP/shared/serialization/serialize-field";
import { RPC } from "CORP/shared/networking/RPC";
import RequiresComponent from "CORP/shared/componentization/decorators/requires-component";
import { NetworkObject } from "CORP/shared/networking/network-object";
import { HealthSystem } from "./HealthSystem";

@RequiresComponent(NetworkObject)
export class HealthPickup extends NetworkBehavior {
    @SerializeField
    public healAmount: number = 25;

    @SerializeField
    public respawnTime: number = 30;

    @SerializeField
    public rotationSpeed: number = 45; // Degrees per second

    private isAvailable: boolean = true;
    private touchConnection: RBXScriptConnection | undefined;

    public onStart(): void {
        if (RunService.IsServer()) {
            this.setupTouchDetection();
        }

        if (RunService.IsClient()) {
            this.setupVisuals();
        }
    }

    private setupTouchDetection(): void {
        const instance = this.getInstance();
        const part = instance.FindFirstChildOfClass("BasePart") as BasePart;

        if (!part) {
            warn("HealthPickup requires a BasePart child!");
            return;
        }

        this.touchConnection = part.Touched.Connect((hit) => {
            if (!this.isAvailable) return;

            // Check if touched by a player
            const touchedGameObject = GameObject.instanceMap.get(hit.Parent as Actor);
            if (!touchedGameObject) return;

            const health = touchedGameObject.getComponent(HealthSystem);
            if (!health) return;

            // Heal the player
            this.collect(touchedGameObject);
        });

        this.collector.add(this.touchConnection);
    }

    private collect(collector: GameObject): void {
        if (!this.isAvailable) return;

        const health = collector.getComponent(HealthSystem);
        if (!health) return;

        // Check if at full health
        if (health.health.getValue() >= health.maxHealth) return;

        // Heal
        health.heal(this.healAmount);

        // Make unavailable
        this.setAvailable(false);

        // Respawn after delay
        task.spawn(() => {
            task.wait(this.respawnTime);
            this.setAvailable(true);
        });
    }

    @RPC.Method({
        endpoints: [RPC.Endpoint.SERVER_TO_CLIENT]
    })
    private setAvailable(available: boolean): void {
        this.isAvailable = available;

        // Update visuals
        const instance = this.getInstance();
        const part = instance.FindFirstChildOfClass("BasePart") as BasePart;

        if (part) {
            part.Transparency = available ? 0 : 1;
        }
    }

    private setupVisuals(): void {
        // Rotate the pickup
        const connection = RunService.RenderStepped.Connect((delta) => {
            if (!this.isAvailable) return;

            const current = this.getTransform();
            const rotation = CFrame.Angles(0, math.rad(this.rotationSpeed * delta), 0);
            this.setTransform(current.mul(rotation));
        });

        this.collector.add(connection);
    }

    public willRemove(): void {}

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

Complete Multiplayer Example

A complete multiplayer game scene with players, enemies, and pickups.

Game Scene

const MultiplayerGame: SceneSerialization.SceneDescription = {
    children: [
        // Game Manager
        {
            name: "GameManager",
            components: [
                {
                    path: ["CORP", "shared", "networking", "NetworkObject"],
                    data: {}
                },
                {
                    path: ["src", "shared", "components", "GameManager"],
                    data: {
                        roundDuration: 300,
                        minPlayers: 2
                    }
                }
            ],
            children: []
        },
        // Spawn Points
        {
            name: "SpawnPoints",
            components: [],
            children: [
                {
                    name: "SpawnPoint_1",
                    transform: {
                        position: { x: 0, y: 5, z: 0 },
                        rotation: { x: 0, y: 0, z: 0 }
                    },
                    components: [
                        {
                            path: ["src", "shared", "components", "SpawnPoint"],
                            data: { spawnId: 1 }
                        }
                    ],
                    children: []
                },
                {
                    name: "SpawnPoint_2",
                    transform: {
                        position: { x: 50, y: 5, z: 0 },
                        rotation: { x: 0, y: 180, z: 0 }
                    },
                    components: [
                        {
                            path: ["src", "shared", "components", "SpawnPoint"],
                            data: { spawnId: 2 }
                        }
                    ],
                    children: []
                }
            ]
        },
        // Health Pickups
        {
            name: "Pickups",
            components: [],
            children: [
                {
                    name: "HealthPack_1",
                    transform: {
                        position: { x: 25, y: 5, z: 25 },
                        rotation: { x: 0, y: 0, z: 0 }
                    },
                    components: [
                        {
                            path: ["CORP", "shared", "networking", "NetworkObject"],
                            data: {}
                        },
                        {
                            path: ["src", "shared", "components", "HealthPickup"],
                            data: {
                                healAmount: 25,
                                respawnTime: 30
                            }
                        }
                    ],
                    children: []
                }
            ]
        },
        // Enemies
        {
            name: "Enemies",
            components: [],
            children: [
                {
                    name: "Enemy_1",
                    transform: {
                        position: { x: 30, y: 5, z: 30 },
                        rotation: { x: 0, y: 0, z: 0 }
                    },
                    components: [
                        {
                            path: ["CORP", "shared", "networking", "NetworkObject"],
                            data: {}
                        },
                        {
                            path: ["src", "shared", "components", "HealthSystem"],
                            data: { maxHealth: 50 }
                        },
                        {
                            path: ["src", "shared", "components", "EnemyAI"],
                            data: {
                                detectionRange: 50,
                                attackDamage: 10,
                                moveSpeed: 8
                            }
                        }
                    ],
                    children: []
                }
            ]
        }
    ]
};

export default MultiplayerGame;

Using the Scene

// server/index.server.ts
import { CORP } from "CORP/shared/CORP";
import { Configurations } from "CORP/shared/configurations";
import MultiplayerGame from "../shared/scenes/MultiplayerGame";
import { Players } from "@rbxts/services";

const config: Configurations.GameConfiguration = {
    startingScene: MultiplayerGame
};

CORP.start(config);

// Spawn players when they join
Players.PlayerAdded.Connect((player) => {
    spawnPlayer(player);
});

function spawnPlayer(player: Player): void {
    const character = new GameObject(`Player_${player.Name}`);
    const netObj = character.addComponent(NetworkObject);
    character.addComponent(HealthSystem, { maxHealth: 100 });
    character.addComponent(PlayerController);
    character.addComponent(Weapon);

    // Give ownership to the player
    netObj.changeOwnership(player);
    netObj.spawn();
}

Next Steps


Build amazing games with these practical examples! 🎮