Reading time: ~35 minutes
Table of Contents
- Simple Player Controller
- Health System
- Weapon System
- Enemy AI
- Pickup Items
- Complete Multiplayer Example
Simple Player Controller
A basic player controller with movement and jumping.PlayerController Component
Copy
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
Copy
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
Copy
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
Copy
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
Copy
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
Copy
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
Copy
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
Copy
// 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
- API Reference: Complete API documentation
- Core Concepts: Understand the fundamentals
- Networking: Master multiplayer features
Build amazing games with these practical examples! 🎮