Reading time: ~30 minutes
Scenes in CORP organize your game world into manageable units. They can be defined using the Roblox instance hierarchy (recommended) or programmatically via TypeScript/JSON.
Table of Contents
What are Scenes?
A scene is a container for GameObjects and represents a level, menu, or any other distinct state in your game. The recommended approach is to define scenes using Roblox Studio’s instance hierarchy, though programmatic definition is also supported.
Key Features
- Roblox Hierarchy: Build scenes directly in Studio using Actors and Attributes (recommended)
- Declarative Structure: Define your game world in code or JSON
- Serialization: Save and load scenes
- Hierarchy Support: Organize GameObjects in parent-child relationships
- Component Configuration: Pre-configure component properties
- Runtime Flexibility: Load scenes dynamically
Scene from Roblox Hierarchy
The primary and recommended way to create scenes is using Roblox Studio’s instance hierarchy. CORP can parse Actor instances in the workspace and convert them to GameObjects with components.
Setting Up a Scene in Studio
- Create Actors: Each Actor in Workspace becomes a GameObject
- Add Attributes: Configure component properties using Roblox attributes
- Use Folders: Organize your scene structure
- Add Components: Use ModuleScripts or StringValues to specify components
Example Studio Structure
Workspace/
├── GameScene (Folder)
│ ├── Player (Actor)
│ │ ├── $network_object (ModuleScript or Attribute)
│ │ └── player_controller (ModuleScript or Attribute)
│ ├── Enemy (Actor)
│ │ ├── $network_object
│ │ └── enemy_ai (ModuleScript)
│ │ └── attributes: attackDamage, moveSpeed
│ └── SpawnPoints (Folder)
│ ├── SpawnPoint1 (Actor)
│ └── SpawnPoint2 (Actor)
Loading from Workspace
import { CORP } from "CORP/shared/CORP";
import { Workspace } from "@rbxts/services";
// Start CORP from workspace hierarchy
// This loads and parses all Actors in Workspace
CORP.startFromWorkspace();
Or start from a specific folder:
import { SceneManager } from "CORP/shared/scenes/scene-manager";
// Get a specific scene folder
const sceneFolder = Workspace.FindFirstChild("GameScene") as Folder;
// Load scene from that folder
if (sceneFolder) {
SceneManager.currentScene?.startFromWorkspace(sceneFolder);
}
Component Mapping in Hierarchy
Components are specified using attributes or child instances:
// In Studio, on an Actor:
// Add Attribute: "player_controller" = true
// Add Attribute: "speed" = 16
// Add Attribute: "health" = 100
// This creates a GameObject with a PlayerController component
// where speed and health are set to those values
Note: Component names should use snake_case. Components prefixed with $ are core CORP components (e.g., $network_object).
Scene Structure
When defining scenes programmatically, they follow the SceneDescription interface:
interface SceneDescription {
children: GameObjectDescription[];
}
interface GameObjectDescription {
name: string;
transform?: SerializedTransform;
components: ComponentDescription[];
children: GameObjectDescription[];
}
interface ComponentDescription {
// ExportPath can be:
// - [path, exportName]: ["src/components/MyComponent", "MyComponent"]
// - [path]: ["src/components/MyComponent"] (uses default export)
path: SceneSerialization.ExportPath;
data: Record<string, any>;
}
// ExportPath type
type ExportPath = [string, string] | [string];
Creating Scenes Programmatically
Basic Scene
Create a simple scene with a few GameObjects:
import { SceneSerialization } from "CORP/shared/serialization/scene-serialization";
const MyScene: SceneSerialization.SceneDescription = {
children: [
{
name: "Player",
components: [
{
path: ["src", "shared", "components", "PlayerController"],
data: {}
},
{
path: ["src", "shared", "components", "HealthSystem"],
data: {
maxHealth: 100,
currentHealth: 100
}
}
],
children: []
},
{
name: "GameManager",
components: [
{
path: ["src", "shared", "components", "GameManager"],
data: {
roundDuration: 300,
maxPlayers: 10
}
}
],
children: []
}
]
};
export default MyScene;
Scene with Hierarchy
Create GameObjects with parent-child relationships:
const HierarchyScene: SceneSerialization.SceneDescription = {
children: [
{
name: "Vehicle",
components: [
{
path: ["src", "shared", "components", "VehicleController"],
data: {
speed: 50,
turnSpeed: 2
}
}
],
children: [
{
name: "Wheel_FrontLeft",
components: [
{
path: ["src", "shared", "components", "Wheel"],
data: { position: "front-left" }
}
],
children: []
},
{
name: "Wheel_FrontRight",
components: [
{
path: ["src", "shared", "components", "Wheel"],
data: { position: "front-right" }
}
],
children: []
}
]
}
]
};
export default HierarchyScene;
Specify initial positions for GameObjects:
const SpawnScene: SceneSerialization.SceneDescription = {
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: { teamId: 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: { teamId: 2 }
}
],
children: []
}
]
};
export default SpawnScene;
Scene with Network Objects
Create a scene with networked GameObjects:
const MultiplayerScene: SceneSerialization.SceneDescription = {
children: [
{
name: "NetworkedPlayer",
components: [
{
path: ["CORP", "shared", "networking", "NetworkObject"],
data: {}
},
{
path: ["src", "shared", "components", "PlayerController"],
data: {}
},
{
path: ["src", "shared", "components", "NetworkedHealth"],
data: {
maxHealth: 100
}
}
],
children: []
},
{
name: "GameState",
components: [
{
path: ["CORP", "shared", "networking", "NetworkObject"],
data: {}
},
{
path: ["src", "shared", "components", "GameState"],
data: {}
}
],
children: []
}
]
};
export default MultiplayerScene;
Loading Scenes
Using SceneManager
The SceneManager class handles scene loading and management:
import { SceneManager } from "CORP/shared/scenes/scene-manager";
import { SceneSerialization } from "CORP/shared/serialization/scene-serialization";
import MainMenu from "./scenes/MainMenu";
import GameLevel from "./scenes/GameLevel";
// Load a scene
SceneManager.loadScene(MainMenu);
// Get the current scene
const currentScene = SceneManager.currentScene;
// Change to a different scene
SceneManager.changeScene(GameLevel);
// Unload the current scene
SceneManager.unloadScene();
Starting with CORP
Use CORP.start() to initialize your game with a starting scene:
import { CORP } from "CORP/shared/CORP";
import { Configurations } from "CORP/shared/configurations";
import MainScene from "./scenes/MainScene";
// Start with a scene object
const config: Configurations.GameConfiguration = {
startingScene: MainScene
};
CORP.start(config);
Or load from a path:
// Start with a scene path
const config: Configurations.GameConfiguration = {
startingScene: "src/shared/scenes/MainScene"
};
CORP.start(config);
Scene Events
Listen for scene changes:
import { SceneManager } from "CORP/shared/scenes/scene-manager";
// Subscribe to scene changes
SceneManager.sceneChanged.connect((newScene) => {
print(`Scene changed to: ${newScene.description}`);
// Perform scene-specific setup
setupScene(newScene);
});
Scene Serialization
Exporting Scenes
Generate an Instance representation of the current scene:
import { SceneManager } from "CORP/shared/scenes/scene-manager";
// Generate scene instance description
const sceneInstance = SceneManager.generateSceneInstanceDescription();
// Can be used to save the scene state
Importing from JSON
Scenes can be stored as JSON and loaded at runtime:
{
"children": [
{
"name": "Player",
"components": [
{
"path": ["src", "shared", "components", "PlayerController"],
"data": {
"speed": 16,
"jumpPower": 50
}
}
],
"children": []
}
]
}
Load the JSON scene:
import sceneData from "./scenes/level1.json";
const scene = sceneData as SceneSerialization.SceneDescription;
SceneManager.loadScene(scene);
Component paths use the ExportPath type:
// Format 1: [path, exportName]
{
path: ["src/shared/components/PlayerController", "PlayerController"]
}
// Format 2: [path] (uses default export)
{
path: ["src/shared/components/PlayerController"]
}
// For CORP built-in components
{
path: ["CORP/shared/networking/network-object", "NetworkObject"]
}
Note: ExportPath is defined as [string, string] | [string], representing either [file path, export name] or [file path] for default exports.
Configuring Components
Pass initial data to components in scenes:
{
name: "Enemy",
components: [
{
path: ["src", "shared", "components", "EnemyAI"],
data: {
// These values are applied via Object.assign
attackDamage: 25,
detectionRange: 50,
moveSpeed: 8,
patrolPoints: [
{ x: 0, y: 0, z: 0 },
{ x: 10, y: 0, z: 10 }
]
}
}
],
children: []
}
The component receives these values:
export class EnemyAI extends Behavior {
@SerializeField
public attackDamage: number = 10; // Will be 25 from scene
@SerializeField
public detectionRange: number = 30; // Will be 50 from scene
@SerializeField
public moveSpeed: number = 5; // Will be 8 from scene
@SerializeField
public patrolPoints: Vector3[] = []; // Will have 2 points from scene
public onStart(): void {
// Values are already applied
print(`Enemy with ${this.attackDamage} damage`);
}
protected getSourceScript(): ModuleScript {
return script as ModuleScript;
}
public willRemove(): void {}
}
Best Practices
1. Use Roblox Hierarchy (Recommended)
Prefer defining scenes in Roblox Studio using the instance hierarchy. This approach provides:
- Visual editing in Studio
- Easier iteration and testing
- Natural integration with Roblox’s tooling
- Faster prototyping
Use programmatic scenes for:
- Procedural generation
- Dynamic content
- Runtime scene construction
2. Organize Scenes by Purpose
Workspace/
├── MainMenu (Folder)
├── Lobby (Folder)
├── Level1 (Folder)
├── Level2 (Folder)
└── GameOver (Folder)
Or for programmatic scenes:
scenes/
├── MainMenu.ts
├── Lobby.ts
├── Level1.ts
├── Level2.ts
└── GameOver.ts
2. Use Scene Hierarchy
Group related GameObjects:
{
name: "Level",
components: [],
children: [
{
name: "Spawners",
components: [],
children: [
{ name: "SpawnPoint_1", components: [...], children: [] },
{ name: "SpawnPoint_2", components: [...], children: [] }
]
},
{
name: "Pickups",
components: [],
children: [
{ name: "HealthPack_1", components: [...], children: [] },
{ name: "HealthPack_2", components: [...], children: [] }
]
}
]
}
3. Separate Configuration
Keep configuration separate from scene structure:
// config.ts
export const PlayerConfig = {
maxHealth: 100,
speed: 16,
jumpPower: 50
};
// scene.ts
import { PlayerConfig } from "./config";
const scene: SceneSerialization.SceneDescription = {
children: [
{
name: "Player",
components: [
{
path: ["src", "shared", "components", "PlayerController"],
data: PlayerConfig
}
],
children: []
}
]
};
For most cases, use Roblox hierarchy in Studio. Use TypeScript/JSON for:
- Procedural content generation
- Runtime dynamic scenes
- Complex scripted setups
When using TypeScript for scenes:
- Get type safety and IntelliSense
- Enable refactoring support
- Use import statements for organization
5. Scene Transitions
Handle scene transitions gracefully:
export class SceneTransition extends Behavior {
public async transitionToScene(newScene: SceneSerialization.SceneDescription): Promise<void> {
// Fade out
await this.fadeOut();
// Clean up current scene
if (SceneManager.currentScene) {
SceneManager.unloadScene();
}
// Load new scene
SceneManager.loadScene(newScene);
// Fade in
await this.fadeIn();
}
private async fadeOut(): Promise<void> {
// Fade animation
}
private async fadeIn(): Promise<void> {
// Fade animation
}
protected getSourceScript(): ModuleScript {
return script as ModuleScript;
}
public onStart(): void {}
public willRemove(): void {}
}
6. Dynamic Scene Loading
Load scenes based on game state:
export class LevelManager extends Behavior {
private levels: SceneSerialization.SceneDescription[] = [];
public onStart(): void {
// Register levels
this.levels = [
require("./scenes/Level1"),
require("./scenes/Level2"),
require("./scenes/Level3")
];
}
public loadLevel(levelIndex: number): void {
if (levelIndex >= 0 && levelIndex < this.levels.size()) {
SceneManager.changeScene(this.levels[levelIndex]);
}
}
protected getSourceScript(): ModuleScript {
return script as ModuleScript;
}
public willRemove(): void {}
}
Common Patterns
Procedural Scene Generation
Generate scenes programmatically:
export function generateDungeonScene(width: number, height: number): SceneSerialization.SceneDescription {
const children: SceneSerialization.GameObjectDescription[] = [];
for (let x = 0; x < width; x++) {
for (let z = 0; z < height; z++) {
children.push({
name: `Tile_${x}_${z}`,
transform: {
position: { x: x * 10, y: 0, z: z * 10 },
rotation: { x: 0, y: 0, z: 0 }
},
components: [
{
path: ["src", "shared", "components", "FloorTile"],
data: {}
}
],
children: []
});
}
}
return { children };
}
// Use it
const dungeon = generateDungeonScene(10, 10);
SceneManager.loadScene(dungeon);
Scene Prefabs
Create reusable GameObject descriptions:
// prefabs.ts
export const PlayerPrefab: SceneSerialization.GameObjectDescription = {
name: "Player",
components: [
{
path: ["CORP", "shared", "networking", "NetworkObject"],
data: {}
},
{
path: ["src", "shared", "components", "PlayerController"],
data: { speed: 16 }
},
{
path: ["src", "shared", "components", "HealthSystem"],
data: { maxHealth: 100 }
}
],
children: []
};
// Use in scenes
const scene: SceneSerialization.SceneDescription = {
children: [
PlayerPrefab,
// ... other GameObjects
]
};
Next Steps
Master scene management to organize your game world effectively! 🎬