> ## Documentation Index
> Fetch the complete documentation index at: https://docs.monolisk.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# ScriptableObjects

> Data assets and configuration management for game content

<Note>**Reading time:** \~35 minutes</Note>

ScriptableObjects in CORP are data containers that allow you to define reusable assets and configuration. They provide a Unity-like approach to managing game data separate from your code.

## Table of Contents

* [What are ScriptableObjects?](#what-are-scriptableobjects)
* [Creating ScriptableObjects](#creating-scriptableobjects)
* [The Data Namespace](#the-data-namespace)
* [Loading ScriptableObjects](#loading-scriptableobjects)
* [Asset Files](#asset-files)
* [Registration](#registration)
* [Common Use Cases](#common-use-cases)
* [Best Practices](#best-practices)

## What are ScriptableObjects?

ScriptableObjects are classes that extend `ScriptableObject` and define data structures for game assets. They separate data from logic, making it easy to create and modify game content without changing code.

### Key Features

* **Data Assets**: Store configuration and content as assets
* **Reusability**: Create multiple instances with different data
* **Serialization**: Automatically serialized and deserialized
* **Type Safety**: Full TypeScript type checking
* **Lazy Loading**: Load assets on-demand or cache them
* **References**: ScriptableObjects can reference other ScriptableObjects

### Benefits

* **Designer-Friendly**: Non-programmers can create and edit assets
* **Version Control**: Assets are separate files, easy to track changes
* **Modularity**: Organize game data independently from code
* **Performance**: Shared data loaded once and reused

## Creating ScriptableObjects

### Basic ScriptableObject

```typescript theme={null}
import { ScriptableObject } from "CORP/shared/assets/scriptable-object";

export class WeaponData extends ScriptableObject {
    public weaponName: string = "Sword";
    public damage: number = 25;
    public attackSpeed: number = 1.0;
    public range: number = 5;
    public cost: number = 100;
}
```

### ScriptableObject with Complex Data

```typescript theme={null}
import { ScriptableObject } from "CORP/shared/assets/scriptable-object";

export class EnemyData extends ScriptableObject {
    // Basic stats
    public enemyName: string = "Goblin";
    public health: number = 100;
    public damage: number = 10;
    public speed: number = 8;
    
    // Behavior configuration
    public aggroRange: number = 50;
    public attackRange: number = 5;
    public retreatHealth: number = 20;
    
    // Loot configuration
    public dropTable: { itemName: string, chance: number }[] = [
        { itemName: "Gold", chance: 1.0 },
        { itemName: "Sword", chance: 0.1 }
    ];
    
    // Visual configuration
    public color: Color3 = new Color3(0, 1, 0);
    public scale: number = 1.0;
}
```

### ScriptableObject with References

ScriptableObjects can reference other ScriptableObjects:

```typescript theme={null}
import { ScriptableObject } from "CORP/shared/assets/scriptable-object";
import { WeaponData } from "./WeaponData";

export class CharacterClass extends ScriptableObject {
    public className: string = "Warrior";
    public baseHealth: number = 150;
    public baseSpeed: number = 10;
    
    // Reference to another ScriptableObject
    public startingWeapon!: WeaponData;
    
    // Array of references
    public availableWeapons: WeaponData[] = [];
}
```

## The Data Namespace

The `Data` namespace provides utilities for working with serialized data in CORP. It defines special data types that are automatically deserialized when loaded, and includes factory methods for creating type-safe references and serialized values.

### Available Types

The Data namespace supports these special serialized types:

```typescript theme={null}
import { Data } from "CORP/shared/data";

// Object References
Data.ComponentReference       // Reference to a Component
Data.GameObjectReference      // Reference to a GameObject
Data.InstanceReference        // Reference to a Roblox Instance
Data.ScriptableObjectReference // Reference to a ScriptableObject

// Serialized Roblox Types
Data.SerializedVector3        // Vector3 as serializable data
Data.SerializedColor3         // Color3 as serializable data

// Function References
Data.FunctionReference        // Reference to a function on an object

// Network References (for networked data)
Data.Networked.NetworkBehaviorReference // Reference to a NetworkBehavior
Data.Networked.InstanceReference        // Reference to a networked Instance
```

### Factory Methods

Create these special types using the factory:

```typescript theme={null}
import { Data } from "CORP/shared/data";

// ScriptableObject references
Data.factory.createScriptableObjectReference("%assets%/Weapons/Sword");

// Serialized Roblox types
Data.factory.createSerializedVector3(0, 10, 0);
Data.factory.createSerializedColor3(1, 0, 0); // Red

// Component/GameObject references
Data.factory.createComponentReference(referenceId);
Data.factory.createGameObjectReference(referenceId);
Data.factory.createInstanceReference(referenceId);

// Function references
Data.factory.createFunctionReference("methodName", object);

// Network references
Data.Networked.factory.createNetworkBehaviorReference(id);
Data.Networked.factory.createInstanceReference(id);
```

### Type Guards

Check if a value is a special data type:

```typescript theme={null}
import { Data } from "CORP/shared/data";

if (Data.isScriptableObjectReference(value)) {
    // value is ScriptableObjectReference
    print(value.$path);
}

if (Data.isSerializedVector3(value)) {
    // value is SerializedVector3
    const vec = new Vector3(value.x, value.y, value.z);
}

// Other type guards:
Data.isSpecial(value)                  // Any special type
Data.isComponentReference(value)
Data.isGameObjectReference(value)
Data.isInstanceReference(value)
Data.isFunctionReference(value)
Data.isSerializedColor3(value)
Data.Networked.isNetworkedSpecial(value)
Data.Networked.isNetworkBehaviorReference(value)
Data.Networked.isInstanceReference(value)
```

### Why Use Data.factory?

* ✅ **Type Safety**: Compile-time validation of structure
* ✅ **Autocomplete**: IDE suggestions for all properties
* ✅ **Automatic Deserialization**: Special types are automatically converted when loaded
* ✅ **Consistency**: Standardized format across all assets
* ✅ **Maintainability**: Easier to refactor and update
* ✅ **Documentation**: Self-documenting code

> **Recommendation**: Always use `Data.factory` methods when creating special references or serialized types in ModuleScript assets for better development experience and fewer runtime errors.

## Loading ScriptableObjects

### Using fetch()

The `fetch()` method loads any ScriptableObject by path:

```typescript theme={null}
import { ScriptableObject } from "CORP/shared/assets/scriptable-object";
import { WeaponData } from "./data/WeaponData";

// Load a weapon data asset using the VFS path
const sword = ScriptableObject.fetch<WeaponData>(
    "%assets%/Weapons/Sword"
);

print(sword.weaponName); // "Sword"
print(sword.damage);     // 25
```

> **VFS Path Convention**: Use `%assets%` to reference the collapsed assets directory. During bootstrap, all Gamepack assets are collected into a centralized location accessible via this path. See [Unreal System - Asset Management](./unreal-system#asset-management) for more details.

### Using fetchThis()

When loading from within a ScriptableObject class:

```typescript theme={null}
export class WeaponData extends ScriptableObject {
    public static loadSword(): WeaponData {
        return this.fetchThis<WeaponData>("%assets%/Weapons/Sword");
    }
}

// Usage
const sword = WeaponData.loadSword();
```

### Using makeLoader()

For lazy-loaded, cached access:

```typescript theme={null}
import { makeLoader } from "CORP/shared/assets/scriptable-object";
import { WeaponData } from "./data/WeaponData";

// Create a loader function
export const getSwordData = makeLoader<WeaponData>(
    "%assets%/Weapons/Sword"
);

// Use anywhere in your code
// First call loads the asset, subsequent calls return cached instance
const sword = getSwordData();
```

### In Components

```typescript theme={null}
import { Behavior } from "CORP/shared/componentization/behavior";
import { ScriptableObject } from "CORP/shared/assets/scriptable-object";
import { WeaponData } from "../data/WeaponData";

export class WeaponController extends Behavior {
    private weaponData!: WeaponData;
    
    public onStart(): void {
        // Load weapon data
        this.weaponData = ScriptableObject.fetch<WeaponData>(
            "%assets%/Weapons/Rifle"
        );
        
        print(`Equipped: ${this.weaponData.weaponName}`);
        print(`Damage: ${this.weaponData.damage}`);
    }
    
    public attack(): void {
        // Use the data
        const damage = this.weaponData.damage;
        const range = this.weaponData.range;
        
        // Perform attack...
    }
    
    protected getSourceScript(): ModuleScript {
        return script as ModuleScript;
    }
    
    public willRemove(): void {}
}
```

## Asset Files

### JSON Format (Recommended)

Create asset files as JSON in your assets folder:

```json theme={null}
{
    "$scriptableObjectClass": "weapon_data",
    "$data": {
        "weaponName": "Magic Sword",
        "damage": 50,
        "attackSpeed": 1.5,
        "range": 6,
        "cost": 500
    }
}
```

Save as `MagicSword.scriptable` or `MagicSword.json` in your assets folder.

### ModuleScript Format

Alternatively, use a ModuleScript:

```typescript theme={null}
// MagicSword.scriptable.ts
export = {
    $scriptableObjectClass: "weapon_data",
    $data: {
        weaponName: "Magic Sword",
        damage: 50,
        attackSpeed: 1.5,
        range: 6,
        cost: 500
    }
};
```

#### Using the Data Namespace

When creating ModuleScript ScriptableObjects, use the `Data` namespace for type-safe asset creation:

```typescript theme={null}
// MagicSword.scriptable.ts
import { Data } from "CORP/shared/data";

export = Data.factory.createScriptableObjectData("weapon_data", {
    weaponName: "Magic Sword",
    damage: 50,
    attackSpeed: 1.5,
    range: 6,
    cost: 500
});
```

**Benefits of using `Data.factory.createScriptableObjectData`:**

* Type safety and autocomplete
* Validates the structure at compile time
* Cleaner, more readable syntax
* Consistent format across assets

#### Referencing Other ScriptableObjects in ModuleScripts

Use `Data.factory.createScriptableObjectReference` for references:

```typescript theme={null}
// Warrior.scriptable.ts
import { Data } from "CORP/shared/data";

export = Data.factory.createScriptableObjectData("character_class", {
    className: "Warrior",
    baseHealth: 150,
    startingWeapon: Data.factory.createScriptableObjectReference(
        "%assets%/Weapons/Sword"
    ),
    availableWeapons: [
        Data.factory.createScriptableObjectReference("%assets%/Weapons/Sword"),
        Data.factory.createScriptableObjectReference("%assets%/Weapons/Axe")
    ]
});
```

#### Serializing Roblox Types

Use `Data.factory` methods to serialize Vector3, Color3, and other Roblox types:

```typescript theme={null}
// Enemy.scriptable.ts
import { Data } from "CORP/shared/data";

export = Data.factory.createScriptableObjectData("enemy_data", {
    enemyName: "Goblin",
    health: 100,
    damage: 10,
    
    // Serialize Vector3
    spawnPosition: Data.factory.createSerializedVector3(0, 10, 0),
    patrolOffset: Data.factory.createSerializedVector3(10, 0, 10),
    
    // Serialize Color3
    bodyColor: Data.factory.createSerializedColor3(0, 1, 0), // Green
    glowColor: Data.factory.createSerializedColor3(1, 0, 0), // Red
    
    // Complex data structures
    dropTable: [
        { itemName: "Gold", chance: 1.0 },
        { itemName: "Sword", chance: 0.1 }
    ]
});
```

When loaded, these serialized types are automatically converted back to their Roblox equivalents:

```typescript theme={null}
const enemyData = ScriptableObject.fetch<EnemyData>("%assets%/Enemies/Goblin");

// Automatically deserialized to Vector3
print(enemyData.spawnPosition); // Vector3(0, 10, 0)

// Automatically deserialized to Color3
print(enemyData.bodyColor); // Color3(0, 1, 0)
```

> **Best Practice**: Always use `Data.factory` methods when creating ModuleScript ScriptableObjects for better type safety and maintainability. The serialization system handles automatic deserialization when assets are loaded.

### Configuration Format

For Roblox Studio, use Configuration instances:

```
MagicSword (Configuration)
├── $scriptableObjectClass (StringValue) = "weapon_data"
└── $data (StringValue) = {"weaponName":"Magic Sword","damage":50,...}
```

### With References

Reference other ScriptableObjects in asset data:

```json theme={null}
{
    "$scriptableObjectClass": "character_class",
    "$data": {
        "className": "Warrior",
        "baseHealth": 150,
        "startingWeapon": {
            "$type": "scriptableObjectReference",
            "$path": "%assets%/Weapons/Sword"
        },
        "availableWeapons": [
            {
                "$type": "scriptableObjectReference",
                "$path": "%assets%/Weapons/Sword"
            },
            {
                "$type": "scriptableObjectReference",
                "$path": "%assets%/Weapons/Axe"
            }
        ]
    }
}
```

### With Roblox Types

Use serialized formats for Roblox types:

```json theme={null}
{
    "$scriptableObjectClass": "enemy_data",
    "$data": {
        "enemyName": "Goblin",
        "health": 100,
        "color": {
            "r": 0,
            "g": 1,
            "b": 0
        },
        "spawnPosition": {
            "x": 0,
            "y": 10,
            "z": 0
        }
    }
}
```

These are automatically converted to `Color3` and `Vector3` when loaded.

## Registration

ScriptableObjects must be registered in your Gamepack's exports.

### Registering in Gamepack

Create `exports/scriptableobjects.ts`:

```typescript theme={null}
import { Config } from "CORP/shared/unreal/gamepacks/config";
import { WeaponData } from "../src/data/WeaponData";
import { EnemyData } from "../src/data/EnemyData";
import { CharacterClass } from "../src/data/CharacterClass";
import { LevelConfig } from "../src/data/LevelConfig";

const scriptableObjects: Config.ScriptableObjects = {
    mappings: {
        // Use snake_case for ScriptableObject names
        "weapon_data": WeaponData,
        "enemy_data": EnemyData,
        "character_class": CharacterClass,
        "level_config": LevelConfig
    }
};

export = scriptableObjects;
```

> **Naming Convention**: Always use `snake_case` for ScriptableObject mapping names. This ensures consistency with components and macros.

## Common Use Cases

### 1. Weapon System

```typescript theme={null}
// WeaponData.ts
export class WeaponData extends ScriptableObject {
    public weaponName: string = "";
    public weaponType: "melee" | "ranged" = "melee";
    public damage: number = 10;
    public attackSpeed: number = 1.0;
    public range: number = 5;
    public ammoCapacity?: number;
    public reloadTime?: number;
}

// WeaponController.ts
import { Behavior } from "CORP/shared/componentization/behavior";
import { ScriptableObject } from "CORP/shared/assets/scriptable-object";
import { WeaponData } from "../data/WeaponData";

export class WeaponController extends Behavior {
    private currentWeapon!: WeaponData;
    
    public equipWeapon(weaponPath: string): void {
        this.currentWeapon = ScriptableObject.fetch<WeaponData>(weaponPath);
        print(`Equipped ${this.currentWeapon.weaponName}`);
    }
    
    public attack(): void {
        if (this.currentWeapon.weaponType === "ranged") {
            this.fireProjectile();
        } else {
            this.meleeAttack();
        }
    }
    
    private fireProjectile(): void {
        // Use this.currentWeapon.damage, range, etc.
    }
    
    private meleeAttack(): void {
        // Use this.currentWeapon.damage, range, etc.
    }
    
    protected getSourceScript(): ModuleScript {
        return script as ModuleScript;
    }
    
    public onStart(): void {}
    public willRemove(): void {}
}
```

### 2. Enemy Configuration

```typescript theme={null}
// EnemyData.ts
export class EnemyData extends ScriptableObject {
    public enemyName: string = "";
    public health: number = 100;
    public damage: number = 10;
    public speed: number = 8;
    public aggroRange: number = 50;
    public goldDrop: number = 10;
    public experienceDrop: number = 50;
}

// EnemySpawner.ts
import { Behavior } from "CORP/shared/componentization/behavior";
import { ScriptableObject } from "CORP/shared/assets/scriptable-object";
import { EnemyData } from "../data/EnemyData";
import { Enemy } from "../components/Enemy";

export class EnemySpawner extends Behavior {
    public spawnEnemy(enemyDataPath: string, position: Vector3): void {
        const enemyData = ScriptableObject.fetch<EnemyData>(enemyDataPath);
        
        const enemyObject = new GameObject(enemyData.enemyName);
        enemyObject.setTransform(new CFrame(position));
        
        const enemy = enemyObject.addComponent(Enemy);
        enemy.initializeFromData(enemyData);
    }
    
    protected getSourceScript(): ModuleScript {
        return script as ModuleScript;
    }
    
    public onStart(): void {}
    public willRemove(): void {}
}
```

### 3. Level Configuration

```typescript theme={null}
// LevelConfig.ts
import { ScriptableObject } from "CORP/shared/assets/scriptable-object";

export class LevelConfig extends ScriptableObject {
    public levelName: string = "";
    public difficulty: number = 1;
    public timeLimit: number = 300;
    public enemySpawnRate: number = 5;
    public maxEnemies: number = 20;
    public requiredKills: number = 50;
    public bossName?: string;
}

// LevelManager.ts
import { Behavior } from "CORP/shared/componentization/behavior";
import { ScriptableObject } from "CORP/shared/assets/scriptable-object";
import { LevelConfig } from "../data/LevelConfig";

export class LevelManager extends Behavior {
    private config!: LevelConfig;
    
    public loadLevel(levelPath: string): void {
        this.config = ScriptableObject.fetch<LevelConfig>(levelPath);
        
        print(`Loading level: ${this.config.levelName}`);
        print(`Difficulty: ${this.config.difficulty}`);
        
        this.startLevel();
    }
    
    private startLevel(): void {
        // Use config to set up level
        // this.config.timeLimit, maxEnemies, etc.
    }
    
    protected getSourceScript(): ModuleScript {
        return script as ModuleScript;
    }
    
    public onStart(): void {}
    public willRemove(): void {}
}
```

### 4. Item Database

```typescript theme={null}
// ItemData.ts
export class ItemData extends ScriptableObject {
    public itemName: string = "";
    public description: string = "";
    public itemType: "consumable" | "equipment" | "quest" = "consumable";
    public stackable: boolean = true;
    public maxStack: number = 99;
    public value: number = 0;
    public rarity: "common" | "rare" | "epic" | "legendary" = "common";
}

// Inventory.ts
import { Behavior } from "CORP/shared/componentization/behavior";
import { ScriptableObject } from "CORP/shared/assets/scriptable-object";
import { ItemData } from "../data/ItemData";

export class Inventory extends Behavior {
    private items: Map<string, { data: ItemData, count: number }> = new Map();
    
    public addItem(itemPath: string, count: number = 1): void {
        const itemData = ScriptableObject.fetch<ItemData>(itemPath);
        
        if (this.items.has(itemPath)) {
            const existing = this.items.get(itemPath)!;
            if (itemData.stackable) {
                existing.count = math.min(
                    existing.count + count,
                    itemData.maxStack
                );
            }
        } else {
            this.items.set(itemPath, { data: itemData, count });
        }
        
        print(`Added ${count}x ${itemData.itemName}`);
    }
    
    protected getSourceScript(): ModuleScript {
        return script as ModuleScript;
    }
    
    public onStart(): void {}
    public willRemove(): void {}
}
```

### 5. Character Presets

```typescript theme={null}
// CharacterPreset.ts
import { ScriptableObject } from "CORP/shared/assets/scriptable-object";
import { WeaponData } from "./WeaponData";

export class CharacterPreset extends ScriptableObject {
    public presetName: string = "";
    public health: number = 100;
    public speed: number = 16;
    public jumpPower: number = 50;
    public startingWeapon!: WeaponData;
    public abilities: string[] = [];
}

// CharacterCreator.ts
import { Behavior } from "CORP/shared/componentization/behavior";
import { ScriptableObject } from "CORP/shared/assets/scriptable-object";
import { CharacterPreset } from "../data/CharacterPreset";

export class CharacterCreator extends Behavior {
    public createCharacter(presetPath: string): GameObject {
        const preset = ScriptableObject.fetch<CharacterPreset>(presetPath);
        
        const character = new GameObject(preset.presetName);
        
        // Apply preset values to character components
        const health = character.addComponent(HealthSystem);
        health.maxHealth = preset.health;
        
        const movement = character.addComponent(Movement);
        movement.speed = preset.speed;
        movement.jumpPower = preset.jumpPower;
        
        // weapon from preset.startingWeapon reference
        const weapon = character.addComponent(WeaponController);
        weapon.equipWeapon(preset.startingWeapon);
        
        return character;
    }
    
    protected getSourceScript(): ModuleScript {
        return script as ModuleScript;
    }
    
    public onStart(): void {}
    public willRemove(): void {}
}
```

## Best Practices

### 1. Use snake\_case for Names

```typescript theme={null}
// ✓ Good - consistent snake_case
"weapon_data": WeaponData,
"enemy_data": EnemyData,
"character_preset": CharacterPreset

// ✗ Bad - inconsistent casing
"WeaponData": WeaponData,
"enemyData": EnemyData
```

### 2. Provide Default Values

```typescript theme={null}
export class WeaponData extends ScriptableObject {
    // ✓ Good - all fields have defaults
    public weaponName: string = "Unknown";
    public damage: number = 0;
    public range: number = 1;
    
    // ✗ Bad - no defaults, can cause errors
    // public weaponName: string;
    // public damage: number;
}
```

### 3. Use Data.factory for ModuleScript Assets

```typescript theme={null}
// ✓ Good - type-safe with Data.factory
import { Data } from "CORP/shared/data";

export = Data.factory.createScriptableObjectData("weapon_data", {
    weaponName: "Sword",
    damage: 25,
    startingWeapon: Data.factory.createScriptableObjectReference("%assets%/Weapons/Default")
});

// ✗ Avoid - manual object creation (error-prone)
export = {
    $scriptableObjectClass: "weapon_data",
    $data: {
        weaponName: "Sword",
        startingWeapon: {
            "$type": "scriptableObjectReference",
            "$path": "%assets%/Weapons/Default"
        }
    }
};
```

### 4. Use makeLoader for Frequently Accessed Assets

```typescript theme={null}
// constants.ts
import { makeLoader } from "CORP/shared/assets/scriptable-object";

export const getDefaultWeapon = makeLoader("%assets%/Weapons/Default");
export const getPlayerConfig = makeLoader("%assets%/Config/Player");

// Use in components
import { getDefaultWeapon } from "./constants";

const weapon = getDefaultWeapon(); // Cached after first call
```

### 5. Organize Assets by Type

```
assets/
├── Weapons/
│   ├── Sword.scriptable
│   ├── Bow.scriptable
│   └── Staff.scriptable
├── Enemies/
│   ├── Goblin.scriptable
│   ├── Dragon.scriptable
│   └── Skeleton.scriptable
└── Config/
    ├── PlayerConfig.scriptable
    └── GameSettings.scriptable
```

### 6. Document ScriptableObject Properties

```typescript theme={null}
/**
 * Defines configuration for weapon items.
 */
export class WeaponData extends ScriptableObject {
    /** Display name of the weapon */
    public weaponName: string = "";
    
    /** Base damage per hit */
    public damage: number = 10;
    
    /** Number of attacks per second */
    public attackSpeed: number = 1.0;
    
    /** Maximum attack range in studs */
    public range: number = 5;
}
```

### 6. Validate Data on Load

```typescript theme={null}
export class WeaponData extends ScriptableObject {
    public weaponName: string = "";
    public damage: number = 10;
    
    public validate(): boolean {
        if (this.weaponName === "") {
            warn("WeaponData: weaponName is empty");
            return false;
        }
        
        if (this.damage < 0) {
            warn("WeaponData: damage cannot be negative");
            return false;
        }
        
        return true;
    }
}

// In component
const weapon = ScriptableObject.fetch<WeaponData>(path);
if (!weapon.validate()) {
    // Handle invalid data
}
```

## Next Steps

* **[Macros](./macros)**: Learn about instance transformation
* **[Unreal System](./unreal-system)**: Understand Gamepack integration
* **[Networking](./networking)**: NetworkedVariables can replicate ScriptableObjects
* **[Examples](./examples)**: See complete implementations

***

Master ScriptableObjects for flexible, designer-friendly game data! 📦
