# O3DESharp TDD
**Version:** 0.2-Draft
**Original Author Date:** 01-05-2026
**Authors:** Scrumpy [WD Studios], O3DE Contributors
**Status:** Draft - RV1
---
## Table of Contents
1. [Executive Summary](#1-executive-summary)
2. [Goals and Non-Goals](#2-goals-and-non-goals)
3. [System Architecture](#3-system-architecture)
4. [BehaviorContext Reflection System](#4-behaviorcontext-reflection-system)
5. [C# Binding Generation Pipeline](#5-c-binding-generation-pipeline)
6. [Assembly Management](#6-assembly-management)
7. [Threading Model](#7-threading-model)
8. [Script Execution Model](#8-script-execution-model)
9. [Developer Experience](#9-developer-experience)
10. [Hot Reload System](#10-hot-reload-system)
11. [Performance Considerations](#11-performance-considerations)
12. [Security Considerations](#12-security-considerations)
13. [Platform Support](#13-platform-support)
14. [Future Considerations](#14-future-considerations)
15. [Appendices](#15-appendices)
---
## 1. Executive Summary
O3DESharp is a Gem that adds C# scripting support to the Open 3D Engine (O3DE) using the [Coral](https://github.com/WatchDogStudios/Coral) .NET host library. This TDD is mainly meant for refining design details as the community helps develop consideration/effort for the project.
### 1.2 TDD Notice
Everything you see in this document **IS SUBJECT TO CHANGE.** Do not take details written in this document as **FINAL.**
### Key Design Decisions
1. **BehaviorContext as Single Source of Truth**: All EBus and class reflection data comes exclusively from O3DE's BehaviorContext. This ensures consistency with Lua scripting and avoids maintaining separate binding definitions.
2. **Two-Assembly Model**:
- **Engine Core DLL**: Contains bindings for core O3DE types (AzCore, AzFramework, math types, entity system)
- **Project Gems DLL**: A monolithic assembly containing bindings for all active project gems (See Section 14 for future per-gem assembly considerations).
3. **Build-Time Generation**: C# bindings are generated at build time (not runtime) to ensure type safety and IDE support.
4. **Threading-Aware Design**: The system explicitly handles threading concerns for both module loading and script execution.
---
## 2. Goals and Non-Goals
### Goals
- **G1**: Provide strongly-typed C# access to any type reflected in BehaviorContext
- **G2**: Generate IntelliSense-compatible C# code for excellent developer UX
- **G3**: Support hot-reload of user scripts during development
- **G4**: Maintain thread safety for all script operations
- **G5**: Enable per-gem organization of generated bindings
- **G6**: Support both Editor and runtime execution
- **G7**: Minimize runtime overhead for script execution
### Non-Goals
- **NG1**: Runtime code generation (all bindings are pre-generated)
- **NG2**: Full C++/CLI interop (we use P/Invoke style calls via Coral)
- **NG3**: Support for .NET Framework (only .NET 8.0+)
- **NG4**: Automatic binding of non-BehaviorContext reflected types
- **NG5**: C# to C++ inheritance (C# classes cannot derive from C++ classes)
---
## 3. System Architecture
### 3.1 High-Level Architecture
```mermaid
graph TD
subgraph Application [O3DE Application]
subgraph Gem [O3DESharp Gem C++]
BC[BehaviorContext<br/>Reflector] --> RD[ReflectionData<br/>Exporter JSON]
RD --> PBG[Python Binding Generator<br/>- gem_dependency_resolver.py<br/>- csharp_binding_generator.py<br/>- generate_bindings.py]
PBG --> GCS[Generated C# Source<br/>- O3DE.Core.dll Engine Types<br/>- O3DE.Gems.dll All Gem Types]
CHM[CoralHostManager<br/>- .NET Runtime<br/>- Assembly Loading<br/>- Thread Dispatch]
CHM --> GCS
CHM --> SEL
subgraph SEL [Script Execution Layer]
direction LR
SB[ScriptBindings<br/>Native Calls]
GD[GenericDispatcher<br/>Dynamic Calls]
CSC[CSharpScriptComponent<br/>Entity Scripts]
end
GCS --- SEL
end
end
subgraph Coral [Coral Library]
CL[HostFXR Wrapper<br/>Assembly Loading/Unloading<br/>Internal Call Registration<br/>Managed Object Lifetime]
end
subgraph DotNet [.NET 8.0 Runtime]
Runtime[.NET 8.0]
end
subgraph UserScripts [User Game Scripts]
US[References O3DE.Core.dll and O3DE.Gems.dll<br/>ScriptComponent-derived classes]
end
Application ==> Coral
Coral ==> DotNet
DotNet ==> UserScripts
```
### 3.2 Component Responsibilities
| Component | Responsibility |
|-----------|---------------|
| **BehaviorContextReflector** | Extracts metadata from O3DE's BehaviorContext at runtime |
| **ReflectionDataExporter** | Serializes reflection metadata to JSON for Python consumption |
| **GemDependencyResolver** | Discovers gems and resolves dependency ordering |
| **CSharpBindingGenerator** | Generates C# source files from reflection JSON |
| **CoralHostManager** | Manages .NET runtime lifecycle and assembly loading |
| **ScriptBindings** | Implements native method callbacks for C# internal calls |
| **GenericDispatcher** | Enables dynamic method invocation for BehaviorContext types |
| **CSharpScriptComponent** | O3DE component that hosts C# script instances |
---
## 4. BehaviorContext Reflection System
### 4.1 Why BehaviorContext?
BehaviorContext is the **only** ["reliable" source](https://www.o3de.org/docs/user-guide/programming/components/reflection/behavior-context/) for discovering reflected EBuses, classes, and methods in O3DE. This is by design:
1. **Consistency**: Same types available to Lua are available to C#
2. **Official API**: BehaviorContext is the supported scripting interface
3. **Completeness**: Includes methods, properties, EBuses, and global functions
4. **Attributes**: Contains documentation, categories, and deprecation info
### 4.2 Reflection Data Model
```cpp
// Core reflection structures
struct ReflectedParameter {
AZStd::string name;
AZ::Uuid typeId;
AZStd::string typeName;
bool isPointer;
bool isReference;
bool isConst;
MarshalType marshalType; // Hint for C# marshalling
};
struct ReflectedMethod {
AZStd::string name;
AZStd::string className;
bool isStatic;
bool isConst;
ReflectedParameter returnType;
AZStd::vector<ReflectedParameter> parameters;
AZStd::string description;
AZStd::string category;
bool isDeprecated;
};
struct ReflectedClass {
AZStd::string name;
AZ::Uuid typeId;
AZStd::vector<AZStd::string> baseClasses;
AZStd::vector<ReflectedMethod> methods;
AZStd::vector<ReflectedProperty> properties;
AZStd::vector<ReflectedMethod> constructors;
AZStd::string description;
AZStd::string category;
AZStd::string sourceGemName; // Which gem this class belongs to
};
struct ReflectedEBus {
AZStd::string name;
AZ::Uuid typeId;
ReflectedParameter addressType;
AZStd::vector<ReflectedEBusEvent> events;
AZStd::string description;
AZStd::string category;
AZStd::string sourceGemName;
};
```
### 4.3 Class-to-Gem Mapping
Since BehaviorContext doesn't inherently track which gem registered a class, we use heuristics:
1. **Category Attribute**: Many classes have categories like "Atom/Rendering"
2. **Name Prefixes**: Classes often have gem-specific prefixes (e.g., "PhysX")
3. **Explicit Mapping**: Configuration file for edge cases
4. **Default Fallback**: Unmapped classes go to "O3DE.Core"
```python
# Default prefix-to-gem mappings
DEFAULT_PREFIX_MAPPINGS = {
"AZ": "AzCore",
"Atom": "Atom",
"PhysX": "PhysX",
"ScriptCanvas": "ScriptCanvas",
"Multiplayer": "Multiplayer",
# ... etc
}
```
---
## 5. C# Binding Generation Pipeline
### 5.1 Pipeline Overview
```mermaid
graph LR
subgraph CppRuntime [C++ Runtime]
BC[BehaviorContext<br/>Reflector]
end
subgraph JSONExport [JSON Export]
JSON[reflection.json]
end
subgraph PythonScripts [Python Scripts]
PY[generate_*.py]
end
subgraph GeneratedCS [Generated C#]
CSFile[*.cs files<br/>*.csproj files]
end
subgraph Build [dotnet build]
MSBuild[MSBuild]
end
subgraph DLLs [Compiled DLLs]
DLL[O3DE.Core.dll<br/>O3DE.Gems.dll]
end
BC --> JSON
JSON --> PY
PY --> CSFile
CSFile --> MSBuild
MSBuild --> DLL
```
### 5.2 Generation Phases
#### Phase 1: Reflection Export (C++)
```cpp
// Triggered at Editor startup or via console command
void ExportReflectionData()
{
AZ::BehaviorContext* context = GetBehaviorContext();
BehaviorContextReflector reflector;
reflector.ReflectFromContext(context);
// Resolve gem sources for organization
GemDependencyResolver gemResolver;
gemResolver.DiscoverGems(settingsRegistry);
// Update class gem sources
for (const auto& className : reflector.GetClassNames())
{
AZStd::string gemName = gemResolver.ResolveGemForClass(
className, reflector.GetClass(className)->category);
reflector.SetClassGemSource(className, gemName);
}
// Export to JSON
ReflectionDataExporter exporter;
exporter.ExportToFile(reflector, "reflection_data.json");
}
```
#### Phase 2: Binding Generation (Python)
```bash
python generate_bindings.py \
--reflection-data reflection_data.json \
--project /path/to/project \
--output Generated/CSharp
```
The Python generator:
1. Loads reflection JSON
2. Discovers gems and their dependencies
3. Groups classes by source gem
4. Generates C# source files with proper namespacing
5. Generates .csproj and .sln files
#### Phase 3: Compilation
```bash
cd Generated/CSharp
dotnet build -c Release
```
Output:
- `O3DE.Core.dll` - Core engine bindings
- `O3DE.Gems.dll` - All gem bindings in one assembly
### 5.3 Generated Code Structure (PoC Structure)
```
Generated/CSharp/
├── O3DE.Generated.sln
├── Core/
│ ├── O3DE.Core.csproj
│ ├── AssemblyInfo.cs
│ ├── Math.cs # Vector3, Quaternion, Transform, Matrix
│ ├── Entity.cs # Entity, EntityId, Component base
│ ├── Core.cs # Debug, Time, Application
│ ├── Core.EBus.cs # TransformBus, EntityBus, TickBus
│ └── InternalCalls.cs # [InternalCall] declarations
├── Gems/
│ ├── O3DE.Gems.csproj
│ ├── Atom/
│ │ ├── Rendering.cs
│ │ ├── Materials.cs
│ │ └── Rendering.EBus.cs
│ ├── PhysX/
│ │ ├── RigidBody.cs
│ │ ├── Collision.cs
│ │ └── Physics.EBus.cs
│ └── ... (other gems)
└── InternalCalls.cs # Shared native method declarations
```
### 5.4 Generated Class Example
```csharp
// Generated from BehaviorContext reflection
namespace O3DE.Generated.PhysX
{
/// <summary>
/// Represents a rigid body physics component.
/// </summary>
public partial class RigidBody : NativeObject
{
/// <summary>
/// Gets or sets the linear velocity of the rigid body.
/// </summary>
public Vector3 LinearVelocity
{
get => NativeMethods.RigidBody_GetLinearVelocity(_nativeHandle);
set => NativeMethods.RigidBody_SetLinearVelocity(_nativeHandle, value);
}
/// <summary>
/// Applies a force to the rigid body at its center of mass.
/// </summary>
/// <param name="force">The force vector to apply.</param>
public void ApplyLinearImpulse(Vector3 force)
{
NativeMethods.RigidBody_ApplyLinearImpulse(_nativeHandle, force);
}
}
}
// Internal calls (separate file)
namespace O3DE.Generated.Internal
{
internal static class NativeMethods
{
[MethodImpl(MethodImplOptions.InternalCall)]
internal static extern Vector3 RigidBody_GetLinearVelocity(IntPtr instance);
[MethodImpl(MethodImplOptions.InternalCall)]
internal static extern void RigidBody_SetLinearVelocity(IntPtr instance, Vector3 value);
[MethodImpl(MethodImplOptions.InternalCall)]
internal static extern void RigidBody_ApplyLinearImpulse(IntPtr instance, Vector3 force);
}
}
```
---
## 6. Assembly Management
### 6.1 Assembly Architecture
```mermaid
graph TD
subgraph UserAssembly [User Game Assembly<br/>MyGame.Scripts.dll]
UA[User ScriptComponents<br/>Game Logic<br/>Refs Core & Gems]
end
subgraph GemsAssembly [O3DE.Gems.dll<br/>Monolithic Gem Bindings]
GA[All Gem Types<br/>Namespaced by Gem<br/>Refs Core]
end
subgraph CoreAssembly [O3DE.Core.dll<br/>Engine Core Bindings]
CA[Math Types, Entity, Component<br/>Core EBuses<br/>Debug, Time, App<br/>Internal Calls]
end
subgraph CoralAssembly [Coral.Managed.dll<br/>Coral Runtime Support]
CMA[ManagedObject Base<br/>Internal Call Infra<br/>Interop Utilities]
end
UserAssembly --> GemsAssembly
UserAssembly --> CoreAssembly
GemsAssembly --> CoreAssembly
CoreAssembly ~~~ CoralAssembly
```
### 6.2 Why Monolithic Gem Assembly?
We chose a single `O3DE.Gems.dll` instead of per-gem assemblies for several reasons:
| Factor | Per-Gem DLLs | Monolithic DLL |
|--------|--------------|----------------|
| **Load Time** | Slower (many files) | Faster (one file) |
| **Cross-Gem References** | Complex dependencies | Simple |
| **Hot Reload** | Individual gem reload | Full gem reload |
| **User Simplicity** | Many references | Two references |
| **Build Complexity** | Higher | Lower |
**Decision**: Use monolithic gem assembly for simplicity. Per-gem assemblies can be added later as an optional mode.
### 6.3 Assembly Loading Sequence
```mermaid
graph TD
Startup[Application Startup]
subgraph Init [1. Initialize Coral Host]
S1[CoralHostManager::Initialize<br/>- Locate .NET runtime<br/>- Initialize HostFXR<br/>- Load Coral.Managed.dll]
end
subgraph LoadCore [2. Load Core Assemblies]
S2[LoadAssembly O3DE.Core.dll<br/>LoadAssembly O3DE.Gems.dll<br/>- Must happen on main thread<br/>- Register internal calls]
end
subgraph RegNative [3. Register Native Methods]
S3[RegisterInternalCalls<br/>- Map C# InternalCall to C++ functions<br/>- Must complete before any script]
end
subgraph LoadUser [4. Load User Assemblies]
S4[LoadAssembly UserGameScripts.dll<br/>- Can be triggered on-demand<br/>- Supports hot-reload in Editor]
end
Ready[5. Ready for Script Execution]
Startup --> Init
Init --> LoadCore
LoadCore --> RegNative
RegNative --> LoadUser
LoadUser --> Ready
```
---
## 7. Threading Model
### 7.1 Threading Challenges
C# scripting in a game engine presents several threading challenges:
1. **Assembly Loading**: Must happen on the thread that owns the .NET runtime
2. **Script Callbacks**: OnUpdate, OnCreate must be called from appropriate threads
3. **Native Calls**: C# calling into C++ must respect O3DE's threading model
4. **EBus Calls**: Many EBuses are main-thread only
5. **Hot Reload**: Assembly unload/reload requires thread coordination
### 7.2 Thread Ownership Model
```mermaid
graph TD
subgraph MainThread [MAIN THREAD<br/>Owns: .NET Runtime, Assembly Loading, Most EBuses]
CHM[CoralHostManager<br/>- All Coral API calls<br/>- Assembly loading/unloading<br/>- ManagedObject creation/destruction<br/>- Internal call invocation]
SLC[Script Lifecycle Callbacks<br/>- OnCreate<br/>- OnDestroy<br/>- OnUpdate]
EBus[EBus Operations<br/>- Most EBus broadcasts/events<br/>- TransformBus, EntityBus, etc.]
end
subgraph JobThreads [JOB THREADS<br/>Physics, Asset Loading, Networking, etc.]
NoExec[NO DIRECT SCRIPT EXECUTION]
JobLogic[If job needs to call C# code:<br/>1. Queue a callback to main thread<br/>2. Main thread executes the C# code<br/>3. Result is communicated back if needed]
end
JobThreads -.->|Queue Callback| MainThread
```
### 7.3 Thread Dispatch System
```cpp
class ScriptThreadDispatcher
{
public:
// Queue a script operation to run on the main thread
using ScriptOperation = AZStd::function<void()>;
void QueueOnMainThread(ScriptOperation operation);
// Process all queued operations (called from main thread tick)
void ProcessQueue();
// Check if current thread is the script thread (main thread)
bool IsScriptThread() const;
// Assert we're on the script thread
void AssertScriptThread() const;
private:
AZStd::mutex m_queueMutex;
AZStd::queue<ScriptOperation> m_pendingOperations;
AZStd::thread::id m_mainThreadId;
};
// Usage in CSharpScriptComponent
void CSharpScriptComponent::OnTick(float deltaTime, AZ::ScriptTimePoint)
{
// Already on main thread, safe to call scripts
AZ_Assert(m_dispatcher.IsScriptThread(), "OnTick must be on main thread");
if (m_managedObject)
{
m_managedObject->InvokeMethod("OnUpdate", deltaTime);
}
}
// Usage from a job thread
void PhysicsCallback::OnCollision(const CollisionEvent& event)
{
// NOT on main thread - must queue
m_dispatcher.QueueOnMainThread([this, event]() {
if (m_managedObject)
{
m_managedObject->InvokeMethod("OnCollision", event.ToManaged());
}
});
}
```
### 7.4 Assembly Loading Thread Safety
```cpp
class CoralHostManager
{
public:
// All assembly operations must happen on main thread
void LoadAssembly(const AZStd::string& path)
{
AssertMainThread();
AZStd::lock_guard<AZStd::mutex> lock(m_assemblyMutex);
// Load via Coral
Coral::ManagedAssembly* assembly = m_hostInstance->LoadAssembly(path);
if (assembly)
{
m_loadedAssemblies[path] = assembly;
RegisterInternalCallsForAssembly(assembly);
}
}
void UnloadAssembly(const AZStd::string& path)
{
AssertMainThread();
AZStd::lock_guard<AZStd::mutex> lock(m_assemblyMutex);
// Must destroy all ManagedObjects first
DestroyManagedObjectsForAssembly(path);
// Then unload
m_hostInstance->UnloadAssembly(m_loadedAssemblies[path]);
m_loadedAssemblies.erase(path);
}
private:
void AssertMainThread()
{
AZ_Assert(AZStd::this_thread::get_id() == m_mainThreadId,
"Coral operations must be on main thread");
}
AZStd::mutex m_assemblyMutex;
AZStd::thread::id m_mainThreadId;
AZStd::unordered_map<AZStd::string, Coral::ManagedAssembly*> m_loadedAssemblies;
};
```
### 7.5 Thread Safety Guidelines
| Operation | Thread | Synchronization |
|-----------|--------|-----------------|
| Assembly Load/Unload | Main only | Mutex |
| ManagedObject Create/Destroy | Main only | Mutex |
| Script method invocation | Main only | None needed |
| Internal call execution | Main thread context | None needed |
| EBus broadcast from C# | Main only (enforced) | None needed |
| Physics callbacks | Queue to main | Thread dispatch |
| Network callbacks | Queue to main | Thread dispatch |
| Asset load callbacks | Queue to main | Thread dispatch |
---
## 8. Script Execution Model
### 8.1 Script Component Lifecycle
```mermaid
graph TD
Activate[Entity Activation]
subgraph ActivateState [CSharpScriptComponent::Activate]
S1[1. Resolve script class from ScriptAsset]
S2[2. Create ManagedObject instance via Coral]
S3[3. Store native entity handle in managed object]
S4[4. Call managed OnCreate]
S5[5. Connect to TickBus for updates]
end
subgraph ActiveState [Active State]
FrameLoop[Every frame:<br/>- TickBus::OnTick triggers<br/>- Call managed OnUpdate deltaTime]
EventLoop[On events:<br/>- Physics callbacks -> OnCollision<br/>- Input callbacks -> OnInput<br/>- Custom EBus -> OnCustomEvent]
end
subgraph DeactivateState [CSharpScriptComponent::Deactivate]
D1[1. Disconnect from TickBus]
D2[2. Call managed OnDestroy]
D3[3. Release ManagedObject reference]
D4[4. Coral handles GC cleanup]
end
Activate --> ActivateState
ActivateState -.->|Result| ActiveState
ActiveState -.->|Deactivate| DeactivateState
S1 --> S2
S2 --> S3
S3 --> S4
S4 --> S5
D1 --> D2
D2 --> D3
D3 --> D4
```
### 8.2 Internal Call Implementation
C# methods marked with `[InternalCall]` are bound to C++ functions:
```cpp
// C++ side - Native method registration
void ScriptBindings::RegisterInternalCalls()
{
// Entity methods
Coral::InternalCall::Register(
"O3DE.Core.Entity::GetName",
&ScriptBindings::Entity_GetName);
Coral::InternalCall::Register(
"O3DE.Core.Transform::GetWorldPosition",
&ScriptBindings::Transform_GetWorldPosition);
Coral::InternalCall::Register(
"O3DE.Core.Transform::SetWorldPosition",
&ScriptBindings::Transform_SetWorldPosition);
// EBus methods
Coral::InternalCall::Register(
"O3DE.Core.EBus.TransformBus::GetWorldPosition",
&ScriptBindings::TransformBus_GetWorldPosition);
}
// Implementation example
Coral::String32 ScriptBindings::Entity_GetName(AZ::EntityId entityId)
{
AZStd::string name;
AZ::ComponentApplicationBus::BroadcastResult(
name,
&AZ::ComponentApplicationRequests::GetEntityName,
entityId);
return Coral::String32(name.c_str());
}
AZ::Vector3 ScriptBindings::Transform_GetWorldPosition(AZ::EntityId entityId)
{
AZ::Vector3 position = AZ::Vector3::CreateZero();
AZ::TransformBus::EventResult(
position,
entityId,
&AZ::TransformBus::Events::GetWorldTranslation);
return position;
}
```
### 8.3 EBus Access Patterns
```csharp
// C# user code - Two approaches
// Approach 1: Strongly-typed generated wrapper (recommended)
public class PlayerController : ScriptComponent
{
public override void OnUpdate(float deltaTime)
{
// Uses generated TransformBus wrapper
Vector3 position = TransformBus.GetWorldPosition(EntityId);
position += new Vector3(1, 0, 0) * deltaTime;
TransformBus.SetWorldPosition(EntityId, position);
}
}
// Approach 2: Dynamic reflection (for uncommon buses)
public class DynamicExample : ScriptComponent
{
public override void OnUpdate(float deltaTime)
{
// Uses reflection system for any BehaviorContext-reflected EBus
NativeReflection.SendEBusEvent(
"SomeCustomBus",
"SomeEvent",
EntityId,
42.0f);
}
}
```
---
## 9. Developer Experience
### 9.1 The Problem
Without generated bindings, developers would need to:
1. Know the exact EBus name as a string
2. Know the exact method name as a string
3. Know the parameter types and order
4. Have no IntelliSense or compile-time checking
5. Discover APIs through documentation only
```csharp
// BAD: Without bindings - error-prone, no IntelliSense
NativeReflection.SendEBusEvent("TransfromBus", "SetWordPosition", entityId, position);
// ^ typo ^ typo - runtime errors!
```
### 9.2 The Solution
With generated bindings:
1. Full IntelliSense support
2. Compile-time type checking
3. XML documentation from BehaviorContext
4. Familiar object-oriented patterns
```csharp
// GOOD: With generated bindings - type-safe, discoverable
TransformBus.SetWorldPosition(entityId, position);
// IntelliSense shows all available methods!
```
### 9.3 Development Workflow
```mermaid
graph TD
subgraph Setup [1. Project Setup One Time]
Cmd1[$ cd YourProject]
Cmd2[$ python Scripts/generate_bindings.py --project .]
Cmd3[$ cd Generated/CSharp && dotnet build]
Cmd4[$ # Add reference to generated DLLs in your script project]
end
subgraph Write [2. Write Scripts Daily Work]
Code["// MyPlayerController.cs<br/>using O3DE.Core;<br/>using O3DE.Generated.PhysX;<br/>class MyScript : ScriptComponent<br/>..."]
end
subgraph Build [3. Build and Test]
B1[$ dotnet build]
B2[$ # Editor hot-reloads automatically]
B3[$ # Test in Editor play mode]
end
subgraph Regen [4. Regenerate When Gems Change]
R1[# When adding/removing gems<br/># When Engine updates<br/># When Custom gem adds types]
R2[$ python Scripts/generate_bindings.py]
end
Setup --> Write
Write --> Build
Build --> Regen
Regen --> Write
```
### 9.4 Editor Integration
The Editor-side of the the gem *will need to provide*:
1. **Script Component Inspector**:
- Dropdown to select script class
- Exposed public properties editable in UI
2. **Console Commands**:
```
o3desharp_generate_bindings - Regenerate all bindings
o3desharp_reload_scripts - Hot-reload user assemblies
o3desharp_list_scripts - List available script classes
```
3. **Asset Processor Integration**:
- Detects changes to .cs files
- Triggers recompilation
- Notifies Editor for hot-reload
---
## 10. Hot Reload System
### 10.1 Hot Reload Scope
| What Can Hot Reload | What Cannot |
|---------------------|-------------|
| User script assemblies | Generated binding assemblies |
| Script logic changes | New internal calls |
| New script classes | Structural changes to ScriptComponent |
| Property value changes | Changes to referenced types |
### 10.2 Hot Reload Sequence
```mermaid
graph TD
S1["Step 1: Detect Assembly Change<br/>File watcher detects: MyGame.Scripts.dll changed"]
S2["Step 2: Pause Script Execution<br/>• Stop calling OnUpdate for all scripts<br/>• Queue any pending events"]
S3["Step 3: Serialize Script State<br/>For each active CSharpScriptComponent:<br/>• Call managed OnBeforeReload<br/>• Serialize public field values<br/>• Store entity reference"]
S4["Step 4: Unload Old Assembly<br/>• Destroy all ManagedObjects from old assembly<br/>• Call Coral::UnloadAssembly<br/>• GC.Collect to clean up"]
S5["Step 5: Load New Assembly<br/>• Call Coral::LoadAssembly with new path<br/>• Verify assembly loaded successfully"]
S6["Step 6: Recreate Script Instances<br/>For each CSharpScriptComponent:<br/>• Create new ManagedObject from new assembly<br/>• Deserialize saved field values<br/>• Call managed OnAfterReload"]
S7["Step 7: Resume Script Execution<br/>• Resume OnUpdate calls<br/>• Process queued events"]
S1 --> S2
S2 --> S3
S3 --> S4
S4 --> S5
S5 --> S6
S6 --> S7
```
### 10.3 State Serialization
```csharp
// User's script with serializable state
public class EnemyAI : ScriptComponent
{
// Public fields are automatically serialized during reload
public float Health = 100f;
public int PatrolIndex = 0;
public Vector3 TargetPosition;
// Private fields with attribute can opt-in
[SerializeField]
private float _attackCooldown;
// Transient fields are not serialized (reset on reload)
[NonSerialized]
private object _cachedPathfinder;
// Called before hot reload - save complex state
public override void OnBeforeReload()
{
// Custom serialization for complex objects if needed
}
// Called after hot reload - restore complex state
public override void OnAfterReload()
{
// Recreate transient objects
_cachedPathfinder = FindPathfinder();
}
}
```
---
## 11. Performance Considerations
### 11.1 Performance Targets
| Metric | Target | Notes |
|--------|--------|-------|
| Script creation | < 1ms | Per ManagedObject |
| OnUpdate overhead | < 10μs | Per script per frame |
| Internal call overhead | < 1μs | Per call |
| Hot reload total | < 500ms | For typical project |
| Memory per script | < 1KB | Managed side only |
### 11.2 Optimization Strategies
#### Batched Updates
```cpp
// Instead of calling each script individually
void ScriptSystem::OnTick(float deltaTime)
{
// Batch all script updates into single managed call
// Reduces managed/native transition overhead
m_scriptBatcher.BeginBatch();
for (auto& script : m_activeScripts)
{
m_scriptBatcher.QueueUpdate(script.GetManagedObject(), deltaTime);
}
// Single transition into managed code
m_scriptBatcher.ExecuteBatch();
}
```
#### Cached Method Handles
```cpp
// Cache method lookups
class CSharpScriptComponent
{
// Resolved once, reused every frame
Coral::ManagedMethod* m_onUpdateMethod = nullptr;
Coral::ManagedMethod* m_onCreateMethod = nullptr;
void CacheMethodHandles()
{
if (m_managedObject)
{
m_onUpdateMethod = m_managedObject->GetMethod("OnUpdate", 1);
m_onCreateMethod = m_managedObject->GetMethod("OnCreate", 0);
}
}
};
```
#### Lazy Loading
```cpp
// Don't create ManagedObjects until entity activates
void CSharpScriptComponent::Activate()
{
// Only now create the managed instance
CreateManagedObject();
}
```
### 11.3 Memory Management
- **Object Pooling**: Reuse ManagedObject wrappers where possible
- **Weak References**: Use for cached script references
- **Explicit Disposal**: IDisposable pattern for native resources
- **GC Pressure Monitoring**: Track allocations in hot paths
---
### 11.4 AOT Support Considerations
---
## 12. Security Considerations
### 12.1 Sandboxing
C# scripts run with full trust in the .NET runtime. For shipped games:
1. **Code Signing**: Sign game assemblies to prevent tampering
2. **No Runtime Compilation**: All scripts are pre-compiled
3. **Limited Reflection**: Generated bindings only expose safe APIs
### 12.2 Sensitive APIs
Certain operations should be restricted or audited:
| API Category | Restriction |
|--------------|-------------|
| File System | Sandboxed to project directories |
| Network | Logged, rate-limited in Editor |
| Process Execution | Disabled by default |
| Reflection | Limited to game assemblies |
---
## 13. Platform Support
### 13.1 Supported Platforms
| Platform | .NET Runtime | Status |
|----------|--------------|--------|
| Windows x64 | .NET 8.0 | Full support |
| Linux x64 | .NET 8.0 | Full support |
| macOS (Apple Silicon) | .NET 8.0 | Planned |
| Android | Mono/NativeAOT | Future consideration |
| iOS | NativeAOT | Future consideration |
| Consoles | TBD | Requires AOT compilation |
### 13.2 Platform-Specific Considerations
#### Windows/Linux (Development Platforms)
- Full JIT compilation
- Hot reload supported
- Debug symbols available
#### Console Platforms (Future)
- Requires Ahead-of-Time (AOT) compilation
- No hot reload in production
- Reduced reflection capabilities
---
## 14. Future Considerations
### 14.1 Potential Enhancements
1. **Visual Scripting Bridge**: Generate ScriptCanvas nodes from C# scripts
2. **Debugger Integration**: VS/Rider debugging of live scripts
3. **Profiler Integration**: C# script profiling in O3DE profiler
4. **Asset References**: Proper C# types for O3DE asset references
5. **Editor Scripting**: C# scripts for Editor tools and automation
6. **Per-Gem Assemblies**: Optional mode for large projects
7. **Source Generators**: C# source generators for compile-time binding
8. **Coral Upgrades**: Extend Coral for better Multithreading support, improved GC performance, and better error handling.
### 14.2 Technical Debt Tracking
| Item | Priority | Effort | Notes |
|------|----------|--------|-------|
| Improve class-to-gem mapping accuracy | Medium | Medium | Machine learning on code patterns? |
| Reduce hot reload time | Medium | High | Incremental reload |
| Add more marshal types | Low | Low | As needed |
| Console platform support | Low | High | Requires AOT |
---
## 15. Appendices
### 15.1 Glossary
| Term | Definition |
|------|------------|
| **BehaviorContext** | O3DE's runtime reflection system for scripting |
| **Coral** | .NET host library used for C# integration |
| **EBus** | O3DE's event bus system for decoupled communication |
| **Internal Call** | P/Invoke-style call from C# to native code |
| **ManagedObject** | Coral wrapper for a C# object instance |
| **Hot Reload** | Replacing running code without restarting |
### 15.2 File Structure
```
Gems/O3DESharp/
├── Code/
│ ├── Source/
│ │ ├── Scripting/
│ │ │ ├── Reflection/
│ │ │ │ ├── BehaviorContextReflector.h/cpp
│ │ │ │ └── GenericDispatcher.h/cpp
│ │ │ ├── BindingGenerator/
│ │ │ │ ├── CSharpBindingGenerator.h/cpp # C++ (unused at runtime)
│ │ │ │ ├── GemDependencyResolver.h/cpp # C++ (unused at runtime)
│ │ │ │ └── ReflectionDataExporter.h/cpp # Exports JSON
│ │ │ ├── CoralHostManager.h/cpp
│ │ │ ├── ScriptBindings.h/cpp
│ │ │ └── CSharpScriptComponent.h/cpp
│ │ └── ...
│ └── CMakeLists.txt
├── Editor/
│ └── Scripts/
│ ├── __init__.py
│ ├── gem_dependency_resolver.py
│ ├── csharp_binding_generator.py
│ └── generate_bindings.py
├── Assets/
│ └── Scripts/
│ └── O3DE.Core/ # Core C# runtime library
│ ├── O3DE.Core.csproj
│ ├── ScriptComponent.cs
│ ├── Entity.cs
│ └── ...
├── Docs/
│ └── TechnicalDesignDocument.md # This document
├── gem.json
└── README.md
```
### 15.3 References
- [Coral GitHub Repository (WD Studios Fork)](https://github.com/WatchDogStudios/Coral)
- [O3DE BehaviorContext Documentation](https://www.o3de.org/docs/user-guide/programming/components/reflection/behavior-context/)
- [.NET Hosting APIs](https://docs.microsoft.com/en-us/dotnet/core/tutorials/netcore-hosting)
- [O3DE EBus Documentation](https://www.o3de.org/docs/user-guide/programming/messaging/ebus/)
---
### Document Editors
| User | GitHub | Contact Email |
| -------- | -------- | -------- |
| Scrumpy [WD Studios] | JailbreakPapa | mikaelamanfo@wdstudios.tech |
---
**Document History**
| Version | Date | Author | Changes |
|---------|------|--------|---------|
| 0.2-Draft | 01-06-2026 | O3DE Contributors | Initial draft |
Copyright (c) Contributors to the Open 3D Engine Project.