Opening

The first post built Ditto’s foundation: GameObject organizes objects, Component carries behavior, and the editor edits and previews the scene. But at that stage the engine was still basically a C++ program. Every gameplay rule had to be hardcoded into native code, and every change required a native rebuild.

The next necessary system is scripting. For a small engine, scripting is not just “running user code.” It defines the boundary between the engine and the game project: what stays in native engine code, what belongs to game scripts, how fields appear in the editor, and how scripts are carried into the final build.

Ditto uses C#. The reason is practical: the language is lightweight, the ecosystem is mature, and the Unity-style workflow is familiar enough. The runtime is Mono, embedded into the C++ engine.

Scripting System

Layered Structure

The scripting boundary can be viewed as three layers:

1
2
3
4
5
6
Game Project Scripts
-> GameScripts.dll
Ditto C# API
-> DittoEngine.dll
Native Engine
-> C++ GameObject / Component / Physics / Audio / UI

Project .cs files do not talk to C++ directly. They reference DittoEngine.dll, then use wrapper classes such as MonoBehaviour, GameObject, Transform, Input, Rigidbody2D, and AudioSource. The actual work eventually crosses into native internal calls: moving transforms, reading input, instantiating objects, destroying objects, or dispatching collision callbacks.

This keeps the boundary explicit. The C# side exposes APIs that are comfortable for gameplay code, while the C++ side owns the actual data layout and lifetime rules. When a new runtime module is added, a binding layer is enough to make it available to scripts.

Mono Runtime

Ditto does not statically link Mono into the engine. It dynamically loads the runtime, finds mono-2.0-sgen.dll, initializes a domain, then loads the required assemblies.

The runtime path needs to handle several jobs:

  • initialize the Mono domain
  • configure assembly search paths
  • load DittoEngine.dll
  • load project GameScripts.dll
  • register native internal calls
  • create script instances and attach them to C++ components

Once a script component is attached to a GameObject, the engine invokes methods such as Start, Update, and FixedUpdate. Collision events, button events, and object lifetime events return to C# through the same boundary.

API Assembly

DittoEngine.dll is the API surface seen by scripts. The project keeps Ditto/3rdParty/Mono/DittoEngine.csproj, targeting netstandard2.0, and also keeps a direct csc build script for quickly producing the DLL without depending on the full .NET project flow. Game scripts reference this DLL instead of including native headers or knowing about C++ implementation details.

A simplified gameplay script looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using DittoEngine;

public class BirdController : MonoBehaviour
{
public float force = 10.0f;
private Rigidbody2D body;

public override void Start()
{
body = GetComponent<Rigidbody2D>();
}

public override void Update()
{
if (Input.GetMouseButtonDown(0))
{
body.AddForce(Vector2.right * force);
}
}
}

The script sees a normal component API, but the call chain crosses the C# wrapper, Mono internal call, and native component. The point is to hide that chain from users and let it feel like a built-in engine feature.

Editor Workflow

Compilation and Hot Reload

Editor-side script compilation is handled by CSharpScriptCompiler. It locates the C# compiler and the required references, including:

  • DittoEngine.dll
  • mscorlib.dll
  • netstandard.dll
  • project script files

On success it outputs a DLL. On failure it records errors, warnings, and compiler output so the Inspector can show the latest compile status. Script errors should not live only in a terminal window; the editor needs to explain why a component is not running.

When building a Windows player, BuildSystem compiles project scripts into GameScripts.dll, then copies DittoEngine.dll and the required Mono runtime files. The final output does not need source files, only the managed assemblies and the native executable.

The goal of hot reload here is not a perfect managed runtime replacement. The goal is fast enough day-to-day iteration. The editor detects script changes, recompiles, loads the new assembly on success, and lets script components rebind their types.

The failure path matters, but this implementation is not a perfect managed-instance swap. The old C# instance is unloaded before the new one is compiled and loaded. If compilation or loading fails, the scene object, component, and serialized field data remain, but the script instance will only come back after a successful compile. That tradeoff is acceptable for this stage: the project structure should not be damaged just because one script build failed.

Serialized Fields

The scripting system also has to support editor serialization. If a script field should be edited in the Inspector, the editor must be able to discover it, draw it, save it, and load it.

The current implementation supports public fields and explicit markers such as [SerializeField]. The editor scans the script type and turns those fields into editable Inspector entries. Gameplay parameters can stay in project scripts instead of forcing every tuning change into C++.

This is less flashy than a renderer backend, but it is what makes scripting usable in an editor workflow. A script that runs is code; a script with saved fields becomes part of the project.

Binding Scope

The earliest version only needed Transform and GameObject bindings. As the project grew, the binding surface expanded. Ditto currently exposes Transform / GameObject,Instantiate / Destroy,Input / Mouse,Camera,Rigidbody2D / Collider2D,AudioSource,UI,Animator,ParticleSystem to C#.

That is why scripting deserves its own post. It is not a single feature; it is the access path that crosses almost every runtime module.

Closing

After scripting, Ditto is no longer just a runnable editor program. It can host project logic. C++ owns stable low-level systems, C# owns fast-changing gameplay code, and the editor ties fields, components, assets, and scenes together.

The next post returns to rendering. The early version was built around OpenGL directly, but the later project moved this layer behind an RHI and added DirectX 12, Vulkan, and OpenGL backends.