For my GAM300/350 class at DigiPen, I was a part of a team of 8 Computer Science majors with the goal to build a custom game engine in C++ and use it to build a game over the course of the two semester class. We started in fall by building out the basics of an engine, the entirety of which was defined by large scope for portfolio spectacle. Our Graphics programmer built a Vulkan renderer, we had a custom Audio mixer and track system to integrate with scripting, our Physics programmer implemented Jolt. But most important to this, was our custom scripting system, Prose.
Our Game was based heavily on existing games like Kodu Game Lab, Project Spark, and Dreams. As such we needed to keep everything approachable for a beginner, and as far as logic goes this can be tricky, but we decided to give it a try. I (as the UI/UX programmer on the team) implemented the Clay UI library and used it to build a drag and drop block editor on top of the custom scripting language our Systems programmer built. This writeup covers the UI layer; the panels, the blocks, and how drag-and-drop works.
The scripting editor is a two-panel view that opens when you select a script file from the properties panel.

Left panel, the block sidebar
NOTE: The "Whiteboard" was our solution to variables, a per-object key value storage.
Right panel, the sentence canvas

A sentence is the basic logical unit in Prose: WHEN [conditions] THEN [actions].
// Generated Prose Code, line 3 in above screenshot
sentence Sentence3:
{
events: told("take_damage");
when:
{
return ((wbget(self, "lives") < 1) and (wbget(self, "immunity_timer") < 0));
}
then:
{
on_start;
tell(self, "die");
}
}
A trash icon sits to the left of each sentence to delete it entirely. Dropping any non-sidebar block back onto the sidebar also deletes it.
Blocks are the draggable pieces. Every block is a horizontal strip of content. It contains a mix of static text labels and interactive input slots.

Each block has:
Blocks are defined entirely in JSON files under res/Scripting/blocks/. Adding a new block means adding an entry to the right category file.
{
"category": {
"id": "wb",
"name": "Whiteboard",
"description": "Use these blocks to directly interact with the values in an object's whiteboard",
"color": {
"text": [ 255, 255, 255 ],
"bg": [ 139, 92, 246]
}
},
"blocks": {
"wbset": {
"name": "Set",
"description": "Set the value of a variable on the whiteboard of this object.",
"argument": false,
"placement_restrictions": {
},
"content": [
{
"type": "STRING",
"label": "variable",
"id": 1
},
{
"type": "TEXT",
"text": "to"
},
{
"type": "ANY",
"label": "value",
"id": 2
}
],
"output": "wbset(self, $1, $2)"
},
}
}Input slots are the interactive pieces embedded inside a block. Each slot has a type:
| Type | Widget | Example |
|---|---|---|
| String | Text field | A dialog line |
| Number | Text field | A distance value |
| Enum | Dropdown | Direction (Left/Right/Up/Down) |
| Variable | Dropdown | A named world-board variable |
| Object Reference | Dropdown | A prefab to spawn |
| SFX / Music | Dropdown | An audio asset |
| Entity Reference | (placeholder) | A live entity |
| Boolean | (placeholder) | True/False |
Dropdown inputs populate their options from the project registry at runtime, so the list of variables or audio assets updates automatically as the project changes.
An input slot can also accept an inner block as its value instead of a typed input. The slot becomes a drop zone and the nested block's output is used in its place.
The DND system has three parts: Draggable, Droppable, and DNDManager.
Draggable is attached to every block. It tracks:
std::any) carrying the block's ID and whether it came from the sidebarDroppable is attached to every drop zone. It tracks:
Draggable was released over it (cleared each frame after handling)DNDManager is the coordinator. There is one per editor. It:
Droppable fires with a pointer to the released DraggableThe editor saves each script as two files:
.json file storing the block tree (which blocks, in which order, with what input values).prose file, the compiled text output that the runtime readsSaving happens automatically on every block drop and explicitly when you hit the Back button. The Back button also triggers a recompile of the .prose file and returns you to the properties panel. the JSON save format mirrors the block tree exactly, so loading is just reconstructing blocks from their category+id template and re-filling the saved input values.