Extending the Editor

The Comet editor is built on the same AngelScript you write your game in — which means you can extend it. Anything you wish the editor did — a bespoke tool in the menu bar, a specialized inspector with a "Bake" button, a whole dockable window for editing your game's data — you write in script. No C++, no recompiling the engine: save the file and the editor picks it up.

Editor scripts live in an Editor/ folder

Any folder named Editor (for example Assets/Editor/) is special. Scripts inside it are compiled only for the editor and stripped from every exported build — they're where your tooling goes so it never ships in the game. (Under the hood they're compiled with COMET_EDITOR defined; see Exporting Builds.)

There are two base classes, both in the CometEditor namespace:

Every example below starts with:

using namespace CometEngine;
using namespace CometEditor;

Adding items to the main menu bar

Put [MainMenuItem("Path/Name")] on a method of an EditorBehaviour and it appears in the top menu bar. The path builds the submenu structure; the last segment is the clickable item.

using namespace CometEngine;
using namespace CometEditor;

class ProjectTools : EditorBehaviour
{
    [MainMenuItem("Tools/Open Design Doc")]
    void OpenDesignDoc()
    {
        App::OpenURL("https://docs.google.com/document/d/your-doc-here");
    }

    [MainMenuItem("Tools/Rebuild Atlas")]
    void RebuildAtlas()
    {
        Debug::Log("Rebuilding atlas...");
        // ...run your tool: read assets, write files, call Shell::ExecuteCommand, etc.
    }
}

Tools becomes a top-level menu (or nests under an existing one), with Open Design Doc and Rebuild Atlas beneath it. This is the fastest way to wire a one-shot tool — an importer, a validator, a "download latest localizations" button — into the editor UI.

Custom inspectors

There are two levels of control over how a component looks in the Inspector.

1. Decorate the fields (the quick way)

Most of the time you don't need a full custom inspector — you just want a field labelled, clamped or hidden. Annotate the fields of your CometBehaviour directly:

using namespace CometEngine;

class Enemy : CometBehaviour
{
    [Header("Stats")]
    [Range(1, 100)] int health = 50;
    [Tooltip("Seconds between attacks")] float attackCooldown = 1.5F;

    [Space]
    [Header("Visuals")]
    [PreviewTexture] Texture portrait;

    [HideInInspector] float internalTimer;   // still serialized, just not shown
    [ReadOnly] int spawnId;                   // shown, but greyed out
}

The field attributes:

Attribute Effect
[Header("...")] A bold section label above the following field.
[Tooltip("...")] Hover help on the field.
[Range(min, max)] Draw a slider; [Min(n)] / [Max(n)] clamp one end.
[Space] A vertical gap.
[HideInInspector] Keep the field serialized but hide it from the Inspector.
[ReadOnly] Show the value greyed-out and non-editable.
[PreviewTexture] Draw a thumbnail for a Texture / Sprite field.
[TreeNodeDefaultOpen] Start a nested object expanded.
[AssetIcon] Use the referenced asset's icon.

2. Draw the whole inspector yourself

When you want buttons, coloured text, live previews or conditional layout, take the inspector over completely. Put [CustomInspector("TargetType")] on an EditorBehaviour and implement OnCustomInspector:

using namespace CometEngine;
using namespace CometEditor;

[CustomInspector("Enemy")]
class EnemyInspector : EditorBehaviour
{
    void OnCustomInspector(Enemy target)
    {
        GUI::TextColored(Color::red, "Enemy - danger level " + target.health);

        GUI::ShowProperty("health");          // the default widget for one field
        GUI::ShowProperty("attackCooldown");

        if (GUI::Button("Kill"))
            target.health = 0;
        GUI::SameLine();
        if (GUI::Button("Full Heal"))
            target.health = 100;

        GUI::ShowTexture(target.portrait, Vector2(150, 100));
    }
}

The GUI API

Everything you draw — in an inspector or a window — goes through the immediate-mode GUI namespace. You call a widget every frame, and its return value is the interaction: GUI::Button returns true on the frame it's clicked, input widgets return the edited value.

GUI::Text("A plain label");
GUI::TextColored(Color::green, "A coloured one");

if (GUI::Button("Do it")) { /* clicked this frame */ }

// Input widgets take the current value and return the (possibly) edited one.
// Store that value in a member field so it survives to the next frame.
count   = GUI::InputInt("Count", count);
speed   = GUI::DragInt("Speed", speed, 0.1F);   // drag left/right to scrub
enabled = GUI::Checkbox("Enabled", enabled);

bool edited = false;
name = GUI::InputText("Name", name, edited, GUI::InputTextFlags::EnterToAccept);
if (edited) Debug::Log("Committed: " + name);

if (GUI::BeginCombo("Mode", mode, GUI::ComboFlags::HeightLargest))
{
    if (GUI::Selectable("Easy", mode == "Easy")) mode = "Easy";
    if (GUI::Selectable("Hard", mode == "Hard")) mode = "Hard";
    GUI::EndCombo();
}

A few things you'll reach for constantly:

[!TIP] Bracket a change with GUI::SaveState() right before you mutate the target, and the edit joins the editor's undo history — Ctrl+Z restores the previous value.

Custom editor windows

For bigger tools — a level validator, an asset browser, a spawn-table editor — subclass EditorWindow. Its OnGUI() runs every frame the window is open, drawn with the same GUI API:

using namespace CometEngine;
using namespace CometEditor;

[MainMenuItemWindow("Tools/Spawn Editor", "Spawn Editor")]
class SpawnEditor : EditorWindow
{
    private array<string> entries = {"Goblin", "Slime", "Bat"};
    private string filter;

    void Awake()
    {
        saveChangesMessage = "Save changes to the spawn table?";
    }

    void OnGUI()
    {
        GUI::Text("Spawn table");
        filter = GUI::InputText("Filter", filter);
        GUI::TextFilter tf = GUI::TextFilter(filter);

        int toRemove = -1;
        for (uint i = 0; i < entries.length(); i++)
        {
            if (!tf.Pass(entries[i])) continue;

            GUI::PushIDNum(i);                 // unique id per row
            GUI::Text(entries[i]);
            GUI::SameLine();
            if (GUI::Button("Remove")) toRemove = int(i);
            GUI::PopID();
        }
        if (toRemove >= 0)                     // mutate after the loop, not during
        {
            entries.removeAt(toRemove);
            hasUnsavedChanges = true;
        }

        if (GUI::Button("Add Goblin"))
        {
            entries.insertLast("Goblin");
            hasUnsavedChanges = true;
        }
    }

    WindowConfig OnGetWindowConfig()
    {
        WindowConfig config;
        config.initialSize = Vector2i(360, 480);
        config.initialPositionType = WindowConfigPositionType::CENTERED;
        config.dockable = true;
        config.iconRaw = RawIcon::AddressBook;
        return config;
    }
}

Opening a window

Two ways, and you'll often use both:

Configuring the window

OnGetWindowConfig() is optional and sets the window up the first time it appears. Beyond the fields above, WindowConfig also carries initialPosition (with WindowConfigPositionType::CUSTOM), resizable, hasMenuBar, hasCloseButton, hasTitleBar, canCollapse and more. iconRaw takes a RawIcon:: name for the tab icon.

A menu bar inside your window

Set hasMenuBar and draw one at the top of OnGUI with the menu widgets:

if (GUI::BeginMenuBar())
{
    if (GUI::BeginMenu("File"))
    {
        if (GUI::MenuItem("Save"))  Save();
        if (GUI::MenuItem("Close")) Close();
        GUI::EndMenu();
    }
    GUI::EndMenuBar();
}

Unsaved changes

Set hasUnsavedChanges = true whenever the user edits something. If they try to close the window with unsaved work, the editor shows a confirmation using your saveChangesMessage, then calls OnSaveChanges() or OnDiscardChanges() so you can react:

void OnSaveChanges()    { WriteTableToDisk(); hasUnsavedChanges = false; }
void OnDiscardChanges() { ReloadTableFromDisk(); }

Windows also expose Focus(), Show(), Hide(), Close(), and read-only state like isDocked, isFocused, isHovered and isVisible — handy when one tool drives another.

Custom asset types on the Create menu

[AssetMenu("Display Name", "Path/In/Create/Menu")] on a CometObject script adds an entry to the Project panel's Create menu, so you (and your team) can make instances of your own data types right in the editor:

using namespace CometEngine;

[AssetMenu("Dialogue Table", "Gameplay/Dialogue Table")]
class DialogueTable : CometObject
{
    array<string> speakers;
    array<string> lines;
}

Now Create → Gameplay → Dialogue Table drops a new DialogueTable asset into the project, editable in the Inspector — and skinnable with a [CustomInspector("DialogueTable")] of its own.

Where to go next

Editor tooling compounds. A [CustomInspector] with a Bake button, an EditorWindow that lists every broken reference in your scenes, a [MainMenuItem] that kicks off your export pipeline — each one shaves minutes off every day. And because it all lives in an Editor/ folder, none of it ships in the game: it exists purely to make building the game faster.