Table of Contents

Hot reload and the in-game editor

This guide sets up the edit loop for a game's UI: change a page's markup in your text editor, or in an editor over the running game, and see the result without restarting. It uses the IcyUI.Design package, which is a development tool; a shipped game doesn't contain it.

You need a game that loads a page from markup (Your first screen). The Markup editing and design tools page describes what the tools are; this guide wires them up and shows how to work with them.

1. Reference the package in Debug builds only

Add the package, then make the reference conditional so a release build doesn't carry it:

<ItemGroup Condition="'$(Configuration)' == 'Debug'">
  <PackageReference Include="IcyUI.Design" Version="0.1.0-alpha.1" />
</ItemGroup>
<!-- The code that uses it lives in a DevTools folder, compiled in Debug only. -->
<ItemGroup Condition="'$(Configuration)' != 'Debug'">
  <Compile Remove="DevTools\**" />
</ItemGroup>

Pages are loaded from the build output (bin/Debug/.../Menu.xml), a copy of the file in your project. Editing the copy would be lost at the next build, so the tools need to know where your project's files are.

2. Attach the tools

Put this class in the DevTools folder. It attaches a design session, points it at your source files, watches them, and adds the editor:

/// <summary>The game's development tools: page editing in a text editor and in the game. Create it before the first screen loads.</summary>
public sealed class DevTools : IDisposable
{
    private readonly IDisposable watch;
    private readonly EditorOverlay overlay;

    public DevTools(IIcyHost ui, string? sourceRoot = null)
    {
        // The session sees only the pages loaded after this line.
        Design = DesignSession.Attach(ui.Configuration);

        // Pages load from the build output; Save and the file watcher use your project's copy.
        sourceRoot ??= DesignSession.FindSourceRoot("MyGame.csproj", "Assets/UI");
        if (sourceRoot != null)
            Design.UseSourceRoot(sourceRoot);

        watch = Design.WatchSourceFiles();          // a file saved in your text editor shows in the game
        overlay = EditorOverlay.Attach(ui.MainCanvas, Design);   // F4 shows and hides the in-game editor
    }

    public DesignSession Design { get; }

    public void Dispose()
    {
        overlay.Dispose();
        watch.Dispose();
        Design.Dispose();
    }
}
  • Attach before the first page loads. The session tracks only the pages loaded after it was attached.
  • UseSourceRoot is what makes saving and watching work. FindSourceRoot walks up from the game's output folder to the folder containing MyGame.csproj and returns Assets/UI inside it, which is where the markup lives. If it finds nothing, saving and watching use the copy in the build output, which the next build overwrites.
  • WatchSourceFiles is opt-in. Nothing is watched unless you call it.

3. Edit in your text editor

With the game running, save the .xml file in your project. Within a fraction of a second the live page changes: a button's Width, a text, a color, an added control. Behind the scenes the tools read the file and apply it to the page as one edit.

  • It also works for editors that save by writing a temporary file and renaming it.
  • Several saves in quick succession apply once.
  • A file whose text equals what the game already has is ignored. That is why saving from the in-game editor (step 4) doesn't reload itself.

Not every edit can change the live page. An edit inside a Style, a ControlTemplate or a property element (<Button.Content>) changes the document but not the elements already on screen; the document reports NeedsReload. Load the page again to see it. ReloadablePage does that on F5, from the document's latest text:

/// <summary>
/// A page that F5 loads again from its document's latest text. Most edits show on the live page at once; an edit inside a
/// style or a template doesn't, and this shows it.
/// </summary>
public sealed class ReloadablePage
{
    private readonly IIcyHost ui;
    private readonly DesignSession design;
    private readonly string path;
    private readonly object dataContext;
    private UIElement page;

    public ReloadablePage(IIcyHost ui, DesignSession design, string path, object dataContext)
    {
        this.ui = ui;
        this.design = design;
        this.path = path;
        this.dataContext = dataContext;
        page = ui.MainCanvas.LoadMarkup(path, dataContext);
        ui.Configuration.Input.Events.RegisterCommand(new RelayCommand(Reload), new KeyGesture(Keys.F5));
    }

    public UIElement Page => page;

    public void Reload()
    {
        // The document holds the latest text, from the file or from the editor. LoadMarkup(path) would read the build
        // output's older copy.
        DesignDocument document = design.FindDocument(page, out _) ?? throw new InvalidOperationException("The page isn't tracked.");
        UIElement fresh = new MarkupLoader(ui.Configuration).Load(document.Text, path);
        fresh.DataContext = dataContext;

        ui.MainCanvas.Remove(page);
        ui.MainCanvas.Add(fresh);
        page = fresh;
    }
}

It builds the page from the document's text and not with LoadMarkup, because LoadMarkup would read the older copy in the build output.

Now start the game with the tools:

    using Icy.Stride.Configuration;
    using Icy.UI.Controls;
    using Stride.Engine;

    public class MenuStartup : StartupScript
    {
#if DEBUG
        private DevTools? tools;
#endif

        public override void Start()
        {
            StrideIcyHost ui = Game.UseIcyUI();
            var viewModel = new MenuViewModel();

#if DEBUG
            tools = new DevTools(ui);
            _ = new ReloadablePage(ui, tools.Design, "Menu.xml", viewModel);
#else
            ui.MainCanvas.LoadMarkup("Menu.xml", viewModel);
#endif
        }

#if DEBUG
        public override void Cancel() => tools?.Dispose();
#endif
    }

In a Release build the #else branch loads the page the normal way, and the tools aren't compiled at all.

4. Edit in the game

Press F4 to show the editor over the running game. Click an element to select it, change its properties in the panel on the right, drag it, or add controls from the Toolbox. Ctrl+Shift+E switches between Edit mode (a click selects) and Interact mode (the UI works as in the game, so you can open a menu and edit what it shows). Ctrl+Z and Ctrl+Y undo and redo.

Save in the command bar writes the document's text to your project's file, with your formatting and comments intact. The file watcher sees the write, finds that the text is already the document's, and does nothing.

5. A debug menu instead of an overlay

If an overlay in the way of a screen bothers you, the same editor is a control. Put an EditorWorkspace on a page of a debug menu, with the design session from step 2:

// The same editor as a page, for a debug menu.
public static void ShowEditorPage(Icy.UI.Controls.ContentControl debugPage, DesignSession design)
{
    debugPage.Content = new EditorWorkspace { Design = design };
}

A canvas takes one editor at a time: while the overlay is shown, the workspace waits, and the other way round.

What you built

A running game whose pages follow your text editor, an editor over it, and F5 to see the edits that can't change the live page, all in Debug builds only. Nothing of it is in a release.

Note

Only pages loaded from markup after the session was attached can be edited. An element created in code can't be selected, and a style or a template edit needs the F5 reload. NeedsReload stays set once an edit needed it, so use F5 whenever the live page looks stale.

Next