Table of Contents

Горячая перезагрузка и редактор в игре

В этом руководстве настраивается цикл правки интерфейса игры: меняйте разметку страницы в текстовом редакторе или в редакторе поверх запущенной игры и смотрите результат без перезапуска. Используется пакет IcyUI.Design — инструмент разработки; в выпущенной игре его нет.

Вам понадобится игра, загружающая страницу из разметки (Первый экран). Страница Редактирование разметки и инструменты дизайна описывает, что это за инструменты; здесь они подключаются и показано, как с ними работать.

1. Подключайте пакет только в Debug-сборках

Добавьте пакет и сделайте ссылку условной, чтобы релизная сборка его не несла:

<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>

Страницы загружаются из вывода сборки (bin/Debug/.../Menu.xml) — копии файла из вашего проекта. Правки в копии пропали бы при следующей сборке, поэтому инструментам нужно знать, где лежат файлы вашего проекта.

2. Подключите инструменты

Поместите этот класс в папку DevTools. Он подключает сеанс дизайна, указывает ему ваши исходные файлы, следит за ними и добавляет редактор:

/// <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();
    }
}
  • Подключайте до загрузки первой страницы. Сеанс отслеживает только страницы, загруженные после подключения.
  • UseSourceRoot — то, что заставляет работать сохранение и слежение. FindSourceRoot поднимается от папки вывода игры к папке с MyGame.csproj и возвращает Assets/UI внутри неё — там лежит разметка. Если ничего не найдено, сохранение и слежение используют копию в выводе сборки, которую перезапишет следующая сборка.
  • WatchSourceFiles включается явно. Ни за чем не следят, пока вы его не вызовете.

3. Правьте в текстовом редакторе

При запущенной игре сохраните файл .xml в проекте. Через доли секунды живая страница меняется: Width кнопки, текст, цвет, добавленный элемент. За кадром инструменты читают файл и применяют его к странице как одну правку.

  • Это работает и для редакторов, которые сохраняют, записывая временный файл и переименовывая его.
  • Несколько быстрых сохранений подряд применяются один раз.
  • Файл, текст которого совпадает с тем, что уже есть в игре, игнорируется. Поэтому сохранение из редактора в игре (шаг 4) не перезагружает само себя.

Не всякая правка может изменить живую страницу. Правка внутри Style, ControlTemplate или элемента-свойства (<Button.Content>) меняет документ, но не уже показанные элементы; документ сообщает NeedsReload. Загрузите страницу заново, чтобы увидеть её. ReloadablePage делает это по F5 из последнего текста документа:

/// <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;
    }
}

Он строит страницу из текста документа, а не через LoadMarkup, потому что LoadMarkup прочитал бы более старую копию в выводе сборки.

Теперь запустите игру с инструментами:

    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
    }

В Release-сборке ветка #else загружает страницу обычным способом, а инструменты не компилируются вовсе.

4. Правьте в игре

Нажмите F4, чтобы показать редактор поверх запущенной игры. Щёлкните элемент, чтобы выбрать его, меняйте свойства на панели справа, перетаскивайте его или добавляйте элементы из набора инструментов. Ctrl+Shift+E переключает режим правки (щелчок выбирает) и режим взаимодействия (интерфейс работает как в игре, так что можно открыть меню и править показанное в нём). Ctrl+Z и Ctrl+Y отменяют и возвращают.

Save на панели команд записывает текст документа в файл вашего проекта с сохранением форматирования и комментариев. Слежение за файлами видит запись, обнаруживает, что текст уже совпадает с документом, и ничего не делает.

5. Меню отладки вместо оверлея

Если оверлей мешает экрану, тот же редактор — это элемент управления. Поместите EditorWorkspace на страницу меню отладки вместе с сеансом дизайна из шага 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 };
}

Canvas принимает один редактор за раз: пока показан оверлей, рабочая область ждёт, и наоборот.

Что вы построили

Запущенная игра, страницы которой следуют за вашим текстовым редактором, редактор поверх неё и F5 для правок, которые не могут изменить живую страницу, — и всё это только в Debug-сборках. В релизе от этого ничего нет.

Примечание

Править можно только страницы, загруженные из разметки после подключения сеанса. Элемент, созданный в коде, выбрать нельзя, а правка стиля или шаблона требует перезагрузки по F5. NeedsReload остаётся установленным, если правка его требовала, поэтому нажимайте F5 всякий раз, когда живая страница выглядит устаревшей.

Дальше