Table of Contents

Написание интеграции с движком

IcyUI рисует и читает ввод через несколько интерфейсов, и интеграция с движком их реализует. Это руководство перечисляет, что она предоставляет, в порядке построения, на примере FNA. В FNA есть API XNA, поэтому каркас похож на интеграцию с MonoGame, которая служит полным образцом: IcyUI.MonoGame на GitHub.

Примечание

IcyUI.FNA сегодня — заготовка; пакета для FNA пока нет. Руководство описывает, как строится интеграция, а не готовую интеграцию. Особенности FNA, например его нативные библиотеки, здесь не рассматриваются.

Вам понадобится хорошее знание целевого движка и страницы Canvas, хост и дерево элементов. Игре ничего из этого не нужно: она лишь использует существующую интеграцию.

Что предоставляет интеграция

Часть Интерфейс Что делает
Рисование IRenderContext Рисует текстурированные прямоугольники внутри sprite batch, создаёт текстуры, сообщает область просмотра и масштаб дисплея.
Ввод IInputSystem Превращает состояние клавиатуры, мыши, геймпада и касаний в события IcyUI; ввод текста и буфер обмена.
Ресурсы IAssetContextFactory, IPlatformAssetImporter<T> Открывает контент движка и превращает потоки изображений в текстуры.
Хост IIcyHost Игровой компонент, который связывает три предыдущих с игровым циклом и владеет Canvas.
Точка входа UseIcyUI(this Game, ...) Строит конфигурацию и добавляет хост.

Ядро IcyUI никогда не ссылается на движок; всё перечисленное — единственный код, который это делает.

1. Контекст отрисовки

IRenderContext рисует только текстурированные прямоугольники: Canvas вызывает Begin, затем Draw для каждого прямоугольника, глифа и изображения, затем End. Сплошной цвет — это закрашенный WhiteTexture.

/// <summary>What an engine gives IcyUI to draw with. Everything IcyUI draws is textured quads in this order: Begin, many Draw, End.</summary>
public abstract class EngineRenderContext : IRenderContext
{
    public abstract event EventHandler? ViewportResize;

    public abstract IRenderOptions Options { get; }      // opacity, scissor rectangle: the engine applies them to what follows

    public abstract ITexture WhiteTexture { get; }       // solid fills are tinted draws of this

    public abstract Transform2D Transform { get; set; }  // applied to every Draw

    public abstract Size ViewportSize { get; }           // the back buffer, in pixels

    public abstract void Begin();                        // open a sprite batch (straight alpha, scissor test on)

    public abstract void Draw(ITexture texture, in TextureRenderingOptions options);   // one quad

    public abstract void End();                          // submit the batch

    public abstract void Flush();                        // End then Begin, when a state changes mid-frame (an effect, the scissor)

    public abstract ITexture CreateTexture<TColor>(int width, int height, TColor[] data)
        where TColor : unmanaged;                        // font atlases and generated images

    public abstract void ApplyEffect(IEffect effect);

    public abstract void ClearEffects();

    public abstract IEffect GetBuiltInEffect(EffectCode code);

    public abstract void Dispose();
}

Места, где раньше ошибались, — посмотрите их в версии для MonoGame:

  • Прямая альфа. Текстуры IcyUI не premultiplied, поэтому sprite batch должен смешивать с прямой альфой; смешивание premultiplied по умолчанию делает края сглаженного текста слишком яркими.
  • Тест ножниц. Обрезка использует прямоугольник ножниц, и состояние растеризатора должно включать тест ножниц, иначе любая обрезка молча игнорируется.
  • Flush вне Begin/End ничего не делает; он завершает и перезапускает batch при смене ножниц или эффекта.
  • Масштаб дисплея. Сообщайте масштаб DPI монитора (DisplayScale, DisplayScaleChanged), опрашивая его на каждом обновлении, чтобы работало масштабирование. Платформенных поставщиков даёт ядро.

2. Ввод

/// <summary>What an engine gives IcyUI to read input from. Each device turns the engine's state into IcyUI's events.</summary>
public abstract class EngineInputSystem : IInputSystem, IUpdateableInput
{
    public abstract IInputEventSystem Events { get; }    // the events canvases subscribe to: pointer, keys, navigation, text

    public abstract IKeyboardInput? Keyboard { get; }

    public abstract IMouseInput Mouse { get; }

    public abstract IGamepadInput? Gamepad { get; }      // null when the engine has no gamepads

    public abstract ITouchInput? Touch { get; }          // null when the engine reports no touch

    public abstract IClipboard Clipboard { get; }

    public abstract bool IsInitialized { get; }

    public abstract void Initialize();                   // hook the engine: window text input, device state

    public abstract void Update(TimeSpan deltaTime);     // poll the devices; called once per game update

    public abstract bool IsDeviceAvailable<T>()
        where T : IInputDeviceListener;

    public abstract T GetInputDevice<T>()
        where T : IInputDeviceListener;

    public abstract bool TryGetInputDevice<T>([System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out T? device)
        where T : IInputDeviceListener;

    public abstract IEnumerator<IInputDeviceListener> GetEnumerator();

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
}

Каждое устройство реализует свой интерфейс: читает состояние движка в Update и вызывает события IcyUI (нажатие, отпускание, перемещение, навигация). Текстовый ввод приходит из окна движка (события символов, IME), а не из нажатий клавиш. Если движок не умеет сообщать об устройстве, оставьте его null: Canvas справится. Буфер обмена — единственная часть, зависящая от ОС: выбирайте его селектором буфера обмена из ядра, а не пишите по одному на каждый движок.

3. Ресурсы

Контекст ресурсов открывает контент по URI (конвейер контента движка, файлы, встроенные ресурсы), а импортёр превращает поток изображения в текстуру движка. Зарегистрируйте оба в конфигурации ресурсов; в IcyUI.MonoGame есть MonoGameAssetContextFactory и TextureImporter по несколько строк каждый.

4. Хост

Хост связывает части с игровым циклом. Он рисует последним, чтобы интерфейс был поверх игры:

/// <summary>The game component that runs IcyUI: input and display scale in Update, every canvas in Draw, after the game's own drawing.</summary>
public sealed class EngineIcyHost : DrawableGameComponent, IIcyHost
{
    public EngineIcyHost(Game game, IcyConfiguration configuration)
        : base(game)
    {
        Configuration = configuration;
        MainCanvas = new Canvas(configuration) { IsInputEnabled = true };
        Canvases = [MainCanvas];
        DrawOrder = int.MaxValue;                        // drawn last, so the UI is on top of the game
    }

    public IcyConfiguration Configuration { get; }

    public Canvas MainCanvas { get; }

    public IList<Canvas> Canvases { get; }

    public override void Initialize()
    {
        Configuration.Input.Initialize();
        base.Initialize();
    }

    public override void Update(GameTime gameTime)
    {
        if (Configuration.Input is IUpdateableInput updateable)
            updateable.Update(gameTime.ElapsedGameTime);
        base.Update(gameTime);
    }

    public override void Draw(GameTime gameTime)
    {
        for (int i = 0; i < Canvases.Count; i++)     // by index: UI code may add a canvas during the frame
            Canvases[i].Render();
        base.Draw(gameTime);
    }
}

5. Точка входа

Расширение строит конфигурацию со значениями движка по умолчанию, затем запускает лямбду игры, чтобы та могла заменить любое из них, и добавляет хост:

/// <summary>The entry point a game calls: build the configuration, let the game adjust it, add the host.</summary>
public static class EngineBuildingExtensions
{
    public static EngineIcyHost UseIcyUI(this Game game, Action<IConfigurationBuilder> configure)
    {
        var builder = new IcyConfigurationBuilder();
        builder.ConfigureRendering(CreateRenderContext(game))     // your IRenderContext
               .ConfigureInput(CreateInputSystem(game))           // your IInputSystem
               .ConfigureAssets();
        builder.AddMarkupSupport();
        builder.UseDefaultTheme();
        configure(builder);                                       // the game's lambda runs after the defaults, so it can replace them

        var host = new EngineIcyHost(game, builder.Build());
        game.Components.Add(host);
        return host;
    }

    private static IRenderContext CreateRenderContext(Game game) => throw new NotImplementedException();

    private static IInputSystem CreateInputSystem(Game game) => throw new NotImplementedException();
}

Бросайте исключение, если игра вызывает его дважды, и предоставьте GetIcyHost и GetIcyCanvas, чтобы код мог позже добраться до хоста; версия для MonoGame показывает оба.

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

Пять частей, нужных движку, в порядке, позволяющем проверять каждую: нарисовать прямоугольник, прочитать щелчок, загрузить текстуру, а затем запустить страницу разметки через хост.

Дальше