Написание интеграции с движком
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 показывает оба.
Что вы построили
Пять частей, нужных движку, в порядке, позволяющем проверять каждую: нарисовать прямоугольник, прочитать щелчок, загрузить текстуру, а затем запустить страницу разметки через хост.
Дальше
- Canvas, хост и дерево элементов: что хост делает с Canvas.
- Конфигурация: построитель, который заполняет точка входа.