reckfellENGINE

Интерфейс: первое меню и структура UI

Полное адаптивное меню C#: иерархия Control, общий стиль, кнопки и проверка размеров.

На этой странице

Из чего собрать интерфейс

Control — сущность интерфейса с прямоугольником, размерами, привязками и фокусом. Label показывает текст, Button выполняет действие, LineEdit принимает строку. Контейнеры распределяют место между дочерними Control. Они входят в обычную иерархию Entity; отдельной ECS-модели для UI нет.

ЗадачаМатериал
Первый экран менюГотовый пример ниже
HUD у края и изменение окнаРазмеры и привязки
Списки, свободное место и прокруткаКонтейнеры
Tab, стрелки, подтверждение и возвратФокус и навигация
Выбрать поле, список или переключательЭлементы интерфейса
Собственный рисунок и поведение вводаСвой Control

Запустить меню

В C#-проекте создайте уровень с корнем Control, сохраните UiSampleStyle.cs и ResponsiveMenu.cs, прикрепите ResponsiveMenu к корню и соберите. Запустите F6. При необходимости подготовки C# начните с первого проекта.

Начать увеличивает счётчик, Enter активирует выбранную кнопку, Tab переключает фокус. Кнопка настроек показывает место подключения вашего экрана; она не создаёт настройки игры. При изменении окна карточка остаётся по центру и занимает не более 520 логических пикселей, с внешними отступами 24. Пример рассчитан на область не меньше 390 × 480; произвольно маленькое окно не может вместить любой текст.

Меню примера при размере области 960 на 540

UiSampleStyle нужен также следующим примерам раздела: он задаёт общие цвета, размер текста и отступы. В игре общую тему удобно хранить ресурсом и назначать корню UI. Наследование темы и настройка её типов разобраны в руководстве по темам.

UiSampleStyle.cs
C#
using System;
using Reckfell;

public static class UiSampleStyle
{
    public static string Text(string ru, string en) =>
        TranslationServer.GetLocale().StartsWith("ru", StringComparison.OrdinalIgnoreCase) ? ru : en;

    public static Theme Create()
    {
        var theme = new Theme { DefaultFontSize = 16 };
        theme.SetColor("font_color", "Label", Colors.White);
        theme.SetConstant("separation", "VBoxContainer", 12);
        theme.SetConstant("separation", "HBoxContainer", 12);
        foreach (string state in new[] { "normal", "hover", "pressed", "disabled" })
        {
            var style = Box(state == "pressed" ? new Color(.12f, .27f, .75f) : new Color(.14f, .14f, .16f), 12);
            theme.SetStylebox(state, "Button", style);
        }
        var focus = Box(Colors.Transparent, 0);
        focus.DrawCenter = false; focus.BorderColor = new Color(.25f, .45f, 1);
        focus.SetBorderWidthAll(2); theme.SetStylebox("focus", "Button", focus);
        theme.SetStylebox("panel", "PanelContainer", Box(new Color(.065f, .065f, .075f), 20));
        return theme;
    }

    private static StyleBoxFlat Box(Color color, int padding)
    {
        var style = new StyleBoxFlat { BgColor = color };
        style.SetCornerRadiusAll(6);
        foreach (Side side in Enum.GetValues<Side>()) style.SetContentMargin(side, padding);
        return style;
    }

    public static void Prepare(Control root)
    {
        root.SetAnchorsAndOffsetsPreset(Control.LayoutPreset.FullRect);
        root.Theme = Create();
        var background = new ColorRect { Color = new Color(.02f, .02f, .025f), MouseFilter = Control.MouseFilterEnum.Ignore };
        root.AddChild(background); background.SetAnchorsAndOffsetsPreset(Control.LayoutPreset.FullRect);
    }
}
ResponsiveMenu.cs
C#
using Reckfell;

public partial class ResponsiveMenu : Control
{
    public PanelContainer Card { get; private set; } = null!;
    public Button Start { get; private set; } = null!;
    public Label Status { get; private set; } = null!;
    public int StartCount { get; private set; }

    public override void _Ready()
    {
        UiSampleStyle.Prepare(this);
        Card = new PanelContainer { Name = "Card" }; AddChild(Card);
        var column = new VBoxContainer(); Card.AddChild(column);
        column.AddChild(new Label { Text = "Reckfell" });
        column.AddChild(new Label
        {
            Text = UiSampleStyle.Text("Начните игру или настройте управление.", "Start a game or adjust the controls."),
            AutowrapMode = TextServer.AutowrapMode.WordSmart
        });
        Start = new Button { Name = "Start", Text = UiSampleStyle.Text("Начать", "Start"), FocusMode = FocusModeEnum.All };
        column.AddChild(Start);
        var settings = new Button { Text = UiSampleStyle.Text("Настройки управления", "Control settings") }; column.AddChild(settings);
        Status = new Label { Text = UiSampleStyle.Text("Готово", "Ready") }; column.AddChild(Status);
        Start.Pressed += () => { StartCount++; Status.Text = UiSampleStyle.Text("Нажатий: ", "Presses: ") + StartCount; };
        settings.Pressed += () => Status.Text = UiSampleStyle.Text("Здесь откройте свои настройки.", "Open your settings here.");
        Resized += LayoutCard;
        Card.MinimumSizeChanged += () => Callable.From(LayoutCard).CallDeferred();
        LayoutCard(); Callable.From(() => Start.GrabFocus()).CallDeferred();
    }

    private void LayoutCard()
    {
        if (Card is null || !IsInsideTree()) return;
        float width = Mathf.Min(520, Mathf.Max(0, Size.X - 48));
        Card.Size = new Vector2(width, Card.GetCombinedMinimumSize().Y);
        Card.Position = (Size - Card.Size) / 2;
    }

    public override void _UnhandledInput(InputEvent @event)
    {
        if (IsVisibleInTree() && @event.IsAction("ui_accept")) GetViewport().SetInputAsHandled();
    }
}

Кто управляет размерами

В примере родитель Card — обычный Control, поэтому LayoutCard задаёт положение и размер карточки. Внутри неё PanelContainer и VBoxContainer раскладывают содержимое. Не выставляйте вручную Position кнопок этой колонки: следующий проход контейнера перезапишет её.

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

Для HUD поверх движущегося мира обычно поместите корень Control под CanvasLayer, чтобы камера мира не двигала экранный интерфейс. Размер Control определяется логической областью родителя/viewport, а не обязательно физическими пикселями монитора. Сначала добейтесь корректной раскладки, затем отдельно настраивайте масштабирование проекта под разрешения и DPI.

Если экран пуст или карточка обрезана, сначала проверьте Size корневого Control, GetParentAreaSize и GetCombinedMinimumSize карточки. Сравнивайте размеры в одной логической системе координат. Большой минимум длинной надписи не помещается автоматически только потому, что у родителя FullRect.

Что проверить перед подключением к игре

Измените окно между широким и узким форматом, увеличьте длину надписей, пройдите меню без мыши и вернитесь в него после скрытия. Задайте действия игры отдельно от ui_*. _Input() вызывается раньше GUI; опрос Input.IsActionJustPressed() тоже не отменяется AcceptEvent. Игровую реакцию нужно отключать состоянием игры/меню, а необработанные события получать в _UnhandledInput().

Документация Reckfell EngineНаверх ↑
БЫСТРЫЙ ПЕРЕХОДНачало работыEntity и иерархияC# & .NET 10Сцены и префабы
Руководства и полный API Reckfell K
reckfellENGINE
RECKFELL ACCOUNT

Вход в аккаунт

Войдите в аккаунт Reckfell.

Один аккаунт для всей платформы Reckfell.