Обновление холста .NET MAUI с помощью событий обмена сообщениями

Обновление холста .NET MAUI с помощью событий обмена сообщениями

28 ноября 2022 г.

С 10 лет я пристрастился к программированию. Я всегда хочу попробовать что-то новое. Профессионально я работаю над приложениями MAUI в качестве архитектора программного обеспечения. Тип архитектора, который не может сопротивляться коду. Так я начал создавать эмуляторы.

Введение

Недавно я узнал о существовании платформы под названием «ЧИП-8». Это не настоящее устройство, как, например, Gameboy. В 1970-х и 1980-х годах CHIP-8 был создан как «виртуальная платформа» для запуска игр на различных комплектных компьютерах. После этого эмуляторы ЧИП-8 стали доступны для графических калькуляторов, для игры в тетрис на уроках математики. CHIP-8 считается стартовым лекарством для разработчиков, стремящихся создать эмулятор.

Основные компоненты моего эмулятора:

  • .NET MAUI, он работает на устройствах Mac, Windows, iOS, Android и Tizen, таких как холодильники
  • Главная страница содержит холст для отображения.
  • Главная страница содержит шестнадцатеричную клавиатуру для ввода данных пользователем.
  • Класс под названием «Chip8.cs» содержит настоящий «компьютер». Он имеет такие компоненты, как память, регистры процессора, стек. Он также имеет «игровой цикл», в котором он может выполнять байт-код с заданной скоростью, выраженной в инструкциях в секунду.

Проблема

Мое отображение представляет собой элемент Canvas на странице MainPage. «Игровой цикл» относится к классу Chip8. Как я могу сообщить главной странице, что холст должен быть перерисован?

Во-первых, Canvas, определенный в Mainpage.xaml

<GraphicsView
    x:Name="gView"
    WidthRequest="640"
    HeightRequest="320"                
    Drawable="{StaticResource GraphicsDrawable}">
</GraphicsView>

Затем код для рисования. Он рисует пиксели одним цветом. Каждый пиксель может быть включен или выключен, разрешение 64x32. Я использую PixelSize для масштабирования пикселей до видимого размера на современном дисплее.

public class GraphicsDrawable : IDrawable
{
    public void Draw(ICanvas canvas, RectF dirtyRect)
    {
        for (int y = 0; y < Display.Pixels.GetLength(1); y++)
        {
            for (int x = 0; x < Display.Pixels.GetLength(0); x++)
            {
                var px = Display.Pixels[x, y];
                if (px == true)
                {
                    canvas.FillColor = new Color(2, 91, 24);
                }
                else
                {
                    canvas.FillColor = new Color(0, 0, 0);
                }
                canvas.FillRectangle(x * Display.PixelSize, y * Display.PixelSize, Display.PixelSize, Display.PixelSize);
            }
        }       
    }
}

public static class Display
{
    public static int PixelSize = 10; // X times 64 * 32 resolution
    public static bool[,] Pixels { get; set; } = new bool[64, 32];

    internal static void SetPixel(int x, int y, bool v)
    {
        Pixels[x, y] = v;
    }
}

Здесь вы можете увидеть код Gameloop. Со скоростью 60 Гц в секунду я читаю ввод, выполняю инструкции байт-кода, а затем перерисовываю экран. Соответствующая часть этой статьи — перерисовка экрана.

while (true)
{
    Stopwatch t = new Stopwatch();
    t.Reset();
    t.Start();

    // Get input
    _keyPressed = CurrentKeyPressed;

    // Decrease timers at 60hz
    _regDelayTimer = (byte)(_regDelayTimer > 0x0 ? _regDelayTimer - 1 : 0x0);
    _regSoundTimer = (byte)(_regSoundTimer > 0x0 ? _regSoundTimer - 1 : 0x0);

    // Batch Execute
    await ExecuteInstructionBatch(batchSizePerHz);

    // Draw
    // How to fix this? Let MainPage know to redraw here

    t.Stop();
    await Task.Delay(Math.Max(0, 1000 / 60 - (int)t.ElapsedMilliseconds));
}

Решение

Я пытался передать холст в качестве параметра классу Chip8, но это пока не помогло. Мне удалось заставить его работать на Windows, но как только я попробовал другую платформу, у меня возникли странные проблемы с рендерингом. Например, Canvas обновляется только для нескольких кадров.

Перемещение всего кода Chip8 на главную страницу было рабочим решением, но это некрасиво. Нет разделения интересов. И как мне поддерживать этого монстра, когда я начну внедрять другие эмуляторы?

Не прикасаясь к проекту какое-то время, я вспомнил, что читал о механизме публикации/подписки в электронной книге Шаблоны корпоративных приложений с использованием .NET MAUI.

Это было опубликовано Microsoft, и я прочитал его, чтобы освоиться с новым способом ведения дел, я пришел из Xamarin Forms. Поэтому я снова открыл эту электронную книгу и, конечно же, MessagingCenter сможет решить мою проблему!

В игровом цикле я сделал:

// Draw
MessagingCenter.Send(this, "draw");

Конструктор MainPage прослушивает это сообщение, а затем дает указание GraphicsView Canvas (gView) перерисовать себя.

MessagingCenter.Subscribe<Chip8>(this, "draw", (sender) =>
{
   MainThread.InvokeOnMainThreadAsync(() => gView.Invalidate());           
});

Это сработало как шарм!

Устарело в .NET 7, перейдите на CommunityToolkit.MVVM

Поскольку я мало занимался этим проектом в течение нескольких недель, я еще не перенес его с .NET 6 на .NET 7. Поэтому я пошел и изменил целевые фреймворки в файлах проекта. К моему удивлению, Intellisense намекнула мне, что MessagingCenter помечен как устаревший. Я нашел этот запрос на вытягивание от великого Джеральда Верслуиса, который добавил этот атрибут в MessagingCenter. Джеральд Верслуис упоминает в своем запросе на включение, что MessagingCenter не должен быть в структуре пользовательского интерфейса. Дело принято, так что давайте посмотрим на альтернативу!

IntelliSense говорит, что я должен изучить пакет CommunityToolkit.MVVM, он указывает на WeakReferenceMessenger. В запросе на вытягивание есть ссылка на обсуждение использования этого вместо MessagingCenter. Там упоминается, что этот мессенджер работает в 100 раз быстрее, и это подтверждается бенчмарком.

MVVMToolkit, безусловно, самая быстрая программа в мире. Вы можете выбрать WeakReferenceMessenger или StrongReferenceMessenger. Сильный быстрее, но имеет слабые ссылки, а это означает, что важно меньше собирать мусор и отписываться вручную.

Чтобы реализовать это, мне пришлось добавить в свой проект пакет CommunityToolkit.MVVM.

Затем мне пришлось определить класс сообщений, наследующий от базового класса сообщений. Я просто инкапсулирую строку. Но я могу себе представить, что могу отправить состояние дисплея, если захочу в дальнейшем переместить компоненты в класс Chip8. В настоящее время состояние отображения находится в статическом классе, к которому можно получить доступ из любого места.

using CommunityToolkit.Mvvm.Messaging.Messages;

namespace MauiEmu;

public class DrawMessage : ValueChangedMessage<string>
{
    public DrawMessage(string value) : base(value)
    {
    }
}

Затем в игровом цикле я обновил оператор отрисовки.

// Draw
StrongReferenceMessenger.Default.Send(new DrawMessage("draw"));

WeakReferenceMessenger также работает. Но я прикинул, что раз у меня только одна подписка, я не беспокоюсь о том, чтобы не собирать мусор. Это принципиальный вопрос, так быстрее.

На главной странице я обновляю подписку, чтобы она стала StrongReferenceMessenger.

StrongReferenceMessenger.Default.Register<DrawMessage>(this, (sender, args) =>
{
    MainThread.InvokeOnMainThreadAsync(() => gView.Invalidate());
});   

После запуска обновление действительно казалось более частым. Это было заметно, особенно в ROMS, где экран медленно строится, загружая более 60 кадров.

Заключение

В .NET MAUI есть хороший способ реализовать шаблон публикации/подписки в ваших приложениях или играх.

Я обязательно буду помнить об этом при разработке решений, как в профессиональных, так и в моих хобби-проектах.

Это не панацея, нужно помнить, что существуют и другие способы передачи данных между компонентами.

Поиск наилучшего варианта для каждого сценария — вот что делает архитектуру программного обеспечения такой увлекательной!


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE