Обновление холста .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 есть хороший способ реализовать шаблон публикации/подписки в ваших приложениях или играх.
Я обязательно буду помнить об этом при разработке решений, как в профессиональных, так и в моих хобби-проектах.
Это не панацея, нужно помнить, что существуют и другие способы передачи данных между компонентами.
Поиск наилучшего варианта для каждого сценария — вот что делает архитектуру программного обеспечения такой увлекательной!
Оригинал