Реактивное программирование игр

Вступление

Combine - это версия функционального реактивного программирования Apple. Согласно Apple, это декларативный API Swift для обработки значений с течением времени. Функциональная реактивная парадигма представляет собой комбинацию функционального программирования с реактивным программированием.

В функциональном программировании все моделируется как функция. При использовании парадигмы программа избегает изменения состояния программы или мутации данных. Эта парадигма сочетается с реактивной парадигмой. Реактивное программирование позволяет обрабатывать асинхронные потоки данных и потоки событий. По этой причине он очень хорошо работает при работе с пользовательским интерфейсом, например при вводе с клавиатуры или нажатии кнопок.

Их сочетание создает парадигму программирования, в которой события или асинхронные вызовы обрабатываются с помощью функций. Это моя первая попытка использовать Combine или любое другое функциональное реактивное программирование. Я сделаю базовую игру Рошамбо (Камень, ножницы, бумага).

Издатель

Издатель - поставщик данных. В этом мини-приложении первый издатель будет вводить выбор пользователя. Я собираюсь публиковать события по мере их поступления от пользователя. Когда пользователь откроет приложение, мы предложим ему три варианта: Вода, Огонь и Трава. Огонь бьет траву. Трава бьет воду. Вода бьет Огонь.

Весь код для мини-приложения Рошамбо можно найти в моем репозитории GitHub. У класса ContentView есть EnvironmentObject под названием App, который я использую для управления логикой объединения. Когда я закончу с этим руководством, ContentView будет:

Объект App создается в функции сцены в SceneDelegate и устанавливается как объект среды. При использовании протокола ObservableObject создать издателя не может быть проще. Единственное необходимое дополнение - это простая аннотация @Published. Вот первый издатель:

class App: ObservableObject {
  @Published var user: Player
}

Теперь, когда объект Player, user, изменяется, он будет предоставлять обновленные данные. ChooseElementView содержит кнопки, с помощью которых пользователь может выбрать вариант.

При нажатии одной из кнопок Roshambo обновляется элемент в объекте Player. Это связано с тем, что существует привязка между selectedElement в ChooseElementView, которая создается при создании представления элемента: ChooseElementView(selectedElement: $app.user.element

Объект Player - это еще один простой ObservableObject, имеющий единственную опубликованную переменную.

class Player: ObservableObject {
  @Published var element = Element.none
}

Протокол ObservableObject - действительно мощная концепция SwiftUI. Это упрощает создание издателя. @Published annotation встроен в Combine. Издатель срабатывает каждый раз при изменении свойства. Тип вывода издателя выводится из типа свойства. В этом случае тип издателя - <Element, Never>, что означает, что тип данных - Element и сбоев нет.

Подписчик

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

В моем мини-приложении Roshambo противник CopyCatPlayer действительно раздражает, когда против него играть. Она всегда ждет, чтобы увидеть, что мы делаем, и делает тот же выбор! Так что результат игры всегда равен ничьей.

Это будет реализовано путем CopyCatPlayer подписки на издателя. Когда издатель отправляет новое значение, другой игрок будет слушать, ждать короткий период времени и делать тот же выбор.

CopyCatPlayer будет инициализирован издателем. Это заставит CopyCatPlayer обновить выбранный Element, когда пользователь сделает выбор.

Assign - подписчик. Он использует издателя и устанавливает опубликованное значение для своей собственной переменной элемента, как только оппонент делает выбор. Ссылка сохраняется в cancellableSet, поэтому подписчик будет работать в течение всего срока службы CopyCatPlayer.

Затем противник добавляется в класс App.swift. Оппонент также публикуется, поэтому ContentView может обновиться.

Когда создается экземпляр CopyCatPlayer, издатель использует функцию eraseToAnyPublisher, чтобы предоставить более чистый тип. Это можно сделать на любом издателе. Это позволяет подписчику поддерживать абстракцию от базовой реализации.

Теперь есть CopyCatPlayer, который будет имитировать выбор пользователя, который выполняется с помощью ubscriber assign . Чтобы у оппонента была задержка перед копированием, мне нужно будет ввести новую концепцию: операторы.

Оператор

Операторы, возможно, являются одним из самых важных элементов Combine. Они посредники между издателями и подписчиками. Операторы могут служить многим целям, включая, помимо прочего, изменение времени событий, преобразование данных, фильтрацию или даже объединение нескольких издателей в один источник данных.

Первый оператор, на который я собираюсь взглянуть, изменит синхронизацию CopyCatPlayer. Это сделает выбор CopyCatPlayer задержки на 200 миллисекунд. Для этого необходимы два оператора: debounce и receive.

Оператор debounce используется для задержки события. Требуется два параметра: время ожидания и планировщик, по которому оператор будет доставлять события. Оператор receive используется для получения события в указанном планировщике. Легко представить, как эти два оператора часто используются вместе.

Чтобы показать еще более эффективный способ использования операторов, мы собираемся создать новый объект Game, чтобы подписаться на элементы нашего игрока, чтобы вычислить результат игры Рошамбо.

Перечисление Winner имеет три варианта. Победителем может быть первый игрок (наш пользователь), второй игрок (противник) или ни один из них (ничья).

enum Winner {
  case one, two, neither
}

Объект Game опубликует Winner, чтобы наш ContentView мог сказать пользователю, кто выиграл игру. Чтобы вычислить Winner, он будет инициализирован двумя издателями, по одному для каждого игрока.

Два издателя объединяются с помощью оператора CombineLatest, который берет последние два элемента от каждого издателя. Таким образом, на входе два объекта Element, но оператор преобразует тип данных в кортеж элементов - (Element, Element). Затем используется новый, знакомый большинству людей оператор map. Оператор map использует замыкание для преобразования типа данных с (Element, Element) на Winner?. Это функция, используемая в закрытии:

Тип возврата от оператора map присваивается переменной-победителю. Затем публикуется переменная-победитель, поэтому ContentView может обновляться в зависимости от победителя. В объект App добавлен класс Game:

Мне нужно добавить еще одну часть нашей игры. Не будет весело, если пользователь сможет сыграть только один раз. Мне нужно снова начать играть. Для этого мне нужно преподавать предметы, новую тему «Объединить».

Тема

У комбината есть издатель особого типа Subject. Это протокол, которого может придерживаться издатель, который добавит метод: send(). Этот издатель используется для того, чтобы элементы можно было вводить в поток и транслировать значения подписчикам.

В Combine встроены два типа Subject: PassthroughSubject и CurrentValueSubject. Единственная разница между ними в том, что CurrentValueSubject требует начального значения. Оба отправят обновленные значения.

Я собираюсь добавить Subject для сброса. Когда будет нажата кнопка сброса, я пришлю Bool. Создать PassthroughSubject почти так же просто, как и с любым другим издателем. Итак, конечный результат объекта App:

Новый издатель будет добавлен в класс Player, чтобы и пользователь, и оппонент могли выполнить сброс. Это приведет к появлению одного нового подписчика sink. Он просто получит значение и выполнит закрытие. В этом случае установка элемента для Player обратно на none.

Это делает сброс настроек чрезвычайно расширяемым. Если добавлен новый элемент пользовательского интерфейса, ему просто нужно подписаться на тему сброса. Без издателя функция сброса должна была бы содержать всю логику, для которой обновлять объекты или пользовательский интерфейс. Тогда функция будет действительно хрупкой. Теперь логика сброса содержится в элементе или классе, который необходимо обновить.

Заключение

Комбайн очень мощный и может использоваться в Swift. Это центральная функция SwiftUI, но она также может использоваться для многих других случаев использования, включая сетевые запросы, обработку ошибок и асинхронные операции. Это руководство лишь поверхностно знакомит с базовой терминологией и концепциями. Я продолжу исследовать, что еще я могу делать с Combine и как начать думать в парадигме функционального реактивного программирования.

Если эта тема вас интересует, есть отличная бесплатная информация, включая шаблоны и рецепты, написанные Джозефом Хеком и документацией Apple.