Доводы против классов текстовых приключенческих игр

Изучите или обучите основам программирования на любом языке, создав однофайловое текстовое приключение (также известное как интерактивная фантастика, также известное как сюжетная игра), используя простую связанную подпрограмму (также известную как функция) для каждой части. Одна часть вызывает другую - классы не нужны. Храните все об игре, которую стоит сохранить, в глобальном словаре (он же карта, он же объект, он же ассоциативный массив). Сохраните игру, сохранив словарь в формате JSON. Начинающие студенты изящно вырвутся из линейного мышления и позже будут лучше подготовлены к концепциям программирования, основанного на событиях.

Вы когда-нибудь сохраняли игру? Конечно, есть. Когда вы это сделали, вы сохранили состояние игры, не ее функциональность, которая скрыта в коде.

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

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

Совершенно не зная кода, вы уже знаете, что вам нужен какой-то формат для сохранения игровых данных на диск или в облако или для их отправки куда-нибудь. Интуитивно вы понимаете, что для этого нужно приложить минимум усилий. Какие у вас варианты?

Класс для каждой комнаты?

Нет нет нет. Я читал несколько рекомендаций, рекомендующих это начинающим программистам, но даже самый продвинутый программист никогда бы не стал. Классы определяют тип и структуру данных для каждого объекта, созданного из класса, например из чертежа или шаблона. Вы знаете, что делаете это неправильно, если создаете классы с именами MailBox и HouseByTheField. Они вызывают классы, которые должны иметь только один объект singleletons, но это наименее практичный подход. Люди были настолько приучены внедрять классы во все, что часто делают это и развивают себя в угол, а не из одного, что было первоначальным намерением классов и основной причиной, по которой традиционное объектно-ориентированное программирование с наследованием превратилось в плохую практику. большую часть времени.

Класс детали

Таким образом, вместо HouseByTheField у вас будет класс Area, Part или Chapter, а houseByTheField будет всего лишь одним из объектов этого класса. Но мы напрягаемся.

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

Есть способы внедрить код в объект, созданный из класса (если использовать традиционный язык объектно-ориентированного программирования), с помощью функций «первого класса», которые могут использоваться, как и любые другие переменные, до тех пор, пока они не будут вызваны. Проблема с этим подходом в том, что - даже если вам посчастливилось иметь первоклассные функции на вашем языке - у вас есть код для данной части, который живет повсюду:

  • Блок определения класса Part
  • Определение функции для обработки детали
  • Объект, созданный из класса Part
  • Инициализация объекта детали, передаваемого в функцию-обработчик
  • Код, вызывающий обработчик из объекта детали

Вы можете комбинировать некоторые из них, но когда вы это делаете, вы в значительной степени отказываетесь от строгой типизации (чего вы не хотите делать). Строгий набор текста всегда ваш друг, даже если он сразу же выплюнет ваши ошибки вам в лицо, а не молча убьет ими игроков позже.

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

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

К счастью, есть более простой способ.

Одна подпрограмма для каждой части

Лучшее решение - создать подпрограмму для каждой детали. Термин подпрограмма устарел. Оператор gosub дополнял инструкцию goto в ранних языках программирования. Это буквально переместило программу к номеру строки, в которой находилась подпрограмма. До этого goto буквально переходил к точному номеру строки в коде (что, очевидно, было огромной проблемой).

В наши дни подпрограммы чаще называются функциями . Некоторое время такие языки, как Паскаль, различали процедуры и функции в основном для того, чтобы показать, что функции имеют возвращаемое значение, а процедуры - нет. Термин методы также часто используется здесь, хотя и неправильно. Метод должен быть связан с конкретным объектом и почти всегда привязан к этому объекту точкой.

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

Внутри подпрограммы операторы if или завершение задачи могут напрямую вызывать подпрограмму следующей части.

Каждая часть связана с одной или несколькими другими частями через прямые вызовы подпрограммы для следующих частей.

Самая сильная сторона этого подхода - то, что вы можете включить всю мини-игру в подпрограмму части, если хотите. «Вы замечаете, что фигура на шахматной доске перед вами случайным образом движется, призывая вас к ответу», после чего вы перемещаете свою фигуру, и шахматный ИИ реагирует на все внутри части, содержащей шахматную доску.

Один интересный побочный эффект использования связанных подпрограмм состоит в том, что стек вызовов (который является способом языка отслеживать порядок вызовов подпрограмм) фактически представляет собой путь через само сюжетное приключение. Если у вас есть ошибка, из-за которой ваша игра вылетает, трассировка стека буквально покажет точные части, которые игрок сделал по порядку. Поговорим о отличной отладке!

Подпрограммы с заглавными буквами

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

Остерегайтесь ограничений рекурсии

В большинстве языков есть ограничения на количество вызовов самой подпрограммы, что называется рекурсией. У вас может возникнуть соблазн просто вызвать ту же подпрограмму еще раз, если игрок не подает правильную команду. Это рискует в конечном итоге преодолеть этот рекурсивный барьер (даже если он обычно исчисляется сотнями или тысячами). Безопасный способ избежать этого - поместить внутри подпрограммы части бесконечный цикл вокруг приглашения к действию игрока, выйти из которого можно только с помощью правильного ответа.

Рекомендации по навигации

Не делайте сетку. Тебе это не нужно. Вы не создаете здесь многопользовательское подземелье (MUD). Фактически, я настоятельно рекомендую вам использовать нестандартные команды направления и триггеры выбора. Есть больше вариантов, чем просто север, юг, восток, запад, левый и правый. Вы создаете интерактивную историю, текстовое приключение, и чем больше возможностей, тем веселее, даже если отследить свои шаги сложнее. Это заставляет игрока присутствовать и пребывать в воображении, а не мчаться по вещам.

Обычно программисты увлекаются добавлением сетки, когда они хотят добавить блуждающих монстров или другого ИИ, который обычно знает обо всем мире, созданном нашим интерактивным текстовым приключением. Вы знаете, что достигли этого, когда начинаете кодировать стандартные параметры навигации в своей игре и оправдываете это тем, что хотите иметь последовательность. Последовательность делает текстовые приключения скучными - действительно скучными. Кроме того, интерактивная фантастика не обязательно должна иметь часть, имеющую определенное направление и местоположение. Все может происходить из одного места. Текстовые приключения - это история, а не только навигационная локация. [Между прочим, двигатели MUD действительно разрушили это.]

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

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

Глобальное состояние игры

Начиная с части «Добро пожаловать» (), вы, вероятно, уже хотите сохранить что-то об игре, которое могут видеть другие части, например, произнесите имя, которое вы предложили игроку. Самый простой способ отслеживать это - создать словарь игры (здесь мне нравится слово Python «словарь» вместо «карта») и добавить к нему имя. Теперь у вас есть единственная переменная, содержащая все данные о вашей игре, которые вы можете легко сохранить или отправить.

Представьте себе, что еще можно сэкономить:

  • Текущее местоположение игрока
  • Посещенные комнаты и даже в каком порядке с своего рода следом хлебных крошек
  • Предметы, доступные в разных комнатах, которые можно забрать
  • Статистика игроков

Действительно, есть много всего.

Но глобальность - это плохо, правда?

Вы, вероятно, были забиты до смерти в какой-то момент своей жизни за то, что когда-либо использовали «глобальное» что-либо в программировании, но фактически это именно то, что делает большинство игр, особенно высокопроизводительные. В многопользовательской игре каждая деталь игрового состояния - вплоть до положения этой ракеты или эффекта AOE - сообщается каждому, кто играет в эту игру, сотни или тысячи раз в секунду.

Фактически, эти протоколы для передачи этих данных состояния являются ядром, а все остальное несколько второстепенным, потому что скорость игры полностью зависит от ее эффективности, чтобы избежать ужасных задержек. Конечно, где-то в этом коде C ++ может быть класс ракеты, но что это такое и где единственные биты данных, которые необходимо передать. Нет времени на маршалинг.

Создайте вспомогательную функцию Tell (вместо Print)

Создание простой функции tell () (которая сначала просто выводит свой аргумент на экран) позволит вам настроить отображение текста истории. Возможно, позже вы захотите добавить цвет или обернуть его для другой ширины экрана. Использование tell () с самого начала вместо встроенной функции print () сэкономит много времени на переписывание кода.

Создание вспомогательной функции подсказки

Если вам повезло, в вашем языке есть способ запрашивать у игрока ввод и возвращать его, чтобы вы могли поместить его в переменную и действовать в соответствии с ней. Но даже в этом случае лучше всего создать вспомогательную функцию prompt (), чтобы вы могли настроить, как это делается. В конце концов, вы добавите функциональность диспетчера игр к функции prompt (), так как игрового цикла событий нет. Встроенные действия, такие как «inventory» или «tell», могут быть добавлены к подсказке, так что каждая подпрограмма части не обязана делать это.

Функция prompt (), вероятно, должна принимать аргумент, чтобы задать конкретный вопрос, но просто отображать базовую подсказку по умолчанию, когда ей не переданы аргументы.

Личный треп против использования классов по умолчанию

Примерно через два года программирования на Go и несколько лет на JavaScript, C, Ruby и Perl до этого я начал ненавидеть классы (хотя я создал прагму классов Perl, которую не обновлял годами). Обратите внимание, я сказал классы, а не объекты.

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

Одна из лучших особенностей го - отсутствие классов. Есть структуры, состояния вещей, объекты, если вы настаиваете, к которым можно прикрепить методы. Это великолепно.

Что выходит за рамки блестящего, так это то, что структуру данных, представляющую объект, можно легко преобразовать (так называемый маршалинг) в любой формат, JSON, текстовый, двоичный и сохранить или отправить куда-нибудь. Когда вы открываете эти данные или получаете их, вам просто нужно сказать Go: «Эй, это что-то типа foo» (это называется демаршалингом и / или приведением), и он просто делает это. Go явно создан для мира, ориентированного на данные, в котором мы находимся сегодня.