Отказ от ответственности: не читайте это руководство как руководство по проектированию ООП, а скорее как способ попрактиковаться в написании кода ООП и рассмотрении концепций ООП в контексте.

Шаг 1. Напишите описание проблемы

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

  • Игрок — Лягушка
  • Препятствия
  • Дорога/Река — Переулки
  • Пейзаж
  • Переехать
  • Получить удар

Шаг 2: Код!

Код будет представлен в 7 «уроках», и вы увидите примечания к коду по мере его продвижения, а также множество рефакторингов, чтобы стать более «ООП».

Урок 1

В этом уроке мы начнем с малого. Вы можете увидеть полный код внизу урока. В коде используется псевдоклассический шаблон ООП, поэтому у нас есть функции-конструкторы, которые могут создавать player, lane, экземпляры landscape и game, а также методы, добавленные к свойствам prototype этих функций.

  1. Добавьте функцию для создания объектов игрока. Объект должен иметь один параметр, icon, со значением по умолчанию для значка игрока.

Решение: мы знаем, что игроку понадобится значок. Поскольку игра называется Frogger, функция конструктора игрока — Frog. Чтобы сделать выбор значка необязательным, для параметра установлено значение по умолчанию.

2. Добавьте функцию-конструктор, которая может создавать «дорожки», по которым Frogger будет перемещаться вверх и вниз. Lane должен принимать параметр общей ширины доски и должен иметь метод для displayобработки дорожки. В этом наборе упражнений мы собираемся представить наше местоположение с помощью структуры вложенного массива, но вы можете изменить ее для своего решения. Также нет необходимости создавать экземпляр кода дорожки в методе makeLane — вы можете сделать это непосредственно в свойстве this.lane в конструкторе Lane.

Давайте поближе посмотрим, что происходит с Lane. В строке 5 выше в методе makeLane мы используем оператор new для создания объекта Array. Это показывает, что встроенные объекты также имеют функции конструктора, такие же, как те, которые мы делаем сегодня. За кулисами создается массив длиной max. Это выглядит примерно так:

> new Array(20)
[ <20 empty items> ]

Затем мы используем метод массива fill, чтобы заменить пустые элементы строкой “_” для представления дороги/полосы.

В строке 8 мы получаем доступ к свойству prototype функции Lane и добавляем значение функции для свойств makeLane и display. Вот как это выглядит. Розовое поле — это функция конструктора дорожки, а Lane.prototype — свойство Lane. play — это свойство объекта Lane.prototype со значением функции.

Вы также можете открыть DevTools и посмотреть свойства с помощью консоли:

> function Lane() {}
undefined
> Lane.prototype.makeLane = function() {
    console.log('example');
}
ƒ () {
    console.log('example');
}
> Lane.prototype
{makeLane: ƒ, constructor: ƒ}

3. Спойлер! Мы удалим это позже, так что пока просто добавьте эту функцию StartLane и не сомневайтесь. ;)

Создайте функцию-конструктор для стартовой дорожки. Эта функция должна наследовать от Lane и передавать аргумент для max в Lane. После этого свойство lane следует изменить так, чтобы игрок icon находился в середине дорожки. Мы еще не заставляли эти объекты сотрудничать, поэтому просто введите символ значка в качестве строки для значения.

Из решения я хочу выделить использование метода функции call в строке 2 — Lane.call(this, max) в функции StartLane, а затем StartLane.prototype = Object.create(Lane.prototype) и StartLane.prototype.constructor = StartLane, которые в целом настраивают наследование. Теперь StartLane может использовать функцию makeLane из объекта prototype Lane.

4. Создайте функцию-конструктор для ландшафта. Как мы упоминали ранее, мы будем использовать структуру вложенного массива для дорожек и движения игроков, поэтому, если вы следуете этой модели, добавьте свойство lanes для представления дорожек в виде текущих пустой массив. После этого создайте экземпляр объекта startLane и передайте значение max, которое на 1 меньше вашего значения max по умолчанию. (т. е. если вы соответствуете нашей длине по умолчанию, равной 20, передайте 19 в качестве значения). Затем в этой функции создайте около 5 новых обычных дорожек, чтобы составить остальную часть доски, и поместите их в массив дорожек. Нам особенно нужно передать свойство lane из новых объектов Lane.

Вы можете увидеть проблему уже с приведенным ниже кодом, почему в массиве нет startlane? Мы исправим это позже.

Создайте метод display, отображающий каждую полосу в свойстве lanes объекта Landscape. Метод должен объединять элементы каждого массива и выводить дорожки на консоль. После того, как вы зарегистрируете каждую дорожку в массиве, зарегистрируйте startlane таким же образом, если вы не добавили его в свой массив.

5. Добавьте функцию-конструктор для игры с именем FroggerGame. Функция должна иметь свойство landscape, которое является новым объектом Landscape. Добавьте метод play, который пока просто вызывает метод display из Landscape. Создайте экземпляр объекта FroggerGame и назначьте его переменной game, затем вызовите метод play.

Вот полный код из этого урока:

Урок 2

В этом уроке мы добавим свойства к Frog, добавим player к FroggerGame и добавим некоторые функции к move игроку вокруг «доски»/пейзажа.

  1. Добавьте некоторые свойства к объекту Frog.
  • position со значением null
  • lives со значением 3
  • row со значением 6
  • column со значением 9

2. Исправить структуру нашего массива и добавить возможность «перемещать» игрока по вложенным массивам и, следовательно, по доске.

Давайте сначала исправим проблему startLane. Вернитесь к Landscape и измените его так, чтобы мы могли добавить свойство startLane в качестве окончательного массива во вложенных массивах и удалить явное console.log из startLane в методе display.

Теперь давайте рассуждать о том, как передвигаться по нашему ландшафту. Прямо сейчас все наши дорожки выглядят так:

В форме массива это примерно так с точки зрения индексов.

let array = [
0: [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
1: [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
2: [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
3: [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
4: [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
5: [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
6: [0,1,2,3,4,5,6,7,8,✅,10,11,12,13,14,15,16,17,18,19,20],
]

Итак, чтобы двигаться влево, нам нужно заменить array[currentRow][currentColumn ] позиции игрока icon маркером полосы, уменьшить свойство column значка игрока, а затем перерисовать значок игрока в множество. Другими словами:

  • array[6][9] = “-”
  • array[6][9–1] = “✅”
  • Добавьте где-нибудь player.column -= 1

Итак, давайте создадим несколько методов перемещения, которые будут делать именно это! Этот пример начнется с 4 методов для каждого направления, но позже мы DRY закончим наш код. (DRY означает «Не повторяйся».)

Я покажу вам, как создать метод moveLeft, и вы сможете экстраполировать его, как настроить остальные направления.

Как мы упоминали выше, этот метод просматривает массив массивов и просто изменяет некоторые строковые значения, а затем обновляет свойство объекта Frog player.

Чтобы написать метод moveLeft для прототипа FroggerGame, напишите метод с одним параметром с именем lanes, массивом массивов, представляющих дорожки. В теле кода сначала переназначьте текущую позицию игрока, чтобы она соответствовала остальной части ландшафта, изменив значение lanes[currentRow][currentColumn ], используя доступ к свойству объекта player. Поскольку мы движемся влево, свойство столбца игрока будет на 1 меньше, поэтому для следующей строки кода переназначьте lanes[currentRow][currentColumn - 1] на значок игрока. Наконец, измените фактическое свойство столбца в проигрывателе, а затем верните новый массив дорожек. Далее продолжайте писать аналогичный метод для остальных трех направлений. О таких вещах, как уход игрока за доску, мы поговорим позже.

3. Вызовите несколько новых методов в методе play для проверки функциональности. Не забудьте показать новую доску после того, как сделаете ход.

Вот полный код из «Урока 2»:

Урок 3

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

  1. Создание препятствий

Что мы знаем о препятствиях в Frogger?

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

С этой информацией мы можем написать нашу функцию-конструктор Obstacle и метод move. Функция должна иметь три параметра: row, column и icon. Метод move должен изменить depth вызывающего объекта на -1.

2. Как мы можем отслеживать множество препятствий?

Нам нужен способ отслеживать, какие препятствия были добавлены на доску. Они имеют такие же свойства столбца и строки, как и объект игрока. Есть много способов настроить это. Для нашего решения добавьте статическое свойство obstacles в функцию Obstacles с пустым значением объекта. Затем добавьте цикл for, который добавит к объекту 5 новых экземпляров Obstacle. Настройте его так, чтобы ключ был строковым значением текущей итерации, а значением было новое препятствие с аргументом столбца и строки. На данный момент все эти значения могут быть одинаковыми; мы будем использовать объект Math, чтобы рандомизировать его позже.

Бонус: обратите внимание, как мы используем String(counter) в строке 4 ниже. Это использует функцию String для создания новой строки, но не с оператором new, поэтому это примитивная строка вместо объекта String.

3. Инициализировать доску с распределенными препятствиями

Теперь нам нужно заполнить доску нашими препятствиями. Для меня это звучит как «инициализация препятствий», что кажется методом, который должен быть в объекте Obstacles.prototype или в объекте FroggerGame.prototype. В этом примере метод будет помещен в объект Obstacles.prototype, но при написании кода вы увидите, почему он может иметь больше смысла в объекте FroggerGame.prototype.

Методу initializeObstacles потребуется доступ к объекту препятствий, массиву полос и значку препятствия. Давайте также планируем добавить этот метод, используя Object.assign() в FroggerGame. Таким образом, при вызове метода мы передадим два аргумента, массив массивов и ссылку на иконку.

Чтобы наш код оставался организованным, объявите локальную переменную и присвойте ей значение this.obstacles.obstacles. Мы будем вызывать функцию из контекста выполнения экземпляра FroggerGame, который будет иметь свойство this.obstacles.

Напишите цикл for/in, перебирающий ключи в объекте obj. Внутри цикла объявите две локальные переменные, одну для свойства столбца и одну для свойства строки. Опять же, это просто для облегчения чтения кода. Наконец, переназначьте значение в массиве[строке][столбце], чтобы оно было icon, которое мы передали функции. После цикла верните обновленный obj.

Не забудьте использовать метод Object.assign(), чтобы добавить метод initializeObstacles к FroggerGame.

Мы будем вызывать метод в методе play в FroggerGame.

4. Необязательно: измените способ передачи значка, чтобы создать новое препятствие.

Так как мы будем создавать препятствия одного и того же типа в зависимости от ландшафта (т.е. река =› бревна и черепахи, дорога =› автомобили и грузовики), я решил сделать Obstacle исключительно размещением сетки, а Obstacles — генерацией и сохраняя тип препятствий, которые у нас есть на доске, и их количество в любой момент времени. Буду рад вашим отзывам об этом дизайнерском решении! Эти изменения довольно просты, поэтому я не буду объяснять дальше того, что прокомментировано в приведенном ниже фрагменте кода.

Это обновленная версия:

5. Наконец – интерактивность. Игра мало что делает… пока!

Нам понадобится какой-то способ, чтобы игрок диктовал, куда должен двигаться лягушонок. Разве не было бы идеально просто использовать стрелки клавиатуры? Для этого мы будем использовать readline-sync для ввода данных игроком, но нам потребуется какой-то способ сообщить игре, что щелчок по стрелке равен ходу.

Используйте readline-sync, чтобы попросить пользователя щелкнуть стрелку, чтобы переместить значок лягушки, и назначить ответ переменной.

Для простоты мы будем использовать оператор switch с соответствующей управляющей последовательностью ANSI, чтобы сопоставить щелчок клавиши со стрелкой с методом перемещения.

  • ‘[A’: up
  • ‘[B’: вниз
  • ‘[C’: правильно
  • ‘[D’: слева

И последнее, давайте добавим метод displayWelcomeMessage к FroggerGame, чтобы мы могли приветствовать пользователя. Запишите какое-нибудь приветствие в теле метода.

Вот полный код урока 3:

Урок 4

Этот урок будет легким и сфокусирован на очистке ранее написанного кода.

  • Удалить startLane
  • Добавить метод для инициализации позиции игрока Frog

Чтобы удалить startLane, нам нужно:

  • Добавьте row и column параметры в функцию Frog
  • Передайте row + column аргументы, когда мы вызываем Frog
  • Удалить функцию startLane
  • Удалить вызов startLane в альбомной ориентации
  • Добавьте метод, который обрабатывает начальную позицию игрока.
  • Вызвать метод, который обрабатывает начальную позицию игрока

Идите вперед и обновите параметры функции Frog. Что касается передачи аргументов, как мы можем заставить это работать для доски любого размера? Мы знаем, что игрок Frogger всегда начинает с последней строки в середине, поэтому мы можем использовать наш массив дорожек и передать длину минус 1 для строки и длину внутреннего массива, деленную на 2, для столбца.

Для handlePlayerStartPosition добавьте его как метод на FroggerGame. Мы можем получить доступ к свойствам row и column из объекта игрока Frog и использовать их для переназначения этой позиции в массиве дорожек значению свойства icon игрока.

Вот как это выглядит:

А вот и код полного урока:

Урок 5

Помните, некоторое время назад я обещал, что мы рандомизируем препятствия? Это время сейчас. Вот что вас ждет на этом уроке:

  • Рандомизация расположения препятствий при инициализации платы
  • Рандомизация ряда при появлении новых препятствий
  • Метод перемещения для обработки движения препятствий
  • Метод добавления дополнительных препятствий в каждом раунде
  • Способ учета игроков, находящихся за пределами поля
  • Обновите методы для перемещения объекта игрока и учета того, что он находится «вне границ».

Поскольку в Уроке 5 многое происходит, я рекомендую прочитать обзор перед тем, как что-либо внедрять, если только вы не хотите писать код, а затем рефакторить его.

Во-первых, мы добавим метод генератора случайных чисел, чтобы мы могли использовать его по мере необходимости в любых экземплярах Obstacles.

Мы будем использовать новый метод getRandom, чтобы мы могли рандомизировать количество препятствий, с которых мы начинаем доску. Передайте максимальный параметр 10, чтобы мы не добавляли слишком много препятствий. ;)

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

Вы могли заметить, что здесь мы также используем метод getRandom. Мы расскажем об этом позже.

Мы хотим рандомизировать места появления препятствий, но метод getRadom находится в Obstacles. Давайте переместим рандомизацию в смесь, чтобы Obstacle + Obstacles могли иметь общее поведение. Использование примесей — это способ моделирования множественного наследования в JavaScript, где мы просто используем Object.assign, чтобы предоставить доступ к свойствам этого объекта из других объектов. Object.assign копирует все перечисляемые свойства в целевой объект.

Теперь мы можем удалить строку/столбец из параметров и просто создать свойства, используя getRandom для строки и свойство this.newGame + логику для столбца.

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

Придание движения препятствиям

Теперь вернемся к методу Obstacles.prototype.initializeObstacles — мы жестко кодировали аргумент столбца и строки, поэтому все препятствия находятся в одном месте. Вместо этого мы рандомизируем это значение. Для этого мы будем использовать метод getRandom в самом методе Obstacle.

Давайте добавим логику, чтобы проверить, не находится ли игрок за пределами поля. Мы используем массив массивов, поэтому мы знаем, что если игрок попытается подняться в первый массив, он выиграл.

Если игрок пытается пройти влево за индекс 0, игрок выходит за пределы поля.

Если игрок пытается пройти мимо последнего индекса, игрок выходит за пределы.

Если игрок пытается пройти мимо последнего массива, игрок выходит за пределы.

Таким образом, мы можем добавить это с помощью простых операторов if.

Полный код урока 5

Урок 6

В методе moveRight есть небольшая ошибка. Обновите, чтобы отразить приведенный ниже код:

Для остальной части этого руководства, пожалуйста, сделайте вывод о том, какие обновления сделаны из фрагмента кода + заголовка, если дополнительные сведения не предоставлены. Не стесняйтесь оставлять комментарии, если у вас есть какие-либо вопросы! Я напишу более подробные подробности позже.

Обновление для учета столкновений с нашими препятствиями

Мы продолжим инкапсулировать поведение препятствий на Obstacles. Поскольку мы вызываем метод gameOver в методе testForCollision, нам нужно передать его в качестве аргумента и передать этот контекст.

Глядя на код ниже, в строке 9 мы вызываем метод gameOver, который передается как аргумент func, и мы используем метод функции .call для использования явного контекста this как thisArg (передается как this из объекта game). В строке 29 мы вызываем этот метод для объекта-сотрудника obstacles в методе play. Вы можете видеть, что мы передаем массив дорожек как this.lanes, метод gameOver из FroggerGame и текущее значение this.

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

Цикл while также был добавлен для игрового процесса, поэтому игра заканчивается, если Фроггер побеждает или попадает в столкновение. Метод gameOver просто обновляет свойство keepPlaying как false, что выводит игру из цикла while.

Бонусные функции

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

Урок 7

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

  • Добавление очков, когда игрок выигрывает раунд
  • Добавление возможности играть больше раундов
  • Добавление логики сброса доски для другого раунда
  • Отслеживание жизней

В каждом новом раунде игрок может столкнуться до 3 раз, при этом игра заканчивается третьим нокаутом. Когда игра заканчивается из-за столкновения, игра заканчивается вообще.

Поскольку у нас есть раунды раундов, чтобы быть точным: каждый набор из 3 жизней такой же, как 1 раунд в игре.

Итак, у нас есть 1 игра, которая состоит из раундов. Каждый раунд состоит из 3 жизней. Каждый раз, когда раунд заканчивается победой, игрок может выбрать следующий раунд. Когда игра заканчивается из-за потери 3 жизней, основная игра заканчивается, как и текущий раунд. Давайте начнем!

Изменения игрока

Нам нужен способ сбрасывать позицию игрока при сбросе доски и способ отслеживать жизни. Три метода на Frog могут обрабатывать сброс позиции игрока, удаление жизней и сброс жизней. resetPlayerPosition будет вызываться при сбросе доски для новой игры. removeLife будет использоваться при столкновении с препятствием. resetLives используется, когда игрок выигрывает и начинается новая игра.

Изменения препятствий

Ниже многое происходит, и, скорее всего, есть лучший способ справиться с этим, но этот метод — отличная возможность попрактиковаться в передаче контекста + с помощью метода call. Код ниже в основном такой же, как:

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

Давайте посмотрим на функции, которые мы передаем в testForCollision.

Отображение точек

Обновляем обмен сообщениями и делаем его более удобным для пользователя

На этом урок заканчивается, но есть много вещей, которые вы можете сделать, чтобы улучшить этот код или сделать его более СУХИМ. Разместите ссылку на Github ниже, чтобы поделиться своей версией! Вот полное репо.