Создание игры на Javascript «Колесо фортуны» для Zoom Group

Этот подход выполняет итерацию по каждой букве в головоломке Программирование и разработка

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

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

Наша новая игра Wheel of Fortune была хорошо принята. Конечно, SitePoint — технический блог, поэтому я представлю обзор того, что было сделано для создания рудиментарной версии игры для демонстрации экрана на наших онлайн-встречах. Я расскажу о некоторых компромиссах, которые я сделал на этом пути, а также выделю некоторые возможности для улучшения и вещи, которые я должен был сделать иначе, задним числом.

Первые дела в первую очередь

Если вы из США, вы, вероятно, уже знакомы с Колесом фортуны., поскольку это самая продолжительная американская игровая выставка в истории. (Даже если вы не в Соединённых Штатах, вы, вероятно, знакомы с каким-то вариантом шоу, поскольку оно было адаптировано и транслировалось на более чем 40 международных рынках.) По сути, игра представляет собой Палач: участники пытаются разгадать скрытое слово или фраза, отгадывая её буквы. Суммы призов за каждую правильную букву определяются путём вращения большого колеса в стиле рулетки с долларовыми суммами и ужасных мест банкротства. Участник вращает колесо, угадывает букву, и обнаруживаются любые экземпляры указанной буквы в головоломке. Правильные догадки дают участнику ещё один шанс повернуть и угадать, в то время как неправильные угадывания продвигают игру к следующему участнику. Загадка решается, когда участник успешно угадывает слово или фразу.

Для меня первым делом было решить, как мы физически (виртуально) будем играть в игру. Игра мне нужна была только для одной или двух встреч, и я не хотел тратить много времени на создание полноценной игровой платформы, поэтому создание приложения в виде веб-страницы, которую я мог загружать локально и показывать экран другим пользователям, было нормально. Я мог вести игру и управлять игровым процессом с помощью различных нажатий клавиш в зависимости от того, что хотели игроки. Я также решил вести счёт карандашом и бумагой — о чём позже пожалел. Но, в конце концов, простой старый JavaScript, немного холста и несколько файлов изображений и звуковых эффектов — это всё, что мне нужно для создания игры.

Цикл игры и состояние игры

Хотя я представлял себе это как «быстрое и грязное» ?? проект, а не какой-то блестяще закодированный шедевр, соответствующий всем известным передовым практикам, моей первой мыслью было начать создание игрового цикла. Вообще говоря, игровой код — это конечный автомат, который поддерживает переменные и тому подобное, представляя текущее состояние игры с некоторым дополнительным кодом, прикреплённым для обработки пользовательского ввода, управления / обновления состояния и визуализации состояния с красивой графикой и звуковыми эффектами. Код, известный как игровой цикл, многократно выполняется, инициируя проверки ввода, обновления состояния и рендеринг. Если вы собираетесь правильно создать игру, вы, скорее всего, будете следовать этому шаблону. Но вскоре я понял, что мне не нужен постоянный мониторинг / обновление / рендеринг состояния, и поэтому я отказался от игрового цикла в пользу базовой обработки событий.

С точки зрения поддержания состояния, код должен был знать текущую головоломку, какие буквы уже были угаданы и какой вид отображать (либо доску головоломки, либо вращающееся колесо). Они будут глобально доступны для любой логики обратного вызова. Любые действия в игре будут запускаться при обработке нажатия клавиш.

Вот как стал выглядеть основной код:

(function (appId) {
  // canvas context
  const canvas = document.getElementById(appId);
  const ctx = canvas.getContext('2d');

  // state vars
  let puzzles = [];
  let currentPuzzle = -1;
  let guessedLetters = [];
  let isSpinning = false;

  // play game
  window.addEventListener('keypress', (evt) => {
    //... respond to inputs
  });
})('app');

Игровое поле и головоломки

Игровое поле Wheel of Fortune представляет собой сетку, каждая ячейка которой находится в одном из трёх состояний:

  • empty: пустые ячейки в головоломке не используются (зелёные);
  • пусто: ячейка представляет собой скрытую букву в головоломке (белый);
  • visible: ячейка показывает букву в головоломке.
Читайте также:  Цикл событий Node.js: Руководство разработчика по концепциям и коду

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

let puzzle = [...'########HELLO##WORLD########'];

const cols = 7;
const width = 30;
const height = 35;

puzzle.forEach((letter, index) => {
  // calculate position
  let x = width * (index % cols);
  let y = height * Math.floor(index / cols);

  // fill
  ctx.fillStyle = (letter === '#') ? 'green' : 'white';
  ctx.fillRect(x, y, width, height);

  // stroke
  ctx.strokeStyle = 'black';
  ctx.strokeRect(x, y, width, height);

  // reveal letter
  if (guessedLetters.includes(letter)) {
      ctx.fillStyle = 'black';
      ctx.fillText(letter, x + (width / 2), y + (height / 2));
  }
});

Этот подход выполняет итерацию по каждой букве в головоломке, вычисляя начальные координаты, рисуя прямоугольник для текущей ячейки на основе индекса и других деталей, таких как количество столбцов в строке, а также ширина и высота каждой ячейки. Он проверяет символ и соответствующим образом окрашивает ячейку, предполагая #?? используется для обозначения пустой ячейки, а буква обозначает пробел. Затем на ячейке рисуются угаданные буквы, чтобы раскрыть их.

Этот подход выполняет итерацию по каждой букве в головоломке

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

Вот как может выглядеть головоломка после второго подхода:

let puzzle = {
  background: 'img/puzzle-01.png',
  letters: [
    {chr: 'H', x: 45,  y: 60},
    {chr: 'E', x: 75,  y: 60},
    {chr: 'L', x: 105, y: 60},
    {chr: 'L', x: 135, y: 60},
    {chr: 'O', x: 165, y: 60},
    {chr: 'W', x: 45,  y: 100},
    {chr: 'O', x: 75,  y: 100},
    {chr: 'R', x: 105, y: 100},
    {chr: 'L', x: 135, y: 100},
    {chr: 'D', x: 165, y: 100}
  ]
};

Ради эффективности я бы рекомендовал включить ещё один массив для отслеживания совпадающих букв. Имея только guessedLettersдоступный массив, вам нужно будет многократно сканировать буквы головоломки на несколько совпадений. Вместо этого вы можете настроить массив для отслеживания решённых букв и просто скопировать в него соответствующие определения, когда игрок сделает своё предположение, например:

const solvedLetters = [];

puzzle.letters.forEach((letter) => {
  if (letter.chr === evt.key) {
    solvedLetters.push(letter);
  }
});

Рендеринг этой головоломки выглядит так:

// draw background
const imgPuzzle = new Image();
imgPuzzle.onload = function () {
  ctx.drawImage(this, , );
};
imgPuzzle.src = puzzle.background;

// reveal letters
solvedLetters.forEach((letter) => {
  ctx.fillText(letter.chr, letter.x, letter.y);
});

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

Вращая Колесо

На первый взгляд, вращение колеса показалось сложной задачей: визуализировать круг из цветных сегментов с суммами призов, оживить его вращение и остановить анимацию на случайной сумме приза. Но немного творческого мышления сделало это самой простой задачей во всём проекте.

Независимо от вашего подхода к кодированию головоломок и рендерингу игрового поля, колесо — это то, для чего вы, вероятно, захотите использовать графику. Повернуть изображение намного проще, чем нарисовать (и оживить) сегментированный круг с текстом; использование изображения устраняет большую часть сложности заранее. Затем вращение колеса сводится к вычислению случайного числа больше 360 и многократному повороту изображения на много градусов:

const maxPos = 360 + Math.floor(Math.random() * 360);
for (let i = 1; i < maxPos; i++) {
  setTimeout(() => {
    ctx.save();
    ctx.translate(640, 640);
    ctx.rotate(i * 0.01745); // radians
    ctx.translate(-640, -640);
    ctx.drawImage(imgWheel, , );
    ctx.restore();
  }, i * 10);
}

Я создал грубый эффект анимации, используя setTimeoutдля планирования вращений, причём каждое вращение планировалось всё дальше и дальше в будущее. В приведённом выше коде запланировано рендеринг первого поворота на 1 градус через 10 миллисекунд, второго рендеринга через 20 миллисекунд и т.д. Конечный эффект — вращающееся колесо примерно на один оборот каждые 360 миллисекунд. И обеспечение того, чтобы начальное случайное число было больше 360, гарантирует, что я анимирую как минимум один полный оборот.

Краткое замечание, которое стоит упомянуть, заключается в том, что вы можете свободно играть с «магическими значениями» предоставляется для установки / сброса центральной точки, вокруг которой вращается холст. В зависимости от размера вашего изображения и от того, хотите ли вы, чтобы было видно всё изображение или только верхнюю часть колеса, точная средняя точка может не дать того, что вы имеете в виду. Можно изменять значения до тех пор, пока не будет достигнут удовлетворительный результат. То же самое касается множителя тайм-аута, который вы можете изменить, чтобы изменить скорость анимации вращения.

Банкротство

Я думаю, что все мы испытываем лёгкое злорадство, когда игрок останавливается на банкроте. Забавно наблюдать, как жадный участник крутит колесо, чтобы набрать ещё несколько букв, когда очевидно, что он уже знает решение головоломки — только чтобы потерять всё. И ещё есть забавный звуковой эффект банкротства! Без него никакая игра «Колесо фортуны» не будет полноценной.

Для этого я использовал объект Audio, который даёт нам возможность воспроизводить звуки в JavaScript:

function playSound(sfx) {
  sfx.currentTime = ;
  sfx.play();
}

const sfxBankrupt = new Audio('sfx/bankrupt.mp3');

// whenever a spin stops on bankrupt...
playSound(sfxBankrupt);

Но что вызывает звуковой эффект?

Одним из решений было бы нажать кнопку, чтобы вызвать эффект, поскольку я уже управлял бы игровым процессом, но было бы более желательно, чтобы игра автоматически воспроизводила звук. Поскольку банкротные клинья — единственные чёрные клинья на колесе, можно узнать, останавливается ли колесо на банкроте, просто взглянув на цвет пикселя:

const maxPos = 360 + Math.floor(Math.random() * 360);
for (let i = 1; i < maxPos; i++) {
  setTimeout(() => {
    ctx.save();
    ctx.translate(640, 640);
    ctx.rotate(i * 0.01745); // radians
    ctx.translate(-640, -640);
    ctx.drawImage(imgWheel, , );
    ctx.restore();

    if (i === maxPos - 1) {
      // play bankrupt sound effect when spin stops on black
      const color = ctx.getImageData(640, 12, 1, 1).data;
      if (color[] ===  && color[1] ===  && color[2] === ) {
        playSound(sfxBankrupt);
      }
    }
  }, i * 10);
}

В своём коде я сосредоточился только на банкротствах, но этот подход можно расширить и для определения суммы призов. Хотя несколько сумм один и тот же клин цвета — например, $ 600, $ 700 и $ 800 все появляются на красных клиньях — вы можете использовать несколько различные оттенки, чтобы дифференцировать суммы: rgb(255, 50, 50), rgb(255, 51, 50)и rgb(255, 50, 51)неразличимы для человеческого глаза, но легко идентифицируется с помощью приложения. Оглядываясь назад, я должен был продолжить изучение этого вопроса. Мне показалось утомительным вручную вести счёт при нажатии клавиш и запуске игры, и дополнительные усилия по автоматизации ведения счета определенно того стоили.

В своём коде я сосредоточился только на банкротствах

Заключение

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

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

Оцените статью
bestprogrammer.ru
Добавить комментарий