Простейший способ написать игру

—

от автора

Нам понадобится лишь блокнот и браузер. Игра может запускаться на большинстве платформ. На windows, android, linux и даже на телевизоре. Технология называется HTML5.

Я немного гуманитарий, если что.

Создание базы

Создадим два файла: index.html и script.js. Названия файлов на английском, как и код игры.

index.html будет использоваться для запуска игры (будет открываться в браузере). Файл представляет собой документ, в котором есть тело (body) и файлы скриптов (script). Пока мы будем использовать только файл script.js. Нужно добавить две строчки в файл:

<body></body> <script src="script.js"></script>

Файл script.js хранит код игры на языке JavaScript. В первую очередь, нам нужно создать элемент холст (canvas), на котором мы будем рисовать игру. Затем, добавляем холст в тело документа (document). Добавляем в script.js:

const canvas = document.createElement('canvas')  // Создали холст document.body.appendChild(canvas)  // Добавляем холст в тело документа
2D

2D

Слово «контекст» означает ситуацию, в которой что-то происходит. Игра двухмерная, поэтому и рисовать мы будем в двумерном 2d контексте (context, 2D, Dimensional — измерение). Нарисуем на холсте два прямоугольника (rectangle, rect). Один черный (#000000), другой — серый (#aaaaaa). Цвета задаются в 16-ричным числом (hexidecimal; hex). Расстояния на холсте измеряются в пикселях (pixel; px), положение измеряется от верхнего левого угла, двумя значениями с названиями икс (x) и игрик (y). Соответственно, у прямоугольника есть положение на холсте (x,y), ширина и высота (width, height), цвет (color). Он может быть обведенным (stroke) или закрашенным (fill). О том, как использовать 2D контекст холста в WEB, а так же — полное руководство.

canvas.width = 400 // Ширина холста canvas.height = 225 // Высота холста context = canvas.getContext('2d') // Получаем 2d контекст context.strokeStyle = '#ff0000' // Цвет обводки context.fillStyle = '#0000ff' // Цвет заливки x = 0, y = 0, width = 100, height = 100 // Положение и размер прямоугольника context.strokeRect(x, y, width, height) // Первый - обводим x2 = 150, y2 = 0, width2 = 100, height2 = 100 // Положение и размер второго context.fillRect(x2, y2, width2, height2) // Второй - закрашиваем
v.0.1

v.0.1

Можно открыть файл index.html в браузере и проверить, что игра запускается и работает. Должно получиться как на картинке v.0.1. Еще мало похоже на игру, но теперь, уже можно добавить интерактивности (interaction, inter — посреди, act — действие), чтобы хоть что-то происходило.

Механики

Происходить что-то может только тогда, когда произошло какое-то событие/происшествие. Событие в контексте игры — это причина. Причины могут по разными. Например, пользователь запустил игру, прошло какое-то время, пользователь нажал клавишу или тыкнул пальцем/мышкой в экран. На эти события мы можем добавить реакцию игры (re — обратно, act — действие), иными словами — ответное действие от игры на событие от игрока. В контексте же игрока (с точки зрения игрока), событием будет появление на экране двух прямоугольников, а его реакцией будет эмоция (emotion, e — наружу, moveo — двигаться). В зависимости от эмоции — действие. Например — клик на экране.

Прежде чем добавить реакцию игры, нужно ответить на вопрос — чтобы что? Игра — инстинктивный способ обучения. Прошло обучение хорошо или плохо — игрок определяет по своим эмоциям. Если эмоции позитивные — материал усвоен успешно, негативные — есть проблемы. Игрок осознает и эмоционально реагирует только на некоторые возможности игры, так называемые механики (mech; megʰ — мочь). Если игра их не демонстрирует — они называются скрытыми, иначе — явными. Игрок в контексте игры — объект (ob — напротив, iactum — воздвигаю), т.е. игра воздействует на игрока посредством механик.

Чаще всего — механики берут уже из известных игр, которые себя зарекомендовали. Обычно, совокупность механик называют жанр игры. Например, жанр «квест» (quest — поиски). Как следует из названия — это механика поиска чего либо. Например, игра «Найди Вальдо» — картинка, на которой нужно найти парня в красно-белом колпаке и свитере среди кучи похожих людей. Помимо обучения — игра может привлечь игрока дополнительными механиками: социальные и материальные. Социальные вызывают эмоции чувства принадлежности, чувства признания; материальные — азарт (Не думаю, что моей компетенции хватает на исчерпывающий список, поэтому буду рад дополнению в комментариях).

Кривая обучения

Кривая обучения

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

Геймдизайн

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

Цветовая палитра "Инженерная"

Цветовая палитра «Инженерная»

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

Скрытая механика

Чтобы проверить, что игрок действительно научился — мы будем считать, сколько раз он успешно выполнил задачу. Мы не будем игрока информировать о количестве, т.е. это скрытая механика. Чем больше попыток потребуется — тем большим упорством должен обладать игрок. Упорство зависит от заинтересованности игрока. Будем считать, что среднее упорство игрока — 1/10, т.е. ожидаемый интерес к нашей игре 10%. Поэтому, мы будем ожидать, что игрок проведет в игре 5.4 секунды.

Согласно отчету Contentsquare 2021 Digital Experience Benchmark, среднее по больнице время просмотра одной страницы составляет 54 секунды.

Считаем количество успехов (successes) и неудач (fails). Если игрок справился, т.е. достиг максимального успеха (wins) — игрок обучен, игра окончена. Если же игрок не справляется — то ему нужно помочь. Если игрок достиг максимального числа неудач (looses) мы поможем ему и подставим прямоугольник под указатель, чтобы у него точно получилось, и сбрасываем число неудач. Это нужно, чтобы улучшить понимание механики. Таким образом мы объясняем, как оно работает. Есть вероятность, что мы не смогли вызвать эмоцию — тогда и сама механика в целом не будет работать. Помним закон Мерфи.

Поощрение => Эмоция» title=»Поощрение => Эмоция» width=»612″ height=»660″ data-src=»https://habrastorage.org/getpro/habr/upload_files/519/308/837/51930883735e719893e0de5df82687c4.jpg» data-blurred=»true»/></p>
<div><figcaption>Поощрение => Эмоция</figcaption></div>
</figure>
<p>Для того, чтобы закрепить обучение — добавим механику поощрения. Если игрок научился и количество успешных попыток достигло максимума (wins), мы наградим игрока новой приятной эмоцией — удивлением. Для этого будем использовать зеленый. Согласно исследованиям — человеческий глаз наиболее восприимчив к <a href=зелёному участку спектра, цвет приятный. Этот цвет игрок не ожидает увидеть, т.к. мы показывали ему только красный и синий.

Явная механика

Регистрируем событие нажатия на экран. Событием будет нажатие указателем (pointwerdown) на тело документа. Следствие события (символ ⇒) будет меняться в зависимости от того, куда игрок попал. У указателя тоже есть положение x и у, поэтому нам нужно проверить, что указатель (точка) находится внутри прямоугольника. Есть хорошая статья на эту тему, расписывать не буду. Если попал указателем в прямоугольник в x,y с шириной и высотой width и height — попытка успешная, мы подкрепляем синим. Если не попал — красным. При каждом нажатии нарисуем на экране прямоугольник попадания в точке указателя. При попадании — сбрасываем промахи и наоборот.

Чтобы игрок чувствовал прогресс (progress, pro — вперед, gradi — идти), что он не стоит на месте в своем обучении — нам нужно добавить элемент прогресса в механику. Цифр количества успехов (successes) и неудач (fails) игрок не видит, но нужно ему дать об этом знать. Для этого будем увеличивать размер нарисованного попадания в зависимости от successes и fails подряд. Если игрок превысит количество ошибок — закрашиваем предыдущий прямоугольник красным и рисуем новый, под указателем.

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

Для ощущения механики прогресса — будем закрашивать холст прозрачным белым цветом на каждое нажатие, из-за чего предыдущие попытки будут растворяться. Для отличника — будем закрашивать постоянно по времени. В среднем, в играх уровень интерактивности графики около 60 обновлений (кадров) в секунду (FPS, frames per secord). Для этого мы создадим событие по времени: каждые 1000/60, где 1000 — количество миллисекунд в секунде (милли-, milli-, одна тысячная), 60 — FPS. Т.е. каждые 16.66.. миллисекунд будет происходить закрашивание экрана прозрачным белым.

Для более ярких эмоций — воспользуемся звуком. Для этого мы будем использовать простой онлайн секвенсор (https://onlinesequencer.net/), создадим по звуку на каждое нажатие, по аналогии с цветом.

Реализация

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

Подготовка страницы
const canvas = document.createElement('canvas')  // Создали холст const context = canvas.getContext('2d') // Получаем 2d контекст document.body.appendChild(canvas)  // Добавляем холст в тело документа canvas.width = screen.width // Ширина холста по ширине экрана canvas.height = screen.height // Высота холста по ширине экрана //Это нужно, чтобы холст располагался в документе ровно canvas.style.left = 0 canvas.style.top = 0 canvas.style.position = 'absolute' //Если не во весь экран - скрываем скролл document.body.style.overflow = 'hidden' //Заголовок станицы document.title = 'Game'

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

Данные
//Конфигурация уровня const level = {     //Среднее время игры     defaultTime: 5400,     //Цвет текста нового уровня     levelUpColor: '#ff00ff',     //Цвет текста очков цепочки     scoreColor: '#000000',     easy: {         //Количество очков до следующего уровня         scoreUp: 10,         currentPosition: 0,         min: [100, 100],         max: [400, 300],         step: [100, 100],         positions:[ [100,100], [100,200], [200,200], [300,200] ],         lastTime: null,         bestTime: null,     }, } //Конфигурация базы const base = { // Положение и размер прямоугольника     x: level.easy.positions[0][0],      y: level.easy.positions[0][1],      width: 100,      height: 100, // Положение и размер второго     x2: level.easy.positions[0][0],     y2: level.easy.positions[0][1],     width2: 10,      height2: 10, //Заливка     strokeStyle:'#000000', //Закраска     fillStyle:'#aaaaaa', } //Конфигурация механики const mechanic = {     //Цвет успокоения     successColor: '#0000ff',     //Цвет победы     winColor: '#00ff00',     //Цвет поражения     failColor: '#ff0000',     //Закраска экрана     eraserColor: '#ffffff20',     wins: 3,     looses: 3,     successes: 0,     fails: 0,     sizeStep: 20,     winner:false,     hits: 0,     success: false,     cursor: { x: 0, y: 0 },     size: 0,     overwinSizeStep: 10,     overwin: 0,     baseWidth: base.width,     baseHeight: base.height,     pointerShrink: 0.85, } //Пути к звукам const audio = {     fail:'sfx/fail.mp3',     level:'sfx/level.mp3',     success: 'sfx/success.mp3',     win:'sfx/win.mp3',     loose:'sfx/loose.mp3',     score:'sfx/score.mp3' }

Теперь можно работать с этими данными и определять, как механика игры будет работать.

База
//Рисуем базу function drawBase() {     context.strokeStyle = base.strokeStyle // Цвет обводки     context.fillStyle = base.fillStyle // Цвет заливки     context.fillRect(base.x, base.y, base.width, base.height) // закрашиваем     context.strokeRect(base.x2, base.y2, base.width2, base.height2) // обводим }

Заливка прозрачным
function fade(){     context.fillStyle = mechanic.eraserColor // Цвет заливки     context.fillRect(0, 0, canvas.width, canvas.height) // закрашиваем }

Событие при клике
function onClick(e) {     fade()     base.x2 = e.x - base.width2 / 2     base.y2 = e.y - base.height2 / 2     mechanic.hits++     mechanic.cursor = getCursorPositionOnCanvas(canvas, e)     mechanic.success =          mechanic.cursor.x >= base.x && mechanic.cursor.y >= base.y         && mechanic.cursor.x <= base.x + base.width         && mechanic.cursor.y <= base.y + base.height     if(mechanic.success){         new Audio(audio.success).play()         mechanic.fails = 0         mechanic.successes++         if(mechanic.successes >= mechanic.wins){             new Audio(audio.win).play()             mechanic.overwin = 1 + mechanic.successes - mechanic.wins             mechanic.size = mechanic.size + mechanic.sizeStep             mechanic.fillStyle = mechanic.winColor             level.easy.currentPosition++             level.easy.currentPosition = level.easy.currentPosition % level.easy.positions.length             if(level.easy.currentPosition === 0){                 mechanic.winner = true                 if(level.easy.lastTime===null){                     level.easy.lastTime = new Date()                 }                  else{                     level.easy.bestTime =  new Date() - level.easy.lastTime                 }             }             const position = level.easy.positions[level.easy.currentPosition]             base.x = position[0]             base.y = position[1]             base.x2 =  position[0] + base.width / 2 -  base.width2 / 2             base.y2 = position[1] + base.height / 2 -  base.height2 / 2             drawBase()         }         else{             mechanic.fillStyle = mechanic.successColor              mechanic.size = mechanic.successes / mechanic.wins * mechanic.sizeStep         }     }     else{         new Audio(audio.fail).play()         level.easy.currentPosition = 0         level.easy.lastTime = null         mechanic.fails++         mechanic.successes=0         if(mechanic.fails >= mechanic.looses){             new Audio(audio.loose).play()             mechanic.winner = false             mechanic.fails = 0             context.fillStyle = mechanic.failColor             context.fillRect(base.x, base.y, base.width, base.height)             base.x = mechanic.cursor.x - base.width /2             base.y = mechanic.cursor.y - base.height /2             drawBase()         }         if(mechanic.winner){             drawBase()         }         if(mechanic.overwin > 0){             mechanic.successes=mechanic.wins             mechanic.overwin = 0             mechanic.size = 0             context.fillStyle = mechanic.failColor             context.fillRect(base.x, base.y, base.width, base.height)             drawBase()         }         mechanic.size = mechanic.fails / mechanic.looses * mechanic.sizeStep         mechanic.fillStyle = mechanic.failColor     }     context.fillStyle = mechanic.fillStyle     context.fillRect(mechanic.cursor.x - mechanic.size/2, mechanic.cursor.y - mechanic.size/2, mechanic.size , mechanic.size)      if(level.easy.bestTime !== null){         showText(e)     } }

Показать текст очков и победы
function showText(e) {     new Audio(audio.score).play()     const speed = level.defaultTime / level.easy.bestTime     const rating = 1 + Math.round(speed * 5)     const fontSize = (10 + 15 * speed)     context.font = fontSize + "px serif"     context.fillStyle = level.scoreColor     context.fillText('+' + rating + '!', e.x, e.y)     level.easy.bestTime = null     level.easy.lastTime = new Date()     if (rating > level.easy.scoreUp) {         new Audio(audio.level).play()         document.title = 'x' + level.easy.positions.length         context.fillStyle = level.levelUpColor         context.fillText('NEXT LEVEL!', e.x, e.y + fontSize)         const random = getRandom(mechanic.hits)         addLevelPosition(random)     } }

Добавляем новую позицию в цепочку для отличника
function addLevelPosition(random){     const lastPosition = level.easy.positions[level.easy.positions.length-1]     const directions = []     if(lastPosition[0]>level.easy.min[0]){ directions.push( [-1,0]) }     if(lastPosition[0]<level.easy.max[0]){ directions.push( [1,0]) }     if(lastPosition[1]>level.easy.min[1]){ directions.push( [0,-1]) }     if(lastPosition[1]<level.easy.max[1]){ directions.push( [0,1]) }     const directionId = Math.round(random * (directions.length-1))     const direction = directions[directionId]     const newX = lastPosition[0]+direction[0]*level.easy.step[0]     const newY = lastPosition[1]+direction[1]*level.easy.step[1]     const newPosition = [newX,newY]     level.easy.positions.push(newPosition) }

Детали

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

function getCursorPositionOnCanvas(canvas, evt) {     var rect = canvas.getBoundingClientRect()     return {         x: evt.clientX - rect.left,         y: evt.clientY - rect.top     } }

В качестве случайного значения (random) — я беру номер нажатия и рассчитываю от него косинус. Получается псевдослучайное (непредсказуемое) значение от -1 до 1, минус — убираем.

function getRandom(increment){     const random = Math.abs(Math.cos(increment))     return random }

События

В игре используется 3 типа событий: игрок запустил игру — рисуем базу. Игрок нажал на документ — обрабатываем нажатие. Запускаем 2 таймера, первый — закраска экрана для отличника (winner), и для геометрического уменьшения размера попадания со временем, чтобы он не становился слишком большим.

drawBase() document.body.addEventListener('pointerdown', onClick) setInterval(()=>{     if(mechanic.winner>0){         fade()     } }, 1000/30) setInterval(()=>{     mechanic.size *= mechanic.pointerShrink },100)

Полировка

После того, как механики готовы — можно приступать к «полировке» игры. Процесс разработки — бесконечен. Всегда есть что улучшить, появляются новые инструменты, новые возможности игровых систем, новые знания и навыки. Для создания и запуска игры из статьи — нужен только браузер и блокнот. Запустить можно в браузере, а это ПО есть в подавляющем числе пользовательских систем.

OSU

OSU

Однако, если есть желание и умение — даже игру с готовой механикой можно улучшить усилением эмоционального отклика (как в примере с добавлением звука). Это может быть как и графическое улучшение, так и звуковое. Можно усовершенствовать или добавить новые механики. Заменить прямоугольники на изображения, добавить фоновую музыку, добавить элементы управления и подсчета очков; мультиплеерное взаимодействие (multi — несколько, player — игрок), оптимизировать загрузку звуков и т.д. Один из успешных примеров кликера — OSU, немаловажной механикой в ней является музыкальный ритм, воздействие на чувства такта (tactus — прикосновение, толчок, удар), эмоция когда «музыка качает».

Бесконечность - не предел

Бесконечность — не предел

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

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

Редактор кода Visual Studio Code https://code.visualstudio.com/

Онлайн генератор музыки Online Sequencer https://onlinesequencer.net/

Стандартный 2D контекст сильно ограничен и не производителен, поэтому я не рекомендую использовать его в профессиональной разработке игр. С появлением WebGL — игры в браузере могут быть намного более производительными, а значит — более наворочены графически. Я его в статье не использовал, т.к. он довольно сложный. Однако, его можно использовать так:

const canvas = document.getElementById("myCanvas"); const gl = canvas.getContext("webgl");

Есть две популярные библиотеки для работы в webgl, которые упрощают работу:

https://threejs.org/ для 3D

https://pixijs.com/ для 2D

QA

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

  • Но скачать ассетов в UE и натыкать скрипты мышкой же проще? Возможно, но, статья про то, как написать игру. Предполагаю, что читатель писать уже умеет.

  • Где попробовать? Выложил на itch.io. Максимум набрал цепочку из 12.

Заключение

Статью написал в рамках подготовки к пятничному игровому джему (GMTK Game Jam 2023). Никогда в них не участвовал, вот и решил проверить, что успею запилить хоть что-то за пару дней, заодно рефлексировал на тему гейм дизайна. Поболейте за меня или сами участвуйте.

Спасибо за внимание!


ссылка на оригинал статьи https://habr.com/ru/articles/745954/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *