Нам понадобится лишь блокнот и браузер. Игра может запускаться на большинстве платформ. На 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 контексте (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) // Второй - закрашиваем
Можно открыть файл 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) мы поможем ему и подставим прямоугольник под указатель, чтобы у него точно получилось, и сбрасываем число неудач. Это нужно, чтобы улучшить понимание механики. Таким образом мы объясняем, как оно работает. Есть вероятность, что мы не смогли вызвать эмоцию — тогда и сама механика в целом не будет работать. Помним закон Мерфи.
зелёному участку спектра, цвет приятный. Этот цвет игрок не ожидает увидеть, т.к. мы показывали ему только красный и синий.
Явная механика
Регистрируем событие нажатия на экран. Событием будет нажатие указателем (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)
Полировка
После того, как механики готовы — можно приступать к «полировке» игры. Процесс разработки — бесконечен. Всегда есть что улучшить, появляются новые инструменты, новые возможности игровых систем, новые знания и навыки. Для создания и запуска игры из статьи — нужен только браузер и блокнот. Запустить можно в браузере, а это ПО есть в подавляющем числе пользовательских систем.
Однако, если есть желание и умение — даже игру с готовой механикой можно улучшить усилением эмоционального отклика (как в примере с добавлением звука). Это может быть как и графическое улучшение, так и звуковое. Можно усовершенствовать или добавить новые механики. Заменить прямоугольники на изображения, добавить фоновую музыку, добавить элементы управления и подсчета очков; мультиплеерное взаимодействие (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/
Добавить комментарий