Как сделать спрайтовую игру
Cocos2d-x — Работа со спрайтами
Предисловие
Эта статья является второй, из серии переводов документации к игровому движку Cocos2d-x. Если вы не видели первую часть перевода, то советую сначала ознакомится с ней:
Cocos2d-x — Основные понятия
А теперь продолжим!
Что такое спрайт
Создание спрайтов
Существуют различные способы создания спрайтов, зависящие от того что вам необходимо выполнить. Вы можете создать спрайт, используя изображения различных графических форматов, включая: PNG, JPEG, TIFF и другие. Давайте рассмотрим некоторые методы создания и поговорим о каждом из них.
Создание простого спрайта
Спрайт может быть создан путем указания файла изображения.
Код выше создает спрайт, используя изображение mysprite.png. В результате, созданный спрайт использует целое изображение. Он имеет те же размеры, что и mysprite.png. Если файл изображения 200 x 200 то результирующий спрайт тоже будет 200 x 200.
Создание спрайта с использованием Rect (прямоугольника)
В предыдущем примере, созданный спрайт имел тот же размер, что оригинальное изображение. Если вы хотите создать спрайт использующий только часть файла изображения, вы можете сделать это путем указания прямоугольного участка – Rect.
Rect имеет 4 значения: origin x, origin y, width и height.
Rect берет левый верхний угол за начало координат. Это противоположно тому принципу, что мы использовали, когда устанавливали позицию на экране, отчитывая координаты от левого нижнего угла. Таким образом, результирующий спрайт – только часть исходного файла изображения. В этом случае, размер спрайта 40 x 40 начиная от левого верхнего угла.
Если вы не зададите Rect, Cocos2d-x автоматически будет использовать полную высоту и ширину заданного изображения. Взгляните на код ниже. Если вы используете изображение с размерами 200 x 200, то следующие два выражения будут иметь одинаковые результаты.
Создание спрайта из текстурного атласа (Sprite Sheet)
Sprite sheet – это способ объединения спрайтов в один файл. Использование sprite sheet помогает добиться лучшей производительности, путем пакетной обработки вызовов рисования. Они могут также сохранить дисковую- и видео-память, если упаковать спрайты более эффективно (обычно требуются специальные инструменты). Вы прочитаете больше об этом в продвинутой главе, но это только один из многих стандартных методов повышения производительности игр.
При использовании sprite sheet, он сначала загружается в SpriteFrameCache. SpriteFrameCache – это класс кеширования, который сохраняет объекты SpriteFrame, для быстрого доступа к ним. SpriteFrame загружается один раз и сохраняется в SpriteFrameCache.
Вот пример sprite sheet:
Давайте рассмотрим подробнее, что происходит:
Как вы можете видеть, sprite sheet объединяет все спрайты в один файл и минимизирует лишнее пространство.
Загрузка Sprite Sheet
Вероятнее всего, вы будете осуществлять загрузку в AppDelegate:
Теперь, когда мы имеем текстурный атлас загруженный в SpriteFrameCache, мы можем создать спрайт.
Создание спрайта из SpriteFrameCache
Это создание спрайта, путем вытягивания его из SpriteFrameCache.
Создание спрайта из SpriteFrame
Еще один способ создать такой же спрайт – получить SpriteFrame из SpriteFrameCache, и затем создать спрайт при помощи SpriteFrame. Пример:
Инструменты для создания Sprite Sheets
Создание sprite sheet вручную – утомительный процесс. К счастью, есть инструменты, которые могут генерировать их автоматически. Эти инструменты предлагают множество возможностей для максимальной оптимизации ваших спрайтов!
Здесь представлено несколько инструментов:
Манипуляции со спрайтами
После создания спрайта, вы получите доступ к разнообразным его свойствам, которыми можно манипулировать.
Опорная точка и позиция
Опорная точка (Anchor Point) – это точка, которую вы задаете, чтобы указать, какая часть спрайта будет использоваться, при установке его координат. Опорная точка влияет только на свойства связанные с трансформацией. Они включают: scale, rotation, skew. И не включают: color и opacity. Опорная точка использует систему координат от нижнего левого угла. Учитывайте это, при выполнении своих расчетов. По умолчанию, все Node объекты имеют стандартную опорную точку (0.5, 0.5).
Задать опорную точку просто:
Чтобы представить это визуально:
Свойства спрайтов, на которые влияет опорная точка
Использование опорной точки влияет только на свойства связанные с трансформированием. Такие как: scale, rotation, skew.
Position
Опорная точка влияет на позицию спрайта, поскольку именно эта точка используется в качестве основной при позиционировании. Давайте посмотрим, как это происходит. Обратите внимание на цветную линию, и то где находятся спрайты относительно ее. Важно отметить, что такой эффект был получен, лишь путем изменения опорной точки. Мы не использовали setPosition() для достижения этого:
Позицию спрайта можно также задать методом setPosition().
Rotation
Изменяет вращение спрайта, на положительное или отрицательное число градусов. Положительные значения поворачивают спрайт по часовой стрелке, в то время как отрицательные поворачивают против часовой стрелки. Стандартное значение – 0.0
Scale
Изменяет масштаб спрайта, либо по каждой оси отдельно, либо равномерно по обеим. Стандартное значение – 1.0
Изменяет наклон спрайта, либо отдельно по осям x и y, либо равномерно для всех. Стандартное значение – 0.0
Свойства спрайта, на которые не влияет опорная точка
Существует несколько свойств объекта Sprite, на которые не влияет опорная точка. Почему? Потому что они изменяют только внешний вид, подобно свойствам color и opacity.
Color
Изменяет цвет спрайта. Это делается при помощи Color3B. Мы еще не сталкивались с Color3B, но это просто объект, представляющий собой RGB цвет. RGB цвет включает в себя 3 значения (Red, Green, Blue), от 0 до 255. Cocos2d-x также предоставляет предопределенные цвета, из которых вы можете выбрать. Использование их будет немного быстрее, поскольку они уже определены заранее. Вот некоторые примеры:
Opacity
Изменяет непрозрачность спрайта на заданное значение. Это свойство принимает значения от 0 до 255, где 255 – полная не прозрачность, а 0 – абсолютная невидимость. Стандартное значение 255 (полная видимость).
Полигональные спрайты
Полигональный спрайт это тоже спрайт, который используется для вывода 2d изображения. Однако, в отличие от нормального спрайта, который является прямоугольником, состоящим из двух треугольников, полигональный спрайт состоит из серии треугольников.
Зачем использовать полигональный спрайт?
Ответ прост — производительность!
Существует много технических терминов о скорости пиксельного заполнения, которые мы могли сюда вывалить, но вам просто нужно понять, что PolygonSprite рисуется на основе формы вашего спрайта, а не прямоугольника с избыточной площадью. Этот способ прорисовки, позволяет сэкономить на ненужной работе. Рассмотрим пример:
Заметили различия между левой и правой версиями?
Слева, типичный Sprite, который прорисовывается, с использованием двух треугольников.
Справа, PolygonSprite, рисующийся большим количеством маленьких треугольников.
Стоит ли идти на такой компромисс, чисто по соображениям производительности, зависит от ряда факторов (форма/детализация спрайта, размер, количество нарисованного на экране, и так далее), но в основном, вершины дешевле пикселей на современных графических процессорах.
AutoPolygon
AutoPolygon – это вспомогательный класс. Его цель – создавать 2d полигональную сетку на основе изображения.
Существуют функции для каждого шага обработки этого процесса, от определения всех точек, до триангуляции. Результат может быть передан в функцию create объекта sprite, для создания PolygonSprite. Пример:
Создаём игру, используя canvas и спрайты
Готовимся
Игра может показаться сложной, но на самом деле всё сводится к использованию нескольких компонентов. Я всегда был поражен насколько далеко можно зайти с canvas, несколькими спрайтами, слежением за столкновениями и игровым циклом.
Для того, чтобы полностью сосредоточиться на компонентах игры, я не буду разжевывать каждую строчку кода и API. Это статья написана advanced-уровня, но я надеюсь, что будет понятна для людей всех уровней. В статье предполагается, что Вы уже знакомы с JavaScript и основами HTML. Также немного затронем canvas API и такие основные игровые принципы, как цикл игры.
Создание Canvas
Начнем изучать код. Большая часть игры находится в app.js.
Первое, что мы делаем, это создаём тег и задаём ему ширину и высоту. Мы делаем это динамически, для того чтобы держать все в JS, но вы может создать canvas в HTML документе, и получить его с помощью getElementById. Нет никакой разницы между этими двумя способами, это просто вопрос предпочтений.
Canvas имеет метод getContext, который используется для получения контекста отображения. Контекст — это объект, вызывая методы которого, вы взаимодействуете с canvas API. Так же Вы можете передать параметром ‘webgl’, если вы хотите использовать WebGL для 3D-сцен.
Дальше мы будем использовать переменную ctx для отображения всех элементов.
Цикл игры
Нам нужен такой цикл игры, который бы постоянно обновлял и отображал игру. Вот как это выглядит:
Мы обновляем и отображаем сцены, и затем используем requestAnimationFrame для постановки в очередь следующего цикла. Действительно проще было бы использовать setTimeout(main, 1000/60), пытаясь отобразить 60 кадров/сек. В сам вверху app.js мы сделали обертку для requestAnimationFrame, так как не все браузеры поддерживают данный метод.
Никогда не используется setTimeout(main, 1000/60), так как он менее точный и тратит много циклов на отображение, тогда, когда это не требуется.
Параметр в dt в функции update — это разница между текущим временем и временем последнего обновления. Никогда не обновляйте сцену используя постоянное значение для кадра (в духе, x += 5). Ваша игра будет будет работать на разных компьютерах/платформах по-разному, по этому необходимо обновлять сцену не зависимо то частоты кадров.
Это достигается путём расчет времени с последнего обновления и выражения всех перемещений в пикселях в секунду. И движение становится следующим x += 50*dt, или 50 пикселей в секунду.
Загрузка ресурсов и запуск игры
Следующая часть кода инициализирует игру и загружает все необходимые ресурсы. Для этого используется один из отдельно написанных вспомогательных классов, resources.js. Это очень простая библиотека, которая загружает все изображения и вызывает событие, когда они все загрузятся.
Игра включается в себя больше количество ресурсов, таких как изображения, данные сцены и другое. Для 2D игр, основным ресурсом являются изображения. Вам необходимо загрузить все ресурсы перед запуском приложения, для того, чтобы было возможно использовать из немедленно.
В JavaScript легко загрузить изображения и использовать их когда они понадобятся:
Это становится очень утомительным, если конечно у вас много изображений. Вам необходимо сделать кучу глобальных переменных и для каждой проверить загрузилась ли она. Я написал базовый загрузчик ресурсов, чтобы все это сделать автоматически:
Как же это работает: Вы вызываете resources.load со всеми изображениями для загрузки, и затем вызываете resources.onReady для создания callback на событие загрузки всех данных. resources.load не используется позже в игре, только в время старта
Загруженные изображения хранятся в кеше в resourcesCache, и когда все изображения буду загружены, будут вызваны все callback’и. Теперь мы можем просто сделать так:
Что бы получить изображение используется resources.get(‘img/sprites.png). Легко!
Вы можете в ручную загружать все изображения и запускать игру, либо, для упрощения процесса, использовать что-то в духе resources.js.
В приведенном выше коде, init вызывается, когда все изображения будут загружены. Init создаст фоновое изображение, повесит события на кнопки «Играть снова», сброса и старта игры, запустит игру.
Состояние игры
Теперь мы начинаем! Давайте приступим к реализации некоторой логики игры. В ядре каждой игры — «состояние игры». Это данные, которые представляют текущее состояние игры: список объектов на карте, координаты и другие данные; текущие очки, и многое другое.
Ниже представлено состояние нашей игры:
Кажется что много всего, но на самом деле все не так сложно. Большинство из переменных — это отслеживаемые значения: когда игрок последний раз выстрелил (lastFired), как долго запущена игра (gameTime), закончена ли игра (isGameOver), изображение местности (terrainPattern) и очки (score). Так же описаны объекты на карте: пули, враги, взрывы.
Так же есть сущность игрока, в которой отслеживается положение игрока и состояние спрайта. Прежде чем перейдем к коду, давайте поговорим о сущностях и спрайтах.
Сущности и спрайты
Сущности
Этот код добавить врага на карту, в положение x = 100, y = 50 с определенным спрайтом.
Спрайты и анимация
Спрайт — это изображение, которое отображает представление сущности. Без анимации спрайты представляют собой обычное изображение, представленное с помощью ctx.drawImage.
Мы можем реализовать анимацию путём загрузки нескольких изображений и смены их в течении времени. Это называется кадр анимации.
Если мы будет чередовать эти изображения от первого к последнему, это будет выглядеть так:
Для того, чтобы упростить редактирование и загрузку изображений, обычно все компонуют на одном, это называется спрайт картой. Возможно, вы уже знакомы с этой техникой из CSS.
Это спрайт карта для нашей игры (с прозрачным фоном):
Мы используем Hard Vacuum набор изображений. Этот набор — это набор bmp файлов, поэтому я скопировал необходимые мне изображения и вставил из на один спрайт лист. Для этого Вам потребуется простой графический редактор.
Будет трудно управлять всеми анимациями вручную. Для этого используем второй вспомогательный класс — sprite.js. Это маленький файл который содержит в себе логику анимации. Посмотрим:
Каждый объект Sprite так же имеет метод render для отрисовки себя. В нем находится основная логика анимации. Он следит за тем, какой кадр должен быть отрисован, рассчитывает его координаты на спрайт карте, и вызывает ctx.drawImage для отрисовки кадра.
Мы используем 3-ю форм drawImage, которая позволяет нам указать размер спрайта, смещении и направлении раздельно.
Обновление сцены
Помните как в нашем игровом цикле мы вызывали update(dt) каждый кадр? Мы должны определить эту функцию сейчас, которая должна обрабатывать обновление всех спрайтов, обновление позиций сущностей и столкновений.
Нажатия клавиш
Если игрок нажмет «s» или стрелку вниз, мы перемещаем игрока вверх по оси ординат. Система координат canvas имеет координаты (0,0) в верхнем левом углу и поэтому увеличении позиции игрока приводит к снижению положения игрока на экране. Мы сделали тоже самое для все остальных клавиш.
Обратите внимание, что мы определили playerSpeed в начале app.js. Вот скорости которые мы задали:
Сущности
Все сущности нуждаются в обновлении. У нас есть сущность игрока и 3 массива с сущностями пуль, врагов и взрывов.
Начнем сначала: спрайт игрока обновляется просто вызывая функцию update спрайта. Это двигает продвигает анимацию вперед.
Следующие 3 циклы для пуль, врагов и взрывов. Процесс одинаков для всех: обновить спрайт, обновить движение, и удалить, если сущность ушла за пределы сцены. Поскольку все сущности никогда не могу изменить направление своего движения, у нас нет необходимости сохранять их сущности после выхода из зоны видимости.
Движение пули является самым сложным:
Если bullet.dir = ‘up’, мы передвигаем пулю вниз по оси ординат. Наоборот, если dir = ‘down’, для значения по-умолчанию, мы передвигаем вдоль оси абсцисс.
Затем мы проверяем можем ли мы удалить сущность пули. Позиции проверяются относительно верхнего, нижнего и правого края, потому что пули движутся только в этих направлениях.
Для удаления пуль, мы удаляем данную объект из массива и уменьшаем i, иначе следующая пуля будет пропущена.
Отслеживание столкновений
Эти 2 функции могли быть объединены в одну, но мне кажется так легче читать. collides принимает координаты верхнего/левого и нижнего/правого углов обоих объектов и проверяет, есть ли какие то пересечения.
Функция boxCollides — это обертка для collides принимающая массивы с положением и размером каждого элемента. В функции используя размеры в рассчитывает абсолютные координаты положения.
А вот и код, который фактически обнаруживает столкновения:
Здесь мы проверяем столкновения игрока и врага, и если столкновение есть — игра окончена.
И наконец, давайте поговорим о checkPlayerBounds:
Он просто не даёт игроку выйти за пределы карты, держа его координаты в пределах 0 и canvas.width/canvas.height.
Рендер
Мы почти закончили! Сейчас нам надо просто определить функцию render, которая будет вызываться наших игровым циклом для отображения сцены каждого фрейма. Вот как это выглядит:
Первое что мы делаем — это отрисовка фона. Мы создали фон местности в init функции используя ctx.createPattern, и мы отрисовываем фон устанавливая fillStyle и вызывая функции fillRect.
Затем мы рисуем игрока, все пули, всех врагов и взрывы. renderEntites обходим циклом массивы сущностей и отрисовывает их. renderEntity использует трансформацию canvas для размещения объекта на экране. ctx.save сохраняет текущую трансформацию, а ctx.restore — восстанавливает.
Если вы посмотрите на функцию рендера спрайтов, то увидите что sprite располагается в позиции (0,0), но вызов ctx.translate перемещает объект на нужное место.
Игра окончена
Последнее что мы должны сделать, это обработка окончания игры. Мы должны определить функции gameOver, которая будет показывать экран окончания игры, и ещё reset, которая будет запускать игру снова.