Создание игры на основе холста HTML5: руководство с использованием AngularJS и CreateJS
Опубликовано: 2022-03-11Разработка игр — одна из самых интересных и продвинутых техник программирования, которая постоянно бросает вызов индустрии разработки программного обеспечения.
Существует множество платформ программирования, используемых для разработки игр, и множество устройств, на которых можно в них играть, но когда дело доходит до игр в веб-браузере, разработка на основе Flash по-прежнему лидирует.
Переписывание Flash-игр на технологию HTML5 Canvas позволит нам играть в них и в мобильных браузерах. А с помощью Apache Cordova опытные веб-разработчики могут легко превратить их в кроссплатформенные мобильные игровые приложения.
Ребята из CreateJS решили сделать это и многое другое.
EaselJS , часть пакета CreateJS, упрощает рисование на холсте HTML5. Представьте себе создание пользовательской визуализации данных с высокой производительностью и тысячами элементов. Масштабируемая векторная графика (SVG) — не лучший выбор, поскольку она использует элементы DOM. Браузеры перегружаются, когда при наличии около 600 элементов DOM первоначальный рендеринг, перерисовка и анимация становятся дорогостоящими операциями. С помощью HTML5 Canvas мы можем легко обойти эти проблемы; Рисунки на холсте похожи на чернила на бумаге, без элементов DOM и связанных с ними затрат.
Это означает, что разработка на основе Canvas требует большего внимания, когда дело доходит до разделения элементов и прикрепления к ним событий и поведения. EaselJS приходит на помощь; мы можем кодировать так, как если бы мы имели дело с отдельными элементами, позволяя библиотеке EaselJS обрабатывать ваши перемещения мыши, щелчки и столкновения.
Кодирование на основе SVG имеет одно большое преимущество: SVG имеет старую спецификацию, и существует множество инструментов проектирования, которые экспортируют активы SVG для использования в разработке, так что сотрудничество между дизайнерами и разработчиками работает хорошо. Популярные библиотеки, такие как D3.JS, и более новые, более мощные библиотеки, такие как SnapSVG, приносят много пользы.
Если рабочий процесс от дизайнера к разработчику — единственная причина, по которой вы будете использовать SVG, рассмотрите расширения для Adobe Illustrator (AI), которые генерируют код из форм, созданных в AI. В нашем контексте такие расширения генерируют код EaselJS или код ProcessingJS, оба из которых являются библиотеками на основе HTML5 Canvas.
В итоге, если вы начинаете новый проект, больше нет причин использовать SVG!
SoundJS является частью пакета CreateJS; он предоставляет простой API для спецификации HTML5 Audio.
PreloadJS используется для предварительной загрузки ресурсов, таких как растровые изображения, звуковые файлы и тому подобное. Он хорошо работает в сочетании с другими библиотеками CreateJS.
EaselJS, SoundJS и PreloadJS упрощают разработку игр для любого ниндзя JavaScript. Методы его API знакомы всем, кто занимался разработкой игр на основе Flash.
«Это все здорово. Но что, если у нас есть команда разработчиков, переводящая кучу игр с Flash на HTML5? Возможно ли это сделать с этим набором?»
Ответ: «Да, но только если все ваши разработчики на уровне джедая!».
Если у вас есть команда разработчиков с разным набором навыков, что часто бывает, может быть немного страшно использовать CreateJS и ожидать масштабируемого и модульного кода. Что, если мы объединим пакет CreateJS с AngularJS? Можем ли мы уменьшить этот риск, внедрив лучшую и наиболее принятую интерфейсную JS-инфраструктуру?
Да , и этот учебник по игре HTML5 Canvas научит вас создавать базовую игру с помощью CreateJS и AngularJS!
Посев семян
AngularJS значительно снижает сложность, предоставляя вашей команде разработчиков следующие возможности:
- Добавление модульности кода, чтобы члены команды могли сосредоточиться на разных аспектах игры.
- Разбивка кода на отдельные тестируемые и поддерживаемые части.
- Включение повторного использования кода, чтобы один фабричный класс можно было создавать несколько раз и повторно использовать для загрузки разных, но похожих активов и поведений.
- Ускорение разработки, поскольку несколько членов команды могут работать параллельно, не наступая друг другу на пятки.
- Защита разработчиков от использования плохих шаблонов (Javascript несет в себе заведомо плохие части, и JSLint может нам только помочь).
- Добавление надежной среды тестирования.
Если, как и я, вы «мастер» или тактильный ученик, вам следует получить код с GitHub и начать учиться. Я предлагаю просмотреть мои проверки и понять шаги, которые я предпринял, чтобы получить преимущества от добавления совершенства AngularJS в код CreateJS.
Запуск вашего исходного проекта AngularJS
Если вы еще этого не сделали, вам необходимо установить nodeJS, прежде чем вы сможете запустить эту демонстрацию.
После создания исходного проекта AngularJS или загрузки его с GitHub запустите npm install
, чтобы загрузить все зависимости в папку вашего приложения.
Чтобы запустить приложение, запустите npm start
из той же папки и перейдите по http://localhost:8000/app/#/view1
в браузере. Ваша страница должна выглядеть как на изображении ниже.
EaselJS встречает AngularJS
Добавьте ссылку на библиотеку CreateJS в исходный проект AngularJS. Убедитесь, что скрипт CreateJS включен после AngularJS.
<script src="http://code.createjs.com/createjs-2014.12.12.min.js"></script>
Далее очищаем приложение:
- Удалите папку view2 из папки вашего приложения
- Удалите меню и информацию о версии AngularJS из index.html, удалив код, показанный ниже:
<ul class="menu"> <li><a href="#/view1">view1</a></li> <li><a href="#/view2">view2</a></li> </ul> … <div>Angular seed app: v<span app-version></span></div> … <script src="view2/view2.js"></script>
Удалите модуль view2
из app.js
, удалив следующую строку
myApp.view2,
Если вы раньше не использовали AngularJS и не знакомы с директивами AngularJS, ознакомьтесь с этим руководством. Директивы в AngularJS — это способ научить HTML новым трюкам. Это наиболее продуманная функция фреймворка, которая делает AngularJS мощным и расширяемым.
Всякий раз, когда вам нужна специализированная функциональность DOM или компонент, ищите его в Интернете; есть большая вероятность, что он уже доступен в таких местах, как модули Angular.
Следующее, что нам нужно сделать, это создать новую директиву AngularJS, которая будет реализовывать пример из EaselJS. Создайте новую директиву с именем spriteSheetRunner в новом файле, расположенном в /app/view1/directives/spriteSheetRunner.js
.
angular.module('myApp.directives', []) .directive('spriteSheetRunner', function () { "use strict"; return { restrict : 'EAC', replace : true, scope :{ }, template: "<canvas width='960' height='400'></canvas>", link: function (scope, element, attribute) { var w, h, loader, manifest, sky, grant, ground, hill, hill2; drawGame(); function drawGame() { //drawing the game canvas from scratch here //In future we can pass stages as param and load indexes from arrays of background elements etc if (scope.stage) { scope.stage.autoClear = true; scope.stage.removeAllChildren(); scope.stage.update(); } else { scope.stage = new createjs.Stage(element[0]); } w = scope.stage.canvas.width; h = scope.stage.canvas.height; manifest = [ {src: "spritesheet_grant.png", id: "grant"}, {src: "sky.png", id: "sky"}, {src: "ground.png", id: "ground"}, {src: "hill1.png", id: "hill"}, {src: "hill2.png", id: "hill2"} ]; loader = new createjs.LoadQueue(false); loader.addEventListener("complete", handleComplete); loader.loadManifest(manifest, true, "/app/assets/"); } function handleComplete() { sky = new createjs.Shape(); sky.graphics.beginBitmapFill(loader.getResult("sky")).drawRect(0, 0, w, h); var groundImg = loader.getResult("ground"); ground = new createjs.Shape(); ground.graphics.beginBitmapFill(groundImg).drawRect(0, 0, w groundImg.width, groundImg.height); ground.tileW = groundImg.width; ground.y = h - groundImg.height; hill = new createjs.Bitmap(loader.getResult("hill")); hill.setTransform(Math.random() * w, h - hill.image.height * 4 - groundImg.height, 4, 4); hill.alpha = 0.5; hill2 = new createjs.Bitmap(loader.getResult("hill2")); hill2.setTransform(Math.random() * w, h - hill2.image.height * 3 - groundImg.height, 3, 3); var spriteSheet = new createjs.SpriteSheet({ framerate: 30, "images": [loader.getResult("grant")], "frames": {"regX": 82, "height": 292, "count": 64, "regY": 0, "width": 165}, // define two animations, run (loops, 1.5x speed) and jump (returns to run): "animations": { "run": [0, 25, "run", 1.5], "jump": [26, 63, "run"] } }); grant = new createjs.Sprite(spriteSheet, "run"); grant.y = 35; scope.stage.addChild(sky, hill, hill2, ground, grant); scope.stage.addEventListener("stagemousedown", handleJumpStart); createjs.Ticker.timingMode = createjs.Ticker.RAF; createjs.Ticker.addEventListener("tick", tick); } function handleJumpStart() { grant.gotoAndPlay("jump"); } function tick(event) { var deltaS = event.delta / 1000; var position = grant.x 150 * deltaS; var grantW = grant.getBounds().width * grant.scaleX; grant.x = (position >= w grantW) ? -grantW : position; ground.x = (ground.x - deltaS * 150) % ground.tileW; hill.x = (hill.x - deltaS * 30); if (hill.x hill.image.width * hill.scaleX <= 0) { hill.x = w; } hill2.x = (hill2.x - deltaS * 45); if (hill2.x hill2.image.width * hill2.scaleX <= 0) { hill2.x = w; } scope.stage.update(event); } } } });
После создания директивы добавьте зависимость к приложению, обновив /app/app.js
, как показано ниже:
'use strict'; // Declare app level module which depends on views, and components angular.module('myApp',[ 'ngRoute', 'myApp.view1', 'myApp.version', 'myApp.services', 'myApp.uiClasses', 'myApp.directives']) .config(['$routeProvider', function($routeProvider) { $routeProvider.otherwise({redirectTo: '/view1'}); }]);
Включите код директивы в index.html
, добавив ссылку на spriteSheetRunner.js
.
<script src="view1/directives/spriteSheetRunner.js"></script>
Мы почти готовы! Скопируйте активы игры в папку вашего приложения. Я подготовил изображения, поэтому не стесняйтесь загружать их и сохранять в папке app/assets.
- приложение/активы/spritesheet_grant.png
- приложение/активы/ground.png
- приложение/активы/hill1.png
- приложение/активы/hill2.png
- приложение/активы/небо.png
В качестве последнего шага добавьте нашу вновь созданную директиву на страницу. Для этого измените файл app/view/view1.html
и сделайте его однострочным:
<sprite-sheet-runner></sprite-sheet-runner>
Запустите свое приложение, и вы запустите свой бегун :)
Если это ваше первое приложение AngularJS или первое приложение CreateJS, празднуйте, вы только что сделали что-то действительно классное!
Предварительная загрузка ресурсов в службе
Сервисы в AngularJS — это синглтоны, используемые в основном для обмена кодом и данными. Мы будем использовать сервис для обмена «игровыми активами» в приложении. Чтобы узнать больше о сервисах AngularJS, ознакомьтесь с документацией по AngularJS.
Службы разработки AngularJS предоставляют эффективный механизм загрузки и управления всеми активами в одном месте. Изменения активов распространяются на каждый отдельный экземпляр службы, что значительно упрощает поддержку нашего кода.
Создайте новый JS-файл с именем loaderSvc.js
в папке /app/view1/services
.
//app/view1/services/loaderSvc.js myServices.service('loaderSvc', function () { var manifest = [ {src: "spritesheet_grant.png", id: "grant"}, {src: "sky.png", id: "sky"}, {src: "ground.png", id: "ground"}, {src: "hill1.png", id: "hill"}, {src: "hill2.png", id: "hill2"} ], loader = new createjs.LoadQueue(true); this.getResult = function (asset) { return loader.getResult(asset); }; this.getLoader = function () { return loader; }; this.loadAssets = function () { loader.loadManifest(manifest, true, "/app/assets/"); }; });
AngularJS требует от нас регистрации любого сервиса, который мы используем. Для этого обновите файл app.js
, включив в него ссылку на myApp.services
.
'use strict'; // Declare app level module which depends on views, and components angular.module('myApp',[ 'ngRoute', 'myApp.view1', 'myApp.version', 'myApp.services', 'myApp.directives']) .config(['$routeProvider', function($routeProvider) { $routeProvider.otherwise({redirectTo: '/view1'}); }]); var myServices = angular.module('myApp.services', []);
Обновите код директивы в app/view1/directives/spriteSheetRunner.js
, чтобы удалить код предварительной загрузки и вместо этого использовать службу.
angular.module('myApp.directives', []) .directive('spriteSheetRunner', ['loaderSvc', function (loaderSvc) { "use strict"; return { restrict : 'EAC', replace : true, scope :{ }, template: "<canvas width='960' height='400'></canvas>", link: function (scope, element, attribute) { var w, h, manifest, sky, grant, ground, hill, hill2; drawGame(); function drawGame() { //drawing the game canvas from scratch here //In future we can pass stages as param and load indexes from arrays of background elements etc if (scope.stage) { scope.stage.autoClear = true; scope.stage.removeAllChildren(); scope.stage.update(); } else { scope.stage = new createjs.Stage(element[0]); } w = scope.stage.canvas.width; h = scope.stage.canvas.height; loaderSvc.getLoader().addEventListener("complete", handleComplete); loaderSvc.loadAssets(); } function handleComplete() { sky = new createjs.Shape(); sky.graphics.beginBitmapFill(loaderSvc.getResult("sky")).drawRect(0, 0, w, h); var groundImg = loaderSvc.getResult("ground"); ground = new createjs.Shape(); ground.graphics.beginBitmapFill(groundImg).drawRect(0, 0, w + groundImg.width, groundImg.height); ground.tileW = groundImg.width; ground.y = h - groundImg.height; hill = new createjs.Bitmap(loaderSvc.getResult("hill")); hill.setTransform(Math.random() * w, h - hill.image.height * 4 - groundImg.height, 4, 4); hill.alpha = 0.5; hill2 = new createjs.Bitmap(loaderSvc.getResult("hill2")); hill2.setTransform(Math.random() * w, h - hill2.image.height * 3 - groundImg.height, 3, 3); var spriteSheet = new createjs.SpriteSheet({ framerate: 30, "images": [loaderSvc.getResult("grant")], "frames": {"regX": 82, "height": 292, "count": 64, "regY": 0, "width": 165}, // define two animations, run (loops, 1.5x speed) and jump (returns to run): "animations": { "run": [0, 25, "run", 1.5], "jump": [26, 63, "run"] } }); grant = new createjs.Sprite(spriteSheet, "run"); grant.y = 35; scope.stage.addChild(sky, hill, hill2, ground, grant); scope.stage.addEventListener("stagemousedown", handleJumpStart); createjs.Ticker.timingMode = createjs.Ticker.RAF; createjs.Ticker.addEventListener("tick", tick); } function handleJumpStart() { grant.gotoAndPlay("jump"); } function tick(event) { var deltaS = event.delta / 1000; var position = grant.x + 150 * deltaS; var grantW = grant.getBounds().width * grant.scaleX; grant.x = (position >= w + grantW) ? -grantW : position; ground.x = (ground.x - deltaS * 150) % ground.tileW; hill.x = (hill.x - deltaS * 30); if (hill.x + hill.image.width * hill.scaleX <= 0) { hill.x = w; } hill2.x = (hill2.x - deltaS * 45); if (hill2.x + hill2.image.width * hill2.scaleX <= 0) { hill2.x = w; } scope.stage.update(event); } } } }]);
Создание фабрики элементов пользовательского интерфейса
Повторное использование и повторение спрайтов в разработке игр очень важно. Чтобы включить создание классов пользовательского интерфейса (которые в нашем случае являются спрайтами), мы будем использовать AngularJS Factories.
Фабрика регистрируется в приложении так же, как и любой другой модуль AngularJS. Чтобы создать фабрику uiClasses, измените файл app.js, чтобы он выглядел следующим образом:
'use strict'; // Declare app level module which depends on views, and components angular.module('myApp',[ 'ngRoute', 'myApp.view1', 'myApp.version', 'myApp.services', 'myApp.uiClasses', 'myApp.directives']) .config(['$routeProvider', function($routeProvider) { $routeProvider.otherwise({redirectTo: '/view1'}); }]); var uiClasses = angular.module('myApp.uiClasses', []); var myServices = angular.module('myApp.services', []);
Давайте воспользуемся новой фабрикой для создания неба, холма, земли и нашего бегуна. Для этого создайте файлы JavaScript, как указано ниже.

- приложение/view1/uiClasses/sky.js
uiClasses.factory("Sky", [ 'loaderSvc', function (loaderSvc) { function Sky(obj) { this.sky = new createjs.Shape(); this.sky.graphics.beginBitmapFill(loaderSvc.getResult("sky")).drawRect(0, 0, obj.width, obj.height); } Sky.prototype = { addToStage: function (stage) { stage.addChild(this.sky); }, removeFromStage: function (stage) { stage.removeChild(this.sky); } }; return (Sky); }]);
- приложение/view1/uiClasses/hill.js
uiClasses.factory("Hill", [ 'loaderSvc', function (loaderSvc) { function Hill(obj) { this.hill = new createjs.Bitmap(loaderSvc.getResult(obj.assetName)); this.hill.setTransform(Math.random() * obj.width, obj.height - this.hill.image.height * obj.scaleFactor - obj.groundHeight, obj.scaleFactor, obj.scaleFactor); } Hill.prototype = { addToStage: function (stage) { stage.addChild(this.hill); }, removeFromStage: function (stage) { stage.removeChild(this.hill); }, setAlpha: function (val) { this.hill.alpha = val; }, getImageWidth: function () { return this.hill.image.width; }, getScaleX: function () { return this.hill.scaleX; }, getX: function () { return this.hill.x; }, getY: function () { return this.hill.y; }, setX: function (val) { this.hill.x = val; }, move: function (x, y) { this.hill.x = this.hill.x + x; this.hill.y = this.hill.y + y; } }; return (Hill); }]);
- приложение/view1/ground.js
uiClasses.factory("Ground", [ 'loaderSvc', function (loaderSvc) { function Ground(obj) { var groundImg = loaderSvc.getResult("ground"); this.ground = new createjs.Shape(); this.ground.graphics.beginBitmapFill(groundImg).drawRect(0, 0, obj.width + groundImg.width, groundImg.height); this.ground.tileW = groundImg.width; this.ground.y = obj.height - groundImg.height; this.height = groundImg.height; } Ground.prototype = { addToStage: function (stage) { stage.addChild(this.ground); }, removeFromStage: function (stage) { stage.removeChild(this.ground); }, getHeight: function () { return this.height; }, getX: function () { return this.ground.x; }, setX: function (val) { this.ground.x = val; }, getTileWidth: function () { return this.ground.tileW; }, move: function (x, y) { this.ground.x = this.ground.x + x; this.ground.y = this.ground.y + y; } }; return (Ground); }]);
- приложение/view1/uiClasses/character.js
uiClasses.factory("Character", [ 'loaderSvc', function (loaderSvc) { function Character(obj) { var spriteSheet = new createjs.SpriteSheet({ framerate: 30, "images": [loaderSvc.getResult(obj.characterAssetName)], "frames": {"regX": 82, "height": 292, "count": 64, "regY": 0, "width": 165}, // define two animations, run (loops, 1.5x speed) and jump (returns to run): "animations": { "run": [0, 25, "run", 1.5], "jump": [26, 63, "run"] } }); this.grant = new createjs.Sprite(spriteSheet, "run"); this.grant.y = obj.y; } Character.prototype = { addToStage: function (stage) { stage.addChild(this.grant); }, removeFromStage: function (stage) { stage.removeChild(this.grant); }, getWidth: function () { return this.grant.getBounds().width * this.grant.scaleX; }, getX: function () { return this.grant.x; }, setX: function (val) { this.grant.x = val; }, playAnimation: function (animation) { this.grant.gotoAndPlay(animation); } }; return (Character); }]);
Не забудьте добавить все эти новые файлы JS в свой index.html
.
Теперь нам нужно обновить директиву игры.
myDirectives.directive('spriteSheetRunner', ['loaderSvc','Sky', 'Ground', 'Hill', 'Character', function (loaderSvc, Sky, Ground, Hill, Character) { "use strict"; return { restrict : 'EAC', replace : true, scope :{ }, template: "<canvas width='960' height='400'></canvas>", link: function (scope, element, attribute) { var w, h, sky, grant, ground, hill, hill2; drawGame(); function drawGame() { //drawing the game canvas from scratch here if (scope.stage) { scope.stage.autoClear = true; scope.stage.removeAllChildren(); scope.stage.update(); } else { scope.stage = new createjs.Stage(element[0]); } w = scope.stage.canvas.width; h = scope.stage.canvas.height; loaderSvc.getLoader().addEventListener("complete", handleComplete); loaderSvc.loadAssets(); } function handleComplete() { sky = new Sky({width:w, height:h}); sky.addToStage(scope.stage); ground = new Ground({width:w, height:h}); hill = new Hill({width:w, height:h, scaleFactor: 4, assetName: 'hill', groundHeight: ground.getHeight()}); hill.setAlpha(0.5); hill.addToStage(scope.stage); hill2 = new Hill({width:w, height:h, scaleFactor: 3, assetName: 'hill2', groundHeight: ground.getHeight()}); hill2.addToStage(scope.stage); ground.addToStage(scope.stage); grant = new Character({characterAssetName: 'grant', y: 34}) grant.addToStage(scope.stage); scope.stage.addEventListener("stagemousedown", handleJumpStart); createjs.Ticker.timingMode = createjs.Ticker.RAF; createjs.Ticker.addEventListener("tick", tick); } function handleJumpStart() { grant.playAnimation("jump"); } function tick(event) { var deltaS = event.delta / 1000; var position = grant.getX() + 150 * deltaS; grant.setX((position >= w + grant.getWidth()) ? -grant.getWidth() : position); ground.setX((ground.getX() - deltaS * 150) % ground.getTileWidth()); hill.move(deltaS * -30, 0); if (hill.getX() + hill.getImageWidth() * hill.getScaleX() <= 0) { hill.setX(w); } hill2.move(deltaS * -45, 0); if (hill2.getX() + hill2.getImageWidth() * hill2.getScaleX() <= 0) { hill2.setX(w); } scope.stage.update(event); } } } }]);
Обратите внимание, что uiClasses
из директивы уменьшило размер директивы на 20%, с 91 до 65 строк.
Кроме того, мы можем самостоятельно написать тесты для каждого фабричного класса, чтобы упростить его обслуживание.
Примечание. Тестирование — это тема, которая не рассматривается в этом посте, но здесь можно начать.
Взаимодействие клавиш со стрелками
На этом этапе нашего руководства по игре HTML5 Canvas щелчок мышью или касание мобильного телефона заставит нашего парня подпрыгнуть, и мы не сможем его остановить. Давайте добавим элементы управления клавишами со стрелками:
- Стрелка влево (пауза в игре)
- Стрелка вверх (прыжок)
- Стрелка вправо (начать бежать)
Для этого создайте функцию keyDown
и добавьте прослушиватель событий в качестве последней строки функции handleComplete()
.
function keydown(event) { if (event.keyCode === 38) {//if keyCode is "Up" handleJumpStart(); } if (event.keyCode === 39) {//if keyCode is "Right" if (scope.status === "paused") { createjs.Ticker.addEventListener("tick", tick); scope.status = "running"; } } if (event.keyCode === 37) {//if keyCode is "Left" createjs.Ticker.removeEventListener("tick", tick); scope.status = "paused"; } } window.onkeydown = keydown;
Попробуйте снова запустить игру и проверьте управление клавиатурой.
Пусть музыка играет
Без музыки игры неинтересны, так что давайте включим музыку.
Сначала нам нужно добавить файлы MP3 в нашу папку app/assets. Вы можете скачать их по URL-адресам, указанным ниже.
- приложение/активы/прыжок.mp3
- приложение/активы/runningTrack.mp3
Теперь нам нужно предварительно загрузить эти звуковые файлы с помощью нашего сервиса загрузчика. Мы будем использовать loadQueue
библиотеки PreloaderJS
. Обновите файл app/view1/services/loaderSvc.js
, чтобы предварительно загрузить эти файлы.
myServices.service('loaderSvc', function () { var manifest = [ {src: "spritesheet_grant.png", id: "grant"}, {src: "sky.png", id: "sky"}, {src: "ground.png", id: "ground"}, {src: "hill1.png", id: "hill"}, {src: "hill2.png", id: "hill2"}, {src: "runningTrack.mp3", id: "runningSound"}, {src: "jump.mp3", id: "jumpingSound"} ], loader = new createjs.LoadQueue(true); // need this so it doesn't default to Web Audio createjs.Sound.registerPlugins([createjs.HTMLAudioPlugin]); loader.installPlugin(createjs.Sound); this.getResult = function (asset) { return loader.getResult(asset); }; this.getLoader = function () { return loader; }; this.loadAssets = function () { loader.loadManifest(manifest, true, "/app/assets/"); }; });
Измените директиву игры, чтобы воспроизводить звуки во время игровых событий.
myDirectives.directive('spriteSheetRunner', [ 'loaderSvc', 'Sky', 'Ground', 'Hill', 'Character', function (loaderSvc, Sky, Ground, Hill, Character) { "use strict"; return { restrict : 'EAC', replace : true, scope :{ }, template: "<canvas width='960' height='400'></canvas>", link: function (scope, element, attribute) { var w, h, sky, grant, ground, hill, hill2, runningSoundInstance, status; drawGame(); function drawGame() { //drawing the game canvas from scratch here if (scope.stage) { scope.stage.autoClear = true; scope.stage.removeAllChildren(); scope.stage.update(); } else { scope.stage = new createjs.Stage(element[0]); } w = scope.stage.canvas.width; h = scope.stage.canvas.height; loaderSvc.getLoader().addEventListener("complete", handleComplete); loaderSvc.loadAssets(); } function handleComplete() { sky = new Sky({width:w, height:h}); sky.addToStage(scope.stage); ground = new Ground({width:w, height:h}); hill = new Hill({width:w, height:h, scaleFactor: 4, assetName: 'hill', groundHeight: ground.getHeight()}); hill.setAlpha(0.5); hill.addToStage(scope.stage); hill2 = new Hill({width:w, height:h, scaleFactor: 3, assetName: 'hill2', groundHeight: ground.getHeight()}); hill2.addToStage(scope.stage); ground.addToStage(scope.stage); grant = new Character({characterAssetName: 'grant', y: 34}); grant.addToStage(scope.stage); scope.stage.addEventListener("stagemousedown", handleJumpStart); createjs.Ticker.timingMode = createjs.Ticker.RAF; createjs.Ticker.addEventListener("tick", tick); // start playing the running sound looping indefinitely runningSoundInstance = createjs.Sound.play("runningSound", {loop: -1}); scope.status = "running"; window.onkeydown = keydown; } function keydown(event) { if (event.keyCode === 38) {//if keyCode is "Up" handleJumpStart(); } if (event.keyCode === 39) {//if keyCode is "Right" if (scope.status === "paused") { createjs.Ticker.addEventListener("tick", tick); runningSoundInstance = createjs.Sound.play("runningSound", {loop: -1}); scope.status = "running"; } } if (event.keyCode === 37) {//if keyCode is "Left" createjs.Ticker.removeEventListener("tick", tick); createjs.Sound.stop(); scope.status = "paused"; } } function handleJumpStart() { if (scope.status === "running") { createjs.Sound.play("jumpingSound"); grant.playAnimation("jump"); } } function tick(event) { var deltaS = event.delta / 1000; var position = grant.getX() + 150 * deltaS; grant.setX((position >= w + grant.getWidth()) ? -grant.getWidth() : position); ground.setX((ground.getX() - deltaS * 150) % ground.getTileWidth()); hill.move(deltaS * -30, 0); if (hill.getX() + hill.getImageWidth() * hill.getScaleX() <= 0) { hill.setX(w); } hill2.move(deltaS * -45, 0); if (hill2.getX() + hill2.getImageWidth() * hill2.getScaleX() <= 0) { hill2.setX(w); } scope.stage.update(event); } } } }]);
Добавление индикаторов очков и жизней
Давайте добавим в игру HTML5 Canvas индикатор игрового счета и жизни (сердца). Счет будет отображаться в виде числа в верхнем левом углу, а символы сердца в верхнем правом углу будут указывать количество жизней.
Мы будем использовать внешнюю библиотеку шрифтов для рендеринга сердечек, поэтому добавьте следующую строку в заголовок файла index.html
.
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css" rel="stylesheet">
Стандартная привязка AngularJS будет обеспечивать обновления в реальном времени. Добавьте следующий код в файл app/view1/view1.html
:
<sprite-sheet-runner score="score" lifes-count="lifesCount"></sprite-sheet-runner> <span class="top-left"><h2>Score: {{score}}</h2></span> <span class="top-right"><h2>Life: <i ng-if="lifesCount > 0" class="fa fa-heart"></i> <i ng-if="lifesCount < 1" class="fa fa-heart-o"></i> <i ng-if="lifesCount > 1" class="fa fa-heart"></i> <i ng-if="lifesCount < 2" class="fa fa-heart-o"></i> <i ng-if="lifesCount > 2" class="fa fa-heart"></i> <i ng-if="lifesCount < 3" class="fa fa-heart-o"></i> </h2></span>
Чтобы правильно расположить наши индикаторы, нам нужно добавить классы CSS для верхнего левого и верхнего правого угла в файле app/app.css
.
.top-left { position: absolute; left: 30px; top: 10px; } .top-right { position: absolute; right: 100px; top: 10px; float: right; }
Инициализируйте переменные score и lifesCount
в контроллере app/view1/view1.js
.
'use strict'; angular.module('myApp.view1', ['ngRoute']) .config(['$routeProvider', function($routeProvider) { $routeProvider.when('/view1', { templateUrl: 'view1/view1.html', controller: 'View1Ctrl' }); }]) .controller('View1Ctrl', ['$scope', function($scope) { $scope.score = 0; $scope.lifesCount = 3; }]);
Чтобы убедиться, что индикаторы обновляются правильно, измените директиву основной игры, чтобы использовать переменные области видимости.
... replace : true, scope :{ score: '=score', lifesCount: '=lifesCount' }, template: ...
Чтобы проверить привязку области видимости, добавьте эти три строки в конец метода handleComplete()
.
scope.score = 10; scope.lifesCount = 2; scope.$apply();
Когда вы запустите приложение, вы должны увидеть индикаторы очков и жизней.
Дополнительное пустое пространство в правой части страницы по-прежнему будет присутствовать, потому что мы все еще жестко задаем ширину и высоту игры на этом этапе нашего руководства по программированию игр HTML5.
Адаптация ширины игры
AngularJS содержит множество полезных методов и сервисов. Одним из них является $window, который предоставляет свойство innerWidth, которое мы будем использовать для вычисления позиции наших элементов.
Измените ваше app/view1/view1.js
, чтобы внедрить службу $window
.
'use strict'; angular.module('myApp.view1', ['ngRoute']) .config(['$routeProvider', function($routeProvider) { $routeProvider.when('/view1', { templateUrl: 'view1/view1.html', controller: 'View1Ctrl' }); }]) .controller('View1Ctrl', ['$scope', '$window', function($scope, $window) { $scope.windowWidth = $window.innerWidth; $scope.gameHeight = 400; $scope.score = 0; $scope.lifesCount = 3; }]);
Расширьте основную директиву игры свойствами ширины и высоты, вот и все!
<sprite-sheet-runner width="windowWidth" height="gameHeight" score="score" lifes-count="lifesCount"> </sprite-sheet-runner>
... scope :{ width: '=width', height: '=height', score: '=score', lifesCount: '=lifesCount' }, ... drawGame(); element[0].width = scope.width; element[0].height = scope.height; w = scope.width; h = scope.height; function drawGame() { ...
Теперь у вас есть игра, которая подстраивается под ширину окна браузера.
Если вы хотите перенести это в мобильное приложение, я предлагаю прочитать мой другой учебник по разработке мобильных приложений об использовании Ionic Framework для создания мобильных приложений. Вы должны быть в состоянии создать приложение ionic seed, скопировать весь код из этого проекта и начать играть в игру на своем мобильном устройстве менее чем за час.
Единственное, чего я здесь не касаюсь, — это обнаружение столкновений. Чтобы узнать больше об этом, я прочитал эту статью.
Заворачивать
Я полагаю, что в ходе этого руководства по разработке игр вы поняли, что AngularJS и CreateJS — выигрышный дуэт для разработки игр на основе HTML5. У вас есть все основы, и я уверен, что вы оценили преимущества объединения этих двух платформ.
Вы можете загрузить код для этой статьи с GitHub, не стесняйтесь использовать, делиться и делать его своим.