HTML5 Canvas 기반 게임 만들기: AngularJS 및 CreateJS를 사용한 자습서

게시 됨: 2022-03-11

게임 개발은 소프트웨어 개발 업계에 끊임없이 도전하는 보다 흥미롭고 고급 프로그래밍 기술 중 하나입니다.

게임을 개발하는 데 사용되는 프로그래밍 플랫폼은 많이 있고 이를 플레이할 수 있는 장치도 많지만 웹 브라우저에서 게임을 하는 경우 플래시 기반 개발이 여전히 선두를 달리고 있습니다.

Flash 기반 게임을 HTML5 Canvas 기술로 다시 작성하면 모바일 브라우저에서도 게임을 플레이할 수 있습니다. 그리고 Apache Cordova를 사용하면 숙련된 웹 개발자가 이를 크로스 플랫폼 모바일 게임 앱으로 쉽게 래핑할 수 있습니다.

CreateJS의 사람들은 그 이상을 수행하기 시작했습니다.

CreateJS 제품군의 일부인 EaselJS 는 HTML5 Canvas에서 그리기를 간단하게 만듭니다. 고성능과 수천 개의 요소로 맞춤형 데이터 시각화를 구축하는 것을 상상해 보십시오. SVG(Scalable Vector Graphic)는 DOM 요소를 사용하기 때문에 올바른 선택이 아닙니다. 약 600개의 DOM 요소에서 초기 렌더링, 다시 그리기 및 애니메이션이 비용이 많이 드는 작업이 되면 브라우저가 압도됩니다. HTML5 Canvas를 사용하면 이러한 문제를 쉽게 해결할 수 있습니다. 캔버스 그림은 종이 위의 잉크와 같으며 DOM 요소와 관련 비용이 없습니다.

이는 Canvas 기반 개발이 요소를 분리하고 이벤트와 동작을 요소에 첨부할 때 더 많은 주의가 필요함을 의미합니다. EaselJS가 도움이 됩니다. EaselJS 라이브러리가 마우스 오버, 클릭 및 충돌을 처리하도록 하여 개별 요소를 처리하는 것처럼 코딩할 수 있습니다.

SVG 기반 코딩에는 한 가지 큰 장점이 있습니다. SVG에는 오래된 사양이 있고 개발에 사용할 SVG 자산을 내보내는 디자인 도구가 많이 있으므로 디자이너와 개발자 간의 협력이 잘 작동합니다. D3.JS와 같은 인기 있는 라이브러리와 SnapSVG와 같은 새롭고 더 강력한 라이브러리는 많은 것을 제공합니다.

디자이너에서 개발자로의 작업 흐름이 SVG를 사용하는 유일한 이유인 경우 AI에서 만든 모양에서 코드를 생성하는 Adobe Illustrator(AI)용 확장을 고려하십시오. 우리의 맥락에서 이러한 확장은 HTML5 Canvas 기반 라이브러리인 EaselJS 코드 또는 ProcessingJS 코드를 생성합니다.

결론부터 말하자면, 새로운 프로젝트를 시작한다면 더 이상 SVG를 사용할 이유가 없습니다!

SoundJS 는 CreateJS 제품군의 일부입니다. HTML5 오디오 사양을 위한 간단한 API를 제공합니다.

PreloadJS 는 비트맵, 사운드 파일 등과 같은 자산을 미리 로드하는 데 사용됩니다. 다른 CreateJS 라이브러리와 함께 잘 작동합니다.

EaselJS, SoundJS 및 PreloadJS를 사용하면 모든 JavaScript 닌자가 게임을 매우 쉽게 개발할 수 있습니다. API 방법은 Flash 기반 게임 개발을 사용해 본 사람이라면 누구에게나 친숙합니다.

"이 모든 것이 훌륭합니다. 하지만 여러 게임을 Flash에서 HTML5로 변환하는 개발자 팀이 있다면 어떻게 될까요? 이 스위트로 할 수 있습니까?”

대답: "예, 하지만 모든 개발자가 제다이 수준에 있는 경우에만 가능합니다!".

다양한 기술 세트 개발자로 구성된 팀이 있는 경우(일반적인 경우), CreateJS를 사용하고 확장 가능하고 모듈화된 코드를 기대하는 것은 약간 무서울 수 있습니다. CreateJS 제품군을 AngularJS와 함께 사용하면 어떻게 될까요? 가장 우수하고 가장 많이 채택된 프론트엔드 JS 프레임워크를 가져와서 이러한 위험을 완화할 수 있습니까?

, 그리고 이 HTML5 Canvas 게임 튜토리얼은 CreateJS와 AngularJS로 기본적인 게임을 만드는 방법을 가르쳐줄 것입니다!

CreateJS 및 AngularJS를 사용한 HTML5 Canvas 게임 튜토리얼

씨앗 심기

AngularJS는 다음을 통해 개발 팀을 지원하여 복잡성을 크게 줄입니다.

  1. 코드 모듈성을 추가하여 팀원들이 게임의 다양한 측면에 집중할 수 있습니다.
  2. 코드를 테스트 가능하고 유지 관리 가능한 별도의 조각으로 나눕니다.
  3. 코드 재사용을 활성화하여 하나의 팩토리 클래스를 여러 번 인스턴스화하고 다르지만 유사한 자산 및 동작을 로드하는 데 재사용할 수 있습니다.
  4. 여러 팀 구성원이 서로의 발가락을 밟지 않고 병렬로 작업할 수 있으므로 개발 속도가 빨라집니다.
  5. 개발자가 나쁜 패턴을 사용하지 않도록 보호합니다(Javascript는 악명 높은 나쁜 부분을 포함하고 JSLint는 우리에게 많은 도움이 될 수 있습니다).
  6. 견고한 테스트 프레임워크 추가.

나처럼 "땜장이"이거나 촉각 학습자라면 GitHub에서 코드를 가져와 학습을 시작해야 합니다. 제 제안은 체크인을 살펴보고 CreateJS 코드에 AngularJS 장점을 추가하는 이점을 얻기 위해 취한 단계를 이해하는 것입니다.

AngularJS 시드 프로젝트 실행

아직 설치하지 않았다면 이 데모를 실행하기 전에 nodeJS를 설치해야 합니다.

AngularJS 시드 프로젝트를 생성하거나 GitHub에서 다운로드한 후 npm install 을 실행하여 모든 종속성을 앱 폴더에 다운로드합니다.

애플리케이션을 실행하려면 동일한 폴더에서 npm start 를 실행하고 브라우저에서 http://localhost:8000/app/#/view1 로 이동합니다. 귀하의 페이지는 아래 이미지와 같아야 합니다.

페이지 예

EaselJS와 AngularJS의 만남

AngularJS 시드 프로젝트에 CreateJS 라이브러리 참조를 추가합니다. AngularJS 다음에 CreateJS 스크립트가 포함되어 있는지 확인하십시오.

<script src="http://code.createjs.com/createjs-2014.12.12.min.js"></script>

다음으로 애플리케이션을 정리합니다.

  • 앱 폴더에서 view2 폴더 삭제
  • 아래 표시된 코드를 삭제하여 index.html에서 메뉴 및 AngularJS 버전 정보를 제거합니다.
 <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>

다음 줄을 삭제하여 app.js 에서 view2 모듈을 제거합니다.

myApp.view2,

이전에 AngularJS를 사용한 적이 없고 AngularJS 지시문에 익숙하지 않은 경우 이 자습서를 확인하십시오. AngularJS의 지시문은 HTML에 몇 가지 새로운 트릭을 가르치는 방법입니다. 그것들은 프레임워크에서 가장 잘 고려된 기능이며 AngularJS를 강력하고 확장 가능하게 만듭니다.

특수 DOM 기능이나 구성 요소가 필요할 때마다 온라인에서 검색하십시오. Angular 모듈과 같은 곳에서 이미 사용할 수 있는 좋은 기회가 있습니다.

다음으로 해야 할 일은 EaselJS의 예제를 구현할 새 AngularJS 지시문을 만드는 것입니다. /app/view1/directives/spriteSheetRunner.js 에 있는 새 파일에 spriteSheetRunner라는 새 지시문을 만듭니다.

 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'}); }]);

spriteSheetRunner.js 에 대한 참조를 추가하여 index.html 에 지시문 코드를 포함합니다.

 <script src="view1/directives/spriteSheetRunner.js"></script>

거의 준비가 되었습니다! 게임 자산을 앱 폴더에 복사합니다. 이미지를 준비했으므로 자유롭게 다운로드하여 앱/자산 폴더에 저장하세요.

  • 앱/자산/spritesheet_grant.png
  • 앱/자산/ground.png
  • 앱/자산/hill1.png
  • 앱/자산/hill2.png
  • 앱/자산/sky.png

마지막 단계로 새로 생성된 지시문을 페이지에 추가합니다. 이렇게 하려면 app/view/view1.html 파일을 변경하고 한 줄짜리로 만드십시오.

 <sprite-sheet-runner></sprite-sheet-runner>

신청을 시작하면 주자가 움직일 수 있습니다. :)

움직이는 주자

이것이 첫 번째 AngularJS 또는 첫 번째 CreateJS 애플리케이션이라면 축하합니다. 정말 멋진 것을 만드셨습니다!

서비스에서 자산 사전 로드

AngularJS의 서비스는 주로 코드와 데이터를 공유하는 데 사용되는 싱글톤입니다. 우리는 서비스를 사용하여 애플리케이션 전체에서 '게임 자산'을 공유할 것입니다. AngularJS 서비스에 대해 자세히 알아보려면 AngularJS 설명서를 확인하세요.

AngularJS 개발 서비스는 모든 자산을 한 곳에서 로드하고 관리하기 위한 효과적인 메커니즘을 제공합니다. 자산 변경 사항은 서비스의 각 개별 인스턴스에 전파되어 코드를 훨씬 쉽게 유지 관리할 수 있습니다.

/app/view1/services 폴더에 loaderSvc.js 라는 새 JS 파일을 만듭니다.

 //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를 사용하려면 사용 중인 서비스를 등록해야 합니다. 그렇게 하려면 myApp.services 에 대한 참조를 포함하도록 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.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); } } } }]);

UI 요소 팩토리 생성

게임 개발에서 스프라이트를 재사용하고 반복하는 것은 매우 중요합니다. UI 클래스(이 경우 스프라이트)의 인스턴스화를 활성화하기 위해 AngularJS 팩토리를 사용할 것입니다.

Factory는 다른 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 파일을 만듭니다.

  • app/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); }]);
  • app/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); }]);
  • 앱/뷰1/그라운드.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); }]);
  • app/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); }]);

index.html 에 이러한 모든 새 JS 파일을 추가하는 것을 잊지 마십시오.

이제 게임 디렉티브를 업데이트해야 합니다.

 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 를 지시문 밖으로 옮기면 지시문 크기가 91줄에서 65줄로 20% 감소했습니다.

또한 유지 관리를 단순화하기 위해 각 팩토리 클래스에 대한 테스트를 독립적으로 작성할 수 있습니다.

참고: 테스트는 이 게시물에서 다루지 않는 주제이지만 여기에서 시작하는 것이 좋습니다.

화살표 키 상호 작용

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 파일을 추가해야 합니다. 아래 제공된 URL에서 다운로드할 수 있습니다.

  • 앱/자산/점프.mp3
  • 앱/자산/runningTrack.mp3

이제 로더 서비스를 사용하여 이러한 사운드 파일을 미리 로드해야 합니다. PreloaderJS 라이브러리의 loadQueue 를 사용하겠습니다. 이 파일을 미리 로드하려면 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); } } } }]);
관련: Toptal 개발자의 AngularJS 모범 사례 및 팁

점수 및 수명 지표 추가

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>

표시기를 적절하게 배치하려면 app/app.css 파일에서 왼쪽 상단과 오른쪽 상단에 CSS 클래스를 추가해야 합니다.

 .top-left { position: absolute; left: 30px; top: 10px; } .top-right { position: absolute; right: 100px; top: 10px; float: right; }

app/view1/view1.js 컨트롤러에서 score 및 lifesCount 변수를 초기화합니다.

 '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는 유용한 방법과 서비스로 가득 차 있습니다. 그 중 하나는 요소의 위치를 ​​계산하는 데 사용할 innerWidth 속성을 제공하는 $window입니다.

$window 서비스를 주입하도록 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', '$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 프레임워크를 사용하여 모바일 앱을 만드는 방법에 대한 다른 모바일 앱 개발 튜토리얼을 읽는 것이 좋습니다. ionic seed 앱을 만들고 이 프로젝트의 모든 코드를 복사하고 1시간 이내에 모바일 장치에서 게임을 시작할 수 있어야 합니다.

여기서 다루지 않는 유일한 것은 충돌 감지입니다. 그것에 대해 더 알아보기 위해 이 기사를 읽었습니다.

마무리

이 게임 개발 튜토리얼을 통해 AngularJS와 CreateJS가 HTML5 기반 게임 개발에서 승리한 듀오임을 깨달았다고 생각합니다. 모든 기본 사항을 숙지했으며 이 두 플랫폼을 결합할 때의 이점을 인식했을 것입니다.

GitHub에서 이 기사의 코드를 다운로드하고 자유롭게 사용, 공유 및 만들 수 있습니다.

관련: 개발자가 저지르는 가장 일반적인 AngularJS 실수 18가지