การสร้างเกมบนผ้าใบ HTML5: บทช่วยสอนโดยใช้ AngularJS และ CreateJS

เผยแพร่แล้ว: 2022-03-11

การพัฒนาเกมเป็นหนึ่งในเทคนิคการเขียนโปรแกรมขั้นสูงที่น่าสนใจและท้าทายมากขึ้นเรื่อยๆ ในอุตสาหกรรมการพัฒนาซอฟต์แวร์

มีแพลตฟอร์มการเขียนโปรแกรมมากมายที่ใช้ในการพัฒนาเกม และมีอุปกรณ์มากมายให้เล่น แต่เมื่อพูดถึงการเล่นเกมในเว็บเบราว์เซอร์ การพัฒนาบน Flash ยังคงเป็นผู้นำอยู่

การเขียนเกมที่ใช้ Flash ใหม่เป็นเทคโนโลยี HTML5 Canvas จะทำให้เราสามารถเล่นเกมเหล่านี้บนเบราว์เซอร์มือถือได้เช่นกัน และด้วย Apache Cordova นักพัฒนาเว็บที่มีทักษะสามารถรวมเข้ากับแอปเกมมือถือข้ามแพลตฟอร์มได้อย่างง่ายดาย

ผู้คนที่ CreateJS มุ่งมั่นที่จะทำเช่นนั้นและอีกมากมาย

EaselJS ซึ่งเป็นส่วนหนึ่งของชุดเครื่องมือของ CreateJS ทำให้การวาดบน HTML5 Canvas เป็นเรื่องง่าย ลองนึกภาพการสร้างภาพข้อมูลแบบกำหนดเองด้วยประสิทธิภาพสูงและองค์ประกอบนับพัน Scalable Vector Graphic (SVG) ไม่ใช่ตัวเลือกที่เหมาะสม เนื่องจากใช้องค์ประกอบ DOM เบราว์เซอร์จะล้นหลามเมื่อองค์ประกอบ DOM ประมาณ 600 รายการ การเรนเดอร์เริ่มต้น การวาดใหม่ และแอนิเมชั่นกลายเป็นการดำเนินการที่มีราคาแพง ด้วย HTML5 Canvas เราสามารถแก้ไขปัญหาเหล่านี้ได้อย่างง่ายดาย ภาพวาดบนผ้าใบเป็นเหมือนหมึกบนกระดาษ ไม่มีองค์ประกอบ DOM และค่าใช้จ่ายที่เกี่ยวข้อง

ซึ่งหมายความว่าการพัฒนาบนผ้าใบต้องการความเอาใจใส่มากขึ้นเมื่อต้องแยกองค์ประกอบ และแนบเหตุการณ์และพฤติกรรมเข้ากับองค์ประกอบเหล่านั้น EaselJS มาช่วย; เราสามารถเขียนโค้ดราวกับว่าเรากำลังจัดการกับแต่ละองค์ประกอบ โดยให้ไลบรารี EaselJS จัดการการเลื่อนเมาส์ การคลิก และการชนกันของคุณ

การเขียนโค้ดแบบ SVG มีข้อดีอย่างหนึ่ง: SVG มีข้อกำหนดแบบเก่า และมีเครื่องมือออกแบบมากมายที่ส่งออกเนื้อหา SVG เพื่อใช้งานในการพัฒนา เพื่อให้ความร่วมมือระหว่างนักออกแบบและนักพัฒนาทำงานได้ดี ไลบรารียอดนิยม เช่น D3.JS และไลบรารีที่ใหม่กว่าและทรงพลังกว่า เช่น SnapSVG นำเสนอสิ่งต่างๆ มากมายให้กับตาราง

หากเวิร์กโฟลว์ระหว่างนักออกแบบกับนักพัฒนาเป็นเหตุผลเดียวที่คุณจะใช้ SVG ให้พิจารณาส่วนขยายสำหรับ Adobe Illustrator (AI) ที่สร้างโค้ดจากรูปร่างที่สร้างใน AI ในบริบทของเรา ส่วนขยายดังกล่าวจะสร้างโค้ด EaselJS หรือโค้ด ProcessingJS ซึ่งทั้งสองอย่างนี้เป็นไลบรารีบนผ้าใบ HTML5

สิ่งสำคัญที่สุด หากคุณกำลังเริ่มโครงการใหม่ ไม่มีเหตุผลที่จะใช้ SVG อีกต่อไป!

SoundJS เป็นส่วนหนึ่งของชุด CreateJS; มันมี API อย่างง่ายสำหรับข้อกำหนด HTML5 Audio

PreloadJS ใช้เพื่อโหลดเนื้อหาล่วงหน้า เช่น บิตแมป ไฟล์เสียง และอื่นๆ ทำงานร่วมกับไลบรารี CreateJS อื่นๆ ได้เป็นอย่างดี

EaselJS, SoundJS และ PreloadJS ทำให้การพัฒนาเกมเป็นเรื่องง่ายสำหรับนินจา JavaScript วิธี API นั้นคุ้นเคยกับทุกคนที่ใช้การพัฒนาเกมแบบ Flash

“ทั้งหมดนี้ยอดเยี่ยมมาก แต่ถ้าเรามีทีมนักพัฒนาที่แปลงเกมจำนวนมากจาก Flash เป็น HTML5 เป็นไปได้ไหมที่จะทำอย่างนั้นกับชุดนี้”

คำตอบ: “ใช่ แต่ถ้านักพัฒนาทั้งหมดของคุณอยู่ในระดับเจได!”

หากคุณมีทีมนักพัฒนาชุดทักษะที่แตกต่างกัน ซึ่งมักจะเป็นกรณีนี้ อาจเป็นเรื่องยากเล็กน้อยที่จะใช้ CreateJS และคาดหวังโค้ดที่ปรับขนาดได้และเป็นโมดูล จะเกิดอะไรขึ้นถ้าเรารวมชุด CreateJS กับ AngularJS เข้าด้วยกัน เราสามารถลดความเสี่ยงนี้โดยนำเฟรมเวิร์ก JS ฟรอนต์เอนด์ที่ดีที่สุดและนำมาใช้มากที่สุดได้หรือไม่

ใช่ และบทช่วยสอนเกม HTML5 Canvas นี้จะสอนวิธีสร้างเกมพื้นฐานด้วย CreateJS และ AngularJS!

บทช่วยสอนเกม HTML5 Canvas ด้วย CreateJS และ AngularJS

การเพาะเมล็ด

AngularJS ลดความซับซ้อนลงอย่างมากโดยทำให้ทีมพัฒนาของคุณทำสิ่งต่อไปนี้ได้:

  1. การเพิ่มรหัสโมดูลเพื่อให้สมาชิกในทีมสามารถมุ่งเน้นไปที่แง่มุมต่าง ๆ ของเกม
  2. แบ่งรหัสออกเป็นชิ้นส่วนที่ทดสอบได้และบำรุงรักษาแยกกัน
  3. เปิดใช้งานการใช้รหัสซ้ำ เพื่อให้คลาสโรงงานหนึ่งสามารถสร้างอินสแตนซ์ได้หลายครั้ง และนำกลับมาใช้ใหม่เพื่อโหลดเนื้อหาและพฤติกรรมที่แตกต่างกันแต่คล้ายคลึงกัน
  4. เร่งการพัฒนาเพราะสมาชิกในทีมหลายคนสามารถทำงานคู่กันได้โดยไม่ต้องเหยียบเท้าของกันและกัน
  5. การปกป้องนักพัฒนาจากการใช้รูปแบบที่ไม่ดี (Javascript มีส่วนที่ไม่ดีอย่างฉาวโฉ่และ JSLint สามารถช่วยเราได้มากเท่านั้น)
  6. เพิ่มกรอบการทดสอบที่มั่นคง

หากคุณเป็น "คนจรจัด" หรือผู้เรียนรู้ที่สัมผัสได้ เช่นเดียวกับฉัน คุณควรรับรหัสจาก 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 ให้ตรวจดูบทช่วยสอนนี้ Directives ใน AngularJS เป็นวิธีสอน HTML เทคนิคใหม่ๆ เป็นคุณลักษณะที่คิดออกมาดีที่สุดในกรอบงาน และทำให้ AngularJS มีประสิทธิภาพและขยายได้

เมื่อใดก็ตามที่คุณต้องการฟังก์ชัน DOM เฉพาะหรือส่วนประกอบ ให้ค้นหาทางออนไลน์ มีโอกาสที่ดีที่จะมีอยู่แล้วในสถานที่เช่นโมดูลเชิงมุม

สิ่งต่อไปที่เราต้องทำคือสร้างคำสั่ง 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
  • app/assets/ground.png
  • app/assets/hill1.png
  • app/assets/hill2.png
  • app/assets/sky.png

ในขั้นตอนสุดท้าย ให้เพิ่มคำสั่งที่เราสร้างขึ้นใหม่ลงในเพจ ให้เปลี่ยนไฟล์ app/view/view1.html ให้เป็นไฟล์เดียว:

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

เริ่มการสมัครของคุณแล้วคุณจะได้นักวิ่งของคุณ :)

นักวิ่งในการเคลื่อนไหว

หากนี่คือ AngularJS แรกของคุณหรือแอปพลิเคชัน CreateJS แรกของคุณ เฉลิมฉลอง คุณเพิ่งทำสิ่งที่เจ๋งจริงๆ!

การโหลดสินทรัพย์ล่วงหน้าในบริการ

บริการใน AngularJS เป็น singletons ที่ใช้เพื่อแบ่งปันรหัสและข้อมูลเป็นหลัก เราจะใช้บริการเพื่อแชร์ 'เนื้อหาเกม' ทั่วทั้งแอปพลิเคชัน หากต้องการเรียนรู้เพิ่มเติมเกี่ยวกับบริการของ 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); } } } }]);

การสร้างโรงงานองค์ประกอบ 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); }]);
  • app/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); }]);
  • 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); }]);

อย่าลืมเพิ่มไฟล์ 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 ลงในโฟลเดอร์แอพ/สินทรัพย์ของเรา คุณสามารถดาวน์โหลดได้จาก URL ที่ให้ไว้ด้านล่าง

  • app/assets/jump.mp3
  • app/assets/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); } } } }]);
ที่เกี่ยวข้อง: แนวทางปฏิบัติที่ดีที่สุดและเคล็ดลับของ AngularJS โดย Toptal Developers

การเพิ่มคะแนนและตัวบ่งชี้ชีวิต

มาเพิ่มคะแนนเกมและตัวบ่งชี้ชีวิต (หัวใจ) ให้กับเกม 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 service

 '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 ใช้งาน แบ่งปัน และสร้างเป็นของคุณเองได้ตามสบาย

ที่เกี่ยวข้อง: 18 ข้อผิดพลาดที่พบบ่อยที่สุดของ AngularJS ที่นักพัฒนาสร้างขึ้น