صنع لعبة تعتمد على قماش HTML5: برنامج تعليمي باستخدام AngularJS و CreateJS
نشرت: 2022-03-11يعد تطوير الألعاب أحد تقنيات البرمجة الأكثر إثارة للاهتمام والمتقدمة التي تتحدى باستمرار صناعة تطوير البرمجيات.
هناك العديد من منصات البرمجة المستخدمة لتطوير الألعاب ، وهناك عدد كبير من الأجهزة التي يمكن تشغيلها عليها ، ولكن عندما يتعلق الأمر بلعب الألعاب في متصفح الويب ، فإن التطوير المستند إلى Flash لا يزال يقود الطريق.
ستتيح لنا إعادة كتابة الألعاب المستندة إلى Flash إلى تقنية HTML5 Canvas تشغيلها على متصفحات الجوال أيضًا. وباستخدام Apache Cordova ، يمكن لمطوري الويب المهرة تحويلها بسهولة إلى تطبيقات ألعاب محمولة متعددة المنصات.
شرع الأشخاص في CreateJS في القيام بذلك والمزيد.
EaselJS ، جزء من مجموعة CreateJS ، يجعل الرسم على HTML5 Canvas بسيطًا. تخيل إنشاء تصور بيانات مخصص بأداء عالٍ وآلاف العناصر. Scalable Vector Graphic (SVG) ليس الخيار الصحيح ، لأنه يستخدم عناصر DOM. تصبح المتصفحات مرهقة عندما تصبح عمليات التصيير الأولية وعمليات إعادة الرسم والرسوم المتحركة ، عند حوالي 600 عنصر من عناصر DOM ، عمليات مكلفة. باستخدام HTML5 Canvas ، يمكننا بسهولة التغلب على هذه المشكلات ؛ الرسومات القماشية تشبه الحبر على الورق ، ولا توجد عناصر DOM والتكاليف المرتبطة بها.
هذا يعني أن التطوير المستند إلى Canvas يحتاج إلى مزيد من الاهتمام عندما يتعلق الأمر بفصل العناصر ، وربط الأحداث والسلوكيات بها. يأتي EaselJS للإنقاذ ؛ يمكننا الترميز كما لو كنا نتعامل مع عناصر فردية ، مما يتيح لمكتبة EaselJS التعامل مع عمليات تمرير الماوس والنقرات والاصطدامات.
يتميز الترميز المستند إلى SVG بميزة واحدة كبيرة: تتمتع SVG بمواصفات قديمة ، وهناك الكثير من أدوات التصميم التي تقوم بتصدير أصول SVG لاستخدامها في التطوير ، بحيث يعمل التعاون بين المصممين والمطورين بشكل جيد. المكتبات الشهيرة ، مثل D3.JS ، والمكتبات الأحدث والأكثر قوة مثل SnapSVG ، تجلب الكثير إلى الطاولة.
إذا كان سير العمل من مصمم إلى مطور هو السبب الوحيد لاستخدام SVGs ، ففكر في امتدادات Adobe Illustrator (AI) التي تنشئ تعليمات برمجية من الأشكال التي تم إنشاؤها في AI. في سياقنا ، تُنشئ مثل هذه الإضافات كود EaselJS أو كود ProcessingJS ، وكلاهما من مكتبات HTML5 Canvas
خلاصة القول ، إذا كنت تبدأ مشروعًا جديدًا ، فلا يوجد سبب لاستخدام SVGs بعد الآن!
SoundJS هو جزء من مجموعة CreateJS ؛ يوفر واجهة برمجة تطبيقات بسيطة لمواصفات صوت HTML5.
يتم استخدام PreloadJS لتحميل الأصول مسبقًا مثل الصور النقطية وملفات الصوت وما شابه. إنه يعمل بشكل جيد مع مكتبات CreateJS الأخرى.
تجعل EaselJS و SoundJS و PreloadJS تطوير اللعبة أمرًا سهلاً للغاية لأي نينجا JavaScript. أساليب واجهة برمجة التطبيقات الخاصة بها مألوفة لأي شخص يستخدم تطوير الألعاب المعتمدة على الفلاش.
"كل هذا رائع. ولكن ، ماذا لو كان لدينا فريق من المطورين قاموا بتحويل مجموعة من الألعاب من Flash إلى HTML5؟ هل من الممكن القيام بذلك مع هذه المجموعة؟ "
الإجابة: "نعم ، ولكن فقط إذا كان جميع مطوريك على مستوى جيد!".
إذا كان لديك فريق من مطوري مجموعة مهارات متنوعة ، وهو ما يحدث غالبًا ، فقد يكون استخدام CreateJS مخيفًا بعض الشيء وتوقع رمزًا معياريًا وقابل للتطوير. ماذا لو جمعنا مجموعة CreateJS مع AngularJS؟ هل يمكننا التخفيف من هذه المخاطر من خلال جلب أفضل إطار عمل للواجهة الأمامية JS وأكثرها اعتمادًا؟
نعم ، وسيعلمك هذا البرنامج التعليمي للعبة HTML5 Canvas كيفية إنشاء لعبة أساسية باستخدام CreateJS و AngularJS!
زرع البذرة
تقلل AngularJS التعقيد بشكل كبير من خلال تمكين فريق التطوير الخاص بك بما يلي:
- إضافة نمطية الكود ، بحيث يمكن لأعضاء الفريق التركيز على جوانب مختلفة من اللعبة.
- تقسيم الكود إلى قطع منفصلة قابلة للاختبار ويمكن صيانتها.
- تمكين إعادة استخدام الكود ، بحيث يمكن إنشاء مثيل لفئة مصنع واحدة عدة مرات ، وإعادة استخدامها لتحميل أصول وسلوكيات مختلفة ولكنها متشابهة.
- تسريع عملية التطوير لأن أعضاء الفريق المتعددين يمكنهم العمل بشكل متوازٍ ، دون الخطو على أصابع بعضهم البعض.
- حماية المطورين من استخدام الأنماط السيئة (تحمل Javascript أجزاء سيئة السمعة معها ويمكن لـ JSLint مساعدتنا كثيرًا فقط).
- إضافة إطار اختبار قوي.
إذا كنت ، مثلي ، "مبتكرًا" أو متعلمًا عن طريق اللمس ، فيجب أن تحصل على الكود من GitHub وتبدأ في التعلم. اقتراحي هو النظر في تسجيلات الوصول الخاصة بي وفهم الخطوات التي اتخذتها للحصول على فوائد إضافة جودة AngularJS إلى كود CreateJS.
تشغيل مشروع AngularJS Seed الخاص بك
إذا لم تكن قد قمت بذلك بالفعل ، فأنت بحاجة إلى تثبيت 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>
نحن جاهزون تقريبًا! انسخ أصول اللعبة إلى مجلد التطبيق الخاص بك. لقد أعددت الصور ، لذا لا تتردد في تنزيلها وحفظها في مجلد التطبيق / الأصول.
- التطبيق / الأصول / spritesheet_grant.png
- app / الأصول / ground.png
- التطبيق / الأصول / hill1.png
- التطبيق / الأصول / hill2.png
- app / الأصول / sky.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.
تم تسجيل المصنع في التطبيق تمامًا مثل أي وحدة 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 المتوفرة أدناه.
- التطبيق / الأصول / jump.mp3
- التطبيق / الأصول / runTrack.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; }
قم بتهيئة النتيجة ومتغيرات 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 لإنشاء تطبيقات الجوال. يجب أن تكون قادرًا على إنشاء تطبيق البذور الأيونية ، ونسخ كل الكود من هذا المشروع ، والبدء في لعب اللعبة على جهازك المحمول في أقل من ساعة.
الشيء الوحيد الذي لا أغطيه هنا هو كشف الاصطدام. لمعرفة المزيد عنها ، قرأت هذا المقال.
يتم إحتوائه
أعتقد أنه من خلال هذا البرنامج التعليمي لتطوير اللعبة ، أدركت أن AngularJS و CreateJS هما ثنائيان رابحان لتطوير الألعاب القائمة على HTML5. لديك كل الأساسيات وأنا متأكد من أنك أدركت فوائد الجمع بين هذين النظامين.
يمكنك تنزيل الكود الخاص بهذه المقالة من GitHub ، ولا تتردد في استخدامه ومشاركته وجعله خاصًا بك.