البحث في الموقع
المحتوى عن 'ألعاب المتصفح'.
-
سيكون من الجيد أن نحدّ من قدرة اللاعب على التحرك للأعلى إلّا من خلال القفز عبر الفجوات أو على الصناديق. ليست هذه هي الخيارات الوحيدة المتاحة. لا يزال يتعين علينا معرفة المزيد عن المصاعد والأدراج والسلالم. لذلك، دعنا نُنشئ سُلّمًا! إليك ما سنعمل عليه في هذه المرحلة: إنشاء أول سلّم السماح للاعبين بتسلق السلالم السماح للاعبين بالوقوف والقفز على السلالم إنشاء أول سلّم لنبدأ إنشاء سلّمنا بنسخ صنف الصندوق Box: class Ladder { constructor(sprite, rectangle) { this.sprite = sprite; this.rectangle = rectangle; } animate(state) { if (this.sprite) { this.sprite.x = this.rectangle.x; this.sprite.y = this.rectangle.y; } } } export default Ladder; import Ladder from "./ladder"; game.addObject( new Ladder( new PIXI.Sprite.fromImage( "ladder.png" ), new PIXI.Rectangle( 200, window.innerHeight - 192, 64, 192 ) ) ); game.addObject( new Box( new PIXI.Sprite.fromImage( "box.png" ), new PIXI.Rectangle( 136, window.innerHeight - 191, 64, 64 ) ) ); ستحتاج إلى إنشاء صورة ladder.png للسلم؛ بالنسبة لي، اخترت استخدام شكلٍ بسيطٍ بمقاسات 64x192 بيكسيل. تأكد من إضافة الصورة إلى اللعبة قبل إضافة اللاعب وإلا سيكون الشكل الخاص بالسلم أمام الشكل الخاص اللاعب. ستلاحظ أن اللاعب يصطدم بالسلم، كما لو كان صندوقًا. سنحتاج إلى إضافة بعض الجوالب إلى الصناديق والسلالم حتى يستطيع كشف التصادمات من تحديد ما إذا كانوا ستستمر بالاصطدام مع بعضها بعضًا … if (me.x < you.x + you.width && me.x + me.width > you.x && me.y < you.y + you.height && me.y + me.height > you.y) { if (object.collides && this.velocityY > 0 && you.y >= me.y) { this.velocityY = 0; this.grounded = true; this.jumping = false; return; } if (object.collides && this.velocityY < 0 && you.y <= me.y) { this.velocityY = this.accelerationY; return; } if (object.collides && this.velocityX < 0 && you.x <= me.x) { this.velocityX = 0; return; } if (object.collides && this.velocityX > 0 && you.x >= me.x) { this.velocityX = 0; return; } } نعطي للصندوق و السلم خاصية الاصطدام بحيث يتمكّن اللاعب من التحرّك في تجاهلٍ للاصطدامات بالكائنات التي يجب ألا يصطدم بها اللاعبون. إذا كنا سنسمح بوجود عدة لاعبين في نفس اللعبة أو المستوى، فسنضيف أيضًا خاصية الاصطدام إلى اللاعب. هذا ما لم نكن نرغب في تصادم لاعبين متعددين مع بعضهم بعضًا. السماح للاعبين بتسلق السلالم لكي يصعد اللاعب السلالم، يجب أن نكون قادرين على معرفة ما إذا كان يحاول تسلق أحدها في لحظة ما. يتعين علينا أيضًا تعليق الجاذبية والحركة الجانبية حتى لا يسقط أو ينزلق: class Player { constructor(sprite, rectangle) { this.sprite = sprite; this.rectangle = rectangle; this.velocityX = 0; this.maximumVelocityX = 12; this.accelerationX = 2; this.frictionX = 0.9; this.velocityY = 0; this.maximumVelocityY = 20; this.accelerationY = 5; this.jumpVelocity = -40; this.climbingSpeed = 10; } animate(state) { if (state.keys[37]) { this.velocityX = Math.max( this.velocityX - this.accelerationX, this.maximumVelocityX * -1 ); } if (state.keys[39]) { this.velocityX = Math.min( this.velocityX + this.accelerationX, this.maximumVelocityX ); } this.velocityX *= this.frictionX; this.velocityY = Math.min( this.velocityY + this.accelerationY, this.maximumVelocityY ); var onLadder = false; state.objects.forEach((object) => { if (object === this) { return; } var me = this.rectangle; var you = object.rectangle; if (me.x < you.x + you.width && me.x + me.width > you.x && me.y < you.y + you.height && me.y + me.height > you.y) { if (object.collides && this.velocityY > 0 && you.y >= me.y) { this.velocityY = 0; this.grounded = true; this.jumping = false; return; } if (object.collides && this.velocityY < 0 && you.y <= me.y) { this.velocityY = this.accelerationY; return; } if (object.collides && this.velocityX < 0 && you.x <= me.x) { this.velocityX = 0; return; } if (object.collides && this.velocityX > 0 && you.x >= me.x) { this.velocityX = 0; return; } if (object.constructor.name === "Ladder") { onLadder = true; if (state.keys[38] || state.keys[40]) { this.grounded = false; this.jumping = false; this.climbing = true; this.velocityY = 0; this.velocityX = 0; } if (state.keys[38]) { this.rectangle.y -= this.climbingSpeed; } if (state.keys[40] && me.y + me.height < you.y + you.height) { this.rectangle.y += this.climbingSpeed; } } } }); if (!onLadder) { this.climbing = false; } if (state.keys[38] && this.grounded) { this.velocityY = this.jumpVelocity; this.jumping = true; this.grounded = false; } this.rectangle.x += this.velocityX; if (!this.climbing) { this.rectangle.y += this.velocityY; } this.sprite.x = this.rectangle.x; this.sprite.y = this.rectangle.y; } } export default Player; نبدأ بإضافة خاصية جديدة سنسميها التسلق (climbing). ستكون قيمتها صحيحة "true" عندما يكون اللاعب في وضع تسلق السلم. ننشِئ أيضًا متغيرًا onLadder محلي، حتى نتمكن من معرفة ما إذا كان لا يزال واقفًا على سلم. أثناء كشف التصادمات المعتاد، نلاحظ ما إذا كان الكائن الذي يصطدم به اللاعب هو سلم أم لا. إذا كان الأمر كذلك، وكان السهم مضغوطًا لأعلى، نبدأ في التسلق. تبدأ عملية التسلق فقط إذا كان السهم مضغوطًا لأعلى، ولهذا السبب نحتاج إلى هذا المتغير المحلي. نعيد ضبط سرعة اللاعب والخصائص المتعلقة بالقفز. ونغير كذلك بشكل مباشر مستطيلَ اللاعب. إذا تم الضغط على السهم للأعلى، فإنّنا نرفع اللاعب لأعلى. إذا تم الضغط لأسفل، فإنّنا نحرّك اللاعب لأسفل حتى يصبح الجزء السفلي من اللاعب أدنى من الجزء السفلي للسلم. هذا يجعل السلالم السفلية لا تتيح للاعب أن يتجاهل التصادم مع الأرض. السماح للاعبين بالوقوف على السلالم لا تزال هناك مشكلة في السلالم. في اللحظة التي نصل فيها إلى القمة، نسقط مرة أخرى. لقد عطلنا تصادمات السلالم، لذلك نحتاج إلى طريقة للوقوف في الجزء العلوي منها حتى نتمكن من القفز أو الانتقال إلى المنصة التالية. دعنا ننقل منطق التصادم إلى الصناديق والسلالم: class Box { collides(state) { var collides = false; state.objects.forEach((object) => { if (object.constructor.name !== "Player") { return; } let edges = this.getEdges(this, object); if (!( edges.boxTop > edges.playerBottom || edges.boxRight < edges.playerLeft || edges.boxBottom < edges.playerTop || edges.boxLeft > edges.playerRight )) { collides = true; return; } }); return collides; } getEdges(box, player) { return { "boxLeft" : box.rectangle.x, "boxRight" : box.rectangle.x + box.rectangle.width, "boxTop" : box.rectangle.y, "boxBottom" : box.rectangle.y + box.rectangle.height, "playerLeft" : player.rectangle.x, "playerRight" : player.rectangle.x + player.rectangle.width, "playerTop" : player.rectangle.y, "playerBottom" : player.rectangle.y + player.rectangle.height }; } collidesInDirection(box, player) { let edges = this.getEdges(box, player); let offsetLeft = edges.playerRight - edges.boxLeft; let offsetRight = edges.boxRight - edges.playerLeft; let offsetTop = edges.playerBottom - edges.boxTop; let offsetBottom = edges.boxBottom - edges.playerTop; if (Math.min(offsetLeft, offsetRight, offsetTop, offsetBottom) === offsetTop) { return "↓"; } if (Math.min(offsetLeft, offsetRight, offsetTop, offsetBottom) === offsetBottom) { return "↑"; } if (Math.min(offsetLeft, offsetRight, offsetTop, offsetBottom) === offsetLeft) { return "→"; } if (Math.min(offsetLeft, offsetRight, offsetTop, offsetBottom) === offsetRight) { return "←"; } return "unknown"; } constructor(sprite, rectangle) { this.sprite = sprite; this.rectangle = rectangle; } animate(state) { if (this.sprite) { this.sprite.x = this.rectangle.x; this.sprite.y = this.rectangle.y; } } } export default Box; يستطيع صنف الصندوق Box الآن حساب ما إذا كان سيتصادم مع اللاعب. بمجرد اكتشاف التصادم بين لاعب وصندوق، يمكننا تنفيذ الدالّة التابع collidesInDirection. سنحصل على سهمٍ أنيقٍ UTF-8 يشير إلى الاتجاه الذي كان اللاعب يتحرك فيه قبل حدوث التصادم. أثناء إجراء هذا التغيير، حدث أنّ صنف السلم لا يقوم بأي شيء مختلف عن صنف الصندوق. يحدث مفهوم التسلق في فئة "اللاعب"، وبالتالي فإن فئة "السلم" يمكن أن تكون امتدادًا لفئة "الصندوق": import Box from "./box"; class Ladder extends Box { } export default Ladder; يبدو صنف اللّاعب أكثر أناقةً وتنظيمًا بعد نقل منطق التصادم خارجه: animate(state) { if (state.keys[37]) { this.velocityX = Math.max( this.velocityX - this.accelerationX, this.maximumVelocityX * -1 ); } if (state.keys[39]) { this.velocityX = Math.min( this.velocityX + this.accelerationX, this.maximumVelocityX ); } this.velocityX *= this.frictionX; this.velocityY = Math.min( this.velocityY + this.accelerationY, this.maximumVelocityY ); var onLadder = false; state.objects.forEach((object) => { if (object === this) { return; } if (object.collides(state)) { let type = object.constructor.name; let direction = object.collidesInDirection(object, this); if (type === "Ladder") { onLadder = true; let player = this.rectangle; let ladder = object.rectangle; if (state.keys[38] || state.keys[40]) { this.grounded = false; this.jumping = false; this.climbing = true; this.velocityY = 0; this.velocityX = 0; } if (state.keys[38]) { let limit = ladder.y - this.rectangle.height + 1; this.rectangle.y = Math.max( this.rectangle.y -= this.climbingSpeed, limit ); if (player.y === limit) { this.grounded = true; this.jumping = false; } } if (state.keys[40] && player.y + player.height < ladder.y + ladder.height) { this.rectangle.y += this.climbingSpeed; } return; } if (type === "Box") { if (direction === "↓") { this.velocityY = 0; this.grounded = true; this.jumping = false; } if (direction === "↑") { this.velocityY = this.accelerationY; } if (direction === "←" && this.velocityX < 0) { this.velocityX = 0; } if (direction === "→" && this.velocityX > 0) { this.velocityX = 0; } } } }); if (!onLadder) { this.climbing = false; } if (state.keys[38] && this.grounded && !this.climbing) { this.velocityY = this.jumpVelocity; this.jumping = true; this.grounded = false; } this.rectangle.x += this.velocityX; if (!this.climbing) { this.rectangle.y += this.velocityY; } this.sprite.x = this.rectangle.x; this.sprite.y = this.rectangle.y; } لم يتغير مفهوم السلم على الأغلب، باستثناء أن اللاعب لن يصل أبدًا إلى أعلى بيكسل في أعلى درجات السلم. هكذا نمنع اللاعب من السقوط بمجرد وصوله إلى القمة. هذا يعني أنك ستحتاج إلى جعل بداية سلالمك في البيكسل الأوّل فوق المنصة التي تتوقع أن يتحرّك عليها اللاعب. وهذا يعني أيضًا أنه لا يمكن للاعب القفز من السلالم. يبدو الآن منطق التصادم المتعلّق بالصندوق أنقى وأبسط بكثير! وماهي النتيجة؟ سلالمٌ تعمل بشكل جيد. ختام اللعبة: ماذا بعد؟ لقد علمتني هذه المقالات القليلة الكثير من الأمور، أو لِنَقُل أنّهما أمران بارزان على وجه التحديد. الأول هو أن هناك قدرًا لا حصر له من الأشياء التي يمكننا إضافتها أو تغييرها في ألعابنا. والثاني هو أن هذا النشاط مربحٌ ومتعبٌ في الوقت ذاته. لقد انطلقت من رغبةٍ في معرفة كيفية صنع لعبة منصة بسيطة، وبعدها قمنا معا بتغطية كل شيء تقريبا نحتاجه لبناء تلك اللعبة. لا أريد التوقف هنا، رغم ذلك. أريد أن أستمر في التعلم وصقل مهاراتي. أريد أن أستمر في مشاركة تلك التجارب معك. ترجمة -وبتصرف- للمقال Ladders لصاحبه Christopher Pitt اقرأ أيضًا المقال السابق: الجاذبية مدخل إلى ألعاب المتصفح
-
سنعمل هذه المرة على بنية الشيفرة لدينا، ونضيف الجاذبية إلى لعبتنا. ويجدر الذكر أننا قمنا بالفعل بمعظم العمل اللازم لتحقيق الجاذبية. إليك ما سنعمل عليه في هذه المرحلة: تنظيف الشيفرة الموجودة لدينا إضافة الجاذبية إلى لعبتنا السماح للاعبين بالقفز تنظيف الشيفرة الموجودة لدينا نحن بحاجة لتنظيف بعض الأشياء! دعنا في البداية نبدّل موضعي x و y إضافةً إلى العرض والارتفاع (بالنسبة للصندوق وللاعب) مع PIXI.Rectangle. إنّ لديهما هذه الخصائص، ولكنهما يتفاعلان أيضًا مع باقي عناصر PIXI بطرق مثيرة للاهتمام. class Box { constructor(sprite, rectangle) { this.sprite = sprite; this.rectangle = rectangle; } animate(state) { if (this.sprite) { this.sprite.x = this.rectangle.x; this.sprite.y = this.rectangle.y; } } } export default Box; class Player { constructor(sprite, rectangle) { this.sprite = sprite; this.rectangle = rectangle; this.velocityX = 0; this.maximumVelocityX = 12; this.accelerationX = 2; this.frictionX = 0.9; } animate(state) { if (state.keys[37]) { this.velocityX = Math.max( this.velocityX - this.accelerationX, this.maximumVelocityX * -1 ); } if (state.keys[39]) { this.velocityX = Math.min( this.velocityX + this.accelerationX, this.maximumVelocityX ); } this.velocityX *= this.frictionX; var move = true; state.objects.forEach((object) => { if (object === this) { return; } var me = this.rectangle; var you = object.rectangle; if (me.x < you.x + you.width && me.x + me.width > you.x && me.y < you.y + you.height && me.height + me.y > you.y) { if (this.velocityX < 0 && you.x <= me.x) { move = false; } if (this.velocityX > 0 && you.x >= me.x) { move = false; } } }); if (move) { this.rectangle.x += this.velocityX; } this.sprite.x = this.rectangle.x; this.sprite.y = this.rectangle.y; } } export default Player; هل لاحظت هذا المقدار من الشيفرة الذي نستطيع حذفه؟ لقد أصبحت عمليات الموازنة طويلة قليلاً، لكن لا شيء يمكن إصلاحه من خلال متغير محليّ واحد أو اثنين. أدركتُ أيضًا أنه يمكننا تغيير قيم x و y الابتدائية من أجل التحريك. بعد ذلك، أرغب في تغليف (encapsulation) الحدث والعارض والمسرح في صنف (class) جديدة خاصّة باللعبة (Game): class Game { constructor() { this.state = { "keys": {}, "clicks": {}, "mouse": {}, "objects": [] }; } get stage() { if (!this._stage) { this._stage = this.newStage(); } return this._stage; } set stage(stage) { this._stage = stage; } newStage() { return new PIXI.Container(); } get renderer() { if (!this._renderer) { this._renderer = this.newRenderer(); } return this._renderer; } set renderer(renderer) { this._renderer = renderer; } newRenderer() { return new PIXI.autoDetectRenderer( window.innerWidth, window.innerHeight, this.newRendererOptions() ); } newRendererOptions() { return { "antialias": true, "autoResize": true, "transparent": true, "resolution": 2 }; } animate() { var caller = () => { requestAnimationFrame(caller); this.state.renderer = this.renderer; this.state.stage = this.stage; this.state.objects.forEach((object) => { object.animate(this.state); }) this.renderer.render(this.stage); }; caller(); return this; } addEventListenerToElement(element) { element.addEventListener("keydown", (event) => { this.state.keys[event.keyCode] = true; }); element.addEventListener("keyup", (event) => { this.state.keys[event.keyCode] = false; }); element.addEventListener("mousedown", (event) => { this.state.clicks[event.which] = { "clientX": event.clientX, "clientY": event.clientY }; }); element.addEventListener("mouseup", (event) => { this.state.clicks[event.which] = false; }); element.addEventListener("mousemove", (event) => { this.state.mouse.clientX = event.clientX; this.state.mouse.clientY = event.clientY; }); return this; } addRendererToElement(element) { element.appendChild(this.renderer.view); return this; } addObject(object) { this.state.objects.push(object); if (object.sprite) { this.stage.addChild(object.sprite); } return this; } } export default Game; لاحِظ دالّتي الجالب والضابط الخاصتين بصنفٍ ES6. إنهما مفيدتان لملء التبعيات الاختيارية حسب الحاجة. يمكننا تجاهل المصيّر والمسرح إذا كنا بحاجة إلى ذلك، ولكن لديهما قيم افتراضية معقولة أيضًا. هذا، على الأغلب، مجرد نقل للشيفرة التي كانت في main.js إلى game.js. الاختلاف الوحيد الملحوظ هو أننا لم نعد نحتاج إلى إضافة الأشكال إلى المسرح بشكل منفصل عن إضافة الكائنات إلى الحالة (state). أنا محتارٌ حول هذا الموضوع. فمن ناحية، يكون الأمر أكثر وضوحًا إذا أضفناها إلى المسرح يدويًا. ومن ناحية أخرى، هل سنضيف أشكالًا (مرتبطة بالكائنات) دون إضافة الكائنات؟ لا أعتقد ذلك. ربما سنعود ونغير ذلك. في الوقت الحالي، تبدو الشيفرة أنظف قليلاً. إذن، كيف تبدو main.js الآن؟ import Game from "./game"; import Player from "./player"; import Box from "./box"; var game = new Game(); game.addObject( new Box( new PIXI.Sprite.fromImage( "box.png" ), new PIXI.Rectangle( window.innerWidth * 1/4, 152, 64, 64 ) ) ); game.addObject( new Player( new PIXI.Sprite.fromImage( "player.png" ), new PIXI.Rectangle( window.innerWidth * 2/4, 200, 64, 64 ) ) ); game.addObject( new Box( new PIXI.Sprite.fromImage( "box.png" ), new PIXI.Rectangle( window.innerWidth * 3/4, 248, 64, 64 ) ) ); game.addEventListenerToElement(window); game.addRendererToElement(document.body); game.animate(); هذا يبدو أفضل بكثير! لدينا سيطرة كافية لتعيين نقاط الانطلاق لكل كائنٍ في اللعبة. ولكن بعد الإطار الأول، تبدأ الدالّة التابع ()animate في العمل. ستبدأ أشياء مثل الجاذبية والتصادمات في التحكم في كيفية تقدم اللعبة. لقد كان الأمر هكذا دائمًا، لذلك يبدو هذا الملف كذلك أيضًا. كان بإمكاننا أن نحصُر الأحداث والعارض في مجموعة صغيرة من العناصر. كما كنا نستطيع إضافة أي عدد من الكائنات إلى اللعبة من هنا. كل شيء آخر مرتبط باللعبة يوجد داخل فئة اللعبة. وكل شيء آخر مرتبط باللاعب أو الصندوق يوجد داخل الفئة الخاصّة به. هكذا تبدو الأمور أنيقة ومنظمة! إضافة الجاذبية إلى لعبتنا أحد الأشياء التي تجعل ألعاب المنصة ممتعةً هو وجود قدرٍ معتدلٍ من الأجسام في محيط اللعب. دعنا نضيف الجدران والأرضية: var game = new Game(); game.addObject( new Box( null, new PIXI.Rectangle( -10, 0, 10, window.innerHeight ) ) ); game.addObject( new Box( null, new PIXI.Rectangle( 0, window.innerHeight, window.innerWidth, 10 ) ) ); game.addObject( new Box( null, new PIXI.Rectangle( window.innerWidth, 0, 10, window.innerHeight ) ) ); game.addObject( new Player( new PIXI.Sprite.fromImage( "player.png" ), new PIXI.Rectangle( window.innerWidth * 2/4, 200, 64, 64 ) ) ); بدلاً من مربعين أحمرين، لدينا ثلاثة مستطيلات رفيعة. إنها تتصادم بنفس الطريقة. الآن، دعنا نرى كيف نضيف هذه الجاذبية: class Player { constructor(sprite, rectangle) { this.sprite = sprite; this.rectangle = rectangle; this.velocityX = 0; this.maximumVelocityX = 12; this.accelerationX = 2; this.frictionX = 0.9; this.velocityY = 0; this.maximumVelocityY = 30; this.accelerationY = 10; this.jumpVelocity = -50; } animate(state) { if (state.keys[37]) { this.velocityX = Math.max( this.velocityX - this.accelerationX, this.maximumVelocityX * -1 ); } if (state.keys[39]) { this.velocityX = Math.min( this.velocityX + this.accelerationX, this.maximumVelocityX ); } this.velocityX *= this.frictionX; this.velocityY = Math.min( this.velocityY + this.accelerationY, this.maximumVelocityY ); state.objects.forEach((object) => { if (object === this) { return; } var me = this.rectangle; var you = object.rectangle; if (me.x < you.x + you.width && me.x + me.width > you.x && me.y < you.y + you.height && me.y + me.height > you.y) { if (this.velocityY > 0 && you.y >= me.y) { this.velocityY = 0; return; } if (this.velocityY < 0 && you.y <= me.y) { this.velocityY = this.accelerationY; return; } if (this.velocityX < 0 && you.x <= me.x) { this.velocityX = 0; return; } if (this.velocityX > 0 && you.x >= me.x) { this.velocityX = 0; return; } } }); this.rectangle.x += this.velocityX; this.rectangle.y += this.velocityY; this.sprite.x = this.rectangle.x; this.sprite.y = this.rectangle.y; } } export default Player; نبدأ بإنشاء مجموعة من الخصائص لتتناسب مع تلك التي أنشأناها لتتبع الحركة الأفقية. لا نحتاج إلى احتكاك عمودي، إذ أنّ هذا المستوى من التفاصيل يُحذَف غالبا من ألعاب المنصّات. علينا أيضًا أن نتتبع التصادمات العمودية والأفقية. عندما يكون التصادم بين اللاعب والمنصة أو الأرضية، فإننا نوقف حركة الهبوط. وعندما يكون مع السقف، فإننا نستبدل الحركة الصعودية بقوة الجاذبية. السماح للاعبين بالقفز القفز هو ببساطة مقاومة للجاذبية لفترة قصيرة: animate(state) { if (state.keys[37]) { this.velocityX = Math.max( this.velocityX - this.accelerationX, this.maximumVelocityX * -1 ); } if (state.keys[39]) { this.velocityX = Math.min( this.velocityX + this.accelerationX, this.maximumVelocityX ); } this.velocityX *= this.frictionX; this.velocityY = Math.min( this.velocityY + this.accelerationY, this.maximumVelocityY ); state.objects.forEach((object) => { if (object === this) { return; } var me = this.rectangle; var you = object.rectangle; if (me.x < you.x + you.width && me.x + me.width > you.x && me.y < you.y + you.height && me.y + me.height > you.y) { if (this.velocityY > 0 && you.y >= me.y) { this.velocityY = 0; this.grounded = true; this.jumping = false; return; } if (this.velocityY < 0 && you.y <= me.y) { this.velocityY = this.accelerationY; return; } if (this.velocityX < 0 && you.x <= me.x) { this.velocityX = 0; return; } if (this.velocityX > 0 && you.x >= me.x) { this.velocityX = 0; return; } } }); if (state.keys[32] && !this.jumping && this.grounded) { this.velocityY = this.jumpVelocity; this.jumping = true; this.grounded = false; } this.rectangle.x += this.velocityX; this.rectangle.y += this.velocityY; this.sprite.x = this.rectangle.x; this.sprite.y = this.rectangle.y; } لقد قمنا هنا بتعيين مفتاح المسافة من أجل حركة القفز. نضيف فحص لوحة المفاتيح بعد فحص التصادم لأننا نريد لاعبنا أن يقفز فقط إذا كان يقف على منصة أو أرضية. يمكنك الآن إنشاء مستوياتٍ انطلاقًا من الصناديق. تستطيع كذلك إعطاءها بنيةً مرئيةً، والقفز من حولها. تمتع بقضاء بعض الوقت في إنشاء المستوى والقفز من خلاله! ترجمة -وبتصرف- للمقال Gravity لصاحبه Christopher Pitt اقرأ أيضًا المقال التالي: إنشاء السلالم وختام اللعبة المقال السابق: كشف التصادمات
-
لقد حان الوقت لكي نتحدث عن كشف التصادمات (Detection Collision). إنه يؤثر على الأجزاء الظاهرة في لعبتنا، مثل الجدران التي لا يمكننا المرور عبرها ومثل الأرضيات التي لا يمكننا السقوط من خلالها. كما أنه يؤثر على الأجزاء المخفية في لعبتنا مثل قذائف الأسلحة ونقاط التفتيش. لن نصل إلى مفهوم الجاذبية بعد، ولن ننظر إلى صحة اللاعب أو استرداد حياته (الدم والأرواح). تعدّ الأرضيات والقذائف ونقاط التفتيش مثيرة للاهتمام، ولكنها تستحق أقسامًا خاصة بها. سنعمل على إنشاء كائنات غير نافذة (لا يمكن العبور من خلالها). سنتعلم طرقًا لمعرفة ما إذا كان جسمان يشغلان نفس الحيّز المكانيِّ. لقد قضيت بعض الوقت أبحث في هذا الموضوع. يبدو أن هناك العديد من الطرق لحل مشكلة جسمين يشغلان نفس المكان. بعض هذه الطرائق يسهل شرحها وتنفيذها وهي التي سنلقي نظرة عليها، والطرائق الأخرى ليست سهلةً ولا تزال حديثة العهد رغم ذلك. إليك ما سنعمل عليه في هذه المرحلة: *إنشاء أول صنف (class) غير متعلقة باللاعب. الكشف عن التصادمات المتعلقة بالدائرة. الكشف عن التصادمات المتعلقة بالمستطيل. إنشاء صناديق اللاعب هو كائنٌ من العديد من الكائنات التي سوف تتواجد على الشاشة في آنٍ واحدٍ. ولعبتنا هذه هي لعبة منصة، لذلك يمكننا أن نتوقع منصة واحدة على الأقل على الشاشة. لدى المنصات بعض الخصائص المثيرة للاهتمام. فهي تسمح في بعض الأحيان للاعبين بالسقوط من خلالها كما هو الحال عندما تكون واقفًا على منصة وترجع للخلف وتقفز في الوقت ذاته. تأخذ بعض الألعاب هذه السلسلة على أنك تريد السقوط من خلال المنصّة. تسمح بعض الألعاب للاعبين بالقفز في أسفل المنصات. وهذا يتيح الحركة العمودية دون وجود ثغرات في المنصات العلوية. وفي بعض الأحيان، تكون المنصات متحرّكة! تعتبر المنصّات فريدةً جدًا إلى حدّ أننا سنقضي بضعة أقسام في تنفيذ سلوكياتها المختلفة فقط. سوف نركز الآن على كائن مشترك آخر هو الصندوق الشامل (generic box). فكّر في هذا الصّندوق كمولّد للمنصّة. قد يشترك في بعض وظائفه مع المنصّة، ولكن السبب الرئيسي لوجوده هو الأشياء التي تتصادم معها وبالخصوص الكائنات مثل اللاعب. قد لا تبدو الصناديق التي سنُنشئها مثل الصناديق الحقيقية. عندما نصل إلى تطبيق الجاذبية، سنحتاج إلى صندوق عريضٍ ورفيعٍ لمنع اللاعب من السقوط خارج محيط اللعبة. وسنحتاج إلى صناديق طويلة ورفيعة لمنع اللاعب من الركض على جوانب أرضية اللعبة. سنصنع منها الجدران. وقد نصنع منها أيضًا صناديق فعلية كبيرة وخشبية تصلح للقفز عليها للوصول إلى أشياء أعلى. حسنًا، لقد تحدّثت بما يكفي. class Box { constructor(sprite, x, y, width, height) { this.sprite = sprite; this.x = x; this.y = y; this.width = width; this.height = height; this.sprite.x = this.x; this.sprite.y = this.y; } animate(state) { this.sprite.x = this.x; this.sprite.y = this.y; } } export default Box; لإنشاء هذا الصّنف، نسخت ولصقت فئة "اللّاعب" وحذفت مجموعة من الأشياء. لقد كان عليّ أن أضيف إليها كذلك خاصيتي العرض والارتفاع. سوف نصل إلى ذلك بعد قليلٍ. بعد ذلك، نحتاج إلى إضافة صندوقين إلى مسرح اللّعبة: var player_sprite = new PIXI.Sprite.fromImage( "player.png" ); var player = new Player( player_sprite, window.innerWidth * 2/4, 200, 64, 64 ); var barrel_sprite1 = new PIXI.Sprite.fromImage( "barrel.png" ); var barrel1 = new Box( barrel_sprite1, window.innerWidth * 1/4, 152, 64, 64 ); var barrel_sprite2 = new PIXI.Sprite.fromImage( "barrel.png" ); var barrel2 = new Box( barrel_sprite2, window.innerWidth * 3/4, 248, 64, 64 ); stage.addChild(player_sprite); stage.addChild(barrel_sprite1); stage.addChild(barrel_sprite2); var state = { "renderer": renderer, "stage": stage, "keys": {}, "clicks": {}, "mouse": {}, "objects": [ player, barrel1, barrel2 ] }; هذا غريب، أليس كذلك! لقد أنشأتُ نسختين جديدتين من الصّندوق، وأطلقت عليهما اسم براميل. ذلك لأننا على وشك أن نلقي نظرة على التصادمات المتعلقة بالدائرة. كشف التصادمات المتعلقة بالدائرة أريدك أن تنشئ دوائر لهذه الصناديق القليلة الأولى لأننا سندرس أولاً كشف التصادمات المتعلقة بالدوائر رغم أنّ الصندوق يتميّز بالعرض والارتفاع بدلاً من الشّعاع الذي يُميّز الدائرة. ولكن، لن نستخدم هذا النوع من الكشف عن التصادمات في كثير من الأحيان ما لم تكن في منصة اللعبة الكثير من الدوائر. لنر كيف يعمل هذا النوع من الكشف: class Player { constructor(sprite, x, y, width, height) { this.sprite = sprite; this.x = x; this.y = y; this.width = width; this.height = height; this.velocityX = 0; this.maximumVelocityX = 12; this.accelerationX = 2; this.frictionX = 0.9; this.sprite.x = this.x; this.sprite.y = this.y; } animate(state) { if (state.keys[37]) { // left this.velocityX = Math.max( this.velocityX - this.accelerationX, this.maximumVelocityX * -1 ); } if (state.keys[39]) { // right this.velocityX = Math.min( this.velocityX + this.accelerationX, this.maximumVelocityX ); } this.velocityX *= this.frictionX; var move = true; state.objects.forEach((object) => { if (object === this) { return; } var deltaX = this.x - object.x; var deltaY = this.y - object.y; var distance = Math.sqrt( deltaX * deltaX + deltaY * deltaY ); if (distance < this.width / 2 + object.width / 2) { if (this.velocityX < 0 && object.x <= this.x) { move = false; } if (this.velocityX > 0 && object.x >= this.x) { move = false; } } }); if (move) { this.x += this.velocityX; } this.sprite.x = this.x; this.sprite.y = this.y; } } export default Player; أول شيء يتعين علينا القيام به هو تحديد العرض والارتفاع. رغم أننا ندعي أن اللاعبين والصناديق عبارة عن دوائر، فإننا نحتاج فقط إلى نصف العرض كشعاع للدائرة. بعد ذلك، نتحقق من كل كائن في الطبقة (state). يمكننا تجاهل اللاعب لأننا لسنا بحاجة لمعرفة متى يصطدم شيء ما بنفسه. ولكن سيكون علينا، مع ذلك، أن نتحقق من كلّ الكائنات الأخرى. تصطدم دائرتان عندما تكون المسافة بين مركزيهما أقلّ من مجموع شعاعيهما معًا. تكون نقطتاهما الأصليتان متقاربتان جدًا بحيث تتداخل خطوطهما. نفحص سريعًا لمعرفة ما إذا كان الاتجاه الذي يتحرك فيه اللاعب هو حيث يتواجد الصندوق. إذا كان الأمر كذلك، فإننا نمنع اللاعب من التحرك في هذا الاتجاه. جرّبها، فمن الممتع جدًا أن ترى كيف تعيق الأشكال غير المربّعة بعضها بعضًا. بالطبع يجب أن تكون جميعها دوائر مثالية حتى تعمل هذه الخوارزمية البسيطة. كشف التصادمات المتعلّقة بالمستطيل يكون كشف التصادمات بالنسبة للمستطيلات سهلاً مثل الدوائر. امضِ قُدُمًا واستبدل صورة البرميل بصورة صندوق. يمكنك حتى ضبط شيفرة bootstrap لتعكس شكلا مربّعًا للصناديق. هذه المرة، سنتعامل مع اللاعب باعتباره مستطيلًا. بدلاً من استعمال الشّعاع، سنحتاج إلى التحقق مما إذا كانت هناك فراغات بين المستطيل الذي يمثّل اللاعب وأيّ صندوق. نحن نسمي هذا الكشف عن التصادم في المربع المحيط المحاذي للمحور (axis-aligned bounding box) أو AABB للاختصار. إذا لم يكن هناك فراغ، وكان اللاعب يريد التحرك في اتجاه الصندوق، فإننا نمنع حدوث ذلك: var move = true; state.objects.forEach((object) => { if (object === this) { return; } if (this.x < object.x + object.width && this.x + this.width > object.x && this.y < object.y + object.height && this.y + this.height > object.y) { if (this.velocityX < 0 && object.x <= this.x) { move = false; } if (this.velocityX > 0 && object.x >= this.x) { move = false; } } }); if (move) { this.x += this.velocityX; } هذه بعض الطرق البسيطة للكشف عن التصادمات، ولكن هناك طرق أخرى. هناك طريقة تعتمد على الإسقاط في الرياضيات المتجهية لتحديد التداخل. وهناك طريقة أخرى تفحص كل خطّ في زوجٍ من المضلّعات لمعرفة ما إذا كان هناك تقاطع للخطوط. إنّها طريقة غريبة. ويمكنك كذلك تجربة مجموعات من الدوائر تتصادم معًا. قد يكون الأمر ممتعًا. سوف أشغّل قليلاً هذه الدائرة الخضراء الصغيرة لتتحرّك نحو هذه الصناديق الحمراء الصغيرة. نلتقي فيما بعد… ترجمة -وبتصرف- للمقال Collision Detection لصاحبه Christopher Pitt اقرأ أيضًا المقال التالي: الجاذبية المقال السابق: جلب المدخلات من اللاعب
-
ما هو الفرق بين الفيلم واللعبة؟ إنّه يكمن في إمكانية جلب المدخلات من اللاعب! في الواقع، من أهمّ الأشياء في تصميم الألعاب هو أنّه في كثير من الأحيان تُعرّف اللعبة من خلال كيفية استقبالها لمدخلات اللاعب. تعتمد ألعاب السباق على إدخال ثابت ودقيق. إذا رفعت أصابعك عن لوحة المفاتيح، أو رفعت قدمك عن الدواسة، فستفقد قوة الدّفع. وإذا لم تدخل تجميعة المفاتيح الصحيحة، فسوف تتعطل عملية القيادة. تحتاج ألعاب المنصّات platform games (مثل اللعبة التي نُنشئها) إلى إدخالٍ من كلتا اليدين. غالبًا ما يكون الجانب الأيمن من لوحة المفاتيح مخصّصًا للحركة. أما الجانب الأيسر من لوحة المفاتيح فيكون غالبًا مخصّصًا لإجراءات اللاعب، مثل إطلاق النار أو فتح الأبواب. ربما ترغب في تصميم لعبة منصة مثل Terraria، والتي تستخدم الفأرة لإجراءات اللاعب والتصويب على الهدف. ربما تريد استخدام WASD لتحريك اللاعب على الشاشة. هيا بنا إلى العمل! إليك ما سنعمل عليه في هذه المرحلة: نقل صنف اللاعب إلى وحدة نمطية الكشف عن الأحداث الخاصّة بلوحة المفاتيح والفأرة التسارع والتباطؤ إنشاء الوحدات لقد أنشأنا سابقًا صنف اللاعب وجعلناها مسؤولة عن حركة اللاعب. ولكننا تركناها في ملف جافاسكربت الرئيسي، وهذا سيجعل الأمر فوضويًا عندما يزداد عدد الفئات لدينا. لذلك سنُنشئ لها وحدة خاصّة: class Player { constructor(sprite, x, y) { this.sprite = sprite; this.x = x; this.y = y; this.sprite.x = this.x; this.sprite.y = this.y; } animate(state) { this.x += 5; if (this.x > window.innerWidth) { this.x = 0; } this.sprite.x = this.x; this.sprite.y = this.y; } } export default Player; يمكن أن تلاحظ أنّ فئة اللاعب مماثلة لتلك التي أنشأناها من قبل. الجزء المهم هنا هو export default Player. هذا يجعل فئة اللاعب قابلة للاستيراد بالطريقة التالية: import Player from "./player"; var player = new Player( sprite, window.innerWidth / 2, window.innerHeight / 2 ); هذا أسلوب شائع لتقسيم الشيفرة، وغالبًا ما ينقل الفئات الفردية إلى ملفات خاصة بها. إنه يحافظ على بساطة الأشياء، ويُكسِبنا القدرة على إعادة استخدام الشيفرة بدون نسخ ولصق. لا تنس أن تتحقق من أن الشّكل ما زال يتحرك عبر الشاشة. ومثلما ذكرنا من قبل، يمكنك بدء اختبارٍ للخادم باستعمال php -S 127.0.0.1:8000. الكشف عن المدخلات عندما حاولت كشف المدخلات لأول مرة، حاولت إضافة مُستمعات الأحداث إلى جميع الكائنات. حاولت إضافة مستمعات للاعب وأخرى للمسرح وهذا أخرج الأمور عن السيطرة. تكمن الحيلة في كشف الأحداث في المستوى العلوي، وحفظ تفاصيلها في كائن خاصّ بحالة اللعبة (game state object). لقد أنشأنا من قبل هذا النوع من الكائنات الخاصّ بحالة اللعبة، وقمنا بتمريره إلى الدالة الوظيفية animate() الخاصّة بتحريك اللاعب. دعنا نتوسع في هذه الحالة: الملف main.js: var state = { "renderer": renderer, "stage": stage, "keys": {}, "clicks": {}, "mouse": {} }; window.addEventListener("keydown", function(event) { state.keys[event.keyCode] = true; }); window.addEventListener("keyup", function(event) { state.keys[event.keyCode] = false; }); window.addEventListener("mousedown", function(event) { state.clicks[event.which] = { "clientX": event.clientX, "clientY": event.clientY }; }); window.addEventListener("mouseup", function(event) { state.clicks[event.which] = false; }); window.addEventListener("mousemove", function(event) { state.mouse.clientX = event.clientX; state.mouse.clientY = event.clientY; }); animate(); function animate() { requestAnimationFrame(animate); player.animate(state); renderer.render(stage); } بدأنا بإضافة المصيّر والحالة إلى كائن حالة اللعبة. لقد أضفنا الآن المفاتيح والنقرات وخصائص الفأرة والتي تتعقب الأزرار والحركات. بينما نضيف المزيد من الكائنات إلى اللعبة، سنمرّر كائن حالة اللعبة إلى كل منها. ويمكنها استخدامه لتعرف ما يجب عليها فعله عندما يتفاعل اللاعب مع اللعبة. دعنا نوقف حركة اللاعب الحالية ونضيف حركةً تعتمد على المُدخلات: animate(state) { if (state.keys[37]) { // left يسار this.x = Math.max( 0, this.x – 5 ); } if (state.keys[39]) { // right يمين this.x = Math.min( window.innerWidth - 64, this.x + 5 ); } if (state.clicks[1]) { // left click نقرة يسار this.x = state.clicks[1].clientX; } this.sprite.x = this.x; this.sprite.y = this.y; } هناك تغيير فعلي بسيط في الدالّة ()animate. نترقب المفتاحين السهميين، ونتحرك إذا ضغط اللاعب على أحدهما. نتحقق أيضًا مما إذا نقر اللاعب على الفأرة، ونحرّك اللاعب على الفور إذا كان الأمر كذلك. حركة اللاعب الطبيعية يستطيع اللاعب التحرك الآن قليلاً. لكنه لا يحسّ أن الأمور جيدة رغم ذلك. في اللحظة التي نترك فيها مفتاح السهم، فإنه يتوقف وحسب. كما أنّ سرعته تبقى على نسق واحد هو النسق البطيئٍ. ما نحتاج إليه هو بعض التسارع للسماح للاعب بتسريع حركته في اتجاهٍ ما. يمكننا أيضًا استخدام بعض الاحتكاك لإبطاء اللاعب عندما لا نرغب في التسارع. دعنا نضيف هذه الأمور: class Player { constructor(sprite, x, y) { this.sprite = sprite; this.x = x; this.y = y; this.velocityX = 0; this.maximumVelocityX = 12; this.accelerationX = 2; this.frictionX = 0.9; this.sprite.x = this.x; this.sprite.y = this.y; } animate(state) { if (state.keys[37]) { // left this.velocityX = Math.max( this.velocityX - this.accelerationX, this.maximumVelocityX * -1 ); } if (state.keys[39]) { // right this.velocityX = Math.min( this.velocityX + this.accelerationX, this.maximumVelocityX ); } this.velocityX *= this.frictionX; this.x += this.velocityX; this.sprite.x = this.x; this.sprite.y = this.y; } } هيّا بنا! دعنا نراجع هذا في بضع خطوات: لقد أنشأنا مجموعة من الخصائص: السرعة والسرعة القصوى والتسارع والاحتكاك. لقد وضعناها على قيمٍ افتراضية معقولة. لا تتردد في تجربة قيمها حتى تتأكد فعلًا أنها كذلك. يمكننا أن نبدأ في تتبع تسارع لاعبنا في أي من الاتجاهين. إذا ضغطنا على المفتاح الأيسر، فسنبدأ بتسريع اللاعب في هذا الاتجاه. هناك تسارع أعظمي يمكن أن يصل إليه اللاعب، لذلك لا يتجاوزه. في غياب نوع من القوة المضادة، سيستمر اللاعب في التحرك في نفس الاتجاه. هذا يشبه ما يحدث في الفضاء حيث لا توجد مقاومة الهواء أو الجاذبية لمواجهة القوّة الدافعة. نعرّف هذه القوة المضادة على أنها احتكاك، ونضاعف بها السرعة. إذا لم يكن هناك تسارع، فهذا يعني أن القوّة الدافعة تؤول نحو الصفر. إنه يعطي انطباعًا أن اللاعب قد توقف. ترجمة -وبتصرف- للمقال Player Input لصاحبه Christopher Pitt اقرأ أيضًا المقال التالي: كشف التصادمات المقال السابق: حلقة اللعبة التكرارية
-
الحلقات التكرارية هي جزء أساسي من كلّ لعبة. سنهيّئ في هذا المقال المسرح للعبتنا من خلال إنشاء مخطّط قويٍّ لسير العمل وبيئة متينة كذلك. سنطّلع على بعض المكتبات المفيدة، كما سننشئ أوّل شخصية لنا في اللعبة. إنه لأمرٌ ممتعٌ! إليك ما سنعمل عليه في هذه المرحلة: تهييئ مسرح اللعبة إنشاء وعرض الأشكال فهم وبناء حلقات اللعبة تهييئ مسرح اللعبة إن هناك الكثير من الأشياء التي ينبغي التفكير فيها عند إنشاء لعبة قائمة على المتصفح. سنتعلم التقنيات التي تعمل مع العديد من المنصات واللغات المختلفة. سنستخدم أيضًا مُحرّك عرضٍ ثنائي الأبعاد يسمى Pixi.js. إنّه ليس مُحرّك ألعاب أو مُحرّك أجسام مادّية. ربّما لا يزال يتعين علينا أن نعرف أكثر كيف تعمل الأجسام المادّية ومكوّنات اللّعبة، ولكن على الأقل لن نضيع الوقت في تناقضات المتصفح. يمكننا أن نأخذ هذا الخيار عندما لا يكون لدينا ما هو أفضل للقيام به. في الوقت الحالي، سنخلق بيئة للمحرّك Pixi كي يعمل فيها: { "scripts": { "build": "browserify main.js -t babelify --outfile main.dist.js", "watch": "watchify main.js -t babelify --outfile main.dist.js" }, "dependencies": { "pixi.js": "^3.0" }, "devDependencies": { "babel": "^5.4", "babelify": "^6.1", "browserify": "^10.2", "watchify": "^3.2" } } أريد أن أكون قادرًا على استخدام ميزات ES6. لذلك، سنحوّل شيفرة ES6 إلى شيفرة ES5 احترازًا حتى يشمل الدعم المتصفحات القديمة. الأدوات التي سنستخدمها في ذلك هي Babel و Browserify و Watchify. سوف يترجم Babel شيفرة ES6 إلى شيفرة ES5. أمّا Browserify فسيربط جميع وحدات ES6 الخاصة بنا أي ملفّاتنا جافاسكربت فيما بينها، وذلك باستخدام صيغة الاستيراد (import) في حين سيعمل Watchify على تفعيل كلّ من Babel وBrowserify عندما تتغير ملفاتنا. سنستعمل أيضًا الإصدار 3.0 من Pixi كما سنستعمل سكريبتين NPM سيكونان مجرّد اختصار للأوامر التي قد نُدخلها في الطّرفيّة. وهكذا، بدلاً من كتابة browserify ... main.dist.js نكتب فقط npm run build. <!doctype html> <html> <head> <style> html, body { margin: 0; padding: 0; cursor: default; background: #000; overflow: hidden; } </style> </head> <body> <script src="node_modules/pixi.js/bin/pixi.js"></script> <script src="main.dist.js"></script> </body> </html> قد تتساءل هل سنحتاج إلى تضمين كلّ ملفّ جافاسكربت أنشأناه. كلّا، لأن Browserify يربط جميع ملفات جافاسكربت التي تُستورَد إلى main.js فيما بينها ثم يُحوّلها بواسطة Babel، مما يؤدي إلى ملفٍّ واحدٍ. سنحتاج فقط لاستيراد هذا الملف. ورّبما تتساءل أيضًا عن العناصر التي سنعرضها في لعبتنا. ستندمج الشيفرة التي نكتبها لعرض لعبتنا تلقائيًا في نص الملفّ. يمكنك عرض هذه الملفات بالانتقال إلى مجلّدها في الطّرفية، وتنفيذ الأمر التالي: php -S 127.0.0.1:8000. يشغّل هذا الأمر خادمَ اختبارٍ PHP في عنوان URL يمكنك وضعه في متصفحك. ستحتاج إلى ذلك قبل أن تتمكن من تحميل صور الأشكال، لذا جرِّب ذلك الآن. إنشاء الأشكال الأشكال اسمٌ شائعٌ للكائنات المرئية في اللعبة. نستطيع تحريكها على شاشة اللعبة رغم أنها غالباً ما تكون مسؤولة عن تحريك نفسها، كما نستطيع التفاعل معها كذلك. ماريو هو أحد هذه الأشكال، والمنصّات التي يمشي عليها أشكالٌ أيضًا، كما أنّ الغيوم في الخلفية أشكالٌ كذلك. فكِّر في الأشكال بعدِّها شرائحَ من ملفّ التصميم الذي نرسمه بناءً على هياكل البيانات المجردة. وتمتلك هياكل البيانات المجردة هذه موقعًا في الشّاشة، وأحيانًا تكون لديها سرعةٌ، كما أنّها هي التي نُطبِّق عليها قواعد اللعبة. إنها تمثل مصادر قوّتنا وأعداءَنا. إذن كيف نُنشِئها؟ لنبدأ بإنشاء صورة PNG صغيرة. نستخدم صيغة PNG لأنها تسمح لنا بجعل أجزاء من نسيج الشّكل شفافةً. يمكنك استخدام صور JPEG لأشكالك إذا كنت ترغب بذلك. بعد ذلك، نحتاج إلى إنشاء أداةِ عرضٍ ومسرحٍ وشكلٍ من الأشكال: var renderer = new PIXI.autoDetectRenderer( window.innerWidth, window.innerHeight, { "antialias": true, "autoResize": true, "transparent": true, "resolution": 2 } ); document.body.appendChild(renderer.view); var sprite = new PIXI.Sprite.fromImage("player.png"); sprite.x = window.innerWidth / 2; sprite.y = window.innerHeight / 2; var stage = new PIXI.Container(); stage.addChild(sprite); animate(); function animate() { requestAnimationFrame(animate); renderer.render(stage); } حسنًا، تحدث أمور كثيرة هنا، دعنا نلقي نظرة على هذه الشيفرة على مراحل: 1- نُنشِئ المصيّر، وهو أداة تُحوّل العناصر المجرّدة في Pixi (أي الأشكال والأنسجة وغيرها) إلى رسومات قماشية أو رسومات WebGL وتعرضها. ليس علينا أن نتفاعل معها، ولكن يجب أن يكون لدينا دائمًا عارضٌ لعرض عناصر Pixi الخاصة بنا. نطلب من العارض أن يغطّي نافذةَ المتصفح كاملة بالطول والعرض. إننا نطلب ذلك من أجل تنقية وضوح الرسومات باستعمال مضادات التعرج (Anti Aliasing) ورفعها إلى دقة شبكية العين. نطلب ذلك أيضا للحصول على خلفية شفافة وتغيير حجم جميع الرسومات لتلائم الشاشة. 2- بعدها، نُنشِئ نسخة جديدة للفئة PIXI.Sprite، باستخدام الصورة PNG التي أنشأناها سابقًا. بشكل افتراضي، تكون النسخة في الموضع x = 0 و y = 0. ويمكننا وضعها في وسط الشاشة بدلًا من ذلك. يتعين علينا إنشاء حاوية جذرية للأشكال والتي تسمى غالبًا مسرحًا، ثم إلحاق الشّكل بالمسرح. إنّ الأمر أقل تعقيدًا مما قد يبدو عليه. فكِّر في الأمر مثل صفحات HTML. يكون لدى الصفحة دائمًا عنصرٌ جذريٌّ HTML يحتوي جميع العناصر الأخرى التي نضيف إليه. إنّه نفس الشيء هنا. 3- أخيرًا، ننشئ دالّةً للتحريك ونحرص على أن يعرض العارض المسرح والشّكل معًا. نستخدم الدالّة requestAnimationFrame() كطريقة للعرض دون إعاقة سلسلة جافاسكربت. إنّ هناك نقاشًا طويلًا يمكن أن نخوضه حول هذا الموضوع. في الوقت الحالي، من المهم فقط أن تعرف أن الدالّة requestAnimationFrame() تُنفّذ عدة مرات في الثانية. ويتجلى الهدف بالنسبة للعبتنا هنا في عرض ما بين 30 و 60 إطارًا في الثانية، مع القيام بكل أنواع الحسابات في الخلفية. هذه هي الدّالّة التي نحتاج إلى استخدامها حتى يسير كل شيء بسلاسة. حلقات اللعبة سوف تتحكم حلقة اللعبة في تدفق لعبتنا. إنها عملية متكررة لقراءة المُدخلات، وحساب التغييرات في الحال وتقديم المُخرجات إلى الشاشة. كلّ ما قُمنا به حتى الآن هو عرض صورة ثابتة أي إعداد السقالة فحسب. دعنا نتحرك قليلا! بادئ ذي بدء، سنُنشِئ فئة لاعبٍ، حتى نتمكن من تتبع موضع اللاعب: class Player { constructor(sprite, x, y) { this.sprite = sprite; this.x = x; this.y = y; this.sprite.x = this.x; this.sprite.y = this.y; } animate(state) { this.x += 5; if (this.x > window.innerWidth) { this.x = 0; } this.sprite.x = this.x; this.sprite.y = this.y; } } هذا ما تبدو عليه أصناف جافاسكريبت في ES6. تمتلك فئة المشغل مُنشئًا (constructor) نستخدمه لتخزين مرجعٍ يحيل على الشَّكل ويخزّن كذلك إحداثيتي x و y البدئيتين، كما أنّها تمتلك كذلك دالّةً وظيفية animate() سنستدعيها عدة مرات في الثانية. يمكننا استخدام الدّالة لمعرفة ما إذا كان اللاعب يحتاج إلى تغيير موضعه أو القيام بشيء آخر. في هذه الحالة، نُحرّك موضع x للشّكل قليلاً. إذا تحرك اللاعب/الشّكل من الجانب الأيمن من الشاشة، فإننا نعيده إلى الجانب الأيسر منها. ينبغي علينا أيضًا أن نغيّر كيفية إنشاء الشّكل: var sprite = new PIXI.Sprite.fromImage("player.png"); var player = new Player( sprite, window.innerWidth / 2, window.innerHeight / 2 ); stage.addChild(sprite); var state = { "renderer": renderer, "stage": stage }; animate(); function animate() { requestAnimationFrame(animate); player.animate(state); renderer.render(stage); } الآن، بدأنا نشاهد مفعول الحلقة في لعبتنا إذ نطلب من لاعبنا تحريك الشّكل عدة مرات في الثانية. يأخذ اللاعب في الاعتبار بعض منطقه الخاصّ، ويُعدّل الشّكل الخاصّ به كما يريد. يعرض العارض النتيجة في المُخرجات، وما نراه هو رسوم متحركة سلسة. الشيء الوحيد الذي ينقصنا لحدّ الآن هو الإدخال، لكننا سنصل إلى هذه المرحلة قريبًا! ترجمة -وبتصرف- للمقال The Game Loop لصاحبه Christopher Pitt اقرأ أيضًا المقال التالي: جلب المدخلات من اللاعب المقال السابق: مدخل إلى صناعة ألعاب المتصفح
-
شهد قطاع صناعة الألعاب في الأونة الأخيرة تطورًا كبيرًا وفي كلّ عام يزداد الإقبال عليها أكثر مما قبل؛ فصُمّمت الألعاب الإلكترونية في عام 1960، إذ كانت تحتاج لحواسيب ضخمة لم تكُ متاحة للعموم آنذاك. وبعد ذلك بعشر سنوات تطورت الألعاب لتصبح تجارية في عام 1970، وذلك مع مجيء أول أجهزة الألعاب الإلكترونية والحواسيب المنزلية. ونظرًا إلى القدرات المنخفضة للحواسيب آنذاك كان بإمكان مطوّر واحد أن يُنتج لعبة بكاملها. ولكن عند مطلع القرن 21 ومع تزايد قدرات الحواسيب على معالجة البيانات، وتصاعد توقعات المستهلك أصبح من المستحيل أن ينتج مطور واحد لعبة تناسب هذا العصر بل ستحتاج إلى فريق عمل متكامل من منتجين ومصممين ومبرمجين ومهندسي صوت ومؤلفين موسيقيين ومصممي المراحل والعديد من الإختصاصات الأخرى لإنتاج لعبة تناسب هذا العصر. يوجد في وقتنا الحالي العديد من منصات الألعاب، مثل أجهزة البلايستيشن (PlayStation) أو أجهزة الإكس بوكس (Xbox) إذ أن لكل منصة منصة ألعاب صُممت لتُعلب عليها فقط، والبعض الأخر من الألعاب مخصصة لأجهزة الحواسيب (مثل الويندوز أو الماك أو اللينكس)، أو بعض الألعاب الأخرى المخصصة لمتصفح الوِب (تدعى ألعاب المتصفح [Browser Game]) والّتي سنناقشها اليوم ونعلمك كيفية إنشاء لعبة بسيطة تعمل على المتصفح. ألعاب المتصفح (Browser Game) عادةً ما تُعرف ألعاب المتصفح بألعاب الوِب، وهي نمط من الألعاب الّتي تُلعب باستخدام متصفح الإنترنت (مثل غوغل كروم أو أوبرا أو أي متصفح أخر) وذلك من خلال طلب عنوان موقع اللعبة، وبعد تحميل الموقع تظهر لدينا اللعبة ويمكننا بعدها البدء باللعب. غالبًا ما تُصمم هذه الألعاب باستخدام تقنيات الوِب القياسية (مثل: HTML5، و CSS3، و JavaScript وغيرها) أو بتقنيات أخرى مثل أدوبي فلاش ولكن جميع هذه التقنيات لديها إمكانيات محدودة؛ وذلك بسبب مسائل التكاملية والجودة مع المتصفح، ولكن مع ذلك فإن هذه التقنيات هي الّتي تجعل هذه الألعاب تعمل على جميع المتصفحات. واجهت ألعاب المتصفح منذ بدايتها العديد من التحديات، بعضها من ناحية الرسوميات، وبعضها الأخر من ناحية الأداء والسرعة، وبعضها من ناحية الضغط على المخدمات الّتي تستضيف هذه الألعاب (وذلك بسبب كثرة العناصر المحملة من ملفاتٍ وصورٍ والعديد من المكونات الأخرى الموجودة في اللعبة). ولكن ما لبثت هذه التحديات بالظهور حتى أُوجدت لها حلولٌ عديدة فعّالة، ولكن مع ذلك فإنها لم تستطع أن تنافس الألعاب التقليدية وخصوصًا مع الفارق الكبير بين طريقة تعامل كلّ منهم مع موارد الحاسوب. حجزت ألعاب المتصفح لنفسها مكانًا في الألعاب التنافسية متعددة اللاعبين (مثل لعبة الشطرنج ولعبة البلياردو وعلى غرارها من الألعاب)، وهي عادةً ما تركز على الجانب الإجتماعي من اللعبة أكثر من اللعبة بحد ذاتها، إذ تمنح إمكانية الدردشة بين اللاعبين أثناء اللعب، وإمكانية إنشاء ملفاتهم الشخصية (User Profile)، وبالإضافة إلى نظام تنافسي مبني على النقاط والكؤوس مما يعزز روح المنافسة ويضيف متعةً أخرى للعبة. نشأة وتاريخ صناعة ألعاب المتصفح وتطورها ظهرت ألعاب متصفح الوِب في بداية التسعينات من القرن الماضي ولكن لم تبدأ بالانتشار إلا في عام 1996 تزامنًا مع ظهور برنامج Macromedia Flash (المستخدم لتعديل الرسوميات) ولغة البرمجة أكشن سكربت (Action Script) وسرعان ما شكّلا مع بعضهما أول أدوات صناعة ألعاب المتصفح آنذاك. وبعد عام واحد أطلقت شركة Sun Microsystems موقعها الخاص هوت جافا (HotJava) لاستضافة الشيفرات البرمجية المحمولة للغة جافا (Java applet) وتعمل هذه الألعاب على جميع المتصفحات الّتي تدعم لغة جافا، والّذي كان شرارة البدء لعصر ألعاب المتصفح. وبعد النجاح الكبير لألعاب المتصفح والّذي لوحظ من خلال الازدياد الكبير لعدد الأشخاص المنضمين لمواقع ألعاب المتصفح والإقبال الشديد عليها، مما أدى لاهتمام بعض الشركات الكبرى لهذا النمط من الألعاب من بينها شركة ياهو والّتي انضمت إلى هذا المجال لتطلق موقعها Yahoo! Games في عام 1998، والّذي يحتوي على العديد من الألعاب ولتصبح بذلك من أوائل شركات صناعة ألعاب المتصفح آنذاك. وفي عام 2016 أعلنت الشركة عن إغلاق الموقع بسبب عدم تمكن الفرق البرمجية لشركة ياهو من حل المشاكل المتزايد للتقنيات المستخدمة في بناء الألعاب وعدم تلبية هذه الألعاب لمتطلبات الوِب الحديثة كلّ هذه العوامل أدت إلى إغلاق موقع في شهر فبراير من عام 2016. الفرق بين ألعاب المتصفح والألعاب التقليدية تختلف ألعاب المتصفح عن الألعاب التقليدية بعدة نواحي أهمها: 1. طريقة اللعب إن ألعاب المتصفح تتطلب الاتصال الدائم بالإنترنت طوال زمن اللعب لأنه من دون الاتصال بالإنترنت لا يمكن تشغيل الألعاب وذلك لأنها مخزنة على مخدمات موجودة عبر الإنترنت وليس في الحاسوب الشخصي. على عكس الألعاب التقليدية إذ أن أغلبها لا يتطلب اتصالًا دائمًا بالإنترنت. وعلى صعيد أخر في دراسة أجراها باحثون من جامعة ماينتس الألمانية على 8203 شخص من لاعبي لعبة المتصفح الشهيرة ترافيان (Travian) تبين لهم أن ألعاب المتصفح يُستمتع بها أساسًا بسبب الجانب الاجتماعي منها الّذي ينطوي عليه خصائص الوقت، والمرونة العالية في العب، وسهولة الاستخدام، والتنافس بين اللاعبين، وكما أشارت الدراسة نفسها أن سهولة الوصول إلى هذا النوع من الألعاب (لعبة ترافيان-وبشكل عام جميع ألعاب المتصفح) أدت إلى زيادة عدد مرات فتح اللعبة بالموازنة مع الألعاب التقليدية، ولكن مدة جلسات اللعب المتواصلة قصيرة. 2. مصادر الدخل للألعاب يختلف اعتماد ألعاب المتصفح على مصادر الدخل فمنها من يعتمد على عرض إعلانات داخل اللعبة (مثل إعلانات غوغل أدسنس وغيرها)، أو إلى عرض عمليات الشراء داخل اللعبة للحصول على مميزات أو نقاطٍ تتفوق بها على المنافسين، ومنها من تعتمد على الاشتراك الشهري أو السنوي، والبعض الأخر يعتمد على شراء اللعبة لمرة واحدة فقط. وفي بعض الحالات النادرة تكون الألعاب مجانية بالكامل. وفي المقابل فإن غالبية الألعاب التقليدية تعتمد على شراء اللعبة لمرة واحدة بالإضافة لعمليات الشراء داخل اللعبة في حال كانت تُلعب على الإنترنت. 3. استخدام موارد الحاسوب تعدّ ألعاب المتصفح ذات رسوميات منخفضة بالموازنة مع الألعاب التقليدية وذلك من كونها تتعامل مع إمكانيات المتصفح والموارد الّتي يستخدمها، على عكس الألعاب التقليدية الّتي تتعامل مع موارد الجهاز تعاملًا مباشرًا ولكن على صعيد أخر تستهدف ألعاب المتصفح شريحة كبيرة من المستخدمين وعلى كافة الأجهزة وخصوصًا أصحاب الأجهزة الضعيفة والمتوسطة. وعلى صعيدٍ آخر إن غالبية الألعاب التقليدية تحجز مساحة كبيرة من سعة القرص الصلب على عكس ألعاب المتصفح، كما تستهلك الألعاب التقليدية قدرًا كبيرًا من ذاكرة الوصول العشوائي (RAM) بالموازنة مع ألعاب المتصفح مما يجعل ألعاب المتصفح خفيفة جدًا على موارد الحاسوب مهما كانت مواصفاته. مشاكل وحلول وكما ذكرنا سابقًا عانت ألعاب المتصفح العديد من المشاكل فما أن تُحلّ مشكلة حتى تظهر أخرى. وإن من أبرز المشاكل الّتي واجهتها هي: معالجة الرسوميات إن تصاعد توقعات المستهلكين أثقلت كاهل المطورين من ناحية رفع جودة الألعاب وتعزيز تجربة المستخدم، وفي ظل الإمكانيات المحدودة لألعاب المتصفح والّتي جعلت من عملية مواكبة تطور الألعاب عمليةً شاقةً جدًا. ومن أكثر المشاكل الّتي تواجه مطوري ألعاب المتصفح هي معالجة الرسوميات ذات الأبعاد الثنائية أو الثلاثية والّتي شكلت لفترة طويلة سدًا منيعًا في تطور هذا النمط من الألعاب. كان ذلك إلى أن جاءت تقنية WebGL والّتي سمحت لنا بكتابة رسوميات ثلاثية الأبعاد في صفحات الوِب باستخدام جافا سكربت (JavaScript) عبر العنصر canvas (وكما تحدثنا في عدة مقالات عن هذه التقنية وأهميتها). ولكن تقنية WebGL لا تُنشِئ «عناصر» على الصفحة، إذ تتعامل مباشرةً مع البكسلات؛ ولذا نقول عن تقنية WebGL أنّها تقنيةٌ منخفضة المستوى: إذ أنّها توفِّر تحكمًا دقيقًا بالفضاء ثلاثي الأبعاد. والجدير بالذكر أن هذه التقنية، WebGL، مبنية على الواجهة البرمجية OpenGL ES والّتي هي نسخة فرعية من OpenGL. ولكن ماهي OpenGL؟ (Open Graphics Library) هي عبارة عن واجهة برمجية رسومية مستقلة تعمل على مختلف أنظمة التشغيل، ومتوافقة مع عدة لغات برمجية، وصُممت لإنتاج رسوميات ثنائية وثلاثية البعد سواءً المعقدة منها أو البسيطة من خلال تركيبها من مكونات هندسية بسيطة مثل المضلعات أو المثلثات أو المستقيمات …إلخ، وكما أنها تشكّل همزة الوصل بين وحدة المعالجة المركزية CPU ووحدة معالجة الرسوميات GPU. أما OpenGL ES (والتي تعرف اختصارًا للعبارة Open Graphics Library for Embedded Systems) فهي نسخة فرعية من OpenGL الرئيسية، ومبنية خصوصًا للأنظمة المدمجة مثل: الأجهزة الذكية وأجهزة التابلت …إلخ. وهي الّتي بنيت عليها تقنية WebGL. ومع أن WebGL مبنية على OpenGL ولكن يوجد فرق بين إمكانيات كلٍّ منهما وهذه بعض النقاط الّتي تختلف فيها WebGL عن OpenGL. تملك WebGL المميزات التالية: مبنية بلغة جافا سكربت. تُستخدم خصيصًا لمعالجة رسوميات المتصفح. إمكانياتها قليلة بالموازنة مع OpenGL لأنها مبينة على نسخة فرعية من OpenGL وهي OpenGL ES. سهلة التعلم والاستخدام. أما OpenGL فلديها المميزات التالية: مبنية بلغة C. تُستخدم خصيصًا لمعالجة رسوميات تطبيقات سطح المكتب والألعاب. إمكانياتها كثيرة نظرًا لأنها تتخاطب مع العتاد تخاطبًا مباشرًا. صعبة التعلم والاستخدام. وبناءً على ذلك تعدّ WebGL خيارًا جيدًا لمعالجة الرسوميات في المتصفح وتصييرها. الضغط على الخادم تعدّ مشكلة الضغط على المخدم وتوقفه عن العمل من أبرز المشاكل المطروحة، إذ أن بنية ألعاب المتصفح المؤلفة من ملفاتٍ (HTML أو CSS أو JavaScript أو أي ملفات أخرى) وصورٍ والمقاطع الصوتية وفي بعض الأحيان مقاطع فيديو تجعل من استقرار المخدم المضيف وبقائه بالخدمة مهمةً صعبةً للغاية بل شبه مستحيلة، وخصوصًا في حال استقبال الموقع عددًا كبيرًا من الزوار في آنٍ واحد. ولقد ظهر في الأونة الأخير حلٌ عملي ومناسب جدًا لهذه المشكلة وهو استخدام الاستضافة السحابية. ولكن ما هي الاستضافة السحابية؟ هي خدمة استضافة تُمكّن العملاء من الاستفادة من خدماتها بقدر ما يحتاجون إليه، اعتمادًا على مطالب مواقعهم على الإنترنت، وأنهم سيدفعون فقط مقابل ما يستخدمونه. وتعتمد الاستضافة السحابية على مفهوم موازنة الحمل (Load Balancing) والّذي يوزع الأحمال (الزوار) على أكثر من خادوم لتحقيق أكبر سرعة استجابة لموقع الوِب. ومن أبرز المميزات الّتي تقدمها الاستضافة السحابية هي: الوثوقية: بدلًا من استضافة الموقع على خادم واحد مادي يستضاف الموقع على قسم افتراضي إذ يستمد موارده من شبكةٍ واسعةٍ من الخوادم المادية. أي أنه في حال توقف أحد الخوادم لن يؤثر ذلك على توافر الموقع وسوف يستمر بسحب الموارد من شبكة الخوادم المتبقية. المرونة: تبقى موارد الموقع متاحة حتى في حال ارتفاع حركة الزوار أو ازدياد الطلبات على الموقع، أو طلب موارد إضافية تبقى إمكانية الوصول للموارد سلسة. الدفع على قدر الاستخدام: يدفع العميل فقط على قدر ما يستخدم، أي ليس هنالك أي هدر في حال كان الطلب أقل من المعتاد. صناعة ألعاب المتصفح تستخدم العديد من اللغات البرمجية لتصميم ألعاب المتصفح ولكل لغة خواص معينة تميزها عن الأخرى. فعلى سبيل المثال يمكنك بناء لعبة على محرك Unity وذلك باستخدام برنامج Unity3D باستخدام لغة #C أو لغة ++C. والّذي يمكنك بناء ألعاب متميزة نظرًا لإمكانياته القوية والكبيرة (ولقد تناولنا في سلسلة سابقة عن أبرز مميزات هذا البرنامج ننصحك بالاطلاع عليها). وكما يمكنك استخدام تقنيات الوِب القياسية مثل HTML5/CSS أو PHP أو حتى لغة جافاسكربت (JavaScript)، والّتي تعدّ من الخيارات الجيدة لبناء هذه النوع من الألعاب نظرًا من كونها تتيح إمكانية عالية لتفاعل المستخدم مع الموقع (في حالتنا اللعبة). وكما لديها العديد من أُطر العمل المتاحة وشعبية كبيرة تجعلها من أبرز اللغات لتطور ألعاب المتصفح. وبعد أن تعرفنا عن ماهية ألعاب المتصفح، وتجولنا في تاريخها ونشأتها، وبيّنا أهم الفروقات بينها وبين الألعاب التقليدية، وناقشنا أهم المشاكل الّتي تتعرض لها والحلول المناسبة، ما رأيك بأن نُكّمل هذا المشوار ونبدأ ببناء أول لعبة لنا؟ بكل تأكيد أنت متحمس لخوض هذه المغامرة ومعرفة تفاصيلها إذًا اِربط الأحزمة واستعد للانطلاق. سنستخدم لغة جافاسكربت لإنشاء هذه اللعبة. وذلك لإنّها تتطلّب مجهودًا قليلًا بالموازنة مع بقية الخيارات المتاحة وتعمل في كلّ البيئات. سوف ننطلق من لا شيء وننشئ لعبةً ممتعة دون أن نستغرق أي وقت يُذكر. تتألف هذه السلسلة حول صناعة لعبة عبر جافاسكربت من خمس مقالات هي: حلقات اللعبة التكرارية جلب المدخلات من اللاعب كشف التصادمات الجاذبية إنشاء السلالم وختام اللعبة نرجو لك قراءة ممتعة! اقرأ أيضًا المقال التالي: حلقات اللعبة التكرارية تعرف على أشهر محركات الألعاب Game Engines
- 1 تعليق
-
- 3