كيف تصمم لعبة جوال باستخدام HTML5

هل أنت من المهتمين بتطوير ألعاب الجوال؟ هل تريد طريقه بسيطة لدخول هذا المجال؟ لست مضطرا أن تكون خبيرا بلغات Java أو Objective-C … HTML5 + JavaScript مع شوية رياضيات تكفي لأن تصمم لعبتك الاولى.
فلنبدأ على بركة الله…
في نهاية هذا المقال ستكون قادر على:
– فهم وبشكل مبسط مبعض مبادئ برمجة الألعاب.
– التعرف على بعض المفاهيم والمصطلحات الخاصة بهذا المجال.
– بناء اللعبه التالية: يمكنك تجربتها من هنا
ولكن لماذا HTML5؟
ببساطه يمكننا القول ان HTML5 تحقق الهدف البرمجي المنشود “اكتب الكود مرة واحدة … يعمل في كل مكان” write once … run anywhere
أي تعمل على أجهزة الموبايل بنفس الجودة التي تعمل بها على أجهزة الحواسيب وباستخدام نفس الكود.
إليك بعض الأمثله عن العاب تم انشاؤها باستخدام HTML5
أمور يجب الانتباه لها:
قبل أن نبدأ يجب ان نكون على نهتم بالأمور التالية:
أولاً – الأداء:
متصفحات الانترنت بالموبايلات و بالرغم من التطوير السريع ليست داعمه بشكل كبير لمحركات الجافا سكريبت بما فيها IOS6 و Chrome beta.
ثانياً – الدقة:
ما يقلقنا هنا هو وجود تنوع كبير في الدقة لأجهزة الاندرويد والايفون والايباد.
ثالثاً – الصوت:
يجب الاشارة هنا إلى أن دعم الصوت في متصفحات الموبايل ضعيف.
خطوات العمل
ببساطه اللعبه التي سنعمل عليها تتلخص بما يلي: يجب على اللاعب أن يقوم بإصابة الفقاعات قبل أن تصل لقمة الشاشة.
سنقوم بتقسيم العمل إلى الخطوات التاليه:
-
العمل على ملائمه لعبتنا مع قياس شاشات الموبايل.
-
نظرة سريعه على convas المفيده للرسم على الشاشة.
-
التعامل مع أحداث اللمس.
-
صناعه حلقة اللعبة الأساسية.
-
التعرف على مصطلح “entities”.
-
اضافة كشف التصادم والتعامل مع بعض العلاقات الرياضيه.
أولاً – العمل على ملائمة لعبتنا مع قياس شاشات الموبايل:
كما اشرنا سابقا، هناك تنوع كبير في مقاسات الأجهزة الذكيه هذا يعني يجب علينا أن نلائم واجهة عرضنا convas مع شاشات الموبايلات viewport.هذه الملائمة ربما تسبب تراجع جودة الصورة.
يمكن أن نتعامل مع هذه المشكلة بخدعة بسيطة وهي جعل convas صغيرة مما يزيد في الأداء.
هذا الكود سيكون اساس عملنا:
<!DOCTYPE HTML><htmllang="en"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width,user-scalable=no, initial-scale=1, maximum-scale=1, user-scalable=0"/><metaname="apple-mobile-web-app-capable"content="yes"/><metaname="apple-mobile-web-app-status-bar-style"content="black-translucent"/><styletype="text/css">body{margin:0;padding:0;background:#000;}canvas{display:block;margin:0 auto;background:#fff;}</style></head><body><canvas></canvas><script>// all the code goes here</script></body></html>
ايضاحات حول الكود:
– viewport meta تخبر متصفحات الموبايل بتعطيل امكانية تغيير المستخدم او اللاعب للقياس و رسم حجم كامل رغم تصغير او تكبير الصورة.
– Apple meta تسمح للعبة أن تحفظ bookmarked.
انظر لما يلي:
// namespace our gamevarPOP={// set up some initial valuesWIDTH:320,HEIGHT:480,// we'll set the rest of these// in the init functionRATIO:null,currentWidth:null,currentHeight:null,canvas:null,ctx:null,init:function(){// the proportion of width to heightPOP.RATIO=POP.WIDTH/POP.HEIGHT;// these will change when the screen is resizedPOP.currentWidth=POP.WIDTH;POP.currentHeight=POP.HEIGHT;// this is our canvas elementPOP.canvas=document.getElementsByTagName('canvas')[0];// setting this is important// otherwise the browser will// default to 320 x 200POP.canvas.width=POP.WIDTH;POP.canvas.height=POP.HEIGHT;// the canvas context enables us to// interact with the canvas apiPOP.ctx=POP.canvas.getContext('2d');// we're ready to resizePOP.resize();},resize:function(){POP.currentHeight=window.innerHeight;// resize the width in proportion// to the new heightPOP.currentWidth=POP.currentHeight*POP.RATIO;// this will create some extra space on the// page, allowing us to scroll past// the address bar, thus hiding it.if(POP.android||POP.ios){document.body.style.height=(window.innerHeight+50)+'px';}// set the new canvas style width and height// note: our canvas is still 320 x 480, but// we're essentially scaling it with CSSPOP.canvas.style.width=POP.currentWidth+'px';POP.canvas.style.height=POP.currentHeight+'px';// we use a timeout here because some mobile// browsers don't fire if there is not// a short delaywindow.setTimeout(function(){window.scrollTo(0,1);},1);}};window.addEventListener('load',POP.init,false);window.addEventListener('resize',POP.resize,false);
ايضاحات حول الكود:
-
خلقنا namespace للعبتنا واسميناها POP.
-
القسم الاول للتصريح عن المتحولات معظمهم من أجل convas و ctx
-
convas اداة موجوده ضمن HTML5 خاصة لرسم العناصر.
-
CTX أداة خاصة بالتعامل مع convas عن طريق Javascript convas API.
-
POP.init لنتعامل بشكل مبدئي مع عنصر convas ونحدد ابعادها 480*320.
-
resize يتم استدعاء هذا التابع عند أحداث التحميل واعادة التحجيم.
-
Convas لاتزال الى الآن بنفس الأبعاد ولكن سيتم تغيير الابعاد باستخدام CSS.
حاول تغيير ابعاد الصفحه بمتصفح الانترنت (اضغط ctrl مع +او – في أغلب المتصفحات) وسوف تر كيف تتلائم convas.
اذا كنت تجرب على جهاز موبايل سوف تلاحظ وجود شريط العنوان.بامكانك معالجة هذا الموضوع باضافة بكسلات اضافية للمستند كما يلي:
// we need to sniff out Android and iOS// so that we can hide the address bar in// our resize functionPOP.ua=navigator.userAgent.toLowerCase();POP.android=POP.ua.indexOf('android')>-1?true:false;POP.ios=(POP.ua.indexOf('iphone')>-1||POP.ua.indexOf('ipad')>-1)?true:false;// this will create some extra space on the// page, enabling us to scroll past// the address bar, thus hiding it.if(POP.android||POP.ios){document.body.style.height=(window.innerHeight+50)+'px';}
ثانيا – نظرة سريعه على convas المفيده للرسم على الشاشة.
الآن يجب أن نحجم convas (عنصر الرسم الذي نعمل عليه) بحجم viewport، دعنا نضيف امكانية لرسم بعض الأشكال.
فيما يلي سنضيف كلاس Draw الذي سيسمح لنا بمسح الشاشة، رسم مستطيل ودائرة وإضافة نص.
نضيف الكود التالي إلى نهاية المكان المخصص لكود Javascript
// abstracts various canvas operations into// standalone functionsPOP.Draw={clear:function(){POP.ctx.clearRect(0,0,POP.WIDTH,POP.HEIGHT);},rect:function(x,y,w,h,col){POP.ctx.fillStyle=col;POP.ctx.fillRect(x,y,w,h);},circle:function(x,y,r,col){POP.ctx.fillStyle=col;POP.ctx.beginPath();POP.ctx.arc(x+5,y+5,r,0,Math.PI*2,true);POP.ctx.closePath();POP.ctx.fill();},text:function(string,x,y,size,col){POP.ctx.font='bold '+size+'px Monospace';POP.ctx.fillStyle=col;POP.ctx.fillText(string,x,y);}
};
ايضاحات حول الكود:
-
Draw يملك طرق لتنظيف الشاشة ورسم المستطيلات والدوائر والنص.
-
من أهم فوائد تجريد هذه العمليات جعلنا غير مضطرين لتذكر استدعاء convas API مثلا يمكننا رسم دائرة بسطر واحد بدل بخمسة أسطر.
دعنا نقوم باختبار بسيط، اضف الكود السابق بنهاية التابع POP.init ويجب أن تر الأشكال مرسومه باداة convas.
// include this at the end of POP.init functionPOP.Draw.clear();POP.Draw.rect(120,120,150,150,'green');POP.Draw.circle(100,100,50,'rgba(255,0,0,0.5)');POP.Draw.text('Welcome to APPCODE.me',100,100,10,'#000');
ايضاحات حول الكود:
-
بالسطر الأول نستدعي تابع مسح الشاشة.
-
بالسطر الثاني نستدعي تابع لاضافة مستطيل أخضر اللون.
-
بالسطر الثالث نستدعي تابع لاضافة دائرة حمراء اللون.
-
بالسطر الرابع نستدعي تابع لاضافة نص “Welcome to APPCODE.me”.
ثالثا – التعامل مع أحداث اللمس:
كوجود دعم لأحداث click لدينا طرق تدعمها متصفحات الموبايل للتعامل مع أحداث اللمس touch.
أحداث اللمس touchstart و touchmove , touchend تحتوي مصفوفه اللمس والتي يحتوي كل عنصر منها على مكان اللمس وبياناته بحيث نصل للمسة الأولى من خلال e.touch[0] .
أضف الكود التالي لتابع POP.init الذي يقوم باضافة مستمعي هذه أحداث اللمس.
// listen for clickswindow.addEventListener('click',function(e){e.preventDefault();POP.Input.set(e);},false);// listen for toucheswindow.addEventListener('touchstart',function(e){e.preventDefault();// the event object has an array// named touches; we just want// the first touchPOP.Input.set(e.touches[0]);},false);window.addEventListener('touchmove',function(e){// we're not interested in this,// but prevent default behaviour// so the screen doesn't scroll// or zoome.preventDefault();},false);window.addEventListener('touchend',function(e){// as abovee.preventDefault();},false);
ربما نلاحظ بأن الكود بالأعلى يمرر بيانات الحدث لمثال input. دعنا نقوم بمايلي:
// + add this at the bottom of your code,// before the window.addEventListenersPOP.Input={x:0,y:0,tapped:false,set:function(data){this.x=data.pageX;this.y=data.pageY;this.tapped=true;POP.Draw.circle(this.x,this.y,10,'red');}};
الآن يجب أن تظهر دائرة حمراء مكان نقر المؤشر يمكنك تجريب ذلك.
لدينا مشكلة وهي عدم ظهور هذه الدوائر صحيح؟
السبب ببساطه اختلاف القياس بين convas و الشاشة ولتلافي هذه المشكلة يمكن أن نقوم بالخطوات التاليه:
– أضف الكود التالي الى بداية البرنامج مع قسم الاعلان عن المتحولات.
// let's keep track of scale// along with all initial declarations// at the start of the programscale:1,// the position of the canvas// in relation to the screenoffset={top:0,left:0},
– نضيف مايلي إلى التابع الخاص بالتحجيم. وبالضبط بعد تحديد طول وعرض convas.
// add this to the resize function.POP.scale=POP.currentWidth/POP.WIDTH;POP.offset.top=POP.canvas.offsetTop;POP.offset.left=POP.canvas.offsetLeft;
– نضيف مايلي لطريقة set الموجوده في POP.Input:
this.x=(data.pageX-POP.offset.left)/POP.scale;this.y=(data.pageY-POP.offset.top)/POP.scale;
رابعاً – صناعة حلقة اللعبة الأساسية:
الألعاب عادة تدور بالخطوات التالية:
– جلب مدخلات المستخدم.
– نحديث التصادمات وخصائص اللعبة.
– إرسال المخرجات على الشاشة.
– كرر ماسبق.
مبدأ عم الألعاب بشكل عام هي رسم واجهات Frames متعدده مع الزمن حتى يبدو الأمر وكأنه مستمر.
سنضيف الكود التالي الذي يقوم بتحديث الواجهات Frame الى بداية كود الجافا سكريبت.
// http://paulirish.com/2011/requestanimationframe-for-smart-animating// shim layer with setTimeout fallbackwindow.requestAnimFrame=(function(){returnwindow.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(callback){window.setTimeout(callback,1000/60);};})();
والآن الكود التالي يمثل هيكل الحلقه الأساسية للعبة سنضيفه لنهاية POP.init.
// Add this at the end of POP.init;// it will then repeat continuouslyPOP.loop();// Add the following functions after POP.init:// this is where all entities will be moved// and checked for collisions, etc.update:function(){},// this is where we draw all the entitiesrender:function(){POP.Draw.clear();},// the actual loop// requests animation frame,// then proceeds to update// and renderloop:function(){requestAnimFrame(POP.loop);POP.update();POP.render();}
ايضاحات حول الكود:
-
POP.loop يستدعي طرقنا POP.update و POP.render.
-
RequestAnimFrame يؤكد بأن الحلقة تستدعى بشكل منفصل 60 واجهة frame بالثانية.
-
لاحظ أننا بشكل مسبق استمعنا (تعاملنا مع) لاحداث اللمس والنقر الذي نصل له عن طريق POP.Input class.
-
مشكلتنا الآن أننا بحاجه لنتذكر ماذا رسم على الشاشة واين؟؟؟
خامسا – التعرف على مصطلح الكائنات Entities:
سنضيف بالبداية مصفوفة كائنات للاحتفاظ بتحركات كل الكائنات لدينا لقسم التصريح عن المتحولات ببداية برنامجنا.
هذه المصفوفه سوف تحتوي على مؤشرات لكل من اللمس، الدوائر، وكل الأشياء الديناميكية التي نريد اضافتها للعبة.
// put this at start of programentities:[],
دعنا نخلق كلاس Touch المسؤول عن رسم، حذف وتلاشي الدوائر عند حدوث التصادم.
POP.Touch=function(x,y){this.type='touch';// we'll need this laterthis.x=x;// the x coordinatethis.y=y;// the y coordinatethis.r=5;// the radiusthis.opacity=1;// initial opacity; the dot will fade outthis.fade=0.05;// amount by which to fade on each game tickthis.remove=false;// flag for removing this entity. POP.update// will take care of thisthis.update=function(){// reduce the opacity accordinglythis.opacity-=this.fade;// if opacity if 0 or less, flag for removalthis.remove=(this.opacity<0)?true:false;};this.render=function(){POP.Draw.circle(this.x,this.y,this.r,'rgba(255,0,0,'+this.opacity+')');};};
ايضاحات حول الكود:
-
كلاس Touch يضع عدد من الخصائص عند التهيئة الأولية.
-
الأبعاد x,y تمرر كمعاملات ثم نهيء نصف القطر this.r بقيمه 5 بكسل.
-
نهيء الشفافية ب 1 و معدل التناقص 0.05.
-
علم ramove الذي يخبر الحلقة الأساسية بلعبتنا قيما إذا كنا نريد إزالة هذا الكائن.
-
هذا الكلاس يملك طريقتين رئيسيتين update و render سوف نستدعهم في القسم المخصص بحلقة لعبتنا.
-
سننشئ مثال جديد من كلاس Touch في حلقة لعبتنا ثم نحركهم باستخدام طريقة update.
// POP.update functionupdate:function(){vari;// spawn a new instance of Touch// if the user has tapped the screenif(POP.Input.tapped){POP.entities.push(newPOP.Touch(POP.Input.x,POP.Input.y));// set tapped back to false// to avoid spawning a new touch// in the next cyclePOP.Input.tapped=false;}// cycle through all entities and update as necessaryfor(i=0;i<POP.entities.length;i+=1){POP.entities[i].update();// delete from array if remove property// flag is set to trueif(POP.entities[i].remove){POP.entities.splice(i,1);}}},
ايضاحات حول الكود:
-
اذا كانت POP.Input.tapped = true عندها سنضيف غرض جديد من POP.Touch لمصفوفة كائناتنا.
-
ثم نكرر خطواتنا خلال مصفوفة كائناتنا مستدعين طريقة update من أجل كل كائن.
-
أخيرا اذا كان الكائن مؤشر عليه remove سيتم حذفه.
-
ثم نرسلهم للشاشة عن طريق التابع render.
-
بشكل مشابه لتابع update سوف نمر على كل الكائنات ونستدعي طريقة render لرسمهم.
// POP.render functionrender:function(){vari;POP.Draw.rect(0,0,POP.WIDTH,POP.HEIGHT,'#036');// cycle through all entities and render to canvasfor(i=0;i<POP.entities.length;i+=1){POP.entities[i].render();}},
الآن سنضيف كلاس Bubble المسؤول عن إنشاء وتعويم الفقاعات للأعلى.
POP.Bubble=function(){this.type='bubble';this.x=100;this.r=5;// the radius of the bubblethis.y=POP.HEIGHT+100;// make sure it starts off screenthis.remove=false;this.update=function(){// move up the screen by 1 pixelthis.y-=1;// if off screen, flag for removalif(this.y<-10){this.remove=true;}};this.render=function(){POP.Draw.circle(this.x,this.y,this.r,'rgba(255,255,255,1)');};};
ايضاحات حول الكود:
-
كلاس POP.Bubble مشابه جدا لكلاس Touch الاختلاف الرئيسي بكونه لا يخفي الفقاعات بل يرفعهم للأعلى (هذا التأثير ينجز بتعديل مكان y). هنا يجب أن نفحص فيما اذا كانت الفقاعات خارج الشاشة ليتم ازالتها عن طريق العلم remove.
-
لاحظ نحن كان بإمكاننا أن نخلق كلاس أساسي Entity ونورث خصائصه لكلا Bubble و Touch.
// Add at the start of the program// the amount of game ticks until// we spawn a bubblenextBubble:100,// at the start of POP.update// decrease our nextBubble counterPOP.nextBubble-=1;// if the counter is less than zeroif(POP.nextBubble<0){// put a new instance of bubble into our entities arrayPOP.entities.push(newPOP.Bubble());// reset the counter with a random valuePOP.nextBubble=(Math.random()*100)+100;}
بالكود السابق أضفنا مؤقت لحلقة لعبتنا التي تخلق فقاعات بأماكن مختلفه. عند بداية اللعبة نضع الفقاعه الجديدة بقيمة 100. وتتناقص هذه القيمه إلى أن نصل 0 نولد فقاعه جديدة بعداد جديد.
سادسا – إضافة كشف التصادم والتعامل مع بعض العلاقات الرياضية:
إلى الآن لم نتكلم عن كشف التصادمات يمكننا إضافتهم بتابع بسيط يعتمد على علاقات رياضية حصلنا عليها من عالم الرياضيات brush up on at Wolfram MathWorld.
// this function checks if two circles overlapPOP.collides=function(a,b){vardistance_squared=(((a.x-b.x)*(a.x-b.x))+((a.y-b.y)*(a.y-b.y)));varradii_squared=(a.r+b.r)*(a.r+b.r);if(distance_squared<radii_squared){returntrue;}else{returnfalse;}};// at the start of POP.update, we set a flag for checking collisionsvari,checkCollision=false; // we only need to check for a collision// if the user tapped on this game tick// and then incorporate into the main logicif(POP.Input.tapped){POP.entities.push(newPOP.Touch(POP.Input.x,POP.Input.y));// set tapped back to false// to avoid spawning a new touch// in the next cyclePOP.Input.tapped=false;checkCollision=true;}// cycle through all entities and update as necessaryfor(i=0;i<POP.entities.length;i+=1){POP.entities[i].update();if(POP.entities[i].type==='bubble'&&checkCollision){hit=POP.collides(POP.entities[i],{x:POP.Input.x,y:POP.Input.y,r:7});POP.entities[i].remove=hit;}// delete from array if remove property// is set to trueif(POP.entities[i].remove){POP.entities.splice(i,1);}}
بالوضع الحالي الفقاعات مملة لذلك سنعطي كل فقاعه سرعة مختلفة.
POP.Bubble=function(){this.type='bubble';this.r=(Math.random()*20)+10;this.speed=(Math.random()*3)+1;this.x=(Math.random()*(POP.WIDTH)-this.r);this.y=POP.HEIGHT+(Math.random()*100)+100;this.remove=false;this.update=function(){this.y-=this.speed;// the rest of the class is unchanged
لأكثر استمتاعا باللعبه دعنا نجعلهم ينتقلون من جانب لآخر بشكل جيبي حتى تزداد صعوبة الاصابة.
// the amount by which the bubble// will move from side to sidethis.waveSize=5+this.r;// we need to remember the original// x position for our sine wave calculationthis.xConstant=this.x;this.remove=false;this.update=function(){// a sine wave is commonly a function of timevartime=newDate().getTime()*0.002;this.y-=this.speed;// the x coordinate to follow a sine wavethis.x=this.waveSize*Math.sin(time)+this.xConstant;// the rest of the class is unchanged
الآن يلزمنا إظهار بعض الاحصائيات التي توصف مستوى تقدم اللاعب.
// this goes at the start of the program// to track players's progressPOP.score={taps:0,hit:0,escaped:0,accuracy:0},
الآن في كلاس Bubble نزيد قيمة المتحول POP.score.escaped عند كل خروج لفقاعه من الشاشة ( حتما يتم الخروج عند عدوم حدوث الإصابة).
// in the bubble class, when a bubble makes it to// the top of the screenif(this.y<-10){POP.score.escaped+=1; // update scorethis.remove=true;}
في حلقة التعديل الاساسية سنزيد POP.score.hit والذي يمثل عدد الفقاعات التي أصابها اللاعب.
// in the update loopif(POP.entities[i].type==='bubble'&&checkCollision){hit=POP.collides(POP.entities[i],{x:POP.Input.x,y:POP.Input.y,r:7});if(hit){POP.score.hit+=1;}POP.entities[i].remove=hit;}
حتى تكون إحصائيتنا صحيحة نحن بحاجة لتسجيل كل نقرة يقوم بها اللاعب.
// and record all tapsif(POP.Input.tapped){// keep track of taps; needed to// calculate accuracyPOP.score.taps+=1;
