لعبة جديدة مع جو قديم على Three.js. الجزء 2

تحدثت في الجزء الأول عن المشكلات التي واجهتها في عملية إنشاء لعبة ثلاثية الأبعاد للمتصفح باستخدام Three.js. أود الآن التركيز على حل بعض المشاكل المهمة عند كتابة لعبة ، مثل إنشاء المستويات ، واكتشاف الاصطدامات ، وتكييف الصورة مع أي نسبة عرض إلى ارتفاع من نافذة المتصفح.


الرسوم البيانية المستوى


في الواقع ، يتم إنشاء المستويات نفسها في المحرر ثلاثي الأبعاد ، أي شكلها الهندسي ورسم خرائط النسيج وخبز الظل وما إلى ذلك. وصفت كل هذا في الجزء الأول. لماذا هناك أي مخططات أخرى؟ الحقيقة هي أن Three.js لا يقدم أي نوع من المحركات المادية ، وأنا استخدم مخططات مستوى لتحديد العقبات.


يوفر Three.js لحل مشكلة التصادم فقط raytracing - أبسط طريقة لتحديد تقاطع هندسة الكائنات. من حيث المبدأ ، يمكن استخدامه ، وقد قمت بذلك في أحد مشاريعي الأخرى. لقد كانت مدينة افتراضية على الموقع مباشرة في المتصفح. يمكنك التنقل في جميع أنحاء المدينة وعدم المرور عبر الجدران.


في حالة حدوث تقاطع لهندسة المشغل والمبنى أثناء الحركة ، قمت بتنفيذ تنافر اللاعب بمسافة معينة في الاتجاه المقابل للجدار. ولكن لهذا الغرض ، يجب أن تكون الكائنات متوازية. حول بعض الكائنات المعقدة ، قمتُ بإنشاء مصادمات (سوف ندعو الأشياء غير المرئية التي تلعب دور العقبات وتمنع اللاعب من المرور عبر نفسه) ، والتي من خلالها تم التقاطعات. والأجزاء السفلية من بعض المباني ، والتي هي ببساطة "صناديق" ، كانت تستخدم أحيانًا كعملاء تصادم.


في الكائنات المعقدة هندسيًا ، قد لا يعمل تتبع الأشعة أو يتصرف بشكل غير لائق. وكحل بديل ، يمكنك تضمين الكائن وليس واحدًا ، ولكن مع عدة مصادمات غير مرئية صغيرة في شكل موازٍ لشفافٍ بنسبة 100٪ ، يتم رسمها بجوار بعضها البعض وعلى رأس بعضها البعض ، مع تكرار شكل الكائن تقريبًا.

في اللعبة حول الأبراج المحصنة ، المستوى عبارة عن كائن طويل واحد مع تحركات مقطوعة لتحريك اللاعب. في الواقع ، من أجل حل مشكلة الاصطدام ، يمكن للمرء أن يلتصق بالمصادمات غير المرئية عند الضرورة واستخدام raytracing. ومع ذلك ، قررت أن أذهب في الاتجاه الآخر.


  • أولاً ، أردت أتمتة عملية إنشاء مجموعة من المصادمات.
  • ثانياً ، يمكنك استخدام المعلومات فقط حول المصادم ، أي إحداثياتها في الفضاء ، وعدم تحميل المشهد ثلاثي الأبعاد نفسه مع بعض الكائنات الفارغة الإضافية.
  • ثالثًا ، نظرًا لأن اللعبة تستخدم طريقة عرض جانبية فقط ولا يتغير أحد الإحداثيات أبدًا عند الحركة ، يمكنك استخدام حساب التقاطعات في إحداثيتين فقط.
  • ورابعا ، بعد كل شيء ، في الواقع ، سيكون هناك مخطط للمستوى. علاوة على ذلك ، فإن الخروج بمستويات جديدة يعد أمرًا مناسبًا بدءًا من مثل هذا المخطط. يمكنك ببساطة سحب الكتل حول الشاشة في أي محرر رسومات ، وإنشاء ممرات وعقبات جديدة ، ثم تشغيل البرنامج النصي والحصول على معلومات حول المصادم. بمعنى ، يتم حل مشكلة محرر المستوى جزئيًا.

كتبت نصًا يأخذ معلمات الإدخال مثل اسم ملف مخطط المستوى (png) واللون ، والذي يتم تفسير تعبئته على أنه عقبة. لون المساحة الحرة الافتراضية أسود. للمعالجة بواسطة البرنامج النصي ، يجب حفظ مخطط كل مستوى في ملف png منفصل. على سبيل المثال ، بالنسبة إلى المستوى الأدنى ، يبدو الأمر كما يلي:


وافقت على أن كتلة واحدة يجب أن تكون بعرض 80 بكسل وارتفاع 48 بكسل. هذا يتوافق مع 4 × 2.4 متر في العالم ثلاثي الأبعاد. سيكون من الممكن إنشاء 40 × 24 بكسل ، أي عشر مرات ، ولكن في الصورة تبدو صغيرة.

نتيجة البرنامج النصي في المستوى الأول (يتم اقتصاص الصورة إلى اليمين):


يتم تنفيذ البرنامج النصي في المتصفح. أعتقد أنه لا فائدة من ترميز HTML ، إنه أساسي: حقول إدخال البيانات وزر البدء. بعد ذلك ، يتم عرض صورة القراءة على القماش. وكنتيجة للبرنامج النصي ، يتم عرض صفيف أسفل الصورة في المقياس العالمي ثلاثي الأبعاد ، والذي يحتوي على الإحداثيات اليمنى السفلى والعليا اليمنى لكل كتلة ، والإزاحة المحددة في البرنامج النصي لكل مستوى. يمكن نسخ هذه المجموعة ولصقها في قائمة المصادمات لاستخدامها في اللعبة (المزيد في ذلك أدناه) ، وسيتم تخزينها في نوع ثابت. تظهر الإحداثيات أيضًا على الصورة نفسها ، ولكن في الإطار المرجعي للصورة ثنائية الأبعاد. يتم عرض هذه الأرقام في وسط كل كتلة وتتيح لك التحقق مما إذا كانت جميع الكتل مدرجة في الحساب. في حد ذاتها ، ليست هناك حاجة لهذه الأرقام لأي شيء باستثناء الفحص البصري. يجب عدم حساب بعض الكتل ، مثل الأعمدة التي يمر فيها اللاعب. حول الكائنات التي يتم استبعادها من الحساب - أدناه.

بالإضافة إلى ذلك ، على سبيل المثال ، توجد في المستوى الثاني لوحات أفقية رقيقة يمشي عليها اللاعب. يجب النظر فيها. وفقًا لذلك ، تحتاج إلى التأكد من ظهور الأرقام عليها أيضًا. في الرسم التخطيطي ، اجعلها عالية 2 بكسل.



الآن ، حول كيفية أخذ البرنامج النصي في الاعتبار الكتل:

  • تتم معالجة المخطط بواسطة 80 × 48 قطعة ، حيث يتم أخذ مساحة من 2 إلى 79 بكسل أفقياً ومن 2 إلى 47 بكسل عموديًا. لا يتم استخدام البيكسلين الأول والأخير بحيث حول الكتل يمكنك إنشاء إطار أسود بعرض 1 بكسل ، مما يحسن الإدراك المرئي للدائرة ويسهل إنشائها.
  • يتم عرض كل بكسل من الصف العلوي من الكتلة. إذا كانت هناك إحداثيات ملونة بينها ، فإن إحداثيات الكتلة تنتقل من الأول إلى آخر بكسل ملون أفقيًا وإلى الارتفاع الكامل للكتلة رأسياً في المصفوفة النهائية. سيكون هذا كتلة فارغة في عرض كامل أو جزئي.
  • يتم عرض كل بكسل من الخط السفلي للكتلة. إذا كان هناك تنسيقات ملونة بينها ، لكن لا يوجد لون واحد في الصف العلوي ، فإن إحداثيات الكتلة تنتقل من الأول إلى آخر بكسل ملون أفقيًا و 3 بكسل رأسيًا من الأسفل إلى الصفيف الأخير. ستكون هذه منصة يمكن السير عليها. داخل كتلة واحدة يمكن أن يكون هناك العديد من المنصات الأفقية. يتم التعرف على المنصات فقط في الجزء السفلي من الكتلة. يتم "غرق" إحداثيات المنصة في كتلة ، تقع أدناه ، بحيث يكون سطح المنصة على نفس المستوى مع الكتل المجاورة - وليس المنصات.
  • لا تتم معالجة الأعمدة والزخارف الأخرى داخل كتلة فارغة ، حيث يتم اعتبار الصف العلوي والسفلي فقط من وحدات البكسل. لذلك ، داخل الكتلة ، يمكنك وضع ديكور ، وشرح للمخطط ، والمؤشرات ، والأعمدة ، وما إلى ذلك ، دون خوف من أن يؤثر ذلك بطريقة ما على نتيجة البرنامج النصي.

ثم تُترجم جميع الإحداثيات المستلمة من المصفوفة إلى مقياس العالم ثلاثي الأبعاد ، مضروبًا بمعامل المقياس (الذي تم اختياره في المحرر ثلاثي الأبعاد عندما تم إنشاؤه). المجموعة جاهزة للاستخدام في اللعبة. تمت كتابة رمز البرنامج النصي على عجل ، لذلك لا يتظاهر بأنه أنيق ، لكنه يؤدي مهمته.

قانون
ap = { //      (  ),   3D   lvd: { 'lv01.png': { invw: false, invh: true, level_dw: -8.5, level_dh: -1.5 }, 'lv02.png': { invw: true, invh: true, level_dw: -19.5, level_dh: -5.5 } }, blockw: 80, //   2D blockh: 48, //   2D sc3d: 0.05, //,   3D  ex: 100, //  3D (-   ) v: { data: [] }, i: 0, par: {}, datai: [], resi: [], ars: [], fStopEncode: false, blockColor: function(cl) { document.getElementById('input_cl').value = cl; }, startEncode: function() { //      for (var key in ap.lvd) { ap.lvd[key].dw = ap.lvd[key].level_dw * ap.blockw; ap.lvd[key].dh = ap.lvd[key].level_dh * ap.blockh; }; document.getElementById('startbtn').style.display = 'none'; document.getElementById('startmsg').style.display = 'block'; var cl = document.getElementById('input_cl').value; var fld = document.getElementById('input_fld').value; var nm = document.getElementById('input_nm').value; ap.nm = nm; ap.par = { path: [fld + '/', nm], key: [nm], cl: aplib.hexToRgb(cl.substring(1, 7)) }; setTimeout(function() { ap.datai[ap.par.key] = new Image(); ap.datai[ap.par.key].onload = function() { ap.parseData(); }; ap.datai[ap.par.key].src = ap.par.path[0] + ap.par.path[1]; }, 500); }, stopEnode: function(e) { if (typeof ap !== "undefined") { if (e.keyCode == 27) { console.log('stop'); ap.fStopEncode = true; }; }; }, parseData: function() { ap.w = ap.datai[ap.par.key[0]].width, ap.h = ap.datai[ap.par.key[0]].height; aplib.initCanv(ap.w, ap.h); ctx.drawImage(ap.datai[ap.par.key[0]], 0, 0, ap.w, ap.h, 0, 0, ap.w, ap.h); ap.ars = []; ap.i = 0; setTimeout(function() { ap.parseData1(); }, 1000); }, parseData1: function() { if (ap.i < ap.par.key.length) { document.getElementById('info').innerHTML = '' + ap.nm; ap.blocksw = Math.floor(ap.w / ap.blockw); ap.blocksh = Math.floor(ap.h / ap.blockh); ap.ar = []; ap.arv = {}; ap.hi = 0; ctx.fillStyle = '#CCCCCC'; ap.parseData2(); } else { document.getElementById('startbtn').style.display = 'block'; document.getElementById('startmsg').style.display = 'none'; }; }, parseData2: function() { if (ap.hi < ap.blocksh) { ap.ar.push([]); ap.wi = 0; ap.parseData3(); } else { ap.parseData4(); }; }, parseData3: function() { var k = ''; if (ap.wi < ap.blocksw) { var fground = true, fvari = false, fempty = true; var upx1 = 0, upx2 = 0, dnx1 = 0, dnx2 = 0; var upxf = false, dnxf = false; for (var wii = 1; wii < ap.blockw - 2 + 2; wii++) { pixelDatai = ctx.getImageData(ap.wi * ap.blockw + wii, ap.hi * ap.blockh + 1, 1, 1).data; //  pixelDatai2 = ctx.getImageData(ap.wi * ap.blockw + wii, (ap.hi + 1) * ap.blockh - 3, 1, 1).data; //  if ((pixelDatai[0] == ap.par.cl.r) & (pixelDatai[1] == ap.par.cl.g) & (pixelDatai[2] == ap.par.cl.b)) { //   ground    if (upxf == false) { upxf = true; upx1 = wii; }; } else { //    if (upxf == true) { upx2 = wii + 1; upx1--; //   dy = -1; // 3D       1 ap.v.data.push([ap.wi * ap.blockw + upx1, ap.hi * ap.blockh + dy, ap.wi * ap.blockw + upx2, ap.hi * (ap.blockh) + ap.blockh - 1]); upxf = false; upx1 = 0; upx2 = 0; }; }; if ((pixelDatai2[0] == ap.par.cl.r) & (pixelDatai2[1] == ap.par.cl.g) & (pixelDatai2[2] == ap.par.cl.b)) { //   ground     if (upxf == false) { if (dnxf == false) { dnxf = true dnx1 = wii; }; }; } else { if (upxf == false) { if (dnxf == true) { dnx2 = wii + 1; dnx1--; //   dy = 2; // 3D    2 ap.v.data.push([ap.wi * ap.blockw + dnx1, (ap.hi + 1) * ap.blockh - 3 + dy, ap.wi * ap.blockw + dnx2, (ap.hi + 1) * ap.blockh - 3 + 2 + dy]); dnxf = false; dnx1 = 0; dnx2 = 0; }; }; }; }; if (ap.fStopEncode == true) { ap.hi = ap.h, ap.wi = ap.w, i = ap.par.key.length; }; setTimeout(function() { ap.wi++; ap.parseData3(); }, 10); } else { ap.hi++; ap.parseData2(); }; }, parseData4: function() { setTimeout(function() { var t, tw, tx, ty, ar = []; //  for (var i = 0; i < ap.v.data.length; i++) { ar = ap.v.data[i]; t = ar[0] + ';' + (ar[1]+1) + '<br/>' + ar[2] + ';' + (ar[3]+1); tw = ar[2] - ar[0]; tx = ar[0]; ty = ar[1] + Math.floor((ar[3] - ar[1]) / 2) - 0; aplib.Tex2Canvas(ctx, t, 'normal 10px Arial', 10, '#CCCCCC', tx, ty, tw, 0, 'center', 'top'); }; ap.parseData5(); }, 10); }, parseData5: function() { var t, tw, tx, ty, ar = [], n; //   3D var lv = ap.lvd[ap.nm]; for (var i = 0; i < ap.v.data.length; i++) { ar = ap.v.data[i]; ar[0] += lv.dw; ar[1] += lv.dh; ar[2] += lv.dw; ar[3] += lv.dh; if (lv.invh == true) { n = -ar[1]; ar[1] = -ar[3]; ar[3] = n; }; if (lv.invw == true) { n = -ar[0] ar[0] = -ar[2]; ar[2] = n; }; ar[0] = Math.round(ap.sc3d * ar[0] * ap.ex) / ap.ex; ar[1] = Math.round(ap.sc3d * ar[1] * ap.ex) / ap.ex; ar[2] = Math.round(ap.sc3d * ar[2] * ap.ex) / ap.ex; ar[3] = Math.round(ap.sc3d * ar[3] * ap.ex) / ap.ex; }; //    ap.v.data.sort(aplib.sortBy0); console.log(ap.v.data); document.getElementById('divresult').innerHTML = JSON.stringify(ap.v.data); } }; aplib = { hexToRgb: function(hex) { var arrBuff = new ArrayBuffer(4); var vw = new DataView(arrBuff); vw.setUint32(0, parseInt(hex, 16), false); var arrByte = new Uint8Array(arrBuff); return { r: arrByte[1], g: arrByte[2], b: arrByte[3], s: arrByte[1] + "," + arrByte[2] + "," + arrByte[3] }; }, //   canvas Tex2Canvas: function(ctx, t, font, lin, fcolor, x, y, w, h, haln, valn) { //left, right, center, center-lim- ctx.font = font; ctx.fillStyle = fcolor; var l = 0; var tx = x; var ftw = false; var tw = 1; var arr = t.split('<br/>'); for (var i = 0; i < arr.length; i++) { arr[i] = arr[i].split(' '); }; for (var i = 0; i < arr.length; i++) { var s = '', slen = 0, s1 = '', j = 0; while (j < arr[i].length) { var wordcount = 0; while ((slen < w) & (j < arr[i].length)) { s = s1; s1 = s + arr[i][j] + ' '; slen = ctx.measureText(s1).width; if (slen < w) { j++; wordcount++; } else { if (wordcount > 0) { s1 = s; } else { j++; }; }; }; ftw = false; tw = ctx.measureText(s1).width; if (haln == 'center') { tx = x + Math.round((w - tw) / 2); }; if (haln == 'right') { tx = x + Math.round((w - tw)); }; if (haln == 'center-lim') { if (tw > w) { tw = w; }; if (tw < 1) { tw = 1; }; tx = x + Math.round((w - tw) / 2); ftw = true; }; if (ftw == false) { ctx.fillText(s1, tx, l * lin + y); } else { ctx.fillText(s1, tx, l * lin + y, tw); }; if (s1 == '') { j = arr[i].length + 1; }; l++; s1 = ''; slen = 0; }; }; return Math.round(tw); }, // canvas initCanv: function(w, h) { function canvErr() { document.getElementById('divcanv').innerHTML = '<div style="height:130px"></div><div style="width:440px; border:#FFFFFF 1px solid; margin:10px; padding:4px; background-color:#000000"><p class="txterr">---> Error<br/>HTML5 Canvas is not supported!<br/>Please, update your browser!</p></div>'; }; if (w == 0) { w = 740; h = 680; }; elcanv = document.getElementById('divcanv'); elcanv.innerHTML = '<canvas id="canv" style="width:' + w + 'px; height:' + h + 'px; display:block;" width="' + w + '" height="' + h + '"></canvas>'; canvas1 = document.getElementById('canv'); if (!canvas1) { canvErr(); return 0; } else { if (canvas1.getContext) { ctx = canvas1.getContext('2d'); ctx.clearRect(0, 0, w, h); return 1; } else { canvErr(); }; }; }, sortBy0: function(i, ii) { if (i[0] > ii[0]) return 1; else if (i[0] < ii[0]) return -1; else return 0; } }; 


الآن - حول كيفية عمل اللعبة مع مجموعة من الكتل. تستخدم اللعبة ممرات متقاطعة (المستويات). عندما يتحول اللاعب إلى ممر ، يتم توصيل مجموعة جديدة من الكتل: وبناءً على ذلك ، يتم الحصول على صفيف خاص به ، يتم الحصول عليه من مخطط المستوى الخاص به. أثناء حركة اللاعب ، يتم فحص إحداثياته ​​لكونه داخل كل كتلة. وإذا كان داخل أي كتلة ، ثم نحصل على تصادم. لكن مع كل حركة للاعب ، لا نحتاج إلى البحث عن التقاطعات مع كل كتل المستوى ، لأنه يمكن أن يكون هناك الكثير منها. قم بإنشاء مجموعة من الكتل الأقرب إلى اللاعب فقط.

 collisionsUpdate: function(x, y, dw, dh) { var coll = []; var o; for (var i = 0; i < ap.v.lv.d.length; i++) { o = ap.v.lv.d[i]; if ((o[0] >= x - ap.v.dw) & (o[2] <= x + ap.v.dw)) { if ((o[1] >= y - ap.v.dh) & (o[3] <= y + ap.v.dh)) { coll.push(o); }; }; }; ap.v.coll = coll; }, 

هنا ، عند المدخلات x ، y هي الإحداثيات الحالية للاعب ، dw ، dh هي المسافة التي تريد البحث فيها عن الكتل أفقياً وعمودياً ، على سبيل المثال 12 و 8 أمتار. بمعنى آخر ، اصطحب جميع الكتل الموجودة حول اللاعب على مساحة 24 × 16 مترًا. سوف يشاركون في البحث عن الاشتباكات. ap.v.lv.d [i] هو عنصر من مجموعة من كتل المستوى الحالي ، في الواقع ، هو نفسه أيضًا عبارة عن مجموعة من 4 أرقام تحدد حدود كتلة واحدة - [x1 ، y1 ، x2 ، y2] ، لذلك ، للتحقق من المربع أفقيًا ، نأخذ عناصر ذات مؤشرات 0 و 2 ، وعموديًا - 1 و 3. إذا كان هناك تطابق ، فأضف هذه الكتلة إلى قائمة التصادمات ap.v.coll.

عندما يتحرك المشغل ، سنقوم بتحديث قائمة التصادمات هذه ، ولكن من أجل توفير الأداء ، فإننا لن نقوم بذلك في كل خطوة (أو بالأحرى ، تقديم الإطار) ، ولكن عندما يترك اللاعب مربعًا معينًا ، أصغر قليلاً ، محدد في ap.v.collwStep و ap.v.collhStep ، على سبيل المثال 8 و 4 أمتار. أي أننا سنقوم بإعادة تجميع صفيف التصادم مرة أخرى عندما يمر اللاعب بمسار معين أفقيًا أو رأسيًا من موضعه الأصلي. في الوقت نفسه ، دعونا نتذكر موقفها الذي قمنا فيه بإعادة تجميع الصفيف من أجل استخدامه للتكرار التالي. pers [ax] - هنا بالفأس نعني محور الإحداثيات (ax) ، يمكن أن يكون x أو z ، وهذا يتوقف على اتجاه الممر الذي يمشي اللاعب على طوله.

 //   if ((Math.abs(pers[ax] - ap.v.collw) > ap.v.collwStep) || (Math.abs(pers.y - ap.v.collh) > ap.v.collhStep)) { ap.v.collw = pers[ax]; ap.v.collh = pers.y; ap.collisionsUpdate(pers[ax], pers.y, 12, 8); }; 

لماذا هذه الصعوبات؟ لماذا لا تستخدم مجموعة كاملة من الاصطدامات على المستوى ولا تأخذ حمام بخار. والحقيقة هي أن الكشف عن التصادم يتم وفقًا لخوارزمية أكثر تعقيدًا ، وليس من المربح التحقق من الاصطدام بجميع كتل المستوى تمامًا ، وليس الأقرب منها ، في كل عملية تقديم للإطار. (رغم أن هذا ليس دقيقًا.)

تعريف التصادمات في كل تجسيد للإطار باستخدام صفيف التصادم المعد أعلاه:

قانون
 collisionsDetect: function(x, y, xOld, yOld, up) { //up=-1 -  var res = false, o; var collw = false, collh = false, collwi = false, collhi = false, collhsup = false, support = [], supportf = false, fw = false, upb = -1; var bub = -1, bubw = 0; var pw2 = ap.v.player.pw2, ph2 = ap.v.player.ph2, supportd = ap.v.supportd; for (var i = 0; i < ap.v.coll.length; i++) { o = ap.v.coll[i]; collwi = false; collhi = false; collhsup = false; fw = false; if ((x + pw2 >= o[0]) & (x - pw2 <= o[2])) { if ((y + ph2 > o[1]) & (y - ph2 < o[3])) { collwi = true; }; }; //     if ((xOld + pw2 >= o[0]) & (xOld - pw2 <= o[2])) { if ((yOld + ph2 > o[1]) & (yOld - ph2 < o[3])) { bub = i; if (Math.abs(xOld - o[0]) < Math.abs(xOld - o[2])) { bubw = -1; } else { bubw = 1; }; }; }; if ((x >= o[0]) & (x <= o[2])) { fw = true; //  i   }; if ((y + ph2 >= o[1]) & (y - ph2 <= o[3])) { if ((x > o[0]) & (x < o[2])) { collhi = true; //  if (y + ph2 > o[3]) { collhsup = true; supportf = true; support = o; upb = 1; }; //  if (y - ph2 < o[1]) { upb = -1; }; }; }; if ((y - ph2 >= o[3] + supportd - 0.11) & (y - ph2 <= o[3] + supportd + 0.001)) { if (fw == true) { collhi = true; collh = true; res = true; collhsup = true; supportf = true; support = o; }; }; if (collwi & collhi) { res = true; }; if (collwi) { collw = true; }; if (collhi) { collh = true; }; }; return { f: res, w: collw, h: collh, support: support, supportf: supportf, upb: upb, bub: bub, bubw: bubw }; }, 


هنا x ، y ، xOld ، yOld هي الإحداثيات الجديدة والحالية للاعب. يتم احتساب إحداثيات جديدة بلمسة زر واحدة ، بناءً على سرعة حركة معينة ، أي أنها إحداثيات محتملة. يتم فحصها لمعرفة ما إذا كانت تقع داخل أي كتلة من قائمة التصادمات. إذا سقطوا ، فإنهم يعودون إلى القديم ، ولا يمر اللاعب من خلال العقبة. وإذا لم يسقطوا ، فسيصبحون حاليين. pw2 و ph2 هما نصف عرض وارتفاع المصادم الوهمي للاعب (عرض المشغل / 2 ، ارتفاع المشغل / 2). يتم إصدار الإخراج إذا كان هناك تصادم أفقي ورأسي (collw ، collh) ، ما إذا كان هناك كتلة دعم تحت المشغل (supportf) - وهذا يوضح ما إذا كان يجب البدء في مزيد من الرسوم المتحركة للسقوط أو إذا كان اللاعب قد انتقل ببساطة إلى كتلة مجاورة ، وهلم جرا. فقط لا تسأل لماذا أضفت 0.001 هناك وطرح 0.11. هذا عكاز فظيع يمنع السقوط من خلال الكتل وتأثير الارتعاش عند الاصطدام بعقبة أفقية ... تعمل هذه الوظيفة ، لكن يجب إعادة كتابتها بالطريقة العادية. تحسين هذه الوظيفة مفقود أيضًا.

أعتقد أنه مع الاصطدامات ، من المفيد أن ننتهي هنا.

من الصعب تحديد مدى سرعة عملي أو ربما أبطأ من تتبع الشعاع ، ولكن في حالة الأخير ، يخزن Three.js أيضًا مجموعة من الكائنات التي تشارك في نظام التصادم. إنه يتم تحديد الاصطدامات هناك بطريقة انبعاث الحزمة وتقاطعها مع مستويات جوانب الكائنات ، ومعى ، من خلال تحديد ما إذا كانت إحداثيات كائن واحد داخل الآخر على طول كل من المحورين.

تحتوي اللعبة أيضًا على كائنات متحركة (سمك قرش) وكائنات علامة تؤدي إلى نوع من الرسوم المتحركة (على سبيل المثال ، يؤدي الاتصال مع الماء إلى تحريك حركة القرش). تشارك كل هذه الكائنات أيضًا في التصادمات ، وبعضها مع إحداثيات متغيرة الوقت. من الغريب أن كل شيء أبسط: أثناء حركة الكائن تتم مقارنة إحداثياته ​​بإحداثيات اللاعب.


غمبد


بشكل عام ، فإن الحفاظ على لوحة ألعاب javascript في المتصفح ليست مهمة تافهة. لا يوجد أي زر الصحافة والإفراج عن الأحداث. لا يوجد سوى أحداث تتصل بالجهاز وفصله والحالة التي يمكن الحصول عليها عن طريق الاستقصاء الدوري ، ثم مقارنتها بالأحداث السابقة.

مقطع فيديو يوضح تشغيل لوحة الألعاب في مستعرض على جهاز كمبيوتر لوحي على نظام التشغيل Windows 8.1 وجهاز كمبيوتر يعمل بنظام Windows 10. ومع ذلك ، فإن الجهاز اللوحي هو إصدار قديم ، تم إصداره في عام 2014 ، لذلك تم تعطيل الإضاءة الديناميكية على اللعبة الموجودة به.


للاستطلاع على لوحة اللعبة ، يتم استخدام وظيفة تسمى مرة واحدة كل 100 مللي ثانية. تم ضبطه باستخدام وظيفة مكتبتي m3d.lib.globalTimer.addEvent.

 m3d.lib.globalTimer.addEvent({ name: 'gamepad', ti: 100, f: function() { var st = m3d.gamepad.state(); if (st == false) { if (contr.gpDownFlag == true) { m3d.gamepad.resetH(); }; }; } }); 

هنا globalTimer هو نظام إدارة حدث timer setInterval javascript الذي كتبته. هناك ، تتم إضافة سلسلة من الأحداث ببساطة إلى مجموعة معينة تحتاج إلى استدعاء في فواصل زمنية مختلفة. بعد ذلك ، يتم ضبط جهاز ضبط وقت واحد للإعداد بتردد مطابق للحدث ذي أعلى تردد على الإطلاق. يستقصي المؤقت الدالة m3d.lib.globalTimer.update () ، التي تعمل من خلال قائمة جميع الأحداث وتقوم بتشغيل وظائف تلك التي أصبحت قابلة للتنفيذ. عند إضافة أحداث أو إزالتها ، يمكن أيضًا تغيير تردد الفاصل (على سبيل المثال ، إذا قمت بحذف الحدث الأسرع).

تقوم اللعبة أيضًا بتعيين معالجات لكل مفتاح لوحة ألعاب: "a" مخصصة للمحور (ax) ، و "b" للزر (button) ، و 11 هي الانحراف الأيسر على طول المحور الأفقي للصليب (كما لو كان الزر 1) ، 12 - الانحراف الأيمن على طول المحور الأفقي للصليب (كما لو كان الزر 2) ، 21 و 22 - للمحور العمودي. على سبيل المثال:

['أ' ، 11] ،
['ب' ، 3]

يعني أنه سيتم تعيين الوظيفة التالية في نفس الوقت للانحراف على طول المحور الأفقي إلى اليسار وللزر 3 (يسار). حسنًا ، يتم تعيين وظيفة سيتم تنفيذها عند الضغط على الزر ، ثم تحريرها.

  m3d.gamepad.setHandler( [ ['a', 11], ['b', 3] ], function(v) { if (contr.btState.lt == false) { contr.keyDownFlag = true; contr.btState.lt = true; contr.gpDownFlag = true; apcontrolsRenderStart(); }; }, function(v) { contr.btState.lt = false; m3d.contr.controlsCheckBt(); apcontrolsRenderStart(); } ); 

هنا apcontrolsRenderStart () هي وظيفة تقوم بتشغيل التجسيد إذا لم تكن قيد التشغيل بالفعل. بشكل عام ، يرتبط دعم لوحة الألعاب بإحكام بمكتبتي m3d ، لذلك إذا تابعت لوصف جميع ميزاته ، فسيتم تمديده لفترة طويلة جدًا ...

لن أعطيك سوى جزءًا منه - لوحة الألعاب ، حيث قمت بتنفيذ تهيئة لوحة اللعبة ، وتثبيت معالجات ، واستطلاع الحالة بأبسط الطرق.

قانون
 gamepad: { connected: false, gamepad: {}, gamepadKey: '', axesCount: 0, buttonsCount: 0, f: [], //  fup: [], //  fval: [], //      fupCall: [], //   buttons: [], //link to f [0.. ] axes: [], //link to f [0.. ] initCb: function() {}, resetH: function() {}, init: function(gp) { var f = false; for (var key in gp) { if (f == false) { if (gp[key] != null) { if (typeof gp[key].id !== "undefined") { f = true; this.connected = true; this.gamepad = gp[key]; this.gamepadKey = key; }; }; }; }; if (typeof this.gamepad.axes !== "undefined") { this.axesCount = this.gamepad.axes.length; }; if (typeof this.gamepad.buttons !== "undefined") { this.buttonsCount = this.gamepad.buttons.length; }; this.f = []; this.fup = []; this.fval = []; this.fupCall = []; this.axes = []; for (var i = 0; i < this.axesCount * 2; i++) { this.axes.push(-1); }; this.buttons = []; for (var i = 0; i < this.buttonsCount; i++) { this.buttons.push(-1); }; this.initCb(); }, setHandlerReset: function(f) { this.resetH = f; }, setHandler: function(ar, f, fup) { //ar['b',3] ['a',11] var fi, bt, ax, finext, finexta; finexta = false; for (var i = 0; i < ar.length; i++) { if (ar[i][0] == 'a') { ax = Math.floor(ar[i][1] / 10); bt = ar[i][1] - (ax * 10); bt = ax * 2 + bt - 3; fi = this.axes[bt]; if (fi == -1) { //   fi = this.f.length; if (finexta == false) { finexta = true; this.f.push(f); this.fup.push(fup); this.fval.push(0); this.fupCall.push(true); this.axes[bt] = fi; } else { fi--; this.f[fi] = f; this.fup[fi] = fup; this.axes[bt] = fi; }; } else { this.f[fi] = f; this.fup[fi] = fup; }; } else if (ar[i][0] == 'b') { bt = ar[i][1] - 1; fi = this.buttons[bt]; if (fi == -1) { //   fi = this.f.length; if (finexta == false) { finexta = true; this.f.push(f); this.fup.push(fup); this.fval.push(0); this.fupCall.push(true); this.buttons[bt] = fi; } else { fi--; this.f[fi] = f; this.fup[fi] = fup; this.buttons[bt] = fi; }; } else { this.f[fi] = f; this.fup[fi] = fup; }; }; }; }, state: function() { var pressed = false; var fi, fval, axesval; for (var i = 0; i < this.fval.length; i++) { this.fval[i] = 0; }; //   var gp = navigator.getGamepads()[this.gamepadKey]; for (var i = 0; i < this.axesCount; i++) { axesval = Math.round(gp.axes[i]); if (axesval < 0) { pressed = true; fi = this.axes[i * 2]; if (fi != -1) { this.fval[fi] = gp.axes[i]; this.fupCall[fi] = true; }; } else if (axesval > 0) { pressed = true; fi = this.axes[i * 2 + 1]; if (fi != -1) { this.fval[fi] = gp.axes[i]; this.fupCall[fi] = true; }; }; }; for (var i = 0; i < this.buttonsCount; i++) { if (gp.buttons[i].pressed == true) { pressed = true; fi = this.buttons[i]; if (fi != -1) { this.fval[fi] = 1; this.fupCall[fi] = true; }; }; }; for (var i = 0; i < this.fval.length; i++) { fval = this.fval[i]; if (fval != 0) { this.f[i](this.fval[i]); } else { if (this.fupCall[i] == true) { this.fupCall[i] = false; this.fup[i](this.fval[i]); }; }; }; return pressed; } }, //gamepad 


بشكل عام ، لا يزال دعم gamepad في اللعبة غير مكتمل: يتم دعم الدعم فقط لأبسط gamepad ، ولكن ليس الدعم الذي يتم استخدامه ، على سبيل المثال ، في XBox ، لأنني لا أمتلكه. إذا حصلت عليها ، فسأبرمجها وأعمل معها. سيكون من الممكن ضبط سرعة الشخصية ، أي أنه سيكون من الممكن التحرك بأي سرعة في النطاق من خطوة إلى أخرى. يتم تحقيق ذلك من خلال أخذ المعلمات الكسرية من المحاور. لا تُرجع لوحة الألعاب الخاصة بي سوى الأعداد الصحيحة -1 و 1. علاوة على ذلك ، فإن لوح اللعبة الخاص بي لديه تقاطع مثير للاشمئزاز ، وعندما يتم الضغط عليه يسارًا أو يمينًا ، يتم الضغط في نفس الوقت إلى أسفل أو لأعلى. لذلك ، لم أستخدم الجزء العلوي والسفلي على التقاطع ونسخته بالأزرار الموجودة على يمين لوحة اللعبة ... قبل إصدار اللعبة ، أخطط لإنشاء العديد من ملفات تعريف لوحات الألعاب. بالإضافة إلى ذلك ، في حالة توصيل العديد من لوحات الألعاب ، سيتم استخدام الأخير فقط حتى الآن.

شاشة استجابة


تم تصميم اللعبة للحصول على نسبة عرض إلى ارتفاع تبلغ 16: 9. لكنني أضفت ضبطًا أفقيًا تلقائيًا قدره ± 10٪ بحيث لم تكن هناك أشرطة سوداء على الجانبين في نافذة المتصفح الموسعة:


وسيكون مثل هذا:


في وضع ملء الشاشة ، سيكون هناك 16: 9 حقيقية. قد يكون من الممكن تكييف الصورة بشكل عام مع أي نسبة عرض إلى ارتفاع من نافذة المتصفح ، لكنني لم أفعل ذلك ، لأن نافذة عريضة منخفضة قد تؤدي إلى زاوية عرض كبيرة جدًا ، وهي ليست جيدة من وجهة نظر اللعب: نهايات مسدودة بعيدة ، كائنات ، سيكون الأعداء مرئيين على الفور وكل شيء آخر لا يحتاج اللاعب إلى رؤيته بعد. لذلك ، حصرت نفسي في التعديل خلال ± 10٪ من 16: 9. ومع ذلك ، بالنسبة للشاشات الضيقة (4: 3) ، أدركت مع ذلك القدرة على التبديل من 16: 9 إلى وضع التكيف من 4: 3 إلى 16: 9 بالضغط على المفتاح Y. ولكن ليس على نطاق أوسع - لذلك ، مرة أخرى ، وليس لكسر اللعب. أي أنه يمكنك اللعب بنسبة 16: 9 الكلاسيكية ، أو يمكنك تكبير الصورة إلى ارتفاع النافذة عن طريق اقتصاصها أفقياً. على الرغم من أن هذا ليس جيدًا أيضًا ، على سبيل المثال ، في مواقف الأروقة ، عندما يطير شيء نحو اللاعب من الجانب. يتبقى القليل من الوقت لرد الفعل. ولكن يمكنك دائمًا العودة بسرعة إلى الوضع الكلاسيكي.


يتم عرض تعديل الشاشة ، وكذلك جميع المفاتيح الساخنة المستخدمة في اللعبة في الفيديو التالي:


في الواقع ، يتم تعيين نسبة العرض إلى الارتفاع في إعدادات اللعبة.

 aspect1:{w:1280, h:720, p:10}, //16x9 +- 10% aspect2:{w:960, h:720, p:34}, //4x3 +- 34% 

وفي اللعبة ، عندما تضغط على Y ، يتم التبديل:

 contr.btCodesDn[89] = function() { //'y' if (m3dcache.setup.aspect.swch == 1) { m3dcache.setup.aspect = m3dcache.setup.aspect2; m3dcache.setup.aspect.swch = 2; } else { m3dcache.setup.aspect = m3dcache.setup.aspect1; m3dcache.setup.aspect.swch = 1; }; m3d.core.onWindowResize(0); m3d.contr.renderAll(); }; 

تحتوي المكتبة الخاصة بي على حدث معلق في نافذة تغيير الحجم. هنا جزء منه:

قانون
 m3dcache.v.vw = window.innerWidth; m3dcache.v.vh = window.innerHeight; m3dcache.v.vclipw = 0; m3dcache.v.vcliph = 0; if (typeof m3dcache.setup.aspect !== "undefined") { if ((m3dcache.setup.aspect.w == 0) || (m3dcache.setup.aspect.h == 0)) {} else { var o = m3d.lib.inBlock(0, 0, m3dcache.setup.aspect.w, m3dcache.setup.aspect.h, 0, 0, m3dcache.v.vw, m3dcache.v.vh, 'center', 'center', 'resize'); if (typeof m3dcache.setup.aspect.p !== "undefined") { if (o.clipx > 0) { ow = ow * (m3dcache.setup.aspect.p / 100 + 1); if (ow > m3dcache.v.vw) { ow = m3dcache.v.vw; }; o = m3d.lib.inBlock(0, 0, ow, oh, 0, 0, m3dcache.v.vw, m3dcache.v.vh, 'center', 'center', 'resize'); }; }; m3dcache.v.vclipw = o.clipx; m3dcache.v.vcliph = o.clipy; var margx = o.clipx + 'px', margy = o.clipy + 'px'; document.getElementById('m3dcontainer').style.marginLeft = margx; document.getElementById('m3dcontainer').style.marginTop = margy; if (document.getElementById('renderer') !== null) { document.getElementById('renderer').style.marginLeft = margx; document.getElementById('renderer').style.marginTop = margy; }; m3dcache.v.vw = ow; m3dcache.v.vh = oh; }; }; 


m3d.lib.inBlock هي أيضًا وظيفة لمكتبتي ، التي تدرج مستطيلًا في مستطيل آخر بمعلمات مثل توسيط أو تغيير الحجم أو الاقتصاص ، وتعرض الأبعاد الجديدة للمستطيل المدرج ، وكذلك أحجام الحقول التي يتم تشكيلها في هذه العملية. بناءً على هذه المعلومات ، يتم وضع حاوية div الخاصة بالنافذة. "العارض" هو عنصر سياق كتلة لمشهد ثلاثي الأبعاد. ثم يتم تحجيم قماش هناك وفقا للمعايير التي تم الحصول عليها.

يتم عرض واجهة المستخدم في الحاوية على عنصر قماش منفصل. بشكل عام ، تتكون شجرة المستندات من ثلاث كتل DIV شفافة ذات وضع مطلق (أكثر أو أقل ممكنًا ، اعتمادًا على احتياجات اللعبة): في الجزء السفلي ، توجد اللوحة الفنية للمشهد ثلاثي الأبعاد ، ويوجد أعلى اللوحة القماشية لوحدة IU ويتم استخدام الجزء العلوي لتحريك عناصر الواجهة وتأثيرات مرئية أخرى . بمعنى أن واجهة المستخدم غير معروضة ثلاثي الأبعاد ، ولكن على knavass ، أو طبقة. يتم ترك مهمة دمج الطبقات في صورة واحدة للمتصفح. للعمل مع واجهة المستخدم ، لدي كائن خاص في المكتبة. لفترة وجيزة - جوهر هو على النحو التالي. يتم تحميل قوائم Sprite مع عناصر واجهة المستخدم بتنسيق png مع الشفافية. من هناك ، يتم أخذ العناصر اللازمة - الخلفيات والأزرار. ويتم رسمها على اللوحة القماشية الوسطى باستخدام دالة js drawImage (img ، ix ، iy ، iw ، ih ، x ، y ، w ، h). أي ، يتم عرض الأجزاء الضرورية من الصورة في المواضع الضرورية على الشاشة. يتم عرض الأزرار أعلى الخلفيات المرتبطة بها - يتم تعيين جميع مواقعها وأحجامها في تكوين واجهة المستخدم. عند تغيير حجم النافذة ، يتم إعادة حساب مواضع العناصر الموجودة على اللوحة القماشية الهدف (التي يتم عرضها عليها) ، اعتمادًا على ما إذا كان هذا العنصر أو ذاك يتركز أفقياً وعموديًا أو موصوفًا إلى أي زاوية أو وجه من الشاشة. يؤدي هذا إلى إنشاء واجهة مستخدم تكيفية لا تعتمد على نسبة العرض إلى الارتفاع للشاشة. فقط من الضروري تعيين الحد الأدنى من الدقة الممكنة أفقياً ورأسياً وعدم سقوطها بحيث لا تتداخل العناصر مع بعضها البعض. سأتحدث عن واجهة المستخدم مرة أخرى ، لأن المقالة اتضح أنها ضخمة ، وما زلت أعمل على واجهة المستخدم ، لأنه لا يزال هناك الكثير من الوظائف التي أحتاجها. على سبيل المثال ، على الشاشات عالية الدقة ، ستبدو الواجهة صغيرة. يمكنك ضرب حجم العناصر بمعامل معين ، اعتمادًا على دقة الشاشة. من ناحية أخرى ، ربما ليست هناك حاجة إلى أزرار ضخمة على الشاشة؟ إذا كانت دقة الشاشة ضخمة ، فإن الشاشة نفسها كبيرة جدًا.


ويمكنك إعطاء المبرمج خيارًا - سواء لتوسيع وحدة IU بشكل ديناميكي بحجم الإطار أو لتوزيع العناصر في الزوايا. في حالة الحجم الديناميكي ، هناك أيضًا أسئلة خاصة بهم - على سبيل المثال ، "الصابون" للواجهة عندما يتم عرضها على نطاق واسع جدًا. إذا قمت بإنشاء أجزاء من عناصر الواجهة بدقة وضوح عميقة ، فستشغل مساحة كبيرة وكذلك ، على الأرجح ، لن يكون مفيدًا للأجهزة الصغيرة - فهي لا تزال بحاجة إلى حروف كبيرة ، ولكنها ستستهلك الذاكرة.

أعتقد أن هذا يكفي لهذا اليوم. لا يزال هناك شيء للتفكير ، وكيفية تنفيذ هذا أو ذاك. في هذه الأثناء ، أستطرد لبعض الوقت من البرمجة وأقوم بالترقية. أخطط للمشاركة في مجموعة من عروض indie وأشارك بنشاط في الترويج للعبة في الشبكات الاجتماعية ، حيث أنوي في نوفمبر أن أذهب إلى منصة التمويل الجماعي: سأحتاج إلى متخصصين في مجال الرسومات ثلاثية الأبعاد والرسوم المتحركة الهيكلية من أجل إكمال اللعبة.

في المقالات التالية ، سأتحدث عن عناصر التحكم باللمس في المتصفحات للأجهزة المحمولة - لا يربط الجميع لوحة الألعاب أو لوحة المفاتيح بالكمبيوتر اللوحي ، وحول تحسين الرسومات ثلاثية الأبعاد للأجهزة منخفضة الطاقة ، وغير ذلك الكثير.

Source: https://habr.com/ru/post/ar472272/


All Articles