Ein neues Spiel mit einer alten Atmosphäre auf Three.js. Teil 2

Im ersten Teil habe ich über die Probleme gesprochen, die beim Erstellen eines 3D-Spiels für den Browser mit Three.js aufgetreten sind. Jetzt möchte ich ausführlich auf die Lösung einiger wichtiger Probleme beim Schreiben eines Spiels eingehen, z. B. das Erstellen von Ebenen, das Erkennen von Kollisionen und das Anpassen des Bilds an beliebige Proportionen des Browserfensters.


Level-Diagramme


Tatsächlich werden die Ebenen selbst in einem 3D-Editor erstellt, nämlich ihre Geometrie, Texturabbildung, Backschatten usw. Ich habe das alles im ersten Teil beschrieben. Warum gibt es andere Systeme? Tatsache ist, dass Three.js keine physische Engine bietet und ich Level-Schemata verwende, um Hindernisse zu identifizieren.


Three.js zur Lösung des Kollisionsproblems bietet nur Raytracing - die einfachste Methode, um den Schnittpunkt der Geometrie von Objekten zu bestimmen. Im Prinzip kann es verwendet werden, und ich habe es sogar in einem meiner anderen Projekte getan. Es war eine virtuelle Stadt direkt auf der Website, im Browser. Sie können sich in der Stadt bewegen und nicht durch die Mauern gehen.


Für den Fall, dass der Schnittpunkt der Geometrie des Spielers und des Gebäudes während der Bewegung auftritt, habe ich die Abstoßung des Spielers um einen bestimmten Abstand in die der Wand entgegengesetzte Richtung implementiert. Dafür müssen Objekte jedoch Parallelepipeds sein. Um einige komplexe Objekte herum habe ich Collider erstellt (wir werden die unsichtbaren Objekte nennen, die die Rolle von Hindernissen spielen und den Spieler daran hindern, durch sich selbst zu gelangen), durch die Kreuzungen ausgearbeitet wurden. Und die unteren Teile einiger Gebäude, die einfach „Kisten“ sind, wurden manchmal selbst als Kollider verwendet.


Bei geometrisch komplexen Objekten funktioniert die Strahlverfolgung möglicherweise nicht oder verhält sich nicht unangemessen. Und genau als Lösung können Sie nicht einen, sondern mehrere kleine unsichtbare Kollider in Form von Parallelepipeds mit 100% Transparenz in das Objekt einbetten, die nebeneinander und übereinander gezeichnet sind und die Form des Objekts grob wiederholen.

Im Spiel über Dungeons ist das Level ein einzelnes langes Objekt mit Schnittbewegungen, um den Spieler zu bewegen. Um das Kollisionsproblem zu lösen, könnte man bei Bedarf unsichtbare Kollider kleben und Raytracing verwenden. Ich entschied mich jedoch für den anderen Weg.


  • Zunächst wollte ich den Prozess der Erstellung einer Reihe von Collidern automatisieren.
  • Zweitens können Sie nur Informationen über die Collider, dh deren Koordinaten im Raum, verwenden und die 3D-Szene selbst nicht mit zusätzlichen leeren Objekten laden.
  • Drittens können Sie die Berechnung von Schnittpunkten in nur zwei Koordinaten verwenden, da das Spiel nur eine Seitenansicht verwendet und sich eine der Koordinaten beim Bewegen nie ändert.
  • Und viertens wird es tatsächlich ein Level-Schema geben. Darüber hinaus ist es einfach bequem, mit einem solchen Schema neue Level zu entwickeln. Sie können einfach Blöcke in einem beliebigen Grafikeditor über den Bildschirm ziehen, neue Korridore und Hindernisse erstellen, dann das Skript ausführen und Informationen zu den Collidern abrufen. Das heißt, das Problem des Level-Editors ist teilweise gelöst.

Ich habe ein Skript geschrieben, das Eingabeparameter wie den Namen der Level-Schema-Datei (png) und die Farbe verwendet, deren Füllung als Hindernis interpretiert wird. Die Standardfarbe für freien Speicherplatz ist Schwarz. Für die Verarbeitung durch das Skript muss das Schema jeder Ebene in einer separaten PNG-Datei gespeichert werden. Für die unterste Ebene sieht es beispielsweise so aus:


Ich stimmte zu, dass ein Block 80 Pixel breit und 48 Pixel hoch sein sollte. Dies entspricht 4 x 2,4 Metern in der 3D-Welt. Es wäre möglich, 40 x 24 Pixel zu erstellen, das heißt zehnmal, aber auf dem Bild sieht es klein aus.

Das Ergebnis des Skripts auf der ersten Ebene (das Bild wird rechts zugeschnitten):


Das Skript wird im Browser ausgeführt. Ich denke, HTML-Markup macht keinen Sinn, es ist elementar: Dateneingabefelder und eine Startschaltfläche. Als nächstes wird das gelesene Bild auf der Leinwand angezeigt. Als Ergebnis des Skripts wird unter dem Bild in der 3D-Weltskala ein Array angezeigt, das die unteren linken und oberen rechten Koordinaten jedes Blocks sowie den im Skript für jede Ebene angegebenen Versatz enthält. Dieses Array kann kopiert und in die Liste der im Spiel zu verwendenden Collider eingefügt werden (mehr dazu weiter unten). Es wird in einer Art Konstante gespeichert. Koordinaten erscheinen auch auf dem Bild selbst, jedoch im Referenzrahmen des 2D-Bildes. Diese Zahlen werden in der Mitte jedes Blocks angezeigt und ermöglichen es Ihnen, zu überprüfen, ob alle Blöcke in die Berechnung einbezogen sind. Diese Nummern werden für nichts anderes als für die Sichtprüfung benötigt. Einige Blöcke, z. B. Spalten, zwischen denen der Spieler spielt, sollten nicht gezählt werden. Über welche Objekte von der Berechnung ausgeschlossen werden - siehe unten.

Zusätzlich gibt es zum Beispiel auf der zweiten Ebene dünne horizontale Platten, auf denen der Spieler geht. Sie müssen berücksichtigt werden. Dementsprechend müssen Sie sicherstellen, dass auch Zahlen darauf erscheinen. Machen Sie sie im Diagramm 2 Pixel hoch.



Nun, wie das Skript Blöcke berücksichtigt:

  • Das Schema wird von 80x48-Blöcken verarbeitet, in denen jeweils ein Bereich vom 2. bis zum 79. Pixel horizontal und vom 2. bis zum 47. Pixel vertikal genommen wird. Das erste und das letzte Pixel werden nicht verwendet, sodass Sie um die Blöcke herum einen schwarzen Rahmen mit einer Breite von 1 Pixel erstellen können. Dies verbessert die visuelle Wahrnehmung der Schaltung und erleichtert deren Erstellung.
  • Alle Pixel der oberen Reihe des Blocks werden angezeigt. Wenn sich unter ihnen farbige befinden, gehen die Koordinaten des Blocks horizontal vom ersten bis zum letzten farbigen Pixel und vertikal bis zur vollen Höhe des Blocks im endgültigen Array. Dies ist ein leerer Block mit voller oder teilweiser Breite.
  • Alle Pixel der unteren Zeile des Blocks werden angezeigt. Wenn sich unter ihnen farbige befinden, in der oberen Reihe jedoch keine farbige, werden die Koordinaten des Blocks horizontal vom ersten bis zum letzten farbigen Pixel und vertikal 3 Pixel vom unteren bis zum endgültigen Array verschoben. Dies wird eine Plattform sein, auf der man laufen kann. Innerhalb eines Blocks können sich mehrere horizontale Plattformen befinden. Plattformen werden nur am unteren Rand des Blocks erkannt. Die Koordinaten der Plattform werden in einen Block „versenkt“, der sich darunter befindet, so dass sich die Oberfläche der Plattform auf derselben Ebene befindet wie benachbarte Blöcke - nicht Plattformen.
  • Spalten und andere Dekorationen innerhalb eines leeren Blocks werden nicht verarbeitet, da nur die obere und untere Pixelreihe berücksichtigt werden. Daher können Sie innerhalb des Blocks Dekor, Erklärungen für das Diagramm, Zeiger, Spalten usw. platzieren, ohne befürchten zu müssen, dass dies das Ergebnis des Skripts irgendwie beeinflusst.

Anschließend werden alle aus dem Array erhaltenen Koordinaten in den Maßstab der 3D-Welt übersetzt, multipliziert mit dem Koeffizienten des Maßstabs (der beim Erstellen im 3D-Editor ausgewählt wurde). Das Array ist für den Einsatz im Spiel bereit. Der Skriptcode wurde in Eile geschrieben, gibt also nicht vor, elegant zu sein, sondern erfüllt seine Aufgabe.

Code
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; } }; 


Nun - darüber, wie das Spiel mit einer Reihe von Blöcken funktioniert. Das Spiel verwendet sich kreuzende Korridore (Ebenen). Wenn sich ein Spieler in einen Korridor verwandelt, wird ein neues Array von Blöcken verbunden: und für jeden Korridor wird dementsprechend ein eigenes Array erhalten, das aus seinem Level-Schema erhalten wird. Während der Bewegung des Spielers werden seine Koordinaten darauf überprüft, dass sie sich in jedem Block befinden. Und wenn er sich in einem Block befindet, bekommen wir eine Kollision. Aber bei jeder Bewegung des Spielers müssen wir nicht nach Schnittpunkten mit allen Blöcken des Levels suchen, da es viele davon geben kann. Erstellen Sie ein Array nur der Blöcke, die dem Player am nächsten liegen.

 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; }, 

Hier am Eingang x, y sind die aktuellen Koordinaten des Spielers, dw, dh ist die Entfernung, in der Sie horizontal und vertikal nach Blöcken suchen möchten, beispielsweise 12 und 8 Meter. Mit anderen Worten, nehmen Sie alle Blöcke um den Spieler auf einem Quadrat von 24 x 16 Metern. Sie werden an der Suche nach Zusammenstößen teilnehmen. ap.v.lv.d [i] ist ein Element eines Arrays von Blöcken der aktuellen Ebene. Tatsächlich ist er selbst auch ein Array von 4 Zahlen, die die Grenzen eines Blocks definieren - [x1, y1, x2, y2], um das Quadrat zu überprüfen horizontal nehmen wir Elemente mit den Indizes 0 und 2 und vertikal - 1 und 3. Wenn es eine Übereinstimmung gibt, fügen Sie diesen Block der Liste für Kollisionen ap.v.coll hinzu.

Wenn sich der Spieler bewegt, aktualisieren wir diese Liste der Kollisionen. Um jedoch die Leistung zu sparen, tun wir dies nicht bei jedem Schritt (oder besser gesagt beim Rendern des Rahmens), sondern wenn der Spieler ein bestimmtes, etwas kleineres Quadrat verlässt, das in ap.v.collwStep und angegeben ist ap.v.collhStep, z. B. 8 und 4 Meter. Das heißt, wir werden das Kollisionsarray wieder zusammensetzen, wenn der Spieler einen bestimmten Pfad horizontal oder vertikal von seiner ursprünglichen Position aus passiert. Erinnern wir uns gleichzeitig an die Position, an der wir das Array wieder zusammengesetzt haben, um es für die nächste Iteration zu verwenden. pers [ax] - hier meinen wir mit ax die Koordinatenachse (ax), sie kann x oder z sein, abhängig von der Richtung des Korridors, entlang dem der Spieler geht.

 //   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); }; 

Warum solche Schwierigkeiten? Warum nicht die gesamte Reihe von Kollisionen auf der Ebene nutzen und kein Dampfbad nehmen? Tatsache ist, dass die Kollisionserkennung nach einem viel komplexeren Algorithmus durchgeführt wird und es unrentabel ist, die Kollision bei jedem Frame-Rendering mit absolut allen Ebenenblöcken und nicht mit den nächstgelegenen zu überprüfen. (Obwohl dies nicht korrekt ist.)

Die Definition von Kollisionen bei jedem Rendern eines Frames unter Verwendung des oben vorbereiteten Kollisionsarrays:

Code
 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 }; }, 


Hier sind x, y, xOld, yOld die neuen und aktuellen Koordinaten des Spielers. Neue werden auf Knopfdruck basierend auf einer bestimmten Bewegungsgeschwindigkeit berechnet, dh dies sind mögliche Koordinaten. Sie werden überprüft, um festzustellen, ob sie in einen Block der Kollisionsliste fallen. Wenn sie fallen, rollen sie zu den alten zurück und der Spieler passiert das Hindernis nicht. Und wenn sie nicht fallen, werden sie aktuell. pw2 und ph2 sind halb so breit und hoch wie der imaginäre Collider des Spielers (Spielerbreite / 2, Spielerhöhe / 2). Die Ausgabe wird ausgegeben, wenn eine horizontale und vertikale Kollision vorliegt (collw, collh), ob sich unter dem Player ein Supportblock befindet (supportf) - dies macht deutlich, ob die Herbstanimation weiter gestartet werden soll oder ob der Player einfach zu einem benachbarten Block gewechselt hat und so weiter. Fragen Sie einfach nicht, warum ich dort 0,001 addiert und 0,11 subtrahiert habe. Dies ist eine schreckliche Krücke, die verhindert, dass sie durch die Blöcke fällt und Jitter verursacht, wenn sie mit einem horizontalen Hindernis kollidiert ... Diese Funktion funktioniert, muss jedoch auf normale Weise neu geschrieben werden. Die Optimierung dieser Funktion fehlt ebenfalls noch.

Ich denke, bei Kollisionen lohnt es sich, hier zu enden.

Es ist schwer zu sagen, wie viel schneller oder vielleicht langsamer als das Raytracing meiner Methode ist, aber im letzteren Fall speichert Three.js auch eine Reihe von Objekten, die am Kollisionssystem beteiligt sind. Es ist nur so, dass die Kollisionen dort durch die Methode der Emission des Strahls und seinen Schnittpunkt mit den Ebenen der Seiten der Objekte und bei mir durch die Bestimmung bestimmt werden, ob die Koordinaten eines Objekts entlang jeder der beiden Achsen innerhalb des anderen liegen.

Das Spiel hat auch sich bewegende Objekte (Hai) und Markierungsobjekte, die eine Art Animation auslösen (z. B. löst der Kontakt mit Wasser die Bewegung eines Hais aus). Alle diese Objekte nehmen auch an Kollisionen teil, einige mit zeitlich variierenden Koordinaten. Dort ist seltsamerweise alles einfacher: Während der Bewegung des Objekts werden seine Koordinaten mit den Koordinaten des Spielers verglichen.


Gamepad


Im Allgemeinen ist die Pflege eines Javascript-Gamepads in einem Browser keine triviale Aufgabe. Es gibt keine Tastendruck- und Freigabeereignisse. Es gibt nur Ereignisse, die das Gerät und den Status verbinden und trennen, die durch regelmäßige Abfragen ermittelt und dann mit dem vorherigen verglichen werden können.

Ein Video, das die Funktionsweise des Gamepads in einem Browser auf einem Tablet unter Windows 8.1 und einem PC unter Windows 10 zeigt. Das Tablet ist jedoch ein altes, das 2014 veröffentlicht wurde, sodass die dynamische Beleuchtung im Spiel darauf ausgeschaltet wird.


Um das Gamepad abzufragen, wird eine Funktion verwendet, die alle 100 Millisekunden aufgerufen wird. Es wird mit der Funktion meiner Bibliothek m3d.lib.globalTimer.addEvent eingestellt.

 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(); }; }; } }); 

Hier ist globalTimer das von mir geschriebene Javascript setInterval Timer Event Management System. Dort wird einfach eine Reihe von Ereignissen zu einem bestimmten Array hinzugefügt, die in unterschiedlichen Intervallen aufgerufen werden müssen. Dann wird ein setInterval-Timer mit der Frequenz eingestellt, die dem Ereignis mit der höchsten Frequenz von allen entspricht. Der Timer fragt die Funktion m3d.lib.globalTimer.update () ab, die die Liste aller Ereignisse durchläuft und die Funktionen derjenigen ausführt, die ausgeführt wurden. Beim Hinzufügen oder Entfernen von Ereignissen kann sich auch die Intervallfrequenz ändern (z. B. wenn Sie das schnellste Ereignis löschen).

Das Spiel legt außerdem Handler für jede Gamepad-Taste fest: 'a' steht für die Achse (Axt), 'b' für die Schaltfläche (Schaltfläche) und 11 für die linke Abweichung entlang der horizontalen Achse des Kreuzes (als ob die Schaltfläche 1). 12 - die rechte Abweichung entlang der horizontalen Achse des Kreuzes (als ob seine Taste 2), 21 und 22 - für die vertikale Achse. Zum Beispiel:

['a', 11],
['b', 3]

bedeutet, dass die nächste Funktion gleichzeitig für die Abweichung entlang der horizontalen Achse nach links und für die Taste 3 (links) eingestellt wird. Nun, dann wird eine Funktion eingestellt, die ausgeführt wird, wenn die Taste gedrückt und dann losgelassen wird.

  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(); } ); 

Hier ist apcontrolsRenderStart () eine Funktion, die ein Rendering startet, wenn es noch nicht ausgeführt wird. Im Allgemeinen ist die Unterstützung für das Gamepad eng mit meiner m3d-Bibliothek verbunden. Wenn ich also alle Funktionen beschreibe, wird es sich sehr lange erstrecken ...

Ich gebe Ihnen nur einen Teil davon - das Gamepad, in dem ich die Initialisierung des Gamepads, die Installation von Handlern und die Statusabfrage auf einfachste Weise implementiert habe.

Code
 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 


Im Allgemeinen ist die Gamepad-Unterstützung im Spiel noch unvollständig: Es wird nur die Unterstützung für das einfachste Gamepad implementiert, nicht jedoch die, die beispielsweise in der XBox verwendet wird, da ich sie nicht habe. Wenn ich es bekomme, programmiere ich es und arbeite damit. Dort ist es möglich, die Geschwindigkeit des Charakters anzupassen, dh es ist möglich, sich mit jeder Geschwindigkeit im Bereich von Schritt zu Lauf zu bewegen. Dies wird erreicht, indem Bruchparameter von den Achsen genommen werden. Mein Gamepad gibt nur die Ganzzahlen -1 und 1 zurück. Außerdem hat mein Gamepad ein ekelhaftes Kreuz, und wenn es nach links oder rechts gedrückt wird, wird gleichzeitig nach unten oder oben gedrückt. Daher habe ich oben und unten am Kreuz nicht verwendet und es mit den Schaltflächen rechts auf dem Gamepad dupliziert ... Bis zur Veröffentlichung des Spiels habe ich vor, mehrere Profile von Gamepads zu erstellen. Außerdem werden beim Anschluss mehrerer Gamepads bisher nur letztere verwendet.

Reaktionsbildschirm


Das Spiel ist für ein Seitenverhältnis von 16: 9 ausgelegt. Ich habe jedoch eine automatische horizontale Anpassung von ± 10% hinzugefügt, damit im erweiterten Browserfenster keine solchen schwarzen Balken an den Seiten angezeigt werden:


Und es wäre so:


Im Vollbildmodus wird es echtes 16: 9 geben. Es wäre möglich, das Bild im Allgemeinen an jedes Seitenverhältnis des Browserfensters anzupassen, aber ich habe es nicht getan, da ein niedrig breites Fenster zu einem zu großen Betrachtungswinkel führen würde, was aus Sicht des Spiels nicht gut ist: entfernte Sackgassen, Objekte, Feinde sind sofort sichtbar und alles andere, was der Spieler noch nicht sehen muss. Daher beschränkte ich mich darauf, innerhalb von ± 10% von 16: 9 einzustellen. Bei schmalen Monitoren (4: 3) erkannte ich jedoch die Möglichkeit, durch Drücken der Y-Taste von 16: 9 in den Anpassungsmodus von 4: 3 auf 16: 9 zu wechseln. Aber nicht breiter - also auch hier, um das Gameplay nicht zu brechen. Das heißt, Sie können im klassischen Verhältnis 16: 9 spielen oder das Bild auf die Höhe des Fensters vergrößern, indem Sie es horizontal zuschneiden. Dies ist jedoch auch nicht sehr gut, beispielsweise in Arcade-Situationen, in denen etwas von der Seite auf den Spieler zufliegt. Für die Reaktion bleibt wenig Zeit. Sie können jedoch jederzeit schnell zum klassischen Modus zurückkehren.


Die Anpassung des Bildschirms sowie alle im Spiel verwendeten Hotkeys werden im folgenden Video gezeigt:


Tatsächlich wird das Seitenverhältnis in den Spieleinstellungen festgelegt.

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

Und im Spiel, wenn Sie Y drücken, wechselt es:

 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(); }; 

Meine Bibliothek hat ein Ereignis, das im Fenster zum Ändern der Größe hängt. Hier ist ein Fragment davon:

Code
 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 ist auch eine Funktion meiner Bibliothek, die ein Rechteck mit Parametern wie Zentrieren, Skalieren oder Zuschneiden in ein anderes Rechteck einschreibt und die neuen Abmessungen des beschrifteten Rechtecks ​​sowie die Größe der Felder anzeigt, die in diesem Prozess gebildet werden. Basierend auf diesen Informationen wird der div-Container des Fensters positioniert. 'Renderer' ist ein Blockkontextelement einer 3D-Szene. Dann wird die Leinwand dort gemäß den erhaltenen Parametern skaliert.

Die Benutzeroberfläche wird im Container auf einem separaten Canvas-Element angezeigt. Im Allgemeinen besteht der Dokumentbaum aus drei transparenten DIV-Blöcken mit absoluter Positionierung (je nach den Anforderungen des Spiels mehr oder weniger möglich): Unten befindet sich die Leinwand der 3D-Szene, oben die Leinwand für IU und oben die Animationselemente der Benutzeroberfläche und andere visuelle Effekte . Das heißt, die Benutzeroberfläche wird nicht in 3D gerendert, sondern auf ihrem Knavass oder ihrer Ebene. Die Aufgabe, Ebenen zu einem einzigen Bild zu kombinieren, bleibt dem Browser überlassen. Um mit der Benutzeroberfläche zu arbeiten, habe ich ein spezielles Objekt in der Bibliothek. Kurz gesagt - das Wesentliche ist wie folgt. Sprite-Listen mit UI-Elementen im PNG-Format mit Transparenz werden geladen. Von dort werden die notwendigen Elemente übernommen - Hintergründe, Schaltflächen. Und sie werden mit der Funktion js drawImage (img, ix, iy, iw, ih, x, y, w, h) auf der mittleren Leinwand gezeichnet. Das heißt, die erforderlichen Fragmente aus dem Bild werden an den erforderlichen Positionen auf dem Bildschirm angezeigt. Über den damit verbundenen Hintergründen werden Schaltflächen angezeigt. Alle Positionen und Größen werden in der UI-Konfiguration festgelegt. Beim Ändern der Fenstergröße werden die Positionen von Elementen auf der Zielleinwand (auf der sie angezeigt werden) neu berechnet, je nachdem, ob dieses oder jenes Element horizontal und vertikal zentriert oder an einer Ecke oder Fläche des Bildschirms ausgerichtet ist. Dadurch wird eine adaptive Benutzeroberfläche erstellt, die nicht vom Seitenverhältnis des Bildschirms abhängt. Es ist nur erforderlich, die minimal mögliche Auflösung horizontal und vertikal einzustellen und nicht darunter zu fallen, damit sich die Elemente nicht überlappen. Ich werde ein anderes Mal über die Benutzeroberfläche sprechen, da sich der Artikel als umfangreich herausstellte und ich immer noch an der Benutzeroberfläche arbeite, da mir noch viele Funktionen fehlen. Auf hochauflösenden Monitoren sieht die Benutzeroberfläche beispielsweise klein aus. Abhängig von der Bildschirmauflösung können Sie die Größe der Elemente mit einem bestimmten Koeffizienten multiplizieren. Andererseits werden die riesigen Schaltflächen auf dem Bildschirm möglicherweise nicht benötigt? Wenn die Bildschirmauflösung sehr groß ist, ist der Bildschirm selbst ziemlich groß.


Sie können dem Programmierer die Wahl lassen, ob die IU dynamisch mit der Größe des Fensters skaliert oder die Elemente in den Ecken verteilt werden sollen. Bei der dynamischen Größe gibt es auch eigene Fragen - zum Beispiel die „Seife“ einer Benutzeroberfläche, wenn sie in zu großem Maßstab angezeigt wird. Wenn Sie Sprites von Schnittstellenelementen in einer absichtlich großen Auflösung erstellen, nehmen diese viel Platz ein und sind wahrscheinlich auch für kleine Geräte nicht nützlich - sie benötigen immer noch keine großen Sprites, verbrauchen jedoch Speicher.

Ich denke das reicht für heute. Es gibt noch etwas zu überlegen, wie man dies oder das umsetzt. In der Zwischenzeit schweife ich für einige Zeit vom Programmieren ab und mache Werbung. Ich habe vor, an zwei Indie-Showcases teilzunehmen, und bin aktiv an der Förderung des Spiels in sozialen Netzwerken beteiligt, da ich im November die Crowdfunding-Plattform betreten möchte: Ich benötige Spezialisten auf dem Gebiet der 3D-Grafik und der Skelettanimation, um das Spiel zu vervollständigen.

In den folgenden Artikeln werde ich über Touch-Steuerelemente in Browsern für mobile Geräte sprechen - nicht jeder verbindet ein Gamepad oder eine Tastatur mit dem Tablet, über die Optimierung von 3D-Grafiken für Geräte mit geringem Stromverbrauch und vieles mehr.

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


All Articles