Erstellen eines pseudo-dreidimensionalen Rennspiels


Als Kind ging ich selten in Arcade-Arcade-Hallen, weil ich sie nicht wirklich brauchte, weil ich zu Hause tolle Spiele für C64 hatte ... aber es gibt drei Arcade-Spiele, für die ich immer Geld hatte - Donkey Kong, Dragons Lair und Outrun ...

... und ich habe Outrun wirklich geliebt - Geschwindigkeit, Hügel, Palmen und Musik, selbst bei der schwachen Version für den C64.


Also beschloss ich, ein pseudo-dreidimensionales Rennspiel der alten Schule im Stil von Outrun, Pitstop oder Pole zu schreiben. Ich habe nicht vor, ein vollständiges und vollständiges Spiel zusammenzustellen, aber es scheint mir interessant zu sein, die Mechanik, mit der diese Spiele ihre Tricks verwirklicht haben, erneut zu untersuchen. Kurven, Hügel, Sprites und ein Gefühl von Geschwindigkeit ...

Hier ist mein „Wochenendprojekt“, das am Wochenende letztendlich fünf oder sechs Wochen dauerte



Die spielbare Version ähnelt eher einer technischen Demo als einem echten Spiel. Wenn Sie eine echte pseudo-dreidimensionale Rasse erstellen möchten, ist dies die minimalste Grundlage, die Sie benötigen, um sich allmählich in ein Spiel zu verwandeln.

Es ist nicht poliert, etwas hässlich, aber voll funktionsfähig. Ich werde Ihnen zeigen, wie Sie es in vier einfachen Schritten selbst implementieren können.

Sie können auch spielen


Über die Leistung


Die Leistung dieses Spiels hängt stark von der Maschine / dem Browser ab. In modernen Browsern funktioniert es gut, insbesondere in Browsern mit Canvas-GPU-Beschleunigung, aber ein schlechter Grafiktreiber kann dazu führen, dass es einfriert. Im Spiel können Sie die Renderauflösung und den Renderabstand ändern.

Über die Codestruktur


Es kam vor, dass das Projekt in Javascript implementiert wurde (aufgrund der Einfachheit des Prototyping), aber es ist nicht beabsichtigt, die Techniken oder empfohlenen Techniken von Javascript zu demonstrieren. Zum besseren Verständnis ist das Javascript jedes Beispiels direkt in die HTML-Seite eingebettet (Horror!). Schlimmer noch, es verwendet globale Variablen und Funktionen.

Wenn ich ein echtes Spiel erstellen würde, wäre der Code viel strukturierter und optimierter, aber da dies eine technische Demo eines Rennspiels ist, habe ich mich entschieden, bei KISS zu bleiben .

Teil 1. Gerade Straßen.


Wie fangen wir an, ein pseudo-dreidimensionales Rennspiel zu entwickeln?

Nun, wir brauchen

  • Wiederholen Sie die Trigonometrie
  • Erinnern Sie sich an die Grundlagen der 3D-Projektion
  • Erstellen Sie eine Spielschleife
  • Sprite-Bilder herunterladen
  • Straßengeometrie erstellen
  • Hintergrund rendern
  • Rendern Sie die Straße
  • Auto rendern
  • Implementieren Sie die Tastaturunterstützung für die Maschinensteuerung

Aber bevor wir beginnen, lesen wir Lou's Pseudo 3d Page [ Übersetzung Habré] - die einzige Informationsquelle (die ich finden konnte) darüber, wie man ein Psevdotrohmernuyu-Rennspiel erstellt.

Lou's Artikel gelesen? Fein! Wir werden eine Variation seiner Realistic Hills mithilfe der 3D-projizierten Segmenttechnik erstellen. Wir werden dies schrittweise in den nächsten vier Teilen tun. Aber wir werden jetzt mit Version v1 beginnen und eine sehr einfache gerade Straßengeometrie erstellen, indem wir sie auf ein HTML5-Canvas-Element projizieren.

Die Demo zu sehen ist hier .

Ein bisschen Trigonometrie


Bevor wir mit der Implementierung beginnen, wollen wir uns anhand der Grundlagen der Trigonometrie daran erinnern, wie ein Punkt in der 3D-Welt auf einen 2D-Bildschirm projiziert wird.

Wenn Sie im einfachsten Fall keine Vektoren und Matrizen berühren, wird das Gesetz ähnlicher Dreiecke für die 3D-Projektion verwendet .

Wir verwenden die folgende Notation:

  • h = Kamerahöhe
  • d = Abstand von Kamera zu Bildschirm
  • z = Abstand von Kamera zu Auto
  • y = Bildschirm y- Koordinate

Dann können wir das Gesetz ähnlicher Dreiecke verwenden, um zu berechnen

y = h * d / z

wie im Diagramm gezeigt:


Sie können auch ein ähnliches Diagramm in einer Draufsicht anstelle einer Seitenansicht zeichnen und eine ähnliche Gleichung ableiten, um die X- Koordinate des Bildschirms zu berechnen :

x = w * d / z

Wobei w = die halbe Breite der Straße (von der Kamera bis zum Straßenrand).

Wie Sie sehen können, skalieren wir für x und y um einen Faktor

d / z

Koordinatensystem


In Form eines Diagramms sieht es schön und einfach aus, aber wenn Sie mit dem Codieren beginnen, können Sie ein wenig verwirrt werden, da wir beliebige Namen gewählt haben und nicht klar ist, wie wir die Koordinaten der 3D-Welt angegeben haben und wie die Koordinaten des 2D-Bildschirms lauten. Wir gehen auch davon aus, dass sich die Kamera im Zentrum des Ursprungs der Welt befindet, obwohl sie in Wirklichkeit der Maschine folgen wird.

Wenn Sie formeller vorgehen, müssen wir Folgendes tun:

  1. Konvertierung von Weltkoordinaten in Bildschirmkoordinaten
  2. Projizieren von Kamerakoordinaten auf eine normalisierte Projektionsebene
  3. Skalieren der projizierten Koordinaten auf die Koordinaten des physischen Bildschirms (in unserem Fall ist dies Leinwand)


Hinweis: Im vorliegenden 3D-System wird die Rotationsstufe zwischen den Stufen 1 und 2 ausgeführt . Da wir jedoch die Kurven simulieren, benötigen wir keine Rotation.

Projektion


Die formalen Projektionsgleichungen können wie folgt dargestellt werden:


  • Umrechnungsgleichungen ( übersetzen ) Punkt berechnet relativ zur Kammer
  • Die Projektionsgleichungen ( Projekt ) sind Variationen des oben gezeigten „Gesetzes ähnlicher Dreiecke“.
  • Skalierungsgleichungen ( Skalierung ) berücksichtigen den Unterschied zwischen:
    • Mathe , wobei 0,0 in der Mitte liegt und die y-Achse oben ist, und
    • , 0,0 , y :


: 3d- Vector Matrix 3d-, , WebGL ( )… . Outrun.



Das letzte Puzzleteil ist eine Möglichkeit, d zu berechnen - den Abstand von der Kamera zur Projektionsebene.

Anstatt nur einen fest eingestellten Wert von d zu schreiben , wäre es sinnvoller, ihn aus dem gewünschten vertikalen Sichtfeld zu berechnen. Dank dessen können wir die Kamera bei Bedarf "zoomen".

Wenn wir annehmen, dass wir auf eine normalisierte Projektionsebene projizieren, deren Koordinaten im Bereich von -1 bis +1 liegen, kann d wie folgt berechnet werden:

d = 1 / tan (fov / 2)

Indem wir fov als eine (von vielen) Variablen definieren, können wir den Bereich anpassen, um den Rendering-Algorithmus zu optimieren .

Javascript-Codestruktur


Zu Beginn des Artikels habe ich bereits gesagt, dass der Code nicht ganz den Richtlinien zum Schreiben von Javascript entspricht - es ist eine „schnelle und schmutzige“ Demo mit einfachen globalen Variablen und Funktionen. Da ich jedoch vier separate Versionen erstellen werde (gerade, Kurven, Hügel und Sprites), werde ich einige wiederverwendbare Methoden common.jsin den folgenden Modulen speichern :

  • Dom ist ein paar kleinere DOM-Hilfsfunktionen.
  • Util - allgemeine Dienstprogramme, hauptsächlich mathematische Hilfsfunktionen.
  • Spiel - Allgemeine Spielunterstützungsfunktionen wie Bild-Downloader und Spieleschleife.
  • Render - Helferfunktionen auf Leinwand Rendering.

Ich werde Methoden common.jsnur dann im Detail erklären, wenn sie sich auf das Spiel selbst beziehen und nicht nur mathematische Hilfs- oder DOM-Funktionen sind. Hoffentlich wird aus dem Namen und dem Kontext klar, was die Methoden tun sollten.

Der Quellcode befindet sich wie gewohnt in der endgültigen Dokumentation.

Einfache Spielschleife


Bevor wir etwas rendern, benötigen wir eine Spielschleife. Wenn Sie einen meiner vorherigen Artikel über Spiele ( Pong , Breakout , Tetris , Schlangen oder Boulderdash ) gelesen haben , haben Sie bereits Beispiele meines Lieblingsspielzyklus mit einem festgelegten Zeitschritt gesehen .

Ich werde nicht tief in die Details gehen und einfach einen Teil des Codes aus früheren Spielen wiederverwenden, um mit requestAnimationFrame eine Spielschleife mit einem festen Zeitschritt zu erstellen .

Das Prinzip ist, dass jedes meiner vier Beispiele Game.run(...)seine eigenen Versionen aufrufen und verwenden kann

  • update - Aktualisierung der Spielwelt mit einem festgelegten Zeitschritt.
  • render - Aktualisierung der Spielwelt, wenn der Browser dies zulässt.

run: function(options) {

  Game.loadImages(options.images, function(images) {

    var update = options.update,    // method to update game logic is provided by caller
        render = options.render,    // method to render the game is provided by caller
        step   = options.step,      // fixed frame step (1/fps) is specified by caller
        now    = null,
        last   = Util.timestamp(),
        dt     = 0,
        gdt    = 0;

    function frame() {
      now = Util.timestamp();
      dt  = Math.min(1, (now - last) / 1000); // using requestAnimationFrame have to be able to handle large delta's caused when it 'hibernates' in a background or non-visible tab
      gdt = gdt + dt;
      while (gdt > step) {
        gdt = gdt - step;
        update(step);
      }
      render();
      last = now;
      requestAnimationFrame(frame);
    }
    frame(); // lets get this party started
  });
}

Auch dies ist ein Remake von Ideen aus meinen vorherigen Canvas-Spielen. Wenn Sie also nicht verstehen, wie die Spieleschleife funktioniert, kehren Sie zu einem der vorherigen Artikel zurück.

Bilder und Sprites


Bevor der Spielzyklus beginnt, laden wir zwei separate Spritesheets (Sprite Sheets):

  • Hintergrund - drei Parallaxenschichten für Himmel, Hügel und Bäume
  • Sprites - Maschinen-Sprites (plus Bäume und Werbetafeln, die der endgültigen Version hinzugefügt werden sollen)


Das Sprite-Blatt wurde mit einer kleinen Aufgabe Rake und Ruby Gem Sprite-Factory erstellt .

Diese Aufgabe generiert die kombinierten Sprite-Blätter sowie die Koordinaten x, y, w, h, die in den Konstanten BACKGROUNDund gespeichert werden SPRITES.

Hinweis: Ich habe die Hintergründe mit Inkscape erstellt. Die meisten Sprites sind Grafiken, die aus der alten Outrun-Version für Genesis stammen und als Trainingsbeispiele verwendet werden.

Spielvariablen


Zusätzlich zu Bildern von Hintergründen und Sprites benötigen wir verschiedene Spielvariablen, nämlich:

var fps           = 60;                      // how many 'update' frames per second
var step          = 1/fps;                   // how long is each frame (in seconds)
var width         = 1024;                    // logical canvas width
var height        = 768;                     // logical canvas height
var segments      = [];                      // array of road segments
var canvas        = Dom.get('canvas');       // our canvas...
var ctx           = canvas.getContext('2d'); // ...and its drawing context
var background    = null;                    // our background image (loaded below)
var sprites       = null;                    // our spritesheet (loaded below)
var resolution    = null;                    // scaling factor to provide resolution independence (computed)
var roadWidth     = 2000;                    // actually half the roads width, easier math if the road spans from -roadWidth to +roadWidth
var segmentLength = 200;                     // length of a single segment
var rumbleLength  = 3;                       // number of segments per red/white rumble strip
var trackLength   = null;                    // z length of entire track (computed)
var lanes         = 3;                       // number of lanes
var fieldOfView   = 100;                     // angle (degrees) for field of view
var cameraHeight  = 1000;                    // z height of camera
var cameraDepth   = null;                    // z distance camera is from screen (computed)
var drawDistance  = 300;                     // number of segments to draw
var playerX       = 0;                       // player x offset from center of road (-1 to 1 to stay independent of roadWidth)
var playerZ       = null;                    // player relative z distance from camera (computed)
var fogDensity    = 5;                       // exponential fog density
var position      = 0;                       // current camera Z position (add playerZ to get player's absolute Z position)
var speed         = 0;                       // current speed
var maxSpeed      = segmentLength/step;      // top speed (ensure we can't move more than 1 segment in a single frame to make collision detection easier)
var accel         =  maxSpeed/5;             // acceleration rate - tuned until it 'felt' right
var breaking      = -maxSpeed;               // deceleration rate when braking
var decel         = -maxSpeed/5;             // 'natural' deceleration rate when neither accelerating, nor braking
var offRoadDecel  = -maxSpeed/2;             // off road deceleration is somewhere in between
var offRoadLimit  =  maxSpeed/4;             // limit when off road deceleration no longer applies (e.g. you can always go at least this speed even when off road)

Einige von ihnen können mithilfe von UI-Steuerelementen angepasst werden, um kritische Werte während der Programmausführung zu ändern, sodass Sie sehen können, wie sie sich auf das Rendern der Straße auswirken. Andere werden aus benutzerdefinierten UI-Werten in der Methode neu berechnet reset().

Wir verwalten Ferrari


Wir führen Tastenkombinationen für durch Game.run, die eine einfache Tastatureingabe ermöglichen, mit der Variablen festgelegt oder zurückgesetzt werden, die die aktuellen Aktionen des Spielers melden:

Game.run({
  ...
  keys: [
    { keys: [KEY.LEFT,  KEY.A], mode: 'down', action: function() { keyLeft   = true;  } },
    { keys: [KEY.RIGHT, KEY.D], mode: 'down', action: function() { keyRight  = true;  } },
    { keys: [KEY.UP,    KEY.W], mode: 'down', action: function() { keyFaster = true;  } },
    { keys: [KEY.DOWN,  KEY.S], mode: 'down', action: function() { keySlower = true;  } },
    { keys: [KEY.LEFT,  KEY.A], mode: 'up',   action: function() { keyLeft   = false; } },
    { keys: [KEY.RIGHT, KEY.D], mode: 'up',   action: function() { keyRight  = false; } },
    { keys: [KEY.UP,    KEY.W], mode: 'up',   action: function() { keyFaster = false; } },
    { keys: [KEY.DOWN,  KEY.S], mode: 'up',   action: function() { keySlower = false; } }
  ],
  ...
}

Der Status des Spielers wird durch die folgenden Variablen gesteuert:

  • Geschwindigkeit - aktuelle Geschwindigkeit.
  • Position - Die aktuelle Z-Position auf der Spur. Beachten Sie, dass dies eine Kameraposition ist, kein Ferrari.
  • playerX - die aktuelle Position des Spielers auf X auf der Straße. Normalisiert im Bereich von -1 bis +1, um nicht vom tatsächlichen Wert abzuhängen roadWidth.

Diese Variablen werden in der Methode festgelegt update, die die folgenden Aktionen ausführt:

  • Updates positionbasierend auf dem aktuellen speed.
  • wird aktualisiert, playerXwenn Sie die linke oder rechte Taste drücken.
  • erhöht sich, speedwenn die Aufwärts-Taste gedrückt wird.
  • nimmt ab, speedwenn die Abwärtstaste gedrückt wird.
  • reduziert sich, speedwenn die Auf- und Ab-Tasten nicht gedrückt werden.
  • reduziert sich, speedwenn es playerXsich am Straßenrand und im Gras befindet.

Bei direkten Straßen ist die Methode updateganz klar und einfach:

function update(dt) {

  position = Util.increase(position, dt * speed, trackLength);

  var dx = dt * 2 * (speed/maxSpeed); // at top speed, should be able to cross from left to right (-1 to 1) in 1 second

  if (keyLeft)
    playerX = playerX - dx;
  else if (keyRight)
    playerX = playerX + dx;

  if (keyFaster)
    speed = Util.accelerate(speed, accel, dt);
  else if (keySlower)
    speed = Util.accelerate(speed, breaking, dt);
  else
    speed = Util.accelerate(speed, decel, dt);

  if (((playerX < -1) || (playerX > 1)) && (speed > offRoadLimit))
    speed = Util.accelerate(speed, offRoadDecel, dt);

  playerX = Util.limit(playerX, -2, 2);     // dont ever let player go too far out of bounds
  speed   = Util.limit(speed, 0, maxSpeed); // or exceed maxSpeed

}

Keine Sorge, es wird viel schwieriger, wenn wir in der fertigen Version Sprites und Kollisionserkennung hinzufügen.

Straßengeometrie


Bevor wir die Spielwelt rendern können, müssen wir ein Array von segmentsIn in der Methode erstellen resetRoad().

Jedes dieser Straßensegmente wird letztendlich von seinen Weltkoordinaten projiziert, sodass es in Bildschirmkoordinaten zu einem 2D-Polygon wird. Für jedes Segment speichern wir zwei Punkte, p1 ist die Mitte der Kante, die der Kamera am nächsten liegt, und p2 ist die Mitte der Kante, die am weitesten von der Kamera entfernt ist.


Genau genommen ist p2 jedes Segments identisch mit p1 des vorherigen Segments, aber es scheint mir, dass es einfacher ist, sie als separate Punkte zu speichern und jedes Segment separat zu konvertieren.

Wir bleiben getrennt, rumbleLengthweil wir schöne detaillierte Kurven und Hügel haben können, aber gleichzeitig horizontale Streifen. Wenn jedes nachfolgende Segment eine andere Farbe hat, wird ein schlechter Strobe-Effekt erzeugt. Daher möchten wir viele kleine Segmente haben, diese aber zu separaten horizontalen Streifen zusammenfassen.

function resetRoad() {
  segments = [];
  for(var n = 0 ; n < 500 ; n++) { // arbitrary road length
    segments.push({
       index: n,
       p1: { world: { z:  n   *segmentLength }, camera: {}, screen: {} },
       p2: { world: { z: (n+1)*segmentLength }, camera: {}, screen: {} },
       color: Math.floor(n/rumbleLength)%2 ? COLORS.DARK : COLORS.LIGHT
    });
  }

  trackLength = segments.length * segmentLength;
}

Wir initialisieren p1 und p2 nur mit den Weltkoordinaten z , weil wir nur gerade Straßen brauchen. Die y- Koordinaten sind immer 0 und die x- Koordinaten hängen immer vom skalierten Wert ab +/- roadWidth. Wenn wir später Kurven und Hügel hinzufügen, ändert sich dieser Teil.

Wir werden auch leere Objekte festlegen, um Darstellungen dieser Punkte in der Kamera und auf dem Bildschirm zu speichern, um nicht in jedem eine Reihe von temporären Objekten zu erstellen render. Um die Speicherbereinigung zu minimieren, müssen wir vermeiden, Objekte innerhalb der Spielschleife zuzuweisen.

Wenn das Auto das Ende der Straße erreicht, kehren wir einfach zum Anfang des Zyklus zurück. Um dies zu vereinfachen, erstellen wir eine Methode, um ein Segment für einen beliebigen Wert von Z zu finden, auch wenn es über die Länge der Straße hinausgeht:

function findSegment(z) {
  return segments[Math.floor(z/segmentLength) % segments.length];
}

Hintergrund-Rendering


Die Methode render()beginnt mit dem Rendern des Hintergrundbilds. In den folgenden Abschnitten, in denen wir Kurven und Hügel hinzufügen, benötigen wir den Hintergrund, um das Parallaxen-Scrollen durchzuführen. Daher beginnen wir jetzt, uns in diese Richtung zu bewegen und den Hintergrund als drei separate Ebenen zu rendern:

function render() {

  ctx.clearRect(0, 0, width, height);

  Render.background(ctx, background, width, height, BACKGROUND.SKY);
  Render.background(ctx, background, width, height, BACKGROUND.HILLS);
  Render.background(ctx, background, width, height, BACKGROUND.TREES);

  ...

Straßenrendering


Anschließend iteriert die Renderfunktion über alle Segmente und Projekte p1 und p2 jedes Segments von Weltkoordinaten zu Bildschirmkoordinaten, schneidet das Segment bei Bedarf ab und rendert es auf andere Weise:

  var baseSegment = findSegment(position);
  var maxy        = height;
  var n, segment;
  for(n = 0 ; n < drawDistance ; n++) {

    segment = segments[(baseSegment.index + n) % segments.length];

    Util.project(segment.p1, (playerX * roadWidth), cameraHeight, position, cameraDepth, width, height, roadWidth);
    Util.project(segment.p2, (playerX * roadWidth), cameraHeight, position, cameraDepth, width, height, roadWidth);

    if ((segment.p1.camera.z <= cameraDepth) || // behind us
        (segment.p2.screen.y >= maxy))          // clip by (already rendered) segment
      continue;

    Render.segment(ctx, width, lanes,
                   segment.p1.screen.x,
                   segment.p1.screen.y,
                   segment.p1.screen.w,
                   segment.p2.screen.x,
                   segment.p2.screen.y,
                   segment.p2.screen.w,
                   segment.color);

    maxy = segment.p2.screen.y;
  }

Oben haben wir bereits die Berechnungen gesehen, die für die Projektion eines Punktes erforderlich sind. Die Javascript-Version kombiniert Transformation, Projektion und Skalierung in einer Methode:

project: function(p, cameraX, cameraY, cameraZ, cameraDepth, width, height, roadWidth) {
  p.camera.x     = (p.world.x || 0) - cameraX;
  p.camera.y     = (p.world.y || 0) - cameraY;
  p.camera.z     = (p.world.z || 0) - cameraZ;
  p.screen.scale = cameraDepth/p.camera.z;
  p.screen.x     = Math.round((width/2)  + (p.screen.scale * p.camera.x  * width/2));
  p.screen.y     = Math.round((height/2) - (p.screen.scale * p.camera.y  * height/2));
  p.screen.w     = Math.round(             (p.screen.scale * roadWidth   * width/2));
}

Zusätzlich zur Berechnung der Bildschirme x und y für jeden Punkt p1 und p2 verwenden wir dieselben Projektionsberechnungen, um die projizierte Breite ( w ) des Segments zu berechnen .

Mit den x- und y- Koordinaten des Bildschirms der Punkte p1 und p2 sowie der projizierten Straßenbreite w können wir mit Hilfe einer Hilfsfunktion ganz einfach Render.segmentalle Polygone berechnen, die zum Rendern von Gras, Straße, horizontalen Streifen und Trennlinien erforderlich sind, unter Verwendung der allgemeinen Hilfsfunktion Render.polygon (siehe . common.js) .

Auto-Rendering


Das Letzte, was die Methode benötigt, renderist ein Ferrari-Rendering:

  Render.player(ctx, width, height, resolution, roadWidth, sprites, speed/maxSpeed,
                cameraDepth/playerZ,
                width/2,
                height);

Diese Methode wird aufgerufen playerund nicht car, da in der endgültigen Version des Spiels andere Autos unterwegs sein werden und wir den Ferrari des Spielers von anderen Autos trennen möchten.

Die Hilfsfunktion Render.playerverwendet die Canvas-Methode, die drawImagezum Rendern des Sprites aufgerufen wird , nachdem sie zuvor mit derselben Projektionsskalierung skaliert wurde, die zuvor verwendet wurde:

d / z

Wobei z in diesem Fall der relative Abstand von der Maschine zur Kamera ist, der in der Variablen playerZ gespeichert ist .

Darüber hinaus „wackelt“ die Funktion das Auto bei hohen Geschwindigkeiten ein wenig und fügt der Skalierungsgleichung je nach Geschwindigkeit / Höchstgeschwindigkeit ein wenig Zufälligkeit hinzu .

Und hier ist was wir haben:


Fazit


Wir haben ziemlich viel Arbeit geleistet, um ein System mit geraden Straßen zu schaffen. Wir fügten hinzu

  • generisches Hilfsmodul dom
  • Verwenden Sie das allgemeine Mathematikmodul
  • Render allgemeinen Leinwand Helfer Modul ...
  • ... einschließlich Render.segment, Render.polygonundRender.sprite
  • Spielzyklus mit fester Tonhöhe
  • Bild-Downloader
  • Tastaturhandler
  • Parallaxenhintergrund
  • Sprite Sheet mit Autos, Bäumen und Werbetafeln
  • rudimentäre Geometrie der Straße
  • Methode update()zur Steuerung der Maschine
  • Methode render()zum Rendern von Hintergrund-, Straßen- und Spielerautos
  • HTML5-Tag <audio>mit Rennmusik (versteckter Bonus!)

... was uns eine gute Grundlage für die weitere Entwicklung gab.

Teil 2. Kurven.



In diesem Teil werden wir detaillierter erklären, wie Kurven funktionieren.

Im vorherigen Teil haben wir die Geometrie der Straße in Form einer Reihe von Segmenten zusammengestellt, von denen jedes Weltkoordinaten aufweist, die relativ zur Kamera transformiert und dann auf den Bildschirm projiziert werden.

Wir brauchten nur die Weltkoordinate z für jeden Punkt, da auf geraden Straßen sowohl x als auch y gleich Null waren.


Wenn wir ein voll funktionsfähiges 3D-System erstellen würden, könnten wir die Kurven implementieren, indem wir die x- und z- Streifen der oben gezeigten Polygone berechnen . Diese Art von Geometrie wird jedoch ziemlich schwierig zu berechnen sein, und dafür wird es notwendig sein, die 3D-Rotationsstufe zu den Projektionsgleichungen hinzuzufügen ...

... wenn wir diesen Weg gehen würden, wäre es besser, WebGL oder seine Analoga zu verwenden, aber dieses Projekt hat keine anderen Aufgaben für unser Projekt. Wir wollen nur pseudo-dreidimensionale Tricks der alten Schule verwenden, um Kurven zu simulieren.

Daher werden Sie wahrscheinlich überrascht sein zu erfahren, dass wir die x- Koordinaten der Straßensegmente überhaupt nicht berechnen ...

Stattdessen verwenden wir Lus Rat :

"Um die Straße zu krümmen, ändern Sie einfach die Position der Mittellinie der Kurvenform. Ausgehend vom unteren Bildschirmrand nimmt die Verschiebung der Straßenmitte nach links oder rechts allmählich zu . "

In unserem Fall ist die Mittellinie der Wert cameraX, der an die Projektionsberechnungen übergeben wird. Dies bedeutet, dass Sie bei render()jedem Straßensegment die Kurven simulieren können, indem Sie den Wert cameraXum einen allmählich ansteigenden Wert verschieben.


Um zu wissen, wie viel verschoben werden muss, müssen wir in jedem Segment einen Wert speichern curve. Dieser Wert gibt an, um wie viel das Segment von der Mittellinie der Kamera verschoben werden soll. Sie wird sein:

  • negativ für linksdrehende Kurven
  • positiv für Kurven nach rechts
  • weniger für glatte Kurven
  • mehr für scharfe Kurven

Die Werte selbst werden ganz willkürlich gewählt; Durch Versuch und Irrtum können wir gute Werte finden, bei denen die Kurven „korrekt“ zu sein scheinen:

var ROAD = {
  LENGTH: { NONE: 0, SHORT:  25, MEDIUM:  50, LONG:  100 }, // num segments
  CURVE:  { NONE: 0, EASY:    2, MEDIUM:   4, HARD:    6 }
};

Zusätzlich zur Auswahl guter Werte für die Kurven müssen Lücken in den Übergängen vermieden werden, wenn sich die Linie in eine Kurve verwandelt (oder umgekehrt). Dies kann durch Erweichen beim Betreten und Verlassen der Kurven erreicht werden. Dazu erhöhen (oder verringern) wir den Wert curvefür jedes Segment schrittweise mithilfe herkömmlicher Glättungsfunktionen, bis der gewünschte Wert erreicht ist:

easeIn:    function(a,b,percent) { return a + (b-a)*Math.pow(percent,2);                           },
easeOut:   function(a,b,percent) { return a + (b-a)*(1-Math.pow(1-percent,2));                     },
easeInOut: function(a,b,percent) { return a + (b-a)*((-Math.cos(percent*Math.PI)/2) + 0.5);        },

Das heißt nun unter Berücksichtigung der Funktion, der Geometrie ein Segment hinzuzufügen ...

function addSegment(curve) {
  var n = segments.length;
  segments.push({
     index: n,
        p1: { world: { z:  n   *segmentLength }, camera: {}, screen: {} },
        p2: { world: { z: (n+1)*segmentLength }, camera: {}, screen: {} },
     curve: curve,
     color: Math.floor(n/rumbleLength)%2 ? COLORS.DARK : COLORS.LIGHT
  });
}

Wir können eine Methode für den reibungslosen Ein- und Ausstieg von einer kurvenreichen Straße erstellen:

function addRoad(enter, hold, leave, curve) {
  var n;
  for(n = 0 ; n < enter ; n++)
    addSegment(Util.easeIn(0, curve, n/enter));
  for(n = 0 ; n < hold  ; n++)
    addSegment(curve);
  for(n = 0 ; n < leave ; n++)
    addSegment(Util.easeInOut(curve, 0, n/leave));
}

... und oben können Sie zusätzliche Geometrie festlegen, z. B. S-förmige Kurven:

function addSCurves() {
  addRoad(ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM,  -ROAD.CURVE.EASY);
  addRoad(ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM,   ROAD.CURVE.MEDIUM);
  addRoad(ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM,   ROAD.CURVE.EASY);
  addRoad(ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM,  -ROAD.CURVE.EASY);
  addRoad(ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM,  -ROAD.CURVE.MEDIUM);
}

Änderungen an der update () -Methode


Die einzigen Änderungen, die an der Methode vorgenommen update()werden müssen, sind das Aufbringen einer Art Zentrifugalkraft, wenn sich die Maschine entlang einer Kurve bewegt.

Wir legen einen beliebigen Faktor fest, der nach unseren Wünschen angepasst werden kann.

var centrifugal = 0.3;   // centrifugal force multiplier when going around curves

Und dann aktualisieren wir einfach die Position playerXbasierend auf der aktuellen Geschwindigkeit, dem Kurvenwert und dem Fliehkraftmultiplikator:

playerX = playerX - (dx * speedPercent * playerSegment.curve * centrifugal);

Kurvenwiedergabe


Wir haben oben gesagt, dass Sie simulierte Kurven rendern können, indem Sie den cameraXin Projektionsberechnungen verwendeten Wert während der Ausführung render()jedes Straßensegments verschieben.


Dazu speichern wir die Antriebsvariable dx , die für jedes Segment um einen Wert erhöht wird curve, sowie die Variable x , die als Versatz des cameraXin Projektionsberechnungen verwendeten Werts verwendet wird.

Um die Kurven zu implementieren, benötigen wir Folgendes:

  • Verschieben Sie die Projektion p1 jedes Segments um x
  • Verschieben Sie die Projektion p2 jedes Segments um x + dx
  • Erhöhen Sie x für das nächste Segment um dx

Schließlich müssen wir dx mit dem interpolierten Wert der Kurve der aktuellen Basissegmente initialisieren , um zerrissene Übergänge beim Überschreiten der Segmentgrenzen zu vermeiden .

Ändern Sie die Methode render()wie folgt:

var baseSegment = findSegment(position);
var basePercent = Util.percentRemaining(position, segmentLength);
var dx = - (baseSegment.curve * basePercent);
var x  = 0;
for(n = 0 ; n < drawDistance ; n++) {

  ...

  Util.project(segment.p1, (playerX * roadWidth) - x,      cameraHeight, position - (segment.looped ? trackLength : 0), cameraDepth, width, height, roadWidth);
  Util.project(segment.p2, (playerX * roadWidth) - x - dx, cameraHeight, position - (segment.looped ? trackLength : 0), cameraDepth, width, height, roadWidth);

  x  = x + dx;
  dx = dx + segment.curve;

  ...
}

Parallax Scrolling Hintergrund


Schließlich müssen wir die Parallaxen-Hintergrundebenen scrollen und den Versatz für jede Ebene speichern ...

var skySpeed    = 0.001; // background sky layer scroll speed when going around curve (or up hill)
var hillSpeed   = 0.002; // background hill layer scroll speed when going around curve (or up hill)
var treeSpeed   = 0.003; // background tree layer scroll speed when going around curve (or up hill)
var skyOffset   = 0;     // current sky scroll offset
var hillOffset  = 0;     // current hill scroll offset
var treeOffset  = 0;     // current tree scroll offset

... und im Laufe der Zeit in update()Abhängigkeit vom Kurvenwert des aktuellen Spielersegments und seiner Geschwindigkeit erhöhen ...

skyOffset  = Util.increase(skyOffset,  skySpeed  * playerSegment.curve * speedPercent, 1);
hillOffset = Util.increase(hillOffset, hillSpeed * playerSegment.curve * speedPercent, 1);
treeOffset = Util.increase(treeOffset, treeSpeed * playerSegment.curve * speedPercent, 1);

... und verwenden Sie dann diesen Offset, wenn Sie render()Hintergrundebenen erstellen.

Render.background(ctx, background, width, height, BACKGROUND.SKY,   skyOffset);
Render.background(ctx, background, width, height, BACKGROUND.HILLS, hillOffset);
Render.background(ctx, background, width, height, BACKGROUND.TREES, treeOffset);

Fazit


Hier erhalten wir also die gefälschten pseudo-dreidimensionalen Kurven:


Der Hauptteil des Codes, den wir hinzugefügt haben, besteht darin, die Geometrie der Straße mit dem entsprechenden Wert zu erstellen curve. Das Hinzufügen von Zentrifugalkraft während der Zeit ist update()viel einfacher.

Das Rendern von Kurven wird in nur wenigen Codezeilen ausgeführt, es kann jedoch schwierig sein, zu verstehen (und zu beschreiben), was genau hier passiert. Es gibt viele Möglichkeiten, Kurven zu simulieren, und es ist sehr einfach zu wandern, wenn sie in eine Sackgasse implementiert werden. Es ist noch einfacher, sich von einer externen Aufgabe mitreißen zu lassen und zu versuchen, alles „richtig“ zu machen. Bevor Sie dies erkennen, werden Sie beginnen, ein voll funktionsfähiges 3D-System mit Matrizen, Rotationen und realer 3D-Geometrie zu erstellen ... was, wie gesagt, nicht unsere Aufgabe ist.

Als ich diesen Artikel schrieb, war ich mir sicher, dass es definitiv Probleme bei der Implementierung der Kurven gab. Beim Versuch, den Algorithmus zu visualisieren, habe ich nicht verstanden, warum ich zwei Werte der dx- und x- Laufwerke anstelle von einem benötigte ... und wenn ich etwas nicht vollständig erklären kann, ist irgendwo etwas schiefgegangen ...

... aber die Arbeitszeit des Projekts "läuft" das Wochenende “ist fast abgelaufen, und ehrlich gesagt scheinen mir die Kurven ziemlich schön zu sein, und am Ende ist dies das Wichtigste.

All Articles