Januar 2016

Band 31, Nummer 1

Entwicklung von Spielen – Babylon.js: Erweiterte Funktionen zur Verbesserung Ihres ersten Webspiels

Von Raanan Weber

In der Dezemberausgabe habe ich dieses Tutorial mit einer Besprechung der grundlegenden Bausteine von Babylon.js begonnen, einer WebGL-basierten 3D-Spiele-Engine (msdn.com/magazine/mt595753). Ich habe damit begonnen, mithilfe der von Babylon.js bereitgestellten Tools ein sehr einfaches Bowlingspiel zu entwerfen. Bisher enthält das Spiel die benötigten Objekte – eine Bowlingkugel (Ball), die Lauffläche, die Rinnen und 10 Pins.

Jetzt zeige ich Ihnen, wie Sie dem Spiel Leben einhauchen – wie man den Ball wirft, die Pins trifft, Audioeffekte hinzufügt, verschiedene Kameraansichten bereitstellt und mehr.

Naturgemäß baut dieser Teil des Tutorials auf dem Code aus dem ersten Teil auf und erweitert ihn. Um zwischen beiden Teilen zu unterscheiden, habe ich eine neue JavaScript-Datei erstellt, die den Code enthält, der in diesem Teil verwendet wird. Selbst, wenn die Funktionalität eines bestimmten Objekts erweitert wird (etwa der Szene oder der Hauptkamera), implementiere ich dies nicht in der Funktion, mit der das Objekt im ersten Teil erstellt wurde. Die einzige Funktion, die ich erweitern muss, ist „init“, die sämtliche für beide Teile des Tutorials benötigten Variablen enthält.

Dies ist zu Ihrer Bequemlichkeit geschehen. Beim Implementieren Ihres eigenen Spiels können Sie natürlich jedes gewünschte Programmierparadigma und jede gültige Syntax verwenden. Ich empfehle Ihnen, TypeScript mit seinem objektorientierten Programmieransatz einen Versuch zu gönnen. Es eignet sich hervorragend, um ein Projekt zu strukturieren.

Während ich diesen Artikel schrieb, wurde eine neue Version von Babylon.js veröffentlicht. Ich verwendet hier weiterhin Version 2.1. Informationen über die Neuerungen in Babylon.js 2.2 finden Sie unter bit.ly/1RC9k6e.

Systemeigene Kollisionserkennung

Die im Spiel verwendete Hauptkamera ist die „Free Camera“, die dem Spieler eine Fahrt durch die gesamte 3D-Szene mithilfe von Maus und Tastatur ermöglicht. Ohne bestimmte Änderungen könnte die Kamera aber durch die Szene hindurchschwimmen, durch Wände gehen, ja sogar den Boden und die Lauffläche durchstoßen. Um einen realistischen Spieleindruck zu erzielen, sollte der Spieler nur in der Lage sein, sich auf dem Boden und der Lauffläche zu bewegen.

Um das zu verwirklichen, verwende ich das interne System zur Kollisionserkennung von Babylon. Das Kollisionserkennungssystem hindert zwei Objekte daran, sich miteinander zu vermischen. Es verfügt auch über ein Schwerkraftfeature, das den Spieler daran hindert, sich in die Luft zu erheben, wenn er vorwärts geht und dabei nach oben blickt.

Aktivieren wir zuerst die Kollisionserkennung und die Schwerkraft:

function enableCameraCollision(camera, scene) {
  // Enable gravity on the scene. Should be similar to earth's gravity. 
  scene.gravity = new BABYLON.Vector3(0, -0.98, 0);
  // Enable collisions globally. 
  scene.collisionsEnabled = true;
  // Enable collision detection and gravity on the free camera. 
  camera.checkCollisions = true;
  camera.applyGravity = true;
  // Set the player size, the camera's ellipsoid. 
  camera.ellipsoid = new BABYLON.Vector3(0.4, 0.8, 0.4);
}

Diese Funktion aktiviert das Kollisionssystem für die Szene des Spiels und die Kamera und legt das Ellipsoid der Kamera fest, das als die Größe des Spielers angesehen werden kann. Das ist ein Kasten mit Abmessungen von (in diesem Fall) 0,8 x 1,6 x 0,8 Einheiten, eine grob durchschnittliche Größe für einen Menschen. Die Kamera benötigt diesen Kasten, da sie kein Gitter ist. Das Kollisionssystem von Babylon.js untersucht nur Kollisionen zwischen Gittern, was der Grund dafür ist, dass für die Kamera ein Gitter simuliert werden sollte. Das Ellipsoid definiert den Mittelpunkt des Objekts, daher übersetzt sich 0,4 in der Größe zu 0,8. Ich habe auch die Schwerkraft der Szene aktiviert, die auf die Bewegungen der Kamera angewendet wird.

Sobald die Kollisionserkennung für die Kamera aktiviert wurde, muss die Untersuchung von Kollisionen mit Untergrund und Lauffläche aktiviert werden. Dies erfolgt mithilfe eines einfachen Wahrheitsflags, das für jedes Gitter festgelegt wird, für das Kollisionen auftreten sollen:

function enableMeshesCollision(meshes) {
  meshes.forEach(function(mesh) {
    mesh.checkCollisions = true;
  });
}

Das Hinzufügen der beiden Funktionsaufrufe zur init-Funktion bildet den letzten Schritt:

// init function from the first part of the tutorial.
  ...
  enableCameraCollision(camera, scene);
  enableMeshesCollision[[floor, lane, gutters[0], gutters[1]]);
}

Die einzige Aufgabe der Kollisionserkennung besteht darin, eine Vermischung von zwei Gitterstrukturen zu verhindern. Das Schwerkraftfeature wurde implementiert, um die Kamera am Boden zu halten. Zum Erstellen eines realistischen physikalischen Zusammenspiels zwischen bestimmten Gitterstrukturen – in diesem Fall der Ball und die Pins – ist ein komplexeres System erforderlich: das Physikmodul.

Das Werfen des Balls – Physikintegration in Babylon.js

Die wichtigste Aktion des Spiels besteht im Werfen des Balls in Richtung der Pins. Die Anforderungen sind ziemlich einfach: Der Spieler soll imstande sein, die Richtung und Stärke des Wurfs festzulegen. Wenn bestimmte Pins getroffen werden, sollten sie umfallen. Wenn die Pins andere Pins treffen, sollten diese ebenfalls umfallen. Wenn der Spieler den Ball zur Seite wirft, sollte er in die Rinne fallen. Die Pins sollten entsprechend der Geschwindigkeit fallen, mit der der Ball auf sie geworfen wurde.

Das ist genau die Domäne eines Physikmoduls. Das Physikmodul berechnet die Körperdynamik der Gitterstrukturen in Echtzeit und ihre nächste Bewegung entsprechend den auftretenden Kräften. Einfach gesagt, das Physikmodul ist der Teil, der entscheidet, was mit einem Gitter geschieht, wenn ein anderes Gitter mit ihm zusammenstößt oder Gitter von einem Benutzer bewegt werden. Es berücksichtigt die aktuelle Geschwindigkeit, das Gewicht, die Form und weitere Aspekte eines Gitters.

Zum Berechnen der Festkörperdynamik (die nächste Bewegung des Gitters im Raum) in Echtzeit muss das Physikmodul das Gitter vereinfachen. Zu diesem Zweck verfügt jedes Gitter über einen „Blender“ (impostor) – ein einfaches Gitter, an das es gebunden ist (normalerweise eine Kugel oder ein Kasten). Dadurch verringert sich die Genauigkeit der Berechnungen, es ermöglicht aber eine schnellere Berechnung der physikalischen Kräfte, die auf das Objekt wirken. Weitere Informationen zur Arbeitsweise des Physikmoduls finden Sie unter bit.ly/1S9AIsU.

Das Physikmodul ist kein Bestandteil von Babylon.js. Statt sich auf ein einzelnes Modul festzulegen, haben sich die Entwickler des Frameworks entschieden, Schnittstellen zu verschiedenen Physikmodulen zu implementieren und den Entwicklern die Entscheidung für eins von ihnen zu überlassen. Babylon.js verfügt aktuell über Schnittstellen zu zwei Physikmodulen: Cannon.js (cannonjs.org) und Oimo.js (github.com/lo-th/Oimo.js). Beide sind großartig! Ich persönlich finde die Integration von Oimo etwas gelungener und setze es daher für mein Bowlingspiel ein. Die Schnittstelle für Cannon.js wurde für Babylon.js 2.3, das jetzt als Alphaversion vorliegt, vollständig neu erstellt und unterstützt jetzt die neueste Cannon.js-Version mit vielen behobenen Programmfehlern und neuen Blendern, die komplexe Höhenzuordnungen bewältigen. Ich empfehle Ihnen, es einmal auszuprobieren, wenn Sie Babylon.js 2.3 oder höher verwenden.

Das Aktivieren des Physikmoduls erfolgt mithilfe einer einfachen Codezeile:

scene.enablePhysics(new BABYLON.Vector3(0, -9.8, 0), new BABYLON.OimoJSPlugin());

Dadurch wird die Schwerkraft der Szene festgelegt und das zu verwendende Physikmodul definiert. Oimo.js durch Cannon.js auszutauschen erfordert einfach die Änderung der zweiten Variablen in:

new BABYLON.CannonJSPlugin()

Im nächsten Schritt muss ich die Blender für alle Objekte festlegen. Dies erfolgt mithilfe der Funktion „setPhysicsState“ des Gitters. Hier ist beispielsweise die Definition der Lauffläche:

lane.setPhysicsState(BABYLON.PhysicsEngine.BoxImpostor, {
  mass: 0,
  friction: 0.5,
  restitution: 0
});

Die erste Variable steht für den Typ des Blenders. Da es sich bei der Lauffläche um einen perfekten Kasten handelt, verwende ich den Kastenblender (box impostor). Die zweite Variable ist die physikalische Definition des Körpers – sein Gewicht (in Kilogramm), seine Reibung und sein Restitutionsfaktor. Die Masse der Lauffläche ist 0, da sie an Ort und Stelle bleiben soll. Das Festlegen der Masse für einen Blender auf 0 hält das Objekt fest an seiner aktuellen Position verankert.

Für die Kugel verwende ich den Kugelblender (sphere impostor). Ein Bowlingball wiegt ungefähr 6,8 kg und ist normalerweise sehr gleichmäßig, daher ist keine Reibung erforderlich:

ball.setPhysicsState(BABYLON.PhysicsEngine.SphereImpostor, {
  mass: 6.8,
  friction: 0,
  restitution: 0
});

Wenn Sie sich wundern, warum ich Kilogramm statt Pfund verwende – das geschieht aus dem gleichen Grund, warum ich im gesamten Projekt Meter anstelle von Fuß verwende: Das Physikmodul verwendet das metrische System. Die standardmäßige Definition der Schwerkraft ist z. B. (0, -9.8, 0), was ungefähr der Schwerkraft der Erde entspricht. Die verwendeten Einheiten sind Meter pro Sekunde zum Quadrat (m/s2 ).

Jetzt muss ich in der Lage sein, den Ball zu werfen. Dazu verwende ich ein anderes Feature des Physikmoduls – die Anwendung eines Impulses auf ein bestimmtes Objekt. Hier stellt der Impuls eine Kraft in einer bestimmten Richtung dar, die auf Gitter mit aktivierter Physik angewendet wird. Um den Ball nach vorn zu werfen, verwende ich beispielsweise folgenden Code:

ball.applyImpulse(new BABYLON.Vector3(0, 0, 20), ball.getAbsolutePosition());

Die erste Variable ist der Vektor des Impulses, hier 20 Einheiten auf der Z-Achse, was vorwärts bedeutet, wenn die Szene zurückgesetzt wurde. Die zweite Variable gibt an, wo am Objekt die Kraft wirken soll. In diesem Fall ist das das Zentrum der Kugel. Denken Sie an einen Effetstoß beim Poolbillard – das Queue kann den Ball an vielen verschiedenen Stellen treffen, nicht nur in der Mitte. Auf diese Weise können Sie ein derartiges Verhalten simulieren.

Jetzt kann ich den Ball nach vorn werfen. Abbildung 1 zeigt, wie es aussieht, wenn der Ball die Pins trifft.

Der Bowlingball trifft die Pins
Abbildng 1 Der Bowlingball trifft die Pins

Allerdings fehlen mir noch Richtung und Stärke.

Es gibt viele Möglichkeiten, die Richtung festzulegen. Die zwei besten Optionen bestehen darin, entweder die aktuelle Kamerarichtung oder die Position der Mauszeigerspitze zu verwenden. Ich entscheide mich für die zweite Option.

Das Ermitteln des Punkts im Raum, den der Benutzer berührt hat, erfolgt mithilfe des PickingInfo-Objekts, das von jedem Zeiger-auf- und Zeiger-ab-Ereignis gesendet wird. Das PickingInfo-Objekt enthält Informationen über den Punkt, an dem das Ereignis ausgelöst wurde, einschließlich des berührten Gitters, des berührten Punkts am Gitter, des Abstands zu diesem Punkt und mehr. Wenn kein Gitter berührt wurde, hat die hit-Variable von PickingInfo den Wert „false“. Eine Szene in Babylon.js verfügt über zwei nützliche Rückruffunktionen, die mich beim Abrufen der Aufnahmeinformationen unterstützen: „onPointerUp“ und „onPointerDown“. Diese beiden Rückruffunktionen werden ausgelöst, wenn Zeigerereignisse ausgelöst werden, und ihre Signatur ist die folgende:

function(evt: PointerEvent, pickInfo: PickingInfo) => void

Die Variable „evt“ ist das ursprüngliche JavaScript-Ereignis, das ausgelöst wurde. Die zweite Variable ist die Aufnahmeinformation, die vom Framework für jedes ausgelöste Ereignis generiert wird.

Ich kann diese Rückrufe verwenden, um den Ball in die betreffende Richtung zu werfen:

scene.onPointerUp = function(evt, pickInfo) {
  if (pickInfo.hit) {
    // Calculate the direction using the picked point and the ball's position. 
    var direction = pickInfo.pickedPoint.subtract(ball.position);
    // To be able to apply scaling correctly, normalization is required.
    direction = direction.normalize();
    // Give it a bit more power (scale the normalized direction).
    var impulse = direction.scale(20);
    // Apply the impulse (and throw the ball). 
    ball.applyImpulse(impulse, new BABYLON.Vector3(0, 0, 0));
  }
}

Jetzt kann ich den Ball in eine bestimmte Richtung werfen. Das Einzige, was fehlt, ist die Stärke des Wurfs. Das füge ich hinzu, indem ich das Delta der Frames zwischen dem Pointer-Down- und dem Pointer-Up-Ereignis berechne. Abbildung 2 zeigt die Funktion, die zum Werfen des Balls mit einer bestimmten Kraft verwendet wird.

Abbildung 2 Werfen des Balls mit Kraft und Richtung

var strengthCounter = 5;
var counterUp = function() {
  strengthCounter += 0.5;
}
// This function will be called on pointer-down events.
scene.onPointerDown = function(evt, pickInfo) {
  // Start increasing the strength counter. 
  scene.registerBeforeRender(counterUp);
}
// This function will be called on pointer-up events.
scene.onPointerUp = function(evt, pickInfo) {
  // Stop increasing the strength counter. 
  scene.unregisterBeforeRender(counterUp);
  // Calculate throw direction. 
  var direction = pickInfo.pickedPoint.subtract(ball.position).normalize();
  // Impulse is multiplied with the strength counter with max value of 25.
  var impulse = direction.scale(Math.min(strengthCounter, 25));
  // Apply the impulse.
  ball.applyImpulse(impulse, ball.getAbsolutePosition());
  // Register a function that will run before each render call 
  scene.registerBeforeRender(function ballCheck() {
    if (ball.intersectsMesh(floor, false)) {
      // The ball intersects with the floor, stop checking its position.  
      scene.unregisterBeforeRender(ballCheck);
      // Let the ball roll around for 1.5 seconds before resetting it. 
      setTimeout(function() {
        var newPosition = scene.activeCameras[0].position.clone();
        newPosition.y /= 2;
        resetBall(ball, newPosition);
      }, 1500);
    }
  });
  strengthCounter = 5;
}

Ein Hinweis zu Zeigerereignissen – ich habe absichtlich nicht den Ausdruck „Klick“ verwendet. Babylon.js verwendet das Pointer Events-System, das die Mausklickfunktionen des Browsers auf Touch- und andere Eingabemethoden ausweitet, mit denen geklickt, gedrückt und gezeigt werden kann. Auf diese Weise lösen eine Berührung auf einem Smartphone oder ein Mausklick auf einem Desktop das gleiche Ereignis aus. Um dieses für Browser zu simulieren, die die Funktion nicht unterstützen, verwendet Babylon.js „hand.js“, ein Polyfill für Zeigerereignisse, das auch in der „index.html“ des Spiels enthalten ist. Mehr über „hand.js“ erfahren Sie auf dessen GitHub-Seite unter bit.ly/1S4taHF. Der Entwurf zu Pointer Events kann unter bit.ly/1PAdo9J abgerufen werden. Beachten Sie, dass „hand.js“ in kommenden Versionen durch jQuery PEP ersetzt wird (bit.ly/1NDMyYa).

Das war's mit der Physik! Das Bowlingspiel ist soeben ein ganzes Stück besser geworden.

Hinzufügen von Audioeffekten

Das Hinzufügen von Audioeffekten stellt einen riesigen Schritt vorwärts für die Benutzererfahrung dar. Audio kann die richtige Atmosphäre schaffen und dem Bowlingspiel ein Stück mehr Realismus verleihen. Glücklicherweise beinhaltet Babylon.js ein Audiomodul, das in Version 2.0 eingeführt wurde. Das Audiomodul basiert auf der Webaudio-API (bit.ly/1YgBWWQ), die von allen wichtigen Browsern mit Ausnahme von Internet Explorer unterstützt wird. Die Dateiformate, die verwendet werden können, hängen vom verwendeten Browser ab.

Ich füge drei verschiedene Audioeffekte hinzu. Das erste sind Umgebungsgeräusche – die Klänge, mit denen die Umgebung simuliert wird. Im Fall eines Bowlingspiels wären die Geräusche einer Bowlingbahn normalerweise ideal, aber da ich die Bowlingbahn draußen im Gras erstellt habe, eignen sich Naturgeräusche besser.

Zum Hinzufügen von Umgebungsgeräuschen lade ich den Klang, spiele ihn automatisch ab, und das fortlaufend endlos:

var atmosphere = new BABYLON.Sound("Ambient", "ambient.mp3", scene, null, {
  loop: true,
  autoplay: true
});

Der Klang wird vom Moment des Ladens an fortlaufend abgespielt.

Der zweite Audioeffekt, den ich hinzufügen möchte, ist der Klang des Balls, der auf der Lauffläche rollt. Dieser Klang wird abgespielt, solange sich der Ball auf der Lauffläche befindet, aber in dem Moment, da der Ball die Lauffläche verlässt, hört er auf.

Zuerst erstelle ich den Klang des Rollens:

var rollingSound = new BABYLON.Sound("rolling", "rolling.mp3", scene, null, {
  loop: true,
  autoplay: false
});

Der Klang wird geladen, er wird aber erst abgespielt, wenn seine Abspielfunktion ausgeführt wird, was eintritt, wenn der Ball geworfen wird. Ich habe die Funktion aus Abbildung 2 erweitert und Folgendes hinzugefügt:

...ball.applyImpulse(impulse, new BABYLON.Vector3(0, 0, 0));
// Start the sound.
rollingSound.play();
...

Ich stoppe den Klang, wenn der Ball die Lauffläche verlässt:

...
If(ball.intersectsMesh(floor, false)) {
  // Stop the sound.
  rollingSound.stop();
  ...
}
...

In Babylon.js kann ich einen Klang an ein bestimmtes Gitter anfügen. Auf diese Weise berechnet es automatisch die Lautstärke des Klangs und die Stereoperspektive mithilfe der Position des Gitters, wodurch sich ein realistischerer Eindruck ergibt. Dazu füge ich einfach die folgenden Zeile hinzu, nachdem ich den Klang erstellt habe:

rollingSound.attachToMesh(ball);

Jetzt wird der Klang immer von der Position des Balls abgespielt.

Der letzte Soundeffekt, den ich hinzufügen möchte, ist der Ball, der auf die Pins trifft. Dazu erstelle ich den Klang und füge ihn dann an den ersten Pin an:

var hitSound = new BABYLON.Sound("hit", "hit.mp3", scene);
hitSound.attachToMesh(pins[0]);

Dieser Klang wird nicht endlos wiedergegeben und auch nicht automatisch.

Der Klang wird bei jeder Berührung eines der Pins durch einen Ball abgespielt. Um das zu bewirken, füge ich eine Funktion hinzu, die nach dem Werfen des Balls fortlaufend untersucht, ob der Ball sich mit einem Pin schneidet. Wenn er sich schneidet, wird die Registrierung der Funktion aufgehoben und der Klang wiedergegeben. Dazu füge ich der Funktion „scene.onPointerUp“ aus Abbildung 2 diese Zeilen hinzu:

scene.registerBeforeRender(function ballIntersectsPins() {
  // Some will return true if the ball hit any of the pins.
  var intersects = pins.some(function (pin) {
    return ball.intersectsMesh(pin, false);
  });
  if (intersects) {
    // Unregister this function – stop inspecting the intersections.
    scene.unregisterBeforeRender(ballIntersectsPins);
    // Play the hit sound.
    hit.play();
  }
});

Das Spiel weist jetzt alle Audioeffekte auf, die ich hinzufügen wollte. Im nächsten Schritt verbessere ich das Spiel weiter, indem ich eine Spielstandsanzeige hinzufüge.

Beachten Sie, dass ich die verwendeten Audioeffekte dem begleitenden Projekt nicht beifügen konnte, da das Material urheberrechtlich geschützt ist. Ich konnte keine frei verfügbaren Audiobeispiele finden, die sich zur Veröffentlichung geeignet hätten. Daher ist der Code auskommentiert. Es funktioniert, wenn Sie die drei von mir verwendeten Audiobeispiele hinzufügen.

Hinzufügen einer Spielstandsanzeige

Nachdem ein Spieler die Pins getroffen hat, wäre es natürlich toll, zu sehen, wie viele gefallen sind und wie viele noch stehen. Zu diesem Zweck füge ich eine Spielstandsanzeige hinzu.

Die eigentliche Anzeigetafel ist nur eine einfache schwarze Fläche mit weißem Text darauf. Um sie zu erstellen, verwende ich das Feature für dynamische Texturen, das im Grundsatz eine 2D-Zeichenfläche ist, die als Textur für 3D-Objekte im Spiel verwendet werden kann.

Das Erstellen der Fläche und der dynamischen Textur ist einfach:

var scoreTexture = new BABYLON.DynamicTexture("scoreTexture", 512, scene, true);
var scoreboard = BABYLON.Mesh.CreatePlane("scoreboard", 5, scene);
// Position the scoreboard after the lane.
scoreboard.position.z = 40;
// Create a material for the scoreboard.
scoreboard.material = new BABYLON.StandardMaterial("scoradboardMat", scene);
// Set the diffuse texture to be the dynamic texture.
scoreboard.material.diffuseTexture = scoreTexture;

Die dynamische Textur macht es möglich, direkt mit ihrer Funktion „getContext“auf der zugrundeliegenden Zeichenfläche zu zeichnen, die einen „CanvasRenderingContext2D“ (mzl.la/1M2mz01) zurückgibt. Das dynamische Texturobjekt stellt darüber hinaus einige Hilfsfunktionen bereit, die nützlich sein können, wenn die direkte Interaktion mit dem Zeichenflächenkontext nicht gewünscht ist. Eine solche Funktion ist „drawText“, mit der eine Zeichenfolge in einer bestimmten Schriftart auf dieser Zeichenfläche gezeichnet werden kann. Ich aktualisiere die Zeichenfläche immer dann, wenn sich die Anzahl der gefallenen Pins ändert:

var score = 0;
scene.registerBeforeRender(function() {
  var newScore = 10 - checkPins(pins, lane);
  if (newScore != score) {
    score = newScore;
    // Clear the canvas. 
    scoreTexture.clear();
    // Draw the text using a white font on black background.
    scoreTexture.drawText(score + " pins down", 40, 100,
      "bold 72px Arial", "white", "black");
  }
});

Die Prüfung, ob die Pins gefallen sind, ist trivial – ich überprüfe einfach, ob ihre Position auf das Y-Achse gleich der ursprünglichen Y-Position aller Pins ist (der vordefinierten Variablen ‚pinYPosition‘):

function checkPins(pins) {
  var pinsStanding = 0;
  pins.forEach(function(pin, idx) {
    // Is the pin still standing on top of the lane?
    if (BABYLON.Tools.WithinEpsilon(pinYPosition, pin.position.y, 0.01)) {
      pinsStanding++;
    }
  });
  return pinsStanding;
}

Die dynamische Textur ist in Abbildung 3 zu sehen.

Die Spielstandsanzeige
Abbildung 3 Die Spielstandsanzeige

Alles, was jetzt noch fehlt, ist eine Funktion, um die Lauffläche und die Anzeige zurückzusetzen. Ich füge einen Aktionstrigger hinzu, der durch Drücken der Taste „R“ auf der Tastatur ausgelöst wird (siehe Abbildung 4).

Abbildung 4 Zurücksetzen von Lauffläche und Anzeige

function clear() {
  // Reset the score.
  score = 0;
  // Initialize the pins.
  initPins(scene, pins);
  // Clear the dynamic texture and draw a welcome string.
  scoreTexture.clear();
  scoreTexture.drawText("welcome!", 120, 100, "bold 72px Arial", "white", "black");
}
scene.actionManager.registerAction(new BABYLON.ExecuteCodeAction({
  trigger: BABYLON.ActionManager.OnKeyUpTrigger,
  parameter: "r"
}, clear));

Durch Drücken auf „R“ wird die Szene zurückgesetzt/initialisiert.

Hinzufügen einer Folgekamera

Ein hübscher Effekt, den ich dem Spiel hinzufügen möchte, ist eine Kamera, die dem Bowlingball folgt, wenn er geworfen wird. Ich möchte eine Kamera, die zusammen mit dem Ball auf die Kugeln „zurollt“ und stoppt, wenn der Ball wieder in seiner ursprünglichen Position ist. Das ist mithilfe des Mehrfachansichtsfeatures von Babylon.js zu erreichen.

Im ersten Teil dieses Tutorials habe ich das freie Kameraobjekt (free camera) als aktive Kamera der Szene festgelegt, in dieser Weise:

scene.activeCamera = camera

Die Variable für die aktive Kamera teilt der Szene mit, welche Kamera gerendert werden muss, für den Fall dass mehrere definiert sind. Das ist in Ordnung, wenn ich während des gesamten Spiels eine einzelne Kamera verwenden möchte. Wenn aber ein „Bild-in-Bild“-Effekt gewünscht ist, ist eine aktive Kamera zu wenig. Stattdessen muss ich das Array für die aktiven Kameras verwenden, das in der Szene unter dem Variablennamen „scene.activeCameras“ gespeichert ist. Die Kameras in diesem Array werden eine nach der anderen gerendert. Wenn „scene.activeCameras“ nicht leer ist, wird „scene.activeCamera“ ignoriert.

Der erste Schritt besteht darin, diesem Array die ursprüngliche freie Kamera hinzuzufügen. Dies erfolgt einfach in der init-Funktion. Ersetzen Sie „scene.activeCamera = camera“ durch:

scene.activeCameras.push(camera);

Im zweiten Schritt erstelle ich eine Folgekamera, wenn der Ball geworfen wird:

var followCamera = new BABYLON.FollowCamera("followCamera", ball.position, scene);
followCamera.radius = 1.5; // How far from the object should the camera be.
followCamera.heightOffset = 0.8; // How high above the object should it be.
followCamera.rotationOffset = 180; // The camera's angle. here - from behind.
followCamera.cameraAcceleration = 0.5 // Acceleration of the camera.
followCamera.maxCameraSpeed = 20; // The camera's max speed.

Hiermit wird die Kamera erstellt und so konfiguriert, dass sie 1,5 Einheiten hinter und 0,8 Einheiten über dem Objekt steht, dem sie folgen soll. Dieses verfolgte Objekt sollte der Ball sein, aber da zeigt sich ein Problem – der Ball könnte einen Drall haben, und dann rotiert die Kamera ebenfalls. Was ich erreichen möchte, ist eine „Flugbahn“ hinter dem Objekt. Zu diesem Zweck erstelle ich ein Folgeobjekt, das die Position des Balls abruft, nicht aber seine Rotation:

// Create a very small simple mesh.
var followObject = BABYLON.Mesh.CreateBox("followObject", 0.001, scene);
// Set its position to be the same as the ball's position.
followObject.position = ball.position;

Anschließend lege ich das „followObject“ als Ziel der Kamera fest:

followCamera.target = followObject;

Jetzt folgt die Kamera dem Folgeobjekt, das sich zusammen mit dem Ball bewegt.

Die letzte für die Kamera erforderliche Konfiguration ist ihr Ansichtsfenster. Jede Kamera kann die von ihr gezeigten Liegenschaften definieren. Dies erfolgt mithilfe der Variablen für das Ansichtsfenster, das mithilfe der folgenden Variablen definiert wird.

var viewport = new BABYLON.Viewport(xPosition, yPosition, width, height);

Alle Werte liegen zwischen 0 und 1, ganz wie bei einem Prozentsatz (wobei 1 für 100 Prozent steht) relativ zur Höhe und Breite des Bildschirms. Die ersten zwei Werte definieren den Anfangspunkt des Kamerarechtecks auf dem Bildschirm, und die Breite und Höhe definieren die Breite und Höhe des Rechtecks im Vergleich zur realen Größe des Bildschirms. Die Standardeinstellungen des Ansichtsfensters sind (0.0, 0.0, 1.0, 1.0), was den gesamten Bildschirm abdeckt. Für die Folgekamera lege ich Höhe und Breite auf 30 % des Bildschirms fest:

followCamera.viewport = new BABYLON.Viewport(0.0, 0.0, 0.3, 0.3);

Abbildung 5 zeigt, wie das Spiel nach dem Werfen eines Balls aussieht. Beachten Sie die Ansicht in der unteren linken Ecke.

Bild-in-Bild-Effekt mit der Folgekamera
Abbildung 5 Bild-in-Bild-Effekt mit der Folgekamera

Die Eingabesteuerung – welcher Bildschirm reagiert auf Eingabeereignisse, wie etwa Mauszeiger oder Tastaturereignisse – verbleibt bei der im ersten Teil des Tutorials definierten freien Kamera. Das war in der init-Funktion bereits festgelegt worden.

Möglichkeiten zur Weiterentwicklung des Spiels

Zu Beginn habe ich festgestellt, dass es sich bei diesem Spiel um einen Prototyp handelt. Es sind noch einige Dinge zu implementieren, um es zu einem richtigen Spiel zu machen.

Das erste wäre eine grafische Benutzeroberfläche, die nach dem Namen des Benutzers fragt, Konfigurationsoptionen bereitstellt (Man könnte ja auch auf dem Mars bowlen! Legen Sie einfach die Schwerkraft anders fest.) und was immer sonst ein Benutzer noch benötigen würde.

Babylon.js stellt keinen systemeigenen Weg zum Erstellen einer Benutzeroberfläche zur Verfügung, aber Mitglieder der Community haben ein paar Erweiterungen erstellt, die Sie verwenden können, um großartige Benutzeroberflächen zu erstellen. Beispielsweise seien genannt CastorGUI (bit.ly/1M2xEhD), bGUi (bit.ly/1LCR6jk) und die Dialogerweiterung unter bit.ly/1MUKpXH. Das erste verwendet HTML und CSS, um Ebenen oberhalb der 3D-Zeichenfläche zur Verfügung zu stellen. Die anderen fügen der Szene selbst 3D-Dialoge hinzu und verwenden dazu gewöhnliche Gitter und dynamische Texturen. Ich empfehle Ihnen, die genannten auszuprobieren, bevor Sie eine eigene Lösung erstellen. Sie sind einfach in der Anwendung und erleichtern die Erstellung einer Benutzeroberfläche erheblich.

Eine andere Verbesserung wäre die Verwendung besserer Gittermodelle. Die Gitter in meinem Spiel wurden alle mithilfe der internen Funktionen von Babylon.js erstellt, und die Beschränkung auf reinen Code bringt deutliche Einschränkungen mit sich. Kostenlose und kostenpflichtige 3D-Objekte sind auf vielen Websites erhältlich. Die beste ist meines Erachtens TurboSquid (bit.ly/1jSrTZy). Suchen Sie nach Gittern mit niedriger Polygonanzahl (Low-Poly Meshes), um bessere Leistung zu erzielen.

Und nachdem Sie bessere Gittermodelle hinzugefügt haben, warum nicht auch noch ein Menschenobjekt hinzufügen, das den Ball tatsächlich wirft? Dazu benötigen Sie das Feature zur Knochenanimation, das in Babylon.js integriert ist, und ein Gittermodell, das dieses Feature unterstützt. Unter babylonjs.com/BONES finden Sie eine Demo, die zeigt, wie das Ergebnis aussieht.

Als letzten Schliff können Sie versuchen, das Spiel VR-freundlich zu gestalten. Die einzige Änderung, die in diesem Fall erforderlich ist, ist die verwendete Kamera. Ersetzen Sie die „FreeCamera“ durch eine „WebVRFreeCamera“, und sehen Sie, wie einfach sich Google Cardboard als Zielplattform bedienen lässt.

Es bleibt viel Raum für weitere Verbesserungen, die Sie vornehmen können – das Hinzufügen einer Kamera oberhalb der Pins, weitere Laufflächen an den Seiten für einen Mehrspielermodus, das Beschränken der Bewegungen der Kamera und der Position, von denen der Ball geworfen werden kann usw. Ich lasse Sie beim Entdecken dieser anderen Features in Ruhe.

Zusammenfassung

Ich hoffe, Sie hatten so viel Spaß beim Lesen dieses Tutorials wie ich beim Schreiben, und Ihre Neugier auf Babylon.js ist geweckt. Es ist wirklich ein tolles Framework, das mit viel Liebe von Entwicklern für Entwickler gemacht wurde. Auf babylonjs.com finden Sie weitere Demos des Frameworks. Und, ich muss es ja nicht mehr eigens erwähnen, Sie und Ihre Fragen sind beim Supportforum herzlich willkommen. Es gibt keine dummen Fragen, nur dumme Antworten!


Raanan Weberist IT-Berater, Full-Stack-Entwickler, Ehemann und Vater. In seiner Freizeit wirkt er an Babylon.js und anderen Open-Source-Projekten mit. Seinen Blog finden Sie unter blog.raananweber.com.

Unser Dank gilt dem folgenden technischen Experten für die Durchsicht dieses Artikels: David Catuhe