In der bisherigen Serie haben wir das Gameplay kodiert, Feinde hinzugefügt und die Dinge mit Blüte- und Partikeleffekten aufgewertet. In diesem letzten Teil erstellen wir ein dynamisches, verzerrendes Hintergrundraster.
Dieses Video zeigt das Raster in Aktion:
Wir erstellen das Gitter mithilfe einer Federsimulation: An jedem Schnittpunkt des Gitters legen wir ein kleines Gewicht (eine Punktmasse) auf und verbinden diese Gewichte mithilfe von Federn. Diese Federn werden nur ziehen und niemals drücken, ähnlich wie ein Gummiband. Um das Gitter in Position zu halten, werden die Massen um den Rand des Gitters fest verankert.
Unten sehen Sie ein Diagramm des Layouts.
Wir erstellen eine Klasse namens Gitter
um diesen Effekt zu erzeugen. Bevor wir jedoch am Gitter selbst arbeiten, müssen wir zwei Hilfsklassen erstellen: Frühling
und PointMass
.
Das PointMass
Klasse stellt die Massen dar, an denen wir die Federn anbringen. Federn sind niemals direkt mit anderen Federn verbunden. Stattdessen üben sie eine Kraft auf die Massen aus, die sie verbinden, was wiederum andere Federn strecken kann.
öffentliche Klasse PointMass private Vector3f Position; private Vector3f Velocity = Vector3f.ZERO; privater Float inverseMass; private Vector3f Beschleunigung = Vector3f.ZERO; Dämpfung des privaten Schwimmers = 0,98 f; public PointMass (Vector3f Position, Float inverseMass) this.position = Position; this.inverseMass = inverseMass; public void applyForce (Vector3f force) acceler.addLocal (force.mult (inverseMass)); public void riseDamping (Float-Faktor) Dämpfung * = Faktor; public void update (float tpf) Velocity.addLocal (Beschleunigung.mult (1f)); position.addLocal (Geschwindigkeit.mult (0.6f)); Beschleunigung = Vector3f.ZERO.clone (); if (Velocity.lengthSquared () < 0.0001f) velocity = Vector3f.ZERO.clone(); velocity.multLocal(damping); damping = 0.98f; damping = 0.8f; position.z *= 0.9f; if (position.z < 0.01) position.z = 0; public Vector3f getPosition() return position; public Vector3f getVelocity() return velocity;
Es gibt ein paar interessante Punkte zu dieser Klasse. Beachten Sie zunächst, dass es die speichert invers der Masse, 1 / Masse
. Dies ist in Physik-Simulationen oft eine gute Idee, da Physikgleichungen dazu neigen, häufiger das Inverse der Masse zu verwenden, und weil es uns eine einfache Möglichkeit gibt, unendlich schwere, unbewegliche Objekte darzustellen, indem die inverse Masse auf Null gesetzt wird.
Die Klasse enthält auch eine Dämpfung Variable, die die Masse allmählich verlangsamt. Dies wird ungefähr als Reibungs- oder Luftwiderstand verwendet. Dies führt dazu, dass das Gitter schließlich zum Stillstand kommt und erhöht auch die Stabilität der Federsimulation.
Das Aktualisieren()
Diese Methode erledigt die Bewegung der Punktmasse für jeden Frame. Es beginnt mit der symplektischen Euler-Integration, was bedeutet, dass wir die Geschwindigkeit mit der Beschleunigung und dann mit der aktualisierten Geschwindigkeit der Position hinzufügen. Dies unterscheidet sich von der Euler-Standardintegration, bei der wir die Geschwindigkeit aktualisieren würden nach dem die Position aktualisieren.
Nach der Aktualisierung der Geschwindigkeit und der Position überprüfen wir, ob die Geschwindigkeit sehr klein ist, und setzen sie gegebenenfalls auf Null. Dies kann aufgrund der Art der denormalisierten Fließkommazahlen für die Leistung wichtig sein.
Das IncreaseDamping ()
Diese Methode wird verwendet, um die Dämpfung vorübergehend zu erhöhen. Wir werden dies später für bestimmte Effekte verwenden.
Eine Feder verbindet zwei Punktmassen und bringt, wenn sie über ihre natürliche Länge gedehnt wird, eine Kraft auf, die die Massen zusammenzieht. Federn folgen einer modifizierten Version von Hookes Gesetz mit Dämpfung:
\ [f = -kx - bv \]
Der Code für die Frühling
Klasse ist wie folgt:
öffentliche Klasse Spring private PointMass end1; privates PointMass end2; private float targetLength; private Schwimmersteifigkeit; private Schwimmerdämpfung; public Spring (PointMass end1, PointMass end2, Schwimmkörpersteifigkeit, Schwimmerdämpfung, Knotengitterknoten, boolescher Wert, Geometrie defaultLine) this.end1 = end1; this.end2 = end2; this.stiffness = Steifheit; this.Damping = Dämpfung; targetLength = end1.getPosition (). distance (end2.getPosition ()) * 0.95f; if (sichtbar) defaultLine.addControl (new LineControl (end1, end2)); gridNode.attachChild (defaultLine); public void update (float tpf) Vector3f x = end1.getPosition (). subtract (end2.getPosition ()); float length = x.length (); if (length> targetLength) x.normalizeLocal (); x.multLocal (Länge - Ziellänge); Vector3f dv = end2.getVelocity (). Subtract (end1.getVelocity ()); Vector3f Kraft = x.mult (Steifheit); Kraftsubtrakt (dv.mult (Dämpfung / 10f)); end1.applyForce (force.negate ()); end2.applyForce (force);
Wenn wir eine Feder erstellen, legen wir die natürliche Länge der Feder so fest, dass sie etwas geringer ist als der Abstand zwischen den beiden Endpunkten. Dies hält das Gitter auch im Ruhezustand straff und verbessert das Erscheinungsbild etwas.
Das Aktualisieren()
Die Methode prüft zunächst, ob die Feder über ihre natürliche Länge hinaus gedehnt ist. Wenn es nicht gedehnt wird, passiert nichts. Wenn ja, verwenden wir das modifizierte Hookesche Gesetz, um die Kraft der Feder zu ermitteln und auf die beiden verbundenen Massen anzuwenden.
Wir müssen eine weitere Klasse erstellen, um die Zeilen korrekt darstellen zu können. Das LineControl
kümmert sich um das Verschieben, Skalieren und Drehen der Linien:
public class LineControl erweitert AbstractControl private PointMass end1, end2; public LineControl (PointMass end1, PointMass end2) this.end1 = end1; this.end2 = end2; @Override protected void controlUpdate (float tpf) // movement spatel.setLocalTranslation (end1.getPosition ()); // skaliere Vector3f dif = end2.getPosition (). subtract (end1.getPosition ()); räumliche.setLocalScale (dif.length ()); // Rotation spatel.lookAt (end2.getPosition (), new Vector3f (1,0,0)); @Override protected void controlRender (RenderManager RM, ViewPort vp)
Jetzt, da wir die notwendigen verschachtelten Klassen haben, können wir das Raster erstellen. Wir beginnen mit dem Erstellen PointMass
Objekte an jeder Kreuzung im Raster. Wir erstellen auch einen unbeweglichen Anker PointMass
Objekte, um das Gitter an Ort und Stelle zu halten. Wir verbinden dann die Massen mit Federn:
öffentliche Klasse Grid privater Knoten gridNode; private Spring [] Federn; private PointMass [] [] Punkte; private Geometrie defaultLine; private Geometrie thickLine; public Grid (Rechteckgröße, Vector2f-Abstand, Knoten guiNode, AssetManager assetManager) gridNode = new Node (); guiNode.attachChild (gridNode); defaultLine = createLine (1f, assetManager); thickLine = createLine (3f, assetManager); ArrayList springList = new ArrayList (); Schwimmsteifigkeit = 0,28f; Schwimmdämpfung = 0,06f; int numColumns = (int) (Größe, Breite, Abstand, x) + 2; int numRows = (int) (Größe.Höhe / Abstand.y) + 2; points = new PointMass [numColumns] [numRows]; PointMass [] [] fixedPoints = neue PointMass [numColumns] [numRows]; // die Punktmassen erstellen float xCoord = 0, yCoord = 0; für (int row = 0; row < numRows; row++) for (int column = 0; column < numColumns; column++) points[column][row] = new PointMass(new Vector3f(xCoord,yCoord,0),1); fixedPoints[column][row] = new PointMass(new Vector3f(xCoord,yCoord,0),0); xCoord += spacing.x; yCoord += spacing.y; xCoord = 0; // link the point masses with springs Geometry line; for (int y=0; y0) wenn (y% 3 == 0) Zeile = dicke Linie; else line = defaultLine; springList.add (neue Feder (Punkte [x-1] [y], Punkte [x] [y], Steifigkeit, Dämpfung, gridNode, true, line.clone ())); if (y> 0) if (x% 3 == 0) line = thickLine; else line = defaultLine; springList.add (neue Feder (Punkte [x] [y-1], Punkte [x] [y], Steifigkeit, Dämpfung, gridNode, true, line.clone ()));
Der Erste zum
Schleife erzeugt an jedem Schnittpunkt des Gitters sowohl regelmäßige Massen als auch unbewegliche Massen. Wir werden nicht wirklich alle unbeweglichen Massen verwenden, und die ungenutzten Massen werden einfach irgendwann gesammelt, wenn der Konstruktor beendet ist. Wir könnten optimieren, indem Sie das Erstellen unnötiger Objekte vermeiden. Da das Raster jedoch normalerweise nur einmal erstellt wird, ist es nicht viel Unterschied.
Neben der Verwendung von Ankerpunktmassen am Rand des Gitters werden auch einige Ankermassen innerhalb des Gitters verwendet. Diese werden verwendet, um das Gitter nach dem Verformen sehr sanft in seine ursprüngliche Position zu ziehen.
Da sich die Ankerpunkte niemals bewegen, müssen sie nicht für jeden Rahmen aktualisiert werden. Wir können sie einfach an die Federn anschließen und sie vergessen. Daher haben wir keine Member-Variable in der Gitter
Klasse für diese Massen.
Es gibt eine Reihe von Werten, die Sie bei der Erstellung des Gitters anpassen können. Die wichtigsten sind die Steifigkeit und Dämpfung der Federn. Die Steifigkeit und Dämpfung der Randanker und Innenanker wird unabhängig von den Hauptfedern eingestellt. Bei höheren Steifigkeitswerten schwingen die Federn schneller, und bei höheren Dämpfungswerten verlangsamen sich die Federn schneller.
Eines ist noch zu erwähnen: das createLine ()
Methode.
private Geometry createLine (Float-Dicke, AssetManager assetManager) Vector3f [] Knoten = new Vector3f (0,0,0), neu Vector3f (0,0,1); int [] Indizes = 0,1; Mesh lineMesh = new Mesh (); lineMesh.setMode (Mesh.Mode.Lines); lineMesh.setLineWidth (Dicke); lineMesh.setBuffer (VertexBuffer.Type.Position, 3, BufferUtils.createFloatBuffer (Vertices)); lineMesh.setBuffer (VertexBuffer.Type.Index, 1, BufferUtils.createIntBuffer (Indizes)); lineMesh.updateBound (); Geometrie lineGeom = neue Geometrie ("lineMesh", lineMesh); Material matWireframe = neues Material (assetManager, "Common / MatDefs / Misc / Unshaded.j3md"); matWireframe.getAdditionalRenderState (). setFaceCullMode (RenderState.FaceCullMode.Off); matWireframe.setColor ("Color", neue ColorRGBA (0,118f, 0,118f, 0,545f, 0,25f)); matWireframe.getAdditionalRenderState (). setBlendMode (BlendMode.AlphaAdditive); lineGeom.setMaterial (matWireframe); return lineGeom;
Hier erstellen wir im Wesentlichen eine Linie, indem wir die Scheitelpunkte der Linie und die Reihenfolge der Scheitelpunkte festlegen, ein Netz erstellen, ein blaues Material hinzufügen usw. Wenn Sie den Prozess der Linienerstellung genau verstehen möchten, können Sie sich jederzeit die jME-Tutorials ansehen.
Warum muss die Linienerstellung so kompliziert sein - ist es nicht nur eine einfache Linie? Ja, das ist es, aber Sie müssen sehen, was jME beabsichtigt. Normalerweise haben Sie in 3D-Spielen keine einzelnen Linien oder Dreiecke im Spiel, sondern Modelle mit Texturen und Animationen. Während es möglich ist, eine einzige Zeile in jME zu generieren, liegt der Hauptfokus auf dem Importieren von Modellen, die mit anderer Software wie Blender erstellt wurden.Damit das Raster verschoben werden kann, müssen wir es für jeden Frame aktualisieren. Dies ist sehr einfach, da wir bereits alle harte Arbeit in der PointMass
und Frühling
Klassen.
public void update (float tpf) für (int i = 0; iJetzt werden wir einige Methoden hinzufügen, die das Raster bearbeiten. Sie können Methoden für jede denkbare Manipulation hinzufügen. Wir werden hier drei Arten von Manipulationen implementieren: Drücken Sie einen Teil des Gitters in eine bestimmte Richtung, drücken Sie das Gitter von einem Punkt aus nach außen und ziehen Sie das Gitter in Richtung eines bestimmten Punktes. Alle drei werden das Gitter innerhalb eines bestimmten Radius von einem Zielpunkt aus beeinflussen.
Nachfolgend einige Bilder dieser Manipulationen in Aktion:
Welle, die durch Verschieben des Gitters entlang der Z-Achse erzeugt wird.
Kugeln, die das Gitter nach außen abweisen.
Das Gitter nach innen saugen.Und hier sind die Methoden für die Auswirkungen:
public void applyDirectedForce (Vector3f-Kraft, Vector3f-Position, Float-Radius) für (int x = 0; xVerwenden des Gitters in Shape Blaster
Jetzt ist es Zeit, das Raster in unserem Spiel zu verwenden. Wir beginnen mit der Erklärung von a
Gitter
Variable inMonkeyBlasterMain
und es in initialisierensimpleInitApp ()
:Rectangle size = new Rectangle (0, 0, settings.getWidth (), settings.getHeight ()); Vector2f-Abstand = new Vector2f (25,25); grid = new Grid (Größe, Abstand, guiNode, assetManager);Dann müssen wir anrufen
grid.update (float tpf)
von demsimpleUpdate
Methode:@Override public void simpleUpdate (float tpf) if ((Boolean) player.getUserData ("alive")) spawnEnemies (); spawnBlackHoles (); handleCollisions (); handleGravity (tpf); else if (System.currentTimeMillis () - (Long) player.getUserData ("dieTime")> 4000f &&! gameOver) // spawn player player.setLocalTranslation (500,500,0); guiNode.attachChild (Spieler); player.setUserData ("lebendig", wahr); sound.spawn (); grid.update (tpf); hud.update ();Als nächstes müssen wir die Effektmethoden an den richtigen Stellen in unserem Spiel aufrufen.
Die erste, eine Welle zu erzeugen, wenn der Spieler erscheint, ist ziemlich einfach - wir erweitern einfach die Stelle, an der wir den Spieler erzeugen
simpleUpdate (Float-Tpf)
mit der folgenden Zeile:grid.applyDirectedForce (neuer Vector3f (0,0,5000), player.getLocalTranslation (), 100);Beachten Sie, dass wir eine Kraft in der anwenden z-Richtung. Wir haben zwar ein 2D-Spiel, aber da es sich bei jME um eine 3D-Engine handelt, können wir problemlos auch 3D-Effekte verwenden. Wenn wir die Kamera drehen würden, würden wir das Gitter nach innen und außen springen sehen.
Der zweite und der dritte Effekt müssen in Kontrollen behandelt werden. Wenn Kugeln durch das Spiel fliegen, rufen sie diese Methode in auf
controlUpdate (float tpf)
:grid.applyExplosiveForce (direction.length () * (18f), spat.getLocalTranslation (), 80);Dies führt dazu, dass Kugeln das Gitter proportional zu ihrer Geschwindigkeit abstoßen. Das war ziemlich einfach.
Bei den schwarzen Löchern ist es ähnlich:
grid.applyImplosiveForce (FastMath.sin (sprayAngle / 2) * 10 +20, räumlich.getLocalTranslation (), 250);Dadurch saugt das Schwarze Loch mit unterschiedlicher Kraft in das Gitter ein. Ich habe das wieder verwendet
sprayAngle
variabel, wodurch die Kraft auf das Gitter synchron mit dem Winkel pulsiert, mit dem die Partikel versprüht werden (obwohl die Hälfte durch die Division durch zwei geteilt wird). Die eingeleitete Kraft variiert sinusförmig zwischen10
und30
.Damit dies funktioniert, müssen Sie nicht vergessen, zu bestehen
Gitter
zuBulletControl
undBlackHoleControl
.
Interpolation
Wir können das Netz optimieren, indem wir die visuelle Qualität für eine bestimmte Anzahl von Federn verbessern, ohne die Leistungskosten erheblich zu erhöhen.
Wir werden das Gitter dichter machen, indem Sie Liniensegmente in die vorhandenen Gitterzellen einfügen. Dazu zeichnen wir Linien vom Mittelpunkt einer Seite der Zelle bis zum Mittelpunkt der gegenüberliegenden Seite. Das Bild unten zeigt die neuen interpolierten Zeilen in Rot:
Wir werden diese zusätzlichen Zeilen im Konstruktor unseres erstellen
Gitter
Klasse. Wenn Sie einen Blick darauf werfen, sehen Sie zweizum
Schleifen, wo wir die Punktmassen mit den Federn verbinden. Fügen Sie einfach diesen Codeblock dort ein:if (x> 0 && y> 0) Geometrie additionalLine = defaultLine.clone (); additionalLine.addControl (neues AdditionalLineControl (Punkte [x-1] [y], Punkte [x] [y], Punkte [x-1] [y-1], Punkte [x] [y-1])); gridNode.attachChild (additionalLine); Geometrie additionalLine2 = defaultLine.clone (); additionalLine2.addControl (neues AdditionalLineControl (Punkte [x] [y-1], Punkte [x] [y], Punkte [x-1] [y-1], Punkte [x-1] [y])); gridNode.attachChild (additionalLine2);Aber wie Sie wissen, ist das Erstellen von Objekten nicht das einzige, was wir tun müssen. Wir müssen ihnen auch ein Steuerelement hinzufügen, damit sie sich korrekt verhalten. Wie Sie oben sehen können, die
AdditionalLineControl
Es werden vier Punktmassen übergeben, sodass Position, Drehung und Skalierung berechnet werden können:Die öffentliche Klasse AdditionalLineControl erweitert AbstractControl private PointMass end11, end12, end21, end22; public AdditionalLineControl (PointMass end11, PointMass end12, PointMass end21, PointMass end22) this.end11 = end11; this.end12 = end12; this.end21 = end21; this.end22 = end22; @Override protected void controlUpdate (float tpf) // movement spatel.setLocalTranslation (position1 ()); // skaliere Vector3f dif = position2 (). subtrahieren (position1 ()); räumliche.setLocalScale (dif.length ()); // Rotation räumlich.lookAt (position2 (), neuer Vector3f (1,0,0)); private Vector3f position1 () return new Vector3f (). interpolate (end11.getPosition (), end12.getPosition (), 0.5f); private Vector3f position2 () return new Vector3f (). interpolate (end21.getPosition (), end22.getPosition (), 0.5f); @Override protected void controlRender (RenderManager RM, ViewPort vp)
Was kommt als nächstes?
Wir haben das grundlegende Gameplay und die Effekte implementiert. Es liegt an Ihnen, daraus ein komplettes und ausgefeiltes Spiel mit Ihrem eigenen Geschmack zu machen. Fügen Sie einige interessante neue Mechaniken hinzu, einige coole neue Effekte oder eine einzigartige Geschichte. Falls Sie nicht sicher sind, wo Sie anfangen sollen, finden Sie hier einige Vorschläge:
- Erstellen Sie neue Feindtypen wie Schlangen oder explodierende Feinde.
- Erstellen Sie neue Waffentypen, wie Raketen oder eine Blitzkanone.
- Fügen Sie einen Titelbildschirm und ein Hauptmenü hinzu.
- Fügen Sie eine High-Score-Tabelle hinzu.
- Fügen Sie einige Power-Ups hinzu, beispielsweise ein Schild oder Bomben. Für Bonuspunkte werden Sie kreativ mit Ihren Power-Ups. Sie können Power-Ups erstellen, die die Schwerkraft beeinflussen, die Zeit verändern oder wie Organismen wachsen. Sie können einen riesigen, auf Physik basierenden Abrissball an das Schiff anschließen, um Feinde zu besiegen. Experimentiere, um Power-Ups zu finden, die Spaß machen und dein Spiel hervorheben.
- Erstellen Sie mehrere Ebenen. Härtere Level können härtere Gegner und fortgeschrittenere Waffen und Powerups einführen.
- Erlaube einem zweiten Spieler, sich mit einem Gamepad anzuschließen.
- Lassen Sie die Arena so scrollen, dass sie größer als das Spielfenster ist.
- Fügen Sie Umweltgefahren wie Laser hinzu.
- Fügen Sie ein Shop- oder Level-System hinzu und ermöglichen Sie dem Spieler, Upgrades zu erhalten.
Danke fürs Lesen!