Meer, Piraten - 3D-Online-Spiel im Browser

Grüße an Habr-Benutzer und Gelegenheitsleser. Dies ist die Geschichte der Entwicklung eines browserbasierten Multiplayer-Online-Spiels mit Low-Poly-3D-Grafiken und einfacher 2D-Physik.



Es gibt viele browserbasierte 2D-Minispiele, aber ein solches Projekt ist für mich neu. In Gamedev kann das Lösen von Problemen, auf die Sie noch nicht gestoßen sind, sehr aufregend und interessant sein. Die Hauptsache ist, nicht mit Schleifteilen hängen zu bleiben und ein funktionierendes Spiel zu starten, während es einen Wunsch und eine Motivation gibt. Verschwenden Sie also keine Zeit und beginnen Sie mit der Entwicklung!





Das Spiel auf den Punkt gebracht



Survival Fight ist derzeit der einzige Spielmodus. Kämpfe von 2 bis 6 Schiffen ohne Wiedergeburt, wobei der letzte überlebende Spieler als Sieger gilt und x3 Punkte und Gold erhält.



Arcade-Steuerelemente : Tasten W, A, D oder Pfeile zum Bewegen, Leertaste zum Schießen auf feindliche Schiffe. Sie müssen nicht zielen, Sie können nicht verfehlen, der Schaden hängt von der Zufälligkeit und dem Winkel des Schusses ab. Größerer Schaden geht mit einer Medaille "direkt am Ziel" einher.



Wir verdienen Gold, indem wir innerhalb von 24 Stunden und 7 Tagen (zurückgesetzt um 00:00 Uhr Moskauer Zeit) die ersten Plätze in der Wertung der Spieler belegen und tägliche Aufgaben erledigen (einer von drei wird wiederum für einen Tag ausgegeben). Es gibt auch Gold für Schlachten, aber weniger.



Gold ausgebenSetzen Sie 24 Stunden lang schwarze Segel auf Ihr Schiff. Die Pläne, die Fähigkeit hinzuzufügen, den Kraken zu wecken, der den Boden eines feindlichen Schiffes mit seinen riesigen Tentakeln abträgt :)



PVP oder Zassal- Feigling? Eine Funktion, die ich bereits vor der Auswahl eines Piratenthemas implementieren wollte, ist die Möglichkeit, mit wenigen Klicks mit Freunden zu kämpfen. Ohne Registrierung und unnötige Gesten können Sie Ihren Freunden einen Einladungslink senden und warten, bis sie über den Link ins Spiel kommen: Ein privater Raum, der für alle geöffnet werden kann, wird automatisch erstellt, wenn jemand dem Link folgt, sofern der "Autor" des Links keinen anderen gestartet hat Schlacht.



Technologie-Stack



Three.js ist eine der beliebtesten Bibliotheken für die Arbeit mit 3D im Browser mit guter Dokumentation und vielen verschiedenen Beispielen. Außerdem habe ich bereits Three.js verwendet - die Wahl liegt auf der Hand.



Das Fehlen einer Spiel-Engine ist auf den Mangel an relevanter Erfahrung und den Wunsch zurückzuführen, etwas zu lernen, ohne das sowieso alles gut funktioniert :)



Node.js, weil es einfach, schnell und bequem ist, obwohl ich keine direkte Erfahrung mit Node.js hatte. Ich betrachtete Java als Alternative, führte einige lokale Experimente durch, unter anderem mit Web-Sockets, wagte jedoch nicht herauszufinden, ob es schwierig war, Java auf einem VPS auszuführen. Eine andere Option - Go, ihre Syntax macht mich entmutigt - hat sich in ihrer Studie nicht um ein Jota weiterentwickelt.



Verwenden Sie für Web-Sockets das Modul ws in Node.js.



PHP und MySQLweniger offensichtliche Wahl, aber das Kriterium ist immer noch dasselbe - schnell und einfach, da Erfahrung mit diesen Technologien vorhanden ist.



Es stellt sich wie







folgt heraus: PHP wird hauptsächlich zum Bereitstellen von Webseiten für den Client und für seltene AJAX-Anfragen benötigt, aber zum größten Teil kommuniziert der Client weiterhin über Web-Sockets mit dem Spieleserver auf Node.js.



Ich wollte den Spieleserver überhaupt nicht mit der Datenbank verbinden, also läuft alles über PHP. Meiner Meinung nach gibt es hier Pluspunkte, obwohl ich nicht sicher bin, ob sie von Bedeutung sind. Da beispielsweise vorgefertigte Daten in der erforderlichen Form zu Node.js gelangen, verschwendet Node.js keine Zeit mit der Verarbeitung und zusätzlichen Abfragen in der Datenbank, sondern befasst sich mit wichtigeren Dingen - es "verdaut" die Aktionen der Spieler und ändert den Status der Spielwelt in den Räumen.



Modell zuerst



Die Entwicklung begann mit einer einfachen und wichtigsten Sache - einem bestimmten Modell der Spielwelt, das Seeschlachten aus Serversicht beschreibt. Plain Canvas 2D ist ideal für die schematische Darstellung des Modells auf dem Bildschirm.







Zunächst stellte ich die normale "Verlet" -Physik ein und berücksichtigte den unterschiedlichen Widerstand gegen die Bewegung des Schiffes in verschiedene Richtungen relativ zur Rumpfrichtung. Aus Sorge um die Serverleistung ersetzte ich die normale Physik durch die einfachste, bei der die Umrisse des Schiffes nur im visuellen Bereich blieben, die Schiffe jedoch physisch runde Objekte sind, die nicht einmal Trägheit aufweisen. Anstelle der Trägheit gibt es eine begrenzte Vorwärtsbeschleunigung.



Schüsse und Treffer werden mit den Vektoren der Schiffsrichtung und der Schussrichtung auf einfache Operationen reduziert. Hier gibt es keine Muscheln. Wenn das Punktprodukt normalisierter Vektoren unter Berücksichtigung der Entfernung zum Ziel in die akzeptablen Werte passt, erfolgt ein Schuss und ein Treffer, wenn der Spieler den Knopf drückt.



Das clientseitige JavaScript zum Rendern des Spielweltmodells, das die Bewegung von Schiffen und Schüssen handhabt, habe ich fast unverändert auf den Node.js-Server portiert.



Spieleserver



Der WebSocket-Server von Node.j besteht nur aus 3 Skripten:



  • main.js - das Hauptskript, das WS-Nachrichten von Spielern empfängt, Räume erstellt und die Zahnräder dieser Maschine dreht
  • room.js - ein Skript, das für das Gameplay im Raum verantwortlich ist: Aktualisierung der Spielwelt, Senden von Updates an die Spieler im Raum
  • funcs.js - enthält eine Klasse zum Arbeiten mit Vektoren, einige Hilfsfunktionen und eine Klasse, die eine doppelt verknüpfte Liste implementiert


Mit fortschreitender Entwicklung wurden neue Klassen hinzugefügt - fast alle stehen in direktem Zusammenhang mit dem Gameplay und landeten in der Datei room.js. Manchmal ist es praktisch, mit Klassen separat zu arbeiten (in separaten Dateien), aber die Option "Alles in einer" ist auch nicht schlecht, solange nicht zu viele Klassen vorhanden sind (es ist praktisch, nach oben zu scrollen und sich zu merken, welche Parameter eine Methode einer anderen Klasse verwendet).



Die aktuelle Liste der Spielserverklassen:



  • WaitRoom - Der Raum, in dem die Spieler auf den Beginn des Kampfes warten. Es verfügt über eine eigene Tick-Methode, die die Aktualisierungen sendet und mit der Erstellung des Spielraums beginnt, wenn mehr als die Hälfte der Spieler für den Kampf bereit ist
  • Room — , : /, ,
  • Player — «» :
  • Ship — : , , ,
  • PhysicsEngine — ,
  • PhysicsBody


Room
let upd = {p: [], t: this.gamet};
let t = Date.now();
let dt = t - this.lt;
let nalive = 0;

for (let i in this.players) {
	this.players[i].tick(t, dt);
}

this.physics.run(dt);

for (let i in this.players) {
	upd.p.push(this.players[i].getUpd());
}

this.chronology.addLast(clone(upd));
if (this.chronology.n > 30) this.chronology.remFirst();

let updjson = JSON.stringify(upd);

for (let i in this.players) {
	let pl = this.players[i];
	if (pl.ship.health > 0) nalive++;
	if (pl.deadLeave) continue;
	pl.cl.ws.send(updjson);
}

this.lt = t;
this.gamet += dt;

if (nalive <= 1) return false;
return true;




Neben Klassen gibt es Funktionen wie das Abrufen von Benutzerdaten, das Aktualisieren einer täglichen Aufgabe, das Abrufen einer Belohnung und den Kauf eines Skins. Diese Funktionen senden grundsätzlich https-Anforderungen an PHP, das eine oder mehrere MySQL-Abfragen ausführt und das Ergebnis zurückgibt.



Netzwerkverzögerungen



Die Kompensation der Netzwerklatenz ist ein wichtiger Bestandteil der Entwicklung von Online-Spielen. Zu diesem Thema habe ich hier auf Habré wiederholt eine Reihe von Artikeln erneut gelesen . Im Falle einer Schlacht von Segelschiffen kann die Verzögerungskompensation einfach sein, aber Sie müssen immer noch Kompromisse eingehen.



Auf dem Client wird ständig eine Interpolation durchgeführt - die Berechnung des Zustands der Spielwelt zwischen zwei Zeitpunkten, für die bereits Daten vorliegen. Es gibt eine kleine Zeitspanne, die die Wahrscheinlichkeit plötzlicher Sprünge verringert, und bei erheblichen Netzwerkverzögerungen und dem Fehlen neuer Daten wird die Interpolation durch Extrapolation ersetzt. Die Extrapolation liefert keine sehr korrekten Ergebnisse, ist jedoch für den Prozessor billig und hängt nicht davon ab, wie die Schiffsbewegung auf dem Server implementiert ist, und kann manchmal natürlich die Situation retten.



Bei der Lösung des Problems der Verzögerungen hängt vieles vom Spiel und seinem Tempo ab. Ich opfere eine schnelle Reaktion auf die Aktionen des Spielers zugunsten einer reibungslosen Animation und einer genauen Übereinstimmung des Bildes mit dem Zustand der Spielwelt zu einem bestimmten Zeitpunkt. Die einzige Ausnahme ist, dass eine Kanonensalve sofort auf Knopfdruck gespielt wird. Der Rest ist auf die Gesetze des Universums und den Rumüberschuss der Schiffsbesatzung zurückzuführen :)



Vorderes Ende



Leider gibt es keine klare Struktur oder Hierarchie von Klassen und Methoden. Alle JS sind in Objekte mit eigenen Funktionen aufgeteilt, die in gewissem Sinne gleich sind. Fast alle meine vorherigen Projekte waren logischer als dieses. Dies liegt zum Teil daran, dass das erste Ziel darin bestand, das Spielweltmodell auf der Server- und Netzwerkinteraktion zu debuggen, ohne auf die Benutzeroberfläche und die visuelle Komponente des Spiels zu achten. Als es an der Zeit war, 3D hinzuzufügen, habe ich es buchstäblich zur vorhandenen Testversion hinzugefügt. Grob gesagt habe ich die 2D-DrawShip-Funktion durch genau dieselbe ersetzt, jedoch 3D, obwohl es sich auf gütliche Weise gelohnt hat, die gesamte Struktur zu überarbeiten und die Grundlage für zukünftige Änderungen vorzubereiten.



3D Schiff



Three.js unterstützt die Verwendung von vorgefertigten 3D-Modellen in verschiedenen Formaten. Ich habe mich für das GLTF / GLB-Format entschieden, in das Texturen und Animationen eingebettet werden können, d. H. Der Entwickler sollte sich nicht fragen, ob alle Texturen geladen sind.



Ich habe mich noch nie mit 3D-Editoren beschäftigt. Der logische Schritt bestand darin, einen Spezialisten für einen freiberuflichen Austausch zu kontaktieren, um ein 3D-Modell eines Segelschiffs mit einer eingebetteten Animation einer Kanonensalve zu erstellen. Aber ich konnte kleinen Änderungen am fertigen Spezialmodell selbst nicht widerstehen und endete damit, dass ich mein Modell in Blender von Grund auf neu erstellt habe. Das Erstellen eines Low-Poly-Modells mit fast keinen Texturen ist einfach und schwierig, ohne ein vorgefertigtes Modell eines Spezialisten in einem 3D-Editor zu untersuchen, was für eine bestimmte Aufgabe erforderlich ist (zumindest moralisch :).







Shader an den Gott der Shader



Der Hauptgrund, warum ich meine Shader benötige, ist die Möglichkeit, die Geometrie eines Objekts auf der Grafikkarte während des Renderns zu ändern, was eine gute Leistung bietet. Mit Three.js können Sie nicht nur Ihre eigenen Shader erstellen, sondern auch einen Teil der Arbeit übernehmen.



Der Mechanismus oder die Methode, die ich beim Erstellen eines Partikelsystems zum Animieren von Schäden an einem Schiff, einer dynamischen Wasseroberfläche oder einem statischen Meeresboden verwendet habe, ist der gleiche: Das spezielle ShaderMaterial bietet eine vereinfachte Schnittstelle für die Verwendung seines Shaders (seines GLSL-Codes). Mit BufferGeometry können Sie Geometrie aus beliebigen Daten erstellen ...



Ein leeres Leerzeichen, eine Codestruktur, die ich bequem kopieren, ergänzen und ändern konnte, um mein 3D-Objekt auf ähnliche Weise zu erstellen:



Code anzeigen
let vs = `
	attribute vec4 color;
	varying vec4 vColor;

	void main(){
		vColor = color;
		gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
		// gl_PointSize = 5.0; // for particles
	}
`;
let fs = `
	uniform float opacity;
	varying vec4 vColor;

	void main() {
		gl_FragColor = vec4(vColor.xyz, vColor.w * opacity);
	}
`;

let material = new THREE.ShaderMaterial( {
	uniforms: {
		opacity: {value: 0.5}
	},
	vertexShader: vs,
	fragmentShader: fs,
	transparent: true
});

let geometry = new THREE.BufferGeometry();

//let indices = [];
let vertices = [];
let colors = [];

/* ... */

//geometry.setIndex( indices );
geometry.setAttribute( 'position', new THREE.Float32BufferAttribute( vertices, 3 ) );
geometry.setAttribute( 'color', new THREE.Float32BufferAttribute( colors, 4 ) );

let mesh = new THREE.Mesh(geometry, material);




Schiffsschaden



Schiffsschadensanimationen sind sich bewegende Partikel, deren Größe und Farbe sich ändern. Das Verhalten wird durch ihre Attribute und den GLSL-Shader-Code bestimmt. Die Erzeugung von Partikeln (Geometrie und Material) erfolgt im Voraus, dann wird für jedes Schiff eine eigene Instanz (Mesh) von Schadenspartikeln erstellt (die Geometrie ist für alle gleich, das Material wird geklont). Es gibt viele Partikelattribute, aber der erstellte Shader implementiert gleichzeitig große, sich langsam bewegende Staubwolken und schnell fliegende Trümmer sowie Feuerpartikel, deren Aktivität vom Grad der Beschädigung des Schiffes abhängt.







Meer



Das Meer wird auch mit ShaderMaterial implementiert. Jeder Scheitelpunkt bewegt sich in alle drei Richtungen entlang einer Sinuskurve und bildet zufällige Wellen. Die Attribute definieren die Amplituden für jede Bewegungsrichtung und die Phase der Sinuskurve.



Um die Farben auf dem Wasser zu variieren und das Spiel für das Auge interessanter und angenehmer zu gestalten, wurde beschlossen, den Boden und die Inseln hinzuzufügen. Die Grundfarbe hängt von der Höhe / Tiefe ab und scheint durch die Wasseroberfläche, wodurch dunkle und helle Bereiche entstehen.



Der Meeresboden wird aus einer Höhenkarte erstellt, die in zwei Schritten erstellt wurde: Zuerst wurde der Boden ohne Inseln in einem grafischen Editor erstellt (in meinem Fall wurden die Werkzeuge gerendert -> Wolken und Gaußsche Unschärfe), dann wurden die Inseln in zufälliger Reihenfolge mit Canvas JS online auf jsFiddle hinzugefügt Zeichnen eines Kreises und Verwischen. Einige Inseln sind niedrig, durch sie kann man auf Gegner schießen, andere haben eine bestimmte Höhe, Schüsse gehen nicht durch sie hindurch. Zusätzlich zur Höhenkarte selbst erhalte ich am Ausgang Daten im JSON-Format über die Inseln (ihre Position und Größe) für die Physik auf dem Server.







Was weiter?



Es gibt viele Pläne für die Entwicklung des Spiels. Die wichtigsten sind neue Spielmodi. Kleinere - Überlegen Sie sich Schatten / Reflexionen auf dem Wasser, und berücksichtigen Sie dabei die Leistungsbeschränkungen von WebGL und JS. Ich habe bereits die Möglichkeit erwähnt, den Kraken aufzuwecken :) Die Vereinigung der Spieler in Räume aufgrund ihrer gesammelten Erfahrung wurde noch nicht umgesetzt. Eine offensichtliche, aber nicht zu vorrangige Verbesserung besteht darin, mehrere Karten des Meeresbodens und der Inseln zu erstellen und eine davon zufällig für eine neue Schlacht auszuwählen.



Sie können viele visuelle Effekte erzeugen, indem Sie die Szene wiederholt "in den Speicher" zeichnen und dann alle Daten in einem Bild kombinieren (tatsächlich kann dies als Nachbearbeitung bezeichnet werden), aber meine Hand erhebt sich nicht, um die Belastung des Clients auf diese Weise zu erhöhen, da der Client immer noch ein Browser ist eher als eine native App. Vielleicht werde ich eines Tages über diesen Schritt entscheiden.



Es gibt auch Fragen, die ich jetzt nur schwer beantworten kann: Wie viele Online-Spieler kann ein billiger virtueller Server aushalten, wird es möglich sein, mindestens eine bestimmte Anzahl interessierter Spieler zu sammeln und wie es geht.



Osterei



Wer erinnert sich nicht gerne an alte Computerspiele, die so viele Emotionen hervorriefen? Ich liebe es, das Spiel Corsairs 2 (auch bekannt als Sea Dogs 2) bisher immer wieder zu spielen. Ich konnte nicht anders, als meinem Spiel ein Geheimnis hinzuzufügen, das explizit und indirekt an "Corsairs 2" erinnert. Ich werde nicht alle Karten aufdecken, aber ich werde Ihnen einen Hinweis geben: Mein Osterei ist ein bestimmtes Objekt, das Sie finden können, wenn Sie das Meer erkunden (Sie müssen nicht weit über das endlose Meer segeln, das Objekt ist in Ordnung, aber die Wahrscheinlichkeit, es zu finden, ist immer noch nicht hoch). Das Osterei repariert das beschädigte Schiff vollständig.



Was ist passiert



Minutenvideo (Test von 2 Geräten):





Link zum Spiel: https://sailfire.pw



Es gibt auch ein Kontaktformular, Nachrichten werden mir in Telegrammen gesendet: https://sailfire.pw/feedback/

Links für diejenigen, die über Neuigkeiten und Updates auf dem Laufenden bleiben möchten: VK Public , Telegrammkanal



All Articles