Portierung der Surreal Engine C++ auf WebAssembly

In diesem Beitrag beschreibe ich, wie ich die Surreal Engine-Spiel-Engine auf WebAssembly portiert habe.

Surreal Engine – eine Spiel-Engine, die die meisten Funktionen der Unreal Engine 1 implementiert, berühmte Spiele auf dieser Engine – Unreal Tournament 99, Unreal, Deus Ex, Undying. Es bezieht sich auf klassische Engines, die hauptsächlich in einer Single-Threaded-Ausführungsumgebung arbeiteten.

Anfangs hatte ich die Idee, ein Projekt zu übernehmen, das ich nicht in angemessener Zeit abschließen konnte, und so meinen Twitch-Followern zu zeigen, dass es Projekte gibt, die selbst ich nicht schaffen kann. Während meines ersten Streams wurde mir plötzlich klar, dass die Aufgabe, Surreal Engine C++ mit Emscripten auf WebAssembly zu portieren, machbar ist.

Surreal Engine Emscripten Demo

Nach einem Monat kann ich meine Gabel- und Motorbaugruppe auf WebAssembly demonstrieren:
https://demensdeum.com/demos/SurrealEngine/

Die Steuerung erfolgt wie im Original über die Tastaturpfeile. Als nächstes plane ich, es für die mobile Steuerung (Tachi) anzupassen und dabei die richtige Beleuchtung und andere grafische Funktionen des Unreal Tournament 99-Renderings hinzuzufügen.

Wo soll ich anfangen?

Das erste, was ich sagen möchte, ist, dass jedes Projekt mit Emscripten von C++ nach WebAssembly portiert werden kann. Die Frage ist nur, wie vollständig die Funktionalität sein wird. Wählen Sie ein Projekt, dessen Bibliotheksports bereits für Emscripten verfügbar sind; im Fall von Surreal Engine haben Sie großes Glück, denn Die Engine verwendet die Bibliotheken SDL 2, OpenAL – Sie sind beide auf Emscripten portiert. Allerdings kommt als Grafik-API Vulkan zum Einsatz, was aktuell nicht für HTML5 verfügbar ist, an der Implementierung von WebGPU wird gearbeitet, befindet sich aber ebenfalls im Entwurfsstadium und es ist auch unbekannt, wie einfach die weitere Portierung von Vulkan auf WebGPU sein wird , nachdem es vollständig standardisiert ist. Daher musste ich mein eigenes grundlegendes OpenGL-ES/WebGL-Rendering für die Surreal Engine schreiben.

Erstellung des Projekts

System in Surreal Engine erstellen – CMake, was auch die Portierung vereinfacht, weil Emscripten stellt seinen nativen Buildern – emcmake, emmake.
Die Surreal Engine-Portierung basierte auf dem Code meines neuesten Spiels in WebGL/OpenGL ES und C++ namens Death-Mask. Dadurch war die Entwicklung viel einfacher, ich hatte alle notwendigen Build-Flags und Codebeispiele dabei.

Einer der wichtigsten Punkte in CMakeLists.txt sind die Build-Flags für Emscripten, unten ist ein Beispiel aus der Projektdatei:


-s MAX_WEBGL_VERSION=2 \

-s EXCEPTION_DEBUG \

-fexceptions \

--preload-file UnrealTournament/ \

--preload-file SurrealEngine.pk3 \

--bind \

--use-preload-plugins \

-Wall \

-Wextra \

-Werror=return-type \

-s USE_SDL=2 \

-s ASSERTIONS=1 \

-w \

-g4 \

-s DISABLE_EXCEPTION_CATCHING=0 \

-O3 \

--no-heap-copy \

-s ALLOW_MEMORY_GROWTH=1 \

-s EXIT_RUNTIME=1")

Das Build-Skript selbst:


emmake make -j 16

cp SurrealEngine.data /srv/http/SurrealEngine/SurrealEngine.data

cp SurrealEngine.js /srv/http/SurrealEngine/SurrealEngine.js

cp SurrealEngine.wasm /srv/http/SurrealEngine/SurrealEngine.wasm

cp ../buildScripts/Emscripten/index.html /srv/http/SurrealEngine/index.html

cp ../buildScripts/Emscripten/background.png /srv/http/SurrealEngine/background.png

Als nächstes bereiten wir den Index vor .html , das den Projektdateisystem-Preloader enthält. Zum Hochladen ins Web habe ich die Unreal Tournament Demo-Version 338 verwendet. Wie Sie der CMake-Datei entnehmen können, wurde der entpackte Spielordner zum Build-Verzeichnis hinzugefügt und als Preload-Datei für Emscripten verlinkt.

Hauptcodeänderungen

Dann war es notwendig, die Spielschleife des Spiels zu ändern, man kann keine Endlosschleife ausführen, das führt zum Einfrieren des Browsers, stattdessen muss man emscripten_set_main_loop verwenden, über diese Funktion habe ich in meiner Notiz von 2017 geschrieben „< a href="https://demensdeum.com /blog/ru/2017/03/29/porting-sdl-c-game-to-html5-emscripten/" rel="noopener" target="_blank">SDL C++-Spiel auf HTML5 (Emscripten) portieren“
Wir ändern den Code zum Beenden der while-Schleife in if, zeigen dann die Hauptklasse der Spiel-Engine, die die Spielschleife enthält, im globalen Bereich an und schreiben eine globale Funktion, die den Spielschleifenschritt vom globalen Objekt aus aufruft :


#include <emscripten.h>

Engine *EMSCRIPTEN_GLOBAL_GAME_ENGINE = nullptr;

void emscripten_game_loop_step() {

	EMSCRIPTEN_GLOBAL_GAME_ENGINE->Run();

}

#endif

Danach müssen Sie sicherstellen, dass in der Anwendung keine Hintergrundthreads vorhanden sind. Wenn ja, dann bereiten Sie sich darauf vor, diese für die Single-Thread-Ausführung neu zu schreiben, oder verwenden Sie die phtread-Bibliothek in Emscripten.
Der Hintergrund-Thread in Surreal Engine wird zum Abspielen von Musik verwendet. Vom Haupt-Engine-Thread kommen Daten über den aktuellen Titel, die Notwendigkeit, Musik abzuspielen, oder dessen Fehlen. Anschließend erhält der Hintergrund-Thread über einen Mutex einen neuen Status und beginnt mit der Wiedergabe neuer Musik , oder pausiert es. Der Hintergrundstream wird auch zum Puffern von Musik während der Wiedergabe verwendet.
Meine Versuche, die Surreal Engine für Emscripten mit pthread zu erstellen, waren erfolglos, da die SDL2- und OpenAL-Ports ohne pthread-Unterstützung erstellt wurden und ich sie nicht aus Gründen der Musik neu erstellen wollte. Daher habe ich die Funktionalität des Hintergrundmusik-Streams mithilfe einer Schleife auf die Single-Threaded-Ausführung übertragen. Durch das Entfernen von pthread-Aufrufen aus dem C++-Code habe ich die Pufferung und Musikwiedergabe in den Hauptthread verschoben, damit es keine Verzögerungen gibt, ich habe den Puffer um einige Sekunden erhöht.

Als nächstes werde ich spezifische Implementierungen von Grafik und Sound beschreiben.

Vulkan wird nicht unterstützt!

Ja, Vulkan wird in HTML5 nicht unterstützt, obwohl alle Marketingbroschüren die plattformübergreifende und breite Plattformunterstützung als Hauptvorteil von Vulkan darstellen. Aus diesem Grund musste ich meinen eigenen grundlegenden Grafikrenderer für einen vereinfachten OpenGL-Typ schreiben – ES, es wird auf mobilen Geräten verwendet, enthält manchmal nicht die modischen Funktionen des modernen OpenGL, lässt sich aber sehr gut auf WebGL portieren, was genau das ist, was Emscripten implementiert. Das Schreiben des grundlegenden Kachel-Renderings, des BSP-Renderings für die einfachste GUI-Anzeige und des Renderns von Modellen und Karten wurde in zwei Wochen abgeschlossen. Dies war vielleicht der schwierigste Teil des Projekts. Es liegt noch viel Arbeit vor uns, die volle Funktionalität des Surreal Engine-Renderings zu implementieren, daher ist jede Hilfe von Lesern in Form von Code und Pull-Requests willkommen.

OpenAL unterstützt!

Das große Glück ist, dass Surreal Engine OpenAL für die Audioausgabe verwendet. Nachdem ich eine einfache Hallo-Welt in OpenAL geschrieben und sie mit Emscripten in WebAssembly zusammengestellt hatte, wurde mir klar, wie einfach alles war, und ich machte mich an die Portierung des Sounds.
Nach mehreren Stunden des Debuggens stellte sich heraus, dass die OpenAL-Implementierung von Emscripten mehrere Fehler aufweist. Beispielsweise gab die Methode beim Initialisieren des Lesens der Anzahl der Monokanäle eine unendliche Zahl zurück, und nachdem versucht wurde, einen Vektor unendlicher Größe zu initialisieren, C++ stürzt mit der Ausnahme vector::length_error ab.

Wir haben es geschafft, dies zu umgehen, indem wir die Anzahl der Monokanäle auf 2048 fest codiert haben:


		alcGetIntegerv(alDevice, ALC_STEREO_SOURCES, 1, &stereoSources);



#if __EMSCRIPTEN__

		monoSources = 2048; // for some reason Emscripten's OpenAL gives infinite monoSources count, bug?

#endif



Gibt es ein Netzwerk?

Surreal Engine unterstützt derzeit kein Online-Spielen, das Spielen mit Bots wird unterstützt, aber wir brauchen jemanden, der KI für diese Bots schreibt. Theoretisch können Sie mithilfe von Websockets ein Netzwerkspiel auf WebAssembly/Emscripten implementieren.

Schlussfolgerung

Abschließend möchte ich sagen, dass die Portierung der Surreal Engine aufgrund der Verwendung von Bibliotheken, für die es Emscripten-Portierungen gibt, sowie meiner bisherigen Erfahrung bei der Implementierung eines Spiels in C++ für WebAssembly recht reibungslos verlief auf Emscripten. Nachfolgend finden Sie Links zu Wissensquellen und Repositories zum Thema.
M-M-M-MONSTER TÖTEN!

Außerdem, wenn Sie dem Projekt helfen möchten, vorzugsweise mit WebGL/OpenGL ES-Rendering-Code, dann schreiben Sie mir per Telegram:
https://t.me/demenscave

Links

https://demensdeum.com/demos/SurrealEngine/
https://github.com/demensdeum/SurrealEngine-Emscripten

https://github.com/dpjudas/SurrealEngine

Einfacher Emscripten-Autotest für ChromeDriver

In diesem Hinweis beschreibe ich die Implementierung eines Autotests für den ChromeDriver des Chrome-Browsers, der mit Emscripten einen aus C++ übersetzten Modul-Autotest ausführt, die Konsolenausgabe liest und das Testergebnis zurückgibt.
Zuerst müssen Sie Selenium installieren, für Python 3-Ubuntu geht das so:

pip3 install selenium

Laden Sie als Nächstes ChromeDriver von der offiziellen Website herunter und legen Sie chromedriver beispielsweise in /usr/local/bin ab. Danach können Sie mit der Implementierung des Autotests beginnen.
Im Folgenden gebe ich den Autotest-Code an, der den Chrome-Browser startet, während die Autotest-Seite auf Emscripten geöffnet ist, und prüft, ob der Text „Fenstertest erfolgreich“ vorhanden ist:

import time
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities

capabilities = DesiredCapabilities.CHROME
capabilities['goog:loggingPrefs'] = { 'browser':'ALL' }
driver = webdriver.Chrome()
driver.get("http://localhost/windowInitializeTest/indexFullscreen.html")

time.sleep(2)

exitCode = 1

for entry in driver.get_log('browser'):
    if entry["source"] == "console-api":
        message = entry["message"]
        if "Window test succeded" in message:
            print("Test succeded")
            exitCode = 0

driver.close()
exit(exitCode)

Speichern Sie den Test als main.py und führen Sie python3 main.py aus

Erstellen eines Projekts mit Abhängigkeiten für Emscripten

In diesem Beitrag beschreibe ich den Aufbau eines Projekts, das aus mehreren Bibliotheken besteht, mit Emscripten.
Derzeit unterstützt Emscripten den Aufbau gemeinsam genutzter Bibliotheken nicht, daher besteht der erste Schritt darin, alle Bibliotheken von „Shared“ auf „Static“ zu übertragen. Emscripten arbeitet mit seinen eigenen Include-Dateien, daher muss das Problem mit der Sichtbarkeit von Header-Dateien gelöst werden, indem ich einen Symlink aus dem Systemverzeichnis an die Emscripten-Toolchain weitergeleitet habe:

ln -s /usr/local/include/FlameSteelFramework $EMSDK/fastcomp/emscripten/system/include/FlameSteelFramework

Wenn Sie CMake verwenden, müssen Sie SHARED->STATIC in der Datei CMakeLists.txt der Methode add_library ändern. Mit den folgenden Befehlen können Sie eine Bibliothek/Anwendung für weitere statische Verknüpfungen erstellen:

emcmake cmake .
emmake make

Als nächstes müssen Sie die Hauptanwendung erstellen und beim Verknüpfen *.a-Bibliotheksdateien angeben. Ich konnte keinen relativen Pfad angeben; der Build wurde erst korrekt abgeschlossen, nachdem die vollständigen Pfade in der Datei CMakeLists.txt angegeben wurden:

elseif(EMSCRIPTEN)
target_link_libraries(${FSEGT_PROJECT_NAME} GL GLEW 
/home/demensdeum/Sources/cube-art-project-bootstrap/cube-art-project/sharedLib/libCubeArtProject.a 
/home/demensdeum/Sources/cube-art-project-bootstrap/FlameSteelFramework/FlameSteelEngineGameToolkitFSGL/libFlameSteelEngineGameToolkitFSGL.a 
/home/demensdeum/Sources/cube-art-project-bootstrap/FlameSteelFramework/FlameSteelEngineGameToolkit/libFlameSteelEngineGameToolkit.a 
/home/demensdeum/Sources/cube-art-project-bootstrap/FlameSteelFramework/FlameSteelCore/libFlameSteelCore.a 
/home/demensdeum/Sources/cube-art-project-bootstrap/FlameSteelFramework/FlameSteelBattleHorn/libFlameSteelBattleHorn.a 
/home/demensdeum/Sources/cube-art-project-bootstrap/FlameSteelFramework/FSGL/libFSGL.a 
/home/demensdeum/Sources/cube-art-project-bootstrap/FlameSteelFramework/FlameSteelCommonTraits/libFlameSteelCommonTraits.a)
else()

Quellen

https://emscripten.org/ docs/compiling/Building-Projects.html#using-libraries

Verlorene Emscripten-Ausnahmen und Regex-Probleme

Verlorene Ausnahme

Eine interessante Funktion von Emscripten: Wenn Sie eine Spielschleife über emscripten_set_main_loop starten, sollten Sie bedenken, dass die Ausnahmebehandlung über try Catch direkt in der Schleifenmethode erneut hinzugefügt werden muss, weil Laufzeit verliert Try-Catch-Block von außen.
Am einfachsten ist es, den Fehlertext über den Browser per Javascript-Benachrichtigung anzuzeigen:

            catch (const std::exception &exc)
            {
                const char *errorText = exc.what();
                cout << "Exception: " << errorText << "; Stop execution" << endl;

                EM_ASM_(
                {
                    var errorText = UTF8ToString($0);
                    alert(errorText);

                }, errorText);

                abort();

Zu komplexer regulärer Ausdruck

Die Standardimplementierung von Regex kann eine error_complexity-Ausnahme auslösen, wenn sie den regulären Ausdruck für zu komplex hält. Dies geschieht in der aktuellen Implementierung von emscripten, daher empfehle ich Ihnen, Tests zum Parsen durch reguläre Ausdrücke zu implementieren oder Regex-Implementierungen von Drittanbietern zu verwenden.