My Virtual Pet 3.0
Von Michael Stidl am 05.11.2023
Grundkonzept und Updates
My Virtual Pet ist ein von Tamagotchis inspiriertes Spiel, welches ich 2021/22 begonnen habe zu programmieren. Hierzu finden sich auch schon zwei frühere Blogeinträge unter dem gleichen Namen. Die erste Version des Spiels diente mehr dazu die Spieleentwicklung mit JavaScript und Unity zu vergleichen bzw. meine JavaScript Kenntnisse zu verbessern. Das Design des Spiels lag hierbei im Hintergrund, da der Fokus mehr auf der Implementierung der verschiedenen Funktionen lag. Bei der nächsten Version “My Virtual Pet 2.0”, lag wiederum der Fokus stark auf Design und zusätzlich darauf einen neuen Spielmodus zu implementieren – den Quiz-Modus. Bei der neuesten Version – 3.0, habe ich das mehr dreidimensionale Design der Charaktere von der 2.0 Version aufgegriffen und einen 3D Modus implementiert. Zusätzlich habe ich erhaltene Kritik aufgenommen und aufgrund dieser, das Spielerlebnis generell versucht zu verbessern.
Was ist Three.js?
Three.js ist eine JavaScript Bibliothek und ein 3D-Grafik-Framework, welches die Erstellung von 3D Grafiken und Animationen für Webanwendungen erleichtert. Es benutzt WebGL (Web Graphics Library), eine API welche speziell für das Web entwickelt wurde und bietet eine benutzerfreundliche Schnittstelle zur Erstellung von 3D Inhalten.
Starten mit Three.js:
Die Installation von Three.js ist mit ein paar Zeilen im Terminal schnell erledigt. Auf threejs.org findet man eine gute Dokumentation und einen Installation Guide.
Zusätzlich benötigt man noch Vite, ein Build-Tool und eine Entwicklungsumgebung speziell für webbasierte JavaScript Projekte. Hierdurch wird der Entwicklungs- und Build-Prozess optimiert. Die Installation war auch mit ein paar Kommandozeilen im Terminal erledigt, dauert nur ein bisschen länger zum Runterladen und Installieren.
First Steps – Szene, Kamera und Renderer:
Zuerst muss man ganz oben im Code dem Import erstellen mit: import * as THREE from ‘three’;
Um 3D Objekte anzeigen zu können benötigt man dann eine Szene, eine Kamera und einen Renderer:
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
const renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
Es stehen mehrere verschiedne Kameras zur Auswahl, wie Orthographic Camera und Stereo Camera, die Perspective Camera ist jedoch die, die die Inhalte am ehesten darstellt so wie das Menschliche Auge es sehen würde.
Nachdem die drei essentiellen Elemente erstellt wurden, fehlt nur noch ein Objekt. Hier hat man zwei Möglichkeiten. Primitive Formen mittels Three.js erstellen oder ein fertiges 3D Objekt, welches mit einem 3D Programm modelliert wurde, laden. In My Virtual Pet habe ich beides gemacht.
Erstellen von Formen:
const geometry = new THREE.SphereGeometry(0.05, 10, 10);
const material = new THREE.MeshStandardMaterial( {color: 0xffffff});
const star = new THREE.Mesh( geometry, material);
scene.add(star);
Hier habe ich mich dazu entschlossen, Kugeln zu erstellen, welche im Spiel kleine Sterne repräsentieren sollen, die verteilt den Hintergrund füllen sollen und so für mehr Tiefe sorgen sollen.
Die erste Codezeile ist für das “Gerüst” der Form. Hier werden Punkte erstellt (Vertices) Welche dann gefüllt werden (faces) und eine Form entstehen lassen. Hier habe ich mich für die Sphere (Kugel) entschieden, aber es gibt weitere primitive Formen wie Box, Circle, Cylinder, Plane usw..
Die zweite Codezeile ist für das Material, welchem wir eine Farbe zuweisen können, um die Form einzufärben.
In der dritten Codezeile machen wir ein Mesh. Das Mesh ist ein Objekt welches die Form nimmt und mit dem Material verbindet, dass wir es in der Szene platzieren und bewegen können
In der vierten und letzten Codezeile fügen wir die fertige Kugel unserer Szene hinzu.
Hiermit habe ich die Kugeln dann noch mit zufälligen Koordinaten im Spiel verteilt:
const [x, y, z] = Array(3).fill().map(() => THREE.MathUtils.randFloatSpread(100));
star.position.set(x, y, z);
Lichtquelle:
Zusätzlich kann man noch eine Lichtquelle in der Szene platzieren. Hier gibt es verschiedene zur Auswahl, wie Ambient Light, Directional Light und Point Light.
Hier ein Beispiel wie man ein Ambient Light hinzufügt:
const light = new THREE.AmbientLight( 0x404040 );
scene.add( light );
Im Konstruktor werden jeweils die Intensität des Lichts sowie die Farbe angegeben. Danach fügt man das Licht der Szene hinzu. Im Spiel habe ich ein Ambient Light verwendet, welches alle Objekte in der Szene gleich beleuchtet und ein Point Light, für eine Stehlampe, welches Licht von einem Punkt in alle Richtungen ausstrahlt.
Orbit Controls:
Eine weitere Funktion, die ich eingebaut habe, ist Orbit Controls. Orbit Controls erlaubet einem die Kamera um ein Objekt zu schwenken und zu zoomen. Diese Funktion ermöglicht es im Spiel den Raum samt Inhalten zu drehen und hinein bzw. hinaus zu zoomen.
Zuerst muss man es oben im Code wieder implementieren, mit:
import { OrbitControls } from ‘three/addons/controls/OrbitControls.js’;
Danach weist man den Konstruktor wieder einer Variable zu. Hier gibt man die Kamera, welche kontrolliert werden soll, an und das HTML Dom Element.
In der Zeile danach wird dann nach jeder Änderung der Kamera der Renderer aufgerufen, um die Szene zu erneuern:
const controls = new OrbitControls( camera, renderer.domElement );
controls.addEventListener( ‘change’, render );
Um den Zoom einzuschränken, habe ich noch eine minimum und maximum Distanz angegeben, mit:
controls.minDistance = 0.6;
controls.maxDistance = 7;
3D Objekte laden:
Die Objekte, welche ich mit Womp modelliert habe, können entweder als OBJ oder STL exportiert werden. In der Three.js habe ich schnell eine Möglichkeit gefunden OBJ Dateien zu laden, mittels dem OBJLoader. Leider hatte ich Probleme die 3D-Modelle damit zu laden. Die Objekte sind zwar geladen worden, jedoch nicht mit den passenden Farben und Texturen, speziell beim Charakter. Auch nach längerem recherchieren und rum probieren habe ich es nicht geschafft die OBJ Dateien mit den richtigen Farben geladen zu bekommen. Meine Lösung war auf einen anderen Loader bzw. auf eine andere Dateiart zu wechseln.
Ein Loader der bei meiner Recherche häufig aufgekommen ist, ist der GLTFLoader. Um diesen für meine schon in Womp modellierten Objekte verwenden zu können, habe ich die exportierten OBJ Dateien in Blender, einer anderen 3D Software, importiert. Hier wurden sie ohne Probleme mit den richtigen Farben und Texturen geladen. Die importierten OBJ Dateien habe ich somit dann als GLTF/GLB Dateien exportieren können und dann mit dem GLTFLoader ohne Probleme laden können.
Das einzige noch bestehende Problem war, dass ich einige Glasoberflächen mit dem entsprechendem Material in den Modellen hatte und diese leider nicht durchsichtig, wie Glas sein sollte, geladen wurden. Ich habe dann die dementsprechenden Oberflächen aus den Modellen gelöscht und leer gelassen, sodass zumindest die Illusion besteht und man hindurchsehen kann.
Der Code für den GLTFLoader sieht wie folgt aus:
import { GLTFLoader } from ‘three/addons/loaders/GLTFLoader.js’;
const loader = new GLTFLoader();
loader.load(
‘CharacterTest.glb’,
scene.add( gltf.scene );
},
function ( xhr ) {
console.log( ( xhr.loaded / xhr.total * 100 ) + ‘% loaded’ );
},
function ( error ) {
console.log( ‘An error happened’ );
} );
In der ersten Codezeile wird der Loader importiert. Danach initiiert. Dann folgt das eigentliche Laden des Objekts, wo man die URL vom Objekt angibt, welches man laden möchte. in der folgenden Funktion wird es dann zu der Szene hinzugefügt. die folgenden zwei Funktionen sind um anzuzeigen wie weit das entsprechende Objekt geladen ist und eine Meldung falls ein Error beim Laden passiert.
Bedürfnisse füllen in 3D Umgebung:
Um den neugewonnenen 3D Bereich zu nutzen, habe ich einen Raum modelliert und ihn mit Objekten gefüllt, welche die Bedürfnisse des Charakters repräsentieren. Für den Hunger einen Kühlschrank, für das Spielen einen Laptop, für das Waschen eine Dusche und für das Schlafen ein Bett.
Damit der Charakter lebendiger wird, wird er bei jedem Betätigen der Bedürfnis-Buttons zu dem dementsprechenden Objekt bewegt. Hierfür ändere ich sowohl die Position als auch die Rotation des Charakters, wenn der Button gedrückt wurde und setze die Werte nach kurzer Zeit wieder auf die Ursprungswerte zurück, um den Character wieder auf die Ausgangsposition zu bringen.
Bedürfnisse sinken offline
Eine Kritik von der letzten Version war, dass die Bedürfnisse auch offline sinken sollen, um es ähnlicher zu einem richtigen Tamagotchi zu machen und um das Spielerlebnis zu verbessern.
Hierfür habe ich das “beforeunload”-Event verwendet. Dieses wird aktiviert, wenn das aktive Fenster aktualisier oder geschlossen wird.
Sobald das Event getriggert wird, werden alle benötigten Werte, wie die von den verschiedenen Bedürfnissen und zusätzlich ein Timestamp in den Local Storage gespeichert.
Bei dem folgenden Start vom Spiel wird der Wert vom gespeicherten Timestamp das von der jetzigen Zeit abgezogen und durch tausend dividiert, um einen Wert zu erhalten, der von den Bedürfnissen abgezogen wird.
Zuerst Werte Speichern, bevor Fenster geschlossen wird(Hier nur am Beispiel vom Timestamp):
window.addEventListener(“beforeunload”, function() {
window.localStorage.setItem(“lastPlayedTimestamp”, new Date().getTime().toString());
});
Bei neu laden, entsprechenden Wert von Bedürfnissen abziehen (Hier nur am Beispiel vom Hunger-Bedürfnis):
function offlineDepleation () {
var currentTime = new Date().getTime();
var lastPlayedTimestamp = localStorage.getItem(“lastPlayedTimestamp”);
var timeElapsed = lastPlayedTimestamp ? currentTime – parseInt(lastPlayedTimestamp) : 0;
var deductedOfflineValue = Math.floor(timeElapsed / 1000);
hunger -= deductedOfflineValue;
if (hunger <= 0) {
hunger = 0;
}
}
In der ersten Codezeile speichere ich mir die aktuelle Zeit ab (welche in Millisekunden seit Jänner 1, 1970 abgespeichert wird) und in der zweiten Zeile hole ich mir die Zeit zu der zuletzt gespielt wurde aus dem Local Storage. Die nächste Zeile kontrolliert ob ein lastPlayedTimestamp existiert, wandelt ihn in einen Integer um und rechnet die Differenz zwischen diesem und der jetzigen Zeit aus. Wenn kein lastPayedTimestamp existiert, wird der Wert 0 genommen. Dieser Wert wird in der nächsten Zeile dann durch tausend dividiert, um Sekunden zu erhalten bzw. einen Wert welcher klein genug ist, um ihn von den Werten der Bedürfnisse abziehen zu können, welches in der darauffolgenden Zeile passiert. Zuletzt wird noch kontrolliert, dass der Wert des Bedürfnisses nicht kleiner als 0 wird.
3D- und 2D-Modus + abgeänderter Startscreen
Der Startscreen hat sich im Vergleich zu der alten Version etwas geändert. Ich habe die alten Sprites von der ersten Version den Spiels wieder hinzugefügt und in den “2D Modus” gesteckt. Somit hat der Spieler die Wahl in 3D oder 2D zu Spielen und die alten Sprites bleiben erhalten.
Der “Survival Mode” von der letzten Version wurde zu den 3D- und 2D-Mode und der Quiz-Modus wurde in den Continue Screen versetzt.
Da das Spiel jetzt automatisch speichert, wurde der “Load Game”-Button unten rechts zum “New Game” -Button. Hier werden die gespeicherten Werte im Local Storage zurückgesetzt, sodass man nicht wieder beim neuen Continue Screen landet (siehe unten), sondern wieder beim diesem Startscreen.
Geld Funktion
Um das Spiel noch ähnlicher zu einem Tamagotchi zu machen, habe ich den Timer bzw. die “Survival” Funktion vom Spiel entfernt. Der Timer, welcher links unter den Bedürfnissbalken war, habe ich durch eine neue Geld Funktion ersetzt. Alle 10 Sekunden, die man aktiv im Spiel verbringt, gewinnt man 1$ dazu. Diese Währung kann man dann dazu verwenden eine neue Version deines ausgewählten Charakters freizuschalten. Diese Funktion soll dazu anregen, mehr Zeit im Spiel selbst zu verbringen und öfter zu spielen.
Continue Screen
Der Continue Screen wird angezeigt, sobald man das Spiel aufruft und davor schon mal ein Spiel angefangen hat. Hier kontrolliere ich mit einer Funktion, ob schon Werte im Local Storage abgespeichert wurden und zeige dann dementsprechend nur gewisse Elemente an. Mit dem New Game Button werden diese Werte wieder zurückgesetzt und man landet wieder beim eigentlichen Startscreen.
3D Assets
Die unten dargestellten 3D-Assets habe ich mithilfe von Womp-3D modelliert. Zu Womp-3D habe ich auch schon einen früheren Blogeintrag erstellt.
Learnings
Mit diesem Projekt habe ich gelernt mit Three.js zu arbeiten, was für mich etwas ganz neues war. Ich habe auch generell mehr gelernt wie man mit 3D Objekten arbeitet und diese in Applikationen einbindet. Bis dahin habe ich mich nur etwas mit der Modellierung von 3D Objekten beschäftigt gehabt. Zusätzlich habe ich mich noch mehr mit dem Spielerlebnis selbst auseinandergesetzt. Bei der ersten Version des Spiels lag der Fokus sehr darauf etwas in mehreren Technologien umzusetzen und diese Technologien zu vergleichen und nicht auf dem Spiel selbst. In der zweiten Version habe ich mich dann auf das Design konzentriert und darauf einen neuen Spielmodus hinzuzufügen, ohne das eigentliche Spiel zu verändern. Bei dieser Version habe ich das Gefühl einen guten Abschluss erreicht zu haben, indem ich sowohl eine neue Technologie erlernt habe als auch das gesamte Spielerlebnis verbessern konnte, zu der vorherigen Version. Das Spiel fühlt sich auf jeden Fall “fertiger” an, wobei man natürlich immer verbessern und erweitern kann.
The comments are closed.