Nachdem unser Konzept stand, haben wir als Gruppe beschlossen, dass wir als Erstes einen kleinen spielbaren Prototyp haben wollen,
in welchem wir unsere Spielidee und unser Konzept testen können.

Grund dafür war, dass man so relativ schnell feststellen kann, ob unser Konzept überhaupt funktioniert und dem Spieler spaß machen kann.

Sozusagen wurde an diesen Punkt entschieden, ob wir das Spiel so weiter führen können, wie geplant oder ob wir fundamentale Änderung vornehmen müssen.

Unser derzeitiger Stand ist ein Raum mit Spielelementen, wie einen Teich, einer Sandbank und diverse Felsen.
Zu diesen reiht sich eine saftige Wiese, die von Bäumen als Raumtrenner umgeben ist.
Unsere Hauptfigur muss sich im Prototyp - Raum gegen eine Armee von explodierenden Lilabären, aggressiven, auf Nahkampf fixierten Braunbären
und Pfeile schießende, kleine Hamster durchsetzen.
Dafür steht dem Spieler ein Arsenal an Waffen zur Verfügung: Pistole, MG, Shotgun, Raketenwerfer.

Der Spieler erleidet passend zum Angriff schaden, ebenso kann der Spieler den Tieren schaden.
Der Spieler erhält Feedback, wenn er den Tieren Schaden zufügt oder sie tötet.

Ein Menü zum Start, bei Pause und beim Tod steht dem Spieler zur Verfügung.

In Anbetracht unseres derzeitigen Standes sind wir überzeugt, dass wir ein gutes und spaßiges Spiel entwickeln können
und halten am bestehenden Konzept fest.

 

Hier folgt eine Zusammenfassung, wie der Prototyp entstanden ist:

Als erstes wurde das Unity Projekt erstellt, ein Dummy und unsere Skizze des Main Charakters eingefügt.
Dazu wurden dann die ersten Einstellungen, wie Collider vorgenommen und einfache Skripte angepasst.

 

Daraufhin haben wir begonnen, die Spielwelt zu entwickeln.
Grundstein dafür war der Boden und die Wände.

 

 

Um die Welt noch lebendiger aussehen zu lassen haben wir einige Spielobjekte konzeptioniert,
erstellt und in die Welt implementiert.


 

Abgerundet wird der Prototypraum
von einer Auswahl an Gegnern:


 

Um einen tieferen Einblick in die Entwicklung zu ermöglichen,
gewähren wir eine kleine Darstellung dessen am Beispiel
unseres pfeilschiessenden Hamsters.

 

Angefangen haben wir mit einer ersten Skizze.

 

 

Auf dieser Skizze haben wir den Charakter aufgebaut und seine Walkcycles passend zum Grundentwurf ausgearbeitet.
Der Charakter hat 6 Walkcycles, die wir alle auf ein „Atlas“ gespeichert haben, um „draw-calls“ von Unity zu vermindern
und somit eine höhere Performance zu gewährleisten.

 



Der Atlas in Animationsabläufe übersetzt sieht wie folgt aus.



Animationen aus der frühen Phase, damals noch fehlerhaft

 

Nun müssen wir Unity mitteilen, woher er wissen soll, wann er welche Animation abspielen soll.
Dafür nutzen wir den Animator.

 

 

Die Variable MoveDir vom Typ Integer erhält, je nachdem in welche Richtung der Hamster laufen soll, einen
bestimmten Wert (oben = 1; rechts = 2, unten = 3, links = 4).
Selbes gilt für die Variable AttackDir, welche dem Animator sagen soll, in welche Richtung der Hamster angreifen soll.

Die einzelnen Animationen verbinden wir miteinander und geben dem Spiel die Anweisung, 
von der alten Animation zu der neuen Animation zu wechseln, wenn AttackDir oder MovementDir den Wert X erhält.

 

 

Nachdem wir die Animation erstellt und Unity mitgeteilt haben, wann er welche Animation einsetzten soll, 
kümmern wir uns um das Verhalten von unserem Hamster, anders gesagt, wir entwickeln seine künstliche Intelligenz.

 

Im Folgenden werden wir das Script, was dafür zuständig ist, erklären:

 

Hier sehen wir den Header unseres Hamsterskripts („TeemoControl“), in dem wir unsere Variablen initialisieren, die wir im Script benutzen.
Die [Header(„X“)] Anweisung dient zur Übersichtlichkeit im Unity Editor.
Public Variablen lassen sich im Unity Editor verändern, private Variablen hingegen sind nur im Skript verwendbar und änderbar.
Die Variablen wurden so benannt, wie die Funktion, die sie im Script erfüllen.

 

 

In der vorgenerierten Funktion void Start(), welche beim Starten der Szene einmal ausgeführt wird, binden wir die Position des Spielers
an die Variable Player und weisen der Variable "animator" den Hamster - Animator zu, den wir vor dem Skript erstellt und konfiguriert haben.
Außerdem binden wir die public Variablen "health" und "attackCooldown" an private Variablen ,um feste Werte im Skript weiter zu verwenden.

Vielleicht mag man sich fragen, warum wir dann überhaupt public Variablen verwenden,
wenn die sowieso wieder in private verwandelt werden.
Der Grund dafür ist, dass man so in Unity schnell verschiedene Balancing - Veränderung durchführen kann.

 

Die void Update() - Funktion wird auch von Unity generiert und wird nach jedem Frame aufgerufen.
Daraufhin lassen wir unsere HP - Leiste Prozentual zum aktuellen Leben füllen.
Sollte der Wert auf "kleiner gleich" 0 fallen, zerstören wir unser Spielobjekt und lassen ein neues Objekt erstellen. 
Dieses neue Objekt ist eine Blutlache, die an der Stelle eines Gegners spawnt, wenn er getötet wurde.
Alle Gameobjekte haben ein eigenes Verhalten, dementsprechend auch ein Skript.
Mit der nächsten If - Abfrage prüfen wir, ob unser boolean "move" auf true gesetzt wurde, welche dann die Funktion Movement() aufruft,
zu welcher wir später noch einmal zurückkommen.
In Zeile 71 reduzieren wir die attackCooldown um die System Zeit, also wenn im Spiel 200ms vergangen sind,
geht der attackCooldown Timer auch wirklich 200ms runter.
In der letzten If - Abfrage überprüfen wir, ob unser Hamster wieder angreifen darf
und er sich in einer Angriffs Animation befindet, also ob die variable AttackDir nicht den Wert 0 hat.
Wenn dies erfüllt ist, rufen wir die Funktion shot() auf und die Funktion resetAttackCooldown(), welche später erläutert werden.

 

 

Auf dem Bild sehen wir die Funktion Movement(), die wir zur Erinnerung immer aufrufen, wenn der boolean "move" auf True gesetzt ist, also wenn sich der Gegner bewegt.
In den ersten beiden Zeilen (87 und 88) berechnen wir jeweils den Unterschied zwischen dem Spieler und unseren Gegner für die X- und für die Y-Achse.
In den nächsten 4 Zeilen errechnen wir die Hypotenuse zu den zuvor ausgerechneten zwei Strecken, um die direkte Entfernung des Gegners zu erhalten.

Es folgt ein Schaubild, welche die Theorie dahinter nochmal verbildlicht.

 

Mit der errechneten Entfernung können wir gucken, ob sich unser Hamster in Angriffsreichweite befindet
oder er vor dem Spieler wegrennen soll oder ihm entgegenrennen bzw. hinterherrennen soll, um in Angriffsreichweite zu kommen.


Die erste If - Abfrage überprüft, ob sich der Spieler in der Angriffszone befindet.
Sollte dies erfüllt sein, sorgt die Anweisung dafür, dass er stehen bleibt und auch keine Bewegungsanimation mehr abspielt.
Die darauffolgende Else if - Abfrage überprüft, ob der Spieler sich zu dicht am Gegner befindet.
Daraufhin sorgt die Anweisung, dafür dass der Gegner vom Spieler wegläuft und keine Angriffsanimation mehr aktiv sein darf.
Falls keine der beiden vorherigen Szenarien zutrifft, rennt der Hamster auf den Spieler zu.

Dazu auch ein Schaubild, das die Theorie dahinter nochmal verbildlicht.

 

 

Wir befinden uns noch immer in der Movement() Funktion.
Auf diesem Bild geht es um das Verhalten des Animators, den wir relativ am Anfang konfiguriert haben.
Ziel hier ist es, dass unser Hamster, passend zur Position, die richtige Angriffs- und Bewegungsanimation abspielt.

Mit der If - Abfrage (Zeile 117) überprüfen wir erstmal, ob der Abstand zum Spieler auf der X-Achse größer ist als auf der Y-Achse.
Sollte dies der Fall sein wissen wir, dass der Spieler sich mehr rechts oder links vom Hamster befindet, als über oder unter ihm.

Da wir jetzt wissen, dass wir entweder die Bewegungsanimation nach links oder nach rechts abspielen müssen,
müssen wir noch überprüfen, ob der Spieler sich weiter rechts oder weiter links befindet.
Zuerst übernehmen wir den Fall rechts.
Dafür prüfen wir in Zeile 119, ob der X-Wert vom Spieler größer als der X-Wert vom Hamster ist.
Trifft dieses Szenario ein, setzten wir die Variable AttackDir auf 0, um die Angriffsanimation aufjedenfall zu unterdrücken,
und setzten die Variable MovementDir auf 2, was nach unserer anfänglichen Konfiguration der Bewegungsanimation nach rechts entspricht.

Da wir wissen, dass der Spieler sich rechts von uns befindet, nutzen wir den Zustand aus
und gucken, ob wir uns in Angriffsreichweite befinden mit der if - Abfrage in Zeile 124.
Sollten wir uns in besagter Angriffsreichweite befinden, rufen wir die Funktion setMoveFalse() auf, worauf wir später nochmal zurückkommen,
kurz gesagt setzt sie unser boolean "move" auf False, setzten die Variable MovementDir auf 0 und die Variable AttackDir auf 2,
was nach unsere anfänglichen Konfiguration Angriff nach rechts entspricht.
Zusätzlich weisen wir dem String shotDirection die Zeichenkette „right“ zu und setzten den currentShotPoint auf 1. Letztendlich rufen wir die Funktion attackOffset() auf.

Die Befehle in Zeile 130 bis 132 werden später erläutert.

Falls sich der Spieler Links befindet wird die If - Abfrage in Zeile 135 ausgelöst,
welche dasselbe wie die If -Abfrage in Zeile 119 macht, nur sind alle Anweisung angepasst, um die Animation nach Links zu steuern.

 

 

Der Vollständigkeitshalber der letzte Teil der Movement() Funktion.
Sollte die If - Abfrage, in der wir testen, ob der horizontale Abstand zum Spieler größer ist als der vertikale Abstand, fehlschlagen, wird die Else - Abfrage in Zeile 152 ausgelöst.
Dementsprechend testen die nachfolgenden If - Abfragen, ob der Spieler sich über oder unter unseren Hamster befindet
und führt dementsprechend dieselben Anweisungen aus, nur auf die richtige Richtung getrimmt.

Um das Thema abzuschließen, noch ein Schaubild, um das Ganze noch einmal zu verbildlichen.

 

 

In Zeile 190 definieren wir die Funktion resetAttackCooldown(),
welche lediglich dafür sorgt, dass bei jedem Aufruf die derzeitige attackCooldown wieder auf den definierten Wert von maxCooldown gesetzt wird.
Die nächste Funktion, die wir definieren, attackOffset() sorgt dafür, das auf attackCooldown 0.7f addiert wird.
Der Grund dafür ist, dass der Hamster den Pfeil erst schießen soll, wenn bei der Animation der richtige Frame abgespielt wird,
daher müssen wir die künstliche Verzögerung einbauen.
Die Letzte Funktion auf dem Bild (Zeile 200) shot() sorgt dafür, dass, wenn die Funktion aufgerufen wird ,
unser Hamster ein GameObject erzeugt, welches sein Pfeil ist, und ihm mitteilt, wo er erzeugt wird und in welche Richtung er fliegen soll.
Aus diesen Grund setzten wir in der Movement() Funktion den currentShotPoint und die shotDirection.
Diese Information Teilen wir dem Skript mit, der mit dem Pfeil vom Hamster verbunden ist und der regelt das Verhalten vom Pfeil.

 

 

Auf diesen Bild sehen wir die Funktion OnTriggerEnter2D(Collider2D collision).
Diese macht letztendlich nichts anderes, als die Information von Unity zu bekommen,
was für eine Berührung mit der Hamster Hitbox, die wir in Unity definiert haben, stattgefunden hat.

Dafür überprüfen wir den Tag des Objektes, welches uns berührt hat und führen dementsprechend eine Anweisung aus.

Als Beispiel gehen wir davon aus, dass den Hamster eine Maschinengewehrkugel, mit dem Tag „BulletMG“, getroffen hat.
Daraufhin wird von der health Variable der Wert 10 abgezogen. Die Kugel, die uns getroffen hat, wird zerstört
und wir initialisieren das Objekt bloodSplattern an der derzeitigen Position des Hamsters.
Das bloodSplattern sind die Blutspritzer, die einen Treffer anzeigen, welche auch wieder von einem eigenen Skript gesteuert werden.

 

 

Im letzten Teil des Skripts definieren wir die Funktion setMoveTrue()
und die Funktion setMoveFalse() welche jeweils den boolean "move" auf true oder false setzten.

Letztendlich haben wir dann mit unseren vorhandenen Gegnern, Weltobjekten
und unserem Spieler den am Anfang besprochenen Prototypen unseres Raums entwickelt.

 

 

Ziel für den nächsten Milestone:
In diesem Milestone planen wir unseren nächsten Raumtyp, den Fallenraum, umzusetzen. Zudem soll unser bisheriger Raum mehr Vielfalt an Gegner bekommen.

 

 

Schlusswort:

Was wir auch niemanden vorenthalten wollen, ist die Wichtigkeit des stetigen Testens von dem, was man macht.
Als optimal hat sich erwiesen, wenn andere Leute die Sachen testen, die man selbst erstellt hat.
So konnten immer mehr Fehler gefunden werden, als der Ersteller selbst entdecken konnte.

Hier nur ein kleiner Ausschnitt unserer Bug Liste die wir hatten, als es nur einen Gegner gab und den Spieler.

 

 

 

  • No labels