owned this note
owned this note
Published
Linked with GitHub
# Dokumentation des Softwareprojekts
**H.E.I.S.T**
von *Gustav Weber* und *Jann Stute*
entstanden im Rahmen des Softwareprojekts in der 12. Klasse
zwischen der dritten und zwölften Kalenderwoche 2021
unter der Nutzung von Unity (+Mirror), JetBrains Rider, Visual Studio, Visual Studio Code, Paint, Paint.net, Photoshop, Audacity
## Inhaltsverzeichnis
1. [Inhaltsverzeichnis](/d7G4cStcS6GLeluEmkCpdQ#Plagiatszettel)
2. [Methodendokumentation](/d7G4cStcS6GLeluEmkCpdQ#Methodendokumentation)
2.1. [wichtigste Networking Grundlagen](/d7G4cStcS6GLeluEmkCpdQ#Methodendokumentation#wichtigste-Networking-Grundlagen)
2.2. [GUI Shaker](/d7G4cStcS6GLeluEmkCpdQ#Methodendokumentation#GUI-Shaker)
2.3. [Audio Player](/d7G4cStcS6GLeluEmkCpdQ#Audio-Player)
2.4. [Checkpointsystem](/d7G4cStcS6GLeluEmkCpdQ#Checkpointsystem)
2.5. [Alarmsystem](/d7G4cStcS6GLeluEmkCpdQ#Alarmsystem)
2.6. [Rätselsystem](/d7G4cStcS6GLeluEmkCpdQ#Rätselsystem)
3. [Benutzerdokumentation](/d7G4cStcS6GLeluEmkCpdQ#Benutzerdokumentation)
3.1. [Generelle Controlls](/d7G4cStcS6GLeluEmkCpdQ#Generelle-Controlls)
3.2. [Kabel Kappen](/d7G4cStcS6GLeluEmkCpdQ#Kabel-Kappen)
3.3. [Decode](/d7G4cStcS6GLeluEmkCpdQ#Decode1)
3.4. [Simon Says](/d7G4cStcS6GLeluEmkCpdQ#Simon-Says1)
3.5. [Maze](/d7G4cStcS6GLeluEmkCpdQ#Maze2)
3.6. [Zahlenschloss](/d7G4cStcS6GLeluEmkCpdQ#Zahlenschloss1)
3.7. [Schaltkreis](/d7G4cStcS6GLeluEmkCpdQ#Schaltkreis1)
3.8. [Tresorschloss](/d7G4cStcS6GLeluEmkCpdQ#Tresorschloss1)
3.9. [Checkpoints](/d7G4cStcS6GLeluEmkCpdQ#Checkpoints)
4. [Entwicklungsdokumentation](/d7G4cStcS6GLeluEmkCpdQ#Entwicklungsdokumentation)
5. [Plagiatszettel](/d7G4cStcS6GLeluEmkCpdQ#Plagiatszettel)
## Methodendokumentation
### wichtigste Networking Grundlagen
Um das Networking zu realisieren haben wir das Mirror-Package aus dem Unity Asset-Store benutzt. Dieses basiert wiederum auf dem in Unity integrierten UNet System und funktioniert als Erweiterung dessen.
Im Projekt wurden folgende Funktionalitäten von Mirror benutzt:
#### SyncVars, SyncLists und SyncDictionarys
Der `SyncVar`-Tag kann zu einer Variable hinzugefügt werden, um sie durch Mirror vom Server zu allen Clients zu synchronisieren. Das kann nur auf standardmäßigen Datentypen angewandt werden und nicht auf Listen und Dictionarys. Dazu gibt es die Mirror eigenen Datentypen SyncList und SyncDictionary. Diese werden ebenfalls automatisch vom Server zu den Clients synchronisiert. Wird also auf dem Server eine Veränderung an einer solchen Variable gemacht, wird diese auf alle Clients übertragen. Wird die Variable allerdings auf einem Client geändert, so wird sie nicht automatisch von Mirror synchronisiert.
Man kann zusätzlich mit folgender Syntax eine hook definieren:
`[SyncVar(hook="functionName")] variableName`
Die Hook ist eine Funktion, die auf den Clients ausgeführt, wann immer die Variable verändert wird. Dabei ist aber darauf zu achten, dass das Definieren einer Hook das Synchronisieren überschreibt. Die Variable muss also in der Hook neu gesetzt werden. Die Hook nimmt dazu zwei Parameter mit dem Typ der Variable an, die für den alten und neuen Wert der Variable stehen.
Es ist außerdem zu beachten, dass die Hook nicht auf dem Server ausgeführt wird.
#### Commands
Eine Funktion kann als Command markiert werden, damit sie vom Client aufgerufen und auf dem Server ausgeführt werden kann. Durch Commands kann also Kommunikation von den Clients zum Server gelangen. Ein Command kann standardmäßig nur von dem aktiven Spielerobjekt ausgeführt werden. Durch das boolean Attribut `requiresAuthority` kann man allerdings auch bestimmen, dass ein Command von jedem Objekt ausgeführt werden darf. In der Syntax sieht das dann so aus:
`[Command(requiresAuthority = false)]`
Im Projekt werden Commands vor allem zur Synchronisierung von SyncVars vom Client zum Server genutzt, da diese nicht automatisch durch die SyncVars funktioniert. Da diese Variablen nicht auf den Spielerobjekten sind, ist bei fast allen Commands im Projekt `requiresAuthority` auf `false` gesetzt.
#### ClientRPC
Eine Funktion kann als ClientRPC markiert werden, damit sie vom Server aufgerufen und auf allen Clients ausgeführt werden kann. ClientRPC sind also ein zweiter Weg, um Kommunikation vom Server auf die Clients zu übertragen. Ein ClientRPC kann nur vom Server aufgerufen werden und wird dann auf allen Clients ausgeführt.
Wenn eine Funktion, die auf einem Client aufgerufen wird, nicht nur auf diesem Client ausgeführt werden soll, kann also ein ClientRPC genutzt werden. Diese dürfen aber nur vom Server ausgeführt werden. Daher ruft der Client einen Command auf, der dann auf dem Server einen ClientRPC aufruft, der auf allen Clients ausgeführt wird.
### GUI Shaker
Der GUI Shaker wird benutzt, um dem Spieler Feedback zu geben, wenn dieser eine falsche Eingabe macht. Dazu wird die Funktion `ShakeScreen` genutzt. Diese nimmt die Länge des Schüttelns in Sekunden (meist 0.5), das Objekt, das geschüttelt werden soll (meist inputparentpanel, immer der root der GUI) und eine stärke zum schütteln. Je höher die Stärke, desto großer ist der Bereich, in dem das Objekt sich bewegt.
Das eigentliche Schütteln passiert in `Update()`. Wenn die duration noch nicht überschritten wurde, wird die Position des Objekts auf einen zufälligen Punkt innerhalb eines Kreises mit Radius 1 * die Stärke des Schüttelns, relativ zur ursprünglichen Position gesetzt. Wird die Zeit überschritten, wird das Objekt wieder auf seinen Ursprungspunkt zurückgesetzt.
Wenn das Objekt beginnt zu schütteln wird außerdem das "falsch" Geräusch über den audioPlayer abgespielt.
### Audio Player
Der Audioplayer ist ein GameObject, das jederzeit in jeder Szene sein sollte. Es wird verwendet, um Geräusche Szenenübergreifend abzuspielen. Wird ein Geräusch auf normale Weise (mit `clip.Play()`) abgespielt, kurz bevor es einen Szenenübergang gibt, wird es von diesem unterbrochen und spielt in der neuen Szene nicht zu Ende.
Der AudioPlayer hingegen wird von einer Szene, in die nächste übertragen. Dazu wird `DontDestroyOnLoad(this);` gesetzt.
In diesem Fall wird der AudioPlayer von Szene zu Szene übertragen und der Sound übernommen. Deshalb sollten alle Geräusche, deren Auslöser auch einen Szenenübergang auslösen können (zum Beispiel der "Host Game" Button im Main Menu) den AudioPlayer benutzen, um ihre Geräusche abzuspielen.
### Checkpointsystem
Der `CheckpointManager` hat eine Variable `lastCheckpoint`, in ihr wird der letzte aktivierte Checkpoint gespeichert. Wird nun ein Checkpoint über seine Methode `triggerCheckpoint()` ausgelöst, wird `lastCheckpoint` auf eben diesen Checkpoint gesetzt. Außerdem speichert der Checkpoint in seiner Variable `SolveStates` den aktuellen Zustand der gleichnamigen Variable des RätselManagers und speichert die Position des lokalen Spielers in `LocalPlayerPos` (Über einen Command-ClientRpc Setup wird dieser letzte Schritt bei allen Clients ausgelöst).
Werden die Spieler zu einem Checkpoint zurückgesetzt, wird `CheckpointManager.resetToCheckpoint()` ausgeführt. Diese Methode sorgt dafür das die Variable `SolveStates` im RatselManager überschrieben wird, außerdem wird bei jedem Client ein Überschreiben der Position des lokalen Spielers durch die gespeicherte ausgelöst. Um den Rätseln, und auch jeder anderen Klasse / Objekt, die Möglichkeit zu geben, auf ein Zurücksetzen der Spieler zu einem Checkpoint zu reagieren, gibt es das `OnResetToCheckpoint` Event. Dieses Event wird ebenfalls in `CheckpointManager.resetToCheckpoint()` ausgelöst.
Damit die Spieler auch zu einem Checkpoint zurückgesetzt werden können, bevor sie ein Rätsel gelöst haben, gibt es den `GameStartCheckpoint`. Dieser wird zu Beginn des Spiels ausgelöst, sodass die Spieler auch beim ersten Rätsel schon zurückgesetzt werden können.
### Alarmsystem
Das Alarmsystem besteht aus der Klasse `AlarmManager`. Diese Klasse hat zwei Variablen, `AlarmCount` (zu Beginn 0) und `AlarmMax` (zu Beginn 3). Jedes Mal, wenn die Methode `triggerAlarm()` ausgeführt wird, wird `CheckpointManager.resetToCheckpoint()` ausgeführt. Außerdem wird `AlarmCount` um 1 erhöht und falls es dadurch `AlarmMax` entspricht, haben die Spieler verloren. Deshalb wird die Bewegung der Spieler deaktiviert und die Game Over GUI wird angezeigt.
### Rätselsystem
Das Rätselsystem basiert auf 3 Klassen, dem `RatselManager`, dem `RatselController` und dem `Ratsel`. Der `RatselManger` existiert einmal, während jede Subklasse von `Ratsel` einmal pro RätselController am Objekt des RätselControllers existiert.
#### RatselManager
Der RätselManager hat eine Liste mit RätselControllern, jeder RätselController der funktionieren soll muss in dieser Liste sein. Der RätselManger hat außerdem eine Liste mit den Namen der Subklassen von `Ratsel`, den Rätseln. Bei Spielbeginn wird je ein Rätsel pro RätselController-ID (mehr zur RätselController-ID im Abschnitt RätselController) ausgewählt, welches aktiviert wird. Die Liste mit Namen der Rätsel wird genutzt, damit kein Rätsel mehrfach auftritt. Außerdem hat der RätselManager die Variable `SyncDictionary<int, bool> SolveStates`, in diese wird bei Spielbeginn für jede RätselController-ID ein Eintrag mit dem Wert `false` gemacht. Zusätzlich zu diesen Einträgen wird auch noch ein Eintrag für die RatselController-ID `-1` mit Wert `true` gemacht, dieser dient Türen, die von Beginn an offen sind. Das Wörterbuch `SolveStates` dient der Speicherung der Löse-Zustände aller Rätsel, ist also der Wert für eine RätselController-ID `true` wurde dieses Rätsel bereits gelöst.
Weiterhin stellt der RätselManager Methoden zum Finden bestimmten RätselController bereit und sorgt für die Funktionalität der `Ratsel.fOnEnable()`-, `Ratsel.fAwake()`-, `Ratsel.fStart()`- und `RatselController.fStart()`-Methoden.
#### Ratselcontroller
Der RatselController sorgt für den Aufruf der GUI in seinem zugehörigen Rätsel. Hierfür nutzt er die, in der Superklasse der Rätsel `Ratsel`, definierten Methoden `showInputGUI()`, `showViewGUI()` und `hideGUI()`.
Des Weiteren enthält er viele Referenzen zu anderen Objekten, die den Zugriff auf diese für die Rätsel einfacher machen.
Die RätselController-ID wird in der Variable `id` gespeichert, sie ist ein Integer der nur für zwei RätselController gleich ist und so die Zusammengehörigkeit dieser RätselController und der entsprechenden Türen markiert. Die ID wird im RatselManager genutzt, um abzuspeichern, wenn Rätsel gelöst wurden und gibt den an den Tür-Objekten die Möglichkeit festzustellen, ob das Rätsel, zu dem sie gehören bereits gelöst wurde.
Die Methode `triggerReward()` des RätselControllers, wird ausgeführt, wenn ein Rätsel gelöst wird. Unteranderem soll die Methode für die Anpassung der Variable `RatselManager.SolveStates` sorgen, da Variablen vom Typ `SyncDictionary` allerdings nur vom Server verändert werden können, gibt es eine gleichnamige Methode im RatselManager, welche als Command markiert ist und von `RatselController.triggerRewad()` aufgerufen wird. Sie sorgt ausschließlich für die Veränderung von `SolveStates`.
#### Ratsel
Die Klasse `Ratsel` dient der Verallgemeinerung aller Rätsel auf eine Superklasse.
Sie enthält zum Beispiel die virtuellen Methoden `fOnEnable()`, `fAwake()` und `fStart()`. Für diese Methoden gilt (global, für aktivierte Rätsel) das folgende: Zuerst werden alle `fOnEnable()`-, dann alle `fAwake()`- und zuletzt alle `fStart`-Methoden ausgeführt. Danach werden die `fStart()` Methoden aller beim RätselManager eingetragenen RätselController ausgeführt.
Die Klasse enthält außerdem die virtuellen Methoden `showViewGUI()`, `showInputGUI()` und `hideGUI()`, welche hauptsächlich von RätselControllern aufgerufen werden und die Methode `createOverworldSprite()`. Diese letzte Methode wird bei der Initialisierung des Rätsels durch den RätselManager aufgerufen und dient des Anzeigens der entsprechenden Sprite im Spiel. Sie wird von Rätseln gewöhnlicherweise überschrieben, es muss aber die Version der Superklasse `Ratsel` auch ausgeführt werden.
##### Maze
###### Labyrinthgenerierung
Für die Generierung des Labyrinthes, musste zuerst ein Datentyp für das Labyrinth festgelegt werden. Hierfür haben wir ein 3-Dimensionales Boolean-Array gewählt. Hier stellt die erste Dimension die x-Koordinate im Labyrinth dar und die zweite Dimension die y-Koordinate. Die Menge der Einträge in erster und zweiter Dimension ist also abhängig von der Größe das Labyrinths, während die dritte Dimension immer genau 2 Elemente umfasst. Das erste Element der dritten Dimension repräsentiert die untere Wand einer Zelle, `true` bedeutet der Spieler kann passieren, `false` bedeutet eine Wand ist im Weg. Für das zweite Element der dritten Dimension gilt das gleiche, nur dass es die rechte Wand repräsentiert. Die linke und die obere Wand einer Zelle werden von der dritten Dimension der jeweils angrenzenden Zelle gespeichert.
Für die eigentliche Generierung des Labyrinthes wird eine Klasse namens `MazeGeneration` genutzt, sie hat folgende Felder und Methoden:
Name |Datentyp |Beschreibung
---------------------------------------|-----------------|------------
RnG |`System.Random` |Eine System.Random Instanz um geseedete Zufallsgenerierung zu ermöglichen
size |`Tuple<int, int>`|Die Größe des Labyrinthes
MazeGeneration(System.Random, int, int)|Konstruktor |Ein Konstruktor der eine `System.Random` Instanz für `MazeGeneration.RnG` und die Größe des Labyrinthes als zwei Integer für `MazeGeneration.size` annimmt.
NextMaze() |`bool[,,]` |Die Methode zum Generieren eines Labyrinthes, sie ist nicht statisch, beruft sich also auf die im Konstruktor gesetzten Werte.
Die Methode `MazeGeneration.NextMaze()` benutzt den Hunt-And-Kill-Algorithmus, um Labyrinthe zu generieren, dieser besteht aus 4 Schritten:
1. Auswählen einer zufälligen Startzelle
2. "Kill"-Modus
Hier wird eine zufällige Nachbarzelle ausgewählt, welche noch nicht besucht wurde. Dann wird die Wand zwischen den zwei Zellen entfernt und die Nachbarzelle wird die aktuelle Zelle. Dieser Schritt wird wiederholt, bis die aktuelle Zelle keine unbesuchten Nachbarn mehr hat.
3. "Hunt"-Modus
Alle Zellen werden nach unbesuchten Zellen durchsucht, wird eine unbesuchte Zelle gefunden wird sie die aktuelle Zelle.
4. Schritt 2 & 3 werden wiederholt, bis Schritt 3 keine unbesuchte Zelle mehr findet.
Um zu verhindern, dass die Wege, die zum Ziel führen eine Tendenz in eine bestimmte Richtung haben, wird die Richtung der Suche im "Hunt"-Modus ebenfalls zufällig ausgesucht (bei jeder Ausführung des 3. Schritts).
###### Maze
Bei der Initialisierung des Rätsels wird zuerst `fAwake()` aufgerufen und so die Variable `Tries` auf den Wert von `MaxTries` gesetzt, was sicherstellt, dass der Spieler das Rätsel so oft versuchen kann wie vorgesehen. Als nächstes wird, falls die Instanz von `Maze` an einen `RatselController` vom Typ `Input` gebunden ist, eine Instanz von `MazeGeneration` erstellt und ein 8x8 Zellen Labyrinth erstellt und in `Maze.GMaze` gespeichert.
In `fStart()` wird Initialisierung fortgesetzt die Methode `Maze.OnResetToCheckpoint()` als Listener des Events `CheckpointManager.OnResetToCheckpoint` registriert wird, um auf ein Zurücksetzen der Spieler zu einem Checkpoint reagieren zu können. Darauffolgend wird, falls es sich bei zugehörigen `RatselController` um einen `RatselController` vom Typ `View` handelt, das schon generierte Labyrinth vom zweiten `Maze` abgerufen und ebenfalls in `Maze.GMaze` abgespeichert. Weiterhin wird die Variable der Superklasse `Ratsel`, `Ratsel.shaker`, auf die am selben Objekt vorhandene Instanz der Klasse `GUIShaker` gesetzt. Als letzten Schritt der Initialisierung werden alle notwendigen Variablen für die GUI von `Maze` gesetzt, das bedeutet abhängig vom Typ des `RatselController`s wird entweder `Maze.inputGUI` oder `Maze.viewGUI` auf eine neue Instanz des Prefabs `MazeInputGUI` bzw. `MazeViewGUI` gesetzt. Außerdem wird, falls es sich bei dem zugehörigen `RatselController` um den Typ `View`, die Methode `MazeViewGUIController.drawWalls(bool[,,])` aufgerufen, um die Wände des Labyrinthes in der GUI anzuzeigen.
Die Methode `Maze.FixedUpdate()` überschreibt die von `UnityEngine.Monobehaviour` geerbte Methode (über die Superklasse von `Ratsel`, `Mirror.NetworkBehaviour`) und nutzt die sogenannte "FixedUpdate-Loop", das bedeutet die Methode `FixedUpdate` von `UnityEngine.Monobehaviour` und seinen Subklassen, wird von Unity regelmäßig aufgerufen.
In der Methode `Maze.FixedUpdate()` wird der Input der durch den Spieler in der Input GUI gegeben wird verarbeitet, falls der Spieler eine der entsprechenden Tasten drückt, wird also die Variable `UnityEngine.Vector2Int Maze.PlayerPos` entsprechend verändert, zum Beispiel wird falls die Taste "Pfeil nach Unten" gedrückt wird, `Maze.PlayerPos.y` um 1 erhöht (Hier wird außerdem `MazeInputGUIController.setPlayerPos(UnityEngine.Vector2Int)` ausgeführt um die Spielerposition in GUI richtig anzuzeigen). Hier findet auch noch eine Menge Inputvalidierung statt, wie zum Beispiel, würde der Spieler versuchen eine Außenwand des Labyrinthes überschreiten passiert einfach nichts. Des Weiteren, falls ein Spieler gegen eine Wand läuft, wird die Variable `Tries` um 1 verringert und sollte diese 0 erreichen, wird durch das Ausführen der Methode `AlarmManager.triggerAlarm()` der Alarm ausgelöst.
Außerdem werden in der FixedUpdate-Loop die Variablen `Maze.wasHInput` und `Maze.wasVInput` genutzt. Diese werden am Ende der Input Verarbeitung auf den aktuellen Zustand des Inputs gesetzt, d.h. `Maze.wasHInput` wird auf `true` gesetzt, falls Horizontaler Input gegeben wurde und `Maze.wasVInput` falls Vertikaler Input gegeben wurde. Dieser Variablen werden dann bei der Inputvalidierung genutzt, um sicherzustellen, dass ein einmaliges Drücken einer Taste auch nur zu einer einmaligen Bewegung des Spielers führt, ist `Maze.wasHInput` also `true`, wird sämtlicher Horizontaler Input abgelehnt, das gleiche gilt für `Maze.wasVInput`. Diese Variablen werden außerdem zurückgesetzt, wenn die Input GUI geschlossen wird.
Eine letzte Sache, die ebenfalls noch in der FixedUpdate-Loop passiert, ist die Überprüfung, ob der Spieler am Ziel angekommen ist, hierfür wird anhand der `Maze.PlayerPos` Variable festgestellt, ob er angekommen ist und falls ja werden folgende Funktionen ausgeführt:
1. `RatselController.triggerReward(PlayerController)`
Um den Fortschritt an den anderen Client und an den RatselManager zu übertragen
2. `Maze.hideGUI()`
Um die GUI auszublenden
3. `Maze.solve()`
Um bei beiden Instanzen `Maze.wasSolved` auf `true` zu setzen und so ein weiteres öffnen der `Maze`-GUI zu verhindern
Die Methoden `Maze.showViewGUI()` und `Maze.showInputGUI()` dienen zum Anzeigen der GUI von Maze abhängig vom Typ des zugehörigen `RatselControllers`. Bei beiden wird die entsprechende GUI zuerst aktiviert und dann die zugehörige Variable aktualisiert (`Maze.InputGUIshowing` und `Maze.ViewGUIshowing`). Außerdem wird noch `PlayerController.canMove` für die Instanz `Maze.PC` auf `false` gesetzt, um zu verhindern das der Spielercharakter sich bewegt, wenn der Spieler versucht sich im Labyrinth zu bewegen. In `Maze.showInputGUI` wird als letztes außerdem `Maze.inputParentPanel` gesetzt (siehe GUI Shaker).
Sollten die Spieler zu einem Checkpoint zurückgesetzt werden, wird außerdem die Methode `Maze.OnResetToCheckpoint()` wichtig, denn diese soll das Rätsel in seinen Ausgangszustand zurücksetzten, insofern es nicht schon gelöst wurde. Hierfür werden `Maze.Tries`, `Maze.wasHInput`, `Maze.wasVInput` und `Maze.PlayerPos` auf ihre Startwerte zurückgesetzt.
###### MazeInputGUIController
Bei der Initialisierung wird die Größe und Position vom Hintergrundbild angepasst, sodass das Seitenverhältnis gleichbleibt, sich die Höhe aber an die Bildschirmgröße anpasst. Außerdem wird die Farbe des Spielers entsprechend der Variable `MazeInputGUIController.PlayerColor` gesetzt.
Zu guter Letzt wird die Methode `MazeInputGUIController.setPlayerPos(UnityEngine.Vector2Int)` aufgerufen, um sicher zu stellen, dass die Spieler Position richtig ist (obere linke Ecke).
Die Methode `MazeInputGUIController.setPlayerPos(UnityEngine.Vector2Int)` berechnet lediglich die neue Position der `UnityEngine.UI.Image`-Instanz und setzt diese.
###### MazeViewGUIController
Die Initialisierung von `MazeViewGUIController` unterscheidet sich nicht von der von `MazeInputGUIController`, genau wie die Methode `MazeViewGUIController.setPlayerPos`. Der einzige Unterschied der Methode ist, dass sie bei `MazeViewGUIController` nur ein Mal (nämlich in der Initialisierung) aufgerufen wird.
Die Methode `MazeViewGUIController.drawWalls(bool[,,])` iteriert über alle Zellen im Labyrinth und zeichnet die Wände ein, falls sie existieren (wichtige Ausnahme sind die Zellen der unteren Reihe und rechten Spalte, hier wird immer nur eine Wand gezeichnet). Der Prozess ist für beide Wände gleich.
Hierfür werden zuerst Anfangs- und Endpunkt der Linie, welche die Wand darstellen soll, berechnet. Danach wird über alle Pixel zwischen diesen Punkten iteriert (mit Hilfe einer simplen for-Schleife, da sie alle in einer Spalte/Zeile liegen) und für jedes Pixel wird dann die Farbe in dem angezeigten Bild geändert. Als letztes folgen noch einige Anweisungen um die Änderungen am Bild anzuwenden.
##### Schaltkreis
###### Erstellen des Schaltkreis auf dem Server
Um den Schaltkreis zu generieren, wird ein rekursiver Algorithmus genutzt.
Wird zum ersten Mal `showViewGUI()` oder `showInputGUI()` aufgerufen, wird der Command `CmdCreateViewGUI()` ausgeführt. Dieser erstellt dann eine Liste `boolgates` mit den Prefabs der Gatter, die jeweils zweimal in der Liste vorkommen.
Es wird außerdem das ViewGUI Prefab instanziiert, da dieses unterschiedlichen möglichen Positionen der Gatter beinhaltet. Diese werden dann nach Spalten sortiert und in die Liste `imagePositions` geschrieben.
Dann wird die rekursive Funktion `Create_schaltkreis` aufgerufen.
Sie nimmt als Argumente:
- `int depth` - entspricht der Spalte, in der das neue Gatter entsteht
- `int maxdepth` - die tiefste Spalte die besetzt werden kann
- `LogicGate output` - das Gatter, das an den Output des neuen Gatters angeschlossen ist
- `int outputrank` - ein Index, der angibt, welcher Input von output an den Output des neuen Gatters angeschlossen ist
Es gibt auch noch folgende optionale Argumente:
- `Vector2 newPosition = new Vector2()` - wird nur verwendet, falls ein IN-Gate erstellt wird und gibt die gewollte Position des Gates an
- `Tranform parent = null` - wird ebenfalls nur für IN-Gates verwendet und gibt das Elterntransform des Gatters an
Da diese Funktion nur von sich selbst und von `CmdCreateViewGUI()` aufgerufen wird, wird sie immer auf dem Server ausgeführt.
Die Funktion überprüft dann, ob die maximale Tiefe erreicht ist. Falls das nicht der Fall ist, wird ein zufälliges Gatter aus der Liste `boolgates` ausgewählt, instanziiert und entfernt. Der Name des Gates wird außerdem als String der Liste `gates` hinzugefügt. Diese hält die Reihenfolge fest, in der die Gatter erstellt werden und wird später für die Synchronisierung auf dem Client benutzt.
Es werden dann die Variablen des neuen Gatters aufgesetzt, es wird an die richtige Position auf dem Bildschirm bewegt und die Position wird aus der Liste `imagePositions` entfernt.
Es wird dann für jeden Input, den das Gatter hat (1, falls es ein NOT Gate ist, andernfalls 2), ein neues Gatter erstellt, für das die gleiche Funktion ausgeführt wird.
Danach werden die Outputs der neuen Gates mit den Inputs des momentanen Gates mit Linien verbunden.
Dazu wird die Funktion `draw_line` verwendet, auf die später genauer eingegangen wird.
Zum Schluss wird das momentane Gate zurückgegeben.
Falls die maximale Tiefe erreicht ist, wird ein IN-Gate erstellt. Diese entsprechen den Inputs der InputGUI. Nachdem die variablen des Gatters ausgefüllt sind, wird es zur Liste `inputs` hinzugefügt, die von der InputGUI verwendet wird, um die Knöpfe mit den Gattern zu verbinden.
###### Synchronisierung auf Clients
Nachdem alle Gatter erstellt wurden, wird in `CmdCreateViewGUI` `RpcStartSetSchaltkreis()` ausgeführt. Es wird also der Schaltkreis vom Server auf die Clients übertragen. Während `Create_schaltkreis()` wurden dazu die Namen der Gatter in der Reihenfolge ihrer Entstehung zur Liste `gates` hinzugefügt. Da ein ClientRPC keine eigenen Datentypen als Argumente nehmen kann, wurden die Namen als Strings gespeichert und dann erst auf den Clients benutzt, um die Gatter zu laden.
`RpcstartSetSchaltkreis()` funktioniert ähnlich zu `CreateViewGUI()` und setzt zuerst die wichtigsten Listen auf und instanziiert die viewGUI.
Danach wird aus der Liste `gates`, die die Namen der Gatter enthält die entsprechenden Prefabs gewonnen und schließlich `ClientSetSchaltkreis()` ausgeführt.
`ClienSetSchaltkreis` nimmt folgende Argumente:
- `int depth` - entspricht `Create_schaltkreis()`
- `int maxpepth`- entspricht `Create_schaltkreis()`
- `List<GameObject> logicGates` - die Liste der Gatter Objekte in der Reihenfolge, in der sie erstellt wurden
- `LogicGate gateoutput` - entspricht `output` in `Create_schaltkreis()`
- `int outputrank` - entspricht `Create_schaltkreis()`
- `Transform parent` - entspricht `Create_schaltkreis()`
- `Vector2 newPosition` - entspricht `Create_schaltkreis()`
- `bool updateInputs` - true, falls die IN-Gates noch zu inputs hinzugefügt werden müssen
Die Funktion funktioniert ähnlich wie Create_schaltkreis, aber anstatt die Gates zufällig auszuwählen, werden sie aus `logicGates` gezogen.
###### draw_line()
Um die Linien zwischen den einzelnen Gattern zu zeigen, wird Unitys Linerenderer Komponente benutzt. Da diese im Worldspace gerendert werden, nicht in der GUI, wird die ViewGUI ebenfalls nicht in der UI Layer gerendert.
`draw_line()` nimmt folgende Argumente:
- `GameObject parent` - das Objekt, dem der LineRenderer hinzugefügt werden soll
- `Vector3 p1` - die Position des ersten Punkt der Linie
- `Vector3 p2` - die Position des zweiten Punkt der Linie
Die Funktion erstellt dann eine schwarze Linie, die die beiden Punkte verbindet und über allem anderen gerendert wird.
###### LogicGate
Die LogicGate klasse wird benutzt, um die Logik der Gatter zu speichern.
Mit der Funktion `UpdateOutput()` wird aus den beiden Inputs ein output gemacht und an output weitergegeben.
`updateWire` wird ausgeführt, wenn `wireActiv` geändert wird und ändert die Farbe des LineRenderers, der in `wire` gespeichert wird.
Die LogicGate-Komponente wird nie durch Skript zu einem Objekt hinzugefügt, sondern ist bereits an den Prefabs der einzelnen Gatter. Dort sind auch alle Variablen, die unabhängig vom Schaltkreis sind gesetzt.
##### Decode
Die Buchstabe-Symbol-Paare werden in einem Dictionary gespeichert und von der View-Komponente festgelegt. In `showViewGUI()` werden dann sämtliche Image Komponenten in den Kindern der ViewGUI gesucht. Das erste wird entfernt, da es sich um das Hintergrundbild ändert. Danach werden die Platzhalter Symbole durch die richtigen ersetzt.
In `showInputGUI()` werden die keyValuePairs von der View-Komponente geholt. Es werden auch hier die Platzhalter Symbole im GUI-Prefab gesammelt. Dann werden die ersten beiden entfernt, da es sich um das Hintergrundbild und das Bild des Eingabefelds handelt. Es wird außerdem das letzte entfernt, da es sich um das Bild des Buttons handelt. Es wird dann ein zufälliges, 5-stelliges Passwort generiert und die Platzhalter werden durch die entsprechenden Symbole ersetzt.
Dem Button wird außerdem `onButtonClick()` als Listener zugewiesen.
In `OnButtonClick()` wird überprüft, ob der Text im inputfield dem Passwort entspricht. Da man seine Eingabe nicht direkt sehen kann (nur \*, wie es bei einem echten Passwort währe) wird die Groß- und Kleinschreibung ignoriert. Falls das Passwort falsch ist, werden die Versuche um eins verringert und die GUI wackelt ein wenig, um dem Spieler Feedback über seine falsche Eingabe zu geben.
##### Simon Says
Die Sequence von Simon Says wird erst festgelegt, wenn eine der beiden GUIs angezeigt wird. In diesem Fall wird sie allerdings immer von der Input-Komponente erstellt. Das wird über den Command `CmdCreateSequence` erledigt, was sicherstellt, dass sie auf dem Server und den Clients gleich ist.
Es werden fünf Integer zwischen 0 und 8 festgelegt, die jeweils für einen der neun Knöpfe stehen.
In `showInputGUI()` wird außerdem jedem Knopf `OnButtonClick` mit der entsprechenden Nummer als Listener hinzugefügt.
In `showViewGUI()` werden die alle Image-Komponenten in den Kindern der viewGUI gefunden und in Lampen abgespeichert. Auch hier wird die erste Stelle entfernt, da es sich dabei um das Hintergrundbild handelt.
In `Update()` wird das Blinken der Lampen durchgeführt. `Update()` wird in jedem Frame des Spiels einmal ausgeführt.
Es wird zuerst überprüft, ob die Lampen überhaupt blinken sollten. Das ist der Fall, wenn es sich bei der Komponente um die View-Komponente handelt, die ViewGUI aktiv ist und sie bereits gezeigt wurde. Wenn sie noch nicht gezeigt wurde, ist sie zwar aktiv, ist aber nur ein Prefab.
Dann wird lamp_pos benutzt, um zu speichern, in welchem Teil des Blinkzyklus die Lampen gerade sind.
Dabei entspricht...
- 0 - eine Lampe ist gerade an
- 1 - alle Lampen sind gerade aus (man ist zwischen zwei blinkern)
- ohne diesen Teil des Zyklus könnte man nicht erkennen, wenn die selbe Lampe zwei Mal direkt hintereinander dran ist.
- 2 - alle Lampen sind aus und man ist am Ende der Sequenz
- hier müssen die Lampen länger aus sein, damit man erkennt, wo die Sequenz anfängt und aufhört
Für jeden Teil dieser Zyklen gibt es eine entsprechende Zeit, für die dieser Teil aktiv sein sollte.
- `blink_length` ist die Zeit in Sekunden, die die Lampen an sein sollen
- `gap_length` ist die Zeit zwischen zwei blinkern
- `extra_gap_length` ist die Zeit zwischen zwei Durchgängen
Um diese Zeiten zu überprüfen wird `timeElapsed` benutzt. Am Ende von `Update()` wird `timeElapsed += Time.deltaTime;` ausgeführt. Time.deltaTime ist die Zeit, die vergangen ist, seitdem das letzte Mal `Update()` ausgeführt wurde. Wenn ein Teil des Zyklus vorbei ist, wird `timeElapsed` auf 0 zurückgesetzt.
Die Farbe einer aktivierten Lampe wird in `lamp_color` gespeichert und am Anfang auf einen zufälligen RGB-Wert gesetzt. Eine deaktivierte Lampe ist immer weiß.
In `OnButtonClick()` wird die entsprechende Nummer des gedrückten Knopfes zur Liste `InputSequence` hinzugefügt. Falls diese die gleiche Länge wie die richtige Sequenz hat, wird sie auf Richtigkeit überprüft. Dazu wird über die beiden Listen iteriert und wenn eine der Stellen ungleich ist, aus der Schleife ausgebrochen. In diesem Fall wackelt die GUI ein wenig (siehe GUIShaker).
##### Zahlenschloss
Die PIN vom Zahlenschloss wird in dem Command `CmdGetPin()` erstellt. Hierbei ist nicht sichergestellt, welche der Komponenten die PIN erstellt. Stattdessen überprüft die Funktion, ob der Partner (also die jeweils andere Komponente) bereits eine PIN erstellt hat. In diesem Fall wird sie vom Partner geholt. Andernfalls werden fünf zufällige Ziffern in eine Liste geschrieben.
Da es sich bei der Funktion um einen Command handelt, wird die PIN immer auf dem Server erstellt und automatisch auf die Clients übertragen.
In `showInputGUI()` wird außerdem dem Knopf die Funktion `confirmPIN()` zugewiesen und das Inputfield eingespeichert.
In `showViewGUI()` wird außerdem der entsprechende Text in der GUI auf die PIN gesetzt. Da diese zu dem entsprechenden Zeitpunkt manchmal noch nicht vom Server auf den Client synchronisiert wurde, wird in `Update()` außerdem sichergestellt, dass die PIN richtig gesetzt ist.
`confirmPIN()` wird ausgeführt, wenn der Knopf in der InputGUI gedrückt wird und wenn die enter-Taste gedrückt wird, während die InputGUI offen ist. Darin wird einfach die Liste `PIN` in einen String umgewandelt, damit sie mit der Eingabe verglichen werden kann. Wenn etwas Falsches eingegeben wird, wackelt die UI und die versuche werden um eins verringert.
##### Kabel
Die Seriennummer für die Input GUI wird als erstes generiert, hierfür wird die Methode `generateProductID()` aufgerufen, welche 6 verschiedene Ziffern von 0-9 aneinanderreiht und zurückgibt. Als nächstes werden für die View GUI die 2 Ziffernpaare für den Anfang und das Ende festgelegt. Hierfür wird jeweils mit einer 50% Chance entschieden, ob diese mit der Seriennummer übereinstimmen sollen oder nicht, falls sie nicht übereinstimmen sollen, werden, wie bei `generateProductID()`, zwei zufällige Ziffern von 0-9 aneinandergehängt. Sollten die Ziffern übereinstimmen wird der Schritt wiederholt bis sie das nicht mehr tun.
Wenn das geschehen ist, wird anhand der vorherigen Entscheidungen, ob Anfang und Ende übereinstimmen sollen, die entsprechende Reihenfolge, in der die Ziffern der Seriennummer neu zusammengesetzt werden müssen aus dem Array `digitGroups` ausgewählt. Anhand dieser ausgewählten Reihenfolge werden dann die 3 zweistelligen Zahlen erstellt und synchron mit dem Array `correctSequence`, welches hier den Wert `{0, 1, 2}` hat, sortiert, sodass dieses Array hinterher die richtige Reihenfolge, in der die Kabel laut der Anleitung durchgeschnitten werden müssen, enthält.
Nachdem die Notwendigen, der eben generierten Werte, der Instanz an dem `RatselController` mit dem Typ View zugewiesen wurden, beginnt diese mit der Vorbereitung der GUI. Hier wird die Methode `KabelViewGUI.updateTextboxes(string, string)` aufgerufen, welche in den Textboxen der GUI die entsprechenden Werte für Anfang und Ende festlegt.
Für die Vorbereitung der anderen GUI wird als erstes die Methode `generateKabels()` aufgerufen, welche eine zufällige Positionierung der Kabel generiert, nach dem diese Positionierung in `kabelSetup` gespeichert wurde, wird `KabelInputGUI.drawWires(int[])` aufgerufen mit `kabelSetup` als einziges Argument. Nachdem die GUI jetzt die richtigen Kabel anzeigt, wird mit Hilfe von `KabelInputGUI.setProductID(string)` die Seriennummer in der GUI gesetzt.
Wenn ein Kabel vom Spieler angeklickt wird, wird die Methode `KabelInputGUI.cutWire(int)` mit der Nummer des Kabels als Argument aufgerufen, welche an die `ObservableCollection<int> Maze.sequence`, die Nummer des durch geschnittenen Kabels anhängt. Die wichtige Eigenschaft des Datentyps `ObservableCollection` ist, dass es Event Listener unterstützt, denn bei der Initialisierung wird die Methode `Kabel.OnSequenceChanged` als Event Listener bei `Kabel.sequence` hinzugefügt, sodass sie bei einer Änderung der `ObservableCollection` ausgeführt wird.
Die Methode `OnSequenceChanged` überprüft jedes Mal, wenn etwas zu der `ObservableCollection` hinzugefügt wurde, ob sie die Länge 3 hat, und falls ja wird außerdem überprüft, ob die Reihenfolge stimmt. Ist die Reihenfolge Falsch, werden die Spieler zum letzten Checkpoint zurückgesetzt, ist sie richtig wird das an den RatselManager weitergeleitet und es wird sichergestellt, dass die GUI nicht mehr geöffnet werden kann.
##### Tresorschloss
Das Tresorschloss ist kein gewöhnliches Rätsel. Es wird nicht vom RatselManager gesetzt und befindet sich in jedem Durchlauf an der gleichen Stelle. Es hat auch keinen Ratselcontroller, weshalb sämtliche Interaktionen im Skript selbst überprüft werden müssen.
In `showViewGUI()` werden, nachdem die viewGUI instanziiert wurde, sämtliche Image-Komponenten in den Kinder der UI gefunden und in die Liste `lamps` geschrieben. Es wird die erste entfernt, da es sich dabei um den Hintergrund handelt.
Falls noch keine sequence existiert, wird eine neue erstellt. Dazu wird der Command `CmdAddToSequence()` benutzt. Dieser stellt sicher, dass die Elemente auf dem Server hinzugefügt werden, sodass sie auf den Clients synchronisiert werden.
In `showInputGUI()` wird die inputGUI instanziiert und das vaultLock GameObject gefunden.
Es wird außerdem der Sound geladen, der gespielt werden soll, wenn das Schloss sich dreht. Dieser wird zur Master Gruppe des AudioMixer hinzugefügt, damit er der Lautstärke aus den Einstellungen angepasst wird.
Falls noch keine Sequence existiert, wird eine erstellt. Der Command wird aber bei dem Partner ausgeführt, da dieser die Eingaben auf Richtigkeit prüft. Es wird außerdem dem Knopf `onButtonClick()` als Listener hinzugefügt.
In `onButtonClick()` wird überprüft, ob die Funktion auf der View-Komponente oder auf der Input-Komponente ausgeführt wird.
Auf der Input-Komponente wird das click-Geräusch über den AudioPlayer abgespielt und dann onButtonClick() vom Partner ausgelöst. Das Click-Geräusch wird über den AudioPlayer abgespielt, da es sein kann, dass durch diesen Knopfdruck eine neue Scene geladen wird. Ohne den Audioplayer wäre das Geräusch in diesem Fall nicht zu hören.
Auf der View-Komponente wird überprüft, ob die ausgewählte Nummer gleich der Nummer in der Sequenz an der aktiven Stelle ist. Wenn das der Fall ist, wird die aktive Stelle (dargestellt durch `stage`) mit `increaseStage()` um eins erhöht. Andernfalls werden die Versuche um eins verringert und ggf. wird der Alarm ausgelöst. Es wird außerdem das Geräusch für "falsch" abgespielt.
`increaseStage` wurde in eine eigene Funktion ausgelagert, da es sich dabei um einen Command handeln muss. Andernfalls wird stage nicht auf den Clients synchronisiert. In `increaseStage` wird außerdem überprüft, ob es drei richtige Eingaben gab. In diesem Fall ist das Spiel gewonnen und die entsprechende Szene wird geladen. Um sicherzugehen, dass die Clients nicht ins Hauptmenü zurückkehren, wird außerdem `gameWon` im EscapeMenuController auf wahr gesetzt.
In `Update()` wird die Eingabe im Rätsel kontrolliert. Zuerst wird überprüft, ob der Spieler bereits aktiv ist. Da die variable `playerController` vom aktiven Spieler gesetzt wird, ist sie null solange der Spieler nicht bereit ist.
Als nächstes wird überprüft, ob die UI gerade angezeigt wird, also ob das Rätsel gerade aktiv ist. Ist das nicht der Fall, wird überprüft, ob der Spieler versucht mit dem Rätsel zu interagieren. Ist das der Fall, wird es aktiviert.
Ist es bereits aktiv, wird überprüft, um welche Komponente es sich handelt. Wenn es sich um die Input-Komponente handelt, wird überprüft, ob es einen Input nach links oder rechts gibt, den es im letzten Frame noch nicht gegeben hat. In diesem Fall wird das vaultLock entsprechend gedreht und die Currentnumber wird erhöht oder verkleinert. Falls die Nummer danach außerhalb der Skala liegt (unter 1 oder über 9) wird sie auf 1 oder 9 zurückgesetzt.
Handelt es sich stattdessen um die View-Komponente, wird die momentan Aktive Lampe auf ihre entsprechende Farbe gesetzt. Da die Farbe auch angepasst wird, wenn die UI nicht aktiv ist, wird die Farbe in der Liste `lampColors` gespeichert.
Schließlich wird in beiden Fällen überprüft, ob die "escape"-Taste gedrückt wird und die UI wird ggf. geschlossen.
Auch wenn die UI nicht aktiv ist, es sich aber um die view-Komponente handelt und diese bereits einmal angezeigt wurde, wird die Farbe der aktiven Lampe in `lampColors` angepasst. Je nachdem, wie weit die `currentNumber` von der gewollten Nummer abweicht, ist die Lampe rot, gelb oder grün.
## Benutzerdokumentation
### Generelle Controlls
Im Hauptemenü gibt es eine Auswahl "Tutorial" dort sind die wichtigsten Controlls ebenfalls erklärt.
Ansonsten gilt:
- Vorwärts: "W" oder Pfeil nach oben
- Links: "A" oder Pfeil nach links
- Rechts: "D" oder Pfeil nach rechts
- Rückwärts: "S" oder Pfeil nach unten
- Sprinten: "lshift"
- Tür/Rätsel öffnen/interagieren: "E"
- Pausenmenü öffnen/Rätsel schließen: "esc"
Falls es bei den einzelnen Rätseln Unklarheiten gibt, hier eine kleine Erklärung:
### Kabel Kappen
Einer von euch sieht eine Reihe von Kabeln vor sich, der andere einen Entscheidungsbaum. Bei den Kabeln ist außerdem eine Seriennummer abgebildet. Mit der Seriennummer und dem Entscheidungsbaum kann eine bunte Reihe von kreuzen erhalten werden. Dies Farben der Kreuze korrespondieren mit den Farben der Kabel. Mit den Kreuzen und der Seriennummer kann dann jedem Kabel eine zweistellige Zahl zugeordnet werden. Es müssen dann die Kabel mit aufsteigender Zahl durchgeschnitten (angeklickt) werden.
Es ist auch eine kleine Erklärung auf der Seite mit dem Entscheidungsbaum zu sehen
### Decode
Einer von euch sieht eine Reihe aus fünf Symbolen und ein Eingabefeld, der andere sieht eine Tabelle, die jedem Buchstaben ein Symbol zuordnet. Mithilfe dieser Tabelle könnt ihr die Symbole entschlüsseln und ein Passwort erhalten, dass in das Eingabefeld eingegeben werden muss.
### Simon Says
Einer von euch sieht ein 3x3 Feld von Lampen, der andere sieht ein 3x3 Feld von Knöpfen. Die Lampen leuchten in einer bestimmten Reihenfolge auf. In dieser Reihenfolge müssen die entsprechenden Knöpfe gedrückt werden.
### Maze
Einer von euch sieht ein Quadrat aus kleinen Feldern und einen Roten Punkt. Der andere sieht ein Labyrinth und einen roten Punkt. Der Spieler, der nicht das Labyrinth sieht, kann seinen Punkt mit "WASD" und den Pfeiltasten bewegen und muss ihn in die rechte untere Ecke bringen, ohne eine der Linien zu überschreiten, die dem anderen Spieler markiert sind.
### Zahlenschloss
Einer von euch sieht ein Eingabefeld für den Personal Employee Code von Jon Kelly. Der andere sieht den Text: "Your Personal Employee Code is:" und eine Nummer. Außerdem gibt es eine Notiz "Don't share this Code, Jon!". Gebt den Code in das Eingabefeld ein und drückt enter.
### Schaltkreis
Einer von euch sieht eine Reihe aus Knöpfen und Abbildungen der Logischen Gatter: AND, OR, XOR und NOT. Der andere sieht einen Schaltung, die aus diesen Gattern besteht. Drückt die Knöpfe so, dass die Ausgabe am rechten Ende der Schaltung Wahr ist.
Die Schalttabellen der Gatter sind folgende:
AND
| Eingabe A | Eingabe B | Ausgabe |
| -------- | -------- | -------- |
|False|False|False|
|False|True|False|
|True|False|False|
|True|True|True|
OR
|Eingabe A|Eingabe B|Ausgabe|
| ------- | ------- | ----- |
|False|False|False|
|False|True|True|
|True|False|True|
|True|True|True|
XOR
|Eingabe A|Eingabe B|Ausgabe|
| ------- | ------- | ----- |
|False|False|False|
|False|True|True|
|True|False|True|
|True|True|False|
NOT
|Eingabe|Ausgabe|
| ----- | ----- |
|False|True|
|True|False|
### Tresorschloss
Einer von euch sieht drei Lampen vor sich. Der zweite sieht einen Drehregler mit den Zahlen 1-9. Der Drehregler kann mit A und D bzw. mit den Pfeiltasten nach links und rechts bedient werden. Die Lampen zeigen an, ob die ausgewählte Zahl richtig ist.
Wenn die Lampe grün leuchtet, ist die Zahl richtig, wenn die Zahl eine Stelle von der richtigen Zahl entfernt ist, leuchtet die Lampe gelb. Andernfalls leuchtet sie rot. Findet die richtige Zahl und drückt dann auf "Confirm". Das wiederholt ihr, bis alle drei Zahlen grün leuchten.
### Checkpoints
Jedes Mal, wenn ihr ein Rätsel löst, erreicht ihr einen Checkpoint, das bedeutet solltet ihr bei einem Rätsel jetzt zu viele Fehler machen werdet ihr zu diesem Checkpoint zurückgesetzt. So müsst ihr kein Rätsel nochmal machen. Seit aber vorsichtig! Wenn ihr es zu oft falsch macht habt ihr verloren und müsst von vorne anfangen.
## Entwicklungsdokumentation
Auf diesem [Trello-Board](https://trello.com/invite/b/rFnEgwwd/58d3f4b52000d41eccd6462a4bc1e3b4/tolles-softwareprojekt) sind Mitschriften aus den Scrum Meetings, verworfene und neu entstandene Ideen, sowie interne Notizen zu finden. Da wir das Board nicht öffentlich stellen wollten, müssen sie sich leider mit einem beliebigem Trello-Account anmelden, um es einzusehen. Falls sie noch keinen Account haben können sie folgende Anmeldedaten benutzen:
```
Email: dha22092@cuoly.com
Nutzername: Anon Ymus
Passwort: 1nf0rm@t1k
```
Ein Archiv mit sämtlichen Versionen des Projektes ist [hier](https://1drv.ms/u/s!AraPKJeXmISdo3j7JvVQWttMZCv4?e=Mz1oP5) zu finden.
In dem ZIP Ordner befindet sich außerdem eine Tabelle mit allen Versionen des Projektes.
Diese Dateien waren ursprüngliche nicht zum Teilen gedacht und sind deshalb nicht in formeller Sprache gehalten.
## Plagiatszettel
Hiermit bestätigen wir, dass wir sämtliche Inhalte in diesem Projekt selbst erstellt oder (im Falle der meisten Grafiken) die Nutzungsrechte erworben haben.
Grafiken wurden von [LimeZu](https://limezu.itch.io/moderninteriors) erstellt. Die Lizenz zur Nutzung haben wir erworben.