Hallo und Willkommen! In diesem Tutorial zeige ich dir, wie du dein eigenes Point and Click Adventure in 2D mit Unity erstellen kannst. Wir werden zusammen einen Charakter durch Mausklick bewegen lassen, mit Gegenständen und Objekten interagieren und eine Kamera erstellen. Das wichtigste ist aber, dass du diese Basis dann für dein eigenes Spiel verwendest und um deine Ideen erweiterst.
Wie programmiert man sowas? – Unity 2018
Legen wir los. Bevor du jetzt direkt in Unity einsteigst und dein Spiel erstellst, ist es sinnvoll deine Idee bzw. auch deine einzelnen Ziele festzulegen.
Ziel: Point and Click Adventure in 2D ← kein gutes Ziel
Ziel: Point and Click Adventure in 2D bei dem der Spieler über ein Level hinweg verschiedene Items finden muss um Türen zu öffnen.
Denke immer über die folgenden Punkte nach, wenn du dir dein Ziel definierst:
- steuerbarer Spieler
- Level
- Start / Ende
- Herausforderung
Damit haben wir jetzt unser Ziel für diese Videoreihe. Sollten wir das Ziel erreicht haben dann können wir sofort über neue Level, Charakter oder auch Herausforderungen nachdenken, vorher ist das aber nicht sinnvoll denn sonst wird unser Spiel einfach nicht fertig.
Mit deinem neuen Ziel kannst du dich nun auf die Suche nach Grafiken machen. Ich werde für mein Spiel eigentlich nur die folgenden benötigen:
- Level Background: https://opengameart.org/content/sci-fi-level
- Charakter: https://opengameart.org/content/bevouliin-free-game-sprites-crocodile-mascot-running-and-jumping-boy-game-character
- Tür: https://opengameart.org/content/double-door-school
- Items: https://assetstore.unity.com/packages/2d/gui/icons/free-rpg-icons-111659
Du musst zum Start jetzt nicht direkt alle Grafiken haben, verwende falls wirklich die irgendwelche Grafiken als Platzhalter. Wenn die Mechanik deines Spiels dann funktioniert kannst du dich um eigene Grafiken kümmern. Sobald du also fertig mit der Suche nach Grafiken bist, dann füge diese in dein Projekt ein und stelle siche, dass diese als Texture Type (2D and UI) importiert wurden. Sollte das nicht so sein, dann ändere es im Inspector und klicke auf den Apply Button.
Mausklick erkennen
Willkommen zurück. In diesem Abschnitt zeige ich dir wie du die Erkennung des Mausklicks für dein Point and Click Adventure programmieren kannst. Dies ist für dein Spiel essentiell, denn durch das Klicken der Maustaste bewegst du deinen Charakter fort und interagierst mit Objekten innerhalb deiner Szene.
Erstelle dir hierfür ein neues C#-Skript. Ich nenne das Skript “AdventureScript” und öffne das Skript mit einem Doppelklick. In diesem neu erstellten Skript kannst du jetzt eine Logik für den Input mit der Maus anlegen. Wie geht das genau? Verwende die Input-Klasse und dann Input.GetMouseButtonDown(0). Dadurch wird jeder Mausklick erkannt. Du kannst zum testen auch gerne eine Ausgabe über Print erstellen: print(“Maustaste gedrückt”); Starte deine Szene und klicke mehrmals in deinem Spiel mit der linken Maustaste. Die Ausgabe auf der Konsole sollte jetzt auch deinem print-Befehl entsprechen. Okay nachdem dies nun funktioniert werden wir im nächsten Schritt die Mausposition auslesen. Diese ist wichtig um später herauszufinden was der Spieler da überhaupt angeklickt hat.
Lege dir eine neue Variable “mousePos” in Form eines Vector3 an. Nun könntest du direkt die Mausposition übergeben mit Input.mousePosition. Hierbei gibt es aber ein Problem. Welches genau wirst du gleich sehen. Lass dir mit einem print Befehl die Mausposition auf deiner Konsole ausgeben.
Vector3 mousPos = Input.mousePostion;
print(mousePosition);
Du bekommst hier die Ausgabe der Mausposition nach einem Klick zurück. Jetzt ist aber die Frage, wie sich das ganze verhält, wenn wir die Kamera weiter nach rechts schieben? Ein Beispielwert an der rechten Kante ist 1900. Durch das verschieben der Kamera ist aber der Wert auch wieder 1900. Der Grund hierfür ist, dass du durch Input.mousePosition immer nur den Wert in Abhängigkeit zum Bildschirm zurück bekommst. Was genau meine ich damit? Der Bildschirm hat ein größe von 1920×1080 Pixeln. Dein Level bzw. deine Szene in Unity ist aber viel größer. Würdest du diesen Wert verwenden, dann hättest du immer den gleichen Pixel zurückgegeben und das Problem, dass du nicht identifizieren kannst welches Objekt sich da befindet. (Zeichnung erstellen: 2x Bildschirmgröße nach rechts, Tür auf die geklickt wird. Koordinate 3000px funktioniert dann nicht)
Um das eben gezeigte Problem zu beheben wandelst du deine Koordinaten in die Punkte innerhalb deiner Welt ab. Heißt du gibst deinen Input an die Kamera weiter und die Kamera soll dir dann sagen an welchem Punkt sich die Maus innerhalb der 3D Welt befindet. Also nicht mehr der Wert auf dem Bildschirm, sondern der Punkt in unserer Szene. Dazu verwendest du Camera.main.ScreenToWorldPoint(Input.mousePosition). Damit wir es vergleichen können erstelle ich einen zweiten Vector3 dann siehst du auch die Ausgabe im Vergleich auf deiner Konsole. print(mousePosReal);
Wie du siehst, jetzt hast du die genauen Koordinaten in deiner Szene. Bedeutet also, dass du den Code für den ersten Test wieder löschen kannst, da dies ja nur der Input in Relation zum Bildschirm war.
Soviel also zur Möglichkeit wie du die genaue Mausposition innerhalb deiner Szene auslesen kannst. Im nächsten Abschnitt werden wir diese Position weiterverarbeiten um einzelnen Objekte zu erkennen.
Selektierbare Objekte
Willkommen zurück. In diesem Beitrag zeige ich dir wie du Objekte in deinem Point and Click Adventure durch die Maus selektierbar machen kannst. Dies ist für dein Spiel ein wichtiges Element, da du den Spieler per Mausklick bewegen möchtest oder auch die Interaktion mit Objekten erkennen möchtest. Im ersten Schritt soll also das Ziel sein, dass du bei einem Mausklick den Namen des Objekts auf der Konsole angezeigt bekommst. Wie könnte dies aber jetzt gelöst werden? Die Antwort hierfür ist Raycasting.
Ein ray ist ein Strahl und du kannst dir diesen wie einen Laser vorstellen. Der ray wird in eine von uns gewählte Richtung ausgesendet. Du kannst also die Richtung und die Weite bestimmen. Alle Objekte die sich in diesem Bereich befinden werden dann erkannt. Ein Raycast kann sowohl 3D als auch 2D sein. Für unser Projekt reicht der Raycast in 2D aus, denn wir haben durch unsere Mausposition ausschließlich die Position entsprechend der X- und der Y-Achse. Erstelle dir als einen RaycastHit2D und übergebe hier deine Mausposition.
Wie ein RaycastHit2D aufgebaut ist, das findest du auch in der Dokumentation.
RaycastHit2D hit = Physics2D.Raycast(mousePos2D, Vector2.zero);
Durch diese neu hinzugefügte Zeile in deinem Skript wirst du jetzt beim Starten des Spiels keinen unterschied sehen, der Raycast ist nämlich für den Spieler nicht sichtbar und wir verarbeiten die Daten in unserem Skript auch nicht weiter. Das heißt, auch wenn wir ein Objekt durch unseren ray registrieren würden, wir würden es überhaupt nicht mitbekommen. Das kannst du aber ändern, indem du dir eine Ausgabe auf der Konsole erstellst sobald ein Objekt getroffen wurde. Verwende dazu deine Variable und das Parameter “.collider” und vergleiche ob dieser Wert nicht null entspricht. Ist dies der Fall dann wurde ein Objekt mit einem Collider erkannt und auf der Konsole soll print(“Objekt wurde getroffen”) ausgegeben werden. Ansonsten gibst du print(“Kein Objekt getroffen”) aus.
Teste jetzt dein Projekt über den Play-Button. Du stellst jetzt aber fest, dass eigentlich immer nach einem Mausklick die Zeichenkette “Kein Objekt getroffen” ausgegeben wird. Der Grund hierfür sind fehlende Collider an deinen Objekte. Heißt für dich, alle Objekte die der Spieler im Spiel anklicken darf müssen mit einem Collider ausgestattet werden. Füge also eine Grafik in deine Szene ein und und verpasse dieser Grafik eine neue Komponente und wähle einen 2D-Collider. Ob dies nun ein Circle- oder Box-Collider ist, das ist nicht wirklich wichtig. Danach startest du dein Spiel erneut über den Play-Button und klickst gezielt auf die neue Grafik mit dem Collider. Jetzt wird dir auch die Zeichenkette “Objekt wurde getroffen” ausgegeben. Du siehst also der Collider spielt hierbei eine große Rolle.
Im nächsten Schritt sorgst du dafür, dass du auch den Namen des game objects zurück bekommst, welches der Spieler mit der Maus selektiert hat. In der Variable hit hast du ja das Objekt gespeichert, du kannst also über diese Variable den Namen ausgeben lassen. Schreibe dazu: print(hit.collider.gameObject.name);
Starte wieder dein Spiel und teste die neue Funktion jetzt auch das gewünschte Ergebnis zurück gibt. Super, nachdem auch das funktioniert, könntest du eine weitere Grafik in dein Projekt einfügen, mit einem Collider ausstatten und testen ob du auch hier den Namen zurück bekommst.
Die Ausgabe des Namens wird für unser Projekt nicht immer hilfreich sein, denn es wird Objekte geben die ein gleiches Verhalten aufweisen. Dazu kannst du dann die game objects auf Basis eines Tags identifizieren, genau das werden wir im nächsten Abschnitt machen, denn da erstellen wir uns den Untergrund auf dem sich der Spieler dann bewegen kann.
Untergrund erkennen und Charakter teleportieren
In diesem Abschnitt programmieren wir die Fortbewegung des Charakters für dein Point and Click Adventure. Bevor wir uns aber um die Steuerung an sich kümmern ist es wichtig, dass wir den Untergrund, also den Bereich auf dem sich der Spieler bewegen kann festlegen. Würdest du nämlich jetzt dein Spiel starten und auf deine Level-Grafik klicken, dann erhältst du hier nicht einmal die Rückmeldung, dass du auf einen Collider geklickt hast. Das kannst du aber ändern: Selektiere deine Grafik und füge zu dieser Grafik einen Polygon Collider 2D hinzu. Dieser Collider gibt dir im Vergleich zu einem Circle- oder Box-Collider mehr Flexibilität. Editiere den Collider und verschiebe die Punkte so, dass alle Bereiche die du in deinem Spiel die der Spieler ansteuern kann innerhalb des Colliders liegen.
In meinem Beispiel ist das jetzt einfacher, denn ich habe die komplette Fläche des Levels ohne ein Hindernis. Du kannst aber auch mehrere Collider auf deiner Grafik platzieren und dadurch Lücken und Positionen an die der Spieler nicht kann. Es gibt aber auch noch eine andere Möglichkeit wie du manche Stellen für den Spieler nicht erreichbar machen könntest. Zu dieser Methode kommen wir aber noch. Nachdem du deinen Collider angelegt hast, startest du deine Anwendung und überprüfst ob der Name deines Untergrund game objects auch nach einem Klick zurückgegeben wird.
Die Verwendung des Namens macht nicht immer Sinn. Dein Level kann nämlich aus verschiedenen Grafiken mit einem Collider bestehen und all diese Collider sollen Teil des Untergrunds und erreichbare Positionen für den Spieler sein. Es macht also Sinn, dass du allen Grafiken die zum Untergrund gehören einen Tag vergibst. Ich nenne diesen Tag einmal “Ground”. Jetzt kannst du in deinem Skript auch über den Tag überprüfen ob es sich um den Ground handelt. Ist dies der Fall dann soll hier auch das Bewegen des Spielers ausgeführt werden. Teste es zuerst mit einem Print-Befehl auf deiner Unity-Konsole.
Im nächsten Schritt erstellst du dir deinen Spieler. Ich verwende in meinem Beispiel dieses Maskottchen und füge es zu meiner Hierarchie hinzu. Die Grafik benötigt fürs erste keine weiteren Komponenten. Ziel ist es jetzt, dass nach dem Klicken die Position der Maus ausgelesen wird und an die Transform-Komponente der Spieler-Grafik weitergegeben wird. Dadurch teleportiert sich der Spieler dann auch direkt an seinen Zielort. Verwende dazu eine neue public GameObject Variable für den Spieler, verlinke den Spieler mit dem Skript und schreibe in deinem Skript dann: myPlayer.transform.position = hit.point. Für hit.point kannst du dir auch gerne eine Variable in Form eines Vector2 anlegen und das ganze dann über eine Variable zuweisen. myPlayer.transform.position = destination;
Starte deine Anwendung und achte darauf was passiert wenn du mit der Maustaste auf dein Level, also deinen Ground klickst. Wie du siehst jetzt teleportiert sich der Spieler direkt an die selektierte Position.
Zu Beginn habe ich erwähnt, dass du mit Hilfe von Grafiken dafür sorgen kannst, dass der Collider an manchen Stellen nicht funktioniert und somit kein Mausklick erkannt wird. Zieh hierfür eine beliebige Grafik in deine Szene und positioniere diese an die Stelle die nicht klickbar sein soll. Jetzt mache die Grafik unsichtbar und schon überlagert die Grafik den Collider, der Ground wird an dieser Stelle nicht erkannt und der Spieler ändert auch nicht seine Position durch den Mausklick.
Die Teleportation ist jetzt vielleicht nicht unbedingt deine gewünschtes Ergebnis zum Fortbewegen deines Charakters. Keine Sorge, im nächsten Abschnitt werden wir den Charakter nach einem Mausklick an die neue Position mittels einer von uns festgelegten Geschwindigkeit bewegen lassen.
Steuerung des Charakters
In diesem Abschnitt werden wir die Steuerung des Charakters verbessern. Derzeit können wir mit Hilfe eines Mausklicks den Spieler an eine neue Position teleportieren. Wie ist es aber jetzt, wenn du den Spieler an seinen Zielort mittels einer gewünschten Geschwindigkeit gehen lassen möchtest. Hierzu ist es hilfreich, dass du dir sowohl die aktuelle Position, als auch die Zielposition des Charakters speicherst. Erstelle dir dazu eine neue Methode. Diese Methode nennst du FixedUpdate. Es ist wichtig, dass du die Methode genau so schreibst, denn dies ist eine Unity-Methode und wir verwende sie für die Fortbewegung. Die FixedUpdate-Methode hat immer einen einheitlichen Zeitabstand und wir müssen uns damit keine Sorgen um die tatsächlichen Bilder pro Sekunde des Spielers machen.
In der FixedUpdate-Methode schreibst du:
myPlayer.transform.position = Vector3.MoveTowards(myPlayer.transform.position, destination, speed);
Die MoveTowards-Methode benötigt jetzt drei verschiedene Übergabeparameter:
- Ursprungsposition
- Unser neues Ziel
- Geschwindigkeit (Zeit wie lange es dauern soll um von Ursprung zum Ziel zu kommen)
Lege dir also für die Geschwindigkeit eine Variable an, z.B. public float speed;
Weitere Information zu MoveTowards findest du auch hier. Durch unsere neue Logik ist die alte Logik aus Episode 04 hinfällig, das heißt du kannst die Zeile: myPlayer.transform.position = destination; auskommentieren.
Lege im Inspektor die speed-Variable fest. Ich nehme hier jetzt einmal einen Wert von 0.1 und starte nun mit deiner neuen Logik dein Projekt. Achte darauf was bei einem Mausklick auf den Untergrund passiert. Super, der Charakter bewegt sich nun von seiner Ursprungsposition zur Zielposition. Du kannst die speed-Variable so anpassen, dass sie zu deinem Setting des Spiels passt. Hast du also eine Schnecke dann macht es Sinn einen kleineren Wert zu wählen, hast du aber als Charakter Speedy Gonzales dann ist es sinnvoll einen höheren Wert zu wählen.
Unseren Code können wir noch ein bisschen optimieren. Im Fixed-Update wird nämlich immer und immer wieder das MoveTowards ausgeführt, auch wenn der Spieler schon an seiner Zielposition ist. Erstelle dir hierfür eine boolesche-Variable z.B. “isMoving” und setze die Variable auf wahr sobald der Untergrund erkannt wurde. Dadurch stoßen wir dann in der Fixed-Update-Methode auch den Befehl zum bewegen an.
Das alles macht aber nur Sinn, wenn du isMoving auch wieder auf false setzt sobald der Spieler das Ziel erreicht hat. Achte hierbei aber darauf, dass du sowohl die X- als auch die Y-Achse abgleichst.
In deinem Spiel erkennst du durch diese Logik jetzt aber keinen Unterschied, es ist aber sinnvoll das Ganze zur Sicherheit noch einmal zu testen. Soviel also zur Steuerung des Charakters.
Charakter Blickrichtung bestimmen
Willkommen zurück. In diesem Abschnitt zeige ich dir wie du die Blickrichtung deines Charakters für dein Point and Click Adventure programmieren kannst. Dir ist vielleicht schon aufgefallen, wenn du mit der Maustaste in den linken Spielbereich des Charakters klickst, dann läuft der Charakter zwar in diese Richtung, jedoch dreht er sich nicht um. Und das sieht nicht wirklich schön aus. Wie kannst du dies also beheben? Die Antwort findest du im SpriteRenderer deines Charakters. Hier gibt es den Wert FlipX. Selektiere die Checkbox bei FlipX und du siehst, dass die Grafik sich jetzt gespiegelt hat. Genau diesen Effekt benötigen wir. Du musst dir also jetzt überlegen wann deine Grafik denn überhaupt gedreht werden soll. Eigentlich ja nur, wenn sich die X-Koordinate des Spielers ändert.
X.Position Start > X.Position Ende
X.Position Start < X.Position Ende
Das skizzierte programmierst du jetzt innerhalb deines Skripts. Begebe dich dazu in die Bedingungsabfrage für deinen Ground und erstelle dir hier in Abhängigkeit zur Position eine Bedinungsabfrage. Hierfür ist es aber notwendig, dass du sowohl die gewünschte Position X als auch die derzeitige Position X kennst. Speichere diese also vorher zwischen und dann kannst du die Werte auch für den FlipX verwenden.
Starte dein Spiel über den Play-Button und wie du jetzt siehst, sobald du mit der Maus in die Entgegengesetzte Richtung klickst dreht sich automatisch die Grafik des Spielers um.
Kamera erstellen und Spieler folgen
In diesem Abschnitt widmen wir uns der Kamera in unserem Point and Click Adventure. Bisher ist es nämlich nicht möglich, das wir uns außerhalb des Bildschirmbereichs bewegen können. Heißt also, dass du mit deinem Level nur auf die breite des Bildschirms beschränkt bist, da die Kamera dem Spieler nicht folgt. Das werden wir nun ändern.
Lade dir zuerst Cinemachine in dein Projekt. Cinemachine ist eine Unity Erweiterung speziell für Kamerafahrten. Beim Erstellen eines Kamera-Skripts zum Folgen eines Spielers gibt es viele Sachen zu beachten. Eine professionelle Lösung macht daher aus meiner Sicht mehr Sinn. Füge das kostenlose Asset zu deinem Projekt hinzu. Nachdem du es importiert hast wählst du in der Menüleiste Cinemachine > Create 2D Camera. Dadurch wird ein neues game object in deiner Hierarchie erstellt “CM vcam1”. Diese virtuelle Kamera benötigt noch einen Punkt zum folgen. Du musst also der Kamera mitteilen welches Objekt in deiner Szene verfolgt werden soll. In unserem Point and Click Adventure ist das ja der Charakter. Also selektiere das Charakter game object und ziehe es auf den Eintrag “Follow” der Cinemachine Virtual Camera. Starte dein Spiel und achte darauf was nun passiert. Wie du siehst, die Kamera folgt dem Spieler und du erreichst jetzt auch andere Bereiche deines Levels da die Kamera diese Bereiche durch das Folgen anzeigt. Wirklich schön sieht das aber noch nicht aus. Du kannst aber die Werte der virtuellen Kamera jederzeit verändern. Der Wert Field of View hilft dir dabei näher an die Szene heranzoomen zu können.
Es macht durchaus Sinn, dass du deinen Polygon Collider so platzierst, dass die Kamera auch wirklich nur den Bereich der auch mit einer Grafik ausgestattet ist einfängt.
Objekte einsammeln
Tür öffnen – Level wechseln
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class AdventureScript : MonoBehaviour
{
public Vector3 mousePos;
public Camera mainCamera;
public Vector3 mousePosWorld;
public Vector2 mousePosWorld2D;
RaycastHit2D hit;
public GameObject player;
public Vector2 targetPos;
public float speed;
public bool isMoving;
public bool key = false;
public int stones = 0;
// Use this for initialization
void Start()
{
}
// Update is called once per frame
void Update()
{
// Wurde die Maustaste gedrückt?
if (Input.GetMouseButtonDown(0))
{
// Maustaste wurde erkannt
print("Maustaste wurde gedrückt");
// Mausposition auslesen
mousePos = Input.mousePosition;
// Mausposition auf Konsole ausgeben
print("Screen Space: " + mousePos);
// Koordinaten von Screen Space nach World Space umwandeln
mousePosWorld = mainCamera.ScreenToWorldPoint(mousePos);
// World Space Koordinaten auf Unity Konsole ausgeben
print("World Space: " + mousePosWorld);
// Umwandlung von Vector3 in Vector 2
mousePosWorld2D = new Vector2(mousePosWorld.x, mousePosWorld.y);
// Raycast2D => Hit abspeichern
hit = Physics2D.Raycast(mousePosWorld2D, Vector2.zero);
// Überprüfe ob hit einen Collider beinhaltet
if (hit.collider != null)
{
print("Objekt mit Collider wurde getroffen!");
// Ausgabe des getroffenen game objects (name)
print("Name: " + hit.collider.gameObject.tag);
// Abfrage ob es der Ground ist
if (hit.collider.gameObject.tag == "Ground")
{
// Position des Spielers verändern
//player.transform.position = hit.point;
targetPos = hit.point;
// isMoving wahr, damit sich Spieler bewegt
isMoving = true;
// Überprüfe ob Sprite-Flip notwendig ist
CheckSpriteFlip();
}
// Abfrage ob es der Schlüssel ist
else if(hit.collider.gameObject.tag == "Key")
{
// Es ist der Schlüssel
// Grafik deaktivieren
hit.collider.gameObject.SetActive(false);
// Schlüssel im Skript abspeichern
key = true;
// Anzahl der Steine um 1 erhöhen
stones = stones + 1;
}
// Abfrage ob es die Tür ist
else if(hit.collider.gameObject.tag == "Door")
{
// Tür erkannt
// Wurden 5 Steine gesammelt?
if(stones >= 5)
{
// Schlüssel wurde aufgesammelt
// Nächstes Level laden
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex + 1);
} else
{
print("Du benötigst fünf oder mehr Steine um die Tür zu öffnen");
}
}
}
else
{
print("Kein Collider erkannt!");
}
}
}
private void FixedUpdate()
{
// Überprüfe ob Spieler sich bewegt?
if(isMoving)
{
// Spieler an Zielort befördern
player.transform.position = Vector3.MoveTowards(player.transform.position, targetPos, speed);
print("Spieler wird bewegt");
// Ist der Spieler am Zielort?
if(player.transform.position.x == targetPos.x && player.transform.position.y == targetPos.y)
{
// Spieler am Zielort => isMoving "deaktivieren"
isMoving = false;
print("Spieler am Zielort");
}
}
}
void CheckSpriteFlip()
{
if(player.transform.position.x > targetPos.x)
{
// Nach links blicken
player.GetComponent().flipX = true;
}
else
{
// Nach rechts blicken
player.GetComponent().flipX = false;
}
}
}