Jump to content
Unity Insider Forum
Sign in to follow this  
Sascha

C# in Unity für Einsteiger

Recommended Posts

C# ist schon seit langer Zeit das Mittel der Wahl für Programmieren für Unity. Der Support für Boo wurde gänzlich abgeschafft, und Unitys JavaScript-Variante beschert einem mehr Probleme als Vorteile. Leider stellt C# eine etwas größere Einstiegshürde dar als "UnityScript", aber nach eventuellen anfänglichen Scherereien wird es immer einfacher zu verwenden.

Dieses Tutorial richtet sich an reine Programmieranfänger, aber auch Erfahrenere, die neu in Unity sind, können etwas davon haben. Dieses Tutorial ist weniger ein "Wie mache ich XY", als mehr eine generelle Einführung, die alle folgenden Schritte in der Welt von C# in Unity (besser) verstehbar machen soll. Am Ende wird also kein großartiges Ergebnis zum Zeigen vorliegen, aber dafür solltest du ein gutes Verständnis dafür haben, was in Unity überhaupt passiert.

Mein Code als Komponente

Wer dieses Tutorial liest, hat vermutlich den grundlegenden Workflow des Unity-Editors bereits vor Augen gehabt. Objekte in einer Szene (GameObjects) bestehen aus einer Reihe von Komponenten, die das GameObject ausmachen. So ist ein GameObject mit einem Collider etwas, wogegen andere Objekte prallen können, und eines mit MeshRenderer und MeshFilter ist ein sichtbares 3D-Modell. Folglich ist ein GameObject mit MeshRenderer, MeshFilter und Collider ein sichtbares 3D-Objekt, mit dem man kollidieren kann. Ein GameObject ist also die Summe aller seiner Komponenten.

Für jedes Spiel muss das, was passieren soll, programmiert werden. Im Unity-Kontext kommt der Code, den man dabei schreibt, in Form von Komponenten zum Einsatz. Man schreibt also etwas Code in eine Datei und fügt diesen als Komponente zu einem GameObject hinzu. Diese Scripts sind größtenteils dazu da, das GameObject, dem sie hinzugefügt werden, zu beeinflussen. Ein Beispiel wäre ein Script, das das Objekt rotieren lässt. Man nehme also einen MeshRenderer, einen MeshFilter, einen Collider und dazu dieses Script als Komponente. Das ergebnis ist ein sichtbares 3D-Objekt, mit dem man kollidieren kann und das sich dreht. Diese Grundidee, in welcher Form ein Script das Spiel, das man baut, formt und beeinflusst, ist immer im Hinterkopf zu behalten.

Mein erstes Script

Um ein neues Script zu erstellen, einfach mit Rechts in die Assets (oder oben links aus "Assets") klicken, dann auf "Create", dann "C# Script". Die entstandene Script-Datei kann und sollte man direkt vernünftig benennen. Dabei ist es nicht unsinnvoll, sich an Unitys Namensgebung zu orientieren, denn der Dateiname wird gleichzeitig der Name der Komponente. Neben "MeshRenderer", "Collider" und "Light" würde also "SimpleRotation" oder "SimpleRotator" passen.

Öffnet man das Script, wird der eingestellte Script-Editor gestartet. Zu empfehlen ist Visual Studio, aber hier kann man nach eigenen Präferenzen gehen. Was man dann sieht, ist in etwa folgendes:

using UnityEngine;
using System.Collections;

public class ScriptName : MonoBehaviour {
  
    // Use this for initialisation
    void Start() {
        
    }
	
    // Update is called once per frame
    void Update() {
        
    }
}

Sehen wir uns die Einzelteile davon einmal an, damit wir anfangen können, darin zu arbeiten.

Am Anfang stehen diese beiden Zeilen:

using UnityEngine;
using System.Collections;

Diese Zeilen heißen "Imports" oder "Using-Statements". Sie geben für den Rest des Codes an, was alles an bereits existierendem Code bekannt sein soll. Unity ist ein riesiger Haufen Code, den unser Code kennen muss, damit man damit arbeiten kann. Die Light-Komponente z.B. ist irgendwo in "UnityEngine" vorhanden, und wenn man von einer solchen Komponente die Farbe ändern will, dann muss bekannt sein, was ein Licht überhaupt ist. Es ist also etwa so, als würde ein Schullehrer ein neues Thema anfangen und zum Beginn der Stunde einige vorherige Themen in's Gedächtnis rufen, auf denen der neue Stoff aufbaut.

System.Collections ist standardmäßig mitimportiert, wird aber erst einmal gar nicht gebraucht. Diese Zeile kann man sogar problemlos löschen, bevor man weiter macht.

Als nächstes folget ein Haufen Begriffe, gefolgt von einem Rumpf. Einen Rumpf erkennt man immer an den geschweiften Klammern { } .

public class ScriptName : MonoBehaviour {
  // Das hier ist im Rumpf
}

Der C#-Standard ist eigentlich, die geschweifte Klammer in die nächste Zeile zu machen. Ist absolut Geschmackssache - bei mir sieht das jedenfalls so aus:

public class ScriptName : MonoBehaviour
{
  // Das hier ist im Rumpf
}

Wir sehen hier also etwas Text, gefolgt von einem Rumpf. In den allermeisten Fällen ist das, was vor dem Rumpf steht, mit dem Rumpf verbunden, oder besser: Der Rumpf gehört zu diesem Text. Was also sagt dieser Text aus?

public class ScriptName : MonoBehaviour

Zuerst einmal steht da "public class". Das public können wir jetzt erstmal ignorieren - es steht da und macht irgendetwas. "class" ist da schon wichtiger - es sagt aus, dass im folgenden Rumpf eine so genannte Klasse deklariert wird. Eine Klasse ist ein Begriff aus der Objektorientierten Programmierung und ist eine Art Bauplan für Objekte. Der Bildschirm vor dir ist sozusagen ein Objekt (oder "Instanz" oder "Exemplar") der Klasse "Bildschirm". Oder vielleicht der Klasse "Flachbildschirm", je nach dem, wie spezifisch da deklariert wurde. In unserem aktuellen Spezialfall ist ein Objekt eine Komponente.

Man kann mehreren GameObjects eine Light-Komponente geben, und sie sind alle mehr oder weniger gleich. Sie alle haben eine Farbe, eine Intensität und mehr Eigenschaften, und sie alle machen auf irgendeine Weise Licht in der Szene. Und doch können sie alle unterschiedlich sein: Das eine Licht ist rot, das andere blau. Es handelt sich hierbei um verschiedene Objekte, aber der grundlegende Aufbaum, also die Klasse, ist immer dieselbe: Light.

Wann immer wir in einer objektorienterten Sprache wie C# Code schreiben, existiert dieser in Form von Klassen. Jedes Script ist eine Klasse und jedes Mal, wenn wir das Script zu einem GameObject hinzufügen, wird ein neues Objekt, also eine neue Komponente dieser Klasse erstellt, die dann auf diesem GameObject existiert, bis sie oder das GameObject wieder gelöscht werden.

Der nächste Begriff kommt uns bekannt vor: Es ist der Dateiname und damit der Name der Klasse, die wir hier schreiben wollen.

Anschließend steht da noch etwas kryptisches:

: MonoBehaviour

Hier steckt eine ganze Menge drin, aber zu diesem Zeitpunkt kann man diesen Teil vereinfachen und sagen: Dieser Ausdruck sorgt dafür, dass Objekte unserer Klasse auch wirklich Komponenten sind. Löscht man diesen Teil, kann man das Script anschließend nicht mehr zu einem GameObject hinzufügen.

Damit haben wir jetzt die Klassendefinition gesehen. Der darauffolgende Rumpf wird alle möglichen Informationen über unsere Klasse enthalten, so wie der Rumpf der Light-Klasse irgendwo in UnityEngine den Code enthält, der Objekte anleuchtet und der angibt, dass eine Lichtkomponente eine Farbeigenschaft hat.

Im Rumpf stehen bereits irgendwelche Dinge, die wir uns gleich ansehen werden. Zuerst einmal gehen wir aber zurück in den Editor und sehen uns eine wichtige Kleinigkeit an - und zwar, wie man ein Script einem GameObject hinzufügt. Das ist denkbar einfach: Per Drag & Drop kann man die Datei auf den leeren Platz eines GameObjects im Inspektor ziehen; unter die anderen Komponenten. Man kann sie auch auf das GameObject in der Hierarchie ziehen oder den "Add Component"-Button im Inspektor benutzen. Scripts sind standardmäßig in der Kategorie "Scripts".

Methoden

Zwei Absätze weiter oben habe ich zwei Beispiele dafür gegeben, was in einer Klasse drinsteht: Die Light-Komponente leuchtet Objekte an, sie tut etwas, und sie hat eine Fabreigenschaft, sie ist irgendwie. Diese beiden Dinge, "wie es ist" und "was es tut" sind genau die beiden Bausteine, aus denen eine Klasse aufgebaut ist.

Fangen wir an mit "was es tut". Die Abläufe dessen, was ein Objekt tut, sind in so genannten Methoden deklariert. Von denen hat unser neues Script bereits zwei:

    // Use this for initialisation
    void Start() {
        
    }
	
    // Update is called once per frame
    void Update() {
        
    }

Eine leere Methode ist nicht ganz unähnlich einer leeren Klasse aufgebaut. Wieder haben wir einnige Worte, gefolgt von einem Rumpf. Diese beiden Methoden heißen Start und Update. Bei beiden steht vorher "void" und beide haben danach runde Klammern ohne etwas darin. Beides wird für uns erst später relevant und kann daher ignoriert werden. Interessant sind für uns die Namen der Methoden, aber dazu gleich mehr.

Eine Methode ist grundätzlich eine Reihe von Anweisungen, die nacheinander ausgeführt werden, wenn die Methode aufgerufen wird. Eine Methode aufzurufen ist verlgleichbar damit, einen Knopf bei einem Gerät zu drücken. Plötzlich passieren im Gerät irgendwelche Dinge, und vielleicht kommt sogar ein sichtbares Ergebnis heraus, zum Beispiel eine Flasche Wasser. Allem voran steht aber das Drücken des Knopfes, oder eben das Aufrufen der Methode.

Um eine Methode aufzurufen, ist es sozusagen erst einmal nur nötig, ihren Namen zu schreiben. Das testen wir gleich, aber vorher sehen wir uns die beiden Methoden an, die wir schon haben. Über den Methoden stehen so genannte Kommentare. Alles, was rechts von "//" steht, ist ein Kommentar und wird vom Computer komplett ignoriert. Man kann dort also alles hinschreiben, was den Code erklärt, wenn gewünscht auch auf deutsch. Die beiden Kommentare über unseren Methoden beschreiben eine Besonderheit, wenn man mit Unity entwickelt. Anhand ihrer Namen nimmt sich Unity das Recht, diese Methoden geradezu magisch im richtigen Moment aufzurufen. Eine Methode mit dem Namen "Start" wird einmalig aufgerufen, wenn das Objekt neu entsteht - das versteht sich inklusive aller Objekte zum Start des Spiels.

Innerhalb eines Methodenrumpfs können jetzt unter anderem weitere Methoden aufgerufen werden. So entsteht eine sinnvolle Kette von Abläufen. Die verschiedenen Objekte werden hierbei wie eine Firmenhierarchie: Ein Kunde ruft in einer Firma an und bestellt eine Dienstleistung. Die Sekretärin informiert die Chefin. Die Chefin schickt einen Mitarbeiter los. Jede dieser Interaktionen ist eine mögliche Metapher für jeweils einen Methodenaufruf, und die verscheidenen Leute sind die Objekte.

Wir bauen jetzt eine ganz simple Kette und fügen folgenden Code in die Start-Methode ein:

// Use this for initialisation
void Start()
{
	Debug.Log("Hallo!");
}

Debug.Log ist eine bestimmte Methode. Warum sie so heißt, wie sie heißt und wieso da ein Punkt drinsteckt, ist erst einmal egal. Die runden Klammern deuten den Methodenaufruf an. In den Klammern steht dieses Mal allerdings etwas. In den Anführungszeichen steht ein String, eine Zeichenkette, also etwas, das Worte, Sätze oder ganze Texte beinhalten kann. Debug.Log nimmt diesen String und schreibt ihn in Unitys Konsole. Am Ende jeder Anweisung wie dieser steht ein Semikolon ; , das die Anweisung beendet.

Speichere das Script und ab zurück zu Unity. Stelle sicher, dass irgendein Objekt in der Szene das Script als Komponente hat und starte das Spiel. Du müsstest in der Konsole (Strg + Shift + C) und ganz unten links im Editor das "Hallo!" sehen können.

Hier hat also Unity Start() aufgerufen und in Start wurde dann wieder Debug.Log aufgerufen. Der String in den Klammern nennt sich dabei Parameter. Ein Parameter spezifiziert genauer, was eine Methode tun soll. Während bei einigen Tätigkeiten keine Parameter nötig sind (z.B. "starte das Auto"), benötigen einige andere genauere Angaben, wie sie im jeweiligen Moment zu funktionieren haben (z.B. "Tritt das Gaspedal" - aber wie doll?). Bei Debug.Log muss angegeben werden, was genau in die Konsole geschrieben werden soll. Würde man der Methode nichts geben, also die runden Klammern leer lassen, würde der ganze Aufruf keinen Sinn mehr ergeben und der Code wird als fehlerhaft erkannt.

Jetzt machen wir aber mal etwas Unity-bezogenes.

Das Drehskript

Wir bauen jetzt ein Skript, das das Objekt dreht, auf dem es als Komponente liegt. Zuerst machen wir das in der Start-Methode. Unser Objekt wird also einmalig zum Spielstart auf eine bestimmte Drehung gesetzt und bleibt dann so.

Wenn es um Position oder Rotation geht, ist immer die Transform-Komponente wichtig. Sie ist immer ganz oben in der Komponentenliste und beim Verschieben oder Drehen eines Objekts geht es immer um ihre Felder. Sie hat außerdem die Besonderheit, dass jedes GameObject genau eine davon hat. Die Transform-Komponente kann man ansprechen, indem man einfach transform schreibt:

void Start()
{
  transform.Rotate(20, 20, 20);
}

Woah, jetzt aber mal langsam!

Wir sehen hier wieder einen Methodenaufruf, zu erkennen an den Klammern nach einem Methodennamen (Rotate). Doch vor dem Methodennamen steht jetzt transform und dazwischen ein Punkt!

Der Punkt ist extrem wichtig. Er erlaubt es, Methoden von anderen Objekten aufzurufen. Die Methode Rotate ist Teil der Klasse "Transform", also der Klasse, die als Bauplan für die Transform-Komponenten dient, die auf den GameObjects liegen. Um nun ein bestimmtes Objekt zu drehen, muss man angeben, welches Objekt das überhaupt ist. Beachte hierbei, dass ich nicht einmal von GameObjects rede, sondern von Objekten im programmiertechnischen Sinne. Die Transform-Komponente unseres GameObjects ist ein solches Objekt.

Mit dem Punkt vereint man nun zwei Dinge: Zur linken steht das Objekt, von dem man etwas will, und rechts steht der Name der Methode, also das, was man vom Objekt will. In diesem Fall also: Ich will, dass sich unsere Transform-Komponente dreht. In den Klammern sind dieses Mal gleich drei Parameter. Diese entsprechen den drei Achsen, um die ein Objekt gedreht werden kann. Der Reihenfolge nach X, Y, und Z.

Dieses Skript kannst du jetzt ausprobieren und nach Belieben die Zahlen ändern.

Als nächstes wollen wir, dass sich das Objekt durchgehend immer weiter dreht, womit wir dem Ende dieses ersten Einstiegs nahe kommen. Wie in den Kommentaren im Code angedeutet, wird Update immer und immer wieder aufgerufen. Genauer: Es werden alle Update-Methoden von allen Komponenten auf allen GameObjects in der Szene aufgerufen, und danach wird das Bild neu gemalt. Der Ablauf ist eigentlich etwas komplexer, aber das ist das grundlegende Prinzip. Update findet auf jedem Objekt also genau so oft statt wie das Bild neu gezeichnet wird, also abhängig von den FPS des Spiels.

Verschiebe nun einfach die Zeile mit transform.Rotate von Start() nach Update(), also in dessen Rumpf, speichere das Script und teste das Spiel. Dein Objekt dreht sich nun immer weiter, weil es in jedem Frame, also in jedem Update ein bisschen weiter gedreht wird.

Aber warte mal... wenn das Spiel jetzt doppelt so viele FPS hast, dann wird Update doppelt so oft aufgerufen! Und wenn es sich doppelt so oft gleich weit dreht, dann dreht es sich doppelt so schnell! Hierfür gibt es Abhilfe: Time.deltaTime.

Wie an den nicht vorhandenen Klammern zu erkennen, handelt es sich hier nicht um einen Methodenaufruf, sondern um eine so genannte Eigenschaft. Eine Eigenschaft stellt einen einzelnen Wert dar. Ein Beispiel dafür wäre wieder die Farbe einer Light-Komponente. Time.deltaTime allerdings gehört zu keinem Objekt - wie das kommt, ist allerdings etwas für ein anderes Kapitel.

Time.deltaTime hat als Wert nicht etwa rot oder blau, sondern einen Zahlenwert; genauer: Die Zeit seit dem letzten Frame in Sekunden, inklusive Nachkommastellen. Diese Zahl ist also halb so groß, wenn wir doppelt so viele FPS haben, und umgekehrt.

Wenn wir Time.deltaTime mit einer beliebigen Zahl multiplizieren, so bedeuetet das quasi "pro Sekunde". Wenn wir wegen hoher FPS doppelt so oft drehen, aber dafür jedes Mal nur halb so weit, dann haben wir eine konstante Rotation unabhängig von der Framerate. Multiplikation funktioniert, wie zu erwarten, mit dem *-Operator:

void Update()
{
  transform.Rotate(0, 20 * Time.deltaTime, 0);
}

 

Von hier aus gibt es noch jede Menge zu erzählen, bevor man so richtig loslegen kann. Ich hoffe allerdings, dass dieser Text hilft, zu verstehen, was man da genau schreibt, wenn man C# codet, und wie die Begriffe richtig heißen. Mal sehen, ob ich die Zeit finde, von hier aus kleinere Tutorials zu schreiben, die auf diesem aufbauen. Bis dahin kannst du hoffentlich andere Tutorials mit diesem hier kombinieren, um weiter zu kommen. Auf lange Sicht soll dieser Text und die darauf folgenden aber unsere JS-Tutorials ersetzen. Bis dahin!

  • Like 8

Share this post


Link to post
Share on other sites

Auf der Suche nach Anfängerfreundlichen Erklärungen für C# in Unity bin ich auf dein Thread gestossen Sascha.

Sehr schön geschrieben und einfach erklärt. Vielen Dank!

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
Sign in to follow this  

×