Jump to content
Unity Insider Forum

Mark

Members
  • Gesamte Inhalte

    2.769
  • Benutzer seit

  • Letzter Besuch

  • Tagessiege

    138

Alle erstellten Inhalte von Mark

  1. Heute fangen wir damit an dass wir den CameraController um die Fähigkeit erweitern sich mit zu drehen. Wer sich noch erinnert: Dies war eine Option die wir im ersten Tutorial sogar explizit ausgeschlossen haben. Heute bauen wir diese Fähigkeit aber wieder ein da wir nun um einiges an Wissen reicher sind als zuvor. Wir wollen dass unsere Kamera außerdem etwas sanfter wird, sie quasi etwas hinter dem Spieler herzieht, was dem Spiel eine bessere Dynamik verschafft. Das hinter her ziehen wollen wir auch für die Rotation der Kamera. Öffnet also den CameraController. Dann fügen wir die Variablen ein die angeben wie schnell unsere Kamera sich anpassen soll und wie die Neigung der Kamera sein soll (dies haben wir bisher direkt anhand der Rotation festgelegt): public float MoveSpeed = 1.0f; public float Pitch = 70.0f; Wir fangen nun damit an dass wir unsere Kamera sanft bewegen, aktuell ist die Bewegung ja plötzlich und final: transform.position = Target.position - Distance * transform.forward; Wir ändern diese Zeile so dass wir den Wert nicht direkt der transform.position zuweisen sondern an eine lokale Variable übergeben. Wir berechnen außerdem eine Position die direkt hinter dem Spieler liegt und unsere Kameraneigung mit in betracht zieht. var finalPosition = Target.position + Quaternion.Euler(Pitch, Target.eulerAngles.y, 0) * -Vector3.forward * Distance; Wir verwenden hierbei Quaternions, Quaternons sind 4 Dimensionale Mathematische Konstrukte die eine Rotation beschreiben. Eine andere Möglichkeit eine Rotation zu beschreiben ist die Rotation an den 3 Grundachsen festzusetzen (Yaw Pitch und Roll, welche zusammen Euler Winkel genannt werden). Quaternions erlauben es uns leichter mit Rotationen auf Vektoren zu rechnen. Dies nutzen wir um uns die Position hinter dem Ziel zu berechnen indem wir leichter verständliche Eulerwinkel an eine Quaternion übergeben. Um nun eine sanfte Bewegung zu erreichen benutzen wir eine neue Methode der Vector3 Klasse, die Lerp Methode. Lerp interpoliert 2 Vectoren in Abhängigkeit einer Kontrollvariable T die von 0.0 bis 1.0 reicht. Je höher der Wert von T desto stärker entspricht der Rückgabewert dem zweiten Parameter. Wir berechnen T aus der Time.deltaTime multipliziert mit der MoveSpeed. Anschließend begrenzen wir T auf einen Bereich von 0 bis 1. Dies geht mit der Methode Mathf.Clamp01 welche genau dies erledigt. Wir schreiben also nun folgendes unter die Zeile mit der lokalen Variable: transform.position = Vector3.Lerp(transform.position, finalPosition, Mathf.Clamp01(Time.deltaTime * MoveSpeed)); Speichert das Script und testet das Ergebnis in Unity. Wenn euch die Bewegung zu langsam ist so könnt ihr die MoveSpeed im Inspektor erhöhen. Ein Wert von 5 wirkt recht angenehm. Wir werden nun sicher eine leicht ruckelige Bewegung der Kamera oder unseres Spieler Raumschiffes feststellen. Dies hat folgenden Grund: Da wir unser Raumschiff über das Rigidbody steuern setzt die Physikberechnung die Position des Raumschiffes. Die Physik läuft mit einer anderern Frequenz als das zeichnen der Frames. Wir müssten die Kamera aber mit der gleichen Frequenz verändern, anstatt also wie bisher mit LateUpdate zu arbeiten reicht es wenn wir die Methode in FixedUpdate umbenennen. FixedUpdate wird nämlich mit der gleichen Frequenz ausgeführt wie unsere Physik. Nun lässt sich unser Raumschiff viel besser steuern, findet ihr nicht auch? Nun noch ein anderes Problem, der Gegner kollidiert andauernd mit uns, wir haben nun die Wahl: Werten wir eine Kollision so aus wie ein Projektil: Wenn wir getroffen werden explodieren wir. Oder ignorieren wir die Kollisionen? Beides wäre eine gute Entscheidung für dieses Problem, für uns und unsere schwache KI die bisher nichts von ausweichen hält wäre es aber recht lästig. Daher entscheiden wir uns für das Ignorieren von Kollisionen. Bisher haben wir Tags verteilt, Kollisionen basieren auf einem ähnlichen System. Die sogenannten Layer. genauso wie Tags können wir Layer erstellen und zuteilen. Erstellt daher wie zuvor bei den Tags eine Reihe von neuen Layern: "TeamA" "TeamB" "Projektile TeamA" "Projektile TeamB" Setzt dann, sowie bei dem Zuweisen der Tags, die Layer der Schiffe. Das Spieler Schiff bekommt TeamA und der Gegner TeamB als Layer. Die Projektile selbst bekommen ihren Layer im Script welches sie abfeuert spendiert. Öffnet also das ProjectileWeapon Script und ersetzt die Zeile: Physics.IgnoreCollision(collider, projectile.collider); durch diese: projectile.layer = LayerMask.NameToLayer("Projektile " + LayerMask.LayerToName(gameObject.layer)); Diese Zeile ist nun weitaus komplexer als das was zuvor dort stand. Macht aber nur relativ simple Dinge. Wir suchen uns unseren Neuen Layer für das Projektil indem wir den Layer des feuernden GameObjektes in den Layernamen umwandeln und das Wort "Projektile " davor stellen. Diesen Namen ("Projektile TeamA" oder "Projektile TeamB") wandeln wir wieder in einen Layer um (Layer sind nur Zahlen von 0 bis 31). Damit kollidiert aber wieder alles mit jedem und muss daher angepasst werden. Öffnet also den Physik editor unter "Edit"->"Project Settings"->"Physics". Ihr solltet nun eine Matrix aller möglichen Layer sehen. Diese Matrix zeigt an wer mit wem kollidieren darf. Wir müssen nun einstellen das Projektile egal welcher Art nicht miteinander Kollidieren dürfen. genauso wie Teams jeder Art. Was aber kollidieren darf ist "TeamA" mit "Projektile TeamB" und "TeamB" mit "Projektile TeamA". Stellt dies in der Matrix ein (eine angehakte Checkbox bedeutet: Kollision ist möglich). Nun haben wir nur noch den Effekt dass uns gegnerische Geschosse stark aus der Bahn werfen. genauso wie es mit unseren geschossen auf den Gegner der Fall ist. Wir können das anpassen indem wir den "Mass" Wert der Rigidbodies der Projektile auf 0 setzen (der Wert wird auf ein Minimum von 1e-07 gesetzt was ein ziemlich kleiner Wert ist). Die Geschossen werden uns nun kaum noch beeinflussen. Wenn wir also Geschosse haben wollen die und böse wegdrücken, brauchen wir nur deren "Mass" Wert wieder erhöhen. Unitypackage: https://dl.dropboxus...t8.unitypackage Webplayer: https://dl.dropboxus...art8/Part8.html Teil 9: http://forum.unity-community.de/topic/7164-0follow-this-topic-wir-bauen-uns-ein-space-shoot-em-up-teil-9-strahlenwaffen/
  2. Top Stelle im Stellenmarkt. Schade dass es soweit weg liegt.

    1. Vorherige Kommentare anzeigen  %s mehr
    2. Sleepy

      Sleepy

      Ich finde das auch oft ärgerlich man liest bezahlte Stelle hier im Forum und dann auf erfolgs Basis. Am besten sind dann die wo weder Programmieren können noch von grafiken ne Ahnung haben. Wie will man so ein Spiel entwickeln ?

       

    3. Mark

      Mark

      Im Jobposting stand btw nichts davon dass man bis zum Erfolg kostenlos dort arbeitet.

    4. AgentCodeMonk

      AgentCodeMonk

      Mit "Aussicht auf Beteiligung"...bedeutet bei nem Startup etwa das Gleiche ;) den Spass hab ich auch durch... exakt so - und bekommen haben wir Studenten, die das Risiko eingehen konnten. Will´s ja garnicht schlechtreden... aber das Risiko ist nicht grade gering.

  3. Meh der Mangel an Feedback ist ja nervig.

    1. Vorherige Kommentare anzeigen  %s mehr
    2. Mr 3d

      Mr 3d

      hmm, irgendwie ist mein Kommentar abgeschnitten (zumindest bei mir)

      Da steht noch, dass du bitte nicht mitten drin aufhören sollst ;)

       

      @AgendCodeMonkey:

      Das habe ich mir auch schon gedacht :)

      und währe auch wirklich logisch ;)

    3. MadLion

      MadLion

      Das ist doch immer so... ist alles gut hört man nix vom Kunden aber stimmt auch nur eine Kleinigkeit nicht bricht gleich die Hölle auf Erden aus.

    4. Mark

      Mark

      Klar, ist nun auch kein Beinbruch, aber mich wundert schon das es keine Nachfragen gibt oder evtl Anreize, bzw Wünsche wie es weitergeht. Dafür bin ich ja auch offen.

  4. Nun wenden wir uns der Künstlichen Intelligenz zu. Wir löschen als erstes unsere Sphere und erstellen uns statt dessen eine weitere Cube die wir einfach nur "Gegner" nennen. Damit wir besser sehen können wohin diese schaut wenn wir sie dazu gebracht haben uns zu verfolgen. Unsere geplante KI ist recht simpel, sie soll uns folgen und uns wenn möglich immer im Blick halten. Wenn sie zu nah an uns herankommt wechselt sie in einen Fluchtmodus der eine kleine Weile anhält und soll uns dann wieder anfliegen. Wenn möglich soll sie auf uns feuern. Wir bestücken unseren Gegner noch mit einigen Komponenten: Rigidbody, Destructable, Drive und Weapon. Und konfigurieren diese soweit wie möglich. Nun erstellen wir uns ein neues C# Script und nennen es "AIController". Erstellen wir uns einige Variablen die angeben wie wir unsere Ziele für die KI finden können und auch speichern, außerdem noch angeben wo unsere Waffe und unser Antrieb ist. public string EnemyTag = "TeamA"; public GameObject Target; private Drive drive; private Weapon weapon; Wie beim PlayerController können wir drive und weapon in der Start Methode befüllen. Ein Tag ist eine Markierung auf einem GameObjekt, wir können Markierungen setzen indem wir zuerst ein Tag erstellen und dann einem GameObjekt zuweisen. Zuerst erstellen wir uns den Tag indem wir im Menü "Edit"->"Project Settings"->"Tags and Layers". Dort klappen wir den "Tags" Bereich aus und geben die nötige Anzahl der Elemente an, wir benötigen 2 Elemente. Anschließend können wir die freien Bereiche auffüllen. Wir machen dies indem wir "TeamA" und "TeamB" angeben. Anschließend weisen wir diese Tags unserem Spieler Raumschiff und dem Gegner zu. Wir selektieren also das Spieler Raumschiff und setzen im Inspektor den Tag (ganz oben) auf "TeamA". Beim Gegner setzen wir "TeamB". Nun fügen wir in unserem Script (welches wir auch gleich dem Gegner zuweisen können) folgende Zeile bei den usings ein: using System.Linq; Linq bietet uns Methoden an die das Arbeiten mit Mengen von Objekten erleichtert. Zum Beispiel wird das sortieren vereinfacht oder das auswählen aus einer Gruppe von Objekten. Fügt nun eine neue Methode hinzu, welche uns ein valides Ziel zum jagen sucht. void SearchTarget() { var enemies = GameObject.FindGameObjectsWithTag(EnemyTag); var enemiesSortedByDistance = enemies.OrderBy(enemy => Vector3.Distance(transform.position, enemy.transform.position)); Target = enemiesSortedByDistance.FirstOrDefault(); } In der ersten Zeile der Methode suchen wir uns alle GameObjekte mit unserem Ziel Tag, also alle GameObjekte die uns als Feinde unterkommen. In der zweiten Methode benutzen wir unsere erste Linq Methode welche unsere gefundenen Gegner je nach Enfernung zu uns sortiert. Dabei kommt uns eine sehr neue schreibweise entgegen: enemy => Vector3.Distance(transform.position, enemy.transform.position) Dies stellt einen Lambda Ausdruck da. Lambdas werden in anonyme Methoden umgewandelt, alles links vom => wird als Parameter für diese Methode gewertet und alles rechts von dem => Symbol wird als Methodenrumpfe definiert. Wir können dabei auch geschweifte Klammern verwenden, und bei den Parametern runde Klammern, wie zB hier: (enemy) => { return Vector3.Distance(transform.position, enemy.transform.position); ] Da wir aber nur einen einzigen Parameter benötigen lassen wir die runden Klammern weg und da wir nur eine Anweisung im Methodenrumpf benötigen können wir die geschweiften Klammern und das return weglassen. Der C# Compiler (die Einheit welche aus den für Menschen lesbaren Text eine für Maschinen lesbare Variante erstellt) wird daraus so etwas in der Art erzeugen: float AnonymeMethode(GameObject enemy) { return Vector3.Distance(transform.position, enemy.transform.position); } Wir können innerhalb der lambda Ausdrücke auch alle definierten variablen verwenden, selbst lokale Variablen. Der C# Compiler baut daraus etwas was funktioniert und kaum Mehraufwand von unserer Seite benötigt. Die SearchTarget Methode wird uns das nächste zur Verfügung stehende Ziel liefern. Wir werden als nächsten Schritt Variablen einbauen die unsere KI ein wenig konfigurieren und angeben was unsere KI machen soll. public float FollowRange = 2.0f; public float MinFollowTime = 5.0f; public float FleeTime = 5.0f; public float FireThreshold = 10.0f; private bool isFollowing = true; private bool canFlee = true; FollowRange benutzen wir um festzulegen ab welcher Entfernung unsere KI abdrehen soll, aber nur dann wenn die KI dies auch darf, dies besagt die canFlee Variable. MinFollowTime gibt an wie lang die KI mindestens das Ziel verfolgen soll bevor es fliehen darf. FleeTime gibt die Zeit in Sekunden an die unsere KI fliehen oder abdrehen darf bevor sie wieder angreift. FireThreshold gibt einen Wert zwischen -1 und +1 an und besagt wann unsere KI feuern darf, ein Wert von 1 bedeutet dass wir nur feuern wenn unser Ziel exakt vor uns ist. isFollowing gibt an ob unsere KI flieht oder angreift/folgt. Wir fangen damit an indem uns eine Methode schreiben die unsere KI veranlasst sich in eine Richtung zu drehen: private void RotateToDirection(Vector3 targetDirection) { if (!drive) { return; } var cross = Vector3.Cross(transform.forward, targetDirection); if (cross.y > 0) { drive.RotateRight(1.0f); } else { drive.RotateLeft(1.0f); } } Diese Methode sucht heraus ob wir uns nach links oder nach rechts drehen müssen indem wir das Kreuzprodukt zwischen unserer Zielrichtung und unserer aktuellen Richtung bilden. Je nach Wert veranlassen wir unsere Drive Komponente uns nach links oder rechts zu drehen. Wir verwenden diese Methode später. Vorerst bauen wir uns die Follow Methode die uns hilft unser Ziel zu verfolgen und wenn nötig uns zum fliehen zu bewegt. private void Follow() { if (canFlee && Vector3.Distance(transform.position, Target.transform.position) <= FollowRange) { StartCoroutine(StartFlee()); return; } if (drive) { drive.Accelerate(1.0f); } var targetDirection = (Target.transform.position - transform.position).normalized; RotateToDirection(targetDirection); } Zu aller erst schauen wir ob wir fliehen dürfen (canFlee) und fliehen müssen (der Distanz Check). Wenn dies so ist, dann starten wir eine Coroutine die wir nachher implementieren werden. Ansonsten geben wir unserer Drive Komponente Anlass zu beschleunigen und drehen uns in die Richtung die uns zum Ziel führt. Wenn wir die KI schlauer machen wollen können wir hier eine Richtung berechnen die uns auf Kollisionskurs mit unserem Ziel bringt. Dies könnte man benutzen um diesen Vorhaltepunkt für das abfeuern unserer Waffen zu verwenden. Wir führen aber erst einmal die StartFlee Methode ein die unser GameObjekt zum fliehen bewegt. private IEnumerator StartFlee() { canFlee = false; isFollowing = false; yield return new WaitForSeconds(FleeTime); isFollowing = true; yield return new WaitForSeconds(MinFollowTime); canFlee = true; } Neu ist hier nicht viel, außer dass unsere Coroutine einiges mehr macht als unsere bisherigen Coroutinen. Als erstes setzen wir unseren Status auf "wir fliehen" (isFollowing = false) und verhindern dass wir später wieder fliehen können. Wir fliehen nun bis die Zeit erreicht ist die durch FleeTime gegeben ist und wechseln unseren Status wieder auf angreifen/verfolgen (isFollowing = true). Anschließend warten wir wieder bis unsere Mindest verfolgzeit erreicht ist um uns wieder zu erlauben fliehen zu können (canFlee = true). Nun sollten wir noch das Fliehen einbauen: private void Flee() { if (drive) { drive.Accelerate(1.0f); } var targetDirection = -(Target.transform.position - transform.position).normalized; RotateToDirection(targetDirection); } Diese Methode sollte mit unserem bisherigen Wissen selbsterklärend sein. Wir berechnen eine Fluchtrichtung und benutzen die RotateToDirection Methode um uns dort hin zu drehen. Außerdem beschleunigen wir wieder wenn wir einen Antrieb besitzen. Wir fügen nun das bisherige zusammen um schon mal testen zu können wie sich unsere KI verhält was das Folgen und Abdrehen angeht. Dazu erweitern wir die Update Methode: if (Target == null) { SearchTarget(); } if (!Target) { return; } if (isFollowing) { Follow(); } else { Flee(); } Wir suchen uns zuerst ein neues Ziel wenn wir noch keins haben. Finden wir kein Zeil so brechen wir alles weitere ab. Ansonsten folgen wir dem Ziel oder fliehen vor ihm, je nach aktuellen Status (isFollowing). Wir können nun Unity starten und schauen was passiert. Wir können nun vermutlich ein oder mehrere der folgenden Punkte erleben: Unser Gegner stürzt ab. In dem Fall sollten wir "Use Gravity" beim Rigidbody deaktivieren. Unser Gegner fliegt als wäre er auf Eis. Hier sollten wir "Drag" und "Angular Drag" des Rigidbodies von unserem Spieler Raumschiff übernehmen. Unser Gegner flieht sehr schnell. Wir sollten die "MaxForce" vom Gegner Drive begrenzen, ein Wert von 500 oder weniger ist hierbei ausreichend. Wenn alles soweit Ok ist können wir uns ans feuern wenden. Fügt nun folgende Methode in unser Script ein: private void TryFire() { if (!weapon) { return; } var targetDirection = (Target.transform.position - transform.position).normalized; if (Vector3.Angle(targetDirection, transform.forward) <= FireThreshold) { weapon.StartFiring(); } else { weapon.StopFiring(); } } Nur wenn wir auch eine Waffe haben, werden wir in dieser Methode auch etwas machen. Mithilfe der Vector3.Angle Methode können wir uns einen Winkel zwischen Ziel und Blickrichtung berechnen. Der Winkel befindet sich in einer Weite von 0 bis 180 Grad. Wenn unser Winkel schmal genug ist (kleiner gleich FireThreshold) eröffnen wir das Feuer. Ansonsten stoppen wir es. Fügt den Aufruf dieser Methode ans Ende der Update Methode ein. Unser Gegner sollte nun das Feuer auf uns eröffnen. Wenn euch der Gegner zu aggressiv erscheint so könnt ihr die "Delay Per Shoot" Variable erhöhen, zB auf 1 (eine Sekunde). Ein Problem gibt es aber noch: Wenn wir getroffen werden oder wir den Gegner treffen driftet er ab und kann womöglich nicht mehr getroffen werden. Dies liegt daran dass die Kollisionen der Projektile das getroffene Objekt drehen können. Wir sollten dies daher unterbinden. Stellt sowohl bei dem Gegner als auch beim Spieler Raumschiff im Rigidbody folgendes ein: Öffnet die "Constraints" des Rigidbodies und checkt bei "Freeze Position" die Y Achse an und bei "Freeze Rotation" die X und Z Achse. Damit kann die Y Achse nicht durch die Physik verändert werden wodurch unsere GameObjekte immer auf der selben Ebene bleiben. Auch kann die Rotation nur an der Y Achse verändert werden. Spielt ein wenig mit den Werten herum, versucht zB auch ein eigenes Projektik Prefab für den Gegner zu basteln. Auch ist es möglich dass ihr die Weapon oder Drive Komponente weg lasst um entweder Kamikaze Piloten zu generieren oder stationäre Geschütze. Der Gegner kann euch nun jagen und beschießen. Um alles besser erkennen zu können solltet ihr noch die Distance des CameraControllers an der Main Camera auf 20 erhöhen. Unitypackage: https://dl.dropboxus...t7.unitypackage Webplayer: https://dl.dropboxus...art7/Part7.html Teil 8: http://forum.unity-community.de/topic/7147-wir-bauen-uns-ein-space-shoot-em-up-teil-8-kollisionen-kamera/
    1. QuesterDesura

      QuesterDesura

      webgl ist aber nur opengl 2.0 und da ist die Shader Sprache gerade ma 1.0 also sehr veraltet

    2. Mark

      Mark

      OpenGL ES 2.0, auch nicht das neuste aber dennoch genau das was die meisten mobile Geräte können, Womit es damit gut vergleichbar ist.

  5. Das klingt so als ob du sowohl auf der kugel als auch auf dem Raumschiff ein Script hast welches sich nach einer gewissen Zeitspanne zerstört. Das macht ja eigentlich nur das Projektil Script, schau mal ob das Script auf den GameObjects liegt. Das sollte eigentlich nur auf das Projektik Prefab. Ansonsten schau mal welche Scripte auf der Sphere liegen, eigentlich sollte das nur das Destructable Script sein.
  6. Heute wenden wir uns ein wenig der Optik zu, aktuell haben wir einfache Primitiven (Würfel und Kugeln). Diese machen optisch absolut nichts her und mittlerweile haben wir uns besseres verdient. Ausserdem brauchten wir unsere Kugel bisher als Wegbarke, aber da diese nun zerstört werden kann und wir später auch Gegner und mehr haben brauchen wir noch etwas anderes, etwas für den Hintergrund. Wir starten damit dass wir unseren blauen Hintergrund etwas weltraumiger gestalten indem wir zuerst die Farbe abdunkeln und ein paar Sterne hinzufügen. Wir selektieren also als erstes unsere "Main Camera" im Inspektor sehen wir eine Camera Komponente und dort das Feld mit dem Text "Background Color" klickt auf die daneben angezeigte Farbe und ändert diese auf eine sehr dunkle blaue Farbe oder was auch immer am besten gefällt. Damit haben wir die Grundhintergrundfarbe festgelegt, was schon etwas ausmacht im vergleich zu zuvor. Nun fügen wir ein simples Licht hinzu um die flache Optik unserer Objekte loszuwerden. Geht auf "GameObject"->"Create Other"->"Directional Light". Ändert nun die Rotation dieses GameObjektes auf: X = 30, Y = 100 und Z = 60 Damit haben wir das Direktionale Licht so gedreht das wir eine ansprechende Beleuchtung haben. Wer Lust hat kann nun ein wenig an der Light Komponente herumspielen, interessant dürften die Lichtfarbe und Intensität sein. Damit haben wir schon 2 wichtige Punkte abgehakt die maßgeblich zur Optik beitragen. Nun fügen wir noch einen Sternenhintergrund hinzu der sich je nach Position der Kamera verändert. Es gibt viele Wege dies zu bewerkstelligen. Wir nehmen dafür einfach ein Partikelsystem welches uns die Sterne zeichnet und eine neue Komponente die die Position des Partikelsystems auf eine Ebene unter dem Raumschiff projeziert. Wir erstellen uns als erstes ein geeignetes Partikelsystem. Erstellt dazu ein neues GameObjekt mit "GameObject"->"Create Other"->"Particle System", nennt das neu erstellte GameObject in "Sterne" um. Ihr solltet nun einige Partikel sehen die empor steigen. Wir wollen aber keine sich selbst bewegenden Sterne und daher ändern wir im Inspektor unter "Particle System" die Start Speed auf 0, damit fliegen unsere Partikel nicht mehr durch die Gegend, statt dessen sehen wir eine Punktwolke. Diese Wolke sollte sich aber etwas weiter ausbreiten, also klicken wir auf "Shape" welches sich dadurch ausklappen sollte (nicht die Checkbox entfernen) und ändern die "Shape" von Cone auf eine Box um. Um die Box besser zu definieren ändern wir noch die Werte der Box auf X = 100, Y = 100 und Z = 10. Nun sehen wir nur noch sehr wenige Sterne da der Raum so riesig geworden ist (wir könnten hier auch kleinere Werte verwenden, aber es schadet auch nicht wenn die Werte so groß sind). Um mehr Sterne zu bekommen setzen wir noch die "Start Lifetime" auf 10 damit wir nicht so viele Sterne aufflackern und verschwinden sehen. Nun müssen wir noch den "Emission" Bereich wie zuvor den "Shape" Bereich ausklappen und dort den "Rate" Wert auf 300 setzen. Dies beeinflusst wieviele Partikel pro Sekunde erzeugt werden sollen. Ausserdem brauchen wir spielraum für die maximale Anzahl der Partikel. ändert daher den Wert von "Max Particles" auf 10000. Da die Partikel noch sehr groß sind müssen wir auch diese anpassen und setzen die Start Size auf einen Wert zwischen 0.2 und 0.3. Damit die Sterne wenn wir das Spiel starten schon vorhanden sind müssen wir noch die Checkbox bei "Prewarm" setzen, dies sorgt dafür dass das Partikel System schon alle Partikel erstellt bevor sie angezeigt werden. Ein kleines aber wichtiges Detail. Nun tauchen die Partikel aber einfach so auf und verschwinden auch einfach so wieder, ohne sanft ein und auszublenden. Auch dies können wir anpassen indem wir die "Color over Lifetime" Kategorie ausklappen und dort die Checkbox aktivieren. Der daraufhin aktivierte Colorwert kann nun dazu benutzt werden einen Farbverlauf zu definieren. Wir klicken also auf die Farbe und erstellen uns einen Farbverlauf. Im oberen Bereich des Farbverlaufeditors können wir die Transparenz (Alpha) festlegen pro Stufe des Verlaufes, wir klicken nun also bei 10% und bei 90% des großen Balkens und stellen dort den Alpha Wert auf 255, die Werte ganz links (0%) und ganz rechts (100%) des Farbverlaufes setzen wir auf 0 Alpha. Damit blenden unsere Partikel sanft ein, bleiben eine weile bestehen und blenden dann wieder sanft aus. Wenn wir jetzt die Szene starten sollten wir schon ein viel bessers Flugerlebniss haben, aber wenn wir zu weit fliegen verschwinden die Sterne auf einmal, da dort der Bereich des Partikel Systems zu Ende ist. Um dies zu ändern erstellen wir uns ein neues C# Script mit dem Namen "ProjectToPlane". Wir fügen nun eine Variable hinzu: public GameObject ObjectToProject; Dies dient dazu um festzulegen welches Objekt auf die Ebene projeziert werden soll. In der Update Methode schreiben wir nun folgendes: var plane = new Plane(Vector3.up, 0); float distance; if (plane.Raycast(new Ray(transform.position, transform.forward), out distance)) { ObjectToProject.transform.position = transform.position + transform.forward * distance; } Die erste Zeile definiert unsere Ebene, wir benutzen einfach die Ebene die nach oben zeigt und durch den 0 Punkte geht. Die nächste Zeile definiert eine lokale float Variable die wir hier noch nicht zuweisen. Dies erledigt die nächste Zeile in der if Abfrage. Hier prüfen wir ob der Strahl (Ray) der durch unsere Position (transform.position) und Richtung (transform.forward) definiert wird, die Ebene schneidet. Wenn die Ebene geschnitten wird wird so hat unsere float Variable die Distanz die der Strahl gewandert ist um die Ebene zu treffen. Das out Keyword gibt dabei an dass unsere float Variable als Referenzweret behandelt wird und damit von der Methode die wir aufrufen verändert werden kann. Dadurch kann uns die Methode die Distanz direkt in diese Variable legen und gleichzeitig einen bool Rückgabewert haben. Diesen bool Rückgabewert verwenden wir um die if Abfrage zu steuern. Wenn diese if Abfrage erfolgreich ist so setzen wir unser ObjectToProject auf den Schnittpunkt den wir berechnen indem wir unseren Ursprungspunkt mit der Richtung multipliziert mit der Distanz addieren. Wir legen dieses Script auf unsere Main Camera und wählen als ObjectToProject Wert unser Sternen Partikelsystem aus. Anschließend selektieren wir nochmal unser Partikel System und ändert dort den "Simulation Space" von "Local" auf "World". Dies sorgt dafür dass sich die Sterne nicht automatisch mitbewegen wenn unser neu erstelltes Script die Position des Partikelsystems ändert. Das war auch schon der schwierigste Teil dieses Tutorials. Aber fertig mit dem aufhübschen sind wir noch nicht. Es wäre toll wenn unsere Projektile einen Schweif hinter sich herziehen könnte wenn sie fliegen. Wählt das Projektil Prefab aus und fügt folgende Komponente hinzu: "Effects"->"Trail Renderer". Ändert bei dieser Komponente "Time" auf 0.3, dies gibt an wie lange der Schweif sichtbar sein darf, 0.3 Sekunden reicht aus um es bei höheren Geschwindigkeiten zu sehen ohne das es zu penetrant wirkt. Ändert ausserdem "End Width" auf 0, dies gibt an dass der Schweif zum Ende hin kleiner wird. Passt noch den "Start Width" Wert auf 0.5 oder kleiner an damit der Schweif auch zu der Größe des Projektils passt. Wenn wir die Szene nun starten sehen wir einen pinken langweiligen Schweif bei jedem gefeuerten Schuss. Dies können wir ändern indem wir dem Trail Renderer ein Material zuweisen. Materialien werden benutzt um das Aussehen von Objekte zu beeinflussen. hier können wir uns eine Textur aussuchen und auch angeben wie etwas gezeichnet werden solle (Shader). Wir sollten uns daher ein neues Material erstellen. Klickt mit der rechten Maustaste in den Asset Bereich und wählt Material. Nennt das neu erzeugte Material "Schweif" und weist es auch gleich dem Trail Renderer zu. Wählt nun wieder das Material aus und ändert die Textur des Materials indem ihr im inspektor auf Select im großen grauen Rechteck klickt. Wählt dort die bereits vorhandene Default-Particle Textur aus. Zusätzlich solltet ihr den verwendeten Shader ändern und zwar auf: "Particles"->"Additive". Dieser Shader sorgt dafür dass der Schweif schön hell erscheint. Ändert wenn ihr wollt die "Tint Color" des Materials um den Schweif eine andere Farbe zu verpassen. Zum Beispiel ein helles Blau. Damit wäre der Teil des Tutorials erledigt. Wer möchte kann versuchen bei einem Treffer des Projektils ein anderes PartikelSystem anzuzeigen, wie das geht ist recht einfach, erstellt einfach an der Stelle des Projektils wenn es etwas getroffen hat ein weiteres PartikelSystem. Ihr könnt das leicht mithilfe eines weiteren Prefabs lösen. Dazu könnt ihr die Partikel Klasse erweitern um dieses Prefab als Aufschlagseffekt anzugeben um es im Code per Instantiate zu spawnen. Im angehangenen Unitypackage habe ich dies gemacht, inklusive eines kleinen Scriptes welches automatisch die Effekte zerstört wenn sie zu ende abgespielt wurden. Particle Systeme übernehmen dies leider nicht automatisch. Unitypackage: https://dl.dropboxus...t6.unitypackage Webplayer: https://dl.dropboxus...art6/Part6.html Teil 7: http://forum.unity-c...m-up-teil-7-ki/
  7. Bestehende Gewohnheiten ausnutzen ist nicht so verkehrt aus einem Businessstandpunkt aus betrachtet. Die Leute wollen ähnliches haben und damit wirds bedient. Ansonsten wenns grade im Hype ist wird man auch oft dazu verleitet. Schau dir die vielen VoxelSpiele an. Mich würden die DL Zahlen interessieren wenn UnityWolle diese herausgeben kann/will.
  8. Naja ein simpler Methodenaufruf vs ein Objekt was in der Lage ist Methoden zu sammeln um sie dann aufzurufen ist nicht wirklich vergleichbar was die Performance angeht. Da gewinnt ein natürlicher Methodenaufruf immer. Wieso ich das nicht so gemacht habe wie du vorgeschlagen hast ist ganz einfach. Ich wollte die Schadensberechnung ja von anderen Komponenten abhängig machen, zb eine Schutzschildkomponente oder eine Komponente welche eine Panzerung simuliert. Dadurch bin ich frei wer was berechnet und kann alles schön modulkar zusammen stecken. Hier mal ein Schutzschild als Beispiel: using UnityEngine; using System.Collections; public class Shild : MonoBehaviour { public int MaxShild = 20; public float CurrentShild = 20; public float ChildRegenerationRate = 1.0f; private Destructable destructable; void Start() { destructable = GetComponent<Destructable>(); destructable.OnPreDamage += OnPreDamage; } void Update() { CurrentShild += Time.deltaTime * ChildRegenerationRate; CurrentShild = Mathf.Min(CurrentShild, MaxShild); } void OnPreDamage(Damage damage) { var savedDamage = Mathf.Min(damage.Amount, CurrentShild); CurrentShild -= savedDamage; damage.Amount -= savedDamage; } } Die OnPreDamage (die Methode kann auch anders heißen) Methode wird aufgerufen bevor der Schaden angerichtet wird. Die Methode kann daher den Schaden prima abwenden sofern das Schutzschild noch genug Energie besitzt. Das Schutzschild ist komplett optional. Durch das von Unity gelieferte Komponentensystem kann ich es also weglassen oder einfach hinzufügen und der Rest funktioniert weiterhin.
  9. Heute beschäftigen wir uns mit dem Beschädigen von Objekten. Da wir bereits das Feuern von Waffen haben ist dies wohl der nächste logische Schritt. Als Testobjekt kommt dafür unsere Sphere zum einsatz die bisher nichts weiter macht ausser einen Positionsanhaltspunkt zu liefern. Zuerst einmal klären wir was genau wir alles haben wollen: - Objekte sollen eine gewisse Anzahl an Lebenspunkten haben - Wenn diese Lebenspunkte kleiner oder gleich 0 erreichen soll das Objekt zerstört werden - Beschädigungen sollen mitgeteilt werden können Legen wir damit los indem wir den ersten Punkt erledigen. Erstellt ein neues C# Script genannt "Destructable" (Zerstörbar) Wir definieren wie immer zuerst unsere Variablen: public int MaxHealthPoints = 100; public float CurrentHealthPoints = 100; public System.Action<Damage> OnPreDamage; Die ersten 2 Variablen sind recht unauffällig und sollten sofort Sinn ergeben. Die letzte Variable dagegen sieht seltsam aus. Was ist "System.Action" und was ist der generische Parameter "Damage"? System ist wie "UnityEngine" und "System.Collections" ein Namensraum der uns Methoden und Klassen bietet. Wir könnten using System; an den Anfang der Datei schreiben. Dies könnte aber mit einigen Sachen kollidieren die in "UnityEngine" definiert wurden. Daher schreiben wir es vor Action, was quasi ein temporäres using System; für Action darstellt. Action ist ein Typ welcher eine Methode repräsentiert, wir können damit Methoden an Variablen übergeben. Der generische Typ den wir optional angeben können gibt an welche Parameter diese Methode besitzen soll. Wenn wir eine Methode an OnPreDamage übergeben wollen dann muss sie wie folgt aussehen: void NameDerMethode(Damage nameDesParameters) { ... } Zuweisen können wir ihr einen Wert wie folgt: OnPreDamage += NameDerMethode; Nun fehlt noch der Damage Typ, den haben wir bisher noch nicht erstellt, daher sollten wir diesen schnell erstellen. Erstellt ein neues C# Script genannt "Damage" und ersetzt die dort enthaltende Klasse mit folgendem Code: public class Damage { public Destructable Target; public float Amount; public GameObject Instigator; public Damage(Destructable target, float amount, GameObject instigator) { Target = target; Amount = amount; Instigator = instigator; } } Auch hier gab es eine Neuerung. Die Klasse hat eine Methode ohne Rückgabetypen mit dem gleichen Namen wie die Klasse selbst. Dies wird Construktor genannt. Ein Constructor wird immer dann aufgerufen wenn wir eine Instanz der Klasse per new erzeugen. Wir benutzen den Construktor um die Variablen der Klasse zuzuweisen. Auch leitet die Klasse von nichts ab, ist also kein MonoBehaviour. Fügt nun die erste Methode in die Destructable Klasse hinzu: public void ApplyDamage(float damage, GameObject instigator) { var damageArguments = new Damage(this, damage, instigator); if (OnPreDamage != null) { OnPreDamage(damageArguments); } CurrentHealthPoints -= damageArguments.Amount; if (CurrentHealthPoints <= 0) { DestroyObject(gameObject); } } Wir erzeugen hier eine Instanz der soeben erstellten Damage Klasse und führen die Methoden (ja es können mehrere Methoden in OnPreDamage angelegt werden) aus die in OnPreDamage gespeichert werden, wenn dort welche gespeichert wurden (die if Abfrage). das this Keyword bedeutet dass dieInstanz der Klasse verwendet werden soll in welcher die aktuelle Methode steht. Also unser Destructable. Anschließend verringern wir unsere Lebenspunkte und schauen ob diese kleiner oder gleich 0 sind. Wenn ja dann zerstören wir das aktuelle GameObjekt auf dem das Script liegt. Wieso nun haben wir die OnPreDamage benutzt und erstellt? Ganz einfach: Wenn wir uns ein Schutzschild oder eine Panzerung für die raumschiffe erstellen wollen, können wir innerhalb dieser Scripte die OnPreDamage Variable des Destructables verwenden um uns vor dem Moment des Schaden nehmens einzulinken und Schaden abwenden oder verstärken. Ausserdem können wir so erfahren ob wir getroffen wurden, ausserdem von wem wir getroffen wurden und so weiter. Was recht nützlich für eine spätere KI ist. Es wird Zeit das Destructable Script zu verwenden, daher schiebt dieses Script sowohl auf das Spieler Raumschiff als auch auf die Sphere. Noch macht nichts was wir haben Schaden, nicht einmal unser Projektil. Öffnet das Projektil Script und fügt folgende Variable hinzu: public int Damage = 10; public GameObject Owner; Wofür die Damage Variable steht sollte klar sein. Die Owner Variable erfordert aber etwas Erklärung. Da wir später die Information benutzen wollen wer uns geschädigt hat benötigen wir eine Variable die den Besitzer des geschosses definiert. Dies macht die Owner Variable, welche wir nach dem erstellen eines Geschosses noch befüllen müssen. Dazu aber später. Nun müssen wir die OnCollisionEnter Methode erweitern. Fügt also folgendes hinzu: var destructable = collision.gameObject.GetComponent<Destructable>(); if (destructable) { destructable.ApplyDamage(Damage, Owner); } Wir suchen uns als erstes an dem Objekt mit dem wir kollidiert sind die Destructable Komponente. Wenn wir eine gefunden haben werden wir ihr Schaden zufügen. Recht simpel soweit. Nun müssen wir noch die ProjectileWeapon Klasse etwas erweitern. Da diese nur eine Methode besitzt (Fire) erweitern wir also diese. Fügt folgendes ans Ende der Methode: var projectileComponent = projectile.GetComponent<Projectile>(); if (projectileComponent) { projectileComponent.Owner = gameObject; } Wir suchen uns hiermit an dem neu erzeugten GameObject die Projektil Komponente, da diese Komponente nicht zwangsläufig existieren muss gibt es eine if Abfrage dazu. Anschließend weisen wir der Komponente unser GameObjekt als Besitzer hinzu. Wenn ihr jetzt die Szene in Unity startet könnt ihr auf die Sphere feuern welche daraufhin nach einigen treffern verschwinden sollte. Sie wurde zerstört. Unitypackage: https://dl.dropboxusercontent.com/u/2446365/ShootEmUpTutorial/Part5.unitypackage Teil 6: http://forum.unity-c...p-teil-6-optik/
  10. Die Skybox ändert aber nur das gerenderte Bild wenn man die Kamera dreht, das machen wir ja noch nicht und bei Bewegungen hilft es uns auch nicht weiter.
  11. Ich würde in meinem nächsten Teil meiner Tutorial Serie entweder das Zerstören/Schädigen/HP Verwaltung von Objekten einbauen oder etwas für die Optik machen. Was würdet ihr empfehlen? Die Optik Sache würde dabei nichtmal viele Assets nutzen sondern die BuildIn Möglichkeiten von Unity nutzen.

    1. Vorherige Kommentare anzeigen  %s mehr
    2. Aragom

      Aragom

      jupp shit auf optik.... lieber coden...

    3. AgentCodeMonk

      AgentCodeMonk

      ^^ so würde ich es jetzt nicht sagen^^ die optiksache fände ich zum schluss besser. polishen, quasi

    4. Mark

      Mark

      Nagut dann wirds eben aufs kaputt machen hinaus laufen. Dennoch sind ein paar Sterne im Hintergrund auf lange Sicht angenehmer zum Probespielen als die doofe olle Kugel die es da aktuell gibt.

  12. Die Reihe erfordert kaum bis keine C# spezifischen Kenntnisse, da ich hoffentlich alles erkläre
  13. Nachdem wir nun schon angefangen haben uns Komponenten zu basteln die wir an das Schiff bauen können um mehr Funktionalität zu erhalten führen wir das fort indem wir uns den Sachen zuwenden die uns dabei helfen andere Sachen kaputt zu machen: Waffen! Überlegen wir zuerst einmal was genau wir alles abdecken wollen mit unseren Waffen. Es gibt Waffen die Projektile abfeuern können und Waffen die sofort treffen sollen. Es gibt Waffen die stark streuen und welche die sehr genau schießen. Waffen sollen unterschiedlich schnell feuern können. Auch wäre es toll wenn wir Waffen haben die mehrere Projektile auf einmal feuern, etc etc. Vieles können wir zusammen fassen. Dadurch können wir uns einem Konzept der C# Sprache zuwenden (dieses Konzept existiert in vielen Sprachen), abstrakte Klassen. Abstrakte Klassen sind Klassen in denen fehlende Funktionen implementiert müssen bevor sie verwendet werden können. Dazu leiten wir von dieser Abstrakten Klasse ab und implementieren diese fehlenden Funktionen in unserer neuen Klasse. Wir werden in diesem Tutorial eine abstrakte Klasse genannt "Weapon" implementieren und eine abgeleitete Klasse genannt "ProjectileWeapon". Später können wir noch mehr Klassen von "Weapon" ableiten, zB die Waffen die keine Projektile besitzen und sofort treffen. Wir erzeuge uns also ein neues C# Script mit dem Namen "Weapon" und ändern public class Weapon : MonoBehaviour zu: public abstract class Weapon : MonoBehaviour abstract ist ein weiteres C# Keyword welches besagt dass die Klasse nicht direkt verwendet werden kann und erst abgeleitet werden muss. Damit legen wir klar fest, Weapon kann so nicht verwendet werden. Unsere Weapon Klasse soll dennoch ein wenig machen, zB regeln wann wir wieder feuern dürfen und einige Grundmethoden vorstellen. Fügt daher als erstes ein paar Variablen zur Klasse hinzu: public float ReloadTime = 0.0f; public int AmmoPerMagazin = 1; public float DelayPerShoot = 0.1f; public float SpreadAngle = 10; public int ShootsPerFire = 1; private int firedShootsPerFire = 0; private int currentAmmoInMagazin = 1; private bool shouldFire = false; private bool isFiring = false; private bool isReloading = false; private bool isCooling = false; Diese Regeln wie viele Schüsse wir feuern können bis wir nachladen müssen. Die Zeit die fürs Nachladen benötigt wird. Wie lange wir warten müssen bis ein neuer Schuss gefeuert werden kann. Wie sehr unsere Waffe streut und wie viele Schüsse wir noch im Magazin haben. shouldFire wird vom Benutzer verändert über 2 Methoden die wir gleiche implementieren werden. isFiring gibt an ob wir grade am feuern sind, wenn wir zB eine Waffe haben die immer 10 Schüsse abgeben soll, dann wird isFiring so lange true bleiben bis diese 10 Schüsse verschossen sind, egal was der Spieler möchte. isReloading gibt an ob die Waffe grade nachlädt und isCooling gibt an dass die Waffe einen Moment warten muss bevor ein weiterer Schuss gelöst wird. Um unsere Waffe benutzen zu können müssen wir die isFiring Variable je nachdem ob wir feuern wollen oder nicht verändern. Deswegen fügen wir 2 Methoden zur Klasse hinzu: public void StartFiring() { shouldFire = true; } public void StopFiring() { shouldFire = false; } Diese beiden Methoden machen nun wirklich nicht viel, was feuert denn nun? Eine weitere Methode, die aber so wie die Klasse selbst als abstract markiert wird, diese Methode muss später von ableitenden Klassen überschrieben und implementiert werden. protected abstract void Fire(); abstract haben wir ja bereits erklärt, neu ist allerdings das protected Keyword. Protected ist in der gleichen Kategorie wie private und public, es regelt also die Erreichbarkeit der Methode. protected hat die Eigenschaft dass die damit ausgeschmückte Methode nur von der eigenen Klasse (so wie private) UND abgeleiteten Klassen aufgerufen oder implementiert werden kann. Nun regeln wir wie das Waffensystem nun agieren soll. Es soll, wenn wir keine Munition mehr im Magazin haben, dieses neu laden und das feuern in dieser Zeitspanne verbieten. Gefeuert darf nicht jeden Frame, dies regeln wir durch die DelayPerShoot Variable, die genauso wie beim nachladen in der Zeitspanne verhindern soll dass die Waffe gefeuert wird. Wie implementieren uns einige kleine Methoden welche sowohl das Nachladen als auch das Verzögern regeln sollen, zuerst das nachladen: private void ReloadWhenNeeded() { if (currentAmmoInMagazin > 0 || isReloading) { return; } if (ReloadTime <= 0.0f) { currentAmmoInMagazin = AmmoPerMagazin; return; } isReloading = true; isFiring = false; StartCoroutine(Reload()); } IEnumerator Reload() { yield return new WaitForSeconds(ReloadTime); isReloading = false; currentAmmoInMagazin = AmmoPerMagazin; } Wow, was für ein Brocken, aber keine Angst, ich beschreibe die einzelnen neuen Aspekte. Die ReloadWhenNeeded Methode prüft am Anfang ob wir überhaupt nachladen müssen und ob wir grade nachladen. Wir testen im ersten if, genau 2 Bedingungen, wenn eine davon erfüllt ist, wird die Methode wieder verlassen. Wir können mehrere Bedingungen aneinander Reihen, wir haben dies zB mit dem || Symbolen getan, || bedeutet "oder", also wird diese if Bedingung erfüllt sein wenn entweder mehr als ein Schuss im Magazin ist, oder wir grade am nachladen sind. Die nächste if Abfrage schaut nach ob wir für diese Waffe überhaupt die Notwendigkeit haben nachladen zu müssen, wir lösen das indem wir einfach schauen wie lange das nachladen dauert, wenn es keine Dauer hat dann setzen wir die aktuelle Munition i Magazin aufs Maximum und beenden die Methode wieder. Wenn keine der if Bedingungen erfüllt wurde dann sagen wir dass wir nun am nachladen sind (isReloading), brechen das aktuelle feuern der Waffe ab und machen nun etwas besonderes: Wir starten unsere erste Coroutine. Coroutinen sind Methoden die über mehrere Frames hinweg arbeiten können. Unsere zweite Methode die Reload genannt wird und ein IEnumerator als Rückgabetyp besitzt ist diese Coroutine. in C# und Unity müssen Coroutinen diesen Typen zurückgeben. Durch das yield sagen wir an dass alles bis zu diesem yield ausgeführt werden soll und erst im nächsten Frame weiter gemacht werden soll. Durch yield return new WaitForSeconds(ReloadTime); besagen wir sogar dass die Methode erst weiter machen soll wenn ReloadTime Sekunden vergangen sind. Wenn diese Zeit vergangen ist sagen wir dass wir fertig sind mit dem Nachladen und füllen das Magazin wieder auf. Das Verzögern sieht ähnlich aus: public void Delay() { if (isCooling) { return; } isCooling = true; StartCoroutine(DelayFiring()); } IEnumerator DelayFiring() { yield return new WaitForSeconds(DelayPerShoot); isCooling = false; } Ist aber weitaus weniger komplex als das Nachladen. So Zeit das ganze zusammen zu führen, wir erweitern unsere Update Methode welche prüft ob wir feuern sollen und können, feuert wenn alles stimmt und nachlädt wenn nötig. if (shouldFire && !isFiring && !isCooling && !isReloading) { isFiring = true; firedShootsPerFire = 0; } if (isFiring && !isCooling && !isReloading) { firedShootsPerFire ++; currentAmmoInMagazin --; if (firedShootsPerFire == ShootsPerFire) { isFiring = false; } Fire(); Delay(); ReloadWhenNeeded(); } Im ersten if gibt es eine weitere Neuigkeit die && Symbole, welche und bedeuten, so wie das oder (||) wird das und benutzt um mehrere Bedingungen zu verknüpfen. Die Bedingung gilt dann nur als erfüllt wenn alle mit dem und verknüpften Unterbedingungen erfüllt sind. Das erste if sagt das wirklich gefeuert wird wenn der Spieler feuern will (shouldFire), wir nicht bereits am feuern sind (isFiring) wir nicht grade eine auf den nächsten Schuss warten (isCooling) und wir nicht grade am nachladen sind (isReloading). Das nächste if erledigt die Hauptarbeit wenn die Bedingung erfüllt ist. Erst wird der Zähler erhöht der festhält welchen Schuss in einer Reihe wir grade machen. Dazu werde die ++ Symbole verwendet. ++ ist eine Kurzform von += 1. Die Kurzform für -= 1 wäre das --. Wenn die Sequenz der Schüsse erledigt ist dann hören wir auf zu feuern bis zum nächsten Update Zyklus in dem erneut geprüft wird ob wir feuern sollen. Anschließend wird gefeurt indem wir unsere abstrackte Fire Methode aufrufen und das Nachladen geprüft sowie der Delay zwischen den Schüssen gestartet. Wir erweitern an dieser Stelle noch schnell unseren PlayerController indem wir wie beim Drive eine Weapon Komponente suchen: weapon = GetComponent<Weapon>(); Nicht vergessen die weapon Variable auch in den PlayerController einzuführen. Benutzung wäre diesmal in der Update Methode des PlayerControllers so: if (weapon) { if (Input.GetKeyDown(KeyCode.Space)) { weapon.StartFiring(); } if (Input.GetKeyUp(KeyCode.Space)) { weapon.StopFiring(); } } Neu ist hier dass wir anstatt Input.GetKey, Input.GetKeyDown und Inout.GetKeyUp verwenden. Im gegensatz zu GetKey wird GetKeyDown nur ein einziges mal true liefern und zwar in dem Frame in dem der Spieler die Taste herunter drückt. GetKeyUp wird ein einziges mal in dem Frame gefeuert in dem der Spieler die Taste wieder los lässt. Nun erstellen wir uns das ProjectileWeapon Script welches von der Weapon Klasse ableitet und die Fire Methode implementiert die dann ein Projektil verschießen soll. Erstellt also ein neues C# Script mit dem Namen ProjectileWeapon. Wenn ihr das Script öffnet sieht dieses noch so aus: using UnityEngine; using System.Collections; public class ProjectileWeapon : MonoBehaviour { // Use this for initialization void Start () { } // Update is called once per frame void Update () { } } Noch hat dieses Script nichts mit dem Weapon Script zu tun von dem es ableiten soll. Ändert daher den Namen "MonoBehaviour" nach den Doppelpunkt unseres Klassennamens zu "Weapon". Ausserdem könnt ihr die Start und Update Methoden entfernen. Wir benötigen diese hier nicht mehr. Das Script sollte nun so aussehen: using UnityEngine; using System.Collections; public class ProjectileWeapon : Weapon { } Um die ProjectileWeapon Klasse ohne Fehler benutzbar zu machen müssen wir noch die abstracte Fire Methode der Weapon Klasse überschreiben. Dies geht indem wir folgendes in die ProjectileWeapon Klasse schreiben: protected override void Fire() { } Das override Keyword gibt an dass wir eine abstrakte Methode implementieren wollen. Das override Keyword hat noch eine weitere Bedeutung auf die wir aber erst eingehen werden wenn es nötig wird. Wir wollen das ein Projektil erzeugt wird wenn wir Fire aufrufen, dafür schauen wir uns ein neues Konzept von Unity an. Die Prefabs. Prefabs steht für pre fabricated, was auf deutsch mit vorbereitetes Objekt gleichzusetzen ist. Wir werden uns nun ein Projektil, einen einfachen Schuss zusammen basteln der einfach nur gerade aus fliegt und nach einer gewissen Zeitspanne oder wenn es etwas getroffen hat, verschwindet. Anschließend wandeln wir dieses Projektil in ein Prefab um. Erstellt in Unity ein neues GameObjekt, eine weitere Cube reicht für den Anfang. Nennt dieses GameObjekt "Projektil" und ändert die Größe des Projektils indem ihr im Inspektor alle 3 Werte der Scale Eigenschaft der Transform Komponente auf 0.1 setzt. Um die Auswirkungen zu sehen könnt ihr das Projektil zur Seite schieben damit es nicht genau in eurem Spieler Raumschiff liegt. Fügt nun eine Rigidbody Komponente hinzu und deaktiviert wieder die "Use Gravity" Checkbox im Inspektor. Nun erstellen wir ein weiteres C# Script um das Projektil zu steuern. Wir nennen das Script "Projectile". Fügt in diesem Script nun eine float Variable hinzu welche angibt wie schnell dieses Projektil fliegen soll: public float StartVelocity = 20.0f; Wir verwenden diese Variable in der Start Methode des Scriptes um die velocity des Rigidbodies damit zu füllen: rigidbody.velocity += StartVelocity * transform.forward; Nachdem die Start Methode aufgerufen wurde wird sich unser Projektil mit konstanter Geschwindigkeit vorwärts bewegen. Für den Anfang reicht uns das. Nun muss noch eingebaut werden dass die Kugel automatisch verschwindet. Dazu fügen wir eine weitere Variable in das Script ein: public float Lifetime = 5.0f; Dieser Wert gibt an wieviele Sekunden unser Projekt lebt bevor es sich selbst zerstört. Fügt nun noch folgende Zeile in die Start Methode hinzu: DestroyObject(gameObject, Lifetime); DestroyObject zerstört ein beliebiges Unity Object, in unserem Fall wollen wir dass das unser GameObject selbst zerstört wird weswegen wir dort gameObject angeben. Wir wollen aber nicht dass unser Projektil sofort verschwindet sondern erst nach der angebebenen Zeitspanne und genau dazu dient der zweite Parameter. Speichert das Script und weist es unserem Projektil GameObject zu. Wenn ihr nun Unity startet seht ihr wie das Projektil wegfliegt und irgendwann aus der Hierarchy Ansicht verschwindet. Das Projektil soll aber auch verschwinden wenn es etwas trifft, also fügen wir nun noch eine weitere Methode hinzu die genau das übernimmt: void OnCollisionEnter(Collision collision) { DestroyObject(gameObject); } OnCollisionEnter wird dann aufgerufen wenn ein Objekt mit einem Rigidboy und Collider ein anderes Objekt mit Collider berührt. Das Script sollte nun so aussehen: using UnityEngine; using System.Collections; public class Projectile : MonoBehaviour { public float StartVelocity = 10.0f; public float Lifetime = 5.0f; void OnCollisionEnter(Collision collision) { DestroyObject(gameObject); } // Use this for initialization void Start () { rigidbody.velocity += StartVelocity * transform.forward; DestroyObject(gameObject, Lifetime); } // Update is called once per frame void Update () { } } Es wird Zeit unser Projektil GameObject in ein Prefab umzuwandeln. Erstellt daher zuerst ein leeres Prefab indem ihr im Asset Bereich "Create"->"Prefab" aufruft. Nennt dieses Prefab "Projektil". Noch ies es leer, ändert dies indem ihr euer Projektil GameObject auf das Projektil Prefab zieht. Ihr könnt das GameObjekt anschließend löschen. Klickt nun auf das Prefab und setzt die Rotation und Positions Werte der Transform Komponente auf 0. Das Prefab ist fertig, hässlich aber es funktioniert. Nur wie verwenden wir das Prefab und wozu genau dient es? Wie bereits erwähnt ist ein Prefab eine Art Vorlage. Wir verwenden diese Vorlage als Bestandteil unseres ProjectileWeapon Scriptes. Erweitert daher das Script um eine neue Variable: public GameObject Projectile; Wir hätten anstelle des GameObject Typs auch den Projectile Script typ verwenden können, so aber sind wir nicht darauf angewisen dass unser Prefab auch wirklich ein Projektil ist. Wenn wir Weltraumaffen verschießen wollen können wir das ebenso tun. Erweitert nun die Fire Methode um folgendes: var projectile = (GameObject)Instantiate(Projectile, transform.position, transform.rotation); projectile.transform.eulerAngles += new Vector3(0, Random.Range(-this.SpreadAngle, this.SpreadAngle), 0); projectile.rigidbody.velocity = rigidbody.velocity; Physics.IgnoreCollision(collider, projectile.collider); In der ersten Zeile erzeugen wir ein Klon unseres Prefabs (Projectile) an der Position (transform.position) und Rotation (transform.rotation) und weisen das gecastete Ergebniss einer lokalen Variable zu. Diese lokale Variable, welche ein GameObjekt ist und unseren Klon entspricht werden wir nun etas drehen um die Streuung zu simulieren. Die dritte Zeile sorgt dafür dass unser Projektil mindestens so schnell ist wie unser Raumschiff. Die letzte Zeile sorgt dafür dass unser Geschoss nicht mit der Waffe (dem Raumschiff) kollidiert welches es gefeuert hat. Weist nun das ProjetileWeapon Script unserem Spieler Raumschiff zu und wählt als Projektil im Inspektor unser Projektil Prefab aus. Wenn wir nun die Szene starten und die Leertaste drücken können wir beobachten wie sich viele kleine Projektile auf den Weg machen. Bei den vielen Dingen die jetzt schon gemacht wurden, hier die Szene mit allen Daten als handliches Package: https://dl.dropboxusercontent.com/u/2446365/ShootEmUpTutorial/Part4.unitypackage Teil 5: http://forum.unity-c...teil-5-schaden/
  14. Es gibt bereits den Dritten Teil, der vierte wird heute auch noch fertig werden (Waffen). Hier erstmal Teil 3: http://forum.unity-community.de/topic/7102-wir-bauen-uns-ein-space-shoot-em-up-teil-3-antrieb/ Der Würfel bewegt sich übrigens auch ohne die Kugel, da wir aber keinerlei Anhaltspunkte zur Position und Bewegung innerhalb der Szene haben (die Kamera schaut immer genau auf den Würfel und zeigt damit keine Positionsveränderung an da wir keine Sterne oder ähnliches haben) haben wir einfach eine Sphere hinzugefügt um einen Bezugspunkt zu haben.
  15. Zeit für etwas Umbauarbeiten, bisher haben wir einen PlayerController der etwas mehr macht als nur zu kontrollieren. Schwer zu glauben bei nur 43 Zeilen nicht wahr? Schauen wir uns einmal an was unser PlayerController so macht, er reagiert auf Tastendrücke vom Spieler und verändert direkt dadurch die Geschwindigkeit und Richtung unseres Raumschiffes. Das sind 2 verschiedene Aufgabenbereiche die wir trennen sollten. Um anschließend die Stärke des von Unity uns auferlegten Komponentensystemes auszuspielen. Wiederverwendbarkeit! Unser PlayerController Script sollte wie der Name schon impliziert nur dafür zuständig sein die Eingaben des Spielers auszuwerten und das was dann damit gemacht werden sollte an andere Komponenten übergeben. Deswegen die Zweiteilung des Scriptes. Ein Teil wird die Eingaben entgegen nehmen, dies bleibt unser PlayerController und der andere Teil wird die Geschwindigkeit und Drehung des Schiffs kontrollieren. Wir fangen an indem wir uns ein neues C# Script erstellen und es "Drive" (Antrieb) nennen. Für unsern Antrieb, welchen wir später wiederverwenden werden (zB für gegnerische Raumschiffe) benötigen wir folgende Dinge: - Beschleunigen (Accelerate) - Abbremsen (Deaccelerate) - Drehen Und um die Steuerung leichter zu machen fügen wir noch etwas hinzu: - Automatisches abbremsen Automatisches abbremsen ist eine passive Eigenschaft unseres Scriptes, bedeutet das wir es nicht extra aufrufen müssen damit es funktioniert. Wir beginnen daher damit dass wir uns einige Basiselemente vom PlayerController nehmen und diese im Drive Script einfügen. Und zwar alle Elemente die direkt den Antrieb des Schiffes betreffen. Was so ziemlich alle Variablen des PlayerController Scriptes wären: public float Acceleration = 1000.0f; public float Deacceleration = 1000.0f; public float RotationAcceleration = 300.0f; public float MaxForce = 1000.0f; public float MinForce = -500.0f; public float CurrentForce = 0.0f; Dies sollte im Drive Script stehen und nicht im PlayerController Script. Nun legen wir mit der ersten Fähigkeit unseres Drive Scriptes los: Beschleunigen! Erstellt eine neue Methode mit dem Namen Accelerate welche einen zusätzlichen float Parameter entgegen nimmt. Dieser Parameter soll dazu dienen fein zu regeln wie sehr wir beschleunigen wollen. Da wir bisher selbst noch keinerlei Parameter in selbst geschriebenen Methoden benutzt haben, hier die Methode: public void Accelerate(float factor) { } Wir benutzen hier so wie bei den Variablen ein public direkt vor der Methode. Da wir diese Methode von dem PlayerController Script aufrufen wollen muss diese public sein (öffentlich erreichbar). Wer nun ein wenig aufgepasst hat weiß auch was ungefähr in diese Methode hinein muss. Den Part vom PlayerController Script welcher ausgeführt wird wenn wir die nach Oben Taste drücken: CurrentForce += Time.deltaTime * Acceleration; Wir müssen noch unseren factor verwenden, wir verwenden ihn indem wir alles rechts von += mit dem factor multiplizieren. CurrentForce += Time.deltaTime * Acceleration * factor; Genau das Gleiche erledigen wir für die Deaccelerate Methode: public void Deaccelerate(float factor) { CurrentForce -= Time.deltaTime * Deacceleration * factor; } Wenn ihr euch noch daran erinnert haben wir im PlayerController Script eine Begrenzung der CurrentForce eingebaut, bisher machen wir dies immer egal ob wir Beschleunigt haben oder Abgebremst haben. Diesen kleinen Fehler können wir beheben indem wir die Stelle welche die Begrenzung aufrechterhält, nach Accelerate und Deaccelerate überträgt. Als Beispiel, so sollte die Accelerate Methode nach dieser Ergänzung aussehen: public void Accelerate(float factor) { CurrentForce += Time.deltaTime * Acceleration * factor; CurrentForce = Mathf.Clamp(CurrentForce, MinForce, MaxForce); } Nun haben wir die Beschleunigung und Abbremsung, aber etwas Kleines fehlt hierbei noch, wir wollen ja eine automatische Abbremsung wenn weder beschleunigt als auch abgebremst wurde. Aus diesem Grund merken wir uns wenn eines dieser beiden Aktionen ausgeführt wurde. Dazu erstellen wir uns zuerst eine neue Variable für das Drive Script: private bool hasChangedCurrentForce = false; Wir ihr bemerkt habt ist diesmal ein private vor dem Typ und kein public. Dies liegt daran dass wir nicht wollen dass diese Variable ausserhalb des Scriptes Bedeutung hat und auch nicht angefasst werden soll. private bedeutet dass die Variable oder Methode nur im inneren der Klasse in der es definiert wurde verwendet werden kann. Wir setzen hasChangedCurrentForce auf true immer dann wenn wir Accelerate oder Deaccelerate verwenden: hasChangedCurrentForce = true; Fügt diese Zeile in beide Methoden ein. Nun fehlt nur noch das Drehen, dies können wir lösen indem wir uns 2 Methoden, für jede Richtung in die wir drehen können wollen eine, einfügen. Wie bei Accelerate und Deaccelerate benutzen wir einen factor Parameter. Und wie zuvor übernehmen wir zu aller erst den Inhalt aus dem PlayerController Script welche für die Drehungen zuständig waren: public void RotateRight(float factor) { rigidbody.AddTorque(0, Time.deltaTime * RotationAcceleration * factor, 0); } public void RotateLeft(float factor) { rigidbody.AddTorque(0, -Time.deltaTime * RotationAcceleration * factor, 0); } Das wars. Die Drehung wurde eingebaut und sollte funktionieren. Was nun noch fehlt ist die automatische Abbremsung und die Anwendung der CurrentForce. Der letzte Part ist der einfachste, da wir auch hier direkt den Code aus dem PlayerController übernehmen können. Daher kopiert die notwendige Zeile und fügt sie in die Update Methode des Drive Scriptes ein: void Update () { rigidbody.AddForce(CurrentForce * Time.deltaTime * transform.forward); } Nun zum letzten Feature des Scriptes, die automatische Abbremsung. Da wir am Anfang des Tutorials klar gestellt haben wann dies einsetzen soll und zwar nur dann wenn weder beschleunigt noch abgebremst wurde, können wir bereits die ersten Zeilen dafür schreiben (wieder in die Update Methode): if (!hasChangedCurrentForce) { } Wir setzen hasChangedCurrentForce immer auf true wenn wir beschleunigen oder abbremsen. Nach der Prüfung sollten wir daher die Variable wieder auf false setzen. Schreibt dies direkt unter den if Scope: hasChangedCurrentForce = false; Wie soll nun aber die automatische Abbremsung funktionieren? Ganz einfach, wir schauen uns die Richtung an in der unser Raumschiff schaut (transform.forward) und vergleichen dies mit der Richtung und Geschwindigkeit in der wir uns bewegen (rigidbody.velocity). Wir benutzen dazu die Vector3.Dot Methode welche uns einen Wert liefert den wir benutzen werden um zu entscheiden ob wir Beschleunigen müssen (wenn wir rückwärts fliegen) oder Abbremsen müssen (wenn wir vorwärts fliegen). Tragt dazu folgendes in den if Scope für die automatische Abbremsung ein: var forwardFactor = Vector3.Dot(transform.forward, rigidbody.velocity); Diesen forwardFactor verwenden wir nun für die Entscheidung was gemacht werden soll: if (forwardFactor > 1.0f) { Deaccelerate(1.0f); } else if (forwardFactor < -1.0f) { Accelerate(1.0f); } else { CurrentForce = 0; } Das if Keyword kennen wir ja bereits, neu sind dagegen das else if und das else. else if wird aufgerifen wenn das vorherige if oder ein vorheriges else if selbst nicht erfüllt werden konnten. else if bietet also einen alternativen Weg an. Sollten auch alle folgenden else if scheitern so wird der Code im else Scope ausgeführt. if, else if und else können also übersetzt werden mit: Wenn X dann Y, Ansonsten Wenn Z dann W, Ansonsten R. Um einen Ping Pong Effekt zu vermeiden interessieren wir uns nur forwardFactor wenn er größer 1 oder kleiner -1 ist, im anderen Fall lassen wir den Luftwiederstand für uns arbeiten, den es im Weltraum zwar nicht gibt, aber für das Spielgefühl dennoch zuträglich ist. Wir stellen daher den "Drag" Wert in der "Rigidbody" Komponente des Spieler Raumschiffs auf 1. Dies sorgt dafür dass wir wenn wir keine Kraft auf unser GameObject auswirken, dieses GameObjekt immer langsamer wird. Unser Drive Script ist damit fertig. Zur Vollständigkeit halber, hier das Drive Script: using UnityEngine; using System.Collections; public class Drive : MonoBehaviour { public float Acceleration = 1000.0f; public float Deacceleration = 1000.0f; public float RotationAcceleration = 300.0f; public float MaxForce = 1000.0f; public float MinForce = -500.0f; public float CurrentForce = 0.0f; private bool hasChangedCurrentForce = false; // Use this for initialization void Start () { } // Update is called once per frame void Update () { rigidbody.AddForce(CurrentForce * Time.deltaTime * transform.forward); if (!hasChangedCurrentForce) { var forwardFactor = Vector3.Dot(transform.forward, rigidbody.velocity); if (forwardFactor > 1.0f) { Deaccelerate(1.0f); } else if (forwardFactor < -1.0f) { Accelerate(1.0f); } else { CurrentForce = 0; } } hasChangedCurrentForce = false; } public void Accelerate(float factor) { CurrentForce += Time.deltaTime * Acceleration * factor; CurrentForce = Mathf.Clamp(CurrentForce, MinForce, MaxForce); hasChangedCurrentForce = true; } public void Deaccelerate(float factor) { CurrentForce -= Time.deltaTime * Deacceleration * factor; CurrentForce = Mathf.Clamp(CurrentForce, MinForce, MaxForce); hasChangedCurrentForce = true; } public void RotateRight(float factor) { rigidbody.AddTorque(0, Time.deltaTime * RotationAcceleration * factor, 0); } public void RotateLeft(float factor) { rigidbody.AddTorque(0, -Time.deltaTime * RotationAcceleration * factor, 0); } } Nun müssen wir es nur noch benutzen. Auf auf zum PlayerController Script, welches nun so aussehen sollte: using UnityEngine; using System.Collections; public class PlayerController : MonoBehaviour { // Use this for initialization void Start () { } // Update is called once per frame void Update () { if (Input.GetKey(KeyCode.UpArrow)) { } if (Input.GetKey(KeyCode.DownArrow)) { } if (Input.GetKey(KeyCode.RightArrow)) { } if (Input.GetKey(KeyCode.LeftArrow)) { } } } Dieses Script wird unser Drive Script benutzen, daher sollten wir uns dieses Drive Script auch suchen. Fügt daher eine neue Variable in das PlayerController Script hinzu: private Drive drive; private deswegen weil es nur für die Internas des PlayerController Scriptes von Relevanz ist. Wir weisen diese Variable innerhalb der Start Methode zu: drive = GetComponent<Drive>(); GetComponent sucht eine Komponente, welche Komponente geben wir über einen generischen Parameter an. Die spitzen Klammern (< und >) umgrenzen die generischen Parameter. Generische Parameter sind immer Typen, mit GetComponent<Drive>() sagen wir demnach dass wir eine Komponente an unserem GameObjekt suchen welche vom Drive Typ ist. Alternativen zu dieser Variante eine Komponente zu suchen gibt es ebenfalls, entweder per Namen der Komponente (GetComponent("Drive")) oder wieder über den Typen der Komponente (GetComponent(typeof(Drive))). Wir benutzen die generische Variante, weil es automatisch einen Fehler gibt wenn es den Typen nicht gibt (zB: ein anderen Namen hat) was bei GetComponent("Drive") keinen Fehler geben würde. Ausserdem hat sie den Vorteil besser lesbar zu sein als GetComponent(typeof(Drive)). Ausserdem bekommen wir direkt als Rückgabetyp unser Drive zurück. Bei allen anderen Varianten müssen wir anschließend den Rückgabewert erst umwandeln, was wieder mehr Schreibarbeit bedeutet und auch unlesbarer wäre, vergleicht selber: drive = GetComponent<Drive>(); vs: drive = (Drive)GetComponent(typeof(Drive)); Fügt nun die Verwendung unseres Drive Scriptes in die Update Methode ein: void Update () { if (drive) { if (Input.GetKey(KeyCode.UpArrow)) { drive.Accelerate(1.0f); } if (Input.GetKey(KeyCode.DownArrow)) { drive.Deaccelerate(1.0f); } if (Input.GetKey(KeyCode.RightArrow)) { drive.RotateRight(1.0f); } if (Input.GetKey(KeyCode.LeftArrow)) { drive.RotateLeft(1.0f); } } } Wie man vermutlich gut sehen kann haben wir eine weitere if Bedingung eingefügt: if (drive) Damit können wir testen ob unsere Drive Komponente eventuell nicht gefunden oder gar gelöscht wurde. Das fertige PlayerController Script sollte nun so aussehen: using UnityEngine; using System.Collections; public class PlayerController : MonoBehaviour { private Drive drive; // Use this for initialization void Start () { drive = GetComponent<Drive>(); } // Update is called once per frame void Update () { if (drive) { if (Input.GetKey(KeyCode.UpArrow)) { drive.Accelerate(1.0f); } if (Input.GetKey(KeyCode.DownArrow)) { drive.Deaccelerate(1.0f); } if (Input.GetKey(KeyCode.RightArrow)) { drive.RotateRight(1.0f); } if (Input.GetKey(KeyCode.LeftArrow)) { drive.RotateLeft(1.0f); } } } } Startet nun wieder die Szene in Unity und schaut wie sich das ganze nun verhält. Abseits vom automatischen Abbremsen sollte sich alles wie gehabt verhalten. Feedback ist wiederum gern gesehen. PS: Das Forum macht mir immer die Tabs im Code kaputt ;( Teil 4: http://forum.unity-community.de/topic/7112-wir-bauen-uns-ein-space-shoot-em-up-teil-4-waffen/
  16. Danke Der nächste Teil ist auch schon Online: http://forum.unity-community.de/topic/7097-wir-bauen-uns-ein-space-shoot-em-up-teil-2-camera/page__fromsearch__1
  17. Willkommen im 2ten Teil der Tutorial Reihe. In diesem Teil wollen wir uns näher mit der Kamera beschäftigen. Bisher zeigt diese immer nur starr auf den Ursprungspunkt der Szene. Besser wäre es wenn die Kamera immer unser Raumschiff verfolgen würde. Wir beginnen damit indem wir ein neues Script schreiben. Erstellt daher wie im ersten Teil ein neues C# Script und nennt es "CameraController". Dieses Script könnt ihr nun gleich dem "Main Camera" GameObject zuweisen, wie das geht wurde euch bereits im ersten Teil der Tutorial Reihe gezeigt. Öffnet danach das Script um es editieren zu können. Ihr solltet nun genau dies hier sehen: using UnityEngine; using System.Collections; public class CameraController : MonoBehaviour { // Use this for initialization void Start () { } // Update is called once per frame void Update () { } } Wir wollen das dieses Script immer auf unser Zielobjekt schaut. Deswegen geben wr dem Script eine Variable die uns ermöglicht per Inspektor dieses Zielobjekt zu setzen. Fügt daher folgendes hinzu: public Transform Target; Transform bedeutet dass hier eine Transform Komponente erwartet wird, jedes GameObjekt hat diese Komponente und uns interessiert auch nur das was in dieser Transform Komponente zu finden ist (die Position). Wenn wir das Script nun speichern und nach Unity gehen können wir nun im Inspektor des ausgewählten "Main Camera" GameObjectes unser Camera Controller sehen und dort auch ein Platz in der wir unser "Target" angeben können. Klickt auf dieses Feld und wählt im darauf folgenden Dialog unser "Spieler Raumschiff" aus. Wenn wir nun auf Play drücken passiert noch rein gar nichts, dies werden wir aber schnell ändern. Geht dazu wieder in das "CameraController" Script und ändert void Update zu void LateUpdate. LateUpdate wird so wie Update jeden Frame aufgerufen. Der Unterschied ist aber dass LateUpdate aufgerufen wird nachdem sowohl die Physik Berechnungen als auch alle anderen Updates durchgeführt wurden. Der Ideale Zeitpunkt also um unsere Kamera zu positionieren ohne dass es zu hässlichen Ruckeleffekten kommt. Fügt nun in diese Methode folgendes hinzu: if (!Target) { return; } !Target bedeutet dass wenn wir kein Target ausgewählt wurden der bool Wert true generiert wird. Einfach nur Target ohne das ! Symbol würde bedeuten dass true nur dann generiert wird wenn Target einen Wert hat. Wir wollen an dieser Stelle aber die Ausführung der LateUpdate Methode beenden wenn kein Target gewählt wurde, weswegen wir auf die Nicht Existenz des Targets prüfen und mit return die Methode wieder verlassen sollte Target nicht gesetzt sein. Dies verhindert Fehler wenn wir mal in die Situation kommen in der unser Spieler Raumschiff verschwindet, weil es zb zerstört wurde. Um die Kamera immer mit einer gewissen Distanz von unserem Target zu halten, fügen wir eine weitere public Variable ein: public float Distance = 10.0f; Hierzu gibt es nicht viel zu sage, da die Verwendung das Interessante ist. Dazu erweitern wir die LateUpdate Methode ein weiteres mal, fügt unter die if Bedingung folgendes hinzu: transform.position = Target.position - Distance * transform.forward; Der Code berechnet eine Position der Camera relativ zum Target. Wir benutzen dazu die Position des Targets (Target.position) und verschieben diesen Wert mithilfe der Blickrichtung unserer Camera (transform.forward). Indem wir unsere Distance Variable mit der Blickrichtung multiplizieren setzen wir die Entfernung fest in der sich die neue Position zum Target befinden soll. Mit dem = weisen wir die neu berechnete Position unserer Camera zu (transform.position). Wenn wir nun das Script speichern sollte es so aussehen: using UnityEngine; using System.Collections; public class CameraController : MonoBehaviour { public Transform Target; public float Distance = 10.0f; void Start() { } void LateUpdate() { if (!Target) { return; } transform.position = Target.position - Distance * transform.forward; } } Wenn wir nun unsere Scene in Unity starten und unser Raumschiff bewegen wollen, sehen wir.. dass sich unser Raumschiff nur dreht aber nicht bewegt! Dies liegt daran dass uns Anhaltspunkte zu dieser Bewegung fehlen, da die Camera ja nun immer auf unser Raumschiff schaut. Um einen Anhaltspunkt zu erschaffen fügen wir ein neues GameObjekt in die Szene ein. Wir benutzen dafür den Menüpunkt: "GameObject"->"Create Other"->"Sphere" Wählt die "Sphere" in Hierarchy Bereich aus und wählt eine Position im Inspektor aus welche in der Nähe unseres Raumschiffes liegt, dieses aber nicht überlappt. Wählt zB die Position: X = 0, Y = 0 und Z = 3. Wenn wir nun die Scene erneut starten können wir sehen dass sich unser Raumschiff relativ zu dieser "Sphere" bewegt. Die Camera zeigt dabei immer fleißig auf unser Raumschiff. Hurra! Im nächsten Teil der Reihe lösen wir den Code des PlayerControllers etwas auf und erstellen uns ein Script für den Antrieb des Schiffes. Feedback ist gern gesehen. Teil 3: http://forum.unity-community.de/topic/7102-wir-bauen-uns-ein-space-shoot-em-up-teil-3-antrieb/
  18. In dieser Tutorial reihe wollen wir uns mithilfe von Unity und der Programmiersprache C# einen Space Shooter basteln. Dabei werde ich euch sanft mit Unity und C# vertraut machen. Deshalb ist diese Reihe sowohl für absolute Anfänger in Sachen Unity als auch im Programmieren geeignet. Im ersten Teil wollen wir unseren Grundstein legen, wir wollen unser Spieler Raumschiff mithilfe der Tastatur steuern. Das Spielerraumschiff stellen wir uns zuerst als einfachen Würfel zur Verfügung. In einem späteren Tutorial werden wir diesen Würfel durch komplexere Gebilde austauschen. Folgendes soll unser Raumschiff in der ersten Version können: - Beschleunigen wenn wir die Vorwärtstaste drücken - Negativ Beschleunigen wenn wir die Rückwärtstaste drücken - Nach links drehen wenn wir die Linkstaste drücken - Und nach rechts drehen wenn wir die Rechtstaste drücken Wir beginnen das ganze indem wir uns ein neues Projekt in Unity erstellen, komplett leer ohne Schnickschnack. Wählt dazu, innerhalb von Unity den Menüpunkt "File"->"New Project.." aus und wählt im darauf folgenden Dialog einen geeigneten Namen und Speicherort für euer Projekt aus. Selektiert zusätzlich die Option "3D" unter dem Punkt "Setup defaults to" im unteren Bereich des Dialoges. Klickt danach auf OK. Wir erstellen nun unser Raumschiff. Dazu wählen wir im Menü "GameObject"->"Create Other"->"Cube". Dies erzeugt uns unser erstes GameObject, einen Würfel mit dem Namen "Cube". Da der Name "Cube" wenig aussagt benennen wir ihn kurzerhand in "Spieler Raumschiff" um: Wählt dazu den Würfel in der Ansicht die "Hierarchy" genannt wird (ganz links) aus. Dann könnt ihr im Fenster welches "Inspector" genannt wird (ganz rechts) den Namen ändern indem ihr in das Textfeld mit dem Inhalt "Cube" klickt und dort den Text "Spieler Raumschiff" eintippt. Die Anführungszeichen bitte weglassen. Ihr könnt zum Umbenennen eines GameObjectes auch, nachdem ihr es in der "Hierarchy" Ansicht ausgewählt habt, die F2 Taste drücken und dort den Namen eintippen. Das Spieler Raumschiff ist nun noch etwas ungünstig positioniert und um das zu ändern wählen wir es wie bereits getan aus und ändern in der "Inspector" Ansicht die Position. Dies kann gemacht werden indem ihr unter "Position" alle 3 Werte (X, Y und Z) auf 0 ändert. Um auch die Kamera etwas passender zu positionieren und zu drehen wählen wir nun auch das "Main Camera" GameObject aus und ändern im Inspektor die Position auf: X = 0, Y = 6 und Z = -2 Sowie die Rotation auf X = 70 Dadurch schaut unsere Kamera auf unser Raumschiff. Speichert nun die Scene ab indem ihr Strg+S drückt. Unity wird euch darauf hin fragen wie die Scene heißen soll. Ich habe meine Scene "Scene0" genannt. Wärend der Entwicklung solltet ihr darauf achten immer wieder Strg+S zu drücken um eure Zwischenstände beim verändern der Scenen auch zu speichern. Nichts ist ärgerlicher als zuvor gemachte Änderungen wiederholen zu müssen. Weil Unity zB abgestürzt ist. Damit sind die Vorbereitungen abgeschlossen und wir können uns nun ein Script erstellen welches unser Raumschiff steuert. Dazu klicken wir mit der rechten Maustaste in den "Assets" Bereich (ganz unten) und wählen den Kontextmenüpunkt "Create"->"C# Script" Anschließend wählen wir als Namen für dieses Script "PlayerControler". "PlayerControler" deshalb weil dies unser Script ist welches das Spieler Schiff kontrolliert. Wir werden in dieser TutorialReihe englische Bezeichner verwenden sofern es sich um das Programmieren handelt. In Unity ist der Name des Scriptes gleichbedeutend mit dem Klassennamen. Dazu aber gleich mehr. Um nun dieses Script zu editieren könnt ihr es doppelt anklicken. Darauf hin sollte sich ein Editor öffnen und ihr seht folgendes: using UnityEngine; using System.Collections; public class PlayerController : MonoBehaviour { // Use this for initialization void Start () { } // Update is called once per frame void Update () { } } Die ersten beiden Zeilen (die die mit using beginnen) helfen dabei uns einige Grundfunktionen zu geben die wir in unserem Script verwenden können. using UnityEngine gibt uns alle Funktionen die direkt mit Unity zu tun haben. using System.Collections gibt uns einige Hilfsmittel um Ansammlungen von Daten zu verwalten, zB eine Reihe von Werten die wachsen kann und aus der Elemente entfernt werden können. Ausserdem stellt es uns etwas bereit was wir benötigen sobald es um Coroutinen geht. Die nächste Zeile definiert den Namen unseres Scriptes (PlayerController) und gibt an was genau es sein soll (class, eine Klasse) und von wem es einige Grundfunktionen bekommen soll (MonoBehaviour). Jedes Script welches wir auf GameObjects legen können leitet von MonoBehaviour ab. "leitet" bedeutet hier dass unser Script zugleich selbst ein MonoBehaviour ist und damit einige Grundfunktionen beherbergt die dieses MonoBehaviour selbst bereit stellt. Stellt euch einfach ein Auto vor. Ein Auto leitet zB von Fahrzeug ab und kann dadurch gefahren werden. Die Zeile mit void Start() bedeutet dass wir hier eine Methode definieren die Start heißt. Im Bezug auf Unity bedeutet dies dass diese Methode automatisch von Unity aufgerufen wird wenn das Script auf ein GameObject liegt und die Scene betritt. Innerhalb des Bereiches der durch { und } eingegrenzt ist können wir schreiben was unser Script machen soll wenn es startet. Der durch { und } eingegrenzte Bereich wird Scope genannt. Die Zeile Update ist ähnlich wie Start, nur wird diese Update Methode nicht aufgerufen wenn unser Script die Welt betritt sondern in jedem Framedurchgang wenn unser GameObject und Script aktiv sind. Frame bedeutet der Moment in dem ein Bild fertig gezeichnet wurde. Je schneller ein Computer desto mehr Frames in einer Sekunde können erreicht werden. In der Update Methode können wir überprüfen ob der Spieler eine Taste gedrückt hat und dies werden wir nun auch tun. Mithilfe der Input Klasse von Unity können wir abfragen ob eine Taste gedrückt wurde, wir erweitern deshalb die Update Methode um folgendes: if (Input.GetKey(KeyCode.UpArrow)) { print("Power!"); } Denkt daran: Schreibt das ganze zwischen { und } der Update Methode. Das if ist ein Schlüsselwort von C# welches besagt dass hier eine Abfrage statt finden soll und das innerhalb der Klammern ist die Bedingung die dazu erfüllt sein muss damit der darauf folgende Bereich (Scope) innerhalb der { und } Zeichen ausgeführt wird. Input gibt eine Klasse an welche die GetKey Methode bereit stellt. Die GetKey Methode benötigt einen Parameter der besagt welche Taste geprüft werden soll und dies definieren wir mit KeyCode.UpArrow. Input.GetKey liefert einen bool Wert zurück. bool bedeutet dass es 2 verschiedene Werte gibt, entweder true, was wahr oder ja bedeutet, oder false, welches unwahr, nein oder falsch bedeutet. Bedingungen erwarten immer ein bool welches sie auswerten können. Nur wenn dieses bool true ist wird die Ausführung des Scopes vom if gestartet. Die print Methode gibt den String aus der ihr als Parameter übergeben wurde. Ein String ist eine Zeichenkette, ein Text. Wenn wir direkt einen Text angeben wollen so müssen wir ihn innerhalb der Anführungszeichen schreiben. Dann speichern wir das ganze und wechseln zurück zu Unity. Unity wird nun dieses von uns gespeicherte Script kompilieren (in Maschinencode umwandeln) und uns auf eventuelle Fehler hinweisen. Wenn alles erfolgreich kompiliert wurde können wir unser Script auf unser Spieler Raumschiff zuweisen. Dazu wählt das Spieler Raumschiff aus und klickt im unteren Inspektor Bereich (möglicherweise müsst ihr nach unten scrollen) auf den Knopf "Add Component" wählt dann "Scripts"->"Player Controller" aus. Unser Script sollte nun unserem Raumschiff zugewiesen worden sein. Das zuweisen ist auch möglich indem man das Script aus dem Asset Bereich auf das Raumschiff schiebt. Jetzt können wir auf den Play Knopf drücken (das Dreieck im oberen Bereich von Unity). Wenn wir nun die nach Oben Taste drücken sollte im unteren linken Bereich von Unity der Text "Power!" stehen. Glückwunsch du hast soeben dein erstes Script geschrieben und es einem GameObjekt zugewiesen und siehst nun noch wie es agiert. Das Raumschiff bewegt sich aber dennoch nicht, was ziemlich öde ist und schleunigst geändert werden sollte. Unser Raumschiff benötigt dazu die Möglichkeit physikalisch bewegt zu werden, dies geht indem wir dem Raumschiff die Rigidbody Komponente (Komponenten sind Scripte!) zuweisen. Dazu klicken wir wieder im Inspektor den Knopf "Add Component" aus und benutzen "Physics"->"Rigidbody". Im Inspektor erscheint nun ein zusätzlicher Bereich mit dem Titel "Rigidbody". Hier müssen wir auch noch einige Änderungen vornehmen, zB müssen wir dafür sorgen dass unser Raumschiff nicht nach unten fällt. Dazu deaktivieren wir die Checkbox "Use Gravity". Nun können wir mithilfe des Rigidbodies unser Raumschiff steuern. Dazu gehen wir wieder in unser PlayerController Script und entfernen die Zeile mit dem print("Power!") und ersetzen die Zeile durch die Anweisung welche unser Raumschiff beschleunigt: rigidbody.AddForce(transform.forward); rigidbody ist eine Abkürzung die es uns erlaubt ohne extra Code direkt auf unser Rigidbody zuzugreifen, diese Abkürzung gibt es in ähnlicher Form auch für andere Komponenten, zB der Transform Komponente (transform). AddForce ist eine Methode der Rigidbody Komponente die wir aufrufen können um etwas Kraft auf das rigidbody auszuüben. Da wir es vorwärts, entlang der Blickrichtung des Objektes beschleunigen wollen, benutzen wir transform.forward als Parameter. transform ist die Abkürzung auf die Transform Komponente welche Position, Richtung, Skalierung des GameObjektes beinhaltet. Und forward ist ein Richtungsvector der dorthin zeigt wo das GameObjekt "hin blickt". Wenn wir nun das Script speichern und uns das Resultat in Unity ansehen (Play drücken) dann können wir sehen wie sich unser Raumschiff bewegt wenn wir die nach Oben Taste drücken. Leider ist diese Beschleunigung stark von der Anzahl der Frames pro Sekunde abhängig. Was bedeutet das auf schnelleren Computer die Beschleunigung höher ist als auf langsameren Computern. Ausserdem können wir die Beschleunigung nicht genauer regeln und entspricht immer der Länge des Richtungsvectors. Um das zu ändern erweitern wir unser Script wieder ein wenig. Als aller erstes machen wir die Beschleunigung unabhängig von der Anzahl der Frames pro Sekunde: aus rigidbody.AddForce(transform.forward); wird deswegen: rigidbody.AddForce(transform.forward * Time.deltaTime); Time ist die Klasse von Unity die einiges an Operationen und Variablen bereit stellt die etwas mit der Zeit zu tun haben. deltaTime zB ist die Zeit in Sekunden die der letzte Frame benötigte. Indem wir transform.forward mit diesem Wert multiplizieren bleibt unsere Beschleunigung auch dann konstant wenn wir einen sehr langsamen oder sehr schnellen Computer verwenden. Wenn wir nun das ganze in Unity ausprobieren werden wir allerdings ein unangenehmes Wunder erleben. Unser Würfel bewegt nur sehr langsam. Deswegen werden wir nur die Kraft erhöhen. Dazu erweitern wir unser Script indem wir innerhalb der Klassendefinition über void Start folgendes schreiben: public float Acceleration = 10.0f; public besagt dass die float Variable öffentlich erreichbar ist, also jeder der unser Script zu fassen bekommt diesen Wert lesen und/oder verändern kann. Dies besagt zB auch dass dieser Wert im Inspektor von Unity angepasst werden kann. Acceleration ist der Name unserer Variable und float der Datentyp. float gibt an dass wir einen Zahlentyp wollen der Kommawerte darstellen kann. Ein Zahlentyp der nur ganze Zahlen darstellen kann wäre zB int. Wir verwenden unsere Acceleration Variable nun indem wir unser Zeile in der wir Kraft auf das Rigidbody wirken lassen erweitern und zwar: rigidbody.AddForce(transform.forward * Time.deltaTime * Acceleration); Wir multiplizieren also unsere Frame unabhängige Kraft mit unserer Variable. Dies ermöglicht es uns nun anzugeben wie Stark die Beschleunigung ist. Wenn wir nun wieder das ganze innerhalb von Unity testen sehen wir dass unser Würfel sich schneller bewegt als zuvor. Wir können diesen Wert anpassen indem wir im Inspektor nach unsererm Script Player Controller suchen und dort den Eintrag mit dem Namen "Acceleration" ändern. Unser Script sollte aktuell wie folgt aussehen: using UnityEngine; using System.Collections; public class PlayerController : MonoBehaviour { public float Acceleration = 10.0f; // Use this for initialization void Start () { } // Update is called once per frame void Update () { if (Input.GetKey(KeyCode.UpArrow)) { rigidbody.AddForce(transform.forward * Time.deltaTime * Acceleration); } } } Da wir nun schon das Beschleunigen haben werden wir nun noch das Abbremsen hinzufügen. Das ganze ist ziemlich identisch zum Beschleunigen und ist eine Ideale Übung zum selber probieren des bereits erlernten. Wenn ihr euch ausgetobt habt oder einfach nur weiter machen wollt, hier ist der Code welcher auch die negative Beschleunigung beinhaltet: using UnityEngine; using System.Collections; public class PlayerController : MonoBehaviour { public float Acceleration = 10.0f; public float Deacceleration = 10.0f; // Use this for initialization void Start () { } // Update is called once per frame void Update () { if (Input.GetKey(KeyCode.UpArrow)) { rigidbody.AddForce(transform.forward * Time.deltaTime * Acceleration); } if (Input.GetKey(KeyCode.DownArrow)) { rigidbody.AddForce(-transform.forward * Time.deltaTime * Deacceleration); } } } Was nun noch fehlt ist das Drehen unseres Raumschiffes. Dies funktioniert auf leicht ähnliche Weise wie das Beschleunigen und Abbremsen. Deswegen legen wir uns wieder eine Variable an welche angibt wie schnell die Drehung statt finden soll. public float RotationAcceleration = 30.0f; Als Nächstes müssen wir nun noch auf den Tastendruck reagieren der das Drehen veranlassen soll, ich werde mit dem nach rechts drehen anfangen was relativ einfach auf eine links Drehung erweitert werden kann: if (Input.GetKey(KeyCode.RightArrow)) { rigidbody.AddTorque(0, Time.deltaTime * RotationAcceleration, 0); } So wie AddForce fügt AddTorque etwas an unser Rigidbody. und zwar ein Drehmoment. Wir beschleunigen das Drehmoment unseres Rigidbdies um die Y Achse. Wenn wir das Ganze nun in Unity testen können wir unseren Cube schon vor der Kamera bewegen. Nur fühlt es sich momentan so an als würden wir auf dem Eis laufen. Dies liegt daran dass wir direkt Kräfte auf unseren Würfel anwenden, was ansich realistisch ist, aber leider das ganze schwieriger Steuerbar macht. Die Lösung dazu? Wir steuern durch das Beschleunigen oder Abbremsen die Kraft die unser Antrieb pro Frame automatisch auf den Würfel anwenden soll. Dazu fügen wir eine Reihe weiterer Variablen ein die dafür zuständig sind die maximale und minimale Kraft zu regeln als auch die momentane Kraft die unser Antrieb generieren soll zu speichern: public float MaxForce = 1000.0f; public float MinForce = -200.0f; public float CurrentForce = 0.0f; Nachdem wir das nun haben müssen wir natürlich auch die Handhabung der nach Oben und Unten Tasten ändern. Wir ändern hierbei jeweils die AddForce Zeilen indem wir den Frame unabhängigen Acceleration Wert zu CurrentForce hinzu addieren und den Deacceleration Wert abziehen. if (Input.GetKey(KeyCode.UpArrow)) { CurrentForce += Time.deltaTime * Acceleration; } if (Input.GetKey(KeyCode.DownArrow)) { CurrentForce -= Time.deltaTime * Deacceleration; } += bedeutet hierbei dass der Wert rechts von += zu den Wert links vom += addiert werden soll. Das ganze ist gleich zu a = a + b -= bedeutet dass der Wert abgezogen anstatt hinzugefügt wird. Jetzt haben wir die CurrentForce zwar angepasst aber noch kann sie sowohl zu hoch als zu niedrig werden und verwenden tun wir diesen Wert ebenso wenig. Daher grenzen wir CurrentForce zuerst ein. Dies geht indem wir die Clamp Methode der Mathf Klasse von Unity verwenden. Schreibe folgendes ans Ende der Update Methode: CurrentForce = Mathf.Clamp(CurrentForce, MinForce, MaxForce); Mathf.Clamp begrenzt den Wert im ersten Parameter innerhalb von den Wert des zweiten und dritten Parameters und gibt das Resultat wieder zurück. Damit ist sicher gestellt dass die CurrentForce niemals größer oder kleiner unserer Grenzwerte wird. Nun müssen wir CurrentForce nur noch irgendwie auf unser Raumschiff angewendet bekommen. Hierfür benutzen wir einfach unser rigidbody.AddForce wieder. Wir multiplizieren CurrentForce mit der Zeit die der letzte Frame zum ausführen brauchte und der Richtung unseres Raumschiffes: rigidbody.AddForce(CurrentForce * Time.deltaTime * transform.forward); Der aktuelle Code des PlayerController Scriptes sollte nun so aussehen: using UnityEngine; using System.Collections; public class PlayerController : MonoBehaviour { public float Acceleration = 100.0f; public float Deacceleration = 100.0f; public float RotationAcceleration = 30.0f; public float MaxForce = 1000.0f; public float MinForce = -200.0f; public float CurrentForce = 0.0f; // Use this for initialization void Start () { } // Update is called once per frame void Update () { if (Input.GetKey(KeyCode.UpArrow)) { CurrentForce += Time.deltaTime * Acceleration; } if (Input.GetKey(KeyCode.DownArrow)) { CurrentForce -= Time.deltaTime * Deacceleration; } if (Input.GetKey(KeyCode.RightArrow)) { rigidbody.AddTorque(0, Time.deltaTime * RotationAcceleration, 0); } if (Input.GetKey(KeyCode.LeftArrow)) { rigidbody.AddTorque(0, -Time.deltaTime * RotationAcceleration, 0); } CurrentForce = Mathf.Clamp(CurrentForce, MinForce, MaxForce); rigidbody.AddForce(CurrentForce * Time.deltaTime * transform.forward); } } Das Raumschiff beschleunigt nun immer mit der aktuellen Antriebskraft in die aktuelle Schiffsrichtung. Das fühlt sich nun schon alles viel besser an als vorher, die Steuerung ist leichter zu beherrschen, aber etwas fehlt noch.. Spielt ein wenig mit den Werten Im Inspektor der Rigidbody und PlayerController Komponenten herum um ein leichtes Gefühl dafür zu bekommen wie sich jeder Wert auswirkt. Gute Werte sind zB folgende: PlayerController: Acceleration 100 Deacceleration 50 Rotation Acceleration 100 Max Force 100 Min Force -20 Rigidbody: Mass 1 Drag 0 Angular Drag 10 Das war es für den ersten Teil der Tutorial Reihe. Im nächsten Teil werden wir die Kamera so umbauen dass diese immer auf das Spieler Raumschiff gerichtet ist. Feedback ist gern gesehen. Teil2: http://forum.unity-c...__fromsearch__1
  19. Der Unterschied wäre etwas gravierender wenn ich bei den Bildern nicht enforced hätte dass die Ränder immer in der höchsten verfügbaren Voxelstufe genommen werden. Der Grund wieso ich das mache ist einfach, wenn ich später die LODs verschiedener Voxel aneinander Reihe gibt es mithilfe eines zusätzlichen Tricks saubere LOD übergänge und ich brauche nicht auf dirty hacks wie Skirts oder schlimmeres zurück greifen. Die nächsten Schritte sind erstmal das Berechnen der Normalen (einfacher Gradient der 6 benachbarten Voxel zu einem Voxel) und das Einbeziehen von Nachbaroctrees (was der beschriebene andere Trick ist den ich im ersten Absatz erwähnt habe), Wenn das steht kann ich mich dran machen dass hier wieder darzustellen (mit vermutlich ettlichen optischen Änderungen):
  20. Ich hab das System wieder (jaja) etwas umgestellt um halbwegs up2date zu sein mit den verwendeten Techniken, es ist noch nicht ganz soweit wie im vorherigen Post (ich kann noch keine Landschaften darstellen), aber dafür ist um einiges mehr an KnowHow vorhandn. So was hat sich geändert.. Ich habe mich von uniformen Grids verabschiedet (16³ gleichgroße Zellen) und habe einen VoxelOctree geschrieben.. Er basiert auf MortonCodes und ist damit weitgehendst Pointerfree, die Vorteile davon sind, dass ich viel leichter gleiche Voxel zu einen größeren Voxel zusammenfassen kann. Ich kann auch viel schneller ein DualVolumen (für das DualContouring) generieren. Ich habe mich dabei an das 2010 Paper von Thomas Lewiner gehalten. Durch das VoxelOctree verringert sich auch die Zahl an Kanten die für das DualContouring relevant sind, da nun nur noch Bereiche hoch detailiert sind die es auch sein sollen. Ich kann nun bis zu 5 Stufen (max wären 10, aber 5 Stufen ermöglichen es mir diverse LUTs zu generieren ohne dabei den verfügbaren Speicher zu sprengen). Je Stufe verdoppelt sich die Genauigkeit, was bei einem UnitCube von 1m³ folgendes bedeutet: Stufe 0: 1m³ (100cm³) pro Voxel (1³ Voxel (1)) Stufe 1: 0.5m³ (50cm³) pro Voxel (2³ Voxel (8)) Stufe 2: 0.25m³ (25cm³) pro Voxel (4³ Voxel (64)) Stufe 3: 0.125m³ (12.5cm³) pro Voxel (8³ Voxel (512)) Stufe 4: 0.0625m³ (6.25cm³) pro Voxel (16³ Voxel (4096)) Stufe 5: 0.03125m³ (3.124cm³) pro Voxel (32³ Voxel (32768)) Was schon recht genau ist und für meine ZWecke völlig ausreicht. (Die Chunks welche auf je einen VoxelOctree basieren können beliebig skaliert werden, ich plane so 10m³ bis 100m³ pro Chunk). Das ganze unterstützt LOD, auf basis eines Fehlerquotienten der sich je stärker pro Übergeordneten Voxel erhöht desto mehr sein zusammengefasster Wert dem der Childvoxel abweicht. DualContouring klappt nun soweit auch ganz ok, noch ohne QEF da ich noch (!) keine Normalen generiere. Dafür ist er in der lage beliebige OctreeLevel unterschiede auszugleichen. Und er ist in der Lage Voxel niedrigerer Stufen ihr Volumen zu geben, so dass diese auch tatsächliches Volumen besitzen und nciht einfach nur punkte im Raum sind (verhindert hässliche Übergänge/Zacken, etc). Bisherige Geschwindigkeit bei meiner 2.8Ghz CPU liegt so bei 2 bis 3ms für das Contouring und Mesh erzeugen. Es fehlt hier aber noch das generieren der Normalen, was sicher etwas Zeit kostet und das verwenden der tatsächlichen QEF, wobei ich hierbei andeuten sollte dass es selbst ohne QEF ok aussieht und ich diese sogar weglassen werde wenn durch Verwendung der QEF nur eine niedrige Qualitätssteigerung zu sehen ist. (Die QEF ziehte in der alten Implementierung ca 20% der Performance). Das ganze System ist generisch aufgebaut so dass ich meinen Voxeln beliebige Daten geben kann, so sieht zB meine Test Voxelstruktur momentan aus: public struct Voxel : IVoxel<Voxel> { public float Iso; #region IVoxel Members public float CalcErrorValue(ref Voxel lowerDetailVoxel, float threshold) { var differentSigns = (lowerDetailVoxel.Iso - threshold <= 0) != (Iso - threshold <= 0); var multiplicator = differentSigns ? 100 : 1; return Math.Abs(lowerDetailVoxel.Iso - Iso) * multiplicator + (differentSigns ? 100 : 0); } public void ConsumeChildVoxel(ref Voxel higherDetailVoxel) { Iso += higherDetailVoxel.Iso; } public void Average(int count) { Iso /= count; } public void CopyFrom(ref Voxel lowerDetailVoxel) { Iso = lowerDetailVoxel.Iso; } public float IsoValue { get { return Iso; } } #endregion } Hier noch ein paar Screenshots eines DualContouring Resultates eines Octrees der mit 16³ Voxeln befüllt wurde mit verschiedenen LOD Stufen:
  21. Hurra mein VoxelOctree funktioniert: Vereinfachung von gleichen Voxeln, Volumenerhaltung, Fehlerquotienten, Adaptives verfeinern bis zu 5 Stufen, yay!

    1. Vorherige Kommentare anzeigen  %s mehr
    2. Mark

      Mark

      Habs mal in meinen UltraTerrain ausführlicher gepostet, mit einigen Bildern.

    3. Life Is Good

      Life Is Good

      Also ist der Sinn dahinter, alles durch diese Stücke generieren zu lassen, sodass alles ganz einfach beeinflusst werden kann(z.B. in Form von Zerstörung) ?

       

    4. Mark

      Mark

      Genau, es macht Sinn das ganze als Stücke zu halten weil dadurch die einzelnen Bereiche isoliert von anderen Bereichen betrachtet werden können was es ermöglicht diese separat an einen Thread zum bearbeiten zu geben. Es gibt noch mehr Gründe, aber der Kommentar kann hier ja nicht zu lang werden.

  22. Ich möchte ja soooo gerne bei manchen Posts im Bereich "Unbezahlte Jobs" antworten können.

    1. Vorherige Kommentare anzeigen  %s mehr
    2. reppiz
    3. Tiles

      Tiles

      Das taugt nix. Da ist das Schloss aus gutem Grund dran ;)

      Schreib demjenigen halt eine PM :)

    4. Slayer

      Slayer

      mal schauen was da steht :)

×
×
  • Neu erstellen...