XNA-Sample: MoonTaxi (Teil 2/3)

Veröffentlicht: 30 Dezember 2012 in Development, Visual Studio, XNA
Schlagwörter:
Kleiner Nachtrag vom 02.03.2015: In der Zwischenzeit gibt es auch eine kleine Video-Reihe zum Spiel. Leider nicht Deckungsgleich mit dem Code und der Vorgehensweise, aber trotzdem mit dem selben Konzept.

Dies hier ist Teil 2 einer Reihe von Blog-Posts, die ich vor Jahren zum Lernen von Abläufen innerhalb von Spielen geschrieben habe. Natürlich gibts auch Teil 1 zu lesen.

Im ersten Teil haben wir zusammen angeschaut, wie man mit Texturen umgeht, und diese auf der Szene zeichnet. Zudem haben wir für das Taxispiel ein kleines Berechnungsmodell entwickelt, indem wir auch bereits verschiedene Kräfte (Gravitation und Schubkräfte der Taxi-Düsen) berechnet und die Positionen des Taxis während der Simulation ermittelt haben. Dazu haben wir auch die Eingabe eines Users abgegriffen und für die Berechnungen verwendet.

In diesem Teil des Tutorials möchte ich gerne weitere wichtige Aspekte ansprechen. Ich würde gerne Hindernisse (Plattformen) in die aktuelle Szene einbauen und die Kollision des Taxis mit diesen Teilen berechnen.

Auch der Quellcode zu diesem Teil liegt wieder bei mir auf Skydrive zur Verfügung.

Schritt 1: Level-Modell

Das Spiel ist im Moment noch etwas spaßfrei, solange es keine Plattformen und Fahrgäste gibt, mit denen man interagieren kann. Einfach nur umher fliegen wird schnell langweilig.

Da wir im ersten Tutorial bereits festgelegt haben, dass wir mit einem festen Raster von größeren Blöcken arbeiten, sollten wir zuerst mal festlegen, wie groß dieses Raster aussieht, wie groß die Levels werden und mit welcher Blockgröße wir dann arbeiten.

Ich dachte mir, dass wir vielleicht auf einer relativ kleinen Auflösung arbeiten sollten, die auf möglichst vielen Geräten problemlos arbeiten wird. Eine Standard-Auflösung von 640 auf 480 bietet sich da an. Wenn wir mit Blocks arbeiten, die eine Auflösung von 20 auf 20 verwenden, ergeben sich ein Raster von 24 Reihen und 32 Spalten.

Die Rahmenbedingungen sollten wir auf jeden Fall per Konstanten in der Game-Klasse festhalten, damit dies auch nachträglich nochmal verändert werden kann.

public const int SCREENWIDTH = 640; 
public const int SCREENHEIGHT = 480; 
public const int BLOCKSIZE = 20;

Standard-Auflösungen lassen sich recht einfach zu Beginn des Programm-Ablaufes im Konstruktor verstecken. Dort wird in der ersten Zeile das GraphicsDevice erstellt. Kurz danach kann man die Einstellungen verändern, um entsprechende Wünsche zu äußern. Man muss hier aber beachten, dass die Einstellungen, die man hier trifft nur “Wünschäußerungen” sind. XNA prüft vorher, ob Auflösungs- oder Farbtiefe-Einstellungen von der Hardware überhaupt unterstützt werden und passt dann erst alles an.

graphics.PreferredBackBufferHeight = SCREENHEIGHT; 
graphics.PreferredBackBufferWidth = SCREENWIDTH;

Was uns aber außerdem fehlt ist eine Klasse, die in unserem Modell das Spiel-Level repräsentiert. Wir erstellen also eine Klasse Level, die genau diese Aufgabe übernimmt. Dort hinein kopieren wir folgende Felder, die ab dann dort verfügbar sein werden.

public const int ROWCOUNT = 24; 
public const int COLUMNCOUNT = 32; 

public FieldType[,] Fields { get; private set; } 
public Vector2 StartPoint { get; private set; } 
public Vector2 GuestPoint { get; private set; } 
public Vector2 GoalPoint { get; private set; }

2 Konstanten halten die Menge an Spalten und Zeilen bereit. In einem 2-dimensionalen Array wird das Raster gespeichert. Es existiert also auch ein Eintrag für leere Zellen. Um diese zu kennzeichnen, besteht der Array nicht aus normalen Flags oder Integern, sondern aus einem selbst definierten Enum, der aus einer Liste aller möglichen Blocktypen besteht. Für spätere Berechnungen werden aber parallel auch nochmal die genaue Position des Fahrgastes, des Fahrziels und der Startposition des Taxis festgehalten.

enum FieldType 
{ 
    Nothing, 
    Bridge, 
    Startpoint, 
    Guest, 
    Goal 
}

Damit lässt sich das komplette Level sehr bequem abbilden und bei den Berechnungen auch sehr schnell für Prüfungen durchlaufen. Fragt sich nur noch, woher so ein Level kommt. Am einfachsten ist es hier, erstmal auf ein vorhandenes Format zurück zu greifen. Ich habe mich hier für eine ganz normale Text-Datei entschieden, die man mit dem Notepad editieren kann. Dabei sollte jedes Zeichen eine Zelle in unserem Raster entsprechen.

Das Level zur SpielzeitDas Level im Editor

Abb. 1: Gegenüberstellung “Level-Designer” Notepad zum Spiel

Punkte repräsentieren leeren Raum, die Raute stellt eine Plattform dar, die Zahlen 1 – 3 geben die Positionen für das Taxi, den Fahrgast und das Fahrziel an. Das ist eine einfache Art einen ordentlichen Level-Editor bereit zu stellen. Leider unterstützt die Content-Pipeline von Haus aus keine Text-Dateien, weshalb wir uns einen Workaround basteln müssen. Im Solution Explorer erstellen wir deshalb einen Ordner namens “Levels” und kopieren dort per Drag&Drop die vorgefertigten Levels hinein. Du musst nur darauf achten, dass bei den Dateien in den Eigenschaften (kurz anklicken und rechts unten schauen) das Feld “Copy to output-Directory” auf “always” oder “copy if newer” steht. Diese Einstellung sorgt dafür, dass beim Erstellen des Spiels auch die Levels in das Zielverzeichnis kopiert werden, damit wir diese dann aus dem Spiel laden können.

Hinweis: Diese Herangehensweise ist zwar recht pragmatisch, entspricht aber nicht gerade dem vorbildlichsten Weg dies zu tun. Für das Laden eines solchen Levels müsste nun noch ein Content-Loader gebaut werden, damit auch die Levels gemeinsam beim Content liegen. Für dieses Tutorial habe ich allerdings darauf verzichtet – also bitte nicht zu Hause nachmachen 🙂

Die Level-Klasse braucht noch einen Platz in der Hauptanwendung und sollte noch aus der Datei geladen werden. Kopier dazu einfach mal den Quelltext aus dem Konstruktor des Democodes heraus. Dort wird im Grunde nur die Text-Datei eingelesen und der mehrdimensionale Array mit den richtigen Daten gefüllt. Dies passiert im Konstruktor der Level-Klasse und wird im Reset der Game-Klasse aufgerufen. Die Reset-Methode wird dann entsprechend angepasst, weil ja nun auch anschließend aus dem Level bekannt ist, wo denn nun wirklich der Ausgangspunkt des Taxis ist. Die Startposition kann jetzt also Level-spezifisch ermittelt werden. Man darf aber nicht vergessen, dass die Zellen und Positionsangaben im Level in Zellen angegeben wird. Die Position des Taxis sind aber Pixel! Übernimmt man die Startposition aus dem Level, muss da auch noch die Block-Größe hinzu multipliziert werden, damit die Position auch wieder stimmt.

level = new Level(@"Levels\level1.txt"); 
taxi.Position = level.StartPoint * BLOCKSIZE; 
taxi.Speed = new Vector2();

Startet man das Spiel, so startet zwar das Taxi an der richtigen Stelle, vom Level zu sehen ist aber noch nichts.

Schritt 2: Das Level zeichnen

Alles ist geladen, als könnte man jetzt doch auch das Level ordentlich zeichnen. Richtig! das machen wir jetzt auch. Was wir zum zeichnen als allererstes brauchen sind Texturen. Eine für die Plattform-Blocks (block.png), eine für den Fahrgast (guest.png) und natürlich auch eine für das Fahrziel (goal.png). Alle 3 befinden sich bereits im fertigen Projekt und können direkt verwendet werden.

Auch diese Texturen brauchen Variablen vom Typ Texture2D in der Game-Klasse und müssen bei LoadContent geladen werden. Wie das funktioniert, weißt du ja schon aus dem ersten Tutorial.

Nun wird gezeichnet! Das passiert wieder in Draw. Blöcke zeichnen wir vor dem Taxi, damit das auch ganz sicher im Vordergrund ist. Wir schreiben unseren folgenden Code also zwischen spriteBatch.Begin() und unsere Zeile die das Taxi malt.

Damit wir alle Blöcke des Levels auch ordentlich zeichnen, durchlaufen wir die Zellen Zeilenweise und Zelle für Zelle. Je nach Inhalt der Zelle malen wir die entsprechende Textur auf den Bildschirm und an die richtige Stelle.

// Blocks zeichnen 
for (int y = 0; y < level.Fields.GetLength(1); y++) 
{ 
    for (int x = 0; x < level.Fields.GetLength(0); x++) 
    { 
        switch (level.Fields[x, y]) 
        { 
            case FieldType.Bridge: 
                spriteBatch.Draw( 
                    blockTexture, 
                    new Vector2(x * BLOCKSIZE, y * BLOCKSIZE), 
                    Color.White); 
                break; 
            case FieldType.Guest: 
                spriteBatch.Draw( 
                    guestTexture, 
                    new Vector2(x * BLOCKSIZE, y * BLOCKSIZE), 
                    Color.White); 
                break; 
            case FieldType.Goal: 
                spriteBatch.Draw( 
                    goalTexture, 
                    new Vector2(x * BLOCKSIZE, y * BLOCKSIZE), 
                    Color.White); 
                break; 
        } 
    } 
}

Im Grunde nichts Neues. In zwei verschachtelten Schleifen (eine über die Reihen, die zweite über die einzelnen Spalten einer Reihe) werden alle Zellen durchlaufen und je nach Inhalt entweder ein Block, ein Gast, das Ziel oder garnichts gezeichnet, wenn die Zelle leer ist. Wichtig ist hier auch unbedingt auf den Zellen-Index (x und y) wieder die Block-Größe zu multiplizieren, damit die Textur auch an der richtigen Stelle am Bildschirm gezeichnet wird.

Wenn du das Projekt jetzt startest, kannst du schon das Level erkennen und der Fahrgast wartet auch schon auf dich. Leider interagiert das Taxi bisher nicht mit diesem Level. Man kann einfach durch die Blocks hindurch fliegen und dem Fahrgast ist das Taxi bislang auch noch egal. Das wird sich aber gleich ändern.

Schritt 3: Einfache Kollision

Als erstes wollen wir verhindern, dass das Taxi weiterhin aus dem Bildschirmrand fliegen kann. Wenn der Spieler an den Rand fliegt, soll es zu einer Kollision kommen und das Spiel soll von vorne beginnen.

Diese Art der Kollision ist besonders leicht zu ermitteln, weil man in diesem Fall, dank des sauberen Rechtecks des Fensters, beide Achsen problemlos getrennt betrachten kann und es genau genommen um eine Grenzüberschreitung auf einem eindimensionalen Zahlenstrahl handelt. Aber zum Aufwärmen kommt uns diese Berechnung gerade recht.

Koordinaten und Grenzen

Abb. 2: Taxi im Koordinatensystem bei der Grenzüberschreitung

Die Kollision mit der linken Wand ist besonders einfach. Sobald nämlich die Position des Taxis kleiner 0 ist, befindet sich das Taxi ganz klar mit der Spitze bereits in der Wand und ist kollidiert. Das Gleiche auch mit der oberen Kante. Befindet sich das Taxi im negativen Koordinatenbereich der Y-Achse, so ist das Taxi nach oben aus dem Bildschirm geflogen.

Ein kleines bisschen komplizierter wird es mit den anderen beiden Bildschirm-Kanten. Dort kommt dann noch die Größe des Taxis (es darf ja nicht mehr die linke, obere Ecke als Referenz genommen werden) und die Größe des Anzeigefensters ins Spiel. Dafür bekommt die Taxi-Klasse zwei weitere Konstanten, die für diese Prüfung verwendet werden können: WIDTH und HEIGHT vom Typ Integer und mit den Ausmaßen der Taxi-Textur (50/25). Kollidiert das Taxi, so wird einfach die Reset-Methode aufgerufen und das Spiel beginnt von vorne.

// Randkollision 
if (taxi.Position.X < 0) 
    Reset(); 
if (taxi.Position.X > SCREENWIDTH - Taxi.WIDTH) 
    Reset(); 
if (taxi.Position.Y < 0) 
    Reset(); 
if (taxi.Position.Y > SCREENHEIGHT - Taxi.HEIGHT) 
    Reset();

Das verhindert nun den Ausflug des Taxis außerhalb des Spielfensters. Sollte das doch passieren, startet das Spiel automatisch von vorne.

Schritt 4: Komplexere Kollisionen

Etwas komplizierter werden die Kollisionen mit den Blocks. Diese befinden sich ja überall auf der Spielfläche, können von allen Seiten und über mehrere Achsen angeflogen werden. Ihre Form (rechteckig) macht eine Kollisionsprüfung aber dann doch noch relativ einfach. Um genau zu sein hilft uns da eine Konstruktion aus dem XNA-Framework: Das Rectangle. Dieses Struct verfügt über eine Methode „Intersect”, die eine Kollisionsprüfung sehr bequem erlaubt und uns keinen weiteren Ärger macht.

Wir müssen jetzt nur noch jeden Block auf die Kollision mit dem Taxi prüfen. In dieser Beispielanwendung mache ich das recht naiv, indem ich wieder über alle Blöcke laufe und auf Kollision prüfe.

Kleiner Hinweis: Diese Herangehensweise an die Kollisionsprüfung ist extrem naiv und prüft im Grunde nur jedes Objekt mit jedem anderen Objekt. Das können wir uns in diesem Fall leisten, weil es im Spiel nur relativ wenig Spielobjekte gibt und sich davon auch nur ein einziges Objekt bewegt. Man muss also nur ein Objekt gegen alle anderen Objekte prüfen. Der Rechenaufwand steigt aber quadratisch mit jedem weiteren beweglichen Objekt das zum Spiel hinzugefügt wird und bringt den Rechner schnell an die Leistungslimits. Es gibt für solche Situationen etwas intelligentere Ansätze (Stichwort “Quad trees” oder “Oct trees”) die vorher grob ermitteln, welche Objekte überhaupt für Kollisionen in Frage kommen und erst dann exakt prüfen.

// Block-Kollision 
Rectangle taxiCollision = new Rectangle( 
    (int)taxi.Position.X, (int)taxi.Position.Y, 
    Taxi.WIDTH, Taxi.HEIGHT); 
for (int y = 0; y < level.Fields.GetLength(1); y++) 
{ 
    for (int x = 0; x < level.Fields.GetLength(0); x++) 
    { 
        // Skip Non-Blocks 
        if (level.Fields[x, y] != FieldType.Bridge) 
            continue;

        Rectangle blockCollision = new Rectangle( 
            x * BLOCKSIZE, y * BLOCKSIZE, BLOCKSIZE, BLOCKSIZE); 
        if (blockCollision.Intersects(taxiCollision)) 
        { 
            // Kollision! 
            Reset(); 
        } 
    } 
}

Dieser Code folgt einfach in der Update-Methode direkt unter dem Code für die Kollision mit der Wand. Erst mal interessiert uns nur die tatsächliche Kollision mit dem Block. Später ist uns auch noch wichtig von welcher Seite man den Block berührt (Thema Taxi landen) und mit welcher Geschwindigkeit das passiert. Jetzt wird es schon etwas schwieriger sich im Level zurecht zu finden und nicht gegen irgendwelche Hindernisse zu stoßen.

Schritt 5: Das Taxi Landen

Wir können fliegen, wir können kollidieren und unser Fahrgast winkt und schon ungeduldig zu. Leider können wir das Taxi noch nicht landen. Das soll sich nun ändern.

Fassen wir mal zusammen, was so einen Landevorgang ausmacht: Das Taxi landet dann, wenn es von oben auf die Plattform herunter sinkt. Das Taxi darf also doch mit der Plattform kollidieren, sobald bestimmte Bedingungen erfüllt sind. Eine weitere Bedingung wäre zum Beispiel, dass die Geschwindigkeit des Taxis zum Kollisionszeitpunkt ein bestimmtes Limit nicht überschreitet. Was heißt das für den Code? Wir müssen bei einer Kollision jedes mal noch zusätzlich prüfen, von welcher Seite das Taxi an die Plattform gestoßen ist. Vorher sollten wir allerdings noch die etwas einfachere Prüfung auf die Geschwindigkeit durchführen.

In Tutorial 1 hatten wir ja schon festgestellt, dass die Geschwindigkeit in 2 verschiedenen Achsen angegeben wird. x und y. Wenn wir aber von der Geschwindigkeit eines Taxis sprechen, brauchen wir die länge des 2-dimensionalen Geschwindigkeitsvektor, den wir vorhin noch getrennt behandelt haben. Wir haben aber Glück. Die beiden Achsen des Koordinaten-Systems zeichnet aus, dass sie orthogonal, im rechten Winkel zueinander stehen. Dadurch können wir den Satz des Phytagoras anwenden. Er besagt, dass das Quadrat aus einem Schenkel eines rechtwinkligen Dreiecks plus das Quadrat des anderen Schenkels das Quadrat der Hypotenuse, also der längsten Kante gegenüber dem rechten Winkel, ergibt. a²+b² = c². Sicher schon mal in Mathe gehört.

Geschwindigkeiten

Abb. 3: Geschwindigkeiten und Phytagoras.

Wir müssen das aber nicht von Hand ausrechnen. Dafür stellt uns Vector2 eine Methode “Length” zur Verfügung, die das schon für uns übernimmt. Ich schreibe das nur deshalb ausführlich hin, um auf eine kleine Performance-Falle hinzuweisen. Die Formel enthält ein c². Das bedeutet, dass bei der Berechnung von c eine Wurzel gezogen werden muss. Dies kostet den Rechner wesentlich mehr Rechenzeit, als das Quadrat zu berechnen. Manchmal ist die Berechnung von c unvermeidlich, da man die exakte Geschwindigkeit des Taxis ermitteln will. Will man allerdings nur Vergleiche zwischen zwei Vektoren machen, so reicht es aus, die Quadrate der beiden Längen zu vergleichen.

In unserem konkreten Beispiel können wir jetzt, anstatt die Wurzel aus c², der Länge unseres Geschwindigkeitsvektors, zu ziehen einfach die Richtgeschwindigkeit quadrieren und anschließend mit c² anstatt mit c zu vergleichen. Das Ergebnis wäre das Selbe. Vektor liefert uns das Quadrat von c übrigens mit der Methode “LengthSquared”.

Fügen wir am besten noch die Konstante, für die maximale Geschwindigkeit mit der gelandet werden kann, zu unseren Konstanten hinzu

public const float MAXLANDINGSPEED = 20f;

und bauen dann noch eine zusätzliche Prüfung in die Blockschleife ein, die wir im vorherigen Kapitel in die Update-Methode eingefügt haben. Aus einem einfachen, einsamen Reset() wird nun eine etwas umfangreichere Prüfung, weil ja auch eine Landung möglich sein kann. An der Stelle des TODOs müssen wir jetzt aber noch rausfinden, von welcher Seite wir an den Block stoßen. Von oben wär ok.

// Auf Landungsbedingungen prüfen 
if (taxi.Speed.LengthSquared() < MAXLANDINGSPEED * MAXLANDINGSPEED) 
{ 
    // Chance auf Landevorgang 
    // TODO: Kollisionsrichtung prüfen 
} 
else 
{ 
    // Kollision! 
    Reset(); 
}

Aber wie machen wir das? Die Kollision verrät uns ja nur, DASS wir kollidieren, aber nicht so recht von wo. Ein Beispiel dazu zu machen ist hier vielleicht sinnvoll. Schauen wir uns das erste Bildchen in Abbildung 4 mal an. Im ersten Frame ist noch alles ok, im zweiten Frame kollidiert das Taxi dann mit dem Block. Um jetzt zu sehen von welcher Seite das passiert, betrachtet man einfach die beiden Geschwindigkeitsachsen getrennt voneinander. das zweite Bild wendet nur die Bewegung in X-Richtung an –> keine Kollision. Das bedeutet, dass das Taxi in diesem Beispiel von der Y-Achse aus (von oben oder unten) an den Block gedonnert ist. Aus welcher Richtung das Taxi angeschossen kam lässt sich leicht am Vorzeichen des Y-Anteils der Geschwindigkeit auslesen.

Kollisionen mit einem Block

Abb. 4: Kollision mit einem Block, aufgesplittet in X- und Y-Achse.

Für diesen speziellen Fall erzeugen wir eine neue Kollisionsbox für das Taxi, bei dem der X-Anteil nicht berücksichtigt wurde. Dazu müssen wir die X-Position des Taxis wieder auf den vorherigen Wert zurück rechnen oder gleich bei der Positionsberechnung weiter oben die alten Werte zwischen speichern.

// Chance auf Landevorgang 
float oldPosX = taxi.Position.X - 
    taxi.Speed.X * (float)gameTime.ElapsedGameTime.TotalSeconds;

Rectangle taxiWithoutXCollision = new Rectangle( 
     (int)oldPosX, (int)taxi.Position.Y, 
    Taxi.WIDTH, Taxi.HEIGHT);

if (taxiWithoutXCollision.Intersects(blockCollision) && ) 
{ 
    // Landeanflug 
    // TODO: Landung durchführen 
} 
else 
{ 
    // Kollision von der Seite 
    Reset(); 
}

Großartig! Wir wissen jetzt also wann das Taxi zur Landung ansetzt. Letzt müssen wir das nur noch in das Spiel einbauen. Landen ist aber nicht ganz unkompliziert. Solange das Taxi gelandet ist, soll nämlich keine Gravitation wirken und auch die Steuerdüsen links und rechts sollen keine Wirkung haben. Abheben kann das Taxi hier nur noch mit der Steuerdüse unten. Da dies Einfluss auf das Verhalten des Taxis hat, speichern wir das auch in der Taxi-Klasse. dort kannst du ein zusätzliches Feld “public bool Landed { get; set; }” ein.

Das muss jetzt natürlich an vielen Stellen berücksichtigt werden.

  • In der Reset-Klasse muss der Wert wieder auf false gesetzt werden.
  • Gravitationsberechnung in der Update-Methode muss unter der Bedingung passieren, dass das Taxi nicht gelandet wurde. (If-Bedingung vor die Berechnung einfügen)
  • Das selbe gilt für die vertikalen Antriebsdüsen
  • Wir brauchen eine Landelogik an der Stelle, an der wir noch das TODO-Kommentar haben.
  • Im Gegenzug brauchen wir auch eine Abhebelogik.

In dem Moment, wo wir feststellen, dass das Taxi landet, setzen wir den Landed-Flag des Taxis auf true. Außerdem setzen wir die Geschwindigkeit des Taxis auf 0 und platzieren es exakt am Landeplatz, damit es dort stabil stehen bleibt. Wir nehmen dazu einfach die Y-Position des Blocks und setzen das Taxi genau auf die richtige Höhe. Das machen wir, weil durch die dynamische Berechnung der Position das Taxi gerade mitten im Block steht, wir aber obenauf landen wollen. Um nun Seiteneffekte durch die Kollision auszuschließen die in späteren Runden passieren können, platzieren wir das Taxi exakt auf dem Block.

// Landeanflug 
taxiCollision.Y = blockCollision.Y - Taxi.HEIGHT; 
taxi.Position = new Vector2( 
    taxi.Position.X, blockCollision.Y - Taxi.HEIGHT); 
taxi.Speed = new Vector2(); 
taxi.Landed = true;

Um wieder starten zu können implementieren wir am besten direkt unter der Verarbeitung der Usereingaben auch gleich eine Sektion, die den Start des Schiffes auch wieder erlaubt. Ich schlage die Position direkt unter der Deklaration der delta-Variablen vor.

// Abhebesequenz 
if (taxi.Landed) 
{ 
    if (forceY > 0) 
        taxi.Landed = false; 
}

Wir prüfen hier einfach nur, sobald das Taxi gelandet ist, ob das Triebwerk unten gezündet wurde und geben damit das Taxi wieder frei.

Nach diesen Änderungen sollte es nun möglich sein frei im Level herum zu fliegen, mit den Wänden und den Plattformen zu kollidieren und bei angepasster Geschwindigkeit auf ihnen zu landen.

Source zum Tutorial & Weiter zu Teil 3

Kommentare
  1. […] Source zum Tutorial & weiter zu Teil 2 […]

  2. m4mm4g0 sagt:

    Ich weiß, dieses Tutorial ist schon etwsa älter, aber ich bekomme hier den Fehler:

    Fehler 1 Inkonsistenter Zugriff: Eigenschaftentyp ‚FirtGame.Level.FieldType[*,*]‘ ist weniger zugreifbar als Eigenschaft ‚FirtGame.Level.Fields‘ c:\users\pan\documents\visual studio 2013\Projects\FirtGame\FirtGame\FirtGame\Level.cs 88 29 FirtGame

    Ich habe allerdings alels so gemacht wie es im Tutorial stand, sogar deinen Quelltext kopiert…

  3. Axel sagt:

    Danke für dein Tutorial – hat Spaß gemacht und sehr verständlich!

  4. […] Blog-Einträgen hatte ich damals detailiert beschrieben, wie das Spiel funktionert. (Teil 1, Teil 2, Teil 3). Damals noch mit XNA. Letzte Woche hatte ich aber mal spontan Lust das Ganze nochmal […]

Hinterlasse einen Kommentar