in

AntMe! - Die Ameisensimulation

AntMe! 2.0 Blog

Gedanken zur neuen AntMe! Version.

November 2007 - Einträge

  • Statische Generische Klassen und Bastis Tutorial

    Vor ein paar Tagen hat Basti mir eine Ameisenklasse geschickt, die er von jemandem bekommen hat, die einen Bug in AntMe! demonstriert. Für solche Einsendungen sind wir immer dankbar, denn genau wie bei der Rechtschreibung gilt bei der Programmierung auch, dass man seine eigenen Fehler meistens nicht selbst findet. Nach Bastis E-Mail habe ich noch eine E-Mail von jemand anderem bekommen, der den selben Bug demonstriert hat. Es handelt sich hierbei um eine neue Version des Bug-Bugs durch die man einen Käfer mehrfach töten und damit sehr viele Punkte sammeln konnte. Das ist jetzt natürlich behoben. Wenn Käfer tot, dann Käfer tot. Nochmal draufhauen bringt nichts.

    Die zweiten Person hat mir dann noch eine andere Ameisenklasse geschickt, durch die ich einen anderen lange gesuchten Bug gefunden habe. Vielen Dank Philipp! Dank dir bewegen sich Äpfel nun nicht mehr von alleine und in die falsche Richtung.

    Wie auch immer. Mir war an Philipps Ameisenvolk eine Hilfklasse aufgefallen, die mir ein wenig seltsam erschien und in dem anderen Volk fand ich selbige Klasse auch wieder. Ich kam dann darauf: beide haben den KoordinatenSpeicher aus Bastis Globales Gedächtnis Tutorial verwendet. Das habe ich, weil ich was Ameisen angeht ein Fan von überhaupt-kein-Gedächtnis-und-schon-gar-nicht-global bin, zugegebenermaßen nie gelesen. Inzwischen habe ich das nachgeholt und das Tutorial ist (wie erwartet) richtig gut, aber eine kleine Sache darin ist sicher nicht im Sinne des Autors und damit kommen wir endlich zum Titel dieses Blog Eintrags.

    Basti schreibt

    Kommen wir also schon zu unserem ersten Schritt. Wir erstellen uns eine neue Klasse mit dem Namen „KoordinatenSpeicher“. Diese Klasse machen wir statisch und erstellen uns dort drin 3 Collections.

    public static class KoordinatenSpeicher<K,V> {
        private static Dictionary<K, V> zuckerKoordinaten = new Dictionary<K, V>();
        private static Dictionary<K, V> obstKoordinaten = new Dictionary<K, V>();
        private static Collection<int> geloeschteObjekte = new Collection<int>();
        // Properties...
    }

    Jetzt Fragt ihr euch bestimmt was hat der Typ da gemacht. Ich erkläre es euch. Also wir ihr seht haben wir jetzt 3 Collections erstellt. Die erste Collection „zuckerKoordinaten“ ist dafür bestimmt alle Zucker Koordinaten abzuspeichern. Die 2te Collection „obstKoordinaten“ ist für die Obst Koordinaten zuständig. Dann seht ihr hier noch, dass ich hier generische Typen benutzt habe und zwar K und V. K steht in diesem Falle für den Typen des Keys und V für den Typen des Values. Als Key nehmen wir im weiteren Tutorial immer die ID des Obstes oder Zuckers und also Value immer das Obst oder den Zucker selbst, aber dazu später mehr. Als drittes haben wir noch die „geloeschteObjekte“, hier werden alle IDs der Obst oder Zuckerobjekte gespeichert, die schon gelöscht wurden, auch dazu später mehr. Das war dann schon erst einmal alles was wir für unseren Speicher benötigen um ihn zu benutzen.

    Gestört hat mich daran der generische Typparameter <K,V> an der statischen Klasse KoordinatenSpeicher. Der Typparameter K für die Dictionary Instanzen ist im Tutorial immer int, bei der Collection Instanz steht aber nicht K sondern direkt int. Das ist meiner Meinung nach ein wenig inkonsequent. Der Zugriff auf den Speicher erfolgt unterschiedlich, je nachdem auf welche der internen Listen zugegriffen wird.

    KoordinatenSpeicher<int, Zucker>.zuckerKoordinaten.Add(zucker.Id, zucker);
    KoordinatenSpeicher<int, Obst>.obstKoordinaten.Add(obst.Id, obst);

    Was nun passiert ist nämlich folgendes: der Compiler merkt sich, mit welchen Typparametern auf den KoordinatenSpeicher zugegriffen wird und erzeugt für jede Parameterkombination eine neue "Instanz" der statischen Klasse. Am Ende gibt es also nicht nur einen, sondern viele KoordinatenSpeicher, jeder mit eigenen Versionen der internen Listen. Freilich wird im Tutorial in jedem Speicher nur auf die Liste zugegriffen, die den passenden Namen hat. Aber folgendes ist trotzdem möglich:

    KoordinatenSpeicher<int, Zucker>.obstKoordinaten.Add(zucker.Id, zucker);
    KoordinatenSpeicher<int, Obst>.zuckerKoordinaten.Add(obst.Id, obst);

    Das ganze ist nicht schlimm. Es werden einfach nur ein paar Listen erzeugt, die nie verwendet werden. Und es hat mich einmal zum Nachdenken angeregt und das kann nie schaden. Damit auch Anders Hejlsberg (der Erfinder von C#) wieder gut schlafen kann, schlage ich vor, die Klasse auf eine der folgenden Arten abzuändern. Ich favorisiere stark die zweite Methode, da dort einfach ersichtlicher ist was passiert:

    public static class KoordinatenSpeicher<K,V> {
        private static Dictionary<K, V> koordinaten = new Dictionary<K, V>();
        private static Collection<K> geloeschteObjekte = new Collection<K>();
        // Properties...
    }

    public static class KoordinatenSpeicher {
        private static Dictionary<int, Zucker> zuckerKoordinaten = new Dictionary<int, Zucker>();
        private static Dictionary<int, Obst> obstKoordinaten = new Dictionary<int, Obst>();
        private static Collection<int> geloeschteObjekte = new Collection<int>();
        // Properties...
    }

    Was haben wir nun von der ganzen Sache? Zwei Bugs weniger und etwas dabei gelernt :-) Im Anhang zu diesem Blog Eintrag findet ihr ein kleines Demo Projekt, das die Sache anschaulich demonstriert.

    There is no theory of evolution. Just a list of creatures Chuck Norris has allowed to live.

  • XNA und Windows Forms

    Im XNA Team Blog stand heute die lang erwartete Meldung, dass die Beta von XNA Game Studio 2.0 bald verfügbar sein wird. Leider stand da auch folgendes:

    Unfortunately, there were a few things that didn't make it into this release. The most prominent being that you cannot host XNA applications in Windows Forms.

    Den kompletten Beitrag gibt es hier: http://blogs.msdn.com/xna/archive/2007/11/15/xna-game-studio-2-0-beta-available-soon.aspx

    Gerade auf dieses Feature habe ich gewartet. Das hätte die Entwicklung von XNA Visualisierungen um so vieles eleganter gemacht. Eine rudimentäre 3D Visualisierung gibt es schon länger. Die zeigt bisher nur das Spielfeld und Ameisen an, dafür ist die Kamera voll beweglich. In Vorbereitung auf einen XNA Workshop den ich nächste Woche halte habe ich gestern abend eine 2D Visualisierung gebaut und die 3D Version baue ich dafür eventuell auch noch ein wenig aus. Dabei legt XNA mir folgende Steine in den Weg:

    • Ein Microsoft.Framework.Xna.Game hat eine eigene Ereigniswarteschleife, die über die Run() Methode synchron ausgeführt wird. Für reine XNA Spiele, die als Hauptfenster laufen, macht das Sinn. Für AntMe! wäre es aber besser ich könnte eine eigene Schleife bauen und die XNA Ereignisbehandlung (Grafik-Hardware!) darin selbst anstoßen.
    • Sämtliche Zugriffe auf die Game Instanz dürfen nur aus dem Thread heraus erfolgen, der die Instanz erzeugt hat. Eine Invoke() Methode wie bei Windows Forms gibt es nicht.
    • Die Game Instanz verweist auf eine GameWindow Instanz. Der fehlen sogar auf dem PC so wichtige Methoden wie Show() und Hide(). Außerdem wird der Mauscursor ausgeblendet.

    Aber man ist ja nicht auf den Kopf gefallen und weiss sich weiterzuhelfen. Inzwischen bin ich so weit, dass ich XNA zwar nicht als Control in eine bestehende Windows Forms Anwendung einbinden kann, aber beide Techniken in einem Fenster, das nicht das Hauptfenster ist, nebeneinander laufen lasse.

    • Das Erzeugen der Game Instanz und der Aufruf der Run() Methode passieren in einer statischen Methode die von einer anderen statischen Methode in einem neuen Thread gestartet und selbst wiederrum von IPlugin.Start() aufgerufen wird.
    • Über Mouse.GetState() können die Mauskoordinaten ausgelesen werden. Den Cursor zeichne ich einfach selbst als Sprite.
    • Die GameWindow Instanz namens Window liefert ein Handle, das benutzt werden kann um eine Windows Forms Form Instanz für das Spielfenster zu ergeugen.
    • Auf dieser Instanz kann dann endlich auch Show() und Hide() aufgerufen werden. So kann das XNA Spiel über mehrere AntMe! Spiele hinweg verwendet und dazwischen nur versteckt werden.
    • Außerdem können der Form Instanz wie gewohnt weitere Kontrollelemente hinzugefügt werden. Diese werden über den XNA Hintergrund gezeichnet. Der Bereich in den XNA zeichnet kann über graphics.GraphicsDevice.Viewport festgelegt werden Auf diese Weise kann dann auch eine XNA Visualisierung die aus der alten 2D-Visualisierung gewohnte Seitenleiste bekommen.

    Hier der Code um an das Fenster zu kommen:

    Form form = (Form)Form.FromHandle(game.Window.Handle);

    Und hier der Code um das Fenster aus einem anderen Thread heraus zu verstecken:

    if (form != null && form.IsHandleCreated)
        form.Invoke(new ThreadStart(form.Hide));

    So, dann werde ich mal meinen Workshop weiter vorbereiten. Viel Spaß mit XNA!

    The chief export of Chuck Norris is pain.

  • Schon wieder Listen in .NET und Profiling

    Ich habe in der MSDN Library eine sehr schöne englische Artikelreihe über Listen in .NET gefunden. Dort wird unter anderem auch bestätigt, was ich mir über die Funktionsweise der Klassen ArrayList und List<T> schon gedacht habe. Die Artikel finden sich hier:

    http://msdn2.microsoft.com/en-us/library/aa287104(VS.71).aspx

    Außerdem habe ich mich ein wenig mit Profiling beschäftigt. Jeder Profi wird mir wahrscheinlich gleich auf den Hinterkopf hauen wollen, wenn ich das sage (Denkvermögen anregen und so), aber wir haben AntMe! bisher nie durch einen Profiler gejagt. Ein Profiler ist ein Programm, das kurz gesagt misst, wie oft jede Methode in einem Programm aufgerufen wird, wie lange das dauert und wie viel Speicher dabei verbraucht wird. Ich werde demnächst einen ausführlichen Blog Eintrag über das Profiling schreiben. Jetzt möchte ich aber bei den Listen bleiben und gebe lediglich folgendes bekannt:

    In einem Spiel mit den Demo-A-Meisen wird der Befehl List<T>.Clear() knapp 200 Millionen mal aufgerufen und dessen Ausführung verschlingt über 38% der gesamten Laufzeit des Spiels. Wird zeit die Listen zu optimieren. Schnell.

    Beim Leeren der Listen wird durch die komplette Liste iteriert und alle Elemente werden auf null gesetzt. Das dauert seite Zeit. ich denke darüber nach meine Group<T> Klasse so zu ändern, dass das unterlassen wird. Damit bleiben die Elemente in der Liste bis sie überschrieben werden. Das wirkt sich dann positiv auf die Geschwindigkeit aber negativ auf den Speicherverbrauch aus. Ich werde damit ein wenig herumspielen und die Ergebnisse dann hier bekannt geben.

    Chuck Norris counted to infinity - twice.

  • Codename "Chuck"

    Der Codename für AntMe! 2.0 ist ab sofort "Chuck", benannt nach den Chuck Norris Facts.

    Outer space only exists because it's afraid to be on the same planet with Chuck Norris.

  • Nocheinmal Listen in .NET

    Ich habe den Test aus dem letzten Blogbeitrag gestern einmal mit Visual C# 2008 und LINQ durchgeführt. Der verwendete Code sieht für die vorgeschlagene Group<T>-Klasse etwa folgendermaßen aus:

    var query = from item in list where item != null select item;
    foreach (Item item in query)
        item.Value--;

    Für die generische Liste und die verkettete Liste fällt natürlich die where-Klausel weg, aber hier ist der Zwischenschritt über LINQ auch sinnlos. Die ArrayList unterstützt LINQ nicht. Auf jeden Fall war die foreach-Iteration über die LINQ-Abfrage in jedem Fall um den Faktor drei bis fünf langsamer als die foreach-Iteration direkt über die Liste. Das bedeutet nun nicht, daß LINQ keine Daseinsberechtigung hätte, es bedeutet nur, daß ich mir ein anderes Projekt ausdenken muß, um einmal damit herumspielen zu können ;-)

© 2008 AntMe Limited | Impressum
© 2007 Microsoft Deutschland GmbH