Nach langer Zeit ist die Engine von Argh! Earthlings! nun endlich ausgetauscht. Das Resultat ist eine deutlich reduzierte CPU Belastung und deutlich geringerer Speicherverbrauch.
Alte Engine
Die Optimierung in Argh! Earthlings! war dringend nötig, um die Zulassung im App Store nicht zu gefährden. Zuletzt war das Spiel zwar flüssig spielbar, aber die Leistungswerte waren im Vergleich zu den Anforderungen sehr schlecht. So verbrauchte Argh! Earthlings! im Ruhemodus 60% der CPU und nach einem kompletten Durchlauf durch alle Szenen nicht weniger als 1,5 GB RAM... Die CPU war nach ca. 30 Minuten Spielzeit heißgelaufen. Das wiederum reduzierte die Batteriekapazitätet und damit auch die Spieldauer... Beides unerwünschte Nebeneffekte.
Also war es an der Zeit sich von der zuvor verwendeten Technik zu verabschieden und zu einer neuen Art und Weise der Darstellung zu wechseln.
Zu Beginn verwendete Argh! Earthlings! ein "Fullscreen Repaint", basierend auf Open GL und entsprechenden Texturen. Das Bild wurde innerhalb einer Sekunde 60 mal neu gezeichnet, egal ob es nötig war oder nicht. Die Framerate hatte ich stets im Blick und sie sah selbst auf einem betuchten iPhone 4S sehr gut aus, aber die anderen Werte gingen vollkommen an mir vorbei. Vor allem die Batterie und die Hitze machten mir zum Schluß richtig sorgen.
Neue Engine
Um das Problem in den Griff zu bekommen musste ich irgendwie auf die Fullscreen Repaints verzichten. Dazu mussten alle Elemente der Szene in Objekte verwaltet werden, anstatt in Sprites, die nur auf eine Oberfläche gemalt werden. Um dieses Problem besser zu beherrschen, entschied ich mich die Basis-Engine in ein Demo auszugeliedern. Inzwischen ist das Demo zu einem ganzen Parallelprojekt gewachsen, was auch erklärt, warum die Änderungen an Argh! Earthlings! auch so lange gedauert haben.
Die neue Engine verwendet nun UIImageView-Komponenten als Träger für die Darstellung aller Elemente, wie Arme und Beine, die auf einem dynamischen Skelett aufgehängt sind. Das führt dazu, dass die UIImageView Komponenten nur noch einmal in die Szenen gemalt und nicht mehr verändert werden. Auch wenn sich die Figur bewegt, ändern sich die einzelnen Teile der Figur nicht. Erst wenn es nötig ist die Glieder der Figur zu verändern, wird das Skelett rekursiv neu gebaut. Die Verwaltung hierfür ist zwar sehr aufwändig, erlaubt aber viele Erweiterungen, wie Drehungen einzelner Körperteile oder des gesamten Skeletts.
Das Ergebnis ist nun, dass nun nicht die gesamte Szene neu gezeichnet werden muss, sondern nur ein paar kleinere Objekte, und dies auch nur, wenn sie sich wirklich ändern. Verschiebungen und Drehungen erfordern nicht einmal mehr ein erneutes Laden der Grafiken. Bewegt sich nun im Spiel nichts, so wird auch keine Last auf der CPU erzeugt. Das spart Batterieleistung und erlaubt deutlich längere Spielzeit. Wirklich sehr effektiv.
Fazit
In einer Demo, in der fast die Hälfte aller Szenen betreten werden und der Spieler mit der Hälfte der Elemente interagiert (Walkthrough) ergaben sich in etwa folgende Durchschnittswerte:
Vor der Änderung: Nach der Änderung: = Optimierung:
1,5 GB RAM 0,2 GB RAM = 1/7 Speicherverbrauch
60% CPU im Ruhemodus 1% CPU im Ruhemodus = 1/60 CPU Bedarf in Ruhe
90% CPU im Spiel 30% CPU im Spiel* = 1/3 CPU Bedarf unter Last
(schwankt stark zwischen 10% - 50%)
Damit sollte die Gefahr, nicht in den App Store zugelassen zu werden, deutlich reduziert worden sein. Eine Klassifikation als "Taschenwärmer" kann ich mir ersparen...
Lessons Learned
- Früh und oft prüfen: Die Änderung der Engine war langwierig. Niemals zu spät Parameter checken.
- Port isolieren: Engine in einem alternativen Projekt auslagern und dort ausarbeiten.
- Mehrere Projekte synchron halten: Bestehen mehrere Projekte aus einer gemeinsamen Komponente, immer synchron arbeiten und öfter mal gegenprüfen, ob noch alles läuft. Glücklich ist derjenige, der eine CI Pipeline vorweisen kann.
- Fokus behalten: Nicht mehr Features einbauen, als für den Port nötig ist. Ansonsten raubt das Feature-Fieber die meisten Entwicklungsressourcen.
- Ausreichend Unittests sammeln: Unittests stabilisieren den gesamten Port und lassen sich in jedem Projekt weiter entwicklen. Dadurch profitieren alle abhängigen Projekte.