115 kb Spielentwicklung - Hacks, Bugs und Ärger


Anfang November nahm ich am 115. IGDC-Community-Wettbewerb (Independent Games Developers Contest) teil, bei dem es darum ging, einen Arcade-Shooter mit einem Limit von 115 Kilobyte pro Woche zu entwickeln. Unter dem Strich experimentiert die Geschichte der Entwicklung eines Spiels auf OpenGL + Free Pascal mit LZO, umgeht Bugs des FPC- Compilers für uFMOD, die einfachste Generation von Texturen und einen nervigen Bug auf NVidia-Grafikkarten, der alles ruiniert.

Video, eine Binärdatei für Windows und Quellcode sind ebenfalls enthalten - siehe Ende des Artikels.

Lyrische Einführung


Spieleentwicklung ist mein Haupt-, Lieblings- und sehr altes Hobby. Ich habe in einem Amateur-Spielev ohne viel Erfolg, hochkarätige Veröffentlichungen und titanische Langzeitkonstruktion für etwa 10 Jahre gesponnen. Es werden viele eingefrorene Projekte in den Sinn gebracht - Einheiten. Irgendwann verzweifelte ich, dass nichts von mir kam. Und dann wurde mir klar, dass mir nicht nur das Endergebnis gefällt, sondern auch der Spielentwicklungsprozess. Von diesem Moment an wurde das Leben ruhiger, aber ich lasse den Gedanken nicht los, dass ich mich eines Tages selbst überwältigen und eine Art Projekt auf die kommerzielle Ebene bringen werde.

Irgendwann bin ich auf die IGDC- Community gestoßen,In kurzen Wettbewerben (von ein paar Tagen bis zu drei Wochen) werden Spiele zu einem bestimmten Thema entwickelt. Sehr, wissen Sie, warm und Lampenwettbewerbe, bei denen die Hauptsache die Teilnahme ist, kein Preis. Die gesammelten Erfahrungen und die Freude an der geleisteten Arbeit, nicht Marketing und Monetarisierung.

Heutzutage, wo die Schwelle für das Betreten von gamedev um ein Vielfaches gesunken ist und ein Strom von aufrichtig erfolglosen Spielen die mobilen Plattformen überflutet, deren Autoren davon träumen, Flappy Bird ein weiteres komfortables Leben zu ermöglichen ... In einer solchen Zeit ist es schwierig, Leute zu finden, die sich aus Spaß an der Spieleentwicklung beteiligen, nicht aus Gewinn.

Natürlich gibt es einen mächtigen Ludum Dare , aber seine harten Begriffe widersprechen bisher meinem Familienleben.

Starten Sie


Am 7. November 2014 wird der nächste Wettbewerb auf der Community-Website angekündigt. Bedingungen:

  • "Pyshch-pyshchk" und Feinde - das sind die Worte, die der Anführer des Wettbewerbs den Arcade-Shooter beschrieb
  • Größe - streng genommen bis zu 115 Kilobyte, da der Wettbewerb der 115. in Folge ist
  • Dauer - Woche

Diesen Bedingungen werden dauerhafte Voraussetzungen hinzugefügt: Sie sollten offline und ohne Installation von Paketen von Drittanbietern und Weiterverteilung funktionieren. Alles, was zum Ausführen des Spiels erforderlich ist und nicht mit dem System geliefert wird, sollte mit dem Release geliefert werden und in dieselben 115 Kilobyte passen.

Die Einschränkung ist für echte Demoszene (4k, 32k ...) unangemessen groß, aber ausreichend, um richtig "pervers" zu sein. Eine schnelle Analyse der Entwicklungswerkzeuge liefert eine ziemlich beeindruckende Liste dessen, was zu diesen Anforderungen passt:

  • Flash
  • html5 + js
  • C, C ++, C #
  • Delphi, FreePascal
  • ...

Übergreifend sind Unity, Cry Engine, Unreal Engine, JVM-basierte Sprachen (erfordert vorinstalliertes jre) sowie die meisten Spieleentwickler.

Über Flash
Trotz der Tatsache, dass Flash den installierten Adobe Flash Player erfordert, ist die Verwendung als Ausnahme weiterhin zulässig (es wird davon ausgegangen, dass Adobe Flash Player weiterhin die Mehrheit darstellt). Es ist historisch passiert.

Während meines Lebens habe ich viele Sprachen und Technologien ausprobiert, und der größte Teil der obigen Liste ist mir aus erster Hand bekannt. Aber dein bescheidener Diener hat nicht die einfachste Option gewählt - Free Pascal. Warum nicht die einfachste?

Erstens gilt Free Pascal (wie Delphi) im Jahr 2014 als unmodern - der FPC-Compiler hat daher trotz Open Source und plattformübergreifender Funktionen nur wenige Benutzer und viele Fehler. Zweitens ist die Größe der kompilierten Exe für das Lazarus IDE + FPC-Bundle Anlass für eine separate Seite im Wiki . Drittens gibt es nur sehr wenig syntaktischen Zucker, was besonders deutlich wird, wenn Sie ständig viele andere Sprachen und Technologien verwenden.

Natürlich gibt es Pluspunkte:

  • Eine korrekt vorbereitete Exe ist autark und mit einer Exe aus C / C ++ mit statischer CRT-Verknüpfung vergleichbar
  • Mit den Standardeinstellungen können Sie nicht wie in C / C ++ in den Fuß schießen
  • Schießt mit den richtigen Einstellungen überhaupt von beiden Beinen ab (was manchmal gewünscht wird)
  • Durch Zufall besitze ich bereits ein Mini-Framework für Free Pascal + OpenGL

Und ich habe bereits ein kostenloses Remake von Lunar Lander gemacht


Lass uns gehen!



Zunächst habe ich mich für das Konzept des zukünftigen Spiels entschieden - einen Arcade-2D-Shooter mit einer Draufsicht, bei dem der Spieler den „Panzer“ kontrolliert und auf jede erdenkliche Weise auf eine Schar von Feinden schießt. Bacnalia geht weiter, bis der Tod dich von deinem Alter Ego trennt. Das nächste Analogon ist Crimsonland .

Nachdem ich die Vorlage mit der üblichen Bewegung kompiliert hatte, stellte ich mir das erste Problem: Die kompilierte Exe mit meinem Framework, die alle kniffligen Optionen des Compilers berücksichtigte, nahm 120 Kilobyte in Anspruch. Angesichts der Tatsache, dass das Framework dies kann (und angesichts der Tatsache, dass dies immer noch FPC ist), ist dies sogar eine Leistung. Aber das passt uns überhaupt nicht, deshalb schneiden wir die exe rücksichtslos mit UPX - 48 Kilobyte. Damit können Sie bereits arbeiten.

Natürlich könnten Sie hier noch ein paar Kilobyte gewinnen, wenn Sie die Funktionalität des Frameworks auf das Notwendigste reduzieren. Ich lehnte dies aus Zeitmangel ab, wie sich später herausstellte - nicht umsonst. Das Limit von 115 Kilobyte reichte mir also aus.

Wir stellen LZO vor


Ein seltenes Spiel kann ohne die Anzeige von Text oder numerischen Informationen auskommen. Ich hatte zwar anfangs die Idee, ein solches Spiel zu erstellen, aber eine interessante Implementierung zu finden, hat nicht funktioniert.

Daher besteht die Aufgabe darin, mit OpenGL Text auf dem Bildschirm anzuzeigen. Ohne auf die archaische Methode zur Anzeige von Vektortext zurückzugreifen, sollten Sie Bitmap-Schriftarten verwenden. Mein Framework hatte bereits Unterstützung für die Textausgabe mit vorgenerierten und sorgfältig gebackenen Bitmap-Schriftarten. Ist das Problem gelöst?

Kurz zur Implementierung
Es gibt ein eigenes Dienstprogramm für Fahrräder, das die erforderlichen Zeichen relativ kompakt packt und sie dann in eine BMP-Datei „backt“, an deren Ende Serviceinformationen über die Metrik der Zeichen (Koordinaten, Größe, Originalgröße usw.) gnadenlos hinzugefügt werden. Jeder Grafikeditor erkennt den Haken nicht und öffnet die Datei ganz korrekt. Wenn Sie jedoch denselben Editoren beibringen möchten, beim Speichern nicht die gesamte Datei zu überschreiben, kann eine solche Schriftart mit Post-Effekten versehen werden ...


Nein, die Aufgabe wird immer komplizierter. Die auf diese Weise erhaltene Datei mit russischen und lateinischen Buchstaben (plus Sonderzeichen und Zahlen) nahm 135 Kilobyte ein. Wir entfernen russische Zeichen, reduzieren die physische Größe der Schrift selbst, das Bild wird in einer der Dimensionen halbiert und dementsprechend in der Größe halbiert - 67 Kilobyte. Trotzdem ist es nicht gut, denn insgesamt ergeben sich bei einem "leeren" Projekt genau 115 Kilobyte.

Jetzt ist mir klar, dass der richtige und einfachste Schritt darin besteht, die Schriftart direkt beim Start aus der Systemschriftart zu erstellen, da der Code zum Kopieren und Einfügen einfach ist. Außerdem wurden in meinem vorherigen Framework auf diese Weise Schriftarten generiert - in "Laufzeit" aus Systemschriftarten oder otf / ttf-Dateien.

Aber die Seele wollte Romantik und den fünften Punkt - Qual. Und ich erinnerte mich an den Genossen XProger2010 beging er einen gewalttätigen Akt gegen die MiniLZO-Bibliothek, riss sie aus dem Müll und wickelte sie in einfache Anweisungen ein. Bei der Extraktion sieht es ungefähr so ​​aus:

function lzo_decompress(const CData; CSize: LongInt; var Data; var Size: LongInt): LongInt; cdecl;
asm
  DB $51
  DD $458B5653,$C558B08,$F08BD003,$33FC5589,$144D8BD2,$68A1189,$3C10558B,$331C7611,$83C88AC9
  DD $8346EFC1,$820F04F9,$1C9,$8846068A,$75494202,$3366EBF7,$460E8AC9,$F10F983,$8D83,$75C98500,$8107EB18
  DD $FFC1,$3E804600,$33F47400,$83068AC0,$C8030FC0,$83068B46,$28904C6,$4904C283,$F9832F74,$8B217204,$83028906
  DD $C68304C2,$4E98304,$7304F983,$76C985EE,$46068A14,$49420288,$9EBF775,$8846068A,$75494202,$8AC933F7
  DD $F983460E,$C12B7310,$828D02E9,$FFFFF7FF,$C933C12B,$C1460E8A,$C12B02E1,$8840088A,$88A420A,$420A8840
  DD $288008A,$113E942,$F9830000,$8B207240,$FF428DD9,$8302EBC1,$C32B07E3,$1E8ADB33,$3E3C146,$2B05E9C1
  DD $D9E949C3,$83000000,$2F7220F9,$851FE183,$EB1875C9,$FFC18107,$46000000,$74003E80,$8AC033F4,$1FC08306
  DD $F46C803,$FBC11EB7,$FF428D02,$C683C32B,$8369EB02,$457210F9,$D98BC28B,$C108E383,$C32B0BE3,$8507E183
  DD $EB1875C9,$FFC18107,$46000000,$74003E80,$8ADB33F4,$7C3831E,$F46CB03,$FBC11EB7,$83C32B02,$D03B02C6
  DD $9A840F,$2D0000,$EB000040,$2E9C11F,$2BFF428D,$8AC933C1,$E1C1460E,$8AC12B02,$A884008,$88008A42
  DD $51EB4202,$7206F983,$2BDA8B37,$4FB83D8,$188B2E7C,$8904C083,$4C2831A,$8B02E983,$831A8918,$C08304C2
  DD $4E98304,$7304F983,$76C985EE,$40188A20,$49421A88,$15EBF775,$8840188A,$188A421A,$421A8840,$8840188A
  DD $7549421A,$8AC933F7,$E183FE4E,$FC98503,$FFFE4284,$46068AFF,$49420288,$C933F775,$E9460E8A,$FFFFFECA
  DD $8B10552B,$10891445,$75FC753B,$EBC03304,$FFF8B80D,$753BFFFF,$830372FC,$5B5E04C0,$90C35D59
end;

... und ähnliche Hexerei zum Komprimieren. Dies funktioniert gut (obwohl ich es nicht sofort geschafft habe), aber ich würde es nicht für den Produktionscode empfehlen. Beim Debuggen gibt es einige Unannehmlichkeiten ...

Nach dem Komprimieren der Schriftart erhalten wir 17 statt 67 Kilobyte. Oder vielleicht 2-3 Kilobyte, wenn ich die Generierung nur im laufenden Betrieb implementiert habe. .


Verwenden Sie uFMOD, um Ton auszugeben


Niemand möchte Spiele ohne Geräusche oder zumindest Musik spielen. Vor diesem Wettbewerb hatte ich Erfahrung mit der Bassbibliothek, musste sie jedoch über Bord lassen - die erforderliche DLL verbrauchte bis zu 97 Kilobyte. Im Berichtsthema des Wettbewerbs wurde uFMOD erwähnt - eine Miniaturbibliothek zur Ausgabe von in Assembler geschriebener xm-Musik. Mit Blick auf die Zukunft werde ich sagen, dass die Implementierung im Projekt praktisch keine Auswirkungen auf die Größe der exe-Datei hatte.

Aber es gab eine kleine Nuance. Auf mehr oder weniger modernen Versionen des FPC-Compilers (über 2.2.x) funktionierte diese Bibliothek nicht. Und das Problem ist mehrdeutiges VerhaltenLinker. Ich bezweifle, dass ich die technischen Aspekte dieses Problems so genau wie möglich beschreiben kann - mit anderen Worten, warum die in der Header-Datei deklarierten externen Funktionen für die direkt dort verbundene Objektdatei nicht sichtbar sind. Dieses Verhalten bleibt bei den Compiler-Entwicklern. Ich werde ein Beispiel für eine "Problemumgehung" dieses Verhaltens für eine der Funktionen geben.

Es war so:

function waveOutClose(hwo:Pointer):LongWord; stdcall; external 'winmm.dll';

Und ich musste es so einpacken:

function my_waveOutClose(hwo:Pointer):LongWord; stdcall; external 'winmm.dll' name 'waveOutClose';
function _waveOutClose(hwo:Pointer):LongWord; stdcall; public name 'waveOutClose';
begin
  Result := my_waveOutClose(hwo);
end;

Und so für ein paar Dutzend Funktionen, die von der Bibliothek benötigt werden. Ich werde klarstellen, dass ich die Implementierung von uFMOD durch winmm als das leichteste und am einfachsten zu verwendende System angesehen habe. Von den Minuspunkten der Einfachheit - es durfte immer nur ein Stream gleichzeitig abgespielt werden. So erschien Musik in meinem Spiel, aber ich musste die Töne aufgeben.

Eigentlich genommen die xm-Spur hier sofort , dann ist die Nützlichkeit von uFMOD verwandelte es in hier ist ein pas-Datei, die mich gerettet ein wenig mehr Platz Bit.

Texturerzeugung


Um den Klassiker zu paraphrasieren - welches Demoszenenprojekt kommt ohne Texturgenerierung aus? Anfangs wollte ich dieses Thema weglassen - es war schmerzlich einfach und „auf der Stirn“ machte ich die Erzeugung einer einzigen (!) Textur. Aber vielleicht werden einige der Neuankömmlinge diesen Ansatz nützlich finden.

Fast alle Sprites im Spiel verwenden dieselbe Textur:



Nur eine gestreifte Textur mit einem "Rand" für mehr Ästhetik. Angesichts des Farbtons (dh "Multiplizieren" der gewünschten Farbe mit einer solchen Textur) erhalten wir gestreifte Texturen jeder Farbe.

Ich mag den Generierungscode absolut nicht, er kann eindeutig optimaler geschrieben werden. Darüber hinaus besteht das anhaltende Gefühl, dass die Grenze falsch gezeichnet ist.

function TGame.GenerateTexture(aWidth, aHeight, aBorderSize: Integer): TglrTexture;
var
  m, m_origin: PByte;
  i, j: Integer;
  value: Byte;
begin
  m := GetMemory(aWidth * aHeight * 3);
  m_origin := m;
  for j := 0 to aHeight - 1 do
    for i := 0 to aWidth - 1 do
    begin
      if (i < aBorderSize) or (j < aBorderSize)
        or (i > aWidth - aBorderSize - 1) or (j > aHeight - aBorderSize - 1) then
        value := 196
      else
        if ((i + j) mod 16) >= 8 then
          value := 255
        else
          value := 196;
      m^ := value; m+=1;
      m^ := value; m+=1;
      m^ := value; m+=1;
    end;
  Result := TglrTexture.Create(m_origin, aWidth, aHeight, tfRGB8);
end;

Zur Abwechslung variiert der Rotton bei Feinden leicht mit random ().

Ärgerlicher Bug bei NVidia


Nach dem Einreichen der Arbeit beim Wettbewerb sammelt der Host das Archiv der eingereichten Arbeit und stellt es öffentlich zur Schau. Die Aufgabe der Teilnehmer ist es, die Plätze der anderen außer für sich selbst zu arrangieren. Zur Auswertung werden drei Tage zur Verfügung gestellt, damit Sie sich nach einer Notarbeit entspannen können. Aber da war es!

Aber nacheinander melden sich die Teilnehmer ab, dass mein Spiel für sie nicht richtig funktioniert - der "Panzer" des Spielers ist im Prinzip nicht sichtbar, viele feindliche Panzer sind nicht sichtbar, manchmal blinken sie. Feinde können nur durch Rauch aus dem Auspuff erkannt werden. Solche Probleme treten bei allen Teilnehmern auf Nvidia-Grafikkarten auf. Außerdem hat einer der Teilnehmer eine Konfiguration, die mit meiner identisch ist, aber mein Fehler erscheint überhaupt nicht. Jemand half beim Start im Kompatibilitätsmodus mit Windows 95 (!), Aber es gab nur wenige.

Ich habe verschiedene Builds angelegt (was bereits ein wenig gegen die Regeln des Wettbewerbs verstößt), alle verdächtigen Stellen im Code bereinigt, verschiedene Einstellungen empfohlen, aber alles war vergebens. Schließlich entdeckte einer der Teilnehmer, der akribischste, für den vielen Dank an ihn (hallo, pelmenka!), Den Grund - wenn Sie die Threading-Optimierung in der Nvidia-Systemsteuerung deaktivieren, funktioniert das Spiel korrekt. Das Ärgernis ist, dass meine Einstellung seit jeher deaktiviert wurde, als sie in einigen Spielen zur Ursache des BSOD wurde.

Dank dieser wertvollen Information konnte ich den Fehler zu Hause reproduzieren und beheben, obwohl ich bereits verstand, dass die meisten Teilnehmer bereits abgestimmt hatten, und im Allgemeinen war ich ein böser Pinocchio für mich, der das Spiel in den Standardeinstellungen des Nvidia-Kontrollfelds nicht überprüfte .

Ich werde den Prozess des Findens von Lösungen für das Problem für eine lange Zeit nicht beschreiben. Ich kann nur sagen, dass ich bei der Suche Folgendes festgestellt habe:

  • Streaming-Optimierung bringt mehr Probleme als Vorteile. Eine einfache Suche nach "NVIDIA Threading-Optimierung" bietet Links zu Spieleforen, in denen es dringend empfohlen wird, diese Einstellung zu deaktivieren, um ein "Blinken" der Spiele zu vermeiden.
  • Es gibt keine Spezifikation, die erklären würde, was diese Optimierung auf Treiberebene bewirkt (oder gibt es?)
  • Eine Reihe von Themen von Indie-Entwicklern, die sich über Fehler bei aktivierter Streaming-Optimierung beschweren. Keine Antworten außer "Mach es einfach aus, Kumpel"

Schließlich wurde meine Aufmerksamkeit auf ein Thema gelenkt, in dem über die fehlerhafte Funktion der Funktion glBufferSubData geklagt wurde, als das nicht benennbare Teil eingeschaltet wurde. Dies gab mir einen Hinweis und nach einer Weile (Debugs, Checks) habe ich das Wesentliche des Problems herausgearbeitet:

Die Funktion glBufferSubData aktualisiert die Daten im Vertex-Puffer. Der Hauptzweck dieser Funktion ist die Aktualisierung des "Teils" des Puffers. Sie sollte jedoch auch verwendet werden, wenn die Aktualisierung des gesamten Puffers ohne Speicherzuordnung erforderlich ist (mein Fall).

Wenn die Streaming-Optimierung aktiviert ist, setzt der NVidia-Treiber manchmal (immer?), Basierend auf einem seiner geführten Attribute, Aufrufe dieser Funktion in einen separaten Stream und gibt sofort die Steuerung zurück, was zu einem bedauerlichen Ergebnis führt. Wie viele Daten Zeit haben, um in den Puffer zu "fluten", bevor sie gezeichnet werden, ist unbekannt. Und der OpenGL-Treiber von Nvidia sieht dies nicht als Problem an. Im Vorfeld Ihrer Frage werde ich antworten: Nein, die Größe der übertragenen Daten ist vernachlässigbar, ein paar Kilobyte (insbesondere im Vergleich zur Busbandbreite), daher ist die Angelegenheit weit von fetten Daten entfernt.

Die OpenGL-Spezifikation sagt uns nicht, dass diese Funktion in einem separaten Thread ausgeführt werden kann. Eigeninitiative der Jungs von Nvidia?

Im Falle einer einmaligen Verwendung von glBufferSubData pro Frame werden Sie nichts bemerken, da der Treiber die Zeichnung dieses Puffers "zwischenspeichert" und sie aufruft, nachdem er (wahrscheinlich) auf den Abschluss aller Operationen an diesem Puffer gewartet hat.

In meinem Fall wird ein Puffer mehrmals pro Frame verwendet. Also:

glBindBuffer(GL_ARRAY_BUFFER, BufferId);
glBufferSubData(GL_ARRAY_BUFFER, 0, Size, Data);
glDrawElements(...);
glBindBuffer(GL_ARRAY_BUFFER, 0);
...
glBindBuffer(GL_ARRAY_BUFFER, BufferId);
glBufferSubData(GL_ARRAY_BUFFER, 0, OtherSize, OtherData);
glDrawElements(...);
glBindBuffer(GL_ARRAY_BUFFER, 0);

Und hier spüren Sie die Stream-Optimierung in ihrer ganzen Pracht. Was eventuell auf dem Bildschirm angezeigt wird, bestimmt den Blindfall.

Der glFinish () -Aufruf hilft, obwohl dies eine so lala Lösung ist. Persönlich habe ich die Logik zum Aktualisieren der Daten im Puffer leicht geändert, um solche heiklen Situationen zu vermeiden.

F: Warum nicht die Daten kombinieren und gleichzeitig zeichnen?
A: Zwischen diesen Aufrufen werden andere Elemente gerendert. Sie können die Zeichenreihenfolge nicht ändern.

F: Warum nicht verschiedene Puffer verwenden?
A: Die Daten im Puffer werden bei jedem Frame aktualisiert. Das Speichern mehrerer Puffer ist eine Verschwendung von Ressourcen

Zusammenfassung


Infolgedessen habe ich den vierten Platz im Wettbewerb erreicht, obwohl ich um den zweiten oder dritten Platz hätte kämpfen können. Der Ärger wurde nicht durch die Tatsache verursacht, dass ich an der „Troika“ der Anführer vorbeigeflogen bin, sondern durch die Erkenntnis, dass ich das gute Spiel und 7 Tage meiner Arbeit mit einem kleinen Ärger „ruiniert“ habe.

Kurzes Videospiel:



Die Gesamtgröße der Veröffentlichung betrug fast 80 Kilobyte. Sie umfassen:

  • 62,0 kb - exe
  • 17,3 kb - Schriftart
  • 0,52 kb - Shader

- Laden Sie die Version herunter (alle erforderlichen Quellen sind beigefügt).
- Quellen zu Github

Jetzt auch beliebt: