
Ich möchte eine weitere abendliche Langzeitkonstruktion teilen, die zeigt, dass Sie Spiele auch auf schwacher Hardware erstellen können.
Über das, was Sie tun mussten, wie es entschieden wurde und wie Sie etwas mehr als nur einen anderen Pong-Klon tun können - willkommen bei Cat.
Achtung: toller Artikel, Verkehr und mehrere Code-Einfügungen!
Kurz über das Spiel
Schieß auf sie! - jetzt auf AVR.
Tatsächlich ist dies eine weitere Shmap, daher muss die Hauptfigur
Shepard die Galaxie erneut vor einem plötzlichen Angriff unbekannter Personen retten und sich durch die Sterne und Felder der Asteroiden durch den Weltraum bewegen, um gleichzeitig jedes Sternensystem zu löschen.
Das ganze Spiel ist in C und C ++ geschrieben, ohne die Wire-Bibliothek von Arduino zu verwenden.
Das Spiel hat 4 Schiffe zur Auswahl (letzteres ist nach dem Passieren verfügbar), jedes mit seinen eigenen Eigenschaften:
- Manövrierfähigkeit;
- Haltbarkeit;
- Waffengewalt.
Ebenfalls implementiert:
- 2D-Farbgrafiken;
- Power für Waffen;
- Bosse am Ende der Levels;
- Levels mit Asteroiden (und deren Rotationsanimation);
- Änderung der Hintergrundfarbe auf Ebenen (und nicht nur im Schwarzraum);
- die Bewegung von Sternen im Hintergrund mit unterschiedlichen Geschwindigkeiten (für den Effekt der Tiefe);
- Scoring und Speichern im EEPROM;
- die gleichen Geräusche (Schüsse, Explosionen usw.);
- ein Meer identischer Gegner.
Plattform
Die Rückkehr des Geistes.
Ich werde im Voraus klarstellen, dass diese Plattform als die alte Spielekonsole der ersten dritten Generation (80er Jahre, shiru8bit ) angesehen werden sollte.
Außerdem sind Hardwaremodifikationen gegenüber der Originalhardware verboten, wodurch der Start auf jeder anderen identischen Karte sofort gewährleistet ist.
Dieses Spiel wurde für das Arduino Esplora-Board geschrieben, aber die Übertragung auf GBA oder eine andere Plattform wird meiner Meinung nach nicht schwierig sein.
Trotzdem wurde dieses Forum selbst auf dieser Ressource nur ein paar Mal behandelt, und andere Boards waren trotz der ziemlich großen Community von jedem überhaupt nicht erwähnenswert:
- GameBuino META:
- Pokitto;
- makerBuino;
- Arduboy;
- UzeBox / FuzeBox;
- und viele andere.
Was ist nicht auf Esplora?
- viel Speicher (ROM 28 KB, RAM 2,5 KB);
- Leistung (8 Bit CPU bei 16 MHz);
- DMA
- Zeichengenerator;
- zugewiesene Speicherbereiche oder Sonderregister. Ziel (Palette, Kacheln, Hintergrund usw.);
- Steuern Sie die Helligkeit des Bildschirms (oh, so viele Effekte im Papierkorb);
- Adressraum-Extender (Mapper);
- Debugger (
aber wer braucht es, wenn es einen ganzen Bildschirm gibt! ).
Ich werde mit der Tatsache fortfahren, dass es gibt:
- Hardware-SPI (kann mit F_CPU / 2-Geschwindigkeit ausgeführt werden);
- Bildschirm basierend auf ST7735 160x128 1,44 ";
- eine Prise Timer (nur 4 Stück);
- eine Prise GPIO;
- eine Handvoll Tasten (5 Stück + zweiachsiger Joystick);
- wenige Sensoren (Beleuchtung, Beschleunigungsmesser, Thermometer);
- Piezo Summer
Irritation Emitter.
Anscheinend ist da fast nichts. Es ist nicht verwunderlich, dass niemand etwas mit ihr machen wollte, außer dem Pong-Klon und ein paar drei Spielen für die ganze Zeit!
Möglicherweise ähnelt das Schreiben unter dem ATmega32u4-Controller (und dergleichen) der Programmierung für Intel 8051 (das zum Zeitpunkt der Veröffentlichung fast 40 Jahre alt ist), bei dem Sie eine Vielzahl von Bedingungen beachten und auf verschiedene Tricks und Tricks zurückgreifen müssen.
Periphere Verarbeitung
Eins für alles!
Bei Betrachtung der Schaltung war deutlich zu erkennen, dass alle Peripheriegeräte über den GPIO-Expander (74HC4067D Multiplexer weiter MUX) angeschlossen und mit dem GPIO PF4, PF5, PF6, PF7 oder dem Senior PORTF Nibble geschaltet werden und der MUX-Ausgang auf GPIO - PF1 gelesen wird.
Es ist sehr praktisch, den Eingang zu wechseln, indem Sie dem PORTF-Port einfach Werte per Maske zuweisen und dabei das kleine Knabbern nicht vergessen:
uint16_t getAnalogMux(uint8_t chMux) { MUX_PORTX = ((MUX_PORTX & 0x0F) | ((chMux<<4)&0xF0)); return readADC(); }
Button-Click-Umfrage:
#define SW_BTN_MIN_LVL 800 bool readSwitchButton(uint8_t btn) { bool state = true; if(getAnalogMux(btn) > SW_BTN_MIN_LVL) {
Die folgenden Werte gelten für Port F:
#define SW_BTN_1_MUX 0 #define SW_BTN_2_MUX 8 #define SW_BTN_3_MUX 4 #define SW_BTN_4_MUX 12
Indem Sie etwas mehr hinzufügen:
#define BUTTON_A SW_BTN_4_MUX #define BUTTON_B SW_BTN_1_MUX #define BUTTON_X SW_BTN_2_MUX #define BUTTON_Y SW_BTN_3_MUX #define buttonIsPressed(a) readSwitchButton(a)
Sie können sicher das richtige Kreuz interviewen:
void updateBtnStates(void) { if(buttonIsPressed(BUTTON_A)) btnStates.aBtn = true; if(buttonIsPressed(BUTTON_B)) btnStates.bBtn = true; if(buttonIsPressed(BUTTON_X)) btnStates.xBtn = true; if(buttonIsPressed(BUTTON_Y)) btnStates.yBtn = true; }
Bitte beachten Sie, dass der vorherige Status nicht zurückgesetzt wird, da Sie sonst das Drücken der Taste übersehen können (dies dient auch als zusätzlicher Schutz gegen Rattern).
Sfx
Ein summendes Stück.
Was ist, wenn es keinen DAC, keinen Chip von Yamaha und nur ein 1-Bit-PWM-Rechteck für Sound gibt?
Auf den ersten Blick scheint es nicht so viel zu sein, aber trotzdem wird hier das listige PWM verwendet, um die „PDM-Audio“ -Technik neu zu erstellen, und mit seiner Hilfe können Sie
dies tun
.Ähnliches bietet die Bibliothek von Gamebuino. Sie müssen lediglich den Popping-Generator auf ein anderes GPIO und den Timer auf Esplora übertragen (Timer4- und OCR4D-Ausgabe). Für einen korrekten Betrieb wird Timer1 auch verwendet, um Interrupts zu generieren und das OCR4D-Register mit neuen Daten neu zu laden.
Die Gamebuino-Engine verwendet Soundmuster (wie bei Trackermusik), was viel Platz spart. Sie müssen jedoch alle Samples selbst erstellen. Es gibt keine Bibliotheken mit vorgefertigten.
Es ist erwähnenswert, dass diese Engine an eine Aktualisierungsperiode von ungefähr 1/50 Sek. Oder 20 Frames / Sek. Gebunden ist.
Um Klangmuster zu lesen, habe ich nach dem Lesen des Wikis im Audioformat eine einfache GUI auf Qt skizziert. Der Ton wird nicht auf die gleiche Weise ausgegeben, sondern es wird ein ungefähres Konzept für den Klang des Musters gegeben, und Sie können ihn laden, speichern und bearbeiten.
Grafik
Unsterblicher Pixelart.
Das Display codiert Farben in zwei Bytes (RGB565). Da jedoch Bilder in diesem Format viel Platz beanspruchen, wurden sie alle von der Palette indiziert, um Platz zu sparen, was ich bereits mehr als einmal in meinen früheren Artikeln beschrieben habe.
Im Gegensatz zu Famicom / NES gibt es keine Farbbeschränkungen für das Bild und es sind mehr Farben in der Palette verfügbar.
Jedes Bild im Spiel ist ein Array von Bytes, in denen die folgenden Daten gespeichert sind:
- Breite, Höhe;
- Datenmarkierung starten;
- Wörterbuch (falls vorhanden, aber dazu später mehr);
- Nutzlast;
- Ende der Datenmarkierung.
Zum Beispiel ein solches Bild (10-fach vergrößert):

im Code sieht es so aus:
pic_t weaponLaserPic1[] PROGMEM = { 0x0f,0x07, 0x02, 0x8f,0x32,0xa2,0x05,0x8f,0x06,0x22,0x41,0xad,0x03,0x41,0x22,0x8f,0x06,0xa2,0x05, 0x8f,0x23,0xff, };
Wo ohne Schiff in diesem Genre? Nach Hunderten von Testskizzen mit einem Pixelunterschied blieben nur diese Schiffe für den Spieler übrig:

Es ist bemerkenswert, dass die Schiffe keine Flamme in den Fliesen haben (hier aus Gründen der Klarheit), es wird separat angewendet, um eine Animation des Abgases vom Motor zu erstellen.
Vergessen Sie nicht die Piloten jedes Schiffes:

Die Variation der feindlichen Schiffe ist nicht zu groß, aber ich möchte Sie daran erinnern, dass es nicht zu viel Platz gibt. Hier sind drei Schiffe:

Ohne kanonische Boni in Form von Waffenverbesserung und Wiederherstellung der Gesundheit wird der Spieler nicht lange durchhalten:

Natürlich ändert sich mit zunehmender Leistung der Kanonen die Art der emittierten Granaten:

Wie es am Anfang geschrieben wurde, hat das Spiel ein Level mit Asteroiden, es kommt nach jedem zweiten Boss. Es ist insofern interessant, als es viele sich bewegende und rotierende Objekte unterschiedlicher Größe gibt. Wenn ein Spieler sie trifft, fallen sie außerdem teilweise zusammen und werden kleiner.
Hinweis: Große Asteroiden verdienen mehr Punkte.



Um diese einfache Animation zu erstellen, reichen 12 kleine Bilder aus:

Sie sind für jede Größe (groß, mittel und klein) in drei Teile unterteilt. Für jeden Drehwinkel benötigen Sie 4 weitere gedrehte 0, 90, 180 und 270 Grad. Im Spiel reicht es aus, den Zeiger auf das Array in einem gleichen Intervall durch das Bild zu ersetzen, wodurch die Illusion einer Rotation entsteht.
void rotateAsteroid(asteroid_t &asteroid) { if(RN & 1) { asteroid.sprite.pPic = getAsteroidPic(asteroid); ++asteroid.angle; } } void moveAsteroids(void) { for(auto &asteroid : asteroids) { if(asteroid.onUse) { updateSprite(&asteroid.sprite); rotateAsteroid(asteroid); ...
Dies geschieht nur aufgrund fehlender Hardwarefunktionen, und eine Softwareimplementierung wie die Affine-Transformation benötigt mehr als die Bilder selbst und ist sehr langsam.
Ein Stück Satin für Interessierte.
Sie können einen Teil der Prototypen und das, was erst im Abspann erscheint, nach dem Bestehen des Spiels bemerken.
Um Platz zu sparen und einen Retro-Effekt hinzuzufügen, wurden neben einfachen Grafiken auch Kleinbuchstaben und alle Glyphen, die bis zu 30 und nach 127 Byte ASCII waren, aus der Schriftart entfernt.
Wichtig!
Vergessen Sie nicht, dass const und constexpr auf AVR überhaupt nicht bedeuten, dass sich die Daten im Programmspeicher befinden. Hierfür müssen Sie zusätzlich PROGMEM verwenden.
Dies liegt an der Tatsache, dass der AVR-Kern auf der Harvard-Architektur basiert, sodass spezielle Zugriffscodes für die CPU erforderlich sind, um auf die Daten zuzugreifen.
Die Galaxie zusammendrücken
Der einfachste Weg zu packen ist RLE.
Nachdem Sie die gepackten Daten untersucht haben, können Sie feststellen, dass das höchstwertige Bit im Nutzlastbyte im Bereich von 0x00 bis 0x50 nicht verwendet wird. Auf diese Weise können Sie die Daten und die Startmarkierung für den Beginn der Wiederholung (0x80) und das nächste Byte hinzufügen, um die Anzahl der Wiederholungen anzugeben. Auf diese Weise können Sie eine Reihe von 257 (+2 aus der Tatsache, dass RLE von zwei Bytes dumm ist) identischer Bytes in nur zwei packen.
Implementierung und Anzeige des Entpackers:
void drawPico_RLE_P(uint8_t x, uint8_t y, pic_t *pPic) { uint16_t repeatColor; uint8_t tmpInd, repeatTimes; alphaReplaceColorId = getAlphaReplaceColorId(); auto tmpData = getPicSize(pPic, 0); tftSetAddrWindow(x, y, x+tmpData.u8Data1, y+tmpData.u8Data2); ++pPic;
Die Hauptsache ist, das Bild nicht außerhalb des Bildschirms anzuzeigen, da es sonst Müll ist, da hier keine Randprüfung stattfindet.
Das Testbild wird in ~ 39ms entpackt. Gleichzeitig werden 3040 Bytes belegt, während ohne Komprimierung 11.200 Bytes oder 22.400 Bytes ohne Indizierung benötigt werden.
Testbild (2-fach vergrößert):

Im Bild oben sehen Sie Interlace, aber auf dem Bildschirm wird es durch Hardware geglättet, wodurch ein CRT-ähnlicher Effekt erzeugt und gleichzeitig das Komprimierungsverhältnis erheblich erhöht wird.
RLE ist kein Allheilmittel
Wir werden wegen Deja Vu behandelt.
Wie Sie wissen, passt RLE gut zu LZ-ähnlichen Packern. WiKi kam mit einer Liste von Komprimierungsmethoden zur Rettung. Der Anstoß war das Video von "GameHut" über die Analyse des unmöglichen
Intro in Sonic 3D Blast.Nachdem ich viele Packer (LZ77, LZW, LZSS, LZO, RNC usw.) studiert hatte, kam ich zu dem Schluss, dass ihre Auspacker:
- benötigen viel RAM für entpackte Daten (mindestens 64 KB und mehr);
- sperrig und langsam (einige müssen Huffman-Bäume für jede Untereinheit bauen);
- ein niedriges Komprimierungsverhältnis mit einem kleinen Fenster haben (sehr strenge RAM-Anforderungen);
- Unklarheiten bei der Lizenzierung haben.
Nach Monaten vergeblicher Anpassungen wurde beschlossen, den vorhandenen Packer zu modifizieren.
In Analogie zu LZ-ähnlichen Packern wurde zur Erzielung einer maximalen Komprimierung der Wörterbuchzugriff verwendet, jedoch auf Byte-Ebene - die am häufigsten wiederholten Bytepaare werden im Wörterbuch durch einen Bytezeiger ersetzt.
Aber es gibt einen Haken: Wie kann man ein Byte mit „wie vielen Wiederholungen“ von einem „Wörterbuchmarker“ unterscheiden?
Nach einer langen Sitzung mit einem Stück Papier und einem magischen Spiel mit Fledermäusen erschien Folgendes:
- "Wörterbuchmarker" ist ein RLE-Marker (0x80) + Datenbyte (0x50) + Positionsnummer im Wörterbuch;
- Begrenzen Sie das Byte "wie viele Wiederholungen" auf die Größe des Wörterbuchmarkers - 1 (0xCF);
- Das Wörterbuch kann den Wert 0xff nicht verwenden (dies gilt für das Ende der Bildmarkierung).
Wenn wir all dies anwenden, erhalten wir eine feste Wörterbuchgröße: nicht mehr als 46 Bytepaare und eine RLE-Reduzierung auf 209 Byte. Natürlich können nicht alle Bilder so verpackt werden, aber sie werden nicht mehr.
In beiden Algorithmen ist die Struktur des gepackten Bildes wie folgt:
- 1 Byte pro Breite und Höhe;
- 1 Byte für die Größe des Wörterbuchs, es ist ein Markierungszeiger auf den Anfang der gepackten Daten;
- von 0 bis 92 Bytes des Wörterbuchs;
- 1 bis N Bytes gepackter Daten.
Das resultierende Packer-Dienstprogramm auf D (pickoPacker) reicht aus, um einen Ordner mit indizierten * .png-Dateien abzulegen und vom Terminal (oder cmd) aus auszuführen. Wenn Sie Hilfe benötigen, führen Sie die Option "-h" oder "--help" aus.
Nachdem das Dienstprogramm ausgeführt wurde, erhalten wir * .h-Dateien, deren Inhalt bequem an die richtige Stelle im Projekt übertragen werden kann (daher gibt es keinen Schutz).
Vor dem Auspacken werden der Bildschirm, das Wörterbuch und die Anfangsdaten vorbereitet:
void drawPico_DIC_P(uint8_t x, uint8_t y, pic_t *pPic) { auto tmpData = getPicSize(pPic, 0); tftSetAddrWindow(x, y, x+tmpData.u8Data1, y+tmpData.u8Data2); uint8_t tmpByte, unfoldPos, dictMarker; alphaReplaceColorId = getAlphaReplaceColorId(); auto pDict = &pPic[3];
Ein gelesenes Datenelement kann in ein Wörterbuch gepackt werden, daher überprüfen und entpacken wir es:
inline uint8_t findPackedMark(uint8_t *ptr) { do { if(*ptr >= DICT_MARK) { return 1; } } while(*(++ptr) != PIC_DATA_END); return 0; } inline uint8_t *unpackBuf_DIC(const uint8_t *pDict) { bool swap = false; bool dictMarker = true; auto getBufferPtr = [&](uint8_t a[], uint8_t b[]) { return swap ? &a[0] : &b[0]; }; auto ptrP = getBufferPtr(buf_unpacked, buf_packed); auto ptrU = getBufferPtr(buf_packed, buf_unpacked); while(dictMarker) { if(*ptrP >= DICT_MARK) { setPicWData(ptrU) = getPicWData(pDict, *ptrP); ++ptrU; } else { *ptrU = *ptrP; } ++ptrU; ++ptrP; if(*ptrP == PIC_DATA_END) { *ptrU = *ptrP;
Aus dem empfangenen Puffer entpacken wir nun RLE auf vertraute Weise und zeigen es auf dem Bildschirm an:
inline void printBuf_RLE(uint8_t *pData) { uint16_t repeatColor; uint8_t repeatTimes, tmpByte; while((tmpByte = *pData) != PIC_DATA_END) {
Überraschenderweise hatte das Ersetzen des Algorithmus keinen wesentlichen Einfluss auf die Auspackzeit und beträgt ~ 47 ms. Das sind fast 8ms. länger, aber das Testbild benötigt nur 1650 Bytes!
Bis zur letzten Maßnahme
Fast alles geht schneller!
Trotz des Vorhandenseins von Hardware-SPI bereitet der AVR-Kern bei der Verwendung große Kopfschmerzen.
Es ist seit langem bekannt, dass SPI auf AVR nicht nur mit F_CPU / 2-Geschwindigkeit läuft, sondern auch ein Datenregister von nur 1 Byte hat (es ist nicht möglich, 2 Bytes gleichzeitig zu laden).
Darüber hinaus funktioniert fast der gesamte SPI-Code auf AVR, den ich getroffen habe, nach diesem Schema:
- Laden Sie SPDR-Daten herunter
- Fragen Sie das SPIF-Bit in der SPSR in einer Schleife ab.
Wie Sie sehen, riecht die kontinuierliche Datenversorgung, wie sie beim STM32 erfolgt, hier nicht. Aber auch hier können Sie die Ausgabe beider Entpacker um ~ 3ms beschleunigen!
Wenn Sie das Datenblatt öffnen und den Abschnitt „Befehlssatzuhren“ lesen, können Sie die CPU-Kosten für die Übertragung eines Bytes über SPI berechnen:
- 1 Zyklus zum Laden des Registers mit neuen Daten;
- 2 Schläge pro Bit (oder 16 Schläge pro Byte);
- 1 Takt pro Taktzeile Magie (etwas später über "NOP");
- 1 Takt zum Überprüfen des Statusbits in SPSR (oder 2 Takt auf dem Zweig);
Insgesamt sollten zur Übertragung eines Pixels (zwei Bytes) 38 Taktzyklen oder ~ 425600 Taktzyklen für das Testbild (11.200 Bytes) ausgegeben werden.
Wenn wir wissen, dass F_CPU == 16 MHz ist, erhalten wir
0,0000000625 62,5 Nanosekunden pro Taktzyklus (
Process0169 ), multipliziert mit den Werten, erhalten wir ~ 26 Millisekunden. Es stellt sich die Frage: „Woher habe ich früher geschrieben, dass die Auspackzeit 39 ms beträgt? und 47ms. "? Alles ist einfach - Entpackerlogik + Interrupt-Handling.
Hier ist ein Beispiel für die Interrupt-Ausgabe:

und ohne Unterbrechung:

Die Grafiken zeigen, dass die Zeit zwischen dem Einstellen des Adressfensters im VRAM-Bildschirm und dem Beginn der Datenübertragung in der Version ohne Unterbrechungen kürzer ist und es während der Übertragung fast keine Lücken zwischen den Bytes gibt (die Grafik ist einheitlich).
Leider können Sie Interrupts nicht für jede Bildausgabe deaktivieren, da sonst der Sound und der Kern des gesamten Spiels unterbrochen werden (dazu später mehr).
Es wurde oben über ein bestimmtes "magisches NOP" für eine Taktleitung geschrieben. Tatsache ist, dass zur Stabilisierung des CLK und zum Setzen des SPIF-Flags genau 1 Taktzyklus benötigt wird und zum Zeitpunkt des Lesens dieses Flags bereits gesetzt ist, wodurch vermieden wird, dass der BREQ-Befehl in 2 Balken verzweigt.
Hier ist ein Beispiel ohne NOP:

und mit ihm:

Der Unterschied scheint unbedeutend zu sein, nur ein paar Mikrosekunden, aber wenn Sie eine andere Skala nehmen:
Großer NOP:

und damit zu groß:

dann wird der Unterschied viel deutlicher und erreicht ~ 4,3 ms.
Lassen Sie uns nun den folgenden schmutzigen Trick machen:
Wir tauschen die Reihenfolge des Ladens und Lesens der Register aus, und Sie können nicht auf jedes zweite Byte des SPIF-Flags warten, sondern es nur überprüfen, bevor Sie das erste Byte des nächsten Pixels laden.
Wir wenden Wissen an und setzen die Funktion "pushColorFast (repeatColor);" ein:
#define SPDR_TX_WAIT(a) asm volatile(a); while((SPSR & (1<<SPIF)) == 0); typedef union { uint16_t val; struct { uint8_t lsb; uint8_t msb; }; } SPDR_t; ... do { #ifdef ESPLORA_OPTIMIZE SPDR_t in = {.val = repeatColor}; SPDR_TX_WAIT(""); SPDR = in.msb; SPDR_TX_WAIT("nop"); SPDR = in.lsb; #else pushColorFast(repeatColor); #endif } while(--repeatTimes); } #ifdef ESPLORA_OPTIMIZE SPDR_TX_WAIT("");
Trotz der Unterbrechung durch den Timer ergibt die Verwendung des obigen Tricks eine Verstärkung von fast 6 ms .:

So einfach können Sie mit Eisen etwas mehr herausholen und etwas Ähnliches ausgeben:

Kolosseumkollisionen
Der Kampf der Kisten.
Zunächst sind alle Objekte (Schiffe, Muscheln, Asteroiden, Boni) Strukturen (Sprites) mit den folgenden Parametern:
- aktuelle X, Y-Koordinaten;
- neue Koordinaten X, Y;
- Zeiger auf das Bild.
Da das Bild die Breite und Höhe speichert, müssen diese Parameter nicht dupliziert werden. Darüber hinaus vereinfacht eine solche Organisation die Logik in vielerlei Hinsicht.
Die Berechnung selbst wird für das Banale einfach gemacht - basierend auf dem Schnittpunkt der Rechtecke. Obwohl es nicht genau genug ist und zukünftige Konflikte nicht berechnet, ist dies mehr als genug.
Die Überprüfung erfolgt abwechselnd auf der X- und Y-Achse. Aufgrund dessen reduziert das Fehlen eines Schnittpunkts auf der X-Achse die Berechnung der Kollision.
Zunächst wird die rechte Seite des ersten Rechtecks mit der linken Seite des zweiten Rechtecks auf den gemeinsamen Teil der X-Achse überprüft. Bei Erfolg wird eine ähnliche Prüfung für die linke Seite der ersten und rechten Seite des zweiten Rechtecks durchgeführt.
Nach dem erfolgreichen Erkennen von Schnittpunkten entlang der X-Achse wird auf die gleiche Weise eine Überprüfung für die Ober- und Unterseite der Rechtecke entlang der Y-Achse durchgeführt.
Das obige sieht viel einfacher aus als es scheint:
bool checkSpriteCollision(sprite_t *pSprOne, sprite_t *pSprTwo) { auto tmpDataOne = getPicSize(pSprOne->pPic, 0); auto tmpDataTwo = getPicSize(pSprTwo->pPic, 0); uint8_t objOnePosEndX = (pSprOne->pos.Old.x + tmpDataOne.u8Data1); if(objOnePosEndX >= pSprTwo->pos.Old.x) { uint8_t objTwoPosEndX = (pSprTwo->pos.Old.x + tmpDataTwo.u8Data1); if(pSprOne->pos.Old.x >= objTwoPosEndX) { return false;
Es bleibt, dies dem Spiel hinzuzufügen:
void checkInVadersCollision(void) { decltype(aliens[0].weapon.ray) gopher; for(auto &alien : aliens) { if(alien.alive) { if(checkSpriteCollision(&ship.sprite, &alien.sprite)) { gopher.sprite.pos.Old = alien.sprite.pos.Old; rocketEpxlosion(&gopher);
Bezier-Kurve
Raumschienen.
Wie in jedem anderen Spiel dieses Genres müssen sich feindliche Schiffe entlang von Kurven bewegen.
Es wurde beschlossen, quadratische Kurven als einfachste für die Steuerung und diese Aufgabe zu implementieren. Drei Punkte reichen ihnen: der Anfang (P0), der Schluss (P2) und der Imaginär (P1). Die ersten beiden geben den Anfang und das Ende der Linie an, der letzte Punkt beschreibt die Art der Krümmung.
Toller Artikel über Kurven.Da es sich um eine Bezier-Parameterkurve handelt, wird außerdem ein weiterer Parameter benötigt - die Anzahl der Zwischenpunkte zwischen Start- und Endpunkt.
Insgesamt bekommen wir hier eine solche Struktur:
typedef struct {
Darin ist position_t eine Struktur aus zwei Bytes der Koordinaten X und Y.
Das Finden eines Punktes für jede Koordinate wird unter Verwendung dieser Formel berechnet (thx Wiki):
B = ((1,0 - t) ^ 2) P0 + 2t (1,0 - t) P1 + (t ^ 2) P2,
t [> = 0 && <= 1]
Die Implementierung wurde lange Zeit ohne Fixpunktmathematik frontal gelöst:
... float t = ((float)pItemLine->step)/((float)pLine->totalSteps); pPos->x = (1.0 - t)*(1.0 - t)*pLine->P0.x + 2*t*(1.0 - t)*pLine->P1.x + t*t*pLine->P2.x; pPos->y = (1.0 - t)*(1.0 - t)*pLine->P0.y + 2*t*(1.0 - t)*pLine->P1.y + t*t*pLine->P2.y; ...
Dies kann natürlich nicht verlassen werden. Schließlich könnte das Entfernen des Schwimmers nicht nur zu einer Verbesserung der Geschwindigkeit führen, sondern auch das ROM freigeben, sodass die folgenden Implementierungen gefunden wurden:
- avrfix;
- stdfix;
- libfixmath;
- fixedptc.
Das erste bleibt ein dunkles Pferd, da es eine kompilierte Bibliothek ist und sich nicht mit dem Disassembler anlegen wollte.
Der zweite Kandidat aus dem GCC-Bundle hat ebenfalls nicht funktioniert, da der verwendete avr-gcc nicht gepatcht wurde und der Typ "short _Accum" nicht verfügbar blieb.
Die dritte Option, trotz der Tatsache, dass es eine große Anzahl von Matten hat. Funktionen, hat hartcodierte Bitoperationen für bestimmte Bits im Format Q16.16, was es unmöglich macht, die Werte von Q und I zu steuern.
Letzteres kann als vereinfachte Version von "fixedmath" betrachtet werden. Der Hauptvorteil ist jedoch die Möglichkeit, nicht nur die Größe der Variablen zu steuern, die standardmäßig 32 Bit mit dem Format Q24.8 beträgt, sondern auch die Werte von Q und I.
Testergebnisse bei verschiedenen Einstellungen:
Typ | IQ | Zusätzliche Flags | ROM-Byte | Tms. * |
---|
float | - - | - - | 4236 | 35 |
Fixmath | 16.16 | - - | 4796 | 119 |
Fixmath | 16.16 | FIXMATH_NO_OVERFLOW | 4664 | 89 |
Fixmath | 16.16 | FIXMATH_OPTIMIZE_8BIT | 5036 | 92 |
Fixmath | 16.16 | _NO_OVERFLOW + _8BIT | 4916 | 89 |
fixedptc | 24.8 | FIXEDPT_BITS 32 | 4420 | 64 |
fixedptc | 9.7 | FIXEDPT_BITS 16 | 3490 | 31 |
* Die Prüfung wurde nach dem Muster "195,175,145,110,170,70,170" und dem Schlüssel "-Os" durchgeführt.
Aus der Tabelle ist ersichtlich, dass beide Bibliotheken mehr ROM beanspruchten und sich bei Verwendung von float als schlechter als der kompilierte Code von GCC zeigten.
Es ist auch ersichtlich, dass eine kleine Überarbeitung des Q9.7-Formats und eine Verringerung der Variablen auf 16 Bit eine Beschleunigung von 4 ms ergab. und Freigeben des ROM bei ~ 50 Bytes.
Der erwartete Effekt war eine Abnahme der Genauigkeit und eine Zunahme der Anzahl der Fehler:

was in diesem Fall unkritisch ist.
Ressourcen zuweisen
Dienstag und Donnerstag arbeiten nur eine Stunde.
In den meisten Fällen werden alle Berechnungen in jedem Frame ausgeführt, was nicht immer gerechtfertigt ist, da im Frame möglicherweise nicht genügend Zeit vorhanden ist, um etwas zu berechnen, und Sie mit dem Abwechseln, Zählen von Frames oder Überspringen von Frames tricksen müssen. Also ging ich weiter - gab das Personal komplett auf.
Nachdem alles in kleine Aufgaben unterteilt wurde, sei es: Berechnen von Kollisionen, Verarbeiten von Ton, Schaltflächen und Anzeigen von Grafiken, reicht es aus, diese in einem bestimmten Intervall auszuführen, und die Trägheit des Auges und die Fähigkeit, nur einen Teil des Bildschirms zu aktualisieren, werden ihre Aufgabe erfüllen.Wir verwalten das alles nicht einmal mit dem Betriebssystem, sondern mit der Zustandsmaschine, die ich vor ein paar Jahren erstellt habe, oder, einfacher gesagt, nicht mit dem verdrängten tinySM-Task-Manager.Ich werde die Gründe für die Verwendung anstelle eines RTOS wiederholen:- geringere ROM-Anforderungen (~ 250 Byte Kern);
- geringere RAM-Anforderungen (~ 9 Bytes pro Task);
- einfaches und verständliches Arbeitsprinzip;
- Determinismus des Verhaltens;
- Es wird weniger CPU-Zeit verschwendet.
- lässt Zugang zu Eisen;
- plattformunabhängig;
- geschrieben in C und einfach in C ++ zu verpacken;
brauchte mein eigenes Fahrrad.
Wie ich einmal beschrieben habe, sind Aufgaben dafür in einem Array von Zeigern auf Strukturen organisiert, in denen ein Zeiger auf eine Funktion und ihr Aufrufintervall gespeichert sind. Diese Gruppierung vereinfacht die Beschreibung des Spiels in separaten Phasen, wodurch Sie auch die Anzahl der Zweige reduzieren und die Aufgaben dynamisch wechseln können.Während des Startbildschirms werden beispielsweise 7 Aufgaben ausgeführt, und während des Spiels gibt es bereits 20 Aufgaben (alle Aufgaben sind in der Datei gameTasks.c beschrieben).Zunächst müssen Sie einige Makros definieren: #define T(a) a##Task #define TASK_N(a) const taskParams_t T(a) #define TASK(a,b) TASK_N(a) PROGMEM = {.pFunc=a, .timeOut=b} #define TASK_P(a) (taskParams_t*)&T(a) #define TASK_ARR_N(a) const tasksArr_t a##TasksArr[] #define TASK_ARR(a) TASK_ARR_N(a) PROGMEM #define TASK_END NULL
Die Taskdeklaration erstellt tatsächlich eine Struktur, initialisiert ihre Felder und platziert sie im ROM: TASK(updateBtnStates, 25);
Jede solche Struktur belegt 4 Bytes ROM (zwei pro Zeiger und zwei pro Intervall).Ein netter Bonus für Makros ist, dass es nicht funktioniert, mehr als eine eindeutige Struktur für jede Funktion zu erstellen.Nachdem wir die erforderlichen Aufgaben deklariert haben, fügen wir sie dem Array hinzu und legen sie auch im ROM ab: TASK_ARR( game ) = { TASK_P(updateBtnStates), TASK_P(playMusic), TASK_P(drawStars), TASK_P(moveShip), TASK_P(drawShip), TASK_P(checkFireButton), TASK_P(pauseMenu), TASK_P(drawPlayerWeapon), TASK_P(checkShipHealth), TASK_P(drawSomeGUI), TASK_P(checkInVaders), TASK_P(drawInVaders), TASK_P(moveInVaders), TASK_P(checkInVadersRespawn), TASK_P(checkInVadersRay), TASK_P(checkInVadersCollision), TASK_P(dropWeaponGift), TASK_END };
Wenn Sie das USE_DYNAMIC_MEM-Flag für den statischen Speicher auf 0 setzen, müssen Sie vor allem die Zeiger auf den Task-Speicher im RAM initialisieren und die maximale Anzahl der Zeiger festlegen, die ausgeführt werden sollen: ... tasksContainer_t tasksContainer; taskFunc_t tasksArr[MAX_GAME_TASKS]; ... initTasksArr(&tasksContainer, &tasksArr[0], MAX_GAME_TASKS); …
Aufgaben für die Ausführung festlegen: ... addTasksArray_P(gameTasksArr); …
Der Überlaufschutz wird durch das Flag USE_MEM_PANIC gesteuert. Wenn Sie sich über die Anzahl der Aufgaben sicher sind, können Sie ihn deaktivieren, um das ROM zu speichern.Es bleibt nur der Handler auszuführen: ... runTasks(); ...
Im Inneren befindet sich eine Endlosschleife, die die Grundlogik enthält. Sobald er sich darin befindet, wird der Stapel dank "__attribute__ ((noreturn))" ebenfalls wiederhergestellt.In der Schleife werden die Elemente des Arrays abwechselnd auf die Notwendigkeit überprüft, die Aufgabe nach Ablauf des Intervalls aufzurufen.Die Intervallzählung wurde auf der Basis von timer0 als System mit einem Quantum von 1 ms durchgeführt ...Trotz der erfolgreichen zeitlichen Verteilung der Aufgaben überlappten sie sich manchmal (Jitter), was zu einem kurzfristigen Verblassen von allem und jedem im Spiel führte.Es musste definitiv entschieden werden, aber wie? Wie das nächste Mal alles profiliert wurde, aber versuchen Sie vorerst, das Osterei in der Quelle zu finden.Das Ende
Mit vielen Tricks (und vielen weiteren, die ich nicht beschrieben habe) stellte sich heraus, dass alles in ein 24-KB-ROM und 1500 Byte RAM passte. Wenn Sie Fragen haben, beantworte ich diese gerne.Für diejenigen, die kein Osterei gefunden oder nicht gesucht haben:zur Seite graben: void invadersMagicRespawn(void) { for(auto &alien : aliens) { if(!alien.alive) { alien.respawnTime = 1; } } }
Nichts Besonderes, oder?Raaaaazvorachivaem Makro-InvasorenMagicRespawn: void action() { tftSetTextSize(1); for(;;) { tftSetCP437(RN & 1); tftSetTextColorBG((((RN % 192 + 64) & 0xFC) << 3), COLOR_BLACK); tftDrawCharInt(((RN % 26) * 6), ((RN & 15) * 8), (RN % 255)); tftPrintAt_P(32, 58, (const char *)creditP0); } } a(void) { for(auto &alien : aliens) { if(!alien.alive) { alien.respawnTime = 1; } } }
«(void)» , «action()» 10 , «disablePause();». «Matrix Falling code» . 130 ROM.
Zum Erstellen und Ausführen reicht es aus, den Ordner (oder Link) "esploraAPI" in "/ arduino / library /" abzulegen.Referenzen:
PS Sie können sehen und hören, wie alles etwas später aussieht, wenn ich ein akzeptables Video mache.