Nortis (ehemals Notris) ist ein selbst erstelltes PSX-Spiel, das mit modernen Tools in C geschrieben wurde. Es ist vollständig auf Originalhardware spielbar und wird von PSNoobSDK unterstützt.
Sehen Sie sich hier die PSX-Codebasis an.
Letztes Jahr habe ich eine seltene, schwarze PlayStation 1 in die Hände bekommen. Diese heißt Net Yaroze und ist eine spezielle Konsole, auf der sowohl Homebrew-Spiele als auch gewöhnliche PSX-Titel gespielt werden können. Es war Teil eines speziellen Sony-Projekts, um Bastler und Studenten für die Spielebranche zu begeistern.
Die Zahl der Yaroze-Spiele war sehr begrenzt, da Sony nicht wollte, dass Schlafzimmer-Programmierer mit kommerziellen Entwicklern konkurrierten. Sie konnten nur auf anderen Yarozes oder auf speziellen Demo-Discs abgespielt werden. Sie mussten vollständig in den System-RAM passen, ohne Zugriff auf die CD-ROM. Trotz dieser Einschränkungen förderte die Yaroze eine leidenschaftliche Community von Indie-Entwicklern.
Und jetzt hatte ich mein eigenes. Was mich zum Nachdenken brachte: Wie war es eigentlich, ein PlayStation-Spiel zu schreiben?
Hier geht es darum, wie ich selbst ein einfaches Homebrew-PSX-Spiel geschrieben habe, wobei ich eine Open-Source-Version der Bibliotheken verwendet habe, aber immer noch auf Originalhardware lief und in klassischem C geschrieben war.
Überspringen Sie diesen Abschnitt
PSX-Spiele wurden normalerweise in C auf Windows 9X-Workstations geschrieben. Das offizielle Devkit bestand aus einem Paar ISA-Erweiterungskarten, die in ein gewöhnliches IBM-PC-Motherboard gesteckt wurden und den gesamten PSX-System-Chipsatz, den Videoausgang und zusätzlichen RAM (8 MB statt 2 MB) enthielten. Dadurch wurden TTY- und Debugger-Ausgaben für den Hostcomputer bereitgestellt.
Vielleicht haben Sie schon von blauen PlayStations gehört. Diese dienten eher der Qualitätssicherung als der Entwicklung und sind identisch mit Einzelhandelsgeräten, außer dass sie gebrannte CD-ROMs abspielen können. Allerdings hat mindestens ein Unternehmen ein spezielles Add-on verkauft, um sie in Devkits umzuwandeln:
Das Design war sehr entwicklerfreundlich. Sie könnten Ihr Spiel auf CRT mit normalen Controllern spielen, während Sie GDB-Haltepunkte auf Ihrem Windows 95-PC durchgehen und durch ein dickes Lehrbuch mit C-SDK-Funktionen blättern.
Im Prinzip könnte ein PSX-Entwickler vollständig in C arbeiten. Das SDK bestand aus einer Reihe von C-Bibliotheken namens PSY-Q und einem Compilerprogramm ccpsx
, das eigentlich nur ein Frontend über GCC war. Dies unterstützte eine Reihe von Optimierungen wie Code-Inlining und Loop-Unrolling, obwohl leistungskritische Abschnitte immer noch eine handoptimierte Assemblierung rechtfertigten.
(Über diese Optimierung können Sie in diesen SCEE-Konferenzfolien nachlesen.)
C++ wurde von ccpsx
unterstützt, hatte jedoch den Ruf, „aufgeblähten“ Code sowie langsamere Kompilierungszeiten zu generieren. Tatsächlich war C die Verkehrssprache der PSX-Entwicklung, aber einige Projekte verwendeten dynamische Skriptsprachen zusätzlich zu einer Basis-Engine. Metal Gear Solid nutzte beispielsweise TCL für das Level-Scripting; und die Final Fantasy -Spiele gingen noch einen Schritt weiter und implementierten ihre eigenen Bytecode-Sprachen für Schlachten, Feld- und Minispielsysteme. (Mehr dazu erfahren Sie hier).
( Weitere Informationen finden Sie unter https://www.retroreversing.com/official-playStation-devkit .)
Überspringen Sie diesen Abschnitt
Aber ich kam aus einer ganz anderen Perspektive dazu: als Softwareentwickler im Jahr 2024, der hauptsächlich an Webanwendungen arbeitete. Meine Berufserfahrung habe ich fast ausschließlich in Hochsprachen wie JavaScript und Haskell gesammelt; Ich habe mich ein wenig mit OpenGL und C++ beschäftigt, aber das moderne C++ ist fast eine völlig andere Sprache als C.
Ich wusste, dass es PSX-SDKs für Sprachen wie Rust gibt, aber ich wollte den Geschmack der „echten“ PSX-Programmierung erleben, wie sie in den 90er-Jahren praktiziert wurde. Es wären also moderne Toolchains und Open-Source-Bibliotheken, aber durchgängig C.
Das Spiel musste etwas 2D sein, das in ein paar Tagen als Prototyp erstellt werden konnte. Ich entschied mich für einen Tetris-Klon – ich dachte, das wäre komplex genug, um das zu erleben, was ich wollte.
Der erste Schritt bestand darin, einen Prototyp in einer bekannten Technologie zu bauen. Dies würde es mir ermöglichen, das grundlegende Design festzulegen, und dann könnte die Logik Stück für Stück in C übersetzt werden.
Als Webentwickler war JavaScript die naheliegendste Technologie für das Prototyping: Es ist einfach, prägnant, leicht zu debuggen und verfügt über die HTML5- <canvas>
-Grafik-API. Die Dinge fügten sich sehr schnell zusammen
Gleichzeitig hatte ich Bedenken, dass die Portierung weiterer JavaScript-Funktionen auf höherer Ebene schwierig sein würde. Alles, was Klassen oder Abschlüsse verwendet, müsste komplett neu geschrieben werden, daher habe ich darauf geachtet, mich auf eine einfache, prozedurale Teilmenge der Sprache zu beschränken.
Tatsächlich hatte ich bei der Übernahme dieses Projekts einen Hintergedanken: Es war ein Vorwand, um endlich C zu lernen. Die Sprache spielte in meinem Kopf eine große Rolle und ich begann, einen Minderwertigkeitskomplex zu entwickeln, weil ich sie nicht kannte.
C hat einen einschüchternden Ruf und ich fürchtete Horrorgeschichten über baumelnde Zeiger, falsch ausgerichtete Lesevorgänge und den gefürchteten segmentation fault
. Genauer gesagt: Ich hatte Angst, dass ich feststellen würde, dass ich eigentlich kein besonders guter Programmierer war, wenn ich versuchte, C zu lernen und scheiterte.
Der Einfachheit halber dachte ich, ich könnte SDL2 verwenden, um die Eingaben und Grafiken zu verarbeiten und für meine Desktop-Umgebung (MacOS) zu kompilieren. Das würde mir einen schnellen Build-/Debug-Zyklus ermöglichen und die Lernkurve so sanft wie möglich gestalten.
Trotz meiner Befürchtungen hat mir C unglaublich viel Spaß gemacht. Sehr schnell hat es bei mir „Klick“ gemacht. Sie beginnen mit sehr einfachen Grundelementen – Strukturen, Zeichen, Funktionen – und bauen sie zu Abstraktionsebenen auf, um sich schließlich auf einem kompletten Arbeitssystem wiederzufinden.
Die Portierung des Spiels dauerte nur ein paar Tage und ich war mit meinem ersten echten C-Projekt sehr zufrieden. Und ich hatte keinen einzigen Segfault!
Es war eine Freude, mit SDL zu arbeiten, aber es gab ein paar Aspekte, die eine dynamische Speicherzuweisung erforderten. Dies wäre auf der PlayStation ein Tabu, da dort der vom PSX-Kernel bereitgestellte malloc
nicht richtig funktioniert. Und die Grafikpipeline wäre ein noch größerer Sprung ...
Wenn es um PlayStation Homebrew geht, gibt es zwei Hauptoptionen für Ihr SDK. Entweder:
Es gibt ein paar andere Optionen wie C++ Psy-Qo , und Sie können sogar auf ein SDK verzichten, nur um speicherzugeordnete E/A selbst durchzuführen – aber dafür war ich nicht mutig genug.
Das größte Problem bei Psy-Q ist, dass es sich auch 30 Jahre später immer noch um Sony-eigenen Code handelt. Rechtlich gesehen ist jedes damit gebaute Homebrew gefährdet. Das ist es, was das Portal64-Demake zum Scheitern brachte: Es verlinkte statisch libultra
, Nintendos proprietäres N64-SDK.
Aber um ehrlich zu sein, der Hauptgrund, warum ich mich für PSNoobSDK entschieden habe, war, dass es sehr gut dokumentiert und einfach einzurichten ist. Die API ist Psy-Q sehr ähnlich: Tatsächlich konnte ich für viele Funktionen einfach die gedruckten Referenzen konsultieren, die mit meinem Yaroze geliefert wurden.
Wenn die Verwendung eines nicht authentischen SDK den PSX-Puristen in Ihnen beleidigt, können Sie jetzt voller Abscheu mit dem Lesen aufhören.
Meine erste Aufgabe war eine Art Hallo-Welt: zwei Quadrate auf farbigem Hintergrund. Klingt einfach, oder?
Überspringen Sie diesen Abschnitt
(*Einige davon sind vereinfacht. Eine aussagekräftigere Anleitung finden Sie im PSNoobSDK-Tutorial.)
Stellen Sie sich den PSX-VRAM zunächst als eine große 1024 x 512 große Leinwand mit 16-Bit-Pixeln vor. Insgesamt ergibt das 1 Megabyte Speicher, der sich Framebuffer und Texturen teilen. Wir können die Auflösung des Ausgabe-Framebuffers wählen – wenn wir gierig sind, sogar bis zu 640 x 480 Pixel – aber mehr Auflösung = weniger Texturen.
Die meisten PSOne-Spiele (und... Spiele im Allgemeinen) nutzen die Idee des Dual-Buffer-Renderings: Während ein Frame vorbereitet wird, wird der andere an den Bildschirm gesendet. Wir müssen also zwei Bildpuffer zuweisen:
(Jetzt können Sie sehen, warum 640 x 480 nicht praktikabel ist – es gibt nicht genug Platz für zwei 480p-Puffer. Dieser Modus KANN jedoch für Dinge wie das PSX-Startlogo verwendet werden, das nicht viel Animation benötigt.)
Die Puffer (abwechselnd als Anzeige- und Zeichenumgebungen bezeichnet) werden bei jedem Frame ausgetauscht. Die meisten PSX-Spiele zielen auf 30 fps (in Nordamerika), aber der tatsächliche VSync-Interrupt liegt bei 60 Hz. Einige Spiele schaffen es, mit vollen 60 fps zu laufen – da fallen mir da Tekken 3 und Kula World (Roll Away) ein – aber dann muss man natürlich in der Hälfte der Zeit rendern. Denken Sie daran, dass wir nur über eine Rechenleistung von 33 MHz verfügen.
Aber – wie funktioniert der Zeichenprozess? Dies übernimmt die GPU, allerdings funktioniert die PSX-GPU ganz anders als eine moderne Grafikkarte. Im Wesentlichen wird der GPU bei jedem Frame eine geordnete Liste von Grafikpaketen oder Befehlen gesendet. „Zeichnen Sie hier ein Dreieck“, „Laden Sie diese Textur, um das nächste Quad zu häuten“ und so weiter.
Die GPU führt keine 3D-Transformationen durch; Das ist die Aufgabe des GTE-Coprozessors (Geometry Transform Engine). Die GPU-Befehle stellen reine 2D-Grafiken dar, die bereits von 3D-Hardware manipuliert wurden.
Das heißt, der Pfad eines PSX-Pixels sieht wie folgt aus:
Im Pseudocode läuft die PSX-Frame-Schleife (im Grunde) so ab
FrameBuffer [0, 1]
OrderingTable [0, 1]
id = 1 // flips every frame
loop {
// Game logic
// Construct the next screen by populating the current ordering table
MakeGraphics(OrderingTable[id])
// Wait for last draw to finish; wait for vertical blank
DrawSync()
VSync()
// The other frame has finished drawing in background, so display it
SetDisplay(Framebuffer[!id])
// Start drawing current frame
SetDrawing(Framebuffer[id])
// Send ordering table contents to GPU via DMA
Transfer(OrderingTable[id])
// Flip
id = !id
}
Daran können Sie erkennen, dass, während Bild 1 auf dem Bildschirm angezeigt wird, Bild 2 noch gezeichnet wird und Bild 3 möglicherweise noch vom Programm selbst „konstruiert“ wird. Dann senden wir nach DrawSync/VSync Frame 2 an den Fernseher und erhalten von der GPU die Zeichnung von Frame 3.
Wie bereits erwähnt, handelt es sich bei der GPU um eine vollständig 2D-Hardware, sie kennt keine Z-Koordinaten im 3D-Raum. Es gibt keinen „Z-Puffer“, um Verdeckungen zu beschreiben – also welche Objekte sich vor anderen befinden. Wie werden die Artikel vor anderen sortiert?
Die Funktionsweise besteht darin, dass die Bestelltabelle aus einer umgekehrt verknüpften Kette von Grafikbefehlen besteht. Diese werden von hinten nach vorne durchlaufen, um den Algorithmus des Malers zu implementieren.
Genauer gesagt handelt es sich bei der Bestelltabelle um eine umgekehrt verknüpfte Liste. Jedes Element hat einen Zeiger auf das vorherige Element in der Liste, und wir fügen Grundelemente hinzu, indem wir sie in die Kette einfügen. Im Allgemeinen werden OTs als festes Array initialisiert, wobei jedes Element im Array eine „Ebene“ oder Ebene in der Anzeige darstellt. OTs können zur Implementierung komplexer Szenen verschachtelt werden.
Das folgende Diagramm hilft, es zu erklären (Quelle)
Dieser Ansatz ist nicht perfekt und manchmal zeigt die PSX-Geometrie seltsame Clippings, da sich jedes Poly nur an einem einzigen „Z-Index“ im Bildschirmbereich befinden kann, aber er funktioniert für die meisten Spiele gut genug. Heutzutage werden solche Einschränkungen als Teil des besonderen Charmes des PSX angesehen.
Überspringen Sie diesen Abschnitt
Wir haben viel über Theorie gesprochen – wie sieht das in der Praxis aus?
In diesem Abschnitt wird nicht der gesamte Code Zeile für Zeile durchgegangen, er soll Ihnen aber einen Vorgeschmack auf PSX-Grafikkonzepte geben. Wenn Sie den vollständigen Code sehen möchten, gehen Sie zu hello-psx/main.c
.
Wenn Sie kein Programmierer sind, können Sie auch gerne weitermachen. Dies ist nur für neugierige Technikfreaks.
Das erste, was wir brauchen, sind einige Strukturen, die unsere Puffer enthalten. Wir werden einen RenderContext
haben, der zwei RenderBuffers
enthält, und jeder RenderBuffer
wird Folgendes enthalten:
displayEnv
(gibt den VRAM-Bereich des aktuellen Anzeigepuffers an)drawEnv
(gibt den VRAM-Bereich des aktuellen Zeichenpuffers an)orderingTable
(umgekehrt verknüpfte Liste, die Zeiger auf Grafikpakete enthält)primitivesBuffer
(Strukturen für Grafikpakete/Befehle – einschließlich aller Polygone) #define OT_SIZE 16
#define PACKETS_SIZE 20480
typedef struct {
DISPENV displayEnv ;
DRAWENV drawEnv ;
uint32_t orderingTable [ OT_SIZE ];
uint8_t primitivesBuffer [ PACKETS_SIZE ];
} RenderBuffer ;
typedef struct {
int bufferID ;
uint8_t * p_primitive ; // next primitive
RenderBuffer buffers [ 2 ];
} RenderContext ;
static RenderContext ctx = { 0 };
Bei jedem Frame invertieren wir die bufferID
was bedeutet, dass wir nahtlos an einem Frame arbeiten können, während der andere angezeigt wird. Ein wichtiges Detail besteht darin, dass p_primitive
ständig auf das nächste Byte im aktuellen primitivesBuffer
verweist. Es ist unbedingt erforderlich , dass dieser Wert jedes Mal erhöht wird, wenn ein Grundelement zugewiesen wird, und am Ende jedes Frames zurückgesetzt wird.
Eigentlich müssen wir zunächst unsere Anzeige- und Zeichenumgebungen in umgekehrter Konfiguration einrichten, sodass DISP_ENV_1
denselben VRAM wie DRAW_ENV_0
verwendet und umgekehrt
// x y width height
SetDefDispEnv ( DISP_ENV_0 , 0 , 0 , 320 , 240 );
SetDefDispEnv ( DISP_ENV_1 , 0 , 240 , 320 , 240 );
SetDefDrawEnv ( DRAW_ENV_0 , 0 , 240 , 320 , 240 );
SetDefDrawEnv ( DRAW_ENV_1 , 0 , 0 , 320 , 240 );
Ich fasse mich hier ziemlich zusammen – aber von hier aus läuft im Grunde jedes Bild wie folgt ab
while ( 1 ) {
// do game stuff... create graphics for next frame...
// at the end of loop body
// wait for drawing to finish, wait for next vblank interval
DrawSync ( 0 );
VSync ( 0 );
DISPENV * p_dispenv = & ( ctx . buffers [ ctx . bufferID ]. displayEnv );
DRAWENV * p_drawenv = & ( ctx . buffers [ ctx . bufferID ]. drawEnv );
uint32_t * p_ordertable = ctx . buffers [ ctx . bufferID ]. orderingTable ;
// Set display and draw environments
PutDispEnv ( p_dispenv );
PutDrawEnv ( p_drawenv );
// Send ordering table commands to GPU via DMA, starting from the end of the table
DrawOTagEnv ( p_ordertable + OT_SIZE - 1 , p_drawEnv );
// Swap buffers and clear state for next frame
ctx . bufferID ^= 1 ;
ctx . p_primitive = ctx . buffers [ ctx . bufferID ]. primitivesBuffer ;
ClearOTagR ( ctx . buffers [ 0 ]. orderingTable , OT_SIZE );
}
Das könnte eine Menge sein, die man in sich aufnehmen muss. Machen Sie sich keine Sorgen.
Wenn Sie das wirklich verstehen wollen, schauen Sie sich am besten hello-psx/main.c
an. Alles ist ziemlich ausführlich kommentiert. Alternativ können Sie auch das PSNoobSDK-Tutorial durchgehen. Es ist ziemlich knapp und klar geschrieben.
Nun... wie zeichnen wir Sachen? Wir schreiben Strukturen in unseren Primitivpuffer. Dieser Puffer ist nur als große alte Liste von chars
typisiert, also wandeln wir ihn in unsere Form-/Befehlsstruktur um und verschieben dann den Primitivpufferzeiger mit sizeof
:
// Create a tile primitive in the primitive buffer
// We cast p_primitive as a TILE*, so that its char used as the head of the TILE struct
TILE * p_tile = ( TILE * ) p_primitive ;
setTile ( p_tile ); // very very important to call this macro
setXY0 ( p_tile , x , y );
setWH ( p_tile , width , width );
setRGB0 ( p_tile , 252 , 32 , 3 );
// Link into ordering table (z level 2)
int z = 2 ;
addPrim ( ordering_table [ buffer_id ] + z , p_primitive );
// Then advance buffer
ctx . p_primitive += sizeof ( TILE );
Wir haben gerade ein gelbes Quadrat eingefügt! ? Versuchen Sie, Ihre Aufregung zu zügeln.
Überspringen Sie diesen Abschnitt
An diesem Punkt meiner Reise hatte ich eigentlich nur ein „Hallo Welt“-Demoprogramm mit grundlegenden Grafiken und Controller-Eingaben. Sie können dem Code in hello-psx
entnehmen, dass ich so viel wie möglich dokumentiert habe, wirklich zu meinem eigenen Vorteil. Ein funktionierendes Programm war ein positiver Schritt, aber kein echtes Spiel.
Es war Zeit , real zu werden .
Unser Spiel muss den Punktestand zeigen.
Die PSX bietet nicht wirklich viel Textwiedergabe. Es gibt eine Debug-Schriftart (siehe oben), aber sie ist äußerst einfach – für die Entwicklung und nicht für viel anderes.
Stattdessen müssen wir eine Schrifttextur erstellen und diese zum Skinieren von Quads verwenden. Ich habe mit https://www.piskelapp.com/ eine Monospace-Schriftart erstellt und diese als transparentes PNG exportiert:
PSX-Texturen werden in einem Format namens TIM gespeichert. Jede TIM-Datei besteht aus:
Da der VRAM-Speicherort der Textur in die TIM-Datei „eingebrannt“ ist, benötigen Sie ein Tool zum Verwalten Ihrer Texturspeicherorte. Ich empfehle hierfür https://github.com/Lameguy64/TIMedit.
Von da an haben wir nur noch die Funktion, eine Reihe von Quads zu häuten, wobei die UV-Offsets auf jedem ASCII-Wert basieren.
Wir brauchen einen Platz, in den die Teile passen. Es wäre einfach, dafür ein langweiliges weißes Rechteck zu verwenden, aber ich wollte etwas, das sich mehr anfühlt ... PlayStation
Unsere Benutzeroberfläche kommt zusammen. Was ist mit den Stücken?
Jetzt kommt ein wichtiges visuelles Design. Idealerweise sollte sich jeder Ziegel optisch durch scharfe, schattierte Kanten unterscheiden. Wir machen das mit zwei Dreiecken und einem Viereck:
Bei 1-facher nativer Auflösung wäre der Effekt weniger klar, sieht aber immer noch schön und klobig aus:
Im ersten Prototyp meines Spiels habe ich ein vollständig naives Rotationssystem implementiert, das den Block tatsächlich um 90 Grad um einen Mittelpunkt dreht. Es stellt sich heraus, dass dies eigentlich kein guter Ansatz ist, da die Blöcke dadurch „wackeln“ und sich beim Drehen nach oben und unten verschieben:
Stattdessen sind die Drehungen so fest codiert, dass sie „schön“ statt „genau“ sind. Ein Stück wird in einem Raster von 4x4 Zellen definiert und jede Zelle kann gefüllt oder ungefüllt sein. Es gibt 4 Rotationen. Daher: Rotationen können nur Arrays aus vier 16-Bit-Zahlen sein. Was so aussieht:
/**
* Example: T block
*
* As a grid:
*
* .X.. -> 0100
* XXX. -> 1110
* .... -> 0000
* .... -> 0000
*
* binary = 0b0100111000000000
* hexadecimal = 0x4E00
*
*/
typedef int16_t ShapeBits ;
static ShapeBits shapeHexes [ 8 ][ 4 ] = {
{ 0 }, // NONE
{ 0x0F00 , 0x4444 , 0x0F00 , 0x4444 }, // I
{ 0xE200 , 0x44C0 , 0x8E00 , 0xC880 }, // J
{ 0xE800 , 0xC440 , 0x2E00 , 0x88C0 }, // L
{ 0xCC00 , 0xCC00 , 0xCC00 , 0xCC00 }, // O
{ 0x6C00 , 0x8C40 , 0x6C00 , 0x8C40 }, // S
{ 0x0E40 , 0x4C40 , 0x4E00 , 0x4640 }, // T
{ 0x4C80 , 0xC600 , 0x4C80 , 0xC600 }, // Z
};
Das Extrahieren der Zellwerte ist nur ein Fall einer einfachen Bitmaskierung:
#define GRID_BIT_OFFSET 0x8000;
int blocks_getShapeBit ( ShapeBits s , int y , int x ) {
int mask = GRID_BIT_OFFSET >> (( y * 4 ) + x );
return s & mask ;
}
Die Dinge kommen jetzt mit Schwung zusammen.
An diesem Punkt stieß ich auf einen Haken: die Randomisierung. Damit sich das Spiel lohnt, müssen die Spielsteine in zufälliger Reihenfolge auftauchen, aber bei Computern ist die Zufallsauswahl schwierig. Auf meiner MacOS-Version konnte ich den Zufallszahlengenerator mit der Systemuhr „seeden“, aber die PSX verfügt nicht über eine interne Uhr.
Stattdessen besteht eine Lösung vieler Spiele darin, den Spieler den Samen erstellen zu lassen. Das Spiel zeigt einen Begrüßungs- oder Titelbildschirm mit Text wie „Drücken Sie Start, um zu beginnen“ an, und dann wird das Timing von diesem Tastendruck übernommen, um den Startwert zu erstellen.
Ich habe eine „Grafik“ erstellt, indem ich einige binär codierte int32
s deklariert habe, bei denen jedes 1
Bit ein „Pixel“ in einer Reihe von Bausteinen wäre:
Ich wollte, dass sich die Linien allmählich in der Sicht auflösten. Zuerst brauchte ich eine Funktion, die effektiv „verfolgt“, wie oft sie aufgerufen wurde. C macht dies mit dem Schlüsselwort static
einfach – wenn es innerhalb einer Funktion verwendet wird, werden beim nächsten Aufruf dieselbe Speicheradresse und derselbe Inhalt wiederverwendet.
Dann gibt es in derselben Funktion eine Schleife, die die x/y-Werte des „Gitters“ durchgeht und entscheidet, ob genügend Ticks passiert sind, um das „Pixel“ anzuzeigen:
void ui_renderTitleScreen () {
static int32_t titleTimer = 0 ;
titleTimer ++ ;
// For every 2 times (2 frames) this function is called, ticks increases by 1
int32_t ticks = titleTimer / 2 ;
// Dissolve-in the title blocks
for ( int y = 0 ; y < 5 ; y ++ ) {
for ( int x = 0 ; x < 22 ; x ++ ) {
int matrixPosition = ( y * 22 ) + x ;
if ( matrixPosition > ticks ) {
break ; // because this 'pixel' of the display is not to be displayed yet
}
int32_t titleLine = titlePattern [ y ];
int32_t bitMask = titleMask >> x ;
if ( titleLine & bitMask ) { // there is a 'pixel' at this location to show
ui_renderBlock ( /* skip boring details */ );
}
}
}
}
Wir sind jetzt fast da.
Klassische PSX-Spiele starten in zwei Phasen: zuerst der Sony Computer Entertainment-Bildschirm, dann das PSX-Logo. Aber wenn wir das hello-psx
-Projekt kompilieren und ausführen, ist das nicht der Fall. Der zweite Bildschirm ist nur schwarz. Warum ist das so?
Nun, der SCE-Splash stammt aus dem BIOS, ebenso wie der PSX-Bootsound, aber das berühmte Logo ist tatsächlich Teil der Disc-Lizenzdaten. Es dient als „Echtheitssiegel“ – jeder, der ein Spiel raubkopiert, kopiert sowohl das geistige Eigentum von Sony als auch das des Herausgebers. Dies gab Sony mehr rechtliche Instrumente, um gegen Softwarepiraterie vorzugehen.
Wenn wir möchten, dass unser Spiel das Logo zeigt, müssen wir eine aus einer ISO extrahierte Lizenzdatei bereitstellen, aber aus Urheberrechtsgründen müssen wir sie .gitignore
.
< license file = " ${PROJECT_SOURCE_DIR}/license_data.dat " />
Okay. Jetzt sind wir bereit.
Alles begann mit einem Impulskauf, meiner schwarzen Yaroze PlayStation. Ironischerweise würde es mein Spiel nicht wirklich spielen, da es immer noch über seine Anti-Piraterie-Hardware verfügt. Ich hatte keine Lust, einen Modchip auf solch einem unbezahlbaren Stück PSX-Geschichte zu installieren – nicht mit meinen Lötkenntnissen.
Stattdessen musste ich eine modifizierte graue PlayStation aufspüren, die immer noch über ein anständiges Laufwerk verfügte. Ich dachte mir, dass der Sinn meines Projekts darin bestand, ein echtes PlayStation-Spiel zu schreiben, und das bedeutete, eine echte PlayStation zu verwenden.
Ich musste auch die richtigen Medien finden. Der PSX-Laser ist ziemlich wählerisch und moderne CD-Rs neigen dazu, viel weniger zu reflektieren als gepresste Discs. Meine ersten Versuche mit CDs mit Lebensmittelgeschichten waren Zeitverschwendung, und innerhalb von etwa zwei Wochen habe ich viele Untersetzer erstellt.
Das war ein dunkler Moment. Hatte ich es bis hierher geschafft, nur um beim Brennen der CD scheitern zu können?
Nach einigen Wochen bekam ich einige besondere Aktien von JVC Taiyo Yuden in die Hände. Soweit ich das lesen konnte, handelte es sich hierbei um recht spezielle Geräte, die typischerweise in industriellen Anwendungen eingesetzt werden. Ich habe die erste CD auf den Plattenteller gebrannt und mit dem Schlimmsten gerechnet.
Das war der Moment der Wahrheit:
Die Startsequenz der PlayStation dröhnte aus meinen winzigen Monitorlautsprechern und das klassische „PS“-Logo plätscherte in lebendiger Auflösung von 640 x 480 über den Bildschirm. Das BIOS hatte eindeutig etwas auf dieser CD gefunden, aber nach diesem Zeitpunkt könnte vieles fehlschlagen. Der Bildschirm wurde schwarz und ich lauschte auf das verräterische Klicken-Klick-Klick eines Laufwerksfehlers.
Stattdessen begannen nach und nach kleine farbige Quadrate aus der Dunkelheit hereinzublinzeln. Zeile für Zeile buchstabierten sie ein Wort: NOTRIS
. Dann: PRESS START TO BEGIN
. Der Text lockte mich. Was würde als nächstes passieren?
Natürlich eine Partie Tetris. Warum war ich überrascht? Das Schreiben eines eigenen PlayStation-Spiels in C ist eigentlich ganz einfach: Man muss lediglich keinerlei Fehler machen . Das ist Rechenarbeit für Sie, vor allem die Low-Level-Sachen. Es ist hart und scharf und wunderschön. Moderne Computer haben weichere Kanten, aber das Wesentliche hat sich nicht geändert.
Diejenigen von uns, die Computer lieben, müssen mit uns etwas falsch machen, eine Irrationalität unserer Rationalität, eine Möglichkeit, alle Beweise unserer Augen und Ohren zu leugnen, dass die feindliche Kiste aus Silizium tot und unnachgiebig ist. Und durch raffinierte Maschinen entsteht die Illusion, dass sie lebt.