Cet article absurde est dédié au développement d’un jeu pour l’ancien ordinateur ZX Spectrum en C. Jetons un coup d’œil au beau mec :

Sa production a commencé en 1982 et a été produite jusqu’en 1992. Caractéristiques techniques de la machine : processeur Z80 8 bits, 16-128 Ko de mémoire et autres extensions, comme la puce sonore AY-3-8910.
Dans le cadre du concours Yandex Retro Games Battle 2019 pour cette machine, j’ai écrit un jeu appelé Interceptor 2020. Comme je n’ai pas eu le temps d’apprendre le langage assembleur pour le Z80, j’ai décidé de le développer en C. En tant que chaîne d’outils, j’ai choisi un ensemble prêt à l’emploi – z88dk, qui contient des compilateurs C et de nombreuses bibliothèques auxiliaires pour accélérer la mise en œuvre des applications pour Spectrum. Il prend également en charge de nombreuses autres machines Z80, telles que les calculatrices MSX et Texas Instruments.
Ensuite, je décrirai mon survol superficiel de l’architecture informatique, la chaîne d’outils z88dk, et montrerai comment j’ai réussi à mettre en œuvre l’approche POO et à utiliser des modèles de conception.
Fonctionnalités d’installation
L’installation de z88dk doit être effectuée conformément au manuel du référentiel, cependant, pour les utilisateurs d’Ubuntu, je voudrais noter une fonctionnalité – Si vous avez déjà installé des compilateurs pour Z80 à partir de packages deb, vous devez les supprimer, puisque z88dk y accédera à partir du dossier bin par défaut en raison de l’incompatibilité des versions du compilateur de la chaîne d’outils, vous ne pourrez probablement rien compiler.< /p>
Bonjour tout le monde
Écrire Hello World est très simple :
#include
void main()
{
printf("Hello World");
}
Il est encore plus simple de compiler un fichier tap :
zcc +zx -lndos -create-app -o helloworld helloworld.c
Pour l’exécuter, utilisez n’importe quel émulateur ZX Spectrum prenant en charge les fichiers Tap, par exemple en ligne :
http://jsspeccy.zxdemo.org/
Dessinez sur l’image en plein écran
tl;dr Les images sont dessinées en tuiles, des tuiles de taille 8×8 pixels, les tuiles elles-mêmes sont intégrées dans la police Spectrum, puis l’image est imprimée sous forme de ligne à partir des index.
La bibliothèque de sortie de sprites et de tuiles sp1 génère des tuiles en utilisant UDG. L’image est traduite en un ensemble d’UDG individuels (tuiles), puis assemblés sur l’écran à l’aide d’indices. Il ne faut pas oublier que UDG est utilisé pour afficher du texte, et si votre image contient un très grand ensemble de tuiles (par exemple, plus de 128 tuiles), alors vous devrez dépasser les limites de l’ensemble et effacer le spectre par défaut. fonte. Pour contourner cette limitation, j’ai utilisé une base de 128 – 255 en simplifiant les images tout en laissant la police d’origine en place. À propos de la simplification des images ci-dessous.
Pour dessiner des images en plein écran, vous devez vous armer de trois utilitaires :
Gimp
img2spec
png2c-z88dk
Il existe un moyen pour les vrais hommes ZX, les vrais guerriers rétro, celui-ci consiste à ouvrir un éditeur graphique à l’aide de la palette Spectrum, connaissant les caractéristiques de la sortie de l’image, à la préparer manuellement et à la télécharger en utilisant png2c-z88dk ou png2scr.< /p>
Le moyen le plus simple – prenez une image 32 bits, changez le nombre de couleurs sur 3-4 dans Gimp, modifiez-la légèrement, puis importez-la dans img2spec afin de ne pas travailler manuellement avec les restrictions de couleur, exportez png et convertissez-la en tableau C en utilisant png2c- z88dk.
N’oubliez pas que pour une exportation réussie, chaque vignette ne peut pas contenir plus de deux couleurs.
En conséquence, vous recevrez un fichier h contenant le nombre de tuiles uniques, s’il y en a plus de ~128, alors simplifiez l’image dans Gimp (augmentez la répétabilité) et effectuez la procédure d’exportation sur une nouvelle. .
Après l’exportation, vous chargez littéralement la « police » des vignettes et imprimez le « texte » des index des vignettes sur l’écran. Vous trouverez ci-dessous un exemple de la « classe » de rendu :
// грузим шрифт в память
unsigned char *pt = fullscreenImage->tiles;
for (i = 0; i < fullscreenImage->tilesLength; i++, pt += 8) {
sp1_TileEntry(fullscreenImage->tilesBase + i, pt);
}
// ставим курсор в 0,0
sp1_SetPrintPos(&ps0, 0, 0);
// печатаем строку
sp1_PrintString(&ps0, fullscreenImage->ptiles);
Dessiner des sprites sur l’écran
Ensuite, je décrirai une méthode pour dessiner des sprites de 16 à 16 pixels sur l’écran. Je n’ai pas abordé l’animation et le changement de couleurs, parce que… C’est trivial qu’à ce stade déjà, comme je suppose, j’ai manqué de mémoire. Par conséquent, le jeu ne contient que des sprites monochromes transparents.
Nous dessinons une image png monochrome 16×16 dans Gimp, puis en utilisant png2sp1sprite nous la traduisons en un fichier d’assemblage asm, en code C nous déclarons des tableaux à partir du fichier d’assemblage et ajoutons le fichier au stade de l’assemblage.< /p>
Après l’étape de déclaration de la ressource sprite, celle-ci doit être ajoutée à l’écran à la position souhaitée, ci-dessous un exemple de code pour la « classe » de l’objet du jeu :
struct sp1_ss *bubble_sprite = sp1_CreateSpr(SP1_DRAW_MASK2LB, SP1_TYPE_2BYTE, 3, 0, 0);
sp1_AddColSpr(bubble_sprite, SP1_DRAW_MASK2, SP1_TYPE_2BYTE, col2-col1, 0);
sp1_AddColSpr(bubble_sprite, SP1_DRAW_MASK2RB, SP1_TYPE_2BYTE, 0, 0);
sp1_IterateSprChar(bubble_sprite, initialiseColour);
À partir des noms des fonctions, vous pouvez comprendre approximativement la signification de « – allouez de la mémoire pour le sprite, ajoutez deux colonnes 8×8, ajoutez une couleur pour le sprite.
La position du sprite est indiquée dans chaque image :
sp1_MoveSprPix(gameObject->gameObjectSprite, Renderer_fullScreenRect, gameObject->sprite_col, gameObject->x, gameObject->y);
Émulation de la POO
Il n’y a pas de syntaxe pour la POO en C, que devez-vous faire si vous le souhaitez toujours ? Vous devez connecter votre esprit et être éclairé par l’idée qu’il n’existe pas d’équipement POO ; tout revient finalement à l’une des architectures de machines, dans lesquelles il n’y a tout simplement aucun concept d’objet et d’autres abstractions qui lui sont associées.< /p>
Ce fait m’a empêché pendant très longtemps de comprendre pourquoi la POO est nécessaire, pourquoi il est nécessaire de l’utiliser si en fin de compte tout se résume au code machine.
Cependant, après avoir travaillé dans le développement de produits, j’ai découvert les plaisirs de ce paradigme de programmation, principalement, bien sûr, la flexibilité du développement, les mécanismes de protection du code, avec la bonne approche, la réduction de l’entropie, la simplification du travail d’équipe. Tous les avantages ci-dessus découlent de trois piliers : polymorphisme, encapsulation, héritage.
Il convient également de noter la simplification de la résolution des problèmes liés à l’architecture des applications, car 80 % des problèmes architecturaux ont été résolus par des informaticiens au siècle dernier et décrits dans la littérature sur les modèles de conception. Ensuite, je décrirai les façons d’ajouter une syntaxe de type POO au C.
Il est plus pratique de prendre des structures C comme base pour stocker les données d’une instance de classe. Bien sûr, vous pouvez utiliser un tampon d’octets, créer votre propre structure pour les classes, les méthodes, mais pourquoi réinventer la roue ? Après tout, nous réinventons déjà la syntaxe.
Données de classe
Exemple de champs de données « classe » GameObject :
struct GameObjectStruct {
struct sp1_ss *gameObjectSprite;
unsigned char *sprite_col;
unsigned char x;
unsigned char y;
unsigned char referenceCount;
unsigned char beforeHideX;
unsigned char beforeHideY;
};
typedef struct GameObjectStruct GameObject;
Enregistrez notre classe sous « GameObject.h », faites #include « GameObject.h » au bon endroit et utilisez-la.
Méthodes de classe
Compte tenu de l’expérience des développeurs du langage Objective-C, la signature d’une méthode de classe sera constituée de fonctions dans une portée globale, le premier argument sera toujours la structure des données, suivi des arguments de la méthode. Vous trouverez ci-dessous un exemple de « méthode » de la « classe » GameObject :
void GameObject_hide(GameObject *gameObject) {
gameObject->beforeHideX = gameObject->x;
gameObject->beforeHideY = gameObject->y;
gameObject->y = 200;
}
L’appel de méthode ressemble à ceci :
GameObject_hide(gameObject);
Les constructeurs et les destructeurs sont implémentés de la même manière. Il est possible d’implémenter un constructeur en tant qu’allocateur et initialiseur de champ, mais je préfère contrôler l’allocation et l’initialisation des objets séparément.
Travailler avec la mémoire
Gestion manuelle de la mémoire du formulaire à l’aide de malloc et free enveloppé dans des macros new et delete pour correspondre à C++ :
#define new(X) (X*)malloc(sizeof(X))
#define delete(X) free(X)
Pour les objets utilisés par plusieurs classes à la fois, une gestion semi-manuelle de la mémoire est implémentée sur la base d’un comptage de références, à l’image et à la ressemblance de l’ancien mécanisme Objective-C Runtime ARC :
void GameObject_retain(GameObject *gameObject) {
gameObject->referenceCount++;
}
void GameObject_release(GameObject *gameObject) {
gameObject->referenceCount--;
if (gameObject->referenceCount < 1) { sp1_MoveSprAbs(gameObject->gameObjectSprite, &Renderer_fullScreenRect, NULL, 0, 34, 0, 0);
sp1_DeleteSpr(gameObject->gameObjectSprite);
delete(gameObject);
}
}
Ainsi, chaque classe doit déclarer l’utilisation d’un objet partagé en utilisant retention, libérant ainsi la propriété via release. La version moderne d’ARC utilise des appels automatiques de rétention/libération.
Le son !
Le Spectrum dispose d’un tweeter capable de reproduire de la musique 1 bit ; les compositeurs de l’époque étaient capables de reproduire jusqu’à 4 canaux sonores simultanément.
Le Spectrum 128k contient une puce sonore distincte AY-3-8910, qui peut lire de la musique de suivi.
Une bibliothèque est proposée pour utiliser le tweeter dans le z88dk
Ce qui reste à apprendre
Je souhaitais me familiariser avec Spectrum, implémenter le jeu à l’aide du z88dk et apprendre beaucoup de choses intéressantes. J’ai encore beaucoup à apprendre, par exemple l’assembleur Z80, car il me permet d’utiliser toute la puissance du Spectrum, de travailler avec des banques de mémoire et de travailler avec la puce sonore AY-3-8910. J’espère participer au concours l’année prochaine !
Liens
https://rgb.yandex
https://vk.com/sinc_lair
https://www.z88dk.org/forum/
Code source
https://gitlab.com/demensdeum/ zx-projects/tree/master/interceptor2020