Création d'un jeu pour Game Boy, partie 2

image

Il y a quelques semaines, j'ai décidé de travailler sur un jeu pour Game Boy, dont la création m'a fait grand plaisir. Son nom de travail est Aqua and Ashes. Le jeu est open source et est publié sur GitHub . La partie précédente de l'article est ici .

Sprites fantastiques et où ils vivent


Dans la dernière partie, j'ai terminé le rendu de plusieurs sprites à l'écran. Cela a été fait de manière très arbitraire et chaotique. En fait, je devais indiquer dans le code quoi et où je veux afficher. Cela a rendu la création d'animation presque impossible, a passé beaucoup de temps CPU et un support de code compliqué. J'avais besoin d'un meilleur moyen.

Plus précisément, j'avais besoin d'un système dans lequel je pouvais simplement répéter le numéro d'animation, le numéro d'image et le minuteur pour chaque animation individuelle. Si je devais changer l'animation, je changerais simplement l'animation et réinitialiserais le compteur d'images. La procédure d'animation effectuée dans chaque image doit simplement choisir les sprites appropriés à afficher et les lancer sur l'écran sans aucun effort de ma part.

Et il s'est avéré que cette tâche est pratiquement résolue. Ce que je cherchais s'appelle des mappages de sprites . Les cartes de sprites sont des structures de données qui (grosso modo) contiennent une liste de sprites. Chaque carte de sprite contient tous les sprites pour le rendu d'un seul objet. Des cartes d'animation (mappages d'animation) , qui sont des listes de cartes de sprites avec des informations sur la boucle, leur sont également associées.

C'est assez drôle qu'en mai, j'ai ajouté un éditeur de carte d'animation à l'éditeur de carte de sprite prêt à l'emploi pour les jeux Sonic 16 bits sur Sonic. (Il est ici , vous pouvez étudier) Il n'est pas encore terminé, car il est plutôt rugueux, douloureusement lent et peu pratique à utiliser. Mais d'un point de vue technique, cela fonctionne. Et il me semble que c'est plutôt cool ... (L'une des raisons de la rugosité était que j'ai d'abord littéralement travaillé avec le framework JavaScript.) Sonic est un ancien jeu, il est donc idéal comme fondement de mon nouveau jeu ancien.

Format de carte Sonic 2


J'avais l'intention d'utiliser l'éditeur dans Sonic 2 parce que je voulais créer un hack pour Genesis. Sonic 1 et 3K sont fondamentalement presque les mêmes, mais pour ne pas compliquer, je me limiterai à l'histoire de la deuxième partie.

Tout d'abord, regardons les cartes de sprites. Voici un sprite Tails assez typique, qui fait partie de l'animation de clignotement.


La console Genesis crée des sprites un peu différemment. La tuile Genesis (la plupart des programmeurs l'appellent un «motif») est 8x8, tout comme sur le Game Boy. Le sprite se compose d'un rectangle jusqu'à 4x4 tuiles, un peu comme le mode sprite 8x16 sur Game Boy, mais plus flexible. L'astuce ici est qu'en mémoire ces tuiles doivent être côte à côte. Les développeurs de Sonic 2 voulaient réutiliser autant de tuiles que possible pour un cadre Tails clignotant à partir d'un cadre Tails debout. Par conséquent, Tails est divisé en 2 sprites matériels, constitués de tuiles 3x2 - une pour la tête, l'autre pour le corps. Ils sont illustrés dans la figure ci-dessous.


Le haut de cette boîte de dialogue contient les attributs du sprite matériel. Il contient leur position par rapport au point de départ (les nombres négatifs sont coupés; en fait, ce sont -16 et -12 pour le premier sprite et -12 pour le second), la tuile initiale utilisée dans VRAM, la largeur et la hauteur du sprite, ainsi que divers bits d'état pour image miroir du sprite et de la palette.

Les vignettes sont affichées en bas lors de leur chargement de la ROM dans la VRAM. Il n'y a pas assez d'espace pour stocker tous les sprites Tails dans VRAM, donc les tuiles nécessaires doivent être copiées en mémoire dans chaque image. Ils sont appelés Dynamic Pattern Load Cues . Cependant, bien que nous puissions les ignorer, car ils sont presque indépendants des cartes de sprites, et donc ils peuvent facilement être ajoutés plus tard.


Quant à l'animation, tout ici est un peu plus simple. Une carte d'animation dans Sonic est une liste de cartes de sprites avec deux morceaux de métadonnées - la valeur de vitesse et l'action à entreprendre après la fin de l'animation. Les trois actions les plus couramment utilisées sont: une boucle sur toutes les images, une boucle sur les N dernières images, ou une transition vers une animation complètement différente (par exemple, lors du passage d'une animation d'un Sonic debout à une animation de son empressement avec son pied). Il existe quelques commandes qui spécifient des indicateurs internes dans la mémoire des objets, mais peu d'objets les utilisent. (Maintenant, il m'est venu à l'esprit que vous pouvez définir le bit dans la RAM de l'objet à une valeur lors de la boucle de l'animation. Cela sera utile pour les effets sonores et d'autres choses.)

Si vous regardez le code Sonic 1 démonté (le code Sonic 2 est trop volumineux pour être lié), vous remarquerez que le lien vers les animations n'est créé par aucun ID. Chaque objet reçoit une liste d'animations et l'index d'animation est stocké en mémoire. Pour rendre une animation spécifique, le jeu prend un index, le recherche dans la liste des animations, puis le rend. Cela rend le travail un peu plus facile, car vous n'avez pas besoin de scanner les animations pour trouver celle dont vous avez besoin.

Nous nettoyons la soupe des structures


Regardons les types de cartes:

  1. Cartes de sprites: une liste de sprites comprenant une tuile initiale, le nombre de tuiles, la position, l'état de réflexion (le sprite est en miroir ou non) et une palette.
  2. DPLC: une liste de tuiles ROM qui doivent être chargées dans VRAM. Chaque élément d'un DPLC se compose d'une tuile initiale et d'une longueur; chaque élément est placé dans VRAM après le dernier.
  3. Cartes d'animation: liste d'animations consistant en une liste de cartes de sprites, de valeurs de vitesse et d'actions de cycle.
  4. Liste d'animation: une liste de pointeurs sur l'action de chaque animation.

Étant donné que nous travaillons avec Game Boy, certaines simplifications peuvent être apportées. Nous savons que dans les cartes de sprites dans un sprite 8x16, il y aura toujours deux tuiles. Cependant, tout le reste doit être préservé. Pour l'instant, nous pouvons complètement abandonner DPLC et simplement tout stocker dans VRAM. Il s'agit d'une solution temporaire, mais, comme je l'ai dit, ce problème sera facile à résoudre. Enfin, nous pouvons ignorer la valeur de vitesse si nous supposons que chaque animation fonctionne à la même vitesse.

Commençons à comprendre comment implémenter un système similaire dans mon jeu.

Vérifiez avec commit 2e5e5b7 !

Commençons par les cartes de sprites. Chaque élément de la carte doit refléter OAM (Object Attribute Memory - sprite VRAM) et ainsi une simple boucle et memcpy suffiront pour afficher l'objet. Permettez-moi de vous rappeler qu'un élément dans OAM se compose de Y, X, d'une tuile initiale et d'un octet d'attribut . J'ai juste besoin d'en créer une liste. En utilisant le pseudo-opérateur EQU assemblé, j'ai préparé à l'avance l'octet d'attribut afin d'avoir un nom lisible pour chaque combinaison possible d'attributs. (Vous pouvez voir que dans le commit précédent, j'ai remplacé la tuile Y / X dans les cartes. Cela s'est produit parce que j'ai lu les spécifications OAM par inadvertance. J'ai également ajouté un compteur de sprites pour savoir combien de temps la boucle devrait prendre.)

Vous remarquerez que le corps et la queue du renard polaire sont stockés séparément. S'ils étaient stockés ensemble, il y aurait alors beaucoup de redondance, car chaque animation devrait être dupliquée pour chaque état de queue. Et l'ampleur de la redondance augmenterait rapidement. Dans Sonic 2, le même problème s'est posé avec Tails. Ils l'ont résolu là-bas, faisant de Tails tails un objet séparé avec son propre état d'animation et son propre temporisateur. Je ne veux pas faire cela parce que je n'essaie pas de résoudre le problème du maintien de la position correcte de la queue par rapport au renard.

J'ai résolu le problème grâce à des cartes d'animation. Si vous regardez ma carte d'animation (unique), il y a trois métadonnées dedans. Il montre le nombre de cartes d'animation, donc je sais quand elles se termineront. (Dans Sonic, il est vérifié que l'animation suivante n'est pas valide, similaire au concept de zéro octet dans les lignes C. Une solution de Sonic libère le cas, mais ajoute une comparaison qui fonctionnerait contre moi.) Bien sûr, il y a toujours une action en boucle. (J'ai transformé les circuits Sonic à 2 octets en un nombre à 1 octet dans lequel le bit 7 est le bit de mode.) Mais j'ai aussi le nombre de cartes sprite , mais ce n'était pas dans Sonic. Avoir plusieurs cartes de sprites par image d'animation me permet de réutiliser des animations dans plusieurs animations, ce qui, à mon avis, économisera beaucoup d'espace précieux. Vous pouvez également remarquer que les animations sont dupliquées pour chaque direction. En effet, les cartes pour chaque direction sont différentes et vous devez les ajouter.

image

Danser avec des registres


Reportez-vous à ce fichier au 1713848.

Commençons par dessiner un seul sprite sur l'écran. Alors, je l'avoue, j'ai menti. Permettez-moi de vous rappeler que nous ne pouvons pas enregistrer sur l'écran en dehors de VBlank. Et tout ce processus est trop long pour l'adapter à VBlank. Par conséquent, nous devons enregistrer la zone de mémoire que nous allouerons pour DMA. Au final, cela ne change rien, il est important d'enregistrer au bon endroit.

Commençons à compter les registres. Le processeur GBZ80 a 6 registres, de A à E, H et L. H et L sont des registres spéciaux, ils sont donc bien adaptés pour effectuer des itérations à partir de la mémoire. (Puisqu'ils sont utilisés ensemble, ils sont appelés HL.) Dans un opcode, je peux écrire à l'adresse mémoire contenue dans HL et en ajouter une. C'est difficile à gérer. Vous pouvez l'utiliser soit comme source soit comme destination. Je l'ai utilisé comme adresses et la combinaison de registres BC comme source, car c'était plus pratique. Nous n'avons que A, D et E. J'ai besoin du registre A pour les opérations mathématiques et similaires. À quoi peut servir DE? J'utilise D comme compteur de boucles et E comme espace de travail. Et c'est là que les registres ont pris fin.

Disons que nous avons 4 sprites. Nous mettons le registre D (compteur de cycles) à 4, le registre HL (destination) l'adresse de tampon OAM et BC (la source) l'emplacement dans la ROM où nos cartes sont stockées. J'aimerais maintenant appeler memcpy. Cependant, un petit problème se pose. Rappelez-vous les coordonnées X et Y? Ils sont indiqués par rapport au point de départ, le centre de l'objet est utilisé pour les collisions et similaires. Si nous les enregistrions tels quels, chaque objet serait affiché dans le coin supérieur gauche de l'écran. Cela ne nous convient pas. Pour résoudre ce problème, nous devons ajouter les coordonnées X et Y de l'objet à X et Y du sprite.

Petite note: je parle des «objets», mais je ne vous ai pas expliqué ce concept. Un objet est simplement un ensemble d'attributs associés à un objet dans un jeu. Les attributs sont une position, une vitesse, une direction. description de l'article, etc. J'en parle parce que j'ai besoin d'extraire des données X et Y de ces objets. Pour ce faire, nous avons besoin d'un troisième ensemble de registres pointant vers la place en RAM des objets où se trouvent les coordonnées. Et puis nous devons stocker X et Y quelque part. La même chose s'applique à la direction, car elle nous aide à déterminer dans quelle direction les sprites regardent. De plus, nous devons rendre tous les objets, ils ont donc également besoin d'un compteur de boucles. Et nous ne sommes pas encore arrivés aux animations! Tout devient rapidement incontrôlable ...

Révision de décision


Donc, je cours trop loin. Revenons en arrière et réfléchissons à chaque élément de données que je dois suivre, et où l'écrire.

Pour commencer, divisons cela en «étapes». Chaque étape ne doit recevoir des données que pour la suivante, à l'exception de la dernière qui effectue la copie.

  1. Object (boucle) - découvre si l'objet doit être rendu et le restitue.
  2. Liste d'animation - détermine quelle animation afficher. Obtient également les attributs d'un objet.
  3. Animation (boucle) - détermine la liste des cartes à utiliser et en rend chaque carte.
  4. Carte (cycle) - parcourt itérativement chaque sprite dans la liste des sprites
  5. Sprite - copie les attributs de sprite dans le tampon OAM

Pour chacune des étapes, j'ai énuméré les variables dont ils ont besoin, les rôles qu'ils jouent et les endroits pour les stocker. Ce tableau ressemble à ceci.

La descriptionLa tailleStageUtiliserD'oùLieuVers où
Tampon OAM2SpritePointeurHlHl
Source de la carte2SpritePointeurBCBC
Octet actuel1SpriteEspace de travailSource de la carteE
X1SpriteVariableHiramUn
Oui1SpriteVariableHiramUn
Début de la carte d'animation2Carte de SpritePointeurStack3DE
Source de la carte2Carte de SpritePointeur[DE]BC
Sprites restants1Carte de SpriteGratterSource de la carteD
Tampon OAM1Carte de SpritePointeurHlHlStack1
Début de la carte d'animation2L'animationEspace de travailBC / Stack3BCStack3
Cartes restantes1L'animationEspace de travailDébut de l'animationHiram
Nombre total de cartes1Des animationsVariableDébut de l'animationHiram
Direction de l'objet1L'animationVariableHiramHiram
Cartes par image1L'animationVariableDébut de l'animationNON UTILISÉ !!!
Numéro de trame1L'animationVariableHiramUn
Pointeur de carte2L'animationPointeurAnimStart + Dir * TMC + MpF * F #BCDE
Tampon OAM2L'animationPointeurStack1Hl
Début de la table d'animation2Liste d'animationEspace de travailEnsemble durDE
Source d'objet2Liste d'animationPointeurHlHlStack2
Numéro de trame1Liste d'animationVariableSource d'objetHiram
Numéro d'animation1Liste d'animationEspace de travailSource d'objetUn
Objet X1Liste d'objetsVariableSource d'objetHiram
Objet Y1Liste d'animationVariableSource d'objetHiram
Direction de l'objet1Liste d'animationVariableObj srcHiram
Début de la carte d'animation2Liste d'animationPointeur[Tableau Anim + Anim #]BC
Tampon OAM2Liste d'animationPointeurDEStack1
Source d'objet2Cycle d'objetPanneauHard Set / Stack2Hl
Objets restants1Cycle d'objetVariableCalculéB
Champ de bits actif d'un objet1Cycle d'objetVariableCalculéC
Tampon OAM2Cycle d'objetPointeurEnsemble durDE

Oui, très déroutant. Pour être tout à fait honnête, j'ai fait ce tableau pour le poste uniquement, pour expliquer plus clairement, mais il a déjà commencé à être utile. Je vais essayer de l'expliquer. Commençons par la fin et arrivons au tout début. Vous verrez chaque élément de données par lequel je commence: la source de l'objet, le tampon OAM et les variables de boucle précalculées. Dans chaque cycle, nous commençons par ceci et seulement cela, sauf que la source de l'objet est mise à jour dans chaque cycle.

Pour chaque objet que nous rendons, il est nécessaire de définir l'animation affichée. Pendant que nous faisons cela, nous pouvons également enregistrer les attributs X, Y, Frame # et Direction avant d'incrémenter le pointeur d'objet sur l'objet suivant et de les enregistrer sur la pile pour les reprendre à la sortie. Nous utilisons le numéro d'animation en combinaison avec la table d'animation codée en dur dans le code pour déterminer où commence la carte d'animation. (Ici, je simplifie, en supposant que chaque objet a la même table d'animation. Cela me limite à 256 animations par jeu, mais il est peu probable que je dépasse cette valeur.) Nous pouvons également écrire un tampon OAM pour enregistrer plusieurs registres.

Après avoir extrait la carte d'animation, nous devons trouver où se trouve la liste des cartes de sprites pour l'image et la direction données, ainsi que le nombre de cartes à rendre. Vous pouvez remarquer que la variable de carte par image n'est pas utilisée. C'est arrivé parce que je n'ai pas réfléchi et réglé la valeur constante 2. Je dois le réparer. Nous devons également extraire le tampon OAM de la pile. Vous pouvez également remarquer un manque total de contrôle du cycle. Il est effectué dans une sous-procédure distincte, beaucoup plus simple, qui vous permet de vous débarrasser du jonglage avec les registres.

Après cela, tout devient assez simple. Une carte est un groupe de sprites, nous les parcourons donc en boucle et dessinons en fonction des coordonnées X et Y enregistrées. Cependant, nous enregistrons à nouveau le pointeur OAM à la fin de la liste des sprites afin que la prochaine carte commence là où nous avons terminé.

Quel a été le résultat final de tout cela? Exactement comme avant: un renard polaire agitant sa queue dans le noir. Mais ajouter de nouvelles animations ou sprites est maintenant beaucoup plus facile. Dans la partie suivante, je parlerai des arrière-plans complexes et du défilement de parallaxe.

image

Partie 4. Contexte de parallaxe


Permettez-moi de vous rappeler qu'au stade actuel, nous avons des sprites animés sur un fond noir uni. Si je ne prévois pas de faire un jeu d'arcade des années 70, alors ce ne sera clairement pas suffisant. J'ai besoin d'une sorte d'image d'arrière-plan.

Dans la première partie, lorsque je dessinais des graphiques, j'ai également créé plusieurs tuiles d'arrière-plan. Il est temps de les utiliser. Nous aurons trois types de tuiles «de base» (ciel, herbe et terre) et deux tuiles transitionnelles. Tous sont chargés dans VRAM et prêts à l'emploi. Il ne nous reste plus qu'à les écrire en arrière-plan.

Contexte


Les arrière-plans du Game Boy sont stockés en mémoire dans un tableau 32x32 de tuiles 8x8. Tous les 32 octets correspondent à une ligne de tuiles.


Jusqu'à présent, je prévois de répéter la même colonne de tuiles dans tout l'espace 32x32. C'est très bien, mais cela crée un petit problème: je devrai placer chaque tuile 32 fois de suite. Ce sera long à écrire.

Instinctivement, j'ai décidé d'utiliser la commande REPT pour ajouter 32 octets / ligne, puis utiliser memcpy pour copier l'arrière-plan dans VRAM.

 REPT 32 db BG_SKY ENDR REPT 32 db BG_GRASS ENDR ... 

Cependant, cela signifie que vous devez allouer 256 octets pour un seul arrière-plan, ce qui est beaucoup. Ce problème est exacerbé si vous vous souvenez que la copie d'une carte d'arrière-plan précédemment créée avec memcpy ne vous permettra pas d'ajouter d'autres types de colonnes (par exemple, des portes, des obstacles) sans une complexité significative et des tas de ROM de cartouche gaspillée.

Au lieu de cela, j'ai décidé de configurer une seule colonne comme suit:

 db BG_SKY, BG_SKY, BG_SKY, ..., BG_GRASS 

puis utilisez une boucle simple pour copier 32 fois chaque élément de cette liste. (voir LoadGFX fichier LoadGFX du commit 739986a .)

La commodité de cette approche est que plus tard, je peux ajouter une file d'attente pour écrire quelque chose comme ceci:

 BGCOL_Field: db BG_SKY, ... BGCOL_LeftGoal: db BG_SKY, ... BGCOL_RightGoal: db BG_SKY, ... ... BGMAP_overview: db 1 dw BGCOL_LeftGoal db 30 dw BGCOL_Field db 1 dw BGCOL_RightGoal db $FF 

Si je décide de rendre BGMAP_overview, il dessinera 1 colonne de LeftGoal, après quoi il y aura 30 colonnes de Field et 1 colonne de RightGoal. Si BGMAP_overview est en RAM, je peux le changer à la volée en fonction de la position de la caméra dans X.

Caméra et position


Oh oui, la caméra. C'est un concept important dont je n'ai pas encore parlé. Ici, nous avons affaire à une multitude de coordonnées, donc avant de parler de la caméra, nous allons d'abord analyser tout cela.

Nous devons travailler avec deux systèmes de coordonnées. Le premier est les coordonnées de l' écran . Il s'agit d'une zone de 256x256 qui peut être contenue dans la VRAM de la console Game Boy. On peut faire défiler la partie visible de l'écran à l'intérieur de ces 256x256, mais quand on dépasse les frontières, on s'effondre.

En largeur, j'ai besoin de plus de 256 pixels, donc j'ajoute des coordonnées mondiales , qui dans ce jeu auront des dimensions de 65536x256. (Je n'ai pas besoin de hauteur supplémentaire en Y, car le jeu se déroule sur un terrain plat.) Ce système est complètement distinct du système de coordonnées d'écran. Toute la physique et les collisions doivent être effectuées en coordonnées mondiales, sinon les objets entreront en collision avec des objets sur d'autres écrans.


Comparaison des coordonnées écran et monde

Étant donné que les positions de tous les objets sont représentées en coordonnées universelles, elles doivent être converties en coordonnées d'écran avant le rendu. À l'extrême gauche du monde, les coordonnées du monde coïncident avec les coordonnées de l'écran. Si nous devons afficher les choses à droite sur l'écran, alors nous devons tout prendre en coordonnées universelles et le déplacer vers la gauche pour qu'elles soient en coordonnées d'écran.

Pour ce faire, nous allons définir la variable «camera X», qui est définie comme la bordure gauche de l'écran dans le monde. Par exemple, si la camera X vaut 1000, alors nous pouvons voir les coordonnées mondiales 1000-1192, car l'écran visible a une largeur de 192 pixels.

Pour traiter les objets, nous prenons simplement leur position dans X (par exemple, 1002), soustrayons la position de la caméra égale à 1000 et dessinons l'objet à la position donnée par la différence (dans notre cas, 2). Pour un arrière-plan qui n'est pas en coordonnées universelles, mais déjà décrit en coordonnées d'écran, nous définissons la position égale à l'octet inférieur de la variable camera X de la camera X . Grâce à cela, l'arrière-plan défile vers la gauche et la droite avec l'appareil photo.

Parallax


Le système que nous avons créé semble plutôt plat. Chaque calque d'arrière-plan se déplace à la même vitesse. Il ne se sent pas en trois dimensions et nous devons le réparer.

Un moyen simple d'ajouter une simulation 3D est appelé défilement parallaxe. Imaginez que vous conduisez sur une route et que vous êtes très fatigué. Le Game Boy est à court de piles et vous devez regarder par la fenêtre de la voiture. Si vous regardez le sol à côté de vous, vous verrez. qu'elle se déplace à une vitesse de 70 miles par heure. Cependant, si vous regardez les champs au loin, il semblerait qu'ils se déplacent beaucoup plus lentement. Et si vous regardez les montagnes très lointaines, elles semblent à peine bouger.

Nous pouvons simuler cet effet avec trois feuilles de papier. Si vous dessinez une chaîne de montagnes sur une feuille, le champ sur la seconde et la route sur la troisième, et les posez les unes sur les autres comme ceci. de sorte que chaque couche soit visible, ce sera une imitation de ce que nous voyons depuis la fenêtre de la voiture. Si nous voulons déplacer la «voiture» vers la gauche, nous déplaçons la feuille supérieure (avec la route) loin vers la droite, la suivante est un peu à droite et la dernière est un peu à droite.



Cependant, lors de la mise en œuvre d'un tel système sur Game Boy, un petit problème se pose. La console n'a qu'une seule couche d'arrière-plan. Cela est similaire au fait que nous n'avons qu'une seule feuille de papier. Vous ne pouvez pas créer un effet de parallaxe avec une seule feuille de papier. Ou est-ce possible?

H-blanc


L'écran de Game Boy est rendu ligne par ligne. En raison de l'émulation du comportement des anciens téléviseurs à tube cathodique, il existe un léger délai entre chaque ligne. Et si nous pouvions l'utiliser d'une manière ou d'une autre? Il s'avère que Game Boy a une interruption matérielle spéciale spécialement conçue à cet effet.

Semblable à l'interruption VBlank, que nous attendions constamment jusqu'à la fin de la trame pour l'enregistrement en VRAM, il y a une interruption HBlank. En définissant le bit 6 du registre à $FF41 , en $FF41 l'interruption LCD STAT et en écrivant le numéro de ligne à $FF45 , nous pouvons dire à Game Boy de démarrer l'interruption LCD STAT quand il est sur le point de tracer la ligne spécifiée (et quand elle est dans son HBlank).

Pendant ce temps, nous pouvons modifier toutes les variables VRAM. Ce n'est pas beaucoup de temps, donc nous ne pouvons pas changer plus que quelques registres, mais nous avons encore quelques possibilités. Nous voulons changer le registre de défilement horizontal à $FF43 . Dans ce cas, tout ce qui se trouve à l'écran sous la ligne spécifiée se déplacera d'un certain décalage, créant un effet de parallaxe.

Si vous revenez à l'exemple de la montagne, vous pouvez remarquer un problème potentiel. Les montagnes, les nuages ​​et les fleurs ne sont pas des lignes plates! Nous ne pouvons pas déplacer la ligne sélectionnée de haut en bas pendant le processus de rendu; si nous le choisissons, alors il reste le même au moins jusqu'au prochain HBlank. Autrement dit, nous ne pouvons couper qu'en lignes droites.

Pour résoudre ce problème, nous devons faire un peu plus intelligemment. Nous pouvons déclarer une ligne en arrière-plan comme une ligne que rien ne peut traverser, ce qui signifie changer les modes des objets au-dessus et en dessous, et le joueur ne pourra rien remarquer. Par exemple, c'est là que ces lignes sont en scène avec la montagne.


Ici, j'ai fait des tranches juste au-dessus et en dessous de la montagne. Tout du haut à la première ligne se déplace lentement, tout à la deuxième ligne se déplace à une vitesse moyenne et tout en dessous de cette ligne se déplace rapidement. C'est une astuce simple mais intelligente. Et en l'apprenant, vous pouvez le remarquer dans de nombreux jeux rétro, principalement pour Genesis / Mega Drive, mais aussi sur d'autres consoles. L'un des exemples les plus évidents est la partie de la grotte de Mickey Mania. Vous pouvez remarquer que les stalagmites et les stalactites en arrière-plan sont séparées exactement le long d'une ligne horizontale avec une bordure noire évidente entre les couches.

J'ai réalisé la même chose dans mon passé. Cependant, il y a une astuce. Supposons que le premier plan se déplace à une vitesse un sur un coïncidant avec le mouvement de la caméra et que la vitesse d'arrière-plan soit un tiers du mouvement des pixels de la caméra, c'est-à-dire que l'arrière-plan se déplace comme un tiers du premier plan. Mais, bien sûr, un tiers du pixel n'existe pas. Par conséquent, je dois déplacer l'arrière-plan d'un pixel pour trois pixels de mouvement.

Si vous travaillez avec des ordinateurs capables de calculs mathématiques, vous devez prendre la position de la caméra, la diviser par 3 et faire de cette valeur un décalage d'arrière-plan. Malheureusement, le Game Boy n'est pas capable de faire la division, sans parler du fait que la division des programmes est un processus très lent et douloureux. Ajouter un appareil pour diviser (ou multiplier) à un CPU faible pour une console de divertissement portable dans les années 80 ne semblait pas être une étape rentable, nous devons donc inventer une autre façon.

Dans le code, j'ai fait ce qui suit: au lieu de lire la position de la caméra à partir d'une variable, j'ai exigé qu'elle augmente ou diminue. Grâce à cela, avec chaque troisième incrément, je peux effectuer un incrément de la position d'arrière-plan, et avec chaque premier incrément - un incrément de la position de premier plan. Cela complique un peu le défilement vers une position de l'autre bord du champ (le moyen le plus simple est de simplement réinitialiser les positions des couches après une certaine transition), mais cela nous évite d'avoir à diviser.

Résultat


Après tout cela, j'ai obtenu ce qui suit:


Pour un jeu sur Game Boy, c'est plutôt cool. Pour autant que je sache, tous n'ont pas le défilement de parallaxe implémenté comme ceci.

Source: https://habr.com/ru/post/fr436918/


All Articles