Comment j’ai fait Brutal Pico Race – 2ème et dernière partie

Je me suis rendu compte que je n’avais pas expliqué ce qu’était le type sur les bloc de piste, donc avant de présenter le rendu des vaisseaux, je vais expliquer le type de bloc de route.

La piste

Le type de piste

Si vous vous souvenez, j’ai présenté que la structure de donnée de la piste est un tableau de bloc de piste, avec pour chaque bloc des coordonnées, l’effet du virage, une couleur et un type. J’ai aussi présenté comment modéliser la route en “demi-lune”. Alors, qu’est-ce que le type de bloc de piste ? Comme vous pouvez le voir en jouant, la piste n’est pas toujours dans la même configuration, des fois plates, des fois avec des virages relevés à gauche ou à droite ou encore les deux. C’est ca le type de piste. Le type de piste est un “bit field” qui précise quel morceau de la piste est activé.

Value                |256|...|16|8|4|2|1|
---------------------+---+---+--+-+-+-+-+
2e élévation droite  |   |   |  | | | |X|
1ère élévation droite|   |   |  | | |X| |
Piste normal, plate  |   |   |  | |X| | |
1ère élévation gauche|   |   |  |X| | | |
2e élévation gauche  |   |   | X| | | | |
Ligne de départ      | X |   |  | | | | |

Par exemple, une piste avec une élévation gauche complète et un premier niveau d’élévation à droite a un type qui vaut 30 et ressemble à ceci :

Cette valeur est réglé automatiquement en fonction de l’effet du virage., mais il est possible de forcer la valeur. C’est le cas de la capture ci-dessus où la valeur par défaut est 4 et la route devrait être plate. J’avais prévu plus de type de route différents, mais j’ai malheureusement atteint le maximum de ce que pico 8 me permet de faire !

Le vaisseau

Données empilées

Les vaisseaux sont stockés directement dans l’éditeur de sprite. Ils sont composés de 3 couches, ainsi leur édition est facilité et amusante (enfin, je pense !). Il n’y a plus qu’a les dessiner. Ils sont chargés dans un tableau à l’initialisation du jeu. Il existe un type de stockage similaire dans Zepton et surement d’autres cartouches pico 8. On stocke aussi l’ombre, qui correspond à la fusion des trois couches.

Couches de vaisseaux

Vous pouvez regarder la fonction loadss qui correspond au chargement des voxels. Il y a aussi quelques informations supplémentaires tirés de ce chargement, comme la largeur, la longueur et la masse du vaisseau calculées directement à partir des pixels. Vous pouvez aussi voir que l’ombre n’est constituée que d’une ligne sur deux, ce qui diminue le temps CPU à l’affichage.

-- loading a ship
function loadss(nbss,ss,sss)
 local m,xmin,xmax,zmin,zmax=
  0,8,0,8,0
 for k=7,0,-1 do-- y
  for j=0,7 do-- x
   local shad=false
   for i=2,0,-1 do
    c=sget(8*i+j,k+nbss*8)
    if c~=0 then
     v,shad=
      {x=j-4,y=i-1,z=4-k,
       oz=4-k,c=c,pat=0},
      c~=10
     m+=1
     add(ss,v)
    end
   end
   --half z
   if shad and k%2==0 then
    v={x=j-4,y=0,z=4-k,
     oz=0,c=9,pat=1}
    add(sss,v)
    xmin,xmax,zmin,zmax=
     min(v.x,xmin),max(v.x,xmax),
     min(v.z,zmin),max(v.z,zmax)
   end
  end
 end
 -- /5 because half_width*scale
 local si=ship_info[nbss+1]
 si.mass,si.width,si.height=
  m,(xmax-xmin)/5,(zmax-zmin)/5
end

Rendu

Comme nous voyons tout le temps les vaisseaux de derrière pendant une course, on s’assure de charger les voxels dans le bon ordre de sorte qu’il n’y ai pas besoin de trier les éléments à l’affichage et ainsi minimiser le coût du rendu. La projection des éléments du monde en 3D à l’écran en 2D se fait de la même façon que la projection des sommets de la piste (weak perspective projection). La projection est appliqué pour chaque voxel.

Un autre point important nécessaire pour l’affichage des vaisseaux est la rotation, sur plusieurs axes. J’utilise les angles d’euler pour modéliser les diverses rotations. La rotation est la même pour chaque élément du vaisseau, alors il suffit de calculer la matrice de rotation une seule fois par image. Ensuite on l’applique pour chaque élément. Lors de l’affichage à deux joueurs en écran divisé, ce calcul n’est fait qu’une fois. J’aurais pu faire en sorte que ce calcul soit abandonné si le vaisseau n’apparait pas, mais cela aurait amélioré certaines situation, où il y a moins de vaisseau à afficher. Quand il est nécessaire de voir tous les vaisseaux, comme sur la grille de départ, cette optimisation n’apporte rien, il y aura même un léger surcoût. De plus, étant à court de tokens pico 8, j’ai préféré abandonner cette optimisation. Voici la fonction calcrotmat, vous pouvez voir que les notions de “pitch, yaw, roll” (tangage, lacet et roulis) ne sont pas sur les axes standards. Je me suis mélangé trop tôt et je n’ai pas respecter les axes.

function calcrotmat(rot)
--pitch x yaw y roll z
local cosa,sina,cosb,sinb,cosc,sinc=
cos(rot.z),sin(rot.z),
cos(-rot.y),sin(-rot.y),
cos(rot.x),sin(rot.x)

local axx,axy,axz=
cosa*cosb,
cosa*sinb*sinc-sina*cosc,
cosa*sinb*cosc+sina*sinc

local ayx,ayy,ayz=
sina*cosb,
sina*sinb*sinc+cosa*cosc,
sina*sinb*cosc-cosa*sinc

local azx,azy,azz=
-sinb,cosb*sinc,cosb*cosc

return {xx=axx,xy=axy,xz=axz,
yx=ayx,yy=ayy,yz=ayz,
zx=azx,zy=azy,zz=azz}
end

Une fois la matrice calculée, on l’applique au vaisseau. Ceci est réalisé une seule fois par image et par vaisseau dans la fonction _update.

function applyrot(ss,m,sort)
-- faster 0.02cpu but +30 tokens
local xx,xy,xz,
yx,yy,yz,
zx,zy,zz,rr
=m.xx,m.xy,m.xz,
m.yx,m.yy,m.yz,
m.zx,m.zy,m.zz,
{}

for v in all(ss) do
local r={}
r.x,r.y,r.z,
r.c,r.pat=
xx*v.x+xy*v.y+xz*v.z,
yx*v.x+yy*v.y+yz*v.z,
zx*v.x+zy*v.y+zz*v.z,
v.c,v.pat
r.key=r.z
add(rr,r)
end
-- z sort
if (sort) shellsort(rr)
return rr
end

Dans cette fonction, on passe la matrice de rotation sous la forme d’un tableau. Une astuce, pour limiter le coup de parcours de ce tableau à chaque itération de la boucle sur les éléments et de le faire une fois pour toute avant la boucle. On économise ainsi environ 2% de CPU.

Nous avons un vaisseau composé de voxels tournés dans le bon sens. Nous pouvons les projeter dans sur l’écran. Et comme je suis chanceux, j’obtiens ceci :

Un xwing et un faucon millenium !

Sur cette capture du début de développement, on peut voir une sorte d’ombre orange. Il s’agit en fait de la projection de la lumière émise par le système de sustentation. Mais ce n’était pas très lisible, et après quelques essais et conseils sur twitter, j’ai finalement laissé tombé et j’ai une vrai ombre noire. Sur ce gif, on applique un tri sur l’axe Z sinon l’affichage serait erroné pendant la rotation. Ce tri a été conservé pour l’écran de menu, lors du choix du vaisseau.

Integration des vaisseaux sur la piste

Comme indiqué plus tôt, la piste est en fait un simple tableau, tout droit. Maintenant je vais ajouter que la demi-lune est plate ! L’intégration des vaisseaux est géré sur une piste plate, “dépliée”. Avant d’afficher la piste et les vaisseaux, le monde “déplié” est converti en monte “replié”, le vrai monde avec une piste en demi-lune. A ce moment, une rotation est appliquée sur l’axe Z pour faire en sorte que le vaisseau soit parallèle avec le morceau de demi lune qu’il survol. J’utilise une interpolation linéaire pour passé de la valeur d’un angle de rotation à un autre pour avoir un mouvement plus fluide.

Grimper la demi-lune !

En fonction de l’endroit où se trouve le vaisseau sur la piste dépliée, une rotation est appliqué sur l’axe Z. On en profite pour appliquer aussi l’effet de la gravité, pour faire redescendre le vaisseau vers le plat de la piste. Et voila la magie.

Ces astuces sont la structure principale du rendu et tout les détails sont intégrés en tenant compte de ces particularités. Cela permet d’économiser du CPU pour certaines autres fonction du jeu, comme la gestion des collisions qui est réalisée en 2D, sur la piste plate.

Voici un extrait de la fonction de conversion :

function convert_xflat_to_x(pos,decx,ytmp,rot,spdp,shipp)

 local xflat,cumx,cumy,newz=
 pos.xflat-decx,0,0,1
 pos.x,pos.y=pos.xflat,0
 -- for each road part
 for stp in all(rd_info) do
  if xflat > stp.fl then
   -- startx,y
   stx,sty=stp.sx,stp.sy
   cumx,cumy=stx+stp.dx,sty+stp.dy
   pos.x=decx+lerp(stx,cumx,(xflat-stp.fl)/stp.dst)
   pos.y=lerp(sty,cumy,(xflat-stp.fl)/stp.dst)
   newz=stp.rotz
   break
  end
 end
 -- keep rotation
 if rot~=nil then
 rot.z=lerp(rot.z,newz,0.25)
 newz=rot.z
 end
 if pos.yflat then
 pos.x=pos.x
 +sin(1-newz)*pos.yflat
 pos.y=ytmp+pos.y
 +cos(newz)*pos.yflat
 end
...
 -- gravity
 if spdp~=nil then
  local gx=sin(newz)
  spdp.x-=gx
 end
end

Les autres vaisseaux, ceux que l’on essaye de rattraper et que l’on ne controle pas sont affichés de la même façon.La seule différence est liée à la façon dont les virages sont affichés, par accumulation de décalage comme les vieux jeux de courses type outrun. Cette accumulation de décalage doit être prise en compte sur l’axe X. Si on l’oubli, les vaisseaux seront toujours affichés au milieu de l’écran, ce qui est problématique pendant les virages.

L’ordre est aussi un élément important. Dans ce type d’affichage, on stock les éléments à afficher directement dans le bloc de piste qui le contient. L’affichage consiste à dessiner chaque élément de route puis les éléments qu’elle contient, du plus loin au plus prêt. Il est nécessaire de trier uniquement si un bloc de piste contient plusieurs vaisseaux. Ceci limite fortement les opérations de tri, car les moments où il y a un maximum de vaisseau sur un seul bloc de route ne sont pas si fréquents.

Ces mécanismes sont bien détaillées dans le lien à la fin de l’article, provenant du site code incomplete.

J’espère que vous avez aimé ces explications, si vous avez des questions vous pouvez laisser un commentaire ou vous pouvez me contacter sur twitter @yourykiki. Vous pouvez aussi accéder au code complet sur le BBS de Lexaloffle, le lien “code” juste en dessous du jeu.

Have fun !

Resource :

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *