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 :

Comment j’ai fait Brutal Pico Race – 1ère partie

Brutal pico race utilise un système de rendu, combinant différentes techniques pour la route et pour les vaisseaux. Calcul des sommets de polygone à la volée pour la route, avec une projection “pseudo 3D” comme les anciens jeux type Outrun pour la route, et un rendu basé sur une structure en voxel pour les vaisseaux.

Voici le premier article décrivant comment j’ai implémenté Brutal Pico Race. Je donne quelques détails technique sur le rendu dont le résultat et une bataille entre l’idée d’un jeu de jeu de course et les possibilités (ou les contraintes) de Pico 8.

La prochaine fois je parlerais du rendu des vaisseaux et comment ils ont été intégré à la piste.

L’idée

Inspiration

J’ai eu l’idée de Brutal pico race après que mon subconscient ai combiné différents éléments que j’ai découvert ici et la. La première pièce du puzzle vient du court métrage the running man tiré de Neo Tokyo (pas du film avec Arnold Schwarzenegger !). Le titre Sega Sunset de Lorn colle particulièrement bien à l’ambiance dans ce montage :

Travaux émergent de twitter

Avant de démarrer Brutal Pico Race, j’étais à la recherche d’un outil permettant à mes enfants de découvrir la programmation informatique. J’avais un pico-8 grace à un ensemble de logiciel contenant Voxatron, je pensais que c’était un bon début. Mais finalement, mes enfants n’ont pas trop accroché autant que moi ! En apprenant comment fonctionnait pico 8 j’ai découvert des projets très intéressant et une communauté très active, le piège était refermé !

Si je devais sélectionner uniquement 3 projets, je garderais Zepton, Alone in pico 8 avec son postmortem très détaillé et Lands of Yocta. J’étais stupéfait ! Je me suis dit : On peut faire ça avec Pico 8 ?

Donc après avec découvert the running man du court métrage Neo Tokyo, et après avoir découvert des supers projets Pico 8 je me suis lancé dans la création de ce “demake” de The running man. Première étape la route !

La route

Brutal pico race, first IA test
Brutal pico race, la route…

Structure de données

La route est un tableau de “bloc de route”. Chaque bloc de  route possède ses propre propriétés. Pour dessiner la route, nous avons besoin de coordonnées x,y,z, d’une propriété fx pour les effets de virages, c pour la couleur et t pour le type de route. La coordonnée x a été utilisée dans les premiers prototypes, avant de choisir de modéliser les virages en pseudo 3D (par accumulation de décalage). J’imagine que je pourrais gagner quelques tokens en le retirant ! Donc fx est la propriété utilisée pour simuler les virages, comme dans les vieux jeux de courses sur des machines pas assez performante pour calculer de vrais virages. Et c’est tout ! Dans Brutal Pico Race, un bloc de route fait 64 unités de long, un choix arbitraire. Toutes ces informations sont initialisées dans la function “init_all_road” disponible dans le code source (lien “code” sous la fenêtre pico 8).

Grosso modo, une piste de course est une grande ligne droite qui boucle.

Rendu Hybride

La piste est rendu du bloc de route le plus loin au plus près par rapport à la caméra (Algorithme du peintre), avec une projection simple (weak perspective projection). On passe de coordonnées 3D à une projection 2D grâce à une division.

projection x = 3D X / 3D Z
projection y = 3D Y / 3D Z

Je n’utilise pas de matrice de projection dans ce cas, car cela est très couteux. Je tente de réduire au maximum ce qui est nécessaire. La contre-partie est qu’il n’y a pas de rotation possible pour la piste. Mais cela permet quand même de projeter les 4 sommets d’un bloc de route pour chaque bloque de route. A partir de la, on peut remplir le polygone obtenu en 2D. Le remplissage est réalisé grâce au rasterizer de 163 tokens de @p01 qui est rapide et léger. Au début du projet, j’utilisais une version adapté du rasterizer de Alone In Pico 8.

En appliquant une fonction cos ou sin aux coordonnées X et Y des blocs de route en 3D et en appliquant une translation sur l’axe Z de la caméra, on obtient le résultat suivant (Notez le faible cout du CPU) :

Ci-dessous j’ai épurer le code de rendu de la route pour se concentrer sur l’affichage de la piste, bloc par bloc. Je fais attention de ne jamais réaliser plus d’une fois la même chose, on voit que les sommets ne sont projetés qu’une seul fois chacun. On ne calcul que 2 sommets par bloc de route. La première itération n’affiche rien, il faut attendre la deuxième pour avoir 4 sommets et ainsi un polygone à remplir.

function drawroad(cam,ird,p)
 -- calculating road
 local r,rn,v,vn,vz=
  nil,nil--render info
 -- first partial offset
 local irdn=ird+1
 local irdlast=ird+nb_rb-1
 
 -- ready to draw road
 for i=irdlast,ird-1,-1 do
  v,vn=road[i%#road+1],
   road[(i+1)%#road+1]
  vz,xl,xr,iy,ylgt=
   v.z+flr(i/#road)*trklen,
   v.x-56,v.x+56,v.y,
   lerp(v.y,vn.y,0.5)
  -- clip
  dz=max(vz-cam.z,7)

  prc=(cam.z+7-vz)/rdz
  -- clipping
  if dz==7 then
   --interpolation
   xl=lerp(xl,vn.x-56,prc)
   xr=lerp(xr,vn.x+56,prc)
   iy=lerp(v.y,vn.y,prc)
  end
  -- current part
  fct=scale/dz

  r={
   x1=cx+fct*(xl-cam.x),
   x2=cx+fct*(xr-cam.x),
   y=cy-fct*(iy-cam.y),
  }
  -- drawing road
  if rn~=nil then 
   -- drawing triangles
   otri(r.x1,r.y,r.x2,r.y,rn.x1,rn.y,v.c)
   otri(rn.x1,rn.y,rn.x2,rn.y,r.x2,r.y,v.c)
  end
  -- next
  rn=r
 end
end

Système Half-pipe

Maintenant que l’on peut afficher une ligne droite, sans rotation mais avec une gestion de l’altitude et qu’il n’y a pas de contrainte de forme pour le remplissage des polygones, on peut imager une amélioration de la piste en “demi lune”. J’avais en tête cette forme en demi lune grace au court métrage Neo Tokyo, où les vaisseaux sont collés sur les bords surélevés par la vitesse. Et, c’est chose possible ! En divisant un bloc de route en 6 parties, avec chacune une altitude différente. Ces nouveaux sommets sont calculés à la volée à partir du point centrale du bloc de route (X=0, Y=0, Z=le z de ce bloc de route) en précisant les coordonnées X et Y du système de coordonnées du monde. Ces nouveaux sommets sont ensuite projetés en 2D et rempli et voila la magie :

Ensuite, je voulais voir jusqu’où Pico8 pouvait aller en ajoutant plein de bloc de route de ce type. Donc j’ai complété le premier prototype en affichant une “demi lune” complète. Et pour 30FPS, Pico 8 s’en tire bien ! Je n’ai pas visé le 60 FPS car je voulais donner la possibilité de faire un écran splitté pour du multijoueur, donc avoir le CPU en dessous de 50%, même si il manque encore beaucoup de chose, était déjà très encourageant !

 

Prochaine partie, le rendu des vaisseaux et comment ils ont été intégrés à la piste.

Ressources :

Damien Hostin (aka YouryKiKi)

Brutal Pico Race 1.0.2

Brutal Pico Race 1.0.2
Brutal Pico Race 1.0.2

Une semaine après la sortie public voici la première mise à jour, prenant en compte les premiers retours. Voici les nouveautés :

1.0.2 :

  • Augmentation de la difficulté
  • 3 sortes de bot (un lourd, un normal et un léger)
  • Meilleur écran de fin de course
  • Possibilité de suivre un bot à la fin de la course
  • Progression des autres vaisseaux sur la gauche
  • Courte invulnérabilité temporaire suite à un dégat
  • Changement du code de chargement des circuits, ce qui permet l’ajout de 3 nouveaux circuits dans la prochaine release,
  • Meilleur effet visuel pour les trainées du boost
  • Correction de l’effet sonore du boost en écran partagé
  • changement du tri de morgan3d (heapsort) avec celui de ciura’s (shellsort)

Dans la prochaine mise à jour, et selon les retours de cette dernière :

  • 3 nouvelles pistes,
  • Un tableau de score
  • Music sur 2 canaux pour la phase de jeu ?
  • Destruction du vaisseau si les points de vies sont épuisés ? pour le moment cela n’affecte que le temps de chargement du boost
  • Ce que vous pensez être important et que je peux inclure selon la taille restante =)

Vous pouvez jouer sur itch.io, official pico8 BBS and Indiedb

Have fun !