Création d'un jeu de course pseudo-tridimensionnel


Enfant, je suis rarement allé dans des salles d'arcade parce que je n'en avais pas vraiment besoin, parce que j'avais des jeux géniaux pour C64 à la maison ... mais il y a trois jeux d'arcade pour lesquels j'ai toujours eu de l'argent - Donkey Kong, Dragons Lair et Outrun ...

... et j'ai vraiment adoré Outrun - vitesse, collines, palmiers et musique, même sur la version faible du C64.


J'ai donc décidé d'essayer d'écrire un jeu de course pseudo-tridimensionnel à l'ancienne dans le style d'Outrun, Pitstop ou Pole position. Je n'ai pas l'intention de monter un jeu complet et complet , mais il me semble qu'il sera intéressant de réexaminer les mécanismes avec lesquels ces jeux ont réalisé leurs tours. Courbes, collines, sprites et sens de la vitesse ...

Alors, voici mon «projet de week-end», qui a finalement pris cinq ou six semaines le week-end



La version jouable ressemble plus à une démo technique qu'à un vrai jeu. En fait, si vous voulez créer une véritable course pseudo-tridimensionnelle, ce sera la base la plus minimale dont vous aurez besoin pour devenir progressivement un jeu.

Il n'est pas poli, un peu moche, mais entièrement fonctionnel. Je vais vous montrer comment l'implémenter par vous-même en quatre étapes simples.

Vous pouvez également jouer


À propos des performances


Les performances de ce jeu dépendent fortement de la machine / du navigateur. Dans les navigateurs modernes, cela fonctionne bien, en particulier dans ceux qui ont une accélération du GPU Canvas, mais un mauvais pilote graphique peut le geler. Dans le jeu, vous pouvez modifier la résolution de rendu et la distance de rendu.

À propos de la structure du code


Il se trouve que le projet a été implémenté en Javascript (en raison de la simplicité du prototypage), mais il n'est pas destiné à démontrer les techniques ou les techniques recommandées de Javascript. En fait, pour faciliter la compréhension, le Javascript de chaque exemple est intégré directement dans la page HTML (horreur!); pire, il utilise des variables et des fonctions globales.

Si je créais un vrai jeu, le code serait beaucoup plus structuré et rationalisé, mais comme il s'agit d'une démonstration technique d'un jeu de course, j'ai décidé de m'en tenir à KISS .

Partie 1. Routes droites.


Alors, comment pouvons-nous commencer à créer un jeu de course pseudo-tridimensionnel?

Eh bien, nous avons besoin

  • Répéter la trigonométrie
  • Rappelez les bases de la projection 3D
  • Créer une boucle de jeu
  • Télécharger des images de sprite
  • Construire la géométrie de la route
  • Fond de rendu
  • Rendre la route
  • Rendre la voiture
  • Implémenter la prise en charge du clavier pour le contrôle de la machine

Mais avant de commencer, Lisons pseudo 3d page de Lou [ traduction Habré] - la seule source d'information (que je pouvais trouver) sur la façon de créer jeu de course psevdotrohmernuyu.

Vous avez fini de lire l'article de Lou? Bien! Nous allons créer une variation de sa technique Realistic Hills Using 3D-Projected Segments. Nous le ferons progressivement au cours des quatre prochaines parties. Mais nous allons commencer maintenant, avec la version v1, et créer une géométrie de route droite très simple en la projetant sur un élément de toile HTML5.

La démo peut être vue ici .

Un peu de trigonométrie


Avant d'entrer dans l'implémentation, utilisons les bases de la trigonométrie pour nous rappeler comment projeter un point du monde 3D sur un écran 2D.

Dans le cas le plus simple, si vous ne touchez pas aux vecteurs et aux matrices, la loi des triangles similaires est utilisée pour la projection 3D .

Nous utilisons la notation suivante:

  • h = hauteur de la caméra
  • d = distance de la caméra à l'écran
  • z = distance de la caméra à la voiture
  • y = coordonnées y de l' écran

Ensuite, nous pouvons utiliser la loi des triangles similaires pour calculer

y = h * d / z

comme le montre le schéma:


Vous pouvez également dessiner un diagramme similaire dans une vue de dessus au lieu d'une vue latérale et dériver une équation similaire pour calculer la coordonnée X de l' écran:

x = w * d / z

w = la moitié de la largeur de la route (de la caméra au bord de la route).

Comme vous pouvez le voir, pour x et y, nous évoluons d'un facteur

d / z

Systèmes de coordonnées


Sous la forme d'un diagramme, il a l'air beau et simple, mais lorsque vous commencez à coder, vous pouvez être un peu confus, car nous avons choisi des noms arbitraires, et on ne sait pas ce que nous avons indiqué les coordonnées du monde 3D et quelles sont les coordonnées de l'écran 2D. Nous supposons également que la caméra est au centre de l'origine du monde, bien qu'en réalité elle suivra la machine.

Si vous vous approchez de manière plus formelle, nous devons effectuer:

  1. conversion des coordonnées du monde en coordonnées d'écran
  2. projection des coordonnées de la caméra sur un plan de projection normalisé
  3. mettre à l'échelle les coordonnées projetées aux coordonnées de l'écran physique (dans notre cas, c'est du canevas)


Remarque: dans le système 3D actuel , l' étape de rotation est effectuée entre les étapes 1 et 2 , mais comme nous simulerons les courbes, nous n'avons pas besoin d'une rotation.

Projection


Les équations de projection formelles peuvent être représentées comme suit:


  • Le point d'équations de conversion ( translate ) est calculé par rapport à la chambre
  • Les équations de projection ( projet ) sont des variations de la «loi des triangles similaires» illustrée ci-dessus.
  • Les équations de mise à l' échelle ( échelle ) tiennent compte de la différence entre:
    • mathématiques , où 0,0 est au centre et l'axe y est en haut, et
    • , 0,0 , y :


: 3d- Vector Matrix 3d-, , WebGL ( )… . Outrun.



La dernière pièce du puzzle sera un moyen de calculer d - la distance entre la caméra et le plan de projection.

Au lieu d'écrire simplement une valeur fixe de d , il serait plus utile de la calculer à partir du champ de vision vertical souhaité. Grâce à cela, nous pourrons "zoomer" la caméra si nécessaire.

Si nous supposons que nous projetons sur un plan de projection normalisé, dont les coordonnées sont comprises entre -1 et +1, alors d peut être calculé comme suit:

d = 1 / bronzage (fov / 2)

En définissant fov comme une (parmi plusieurs) variables, nous pouvons ajuster la portée pour affiner l'algorithme de rendu.

Structure du code Javascript


Au début de l'article, j'ai déjà dit que le code n'était pas tout à fait conforme aux directives pour écrire Javascript - c'est une démo «rapide et sale» avec des variables et fonctions globales simples. Cependant, puisque je vais créer quatre versions distinctes (droite, courbes, collines et sprites), je vais stocker des méthodes réutilisables à l'intérieur common.jsdans les modules suivants:

  • Dom est quelques fonctions d'assistance DOM mineures.
  • Util - utilitaires généraux, principalement des fonctions mathématiques auxiliaires.
  • Jeu - Fonctions générales d'assistance au jeu, telles que le téléchargement d'images et la boucle de jeu.
  • Rendu - fonctions de rendu d'aide sur le canevas.

J'expliquerai en détail les méthodes de common.jsseulement si elles se rapportent au jeu lui-même, et ne sont pas seulement des fonctions mathématiques ou DOM auxiliaires. Espérons que d'après le nom et le contexte, il sera clair ce que les méthodes devraient faire.

Comme d'habitude, le code source est dans la documentation finale.

Boucle de jeu simple


Avant de rendre quelque chose, nous avons besoin d'une boucle de jeu. Si vous avez lu l'un de mes articles précédents sur les jeux ( pong , breakout , tetris , snakes ou boulderdash ), vous avez déjà vu des exemples de mon cycle de jeu préféré avec un pas de temps fixe .

Je n'entrerai pas dans les détails, et je réutiliserai simplement une partie du code des jeux précédents pour créer une boucle de jeu avec un pas de temps fixe en utilisant requestAnimationFrame .

Le principe est que chacun de mes quatre exemples peut appeler Game.run(...)et utiliser ses propres versions

  • update - Mise à jour du monde du jeu avec un pas de temps fixe.
  • render - Mise à jour du monde du jeu lorsque le navigateur le permet.

run: function(options) {

  Game.loadImages(options.images, function(images) {

    var update = options.update,    // method to update game logic is provided by caller
        render = options.render,    // method to render the game is provided by caller
        step   = options.step,      // fixed frame step (1/fps) is specified by caller
        now    = null,
        last   = Util.timestamp(),
        dt     = 0,
        gdt    = 0;

    function frame() {
      now = Util.timestamp();
      dt  = Math.min(1, (now - last) / 1000); // using requestAnimationFrame have to be able to handle large delta's caused when it 'hibernates' in a background or non-visible tab
      gdt = gdt + dt;
      while (gdt > step) {
        gdt = gdt - step;
        update(step);
      }
      render();
      last = now;
      requestAnimationFrame(frame);
    }
    frame(); // lets get this party started
  });
}

Encore une fois, il s'agit d'un remake des idées de mes précédents jeux de toile, donc si vous ne comprenez pas comment fonctionne la boucle de jeu, revenez à l'un des articles précédents.

Images et sprites


Avant le début du cycle de jeu, nous chargeons deux feuilles de sprites séparées (feuilles de sprites):

  • fond - trois couches de parallaxe pour le ciel, les collines et les arbres
  • sprites - sprites machine (plus des arbres et des panneaux d'affichage à ajouter à la version finale)


La feuille de sprite a été générée à l'aide d'une petite usine de sprites Rake and Ruby Gem .

Cette tâche génère les feuilles de sprites combinées, ainsi que les coordonnées x, y, w, h, qui seront stockées dans les constantes BACKGROUNDet SPRITES.

Remarque: J'ai créé les arrière-plans à l'aide d'Inkscape, et la plupart des sprites sont des graphiques tirés de l'ancienne version Outrun pour Genesis et utilisés comme exemples de formation.

Variables de jeu


En plus des images d'arrière-plans et de sprites, nous aurons besoin de plusieurs variables de jeu, à savoir:

var fps           = 60;                      // how many 'update' frames per second
var step          = 1/fps;                   // how long is each frame (in seconds)
var width         = 1024;                    // logical canvas width
var height        = 768;                     // logical canvas height
var segments      = [];                      // array of road segments
var canvas        = Dom.get('canvas');       // our canvas...
var ctx           = canvas.getContext('2d'); // ...and its drawing context
var background    = null;                    // our background image (loaded below)
var sprites       = null;                    // our spritesheet (loaded below)
var resolution    = null;                    // scaling factor to provide resolution independence (computed)
var roadWidth     = 2000;                    // actually half the roads width, easier math if the road spans from -roadWidth to +roadWidth
var segmentLength = 200;                     // length of a single segment
var rumbleLength  = 3;                       // number of segments per red/white rumble strip
var trackLength   = null;                    // z length of entire track (computed)
var lanes         = 3;                       // number of lanes
var fieldOfView   = 100;                     // angle (degrees) for field of view
var cameraHeight  = 1000;                    // z height of camera
var cameraDepth   = null;                    // z distance camera is from screen (computed)
var drawDistance  = 300;                     // number of segments to draw
var playerX       = 0;                       // player x offset from center of road (-1 to 1 to stay independent of roadWidth)
var playerZ       = null;                    // player relative z distance from camera (computed)
var fogDensity    = 5;                       // exponential fog density
var position      = 0;                       // current camera Z position (add playerZ to get player's absolute Z position)
var speed         = 0;                       // current speed
var maxSpeed      = segmentLength/step;      // top speed (ensure we can't move more than 1 segment in a single frame to make collision detection easier)
var accel         =  maxSpeed/5;             // acceleration rate - tuned until it 'felt' right
var breaking      = -maxSpeed;               // deceleration rate when braking
var decel         = -maxSpeed/5;             // 'natural' deceleration rate when neither accelerating, nor braking
var offRoadDecel  = -maxSpeed/2;             // off road deceleration is somewhere in between
var offRoadLimit  =  maxSpeed/4;             // limit when off road deceleration no longer applies (e.g. you can always go at least this speed even when off road)

Certains d'entre eux peuvent être personnalisés à l'aide de contrôles d'interface utilisateur pour modifier les valeurs critiques lors de l'exécution du programme afin que vous puissiez voir comment elles affectent le rendu de la route. D'autres sont recalculés à partir des valeurs d'interface utilisateur personnalisées dans la méthode reset().

Nous gérons Ferrari


Nous effectuons des raccourcis clavier pour Game.run, qui fournit une entrée de clavier simple qui définit ou réinitialise les variables qui signalent les actions actuelles du joueur:

Game.run({
  ...
  keys: [
    { keys: [KEY.LEFT,  KEY.A], mode: 'down', action: function() { keyLeft   = true;  } },
    { keys: [KEY.RIGHT, KEY.D], mode: 'down', action: function() { keyRight  = true;  } },
    { keys: [KEY.UP,    KEY.W], mode: 'down', action: function() { keyFaster = true;  } },
    { keys: [KEY.DOWN,  KEY.S], mode: 'down', action: function() { keySlower = true;  } },
    { keys: [KEY.LEFT,  KEY.A], mode: 'up',   action: function() { keyLeft   = false; } },
    { keys: [KEY.RIGHT, KEY.D], mode: 'up',   action: function() { keyRight  = false; } },
    { keys: [KEY.UP,    KEY.W], mode: 'up',   action: function() { keyFaster = false; } },
    { keys: [KEY.DOWN,  KEY.S], mode: 'up',   action: function() { keySlower = false; } }
  ],
  ...
}

L'état du joueur est contrôlé par les variables suivantes:

  • speed - vitesse actuelle.
  • position - la position Z actuelle sur la piste. Notez qu'il s'agit d'une position de caméra, pas d'une Ferrari.
  • playerX - position actuelle du joueur sur X sur la route. Normalisé dans la plage de -1 à +1, afin de ne pas dépendre de la valeur réelle roadWidth.

Ces variables sont définies à l'intérieur de la méthode update, qui effectue les actions suivantes:

  • mises à jour positionbasées sur le courant speed.
  • se met playerXà jour lorsque vous appuyez sur la touche gauche ou droite.
  • augmente speedsi la touche haut est enfoncée.
  • diminue speedsi la touche bas est enfoncée.
  • réduit speedsi les touches haut et bas ne sont pas enfoncées.
  • réduit speeds'il est playerXsitué en bordure de route et sur l'herbe.

Dans le cas des routes directes, la méthode est updateassez claire et simple:

function update(dt) {

  position = Util.increase(position, dt * speed, trackLength);

  var dx = dt * 2 * (speed/maxSpeed); // at top speed, should be able to cross from left to right (-1 to 1) in 1 second

  if (keyLeft)
    playerX = playerX - dx;
  else if (keyRight)
    playerX = playerX + dx;

  if (keyFaster)
    speed = Util.accelerate(speed, accel, dt);
  else if (keySlower)
    speed = Util.accelerate(speed, breaking, dt);
  else
    speed = Util.accelerate(speed, decel, dt);

  if (((playerX < -1) || (playerX > 1)) && (speed > offRoadLimit))
    speed = Util.accelerate(speed, offRoadDecel, dt);

  playerX = Util.limit(playerX, -2, 2);     // dont ever let player go too far out of bounds
  speed   = Util.limit(speed, 0, maxSpeed); // or exceed maxSpeed

}

Ne vous inquiétez pas, cela deviendra beaucoup plus difficile lorsque dans la version finale, nous ajouterons des sprites et la reconnaissance des collisions.

Géométrie routière


Avant de pouvoir rendre le monde du jeu, nous devons créer un tableau de segmentsin dans la méthode resetRoad().

Chacun de ces segments de la route sera finalement projeté à partir de ses coordonnées mondiales afin qu'il se transforme en un polygone 2D en coordonnées d'écran. Pour chaque segment, nous stockons deux points, p1 est le centre du bord le plus proche de la caméra et p2 est le centre du bord le plus éloigné de la caméra.


À proprement parler, p2 de chaque segment est identique à p1 du segment précédent, mais il me semble qu'il est plus facile de les stocker en tant que points séparés et de convertir chaque segment séparément.

Nous restons séparés rumbleLengthcar nous pouvons avoir de belles courbes et collines détaillées, mais en même temps des rayures horizontales. Si chaque segment suivant a une couleur différente, cela créera un mauvais effet stroboscopique. Par conséquent, nous voulons avoir de nombreux petits segments, mais les regrouper pour former des bandes horizontales distinctes.

function resetRoad() {
  segments = [];
  for(var n = 0 ; n < 500 ; n++) { // arbitrary road length
    segments.push({
       index: n,
       p1: { world: { z:  n   *segmentLength }, camera: {}, screen: {} },
       p2: { world: { z: (n+1)*segmentLength }, camera: {}, screen: {} },
       color: Math.floor(n/rumbleLength)%2 ? COLORS.DARK : COLORS.LIGHT
    });
  }

  trackLength = segments.length * segmentLength;
}

Nous initialisons p1 et p2 uniquement avec les coordonnées du monde z , car nous n'avons besoin que de routes droites. Les coordonnées y seront toujours 0 et les coordonnées x dépendront toujours de la valeur mise à l'échelle +/- roadWidth. Plus tard, lorsque nous ajouterons des courbes et des collines, cette partie changera.

Nous allons également définir des objets vides pour stocker des représentations de ces points dans la caméra et sur l'écran afin de ne pas créer un tas d'objets temporaires dans chacun render. Pour minimiser la récupération de place, nous devons éviter d'allouer des objets dans la boucle de jeu.

Lorsque la voiture atteint la fin de la route, nous revenons simplement au début du cycle. Pour simplifier cela, nous allons créer une méthode pour trouver un segment pour n'importe quelle valeur de Z, même s'il dépasse la longueur de la route:

function findSegment(z) {
  return segments[Math.floor(z/segmentLength) % segments.length];
}

Rendu en arrière-plan


La méthode render()commence par le rendu de l'image d'arrière-plan. Dans les parties suivantes, où nous ajouterons des courbes et des collines, nous aurons besoin de l'arrière-plan pour effectuer le défilement de parallaxe, nous allons donc commencer à nous déplacer dans cette direction, rendant l'arrière-plan en trois couches distinctes:

function render() {

  ctx.clearRect(0, 0, width, height);

  Render.background(ctx, background, width, height, BACKGROUND.SKY);
  Render.background(ctx, background, width, height, BACKGROUND.HILLS);
  Render.background(ctx, background, width, height, BACKGROUND.TREES);

  ...

Rendu routier


Ensuite, la fonction de rendu itère sur tous les segments et projette p1 et p2 de chaque segment des coordonnées universelles aux coordonnées d'écran, en coupant le segment si nécessaire et en le rendant autrement:

  var baseSegment = findSegment(position);
  var maxy        = height;
  var n, segment;
  for(n = 0 ; n < drawDistance ; n++) {

    segment = segments[(baseSegment.index + n) % segments.length];

    Util.project(segment.p1, (playerX * roadWidth), cameraHeight, position, cameraDepth, width, height, roadWidth);
    Util.project(segment.p2, (playerX * roadWidth), cameraHeight, position, cameraDepth, width, height, roadWidth);

    if ((segment.p1.camera.z <= cameraDepth) || // behind us
        (segment.p2.screen.y >= maxy))          // clip by (already rendered) segment
      continue;

    Render.segment(ctx, width, lanes,
                   segment.p1.screen.x,
                   segment.p1.screen.y,
                   segment.p1.screen.w,
                   segment.p2.screen.x,
                   segment.p2.screen.y,
                   segment.p2.screen.w,
                   segment.color);

    maxy = segment.p2.screen.y;
  }

Ci-dessus, nous avons déjà vu les calculs nécessaires à la projection d'un point; La version javascript combine la transformation, la projection et la mise à l'échelle en une seule méthode:

project: function(p, cameraX, cameraY, cameraZ, cameraDepth, width, height, roadWidth) {
  p.camera.x     = (p.world.x || 0) - cameraX;
  p.camera.y     = (p.world.y || 0) - cameraY;
  p.camera.z     = (p.world.z || 0) - cameraZ;
  p.screen.scale = cameraDepth/p.camera.z;
  p.screen.x     = Math.round((width/2)  + (p.screen.scale * p.camera.x  * width/2));
  p.screen.y     = Math.round((height/2) - (p.screen.scale * p.camera.y  * height/2));
  p.screen.w     = Math.round(             (p.screen.scale * roadWidth   * width/2));
}

En plus de calculer l'écran x et y pour chaque point p1 et p2, nous utilisons les mêmes calculs de projection pour calculer la largeur projetée ( w ) du segment.

Ayant les coordonnées d'écran x et y des points p1 et p2 , ainsi que la largeur projetée de la route w , nous pouvons assez facilement calculer à l'aide d'une fonction auxiliaire Render.segmenttous les polygones nécessaires pour rendre l'herbe, la route, les bandes horizontales et les lignes de division, en utilisant la fonction auxiliaire générale Render.polygon (voir . common.js) .

Rendu de voiture


Enfin, la dernière chose dont la méthode a besoin renderest un rendu Ferrari:

  Render.player(ctx, width, height, resolution, roadWidth, sprites, speed/maxSpeed,
                cameraDepth/playerZ,
                width/2,
                height);

Cette méthode est appelée player, et non car, car dans la version finale du jeu, il y aura d'autres voitures sur la route, et nous voulons séparer la Ferrari du joueur des autres voitures.

La fonction d'assistance Render.playerutilise la méthode canvas appelée drawImagepour rendre le sprite, après l'avoir précédemment mis à l'échelle en utilisant la même mise à l'échelle de projection que celle utilisée auparavant:

d / z

z dans ce cas est la distance relative de la machine à la caméra, stockée dans la variable playerZ .

De plus, la fonction «secoue» un peu la voiture à grande vitesse, ajoutant un peu de caractère aléatoire à l'équation de mise à l'échelle, en fonction de la vitesse / vitesse maximale .

Et voici ce que nous avons obtenu:


Conclusion


Nous avons fait un travail assez important juste pour créer un système avec des routes droites. Nous avons ajouté

  • module d'assistance générique dom
  • Utiliser le module mathématique général
  • Module d'aide au rendu général du canevas ...
  • ... y compris Render.segment, Render.polygonetRender.sprite
  • cycle de jeu à pas fixe
  • téléchargeur d'images
  • gestionnaire de clavier
  • fond de parallaxe
  • feuille de sprite avec des voitures, des arbres et des panneaux d'affichage
  • géométrie rudimentaire de la route
  • méthode update()pour contrôler la machine
  • méthode render()de rendu de l'arrière-plan, de la route et de la voiture du joueur
  • Balise HTML5 <audio>avec musique de course (bonus caché!)

... ce qui nous a donné une bonne base pour un développement ultérieur.

Partie 2. Courbes.



Dans cette partie, nous expliquerons plus en détail le fonctionnement des courbes.

Dans la partie précédente, nous avons compilé la géométrie de la route sous la forme d'un tableau de segments, dont chacun a des coordonnées mondiales, transformés par rapport à la caméra, puis projetés sur l'écran.

Nous n'avions besoin que de la coordonnée mondiale z pour chaque point, car sur les routes droites, x et y étaient égaux à zéro.


Si nous devions créer un système 3D entièrement fonctionnel, nous pourrions implémenter les courbes en calculant les bandes x et z des polygones illustrés ci-dessus. Cependant, ce type de géométrie sera assez difficile à calculer, et pour cela il faudra ajouter l'étape de rotation 3d aux équations de projection ...

... si nous allions dans ce sens, il serait préférable d'utiliser WebGL ou ses analogues, mais ce projet n'a pas d'autres tâches pour notre projet. Nous voulons simplement utiliser des astuces pseudo-tridimensionnelles à l'ancienne pour simuler des courbes.

Par conséquent, vous serez probablement surpris d'apprendre que nous ne calculerons pas du tout les coordonnées x des segments de route ...

Au lieu de cela, nous utiliserons les conseils de Lu :

"Pour courber la route, changez simplement la position de la ligne centrale de la forme de la courbe ... à partir du bas de l'écran, la quantité de décalage du centre de la route vers la gauche ou la droite augmente progressivement . "

Dans notre cas, la ligne médiane est la valeur cameraXpassée aux calculs de projection. Cela signifie que lorsque nous effectuons render()chaque segment de la route, vous pouvez simuler les courbes en décalant la valeur cameraXd'une valeur progressivement croissante.


Pour savoir combien changer, nous devons stocker une valeur dans chaque segment curve. Cette valeur indique dans quelle mesure le segment doit être décalé de la ligne centrale de la caméra. Elle sera:

  • négatif pour les virages à gauche
  • positif pour les courbes tournant à droite
  • moins pour les courbes lisses
  • plus pour les courbes nettes

Les valeurs elles-mêmes sont choisies assez arbitrairement; par essais et erreurs, nous pouvons trouver de bonnes valeurs auxquelles les courbes semblent être «correctes»:

var ROAD = {
  LENGTH: { NONE: 0, SHORT:  25, MEDIUM:  50, LONG:  100 }, // num segments
  CURVE:  { NONE: 0, EASY:    2, MEDIUM:   4, HARD:    6 }
};

En plus de choisir de bonnes valeurs pour les courbes, nous devons éviter tout écart dans les transitions lorsque la ligne se transforme en courbe (ou vice versa). Ceci peut être réalisé en adoucissant lors de l'entrée et de la sortie des courbes. Pour ce faire, nous augmenterons (ou diminuerons) progressivement la valeur curvede chaque segment à l'aide des fonctions de lissage traditionnelles jusqu'à ce qu'il atteigne la valeur souhaitée:

easeIn:    function(a,b,percent) { return a + (b-a)*Math.pow(percent,2);                           },
easeOut:   function(a,b,percent) { return a + (b-a)*(1-Math.pow(1-percent,2));                     },
easeInOut: function(a,b,percent) { return a + (b-a)*((-Math.cos(percent*Math.PI)/2) + 0.5);        },

C'est, maintenant, en tenant compte de la fonction d'ajouter un segment à la géométrie ...

function addSegment(curve) {
  var n = segments.length;
  segments.push({
     index: n,
        p1: { world: { z:  n   *segmentLength }, camera: {}, screen: {} },
        p2: { world: { z: (n+1)*segmentLength }, camera: {}, screen: {} },
     curve: curve,
     color: Math.floor(n/rumbleLength)%2 ? COLORS.DARK : COLORS.LIGHT
  });
}

nous pouvons créer une méthode pour une entrée, une recherche et une sortie en douceur d'une route incurvée:

function addRoad(enter, hold, leave, curve) {
  var n;
  for(n = 0 ; n < enter ; n++)
    addSegment(Util.easeIn(0, curve, n/enter));
  for(n = 0 ; n < hold  ; n++)
    addSegment(curve);
  for(n = 0 ; n < leave ; n++)
    addSegment(Util.easeInOut(curve, 0, n/leave));
}

... et en plus, vous pouvez imposer une géométrie supplémentaire, par exemple des courbes en S:

function addSCurves() {
  addRoad(ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM,  -ROAD.CURVE.EASY);
  addRoad(ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM,   ROAD.CURVE.MEDIUM);
  addRoad(ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM,   ROAD.CURVE.EASY);
  addRoad(ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM,  -ROAD.CURVE.EASY);
  addRoad(ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM,  -ROAD.CURVE.MEDIUM);
}

Modifications de la méthode update ()


Les seuls changements à apporter à la méthode update()sont l'application d'une sorte de force centrifuge lorsque la machine se déplace le long d'une courbe.

Nous définissons un facteur arbitraire qui peut être ajusté selon nos préférences.

var centrifugal = 0.3;   // centrifugal force multiplier when going around curves

Et puis nous allons simplement mettre à jour la position en playerXfonction de sa vitesse actuelle, de la valeur de la courbe et du multiplicateur de force centrifuge:

playerX = playerX - (dx * speedPercent * playerSegment.curve * centrifugal);

Rendu de courbe


Nous avons dit ci-dessus que vous pouvez rendre des courbes simulées en décalant la valeur cameraXutilisée dans les calculs de projection lors de l'exécution de render()chaque segment de route.


Pour ce faire, nous allons stocker la variable d'entraînement dx , en augmentant pour chaque segment d'une valeur curve, ainsi que la variable x , qui sera utilisée comme décalage de la valeur cameraXutilisée dans les calculs de projection.

Pour implémenter les courbes, nous avons besoin des éléments suivants:

  • décaler la projection p1 de chaque segment de x
  • décaler la projection p2 de chaque segment de x + dx
  • augmenter x pour le segment suivant de dx

Enfin, afin d'éviter les transitions déchirées lors du franchissement des frontières des segments, il faut faire dx initialisé avec la valeur interpolée de la courbe des segments de base courants.

Modifiez la méthode render()comme suit:

var baseSegment = findSegment(position);
var basePercent = Util.percentRemaining(position, segmentLength);
var dx = - (baseSegment.curve * basePercent);
var x  = 0;
for(n = 0 ; n < drawDistance ; n++) {

  ...

  Util.project(segment.p1, (playerX * roadWidth) - x,      cameraHeight, position - (segment.looped ? trackLength : 0), cameraDepth, width, height, roadWidth);
  Util.project(segment.p2, (playerX * roadWidth) - x - dx, cameraHeight, position - (segment.looped ? trackLength : 0), cameraDepth, width, height, roadWidth);

  x  = x + dx;
  dx = dx + segment.curve;

  ...
}

Fond de défilement de parallaxe


Enfin, nous devons faire défiler les couches d'arrière-plan de parallaxe, en stockant le décalage pour chaque couche ...

var skySpeed    = 0.001; // background sky layer scroll speed when going around curve (or up hill)
var hillSpeed   = 0.002; // background hill layer scroll speed when going around curve (or up hill)
var treeSpeed   = 0.003; // background tree layer scroll speed when going around curve (or up hill)
var skyOffset   = 0;     // current sky scroll offset
var hillOffset  = 0;     // current hill scroll offset
var treeOffset  = 0;     // current tree scroll offset

... et l'augmenter dans le temps en update()fonction de la valeur de la courbe du segment de joueur actuel et de sa vitesse ...

skyOffset  = Util.increase(skyOffset,  skySpeed  * playerSegment.curve * speedPercent, 1);
hillOffset = Util.increase(hillOffset, hillSpeed * playerSegment.curve * speedPercent, 1);
treeOffset = Util.increase(treeOffset, treeSpeed * playerSegment.curve * speedPercent, 1);

... puis utilisez pour utiliser ce décalage lors de la création de render()calques d'arrière-plan.

Render.background(ctx, background, width, height, BACKGROUND.SKY,   skyOffset);
Render.background(ctx, background, width, height, BACKGROUND.HILLS, hillOffset);
Render.background(ctx, background, width, height, BACKGROUND.TREES, treeOffset);

Conclusion


Donc, ici, nous obtenons les fausses courbes pseudo-tridimensionnelles:


La partie principale du code que nous avons ajouté est de construire la géométrie de la route avec la valeur correspondante curve. En le réalisant, ajouter de la force centrifuge pendant le temps est update()beaucoup plus facile.

Le rendu de courbe est effectué en quelques lignes de code, mais il peut être difficile de comprendre (et décrire) ce qui se passe exactement ici. Il existe de nombreuses façons de simuler des courbes et il est très facile de se promener lorsqu'elles sont implémentées dans une impasse. Il est encore plus facile de se laisser emporter par une tâche extérieure et d'essayer de tout faire «correctement»; avant de vous en rendre compte, vous commencerez à créer un système 3D entièrement fonctionnel avec des matrices, des rotations et une vraie géométrie 3D ... ce qui, comme je l'ai dit, n'est pas notre tâche.

Lorsque j'ai écrit cet article, j'étais sûr qu'il y avait définitivement des problèmes dans ma mise en œuvre des courbes. En essayant de visualiser l'algorithme, je ne comprenais pas pourquoi j'avais besoin de deux valeurs des lecteurs dx et x au lieu d'une ... et si je ne peux pas expliquer complètement quelque chose, alors quelque chose s'est mal passé quelque part ...

... mais le temps du projet " le week-end »a presque expiré et, franchement, les courbes me semblent assez belles, et au final, c'est le plus important.

All Articles