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-endLa 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 calculery = 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
Où 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 facteurd / 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:- conversion des coordonnées du monde en coordonnées d'écran
- projection des coordonnées de la caméra sur un plan de projection normalisé
- 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.js
dans 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.js
seulement 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 versionsupdate
- 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,
render = options.render,
step = options.step,
now = null,
last = Util.timestamp(),
dt = 0,
gdt = 0;
function frame() {
now = Util.timestamp();
dt = Math.min(1, (now - last) / 1000);
gdt = gdt + dt;
while (gdt > step) {
gdt = gdt - step;
update(step);
}
render();
last = now;
requestAnimationFrame(frame);
}
frame();
});
}
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 BACKGROUND
et 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;
var step = 1/fps;
var width = 1024;
var height = 768;
var segments = [];
var canvas = Dom.get('canvas');
var ctx = canvas.getContext('2d');
var background = null;
var sprites = null;
var resolution = null;
var roadWidth = 2000;
var segmentLength = 200;
var rumbleLength = 3;
var trackLength = null;
var lanes = 3;
var fieldOfView = 100;
var cameraHeight = 1000;
var cameraDepth = null;
var drawDistance = 300;
var playerX = 0;
var playerZ = null;
var fogDensity = 5;
var position = 0;
var speed = 0;
var maxSpeed = segmentLength/step;
var accel = maxSpeed/5;
var breaking = -maxSpeed;
var decel = -maxSpeed/5;
var offRoadDecel = -maxSpeed/2;
var offRoadLimit = maxSpeed/4;
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
position
basées sur le courant speed
. - se met
playerX
à jour lorsque vous appuyez sur la touche gauche ou droite. - augmente
speed
si la touche haut est enfoncée. - diminue
speed
si la touche bas est enfoncée. - réduit
speed
si les touches haut et bas ne sont pas enfoncées. - réduit
speed
s'il est playerX
situé en bordure de route et sur l'herbe.
Dans le cas des routes directes, la méthode est update
assez claire et simple:function update(dt) {
position = Util.increase(position, dt * speed, trackLength);
var dx = dt * 2 * (speed/maxSpeed);
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);
speed = Util.limit(speed, 0, 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 segments
in 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 rumbleLength
car 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++) {
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) ||
(segment.p2.screen.y >= maxy))
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.segment
tous 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 render
est 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.player
utilise la méthode canvas appelée drawImage
pour 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
Où 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.polygon
etRender.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 cameraX
passé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 cameraX
d'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 },
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 curve
de 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;
Et puis nous allons simplement mettre à jour la position en playerX
fonction 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 cameraX
utilisé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 cameraX
utilisé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;
var hillSpeed = 0.002;
var treeSpeed = 0.003;
var skyOffset = 0;
var hillOffset = 0;
var treeOffset = 0;
... 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.