Crear un juego de carreras pseudo-tridimensional


Cuando era niño, rara vez iba a salas de arcade porque realmente no las necesitaba, porque tenía juegos increíbles para C64 en casa ... pero hay tres juegos de arcade para los que siempre tuve dinero: Donkey Kong, Dragons Lair y Outrun ...

... y realmente me encantó Outrun: velocidad, colinas, palmeras y música, incluso en la versión débil para el C64.


Así que decidí intentar escribir un juego de carreras pseudo-tridimensional de la vieja escuela al estilo de Outrun, Pitstop o Pole position. No planeo armar un juego completo y completo , pero me parece interesante volver a examinar la mecánica con la que estos juegos realizaron sus trucos. Curvas, colinas, sprites y una sensación de velocidad ...

Entonces, aquí está mi "proyecto de fin de semana", que finalmente tardó cinco o seis semanas en el fin de semana



La versión jugable se parece más a una demostración técnica que a un juego real. De hecho, si quieres crear una verdadera carrera pseudo-tridimensional, esta será la base más mínima que necesitas para convertir gradualmente en un juego.

No está pulido, es un poco feo, pero completamente funcional. Le mostraré cómo implementarlo por su cuenta en cuatro simples pasos.

También puedes jugar


Sobre el rendimiento


El rendimiento de este juego depende mucho de la máquina / navegador. En los navegadores modernos, funciona bien, especialmente en aquellos que tienen aceleración de GPU de lienzo, pero un mal controlador de gráficos puede hacer que se congele. En el juego, puedes cambiar la resolución y la distancia de representación.

Sobre la estructura del código


Sucedió que el proyecto se implementó en Javascript (debido a la simplicidad de la creación de prototipos), pero no está destinado a demostrar las técnicas o técnicas recomendadas de Javascript. De hecho, para facilitar la comprensión, el Javascript de cada ejemplo está incrustado directamente en la página HTML (¡horror!); peor, usa variables y funciones globales.

Si estuviera creando un juego real, el código sería mucho más estructurado y racionalizado, pero como se trata de una demostración técnica de un juego de carreras, decidí seguir con KISS .

Parte 1. Carreteras rectas.


Entonces, ¿cómo comenzamos a crear un juego de carreras pseudo-tridimensional?

Bueno, necesitamos

  • Repetir trigonometría
  • Recordemos los conceptos básicos de la proyección en 3D.
  • Crea un bucle de juego
  • Descargar imágenes de sprites
  • Construir geometría de carreteras
  • Renderizar fondo
  • Renderiza el camino
  • Render car
  • Implementar soporte de teclado para control de máquina

Pero antes de comenzar, leamos la página pseudo 3d de Lou [ traducción Habré], la única fuente de información (que pude encontrar) sobre cómo crear el juego de carreras psevdotrohmernuyu.

¿Terminaste de leer el artículo de Lou? ¡Multa! Crearemos una variación de sus colinas realistas utilizando la técnica de segmentos proyectados en 3D. Haremos esto gradualmente durante las siguientes cuatro partes. Pero comenzaremos ahora, con la versión v1, y crearemos una geometría de carretera recta muy simple al proyectarla en un elemento de lienzo HTML5.

La demostración se puede ver aquí .

Un poco de trigonometría


Antes de comenzar con la implementación, usemos los conceptos básicos de la trigonometría para recordar cómo proyectar un punto en el mundo 3D en una pantalla 2D.

En el caso más simple, si no toca vectores y matrices, la ley de triángulos similares se usa para la proyección 3D .

Usamos la siguiente notación:

  • h = altura de la cámara
  • d = distancia de la cámara a la pantalla
  • z = distancia de la cámara al automóvil
  • y = pantalla y coordenada

Entonces podemos usar la ley de triángulos similares para calcular

y = h * d / z

como se muestra en el diagrama:


También podría dibujar un diagrama similar en una vista superior en lugar de una vista lateral, y derivar una ecuación similar para calcular la coordenada X de la pantalla:

x = w * d / z

Donde w = la mitad del ancho de la carretera (desde la cámara hasta el borde de la carretera).

Como puede ver, para x e y escalamos por un factor

d / z

Sistemas coordinados


En forma de diagrama, se ve hermoso y simple, pero cuando comienza a codificar, puede confundirse un poco, porque elegimos nombres arbitrarios, y no está claro con qué designamos las coordenadas del mundo 3D y cuáles son las coordenadas de la pantalla 2D. También suponemos que la cámara está en el centro del origen del mundo, aunque en realidad seguirá a la máquina.

Si se acerca más formalmente, entonces debemos realizar:

  1. conversión de coordenadas mundiales a coordenadas de pantalla
  2. proyectar coordenadas de la cámara en un plano de proyección normalizado
  3. escalar las coordenadas proyectadas a las coordenadas de la pantalla física (en nuestro caso, esto es lienzo)


Nota: en el presente sistema 3D , la etapa de rotación se realiza entre las etapas 1 y 2 , pero como simularemos las curvas, no necesitamos una rotación.

Proyección


Las ecuaciones de proyección formales se pueden representar de la siguiente manera:


  • El punto de conversión de ecuaciones ( traslación ) se calcula en relación con la cámara
  • Las ecuaciones de proyección ( proyecto ) son variaciones de la "ley de triángulos similares" que se muestra arriba.
  • Las ecuaciones de escala ( escala ) tienen en cuenta la diferencia entre:
    • matemática , donde 0,0 está en el centro y el eje y está arriba, y
    • , 0,0 , y :


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



La última pieza del rompecabezas será una forma de calcular d : la distancia desde la cámara al plano de proyección.

En lugar de simplemente escribir un valor fijo de d , sería más útil calcularlo desde el campo de visión vertical deseado. Gracias a esto, podremos "acercar" la cámara si es necesario.

Si suponemos que estamos proyectando en un plano de proyección normalizado, cuyas coordenadas están en el rango de -1 a +1, entonces d puede calcularse de la siguiente manera:

d = 1 / tan (fov / 2)

Al definir fov como una (de muchas) variables, podemos ajustar el alcance para ajustar el algoritmo de representación.

Estructura del código Javascript


Al comienzo del artículo, ya dije que el código no cumple con las pautas para escribir Javascript: es una demostración “rápida y sucia” con funciones y variables globales simples. Sin embargo, dado que voy a crear cuatro versiones separadas (recta, curvas, colinas y sprites), almacenaré algunos métodos reutilizables common.jsdentro de los siguientes módulos:

  • Dom es algunas funciones secundarias de ayuda DOM.
  • Util : utilidades generales, principalmente funciones matemáticas auxiliares.
  • Juego : funciones generales de soporte de juegos, como el descargador de imágenes y el bucle del juego.
  • Renderizado : funciones de representación auxiliares en el lienzo.

Explicaré en detalle los métodos common.jssolo si se relacionan con el juego en sí y no son solo funciones matemáticas o DOM auxiliares. Con suerte, por el nombre y el contexto, quedará claro qué deberían hacer los métodos.

Como de costumbre, el código fuente está en la documentación final.

Bucle de juego simple


Antes de renderizar algo, necesitamos un bucle de juego. Si lees alguno de mis artículos anteriores sobre juegos ( pong , breakout , tetris , serpientes o boulderdash ), entonces ya has visto ejemplos de mi ciclo de juego favorito con un paso de tiempo fijo .

No profundizaré en los detalles, y simplemente reutilizaré parte del código de juegos anteriores para crear un bucle de juego con un paso de tiempo fijo usando requestAnimationFrame .

El principio es que cada uno de mis cuatro ejemplos puede llamar Game.run(...)y usar sus propias versiones

  • update - Actualización del mundo del juego con un paso de tiempo fijo.
  • render - Actualización del mundo del juego cuando el navegador lo permite.

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
  });
}

Una vez más, esta es una nueva versión de las ideas de mis juegos de lienzo anteriores, por lo que si no comprende cómo funciona el bucle del juego, vuelva a uno de los artículos anteriores.

Imágenes y sprites


Antes de que comience el ciclo del juego, cargamos dos hojas de sprites separadas (hojas de sprites):

  • Fondo : tres capas de paralaje para cielo, colinas y árboles
  • sprites : sprites de máquina (además de árboles y vallas publicitarias que se agregarán a la versión final)


La hoja de sprites se generó utilizando una pequeña tarea de la fábrica de sprites Rake and Ruby Gem .

Esta tarea genera las hojas de sprites combinadas, así como las coordenadas x, y, w, h, que se almacenarán en las constantes BACKGROUNDy SPRITES.

Nota: Creé los fondos usando Inkscape, y la mayoría de los sprites son gráficos tomados de la versión anterior de Outrun para Genesis y utilizados como ejemplos de entrenamiento.

Variables del juego


Además de las imágenes de fondos y sprites, necesitaremos varias variables del juego, a saber:

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)

Algunos de ellos se pueden personalizar mediante controles de la interfaz de usuario para cambiar los valores críticos durante la ejecución del programa, de modo que pueda ver cómo afectan la representación del camino. Otros se recalculan a partir de valores de IU personalizados en el método reset().

Gestionamos Ferrari


Realizamos combinaciones de teclas para Game.run, que proporcionan una entrada de teclado simple que establece o restablece variables que informan las acciones actuales del jugador:

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; } }
  ],
  ...
}

El estado del jugador está controlado por las siguientes variables:

  • velocidad - velocidad actual.
  • position : la posición Z actual en la pista. Tenga en cuenta que esta es una posición de cámara, no un Ferrari.
  • playerX : la posición actual del jugador en X en el camino. Normalizado en el rango de -1 a +1, para no depender del valor real roadWidth.

Estas variables se establecen dentro del método update, que realiza las siguientes acciones:

  • actualizaciones positionbasadas en la actual speed.
  • se actualiza playerXcuando presiona la tecla izquierda o derecha.
  • aumenta speedsi se presiona la tecla arriba.
  • disminuye speedsi se presiona la tecla hacia abajo.
  • se reduce speedsi no se presionan las teclas arriba y abajo.
  • se reduce speedsi se playerXencuentra fuera del borde de la carretera y en el césped.

En el caso de las carreteras directas, el método es updatebastante claro y 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

}

No se preocupe, será mucho más difícil cuando en la versión final agreguemos sprites y reconocimiento de colisión.

Geometría del camino


Antes de que podamos renderizar el mundo del juego, necesitamos construir una matriz segmentsen el método resetRoad().

Cada uno de estos segmentos de la carretera finalmente se proyectará desde sus coordenadas mundiales para que se convierta en un polígono 2D en las coordenadas de la pantalla. Para cada segmento, almacenamos dos puntos, p1 es el centro del borde más cercano a la cámara y p2 es el centro del borde más alejado de la cámara.


Estrictamente hablando, p2 de cada segmento es idéntico a p1 del segmento anterior, pero me parece que es más fácil almacenarlos como puntos separados y convertir cada segmento por separado.

Nos mantenemos separados rumbleLengthporque podemos tener hermosas curvas detalladas y colinas, pero al mismo tiempo rayas horizontales. Si cada segmento posterior tiene un color diferente, esto creará un mal efecto estroboscópico. Por lo tanto, queremos tener muchos segmentos pequeños, pero agruparlos para formar franjas horizontales separadas.

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;
}

Inicializamos p1 y p2 solo con coordenadas mundiales z , porque solo necesitamos carreteras rectas. Las Y. coordenadas siempre será 0, y los x coordenadas siempre dependerán del valor escalado +/- roadWidth. Más tarde, cuando agreguemos curvas y colinas, esta parte cambiará.

También estableceremos objetos vacíos para almacenar representaciones de estos puntos en la cámara y en la pantalla para no crear un montón de objetos temporales en cada uno render. Para minimizar la recolección de basura, debemos evitar asignar objetos dentro del ciclo del juego.

Cuando el automóvil llega al final del camino, simplemente regresamos al comienzo del ciclo. Para simplificar esto, crearemos un método para encontrar un segmento para cualquier valor de Z, incluso si va más allá de la longitud del camino:

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

Representación de fondo


El método render()comienza representando la imagen de fondo. En las siguientes partes, donde agregaremos curvas y colinas, necesitaremos el fondo para realizar el desplazamiento de paralaje, por lo que ahora comenzaremos a movernos en esta dirección, representando el fondo como tres capas separadas:

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);

  ...

Renderizado de carreteras


Luego, la función de renderizado itera a través de todos los segmentos y proyectos p1 y p2 de cada segmento desde las coordenadas mundiales hasta las coordenadas de la pantalla, recortando el segmento si es necesario y de otra manera renderizándolo:

  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;
  }

Arriba, ya hemos visto los cálculos necesarios para proyectar un punto; La versión de JavaScript combina transformación, proyección y escala en un solo método:

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));
}

Además de calcular la pantalla x e y para cada punto p1 y p2, utilizamos los mismos cálculos de proyección para calcular el ancho proyectado ( w ) del segmento.

Teniendo las coordenadas x e y de la pantalla de los puntos p1 y p2 , así como el ancho proyectado de la carretera w , podemos calcular fácilmente con la ayuda de una función auxiliar Render.segmenttodos los polígonos necesarios para renderizar hierba, carretera, franjas horizontales y líneas divisorias, utilizando la función auxiliar general Render.polygon (ver . common.js) .

Renderizado de autos


Finalmente, lo último que necesita el método renderes una representación de Ferrari:

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

Este método se llama player, y no car, porque en la versión final del juego habrá otros autos en la carretera, y queremos separar el Ferrari del jugador de otros autos.

La función auxiliar Render.playerusa el método de lienzo llamado drawImagepara renderizar el sprite, habiéndolo escalado previamente usando la misma escala de proyección que se usó antes:

d / z

Donde z en este caso es la distancia relativa de la máquina a la cámara, almacenada en la variable playerZ .

Además, la función "sacude" el automóvil un poco a altas velocidades, agregando un poco de aleatoriedad a la ecuación de escala, dependiendo de la velocidad / velocidad máxima .

Y aquí está lo que tenemos:


Conclusión


Hicimos una gran cantidad de trabajo solo para crear un sistema con carreteras rectas. Agregamos

  • módulo auxiliar genérico dom
  • Util módulo de matemáticas general
  • Renderizar módulo auxiliar de lienzo general ...
  • ... incluyendo Render.segment, Render.polygonyRender.sprite
  • ciclo de juego de lanzamiento fijo
  • descargador de imágenes
  • controlador de teclado
  • fondo de paralaje
  • hoja de sprites con autos, árboles y vallas publicitarias
  • geometría rudimentaria de la carretera
  • método update()para controlar la máquina
  • Método render()para representar el fondo, la carretera y el auto del jugador
  • Etiqueta HTML5 <audio>con música de carreras (¡bonificación oculta!)

... lo que nos dio una buena base para un mayor desarrollo.

Parte 2. Curvas.



En esta parte, explicaremos con más detalle cómo funcionan las curvas.

En la parte anterior, compilamos la geometría de la carretera en forma de una serie de segmentos, cada uno de los cuales tiene coordenadas mundiales que se transforman en relación con la cámara y luego se proyectan en la pantalla.

Solo necesitábamos la coordenada mundial z para cada punto, porque en carreteras rectas, tanto x como y eran iguales a cero.


Si creáramos un sistema 3D completamente funcional, podríamos implementar las curvas calculando las franjas x y z de los polígonos que se muestran arriba. Sin embargo, este tipo de geometría será bastante difícil de calcular, y para esto será necesario agregar la etapa de rotación 3D a las ecuaciones de proyección ...

... si seguimos este camino, sería mejor usar WebGL o sus análogos, pero este proyecto no tiene otras tareas para nuestro proyecto. Solo queremos usar trucos pseudo-tridimensionales de la vieja escuela para simular curvas.

Por lo tanto, probablemente se sorprenderá al saber que no calcularemos las coordenadas x de los segmentos de la carretera en absoluto ...

En su lugar, utilizaremos el consejo de Lu :

"Para curvar la carretera, simplemente cambie la posición de la línea central de la forma de la curva ... comenzando desde la parte inferior de la pantalla, la cantidad de desplazamiento del centro de la carretera hacia la izquierda o hacia la derecha aumenta gradualmente" .

En nuestro caso, la línea central es el valor cameraXpasado a los cálculos de proyección. Esto significa que cuando realizamos render()cada segmento de la carretera, puede simular las curvas cambiando el valor cameraXgradualmente.


Para saber cuánto cambiar, necesitamos almacenar un valor en cada segmento curve. Este valor indica cuánto se debe desplazar el segmento desde la línea central de la cámara. Ella estará:

  • negativo para curvas de giro a la izquierda
  • positivo para curvas que giran a la derecha
  • menos para curvas suaves
  • más para curvas cerradas

Los valores mismos se eligen de manera bastante arbitraria; a través de prueba y error, podemos encontrar buenos valores en los que las curvas parecen ser "correctas":

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

Además de elegir buenos valores para las curvas, debemos evitar espacios en las transiciones cuando la línea se convierte en una curva (o viceversa). Esto se puede lograr ablandando al entrar y salir de las curvas. Haremos esto aumentando gradualmente (o disminuyendo) el valor curvede cada segmento utilizando las funciones de suavizado tradicionales hasta que alcance el valor deseado:

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);        },

Es decir, ahora, teniendo en cuenta la función de agregar un segmento a la geometría ...

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
  });
}

Podemos crear un método para entrar, encontrar y salir sin problemas de una carretera curva:

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));
}

... y encima puedes imponer geometría adicional, por ejemplo, curvas en forma de 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);
}

Cambios en el método update ()


Los únicos cambios que deben hacerse al método update()son la aplicación de un tipo de fuerza centrífuga cuando la máquina se mueve a lo largo de una curva.

Establecemos un factor arbitrario que se puede ajustar de acuerdo con nuestras preferencias.

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

Y luego actualizaremos la posición en playerXfunción de su velocidad actual, valor de curva y multiplicador de fuerza centrífuga:

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

Representación de curvas


Dijimos anteriormente que puede representar curvas simuladas cambiando el valor cameraXutilizado en los cálculos de proyección durante la ejecución de render()cada segmento de carretera.


Para hacer esto, almacenaremos la variable de unidad dx , que aumentará para cada segmento en un valor curve, así como la variable x , que se utilizará como compensación del valor cameraXutilizado en los cálculos de proyección.

Para implementar las curvas, necesitamos lo siguiente:

  • desplazar la proyección p1 de cada segmento por x
  • desplazar la proyección p2 de cada segmento por x + dx
  • aumentar x para el siguiente segmento en dx

Finalmente, para evitar transiciones desgarradas al cruzar los límites del segmento, debemos hacer que dx se inicialice con el valor de la curva interpolada de los segmentos base actuales.

Cambie el método de la render()siguiente manera:

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;

  ...
}

Fondo de desplazamiento de paralaje


Finalmente, necesitamos desplazar las capas de fondo de paralaje, almacenando el desplazamiento para cada capa ...

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

... y aumentarlo durante el tiempo update()dependiendo del valor de la curva del segmento de jugador actual y su velocidad ...

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);

... y luego usa para usar este desplazamiento cuando haces render()capas de fondo.

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);

Conclusión


Entonces, aquí obtenemos las curvas pseudo-tridimensionales falsas:


La parte principal del código que agregamos es construir la geometría de la carretera con el valor correspondiente curve. Al darse cuenta de ello, agregar fuerza centrífuga durante el tiempo es update()mucho más fácil.

La representación de curvas se realiza en solo unas pocas líneas de código, pero puede ser difícil entender (y describir) exactamente qué está sucediendo aquí. Hay muchas formas de simular curvas y es muy fácil deambular cuando se implementan en un callejón sin salida. Es aún más fácil dejarse llevar por una tarea externa e intentar hacer todo "correctamente"; Antes de que te des cuenta de esto, comenzarás a crear un sistema 3D completamente funcional con matrices, rotaciones y geometría 3D real ... que, como dije, no es nuestra tarea.

Cuando escribí este artículo, estaba seguro de que definitivamente había problemas en mi implementación de las curvas. Tratando de visualizar el algoritmo, no entendí por qué necesitaba dos valores de las unidades dx y x en lugar de uno ... y si no puedo explicar completamente algo, entonces algo salió mal en alguna parte ...

... pero el tiempo del proyecto "en el fin de semana ” casi ha expirado, y, francamente, las curvas me parecen bastante hermosas, y al final, esto es lo más importante.

All Articles