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 semanaLa 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 jugarSobre 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 calculary = 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 factord / 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:- conversión de coordenadas mundiales a coordenadas de pantalla
- proyectar coordenadas de la cámara en un plano de proyección normalizado
- 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.js
dentro 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.js
solo 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 versionesupdate
- 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,
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();
});
}
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 BACKGROUND
y 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;
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;
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
position
basadas en la actual speed
. - se actualiza
playerX
cuando presiona la tecla izquierda o derecha. - aumenta
speed
si se presiona la tecla arriba. - disminuye
speed
si se presiona la tecla hacia abajo. - se reduce
speed
si no se presionan las teclas arriba y abajo. - se reduce
speed
si se playerX
encuentra fuera del borde de la carretera y en el césped.
En el caso de las carreteras directas, el método es update
bastante claro y 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);
}
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 segments
en 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 rumbleLength
porque 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++) {
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) ||
(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;
}
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.segment
todos 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 render
es 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.player
usa el método de lienzo llamado drawImage
para 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.polygon
yRender.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 cameraX
pasado 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 cameraX
gradualmente.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 },
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 curve
de 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;
Y luego actualizaremos la posición en playerX
funció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 cameraX
utilizado 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 cameraX
utilizado 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;
var hillSpeed = 0.002;
var treeSpeed = 0.003;
var skyOffset = 0;
var hillOffset = 0;
var treeOffset = 0;
... 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.