Quando criança, raramente ia a salões de fliperama porque não precisava deles, porque tinha jogos incríveis para C64 em casa ... mas há três jogos de arcade para os quais sempre tinha dinheiro - Donkey Kong, Dragons Lair e Outrun ...... e eu realmente amei Outrun - velocidade, morros, palmeiras e música, mesmo na versão fraca do C64.Por isso, decidi tentar escrever um jogo de corrida pseudo-tridimensional da velha escola no estilo da posição Outrun, Pitstop ou Pole. Não pretendo montar um jogo completo e completo , mas parece-me interessante reexaminar a mecânica com a qual esses jogos realizaram seus truques. Curvas, colinas, sprites e uma sensação de velocidade ...Então, aqui está o meu "projeto de fim de semana", que levou cinco ou seis semanas no fim de semanaA versão jogável é mais uma demonstração técnica do que um jogo real. De fato, se você deseja criar uma verdadeira corrida pseudo-tridimensional, essa será a base mais mínima que você precisa para gradualmente se transformar em um jogo.Não é polido, um pouco feio, mas totalmente funcional. Vou mostrar como implementá-lo por conta própria em quatro etapas simples.Você também pode jogarSobre desempenho
O desempenho deste jogo é muito dependente da máquina / navegador. Nos navegadores modernos, funciona bem, especialmente naqueles com aceleração de GPU de tela, mas um driver gráfico ruim pode congelar. No jogo, você pode alterar a resolução e a distância de renderização.Sobre a estrutura do código
Aconteceu que o projeto foi implementado em Javascript (devido à simplicidade da prototipagem), mas não se destina a demonstrar as técnicas ou técnicas recomendadas de Javascript. De fato, para facilitar a compreensão, o Javascript de cada exemplo é incorporado diretamente na página HTML (horror!); pior, ele usa variáveis e funções globais.Se eu estivesse criando um jogo real, o código seria muito mais estruturado e simplificado, mas como essa é uma demonstração técnica de um jogo de corrida, decidi seguir o KISS .Parte 1. Estradas retas.
Então, como começamos a criar um jogo de corrida pseudo-tridimensional?Bem, precisamos- Repita a trigonometria
- Lembre-se do básico da projeção em 3D
- Crie um loop de jogo
- Baixar imagens de sprite
- Construir geometria da estrada
- Renderizar plano de fundo
- Renderize a estrada
- Renderizar carro
- Implementar suporte de teclado para controle da máquina
Mas antes de começarmos, vamos ler a Pseudo 3d Page de Lou [ tradução Habré] - a única fonte de informação (que eu pude encontrar) sobre como criar o jogo de corrida psevdotrohmernuyu.Terminou de ler o artigo de Lou? Bem! Criaremos uma variação de sua técnica Realistic Hills Using 3d-Projected Segments. Faremos isso gradualmente nas próximas quatro partes. Mas começaremos agora, com a versão v1, e criaremos uma geometria de estrada reta muito simples projetando-a em um elemento de tela HTML5.A demo pode ser vista aqui .
Um pouco de trigonometria
Antes de começarmos a implementação, vamos usar o básico da trigonometria para lembrar como projetar um ponto no mundo 3D em uma tela 2D.No caso mais simples, se você não tocar em vetores e matrizes, a lei de triângulos semelhantes é usada para projeção em 3D .Usamos a seguinte notação:- h = altura da câmera
- d = distância da câmera à tela
- z = distância da câmera ao carro
- y = tela y coordenada
Então podemos usar a lei de triângulos semelhantes para calculary = h * d / z
como mostrado no diagrama:Você também pode desenhar um diagrama semelhante em uma vista superior, em vez de uma vista lateral, e derivar uma equação semelhante para calcular a coordenada X da tela:x = w * d / z
Onde w = metade da largura da estrada (da câmera até a beira da estrada).Como você pode ver, para x e y , escalamos por um fatord / z
Sistemas coordenados
Na forma de um diagrama, parece bonito e simples, mas quando você começa a codificar, pode ficar um pouco confuso, porque escolhemos nomes arbitrários e não está claro o que indicamos as coordenadas do mundo 3D e quais são as coordenadas da tela 2D. Também assumimos que a câmera está no centro da origem do mundo, embora na realidade ela siga a máquina.Se você se aproximar mais formalmente, precisamos executar:- conversão de coordenadas mundiais em coordenadas de tela
- projetar as coordenadas da câmera em um plano de projeção normalizado
- escalando as coordenadas projetadas para as coordenadas da tela física (no nosso caso, isso é tela)
Nota: no atual sistema 3d , o estágio de rotação é realizado entre os estágios 1 e 2 , mas, como simularemos as curvas, não precisamos de rotação.
Projeção
As equações formais de projeção podem ser representadas da seguinte forma:- O ponto das equações de conversão ( translação ) é calculado em relação à câmara
- As equações de projeção ( projeto ) são variações da “lei dos triângulos semelhantes” mostrada acima.
- Equações de escala ( escala ) levam em consideração a diferença entre:
- matemática , onde 0,0 está no centro e o eixo y está elevado, e
- , 0,0 , y :
: 3d- Vector
Matrix
3d-, , WebGL ( )… . Outrun.
A última peça do quebra-cabeça será uma maneira de calcular d - a distância da câmera ao plano de projeção.Em vez de apenas escrever um valor fixo de d , seria mais útil calculá-lo a partir do campo de visão vertical desejado. Graças a isso, poderemos "ampliar" a câmera, se necessário.Se assumirmos que estamos projetando em um plano de projeção normalizado, cujas coordenadas estão no intervalo de -1 a +1, então d pode ser calculado da seguinte forma:d = 1 / tan (fov / 2)
Ao definir fov como uma (de muitas) variáveis, podemos ajustar o escopo para ajustar o algoritmo de renderização.Estrutura de código Javascript
No começo do artigo, eu já disse que o código não está de acordo com as diretrizes para escrever Javascript - é uma demonstração "rápida e suja", com variáveis e funções globais simples. No entanto, como vou criar quatro versões separadas (retas, curvas, morros e sprites), armazenarei alguns métodos reutilizáveis dentro common.js
dos seguintes módulos:- Dom são algumas funções auxiliares menores do DOM.
- Util - utilitários gerais, principalmente funções matemáticas auxiliares.
- Jogo - funções de suporte de jogos gerais, como downloader imagem e loop do jogo.
- Render - funções de renderização auxiliar na tela.
Explicarei em detalhes os métodos common.js
apenas se eles se relacionarem com o jogo em si, e não forem apenas funções matemáticas ou DOM auxiliares. Felizmente, a partir do nome e do contexto, ficará claro o que os métodos devem fazer.Como de costume, o código fonte está na documentação final.
Loop de jogo simples
Antes de renderizar algo, precisamos de um loop de jogo. Se você leu algum dos meus artigos anteriores sobre jogos ( pong , breakout , tetris , cobras ou boulderdash ), então você já viu exemplos do meu ciclo de jogo favorito com um tempo fixo .Não vou me aprofundar nos detalhes e simplesmente reutilizar parte do código dos jogos anteriores para criar um loop de jogo com uma etapa de tempo fixo usando requestAnimationFrame .O princípio é que cada um dos meus quatro exemplos pode chamar Game.run(...)
e usar suas próprias versõesupdate
- Atualizando o mundo do jogo com uma etapa de tempo fixo.render
- Atualizando o mundo do jogo quando o navegador permitir.
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();
});
}
Novamente, este é um remake de idéias dos meus jogos de tela anteriores. Portanto, se você não entender como o loop do jogo funciona, volte para um dos artigos anteriores.Imagens e sprites
Antes do início do ciclo do jogo, carregamos duas planilhas separadas (planilhas):- plano de fundo - três camadas de paralaxe para o céu, colinas e árvores
- sprites - sprites de máquinas (mais árvores e outdoors a serem adicionados à versão final)
A folha de sprite foi gerada usando uma pequena tarefa Rake and Ruby Gem sprite-factory .Essa tarefa gera as folhas de sprite combinadas, bem como as coordenadas x, y, w, h, que serão armazenadas nas constantes BACKGROUND
e SPRITES
.Nota: Criei os fundos usando o Inkscape, e a maioria dos sprites são gráficos retirados da versão antiga do Outrun para Genesis e usados como exemplos de treinamento.
Variáveis do jogo
Além das imagens de planos de fundo e sprites, precisaremos de várias variáveis de jogo, 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;
Alguns deles podem ser personalizados usando os controles da interface do usuário para alterar valores críticos durante a execução do programa, para que você possa ver como eles afetam a renderização da estrada. Outros são recalculados dos valores da interface do usuário personalizados no método reset()
.Nós gerenciamos Ferrari
Realizamos combinações de teclas Game.run
, o que fornece uma entrada simples do teclado que define ou redefine variáveis que relatam as ações atuais do jogador: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; } }
],
...
}
O estado do jogador é controlado pelas seguintes variáveis:- velocidade - velocidade atual.
- position - a posição Z atual na pista. Note que esta é uma posição de câmera, não uma Ferrari.
- playerX - posição atual do jogador em X na estrada. Normalizado no intervalo de -1 a +1, para não depender do valor real
roadWidth
.
Essas variáveis são definidas dentro do método update
, que executa as seguintes ações:- atualizações
position
baseadas no atual speed
. - atualizações
playerX
quando você pressiona a tecla esquerda ou direita. - aumenta
speed
se a tecla para cima for pressionada. - diminui
speed
se a tecla para baixo for pressionada. - reduz
speed
se as teclas para cima e para baixo não forem pressionadas. - reduz
speed
se playerX
localizado fora da beira da estrada e na grama.
No caso de estradas diretas, o método é update
bastante claro e simples: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);
}
Não se preocupe, isso se tornará muito mais difícil quando, na versão final, adicionarmos sprites e reconhecimento de colisão.Geometria da estrada
Antes de nós pode tornar o mundo do jogo, precisamos construir uma matriz de segments
no no método resetRoad()
.Cada um desses segmentos da estrada será finalmente projetado a partir de suas coordenadas mundiais, para que se transforme em um polígono 2D nas coordenadas da tela. Para cada segmento, armazenamos dois pontos, p1 é o centro da borda mais próxima da câmera e p2 é o centro da borda mais distante da câmera.A rigor, p2 de cada segmento é idêntico ao p1 do segmento anterior, mas parece-me que é mais fácil armazená-los como pontos separados e converter cada segmento separadamente.Nós nos mantemos separados rumbleLength
porque podemos ter belas curvas e colinas detalhadas, mas ao mesmo tempo listras horizontais. Se cada segmento subsequente tiver uma cor diferente, isso criará um efeito estroboscópico ruim. Portanto, queremos ter muitos segmentos pequenos, mas agrupe-os para formar faixas horizontais 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 e p2 apenas com as coordenadas mundiais z , porque precisamos apenas de estradas retas. As coordenadas y sempre serão 0 e as coordenadas x sempre dependerão do valor escalado +/- roadWidth
. Mais tarde, quando adicionarmos curvas e colinas, essa parte mudará.Também definiremos objetos vazios para armazenar representações desses pontos na câmera e na tela para não criar um monte de objetos temporários em cada um render
. Para minimizar a coleta de lixo, devemos evitar alocar objetos dentro do loop do jogo.Quando o carro chega ao fim da estrada, simplesmente retornamos ao início do ciclo. Para simplificar isso, criaremos um método para encontrar um segmento para qualquer valor de Z, mesmo que ultrapasse o comprimento da estrada:function findSegment(z) {
return segments[Math.floor(z/segmentLength) % segments.length];
}
Renderização em segundo plano
O método render()
começa renderizando a imagem de plano de fundo. Nas partes a seguir, onde adicionaremos curvas e colinas, precisaremos do plano de fundo para executar a rolagem de paralaxe; portanto, começaremos a avançar nessa direção, renderizando o plano de fundo em três camadas 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);
...
Renderização de estradas
Em seguida, a função render itera todos os segmentos e projetos p1 e p2 de cada segmento, desde coordenadas mundiais até coordenadas de tela, aparando o segmento, se necessário, e renderizando-o: 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;
}
Acima, já vimos os cálculos necessários para projetar um ponto; A versão javascript combina transformação, projeção e dimensionamento em um 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));
}
Além de calcular a tela x e y para cada ponto p1 e p2, usamos os mesmos cálculos de projeção para calcular a largura projetada ( w ) do segmento.Ter a coordenadas de tela x e y de pontos p1 e p2 , bem como a largura projetada da estrada w , podemos facilmente calcular com a ajuda de uma função auxiliar Render.segment
todos os polígonos necessários para a prestação de grama, estrada, listras horizontais e linhas de divisão, usando a função de auxiliar geral Render.polygon
(ver . common.js
) .Renderização de carro
Finalmente, a última coisa que o método precisa render
é uma renderização da Ferrari: Render.player(ctx, width, height, resolution, roadWidth, sprites, speed/maxSpeed,
cameraDepth/playerZ,
width/2,
height);
Esse método é chamado player
, e não car
, porque na versão final do jogo haverá outros carros na estrada, e queremos separar a Ferrari do jogador de outros carros.A função auxiliar Render.player
usa o método de tela chamado drawImage
para renderizar o sprite, tendo-o escalado anteriormente usando o mesmo dimensionamento de projeção usado anteriormente:d / z
Onde z , neste caso, é a distância relativa da máquina à câmera, armazenada na variável playerZ .Além disso, a função sacode o carro um pouco em alta velocidade, adicionando um pouco de aleatoriedade à equação da escala, dependendo da velocidade / velocidade máxima .E aqui está o que temos:Conclusão
Fizemos uma quantidade bastante grande de trabalho apenas para criar um sistema com estradas retas. Nós adicionamos- módulo auxiliar genérico dom
- Util general math module
- Renderizar módulo auxiliar de lona geral ...
- ... incluindo
Render.segment
, Render.polygon
eRender.sprite
- ciclo de jogo com passo fixo
- downloader de imagem
- manipulador de teclado
- fundo de paralaxe
- folha de sprite com carros, árvores e outdoors
- geometria rudimentar da estrada
- método
update()
para controlar a máquina - método
render()
para renderizar fundo, estrada e carro do jogador - Tag HTML5
<audio>
com música de corrida (bônus oculto!)
... o que nos deu uma boa base para um maior desenvolvimento.Parte 2. Curvas.
Nesta parte, explicaremos mais detalhadamente como as curvas funcionam.Na parte anterior, compilamos a geometria da estrada na forma de uma matriz de segmentos, cada um com coordenadas mundiais que são transformadas em relação à câmera e projetadas na tela.Precisávamos apenas da coordenada mundial z para cada ponto, porque nas estradas retas x e y eram iguais a zero.Se criarmos um sistema 3D totalmente funcional, poderemos implementar as curvas calculando as listras x e z dos polígonos mostrados acima. No entanto, esse tipo de geometria será um pouco difícil de calcular e, para isso, será necessário adicionar o estágio de rotação 3d às equações de projeção ...... se seguimos esse caminho, seria melhor usar o WebGL ou seus análogos, mas esse projeto não tem outras tarefas para o nosso projeto. Nós apenas queremos usar truques pseudo-tridimensionais da velha escola para simular curvas.Portanto, você provavelmente ficará surpreso ao saber que não calcularemos as coordenadas x dos segmentos da estrada ...Em vez disso, usaremos o conselho de Lu :"Para curvar a estrada, basta alterar a posição da linha central da forma da curva ... a partir da parte inferior da tela, a quantidade de deslocamento do centro da estrada para a esquerda ou para a direita aumenta gradualmente . "
No nosso caso, a linha central é o valor cameraX
passado para os cálculos da projeção. Isso significa que, quando executamos render()
cada segmento da estrada, você pode simular as curvas deslocando o valor cameraX
para um valor gradualmente crescente.Para saber quanto mudar, precisamos armazenar um valor em cada segmento curve
. Este valor indica quanto o segmento deve ser deslocado da linha central da câmera. Ela vai ser:- negativo para curvas à esquerda
- positivo para curvas virando à direita
- menos para curvas suaves
- mais para curvas nítidas
Os próprios valores são escolhidos arbitrariamente; por tentativa e erro, podemos encontrar bons valores nos quais as curvas parecem estar "corretas":var ROAD = {
LENGTH: { NONE: 0, SHORT: 25, MEDIUM: 50, LONG: 100 },
CURVE: { NONE: 0, EASY: 2, MEDIUM: 4, HARD: 6 }
};
Além de escolher bons valores para as curvas, precisamos evitar lacunas nas transições quando a linha se transformar em uma curva (ou vice-versa). Isso pode ser conseguido suavizando-se ao entrar e sair das curvas. Faremos isso aumentando gradualmente (ou diminuindo) o valor curve
de cada segmento usando as funções tradicionais de suavização até atingir o valor desejado: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); },
Ou seja, agora, considerando a função de adicionar um segmento à geometria ...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 criar um método para entrada suave, localização e saída suave de uma estrada 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));
}
... e no topo você pode impor geometria adicional, por exemplo, curvas em 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);
}
Alterações no método update ()
As únicas mudanças que precisam ser feitas no método update()
são a aplicação de um tipo de força centrífuga quando a máquina se move ao longo de uma curva.Definimos um fator arbitrário que pode ser ajustado de acordo com nossas preferências.var centrifugal = 0.3;
E então apenas atualizaremos a posição com playerX
base em sua velocidade atual, valor da curva e multiplicador de força centrífuga:playerX = playerX - (dx * speedPercent * playerSegment.curve * centrifugal);
Renderização de curva
Dissemos acima que é possível renderizar curvas simuladas alterando o valor cameraX
usado nos cálculos de projeção durante a execução de render()
cada segmento de estrada.Para isso, armazenaremos a variável de acionamento dx , aumentando para cada segmento por um valor curve
, bem como a variável x , que será usada como deslocamento do valor cameraX
usado nos cálculos de projeção.Para implementar as curvas, precisamos do seguinte:- desloque a projeção p1 de cada segmento por x
- desloque a projeção p2 de cada segmento por x + dx
- aumentar x para o próximo segmento em dx
Finalmente, para evitar transições rasgadas ao cruzar os limites dos segmentos, devemos fazer dx inicializado com o valor interpolado da curva dos segmentos base atuais.Altere o método da render()
seguinte maneira: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;
...
}
Fundo de rolagem de paralaxe
Por fim, precisamos rolar as camadas de fundo de paralaxe, armazenando o deslocamento de cada camada ...var skySpeed = 0.001;
var hillSpeed = 0.002;
var treeSpeed = 0.003;
var skyOffset = 0;
var hillOffset = 0;
var treeOffset = 0;
... e aumentá-lo durante o tempo, update()
dependendo do valor da curva do segmento de jogador atual e sua velocidade ...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);
... e use para usar esse deslocamento ao fazer render()
camadas de fundo.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);
Conclusão
Então, aqui temos as curvas pseudo-tridimensionais falsas:A parte principal do código que adicionamos é construir a geometria da estrada com o valor correspondente curve
. Percebendo isso, adicionar força centrífuga durante o tempo é update()
muito mais fácil.A renderização da curva é realizada em apenas algumas linhas de código, mas pode ser difícil entender (e descrever) o que exatamente está acontecendo aqui. Existem muitas maneiras de simular curvas e é muito fácil vagar quando elas são implementadas em um beco sem saída. É ainda mais fácil se deixar levar por uma tarefa externa e tentar fazer tudo "corretamente"; Antes que você perceba isso, você começará a criar um sistema 3D totalmente funcional com matrizes, rotações e geometria 3D real ... que, como eu disse, não é nossa tarefa.Quando escrevi este artigo, eu tinha certeza de que definitivamente havia problemas na minha implementação das curvas. Tentando visualizar o algoritmo, não entendi por que precisava de dois valores das unidades dx e x em vez de um ... e se não consigo explicar completamente algo, algo deu errado em algum lugar ...... mas o tempo do projeto "continua o fim de semana ” quase expirou e, francamente, as curvas me parecem bastante bonitas e, no final, é a mais importante.