Criando um jogo de corrida pseudo-tridimensional


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 semana



A 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 jogar


Sobre 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 calcular

y = 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 fator

d / 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:

  1. conversão de coordenadas mundiais em coordenadas de tela
  2. projetar as coordenadas da câmera em um plano de projeção normalizado
  3. 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.jsdos 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.jsapenas 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ões

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

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

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 positionbaseadas no atual speed.
  • atualizações playerXquando você pressiona a tecla esquerda ou direita.
  • aumenta speedse a tecla para cima for pressionada.
  • diminui speedse a tecla para baixo for pressionada.
  • reduz speedse as teclas para cima e para baixo não forem pressionadas.
  • reduz speedse playerXlocalizado fora da beira da estrada e na grama.

No caso de estradas diretas, o método é updatebastante claro e simples:

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

}

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 segmentsno 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 rumbleLengthporque 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++) { // 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 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) || // 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;
  }

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.segmenttodos 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.playerusa o método de tela chamado drawImagepara 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.polygoneRender.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 cameraXpassado 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 cameraXpara 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 }, // num segments
  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 curvede 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;   // centrifugal force multiplier when going around curves

E então apenas atualizaremos a posição com playerXbase 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 cameraXusado 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 cameraXusado 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; // 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

... 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.

All Articles