Membuat game balap pseudo-tiga dimensi


Sebagai seorang anak, saya jarang pergi ke ruang-ruang arcade arcade karena saya tidak benar-benar membutuhkannya, karena saya punya game yang luar biasa untuk C64 di rumah ... tetapi ada tiga game arcade yang selalu saya punya uang - Donkey Kong, Dragon Lair dan Outrun ...

... dan saya sangat suka berlari lebih cepat - kecepatan, bukit, pohon-pohon palem dan musik, bahkan pada versi yang lemah untuk C64.


Jadi saya memutuskan untuk mencoba menulis game balap pseudo-tiga dimensi sekolah tua dengan gaya Outrun, Pitstop atau Pole. Saya tidak berencana untuk membuat permainan yang lengkap dan lengkap , tetapi bagi saya tampaknya akan menarik untuk memeriksa kembali mekanisme yang dengannya permainan ini mewujudkan trik mereka. Kurva, bukit, sprite, dan kecepatan ...

Jadi, inilah "proyek akhir pekan" saya, yang pada akhirnya memakan waktu lima atau enam minggu di akhir pekan



Versi yang dapat diputar lebih seperti demo teknis daripada game nyata. Bahkan, jika Anda ingin membuat balapan pseudo-tiga-dimensi yang nyata, maka ini akan menjadi fondasi paling minimal yang Anda butuhkan untuk secara bertahap berubah menjadi permainan.

Ini tidak dipoles, sedikit jelek, tetapi berfungsi penuh. Saya akan menunjukkan kepada Anda bagaimana menerapkannya sendiri dalam empat langkah sederhana.

Anda juga bisa bermain


Tentang kinerja


Kinerja game ini sangat tergantung pada mesin / browser. Di browser modern, ini berfungsi dengan baik, terutama pada mereka yang memiliki akselerasi GPU kanvas, tetapi driver grafis yang buruk dapat menyebabkannya membeku. Dalam gim, Anda dapat mengubah resolusi rendering dan jarak rendering.

Tentang struktur kode


Kebetulan proyek tersebut diimplementasikan dalam Javascript (karena kesederhanaan prototyping), tetapi tidak dimaksudkan untuk menunjukkan teknik atau teknik yang direkomendasikan dari Javascript. Bahkan, untuk memudahkan pemahaman, Javascript dari setiap contoh disematkan langsung di halaman HTML (horor!); lebih buruk lagi, ia menggunakan variabel dan fungsi global.

Jika saya membuat game nyata, kodenya akan jauh lebih terstruktur dan efisien, tetapi karena ini adalah demo teknis dari game balap, saya memutuskan untuk tetap menggunakan KISS .

Bagian 1. Jalan lurus.


Jadi, bagaimana kita memulai membuat game balap pseudo-tiga dimensi?

Ya, kita perlu

  • Ulangi trigonometri
  • Ingat kembali dasar-dasar proyeksi 3d
  • Buat lingkaran game
  • Unduh gambar sprite
  • Bangun geometri jalan
  • Jadikan latar belakang
  • Berikan jalan
  • Render mobil
  • Terapkan dukungan keyboard untuk kontrol alat berat

Tapi sebelum kita mulai, mari kita baca Lou's Pseudo 3d Page [ terjemahan Habré] - satu-satunya sumber informasi (yang bisa saya temukan) tentang cara membuat game balap psevdotrohmernuyu.

Selesai membaca artikel Lou? Baik! Kami akan membuat variasi teknik Realistic Hills Using 3d-Projected Segments. Kami akan melakukan ini secara bertahap selama empat bagian berikutnya. Tetapi kita akan mulai sekarang, dengan versi v1, dan membuat geometri jalan lurus yang sangat sederhana dengan memproyeksikannya ke elemen kanvas HTML5.

Demo dapat dilihat di sini .

Sedikit trigonometri


Sebelum kita masuk ke implementasi, mari kita gunakan dasar-dasar trigonometri untuk mengingat bagaimana memproyeksikan titik di dunia 3D ke layar 2D.

Dalam kasus paling sederhana, jika Anda tidak menyentuh vektor dan matriks, hukum segitiga serupa digunakan untuk proyeksi 3D .

Kami menggunakan notasi berikut:

  • h = tinggi kamera
  • d = jarak dari kamera ke layar
  • z = jarak dari kamera ke mobil
  • y = layar y koordinat

Lalu kita bisa menggunakan hukum segitiga yang sama untuk menghitung

y = h * d / z

seperti yang ditunjukkan pada diagram:


Anda juga bisa menggambar diagram serupa di tampilan atas, bukan tampilan samping, dan mendapatkan persamaan yang sama untuk menghitung koordinat X layar:

x = w * d / z

Di mana w = setengah lebar jalan (dari kamera ke tepi jalan).

Seperti yang Anda lihat, untuk x , dan y, kami mengukur berdasarkan faktor

d / z

Sistem koordinat


Dalam bentuk diagram, itu terlihat indah dan sederhana, tetapi ketika Anda memulai pengkodean, Anda bisa sedikit bingung, karena kami memilih nama yang berubah-ubah, dan tidak jelas dengan apa yang kami tetapkan koordinat dunia 3D, dan apa koordinat layar 2D itu. Kami juga berasumsi bahwa kamera berada di pusat asal usul dunia, meskipun pada kenyataannya ia akan mengikuti mesin.

Jika Anda melakukan pendekatan secara lebih formal, maka kami perlu melakukan:

  1. konversi dari koordinat dunia ke koordinat layar
  2. memproyeksikan koordinat kamera ke bidang proyeksi yang dinormalisasi
  3. scaling koordinat yang diproyeksikan ke koordinat layar fisik (dalam kasus kami, ini kanvas)


Catatan: dalam sistem-3d saat ini , tahap rotasi dilakukan antara tahap 1 dan 2 , tetapi karena kita akan mensimulasikan kurva, kita tidak perlu rotasi.

Proyeksi


Persamaan proyeksi formal dapat direpresentasikan sebagai berikut:


  • Titik Persamaan Konversi ( terjemahkan ) dihitung relatif terhadap bilik
  • Persamaan proyeksi ( proyek ) adalah variasi dari "hukum segitiga serupa" yang ditunjukkan di atas.
  • Persamaan Penskalaan ( skala ) memperhitungkan perbedaan antara:
    • matematika , di mana 0,0 berada di tengah dan sumbu y naik, dan
    • , 0,0 , y :


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



Bagian terakhir dari teka-teki akan menjadi cara untuk menghitung d - jarak dari kamera ke bidang proyeksi.

Alih-alih hanya menulis nilai hard-set dari d , akan lebih berguna untuk menghitungnya dari bidang tampilan vertikal yang diinginkan. Berkat ini, kami akan dapat "memperbesar" kamera jika perlu.

Jika kita mengasumsikan bahwa kita memproyeksikan ke bidang proyeksi yang dinormalisasi, koordinatnya berada dalam kisaran dari -1 hingga +1, maka d dapat dihitung sebagai berikut:

d = 1 / tan (fov / 2)

Dengan mendefinisikan fov sebagai satu (dari banyak) variabel, kita dapat menyesuaikan ruang lingkup untuk menyempurnakan algoritma rendering.

Struktur Kode Javascript


Pada awal artikel, saya sudah mengatakan bahwa kode tersebut tidak cukup sesuai dengan pedoman untuk menulis Javascript - ini adalah demo "cepat dan kotor" dengan variabel dan fungsi global yang sederhana. Namun, karena saya akan membuat empat versi terpisah (lurus, kurva, bukit dan sprite), saya akan menyimpan beberapa metode yang dapat digunakan kembali di common.jsdalam modul berikut:

  • Dom adalah beberapa fungsi pembantu DOM kecil.
  • Util - utilitas umum, terutama fungsi matematika bantu.
  • Game - fungsi dukungan game umum, seperti pengunduh gambar dan loop game.
  • Render - fungsi rendering pembantu di atas kanvas.

Saya akan menjelaskan secara terperinci metode dari common.jshanya jika mereka berhubungan dengan permainan itu sendiri, dan bukan hanya fungsi matematika atau DOM tambahan. Semoga dari nama dan konteksnya akan jelas apa metode yang harus dilakukan.

Seperti biasa, kode sumber ada di dokumentasi akhir.

Loop game sederhana


Sebelum merender sesuatu, kita perlu loop game. Jika Anda membaca salah satu artikel saya sebelumnya tentang permainan ( pong , breakout , tetris , ular atau boulderdash ), maka Anda telah melihat contoh siklus permainan favorit saya dengan langkah waktu yang tetap .

Saya tidak akan masuk jauh ke detail, dan hanya menggunakan kembali bagian dari kode dari game sebelumnya untuk membuat loop game dengan langkah waktu yang tetap menggunakan requestAnimationFrame .

Prinsipnya adalah bahwa masing-masing dari empat contoh saya dapat memanggil Game.run(...)dan menggunakan versinya sendiri

  • update - Memperbarui dunia game dengan langkah waktu yang tetap.
  • render - Memperbarui dunia game saat browser memungkinkan.

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

Sekali lagi, ini adalah pembuatan ulang ide dari game kanvas saya sebelumnya, jadi jika Anda tidak mengerti cara kerja loop game, maka kembalilah ke salah satu artikel sebelumnya.

Gambar dan sprite


Sebelum siklus permainan dimulai, kami memuat dua spritesheet (sprite sheet) yang terpisah:

  • background - tiga lapisan paralaks untuk langit, bukit, dan pohon
  • sprite - sprite mesin (ditambah pohon dan papan iklan yang akan ditambahkan ke versi final)


Lembar sprite dibuat menggunakan tugas kecil Rake dan Ruby Gem sprite-factory .

Tugas ini menghasilkan lembar sprite gabungan, serta koordinat x, y, w, h, yang akan disimpan dalam konstanta BACKGROUNDdan SPRITES.

Catatan: Saya membuat latar belakang menggunakan Inkscape, dan kebanyakan sprite adalah grafik yang diambil dari versi Outrun lama untuk Genesis dan digunakan sebagai contoh pelatihan.

Variabel game


Selain gambar latar dan sprite, kami membutuhkan beberapa variabel game, yaitu:

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)

Beberapa di antaranya dapat dikustomisasi menggunakan kontrol UI untuk mengubah nilai kritis selama eksekusi program sehingga Anda dapat melihat bagaimana pengaruhnya terhadap rendering jalan. Lainnya dihitung ulang dari nilai UI khusus dalam metode ini reset().

Kami mengelola Ferrari


Kami melakukan binding kunci untuk Game.run, yang menyediakan input keyboard sederhana yang mengatur atau me-reset variabel yang melaporkan tindakan pemain saat ini:

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

Status pemain dikendalikan oleh variabel-variabel berikut:

  • kecepatan - kecepatan saat ini.
  • position - posisi Z saat ini di trek. Perhatikan bahwa ini adalah posisi kamera, bukan Ferrari.
  • playerX - posisi pemain saat ini di X di jalan. Dinormalisasi dalam kisaran dari -1 hingga +1, agar tidak bergantung pada nilai aktual roadWidth.

Variabel-variabel ini diatur di dalam metode update, yang melakukan tindakan berikut:

  • pembaruan positionberdasarkan saat ini speed.
  • pembaruan playerXsaat Anda menekan tombol kiri atau kanan.
  • meningkat speedjika tombol atas ditekan.
  • berkurang speedjika tombol bawah ditekan.
  • berkurang speedjika tombol atas dan bawah tidak ditekan.
  • Mengurangi speedjika playerXterletak di tepi jalan dan di rumput.

Dalam kasus jalan langsung, metode ini updatecukup jelas dan sederhana:

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

}

Jangan khawatir, itu akan menjadi jauh lebih sulit ketika dalam versi selesai kami menambahkan sprite dan pengenalan tabrakan.

Geometri jalan


Sebelum kita dapat membuat dunia game, kita perlu membangun sebuah array segmentsdi dalam metode resetRoad().

Masing-masing segmen jalan ini pada akhirnya akan diproyeksikan dari koordinat dunianya sehingga berubah menjadi poligon 2d dalam koordinat layar. Untuk setiap segmen, kami menyimpan dua titik, p1 adalah pusat tepi paling dekat dengan kamera, dan p2 adalah pusat tepi terjauh dari kamera.


Sebenarnya, p2 dari setiap segmen identik dengan p1 dari segmen sebelumnya, tetapi bagi saya lebih mudah untuk menyimpannya sebagai titik yang terpisah dan mengonversi setiap segmen secara terpisah.

Kami tetap terpisah rumbleLengthkarena kami dapat memiliki kurva rinci dan indah, tetapi pada saat yang sama garis-garis horizontal. Jika setiap segmen berikutnya memiliki warna yang berbeda, maka ini akan menciptakan efek strobo yang buruk. Oleh karena itu, kami ingin memiliki banyak segmen kecil, tetapi kelompokkan bersama untuk membentuk garis horizontal terpisah.

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

Kami menginisialisasi p1 dan p2 hanya dengan koordinat dunia z , karena kami hanya perlu jalan lurus. Koordinat y akan selalu 0, dan koordinat x akan selalu bergantung pada nilai yang diskalakan +/- roadWidth. Nanti, ketika kita menambahkan kurva dan bukit, bagian ini akan berubah.

Kami juga akan mengatur objek kosong untuk menyimpan representasi titik-titik ini di kamera dan di layar agar tidak membuat banyak objek sementara di masing-masing render. Untuk meminimalkan pengumpulan sampah, kita harus menghindari mengalokasikan objek dalam loop game.

Ketika mobil mencapai ujung jalan, kita cukup kembali ke awal siklus. Untuk menyederhanakan ini, kami akan membuat metode untuk menemukan segmen untuk nilai Z apa pun, bahkan jika melampaui panjang jalan:

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

Render latar belakang


Metode render()dimulai dengan merender gambar latar belakang. Di bagian berikut, di mana kita akan menambahkan kurva dan bukit, kita akan membutuhkan latar belakang untuk melakukan pengguliran paralaks, jadi sekarang kita akan mulai bergerak ke arah ini, menjadikan latar belakang sebagai tiga lapisan terpisah:

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

  ...

Render jalan


Kemudian, fungsi render berulang melalui semua segmen dan memproyeksikan p1 dan p2 masing - masing segmen dari koordinat dunia ke koordinat layar, memangkas segmen jika perlu, dan merendernya:

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

Di atas, kita telah melihat perhitungan yang diperlukan untuk memproyeksikan suatu poin; Versi javascript menggabungkan transformasi, proyeksi dan penskalaan menjadi satu metode:

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

Selain menghitung layar x dan y untuk setiap titik p1 dan p2, kami menggunakan perhitungan proyeksi yang sama untuk menghitung proyeksi lebar ( w ) segmen.

Memiliki layar x dan y koordinat titik p1 dan p2 , serta lebar jalan yang diproyeksikan w , kita dapat dengan mudah menghitung dengan bantuan fungsi bantu Render.segmentsemua poligon yang diperlukan untuk rendering rumput, jalan, garis horizontal dan garis pemisah, menggunakan fungsi bantu umum Render.polygon (lihatcommon.js fungsi tambahan umum) (lihat . ) .

Render mobil


Akhirnya, hal terakhir yang dibutuhkan metode renderini adalah rendering Ferrari:

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

Metode ini disebut player, dan bukan car, karena dalam versi final permainan akan ada mobil lain di jalan, dan kami ingin memisahkan pemain Ferrari dari mobil lain.

Fungsi helper Render.playermenggunakan metode kanvas yang dipanggil drawImageuntuk membuat sprite, setelah sebelumnya menskalanya menggunakan skala proyeksi yang sama yang digunakan sebelumnya:

d / z

Di mana z dalam hal ini adalah jarak relatif dari mesin ke kamera, disimpan dalam pemutar variabelZ .

Selain itu, fungsi "mengguncang" mobil sedikit pada kecepatan tinggi, menambahkan sedikit keacakan ke persamaan penskalaan, tergantung pada kecepatan / kecepatan maksimum .

Dan inilah yang kami dapatkan:


Kesimpulan


Kami melakukan pekerjaan yang cukup besar hanya untuk menciptakan sistem dengan jalan lurus. Kami menambahkan

  • generik pembantu modul dom
  • Gunakan modul matematika umum
  • Membuat modul pembantu umum ...
  • ... termasuk Render.segment, Render.polygondanRender.sprite
  • siklus permainan pitch tetap
  • pengunduh gambar
  • pengendali keyboard
  • latar belakang paralaks
  • sprite sheet dengan mobil, pohon, dan papan iklan
  • geometri dasar belum sempurna
  • metode update()untuk mengendalikan mesin
  • metode render()untuk menampilkan latar belakang, jalan, dan mobil pemain
  • Tag HTML5 <audio>dengan musik balap (bonus tersembunyi!)

... yang memberi kami dasar yang baik untuk pengembangan lebih lanjut.

Bagian 2. Kurva.



Pada bagian ini, kami akan menjelaskan secara lebih rinci bagaimana kurva bekerja.

Pada bagian sebelumnya, kami menyusun geometri jalan dalam bentuk array segmen, yang masing-masing memiliki koordinat dunia yang ditransformasikan relatif ke kamera dan kemudian diproyeksikan ke layar.

Kami hanya membutuhkan koordinat dunia z untuk setiap titik, karena pada jalan lurus x dan y sama dengan nol.


Jika kita membuat sistem 3d yang berfungsi penuh, kita bisa menerapkan kurva dengan menghitung garis x dan z dari poligon yang ditunjukkan di atas. Namun, jenis geometri ini akan agak sulit untuk dihitung, dan untuk ini perlu menambahkan tahap rotasi-3d ke persamaan proyeksi ...

... jika kita menggunakan cara ini, akan lebih baik menggunakan WebGL atau analognya, tetapi proyek ini tidak memiliki tugas lain untuk proyek kita. Kami hanya ingin menggunakan trik pseudo-tiga dimensi jadul untuk mensimulasikan kurva.

Oleh karena itu, Anda mungkin akan terkejut mengetahui bahwa kami tidak akan menghitung koordinat x segmen jalan sama sekali ...

Sebaliknya, kami akan menggunakan saran Lu :

"Untuk melengkung jalan, cukup ubah posisi garis tengah bentuk kurva ... mulai dari bagian bawah layar, jumlah pergeseran pusat jalan ke kiri atau ke kanan secara bertahap meningkat . "

Dalam kasus kami, garis tengah adalah nilai yang cameraXditeruskan ke perhitungan proyeksi. Ini berarti bahwa ketika kami melakukan render()setiap segmen jalan, Anda dapat mensimulasikan kurva dengan menggeser nilainya cameraXdengan nilai yang meningkat secara bertahap.


Untuk mengetahui berapa banyak yang harus diubah, kita perlu menyimpan nilai di setiap segmen curve. Nilai ini menunjukkan seberapa banyak segmen harus digeser dari garis tengah kamera. Dia akan:

  • negatif untuk kurva belok kiri
  • positif untuk belokan ke kanan
  • kurang untuk kurva halus
  • lebih banyak untuk kurva tajam

Nilai-nilai itu sendiri dipilih secara sewenang-wenang; melalui coba-coba, kita dapat menemukan nilai bagus di mana kurva tampaknya “benar”:

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

Selain memilih nilai yang baik untuk kurva, kita perlu menghindari celah dalam transisi ketika garis berubah menjadi kurva (atau sebaliknya). Ini dapat dicapai dengan melunakkan saat memasuki dan keluar dari kurva. Kami akan melakukan ini dengan secara bertahap meningkatkan (atau mengurangi) nilai curveuntuk setiap segmen menggunakan fungsi smoothing tradisional hingga mencapai nilai yang diinginkan:

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

Artinya, sekarang, dengan mempertimbangkan fungsi menambahkan satu segmen ke geometri ...

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

kita dapat membuat metode untuk entri halus, menemukan dan keluar mulus dari jalan melengkung:

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

... dan di atas Anda dapat menerapkan geometri tambahan, misalnya, kurva berbentuk 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);
}

Perubahan ke metode pembaruan ()


Satu-satunya perubahan yang perlu dilakukan untuk metode update()ini adalah penerapan semacam gaya sentrifugal ketika mesin bergerak sepanjang kurva.

Kami menetapkan faktor sewenang-wenang yang dapat disesuaikan menurut preferensi kami.

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

Dan kemudian kita hanya akan memperbarui posisi playerXberdasarkan kecepatannya saat ini, nilai kurva dan pengganda gaya sentrifugal:

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

Render kurva


Kami mengatakan di atas bahwa Anda dapat membuat kurva simulasi dengan menggeser nilai yang cameraXdigunakan dalam perhitungan proyeksi selama pelaksanaan render()setiap ruas jalan.


Untuk melakukan ini, kami akan menyimpan variabel drive dx , meningkat untuk setiap segmen dengan suatu nilai curve, serta variabel x , yang akan digunakan sebagai offset dari nilai yang cameraXdigunakan dalam perhitungan proyeksi.

Untuk menerapkan kurva, kita perlu yang berikut:

  • menggeser p1 proyeksi setiap segmen dengan x
  • menggeser p2 proyeksi masing - masing segmen dengan x + dx
  • tingkatkan x untuk segmen berikutnya dengan dx

Akhirnya, untuk menghindari transisi yang sobek ketika melintasi batas segmen, kita harus membuat dx diinisialisasi dengan nilai interpolasi dari kurva segmen dasar saat ini.

Ubah metode render()sebagai berikut:

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;

  ...
}

Latar Belakang Menggulir Parallax


Akhirnya, kita perlu menggulung lapisan latar belakang paralaks, menyimpan offset untuk setiap lapisan ...

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

... dan meningkatkannya selama waktu update()tergantung pada nilai kurva dari segmen pemain saat ini dan kecepatannya ...

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

... dan kemudian gunakan untuk menggunakan offset ini saat melakukan render()lapisan latar belakang.

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

Kesimpulan


Jadi, di sini kita dapatkan kurva pseudo-tiga dimensi palsu:


Bagian utama dari kode yang kami tambahkan adalah untuk membangun geometri jalan dengan nilai yang sesuai curve. Menyadari itu, menambahkan gaya sentrifugal selama ini update()jauh lebih mudah.

Render kurva dilakukan hanya dalam beberapa baris kode, tetapi mungkin sulit untuk memahami (dan menjelaskan) apa sebenarnya yang terjadi di sini. Ada banyak cara untuk mensimulasikan kurva dan sangat mudah untuk berkeliaran ketika mereka diterapkan ke jalan buntu. Bahkan lebih mudah terbawa dengan tugas luar dan mencoba melakukan segala sesuatu "dengan benar"; sebelum Anda menyadarinya, Anda akan mulai membuat sistem 3d yang berfungsi penuh dengan matriks, rotasi, dan 3d-geometri nyata ... yang, seperti yang saya katakan, bukan tugas kami.

Ketika saya menulis artikel ini, saya yakin pasti ada masalah dalam implementasi kurva saya. Mencoba memvisualisasikan algoritme, saya tidak mengerti mengapa saya membutuhkan dua nilai drive dx dan x daripada satu ... dan jika saya tidak dapat sepenuhnya menjelaskan sesuatu, maka ada sesuatu yang salah di suatu tempat ...

... tetapi waktu proyek "menyala" akhir pekan ” hampir kedaluwarsa, dan, sejujurnya, kurva menurut saya cukup indah, dan pada akhirnya, ini yang paling penting.

All Articles