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 pekanVersi 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 bermainTentang 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 menghitungy = 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 faktord / 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:- konversi dari koordinat dunia ke koordinat layar
- memproyeksikan koordinat kamera ke bidang proyeksi yang dinormalisasi
- 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.js
dalam 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.js
hanya 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 sendiriupdate
- 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,
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();
});
}
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 BACKGROUND
dan 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;
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;
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
position
berdasarkan saat ini speed
. - pembaruan
playerX
saat Anda menekan tombol kiri atau kanan. - meningkat
speed
jika tombol atas ditekan. - berkurang
speed
jika tombol bawah ditekan. - berkurang
speed
jika tombol atas dan bawah tidak ditekan. - Mengurangi
speed
jika playerX
terletak di tepi jalan dan di rumput.
Dalam kasus jalan langsung, metode ini update
cukup jelas dan sederhana: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);
}
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 segments
di 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 rumbleLength
karena 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++) {
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) ||
(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;
}
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.segment
semua 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 render
ini 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.player
menggunakan metode kanvas yang dipanggil drawImage
untuk 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.polygon
danRender.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 cameraX
diteruskan ke perhitungan proyeksi. Ini berarti bahwa ketika kami melakukan render()
setiap segmen jalan, Anda dapat mensimulasikan kurva dengan menggeser nilainya cameraX
dengan 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 },
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 curve
untuk 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;
Dan kemudian kita hanya akan memperbarui posisi playerX
berdasarkan 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 cameraX
digunakan 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 cameraX
digunakan 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;
var hillSpeed = 0.002;
var treeSpeed = 0.003;
var skyOffset = 0;
var hillOffset = 0;
var treeOffset = 0;
... 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.