丁香五月天婷婷久久婷婷色综合91|国产传媒自偷自拍|久久影院亚洲精品|国产欧美VA天堂国产美女自慰视屏|免费黄色av网站|婷婷丁香五月激情四射|日韩AV一区二区中文字幕在线观看|亚洲欧美日本性爱|日日噜噜噜夜夜噜噜噜|中文Av日韩一区二区

您正在使用IE低版瀏覽器,為了您的雷峰網(wǎng)賬號安全和更好的產(chǎn)品體驗(yàn),強(qiáng)烈建議使用更快更安全的瀏覽器
此為臨時(shí)鏈接,僅用于文章預(yù)覽,將在時(shí)失效
人工智能開發(fā)者 正文
發(fā)私信給陳伊莉
發(fā)送

0

如何用不到 2KB 的 JavaScript 代碼寫一個(gè) 3D 賽車游戲?

本文作者: 陳伊莉 2020-05-05 10:31
導(dǎo)語:2KB 到底有多小呢?提供一個(gè)參考,一個(gè)3.5英寸軟盤可以容納700多這樣的游戲。

 幾個(gè)月前,JS1k游戲制作節(jié)(JS1K game jam)傳出不再舉辦消息后,許多游戲迷開始哀嚎。

       如何用不到 2KB 的 JavaScript 代碼寫一個(gè) 3D 賽車游戲?      

Frank Force 也是其中一位,但他還有另一層身份——一位德克薩斯州奧斯汀的獨(dú)立游戲設(shè)計(jì)師。Frank Force 在游戲行業(yè)工作了20年,參與過9款主流游戲、47個(gè)獨(dú)立游戲的設(shè)計(jì)。在聽到這個(gè)消息后,他馬上和其他開發(fā)朋友討論了這個(gè)問題,并決定做點(diǎn)什么為此紀(jì)念。

在此期間,他們受到三重因素的啟發(fā)。一是賽車游戲,包括懷舊向的80年代賽車游戲,他們在非常早期的硬件上推動實(shí)時(shí) 3D 圖形,所以作者沿用了相同的技術(shù),用純 JavaScript 從頭開始實(shí)現(xiàn)做 3D 圖形和物理引擎;還有一些現(xiàn)代賽車游戲帶來了視覺設(shè)計(jì)的靈感,比如《Distance》和《Lonely Mountains: Downhill》;二是之前 Jake Gordon 用 JavaScript 創(chuàng)建一個(gè)虛擬3D賽車的項(xiàng)目,并分享了代碼;三是 Chris Glover 曾經(jīng)做過一款小到只有 1KB 的 JS1k 賽車游戲《Moto1kross by Chris Glover》。

于是 Frank 和他的朋友們決定做一個(gè)壓縮后只有 2KB 的 3D 賽車游戲。2KB 到底有多小呢?提供一個(gè)參考,一個(gè)3.5英寸軟盤可以容納700多個(gè)這樣的游戲。

如何用不到 2KB 的 JavaScript 代碼寫一個(gè) 3D 賽車游戲?

他給這個(gè)游戲取名 Hue Jumper。關(guān)于名字的由來,F(xiàn)rank 表示,游戲的核心操作是移動。當(dāng)玩家通過一個(gè)關(guān)卡時(shí),游戲世界就會換一個(gè)顏色色調(diào)?!霸谖蚁胂笾校客ㄟ^過一個(gè)關(guān)卡,玩家都會跳轉(zhuǎn)到另一個(gè)維度,有著完全不同的色調(diào)?!?/p>

做完這個(gè)游戲后,F(xiàn)rank 將包含了游戲的全部 JavaScript 代碼都發(fā)布在他的個(gè)人博客上,其中用到的軟件主要也是免費(fèi)或開源軟件的。游戲代碼發(fā)布在 CodePen,可以在 iframe 中試玩,有興趣的朋友可以去看看。

如何用不到 2KB 的 JavaScript 代碼寫一個(gè) 3D 賽車游戲?

以下是原博內(nèi)容,AI源創(chuàng)評論進(jìn)行了不改變原意的編譯:

確定最高目標(biāo)

因?yàn)閲?yán)格的大小限制,我需要非常仔細(xì)對待我的程序。我的總體策略是盡可能保持一切簡單,為最終目標(biāo)服務(wù)。

為了幫助壓縮代碼,我使用了 Google Closure Compiler,它刪除了所有空格,將變量重命名為1個(gè)字母字符,并進(jìn)行了一些輕量級優(yōu)化。

用戶可以通過 Google Closure Compiler 官網(wǎng)在線跑代碼。不幸的是,Closure Compiler 做了一些沒有幫助的事情,比如替換模板字符串、默認(rèn)參數(shù)和其他幫助節(jié)省空間的ES6特性。所以我需要手動撤銷其中一些事情,并執(zhí)行一些更“危險(xiǎn)”的壓縮技術(shù)來擠出最后一個(gè)字節(jié)空間。在壓縮方面,這不算很成功,大部分?jǐn)D出的空間來自代碼本身的結(jié)構(gòu)優(yōu)化。

代碼需要壓縮到2KB。如果不是非要這么做不可,有一個(gè)類似的但功能沒那么強(qiáng)的工具叫做 RegPack 。

無論哪種方式,策略都是一樣的:盡最大可能重復(fù)代碼,然后用壓縮工具壓縮。最好的例子是 c.width,c.height和 Math。因此,在閱讀這段代碼時(shí),請記住,你經(jīng)常會看到我不斷重復(fù)一些東西,最終目的就是為了壓縮。

HTML

其實(shí)我的游戲很少使用 html ,因?yàn)樗饕玫降氖?JavaScript 。但這是創(chuàng)建全屏畫布 Canvas ,也能將畫布 Canvas 設(shè)為窗口內(nèi)部大小的代碼最小方法。我不知道為什么在 CodePen 上有必要添加 overflow:hiddento the body,當(dāng)直接打開時(shí)按理說也可以運(yùn)行。

我將 JavaScript 封裝在一個(gè) onload 調(diào)用,得到了一個(gè)更小的最終版本…< body style = margin:0 onload = " code _ goes _ here " > < canvas id = c >但是,在開發(fā)過程中,我不喜歡用這個(gè)壓縮設(shè)置,因?yàn)榇a存儲在一個(gè)字符串中,所以編輯器不能正確地高亮顯示語法。

<body style=margin:0>

<canvas id=c>

<script>

常量

有許多常量在各方面控制著游戲。當(dāng)代碼被 Google Closure 這樣的工具縮小時(shí),這些常量將被替換,就像 C++ 中的 #define 一樣,把它們放在第一位會加快游戲微調(diào)的過程。

// draw settings

const context = c.getContext`2d`; // canvas context

const drawDistance = 800;         // how far ahead to draw

const cameraDepth = 1;            // FOV of camera

const segmentLength = 100;        // length of each road segment

const roadWidth = 500;            // how wide is road

const curbWidth = 150;            // with of warning track

const dashLineWidth = 9;          // width of the dashed line

const maxPlayerX = 2e3;           // limit player offset

const mountainCount = 30;         // how many mountains are there

const timeDelta = 1/60;           // inverse frame rate

const PI = Math.PI;               // shorthand for Math.PI

 

// player settings

const height = 150;               // high of player above ground

const maxSpeed = 300;             // limit max player speed

const playerAccel = 1;            // player forward acceleration

const playerBrake = -3;           // player breaking acceleration

const turnControl = .2;           // player turning rate

const jumpAccel = 25;             // z speed added for jump

const springConstant = .01;       // spring players pitch

const collisionSlow = .1;         // slow down from collisions

const pitchLerp = .1;             // rate camera pitch changes

const pitchSpringDamp = .9;       // dampen the pitch spring

const elasticity = 1.2;           // bounce elasticity

const centrifugal = .002;         // how much turns pull player

const forwardDamp = .999;         // dampen player z speed

const lateralDamp = .7;           // dampen player x speed

const offRoadDamp = .98;          // more damping when off road

const gravity = -1;               // gravity to apply in y axis

const cameraTurnScale = 2;        // how much to rotate camera

const worldRotateScale = .00005;  // how much to rotate world

   

// level settings

const maxTime = 20;               // time to start

const checkPointTime = 10;        // add time at checkpoints

const checkPointDistance = 1e5;   // how far between checkpoints

const maxDifficultySegment = 9e3; // how far until max difficulty

const roadEnd = 1e4;              // how far until end of road

鼠標(biāo)控制

鼠標(biāo)是唯一的輸入系統(tǒng)。通過這段代碼,我們可以跟蹤鼠標(biāo)點(diǎn)擊和光標(biāo)位置,位置顯示為-1到1之間的值。

雙擊是通過 mouseUpFrames 實(shí)現(xiàn)的。mousePressed 變量只在玩家第一次點(diǎn)擊開始游戲時(shí)使用這么一次。

mouseDown     =

mousePressed  =

mouseUpFrames =

mouseX        = 0;

   

onmouseup   =e=> mouseDown = 0;

onmousedown =e=> mousePressed ? mouseDown = 1 : mousePressed = 1;

onmousemove =e=> mouseX = e.x/window.innerWidth*2 - 1;

數(shù)學(xué)函數(shù)

這個(gè)游戲使用了一些函數(shù)來簡化代碼和減少重復(fù),一些標(biāo)準(zhǔn)的數(shù)學(xué)函數(shù)用于 Clamp 和 Lerp 值。 ClampAngle 是有用的,因?yàn)樗?-PI 和 PI 之間 wrap angles,在許多游戲中已經(jīng)廣泛應(yīng)用。

R函數(shù)就像個(gè)魔術(shù)師,因?yàn)樗呻S機(jī)數(shù),通過取當(dāng)前隨機(jī)數(shù)種子的正弦,乘以一個(gè)大數(shù)字,然后看分?jǐn)?shù)部分來實(shí)現(xiàn)的。其實(shí)有很多方法可以做到,但這是最小的方法之一,而且對我們來說也是足夠隨機(jī)。

我們將使用這個(gè)隨機(jī)生成器來創(chuàng)建各種程序,且不需要保存任何數(shù)據(jù)。例如,山脈、巖石和樹木的變化不用存到內(nèi)存。在這種情況下,目標(biāo)不是減少內(nèi)存,而是去除存儲和檢索數(shù)據(jù)所需的代碼。

因?yàn)檫@是一個(gè)“真正的3D”游戲,所以有一個(gè) 3D vector class 非常有用,它也能減少代碼量。這個(gè) class 只包含這個(gè)游戲必需的基本元素,一個(gè)帶有加法和乘法函數(shù)的 constructor 可以接受標(biāo)量或向量參數(shù)。為了確定標(biāo)量是否被傳入,我們只需檢查它是否小于一個(gè)大數(shù)。更正確的方法是使用 isNan 或者檢查它的類型是否是 Vec3,但是這需要更多的存儲。

Clamp     =(v, a, b)  => Math.min(Math.max(v, a), b);
ClampAngle=(a)        => (a+PI) % (2*PI) + (a+PI<0? PI : -PI);
Lerp      =(p, a, b)  => a + Clamp(p, 0, 1) * (b-a);
R         =(a=1, b=0) => Lerp((Math.sin(++randSeed)+1)*1e5%1,a,b);
 
class Vec3 // 3d vector class
{
 constructor(x=0, y=0, z=0) {this.x = x; this.y = y; this.z = z;}
 
 Add=(v)=>(
   v = v < 1e5 ? new Vec3(v,v,v) : v,
   new Vec3( this.x + v.x, this.y + v.y, this.z + v.z ));
   
 Multiply=(v)=>(
   v = v < 1e5 ? new Vec3(v,v,v) : v,
   new Vec3( this.x * v.x, this.y * v.y, this.z * v.z ));
}

Render Functions渲染函數(shù)

LSHA 通過模板字符串生成一組標(biāo)準(zhǔn)的 HSLA (色調(diào)、飽和度、亮度、alpha)顏色,并且剛剛被重新排序,所以更常用的 component 排在第一位。每過一關(guān)換一個(gè)整體色調(diào)也是通過這設(shè)置的。

DrawPoly 繪制一個(gè)梯形形狀,用于渲染場景中的一切。使用 |0 將 Ycomponent 轉(zhuǎn)換為整數(shù),以確保每段多邊形道路都能無縫連接,不然路段之間就會有一條細(xì)線。

DrawText 則用于顯示時(shí)間、距離和游戲標(biāo)題等文本渲染。

 LSHA=(l,s=0,h=0,a=1)=>`hsl(${h+hueShift},${s}%,${l}%,${a})`;

// draw a trapazoid shaped poly
DrawPoly=(x1, y1, w1, x2, y2, w2, fillStyle)=>
{
   context.beginPath(context.fillStyle = fillStyle);
   context.lineTo(x1-w1, y1|0);
   context.lineTo(x1+w1, y1|0);
   context.lineTo(x2+w2, y2|0);
   context.lineTo(x2-w2, y2|0);
   context.fill();
}

// draw outlined hud text
DrawText=(text, posX)=>
{
   context.font = '9em impact';         // set font size
   context.fillStyle = LSHA(99,0,0,.5); // set font color
   context.fillText(text, posX, 129);   // fill text
   context.lineWidth = 3;               // line width
   context.strokeText(text, posX, 129); // outline text
}

設(shè)計(jì)軌道

如何用不到 2KB 的 JavaScript 代碼寫一個(gè) 3D 賽車游戲?

首先,我們必須生成完整的軌道,而且準(zhǔn)備做到每次游戲軌道都是不同的。如何做呢?我們建立了一個(gè)道路段列表,存儲道路在軌道上每一關(guān)卡的位置和寬度。軌道生成器是非常基礎(chǔ)的操作,不同頻率、振幅和寬度的道路都會逐漸變窄,沿著跑道的距離決定這一段路有多難。

atan2 函數(shù)可以用來計(jì)算道路俯仰角,據(jù)此來設(shè)計(jì)物理運(yùn)動和光線。

 roadGenLengthMax =                     // end of section
roadGenLength =                        // distance left
roadGenTaper =                         // length of taper
roadGenFreqX =                         // X wave frequency
roadGenFreqY =                         // Y wave frequency
roadGenScaleX =                        // X wave amplitude
roadGenScaleY = 0;                     // Y wave amplitude
roadGenWidth = roadWidth;              // starting road width
startRandSeed = randSeed = Date.now(); // set random seed
road = [];                             // clear road

// generate the road
for( i = 0; i < roadEnd*2; ++i )          // build road past end
{
 if (roadGenLength++ > roadGenLengthMax) // is end of section?
 {
   // calculate difficulty percent
   d = Math.min(1, i/maxDifficultySegment);
 
   // randomize road settings
   roadGenWidth = roadWidth*R(1-d*.7,3-2*d);        // road width
   roadGenFreqX = R(Lerp(d,.01,.02));               // X curves
   roadGenFreqY = R(Lerp(d,.01,.03));               // Y bumps
   roadGenScaleX = i>roadEnd ? 0 : R(Lerp(d,.2,.6));// X scale
   roadGenScaleY = R(Lerp(d,1e3,2e3));              // Y scale
 
   // apply taper and move back
   roadGenTaper = R(99, 1e3)|0;                 // random taper
   roadGenLengthMax = roadGenTaper + R(99,1e3); // random length
   roadGenLength = 0;                           // reset length
   i -= roadGenTaper;                           // subtract taper
 }
 
 // make a wavy road
 x = Math.sin(i*roadGenFreqX) * roadGenScaleX;
 y = Math.sin(i*roadGenFreqY) * roadGenScaleY;
 road[i] = road[i]? road[i] : {x:x, y:y, w:roadGenWidth};
 
 // apply taper from last section and lerp values
 p = Clamp(roadGenLength / roadGenTaper, 0, 1);
 road[i].x = Lerp(p, road[i].x, x);
 road[i].y = Lerp(p, road[i].y, y);
 road[i].w = i > roadEnd ? 0 : Lerp(p, road[i].w, roadGenWidth);
   
 // calculate road pitch angle
 road[i].a = road[i-1] ?
   Math.atan2(road[i-1].y-road[i].y, segmentLength) : 0;
}

啟動游戲

現(xiàn)在跑道就緒,我們只需要預(yù)置一些變量就可以開始游戲了。

 // reset everything
velocity = new Vec3
 ( pitchSpring =  pitchSpringSpeed =  pitchRoad = hueShift = 0 );
 
position = new Vec3(0, height);      // set player start pos
nextCheckPoint = checkPointDistance; // init next checkpoint
time = maxTime;                      // set the start time
heading = randSeed;                  // random world heading

更新玩家

這是主要的更新功能,用來更新和渲染游戲中的一切!一般來說,如果你的代碼中有一個(gè)很大的函數(shù),這不是好事,為了更簡潔易懂,我們會把它分幾個(gè)成子函數(shù)。

首先,我們需要得到一些玩家所在位置的道路信息。為了使物理和渲染感覺平滑,需要在當(dāng)前和下一個(gè)路段之間插入一些數(shù)值。

玩家的位置和速度是 3D 向量,并受重力、dampening 和其他因素等影響更新。如果玩家跑在地面上時(shí),會受到加速度影響;當(dāng)他離開這段路時(shí),攝像機(jī)還會抖動。另外,在對游戲測試后,我決定讓玩家在空中時(shí)仍然可以跑。

接下來要處理輸入指令,涉及加速、剎車、跳躍和轉(zhuǎn)彎等操作。雙擊通過 mouseUpFrames 測試。還有一些代碼是來跟蹤玩家在空中停留了多少幀,如果時(shí)間很短,游戲允許玩家還可以跳躍。

當(dāng)玩家加速、剎車和跳躍時(shí),我通過spring system展示相機(jī)的俯仰角以給玩家動態(tài)運(yùn)動的感覺。此外,當(dāng)玩家駕車翻越山丘或跳躍時(shí),相機(jī)還會隨著道路傾斜而傾斜。

 Update=()=>
{

// get player road segment
s = position.z / segmentLength | 0; // current road segment
p = position.z / segmentLength % 1; // percent along segment

// get lerped values between last and current road segment
roadX = Lerp(p, road[s].x, road[s+1].x);
roadY = Lerp(p, road[s].y, road[s+1].y) + height;
roadA = Lerp(p, road[s].a, road[s+1].a);

// update player velocity
lastVelocity = velocity.Add(0);
velocity.y += gravity;
velocity.x *= lateralDamp;
velocity.z = Math.max(0, time?forwardDamp*velocity.z:0);

// add velocity to position
position = position.Add(velocity);
 
// limit player x position (how far off road)
position.x = Clamp(position.x, -maxPlayerX, maxPlayerX);

// check if on ground
if (position.y < roadY)
{
 position.y = roadY; // match y to ground plane
 airFrame = 0;       // reset air frames
 
 // get the dot product of the ground normal and the velocity
 dp = Math.cos(roadA)*velocity.y + Math.sin(roadA)*velocity.z;
 
 // bounce velocity against ground normal
 velocity = new Vec3(0, Math.cos(roadA), Math.sin(roadA))
   .Multiply(-elasticity * dp).Add(velocity);
   
 // apply player brake and accel
 velocity.z +=
   mouseDown? playerBrake :
   Lerp(velocity.z/maxSpeed, mousePressed*playerAccel, 0);
 
 // check if off road
 if (Math.abs(position.x) > road[s].w)
 {
   velocity.z *= offRoadDamp;                    // slow down
   pitchSpring += Math.sin(position.z/99)**4/99; // rumble
 }
}

// update player turning and apply centrifugal force
turn = Lerp(velocity.z/maxSpeed, mouseX * turnControl, 0);
velocity.x +=
 velocity.z * turn -
 velocity.z ** 2 * centrifugal * roadX;

// update jump
if (airFrame++<6 && time
 && mouseDown && mouseUpFrames && mouseUpFrames<9)
{
 velocity.y += jumpAccel; // apply jump velocity
 airFrame = 9;            // prevent jumping again
}
mouseUpFrames = mouseDown? 0 : mouseUpFrames+1;

// pitch down with vertical velocity when in air
airPercent = (position.y-roadY) / 99;
pitchSpringSpeed += Lerp(airPercent, 0, velocity.y/4e4);

// update player pitch spring
pitchSpringSpeed += (velocity.z - lastVelocity.z)/2e3;
pitchSpringSpeed -= pitchSpring * springConstant;
pitchSpringSpeed *= pitchSpringDamp;
pitchSpring += pitchSpringSpeed;
pitchRoad = Lerp(pitchLerp, pitchRoad, Lerp(airPercent,-roadA,0));
playerPitch = pitchSpring + pitchRoad;

// update heading
heading = ClampAngle(heading + velocity.z*roadX*worldRotateScale);
cameraHeading = turn * cameraTurnScale;

// was checkpoint crossed?
if (position.z > nextCheckPoint)
{
 time += checkPointTime;               // add more time
 nextCheckPoint += checkPointDistance; // set next checkpoint
 hueShift += 36;                       // shift hue
}

預(yù)渲染

在渲染之前,canvas 每當(dāng)高度或?qū)挾缺恢卦O(shè)時(shí),畫布內(nèi)容就會被清空。這也適用于自適應(yīng)窗口的畫布。

我們還計(jì)算了將世界點(diǎn)轉(zhuǎn)換到畫布的投影比例。cameraDepth 值代表攝像機(jī)的視場(FOV)。這個(gè)游戲是90度。計(jì)算結(jié)果是 1/Math.tan(fovRadians/2) ,F(xiàn)OV 是90度的時(shí)候,計(jì)算結(jié)果正好是1。另外為了保持屏幕長寬比,投影按 c.width 縮放。

 // clear the screen and set size
c.width = window.innerWidth, c.height = window.innerHeight;

// calculate projection scale, flip y
projectScale = (new Vec3(1,-1,1)).Multiply(c.width/2/cameraDepth);

給世界畫上天空、太陽和月亮

如何用不到 2KB 的 JavaScript 代碼寫一個(gè) 3D 賽車游戲?

空氣背景是用全屏的 linear gradient (徑向漸變)繪制的,它還會根據(jù)太陽的位置改變顏色。

為了節(jié)省存儲空間,太陽和月亮在同一個(gè)循環(huán)中,使用了一個(gè)帶有透明度的全屏 radial gradient(線性漸變)。

線性和徑向漸變相結(jié)合,形成一個(gè)完全包圍場景的天空背景。

 // get horizon, offset, and light amount
horizon = c.height/2 - Math.tan(playerPitch)*projectScale.y;
backgroundOffset = Math.sin(cameraHeading)/2;
light = Math.cos(heading);

// create linear gradient for sky
g = context.createLinearGradient(0,horizon-c.height/2,0,horizon);
g.addColorStop(0,LSHA(39+light*25,49+light*19,230-light*19));
g.addColorStop(1,LSHA(5,79,250-light*9));

// draw sky as full screen poly
DrawPoly(c.width/2,0,c.width/2,c.width/2,c.height,c.width/2,g);

// draw sun and moon (0=sun, 1=moon)
for( i = 2 ; i--; )
{
 // create radial gradient
 g = context.createRadialGradient(
   x = c.width*(.5+Lerp(
     (heading/PI/2+.5+i/2)%1,
     4, -4)-backgroundOffset),
   y = horizon - c.width/5,
   c.width/25,
   x, y, i?c.width/23:c.width);
 g.addColorStop(0, LSHA(i?70:99));
 g.addColorStop(1, LSHA(0,0,0,0));
 
 // draw full screen poly
 DrawPoly(c.width/2,0,c.width/2,c.width/2,c.height,c.width/2,g);
}

給世界畫上山峰、地平線

如何用不到 2KB 的 JavaScript 代碼寫一個(gè) 3D 賽車游戲?

山脈是通過在地平線上畫50個(gè)三角形,然后根據(jù)程序自己生成的。

因?yàn)橛昧斯饩€照明,山脈在面對太陽時(shí)會更暗,因?yàn)樗鼈兲幱陉幱爸?。此外,越近的山脈顏色越暗,我想以此來模擬霧氣。這里我有個(gè)訣竅,就是微調(diào)大小和顏色的隨機(jī)值。

背景的最后一部分是繪制地平線,再用純綠填充畫布的底部。

 // set random seed for mountains
randSeed = startRandSeed;

// draw mountains
for( i = mountainCount; i--; )
{
 angle = ClampAngle(heading+R(19));
 light = Math.cos(angle-heading);
 DrawPoly(
   x = c.width*(.5+Lerp(angle/PI/2+.5,4,-4)-backgroundOffset),
   y = horizon,
   w = R(.2,.8)**2*c.width/2,
   x + w*R(-.5,.5),
   y - R(.5,.8)*w, 0,
   LSHA(R(15,25)+i/3-light*9, i/2+R(19), R(220,230)));
}

// draw horizon
DrawPoly(
 c.width/2, horizon, c.width/2, c.width/2, c.height, c.width/2,
 LSHA(25, 30, 95));

將路段投影到畫布空間

在渲染道路之前,我們必須首先獲得投影的道路點(diǎn)。第一部分有點(diǎn)棘手,因?yàn)槲覀兊牡缆返?x 值需要轉(zhuǎn)換成世界空間位置。為了使道路看起來蜿蜒曲折,我們把x值作為二階導(dǎo)數(shù)。這就是為什么有奇怪的代碼“x+=w+=”出現(xiàn)的原因。由于這種工作方式,路段沒有固定的世界空間位置,每一幀都是根據(jù)玩家的位置重新計(jì)算。

一旦我們有了世界空間位置,我們就可以從道路位置中知道玩家的位置,從而得到本地?cái)z像機(jī)空間位置。代碼的其余部分,首先通過旋轉(zhuǎn)標(biāo)題、俯仰角來應(yīng)用變換,然后通過投影變換,做到近大遠(yuǎn)小的效果,最后將其移動到畫布空間。

 for( x = w = i = 0; i < drawDistance+1; )
{
 p = new Vec3(x+=w+=road[s+i].x,     // sum local road offsets
   road[s+i].y, (s+i)*segmentLength) // road y and z pos
     .Add(position.Multiply(-1));    // get local camera space

 // apply camera heading
 p.x = p.x*Math.cos(cameraHeading) - p.z*Math.sin(cameraHeading);
 
 // tilt camera pitch and invert z
 z = 1/(p.z*Math.cos(playerPitch) - p.y*Math.sin(playerPitch));
 p.y = p.y*Math.cos(playerPitch) - p.z*Math.sin(playerPitch);
 p.z = z;
 
 // project road segment to canvas space
 road[s+i++].p =                         // projected road point
   p.Multiply(new Vec3(z, z, 1))         // projection
   .Multiply(projectScale)               // scale
   .Add(new Vec3(c.width/2,c.height/2)); // center on canvas
}

繪制路段

 如何用不到 2KB 的 JavaScript 代碼寫一個(gè) 3D 賽車游戲?

現(xiàn)在我們有了每個(gè)路段的畫布空間點(diǎn),渲染就相當(dāng)簡單了。我們需要從后向前畫出每一個(gè)路段,或者更具體地說,連接上一路段的梯形多邊形。

為了創(chuàng)建道路,這里有4層渲染:地面,條紋路邊緣,道路本身和白色虛線。每一個(gè)都是基于路段的俯仰角和方向來加陰影,并且根據(jù)該層的表現(xiàn)還有一些額外的邏輯。

有必要檢查路段是在近還是遠(yuǎn)剪輯范圍,以防止渲染出現(xiàn) bug 。此外,還有一個(gè)很好的優(yōu)化方法是,當(dāng)?shù)缆纷兊煤苷瓡r(shí),可以通過 distance 來減小道路的分辨率。如此,不僅減少了 draw count 一半以上,而且沒有明顯的質(zhì)量損失,這是一次性能勝利。

 let segment2 = road[s+drawDistance]; // store the last segment
for( i = drawDistance; i--; )        // iterate in reverse
{
 // get projected road points
 segment1 = road[s+i];
 p1 = segment1.p;
 p2 = segment2.p;
 
 // random seed and lighting
 randSeed = startRandSeed + s + i;
 light = Math.sin(segment1.a) * Math.cos(heading) * 99;
 
 // check near and far clip
 if (p1.z < 1e5 && p1.z > 0)
 {
   // fade in road resolution over distance
   if (i % (Lerp(i/drawDistance,1,9)|0) == 0)
   {
     // ground
     DrawPoly(c.width/2, p1.y, c.width/2,
       c.width/2, p2.y, c.width/2,
       LSHA(25 + light, 30, 95));

     // curb if wide enough
     if (segment1.w > 400)
       DrawPoly(p1.x, p1.y, p1.z*(segment1.w+curbWidth),
         p2.x, p2.y, p2.z*(segment2.w+curbWidth),
         LSHA(((s+i)%19<9? 50: 20) + light));
     
     // road and checkpoint marker
     DrawPoly(p1.x, p1.y, p1.z*segment1.w,
       p2.x, p2.y, p2.z*segment2.w,
       LSHA(((s+i)*segmentLength%checkPointDistance < 300 ?
         70 : 7) + light));
       
     // dashed lines if wide and close enough
     if ((segment1.w > 300) && (s+i)%9==0 && i < drawDistance/3)
         DrawPoly(p1.x, p1.y, p1.z*dashLineWidth,
         p2.x, p2.y, p2.z*dashLineWidth,
         LSHA(70 + light));

     // save this segment
     segment2 = segment1;
   }

繪制路邊的樹和石頭

如何用不到 2KB 的 JavaScript 代碼寫一個(gè) 3D 賽車游戲?

游戲有兩種不同類型的物體:樹和石頭。首先,我們通過使用 R() 函數(shù)來確定是否加一個(gè)對象。這是隨機(jī)數(shù)和隨機(jī)數(shù)種子特別有意思的地方。我們還將使用 R() 為對象隨機(jī)添加不同的形狀和顏色。

最初我還想涉及其他車型,但為了達(dá)到 2KB 的要求,必須要進(jìn)行特別多的削減,因此我最后放棄了這個(gè)想法,用風(fēng)景作為障礙。這些位置是隨機(jī)的,也比較靠近道路,不然它們太稀疏,就很容易行駛。為了節(jié)省空間,對象高度還決定了對象的類型。

這是通過比較玩家和物體在 3D 空間中的位置來檢查它們之間的碰撞位置。當(dāng)玩家撞到一個(gè)物體時(shí),玩家減速,該物體被標(biāo)記為“ hit ”,這樣它就可以安全通過。

為了防止對象突然出現(xiàn)在地平線上,透明度會隨著距離的接近而削弱。梯形繪圖函數(shù)定義物體的形狀和顏色,另外隨機(jī)函數(shù)會改變這兩個(gè)屬性。

  if (R()<.2 && s+i>29)                  // is there an object?
   {
     // player object collision check
     x = 2*roadWidth * R(10,-10) * R(9);  // choose object pos
     const objectHeight = (R(2)|0) * 400; // choose tree or rock
     if (!segment1.h                      // dont hit same object
       && Math.abs(position.x-x)<200                      // X
       && Math.abs(position.z-(s+i)*segmentLength)<200    // Z
       && position.y-height<segment1.y+objectHeight+200)  // Y
     {
       // slow player and mark object as hit
       velocity = velocity.Multiply(segment1.h = collisionSlow);
     }

     // draw road object
     const alpha = Lerp(i/drawDistance, 4, 0);  // fade in object
     if (objectHeight)
     {
       // tree trunk
       DrawPoly(x = p1.x+p1.z * x, p1.y, p1.z*29,
         x, p1.y-99*p1.z, p1.z*29,
         LSHA(5+R(9), 50+R(9), 29+R(9), alpha));
         
       // tree leaves
       DrawPoly(x, p1.y-R(50,99)*p1.z, p1.z*R(199,250),
         x, p1.y-R(600,800)*p1.z, 0,
         LSHA(25+R(9), 80+R(9), 9+R(29), alpha));
     }
     else
     {
       // rock
       DrawPoly(x = p1.x+p1.z*x, p1.y, p1.z*R(200,250),
         x+p1.z*(R(99,-99)), p1.y-R(200,250)*p1.z, p1.z*R(99),
         LSHA(50+R(19), 25+R(19), 209+R(9), alpha));
     }
   }
 }
}

畫上 HUD,更新時(shí)間,請求下一次更新

如何用不到 2KB 的 JavaScript 代碼寫一個(gè) 3D 賽車游戲?

游戲的標(biāo)題、時(shí)間和距離是用一個(gè)非?;A(chǔ)的字體渲染系統(tǒng)顯示出來的,就是之前設(shè)置的 DrawText 函數(shù)。在玩家點(diǎn)擊鼠標(biāo)之前,它會在屏幕中央顯示標(biāo)題。

按下鼠標(biāo)后,游戲開始,然后 HUD 會顯示剩余時(shí)間和當(dāng)前距離。時(shí)間也在這塊更新,玩過此類游戲的都知道,時(shí)間只在比賽開始后減少。

在這個(gè) massive Update function 結(jié)束后,它調(diào)用 requestAnimationFrame (Update) 來觸發(fā)下一次更新。

 if (mousePressed)
{
 time = Clamp(time - timeDelta, 0, maxTime); // update time
 DrawText(Math.ceil(time), 9);               // show time
 context.textAlign = 'right';                // right alignment
 DrawText(0|position.z/1e3, c.width-9);      // show distance
}
else
{
 context.textAlign = 'center';      // center alignment
 DrawText('HUE JUMPER', c.width/2); // draw title text
}

requestAnimationFrame(Update); // kick off next frame

} // end of update function

代碼的最后一位

HTML 需要一個(gè)結(jié)束腳本標(biāo)簽來讓所有的代碼能夠跑起來。

 Update(); // kick off update loop
</script>

壓縮

這就是整個(gè)游戲啦!下方的一小段代碼就是壓縮后的最終結(jié)果,我用不同的顏色標(biāo)注了不同的部分。完成所有這些工作后,你能感受到我在2KB內(nèi)就做完了整個(gè)游戲是多么讓我滿意了嗎?而這還是在zip之前的工作,zip還可以進(jìn)一步壓縮大小。

如何用不到 2KB 的 JavaScript 代碼寫一個(gè) 3D 賽車游戲?

如何用不到 2KB 的 JavaScript 代碼寫一個(gè) 3D 賽車游戲?

警告 Caveats

當(dāng)然,還有很多其他 3D 渲染方法可以同時(shí)保證性能和視覺效果。如果我有更多的可用空間,我會更傾向于使用一個(gè) WebGL API 比如 three.js ,我在去年制作的一個(gè)類似游戲“Bogus Roads”中用過這個(gè)框架。此外,因?yàn)樗褂玫氖?requestAnimationFrame ,所以需要一些額外的代碼來確保幀速率不超過60 fps,增強(qiáng)版本中我會這么用,盡管我更喜歡使用 requestAnimationFrame 而不是 setInterval ,因?yàn)樗谴怪蓖诘模╒Syn,VerticalSynchronization),所以渲染更絲滑。這種代碼的一個(gè)主要好處是它非常兼容,可以在任何設(shè)備上運(yùn)行,盡管在我舊 iPhone 上運(yùn)行有點(diǎn)慢。

游戲代碼被我放到了 GitHub 上的 GPL-3.0 下(https://github.com/KilledByAPixel/HueJumper2k),所以你可以在自己的項(xiàng)目中自由使用它。該庫中還包含 2KB 版本的游戲,準(zhǔn)確說是2031字節(jié)!歡迎你添加一些其他的功能,比如音樂和音效到“增強(qiáng)”版本中。

后記

雷鋒網(wǎng)注意到,F(xiàn)rank Force 在個(gè)人博客發(fā)了這篇文章后,在內(nèi)容、標(biāo)題的加持下,這篇文章后來被不少國外媒體轉(zhuǎn)載。在盛贊之余,也有質(zhì)疑的聲音。網(wǎng)友“Anon”在原文下評論:你是如何在 2KB 安裝一個(gè)完整的 javascript 的,除非你可以隨意忽略 dependencies 插件庫的大小,或者你將整個(gè)游戲作為 dependency,大小才有可能控制到 2KB,否則就是欺騙。

Frank 回復(fù)表示,大多數(shù) small demos 都需要某種運(yùn)行環(huán)境,即使它是可執(zhí)行的。在這種情況下,就是 javascript 運(yùn)行時(shí)環(huán)境,沒有其他 dependencies.。因?yàn)?javascript 是解釋的,所以也可以說壓縮后的代碼是在2KB以內(nèi)的。

雷鋒網(wǎng)發(fā)現(xiàn),有其他網(wǎng)友表示認(rèn)可 Frank 的說法,他們認(rèn)為 JS 是一種解釋語言,不能將其與其他編譯語言相比較。

雷鋒網(wǎng)

雷峰網(wǎng)版權(quán)文章,未經(jīng)授權(quán)禁止轉(zhuǎn)載。詳情見轉(zhuǎn)載須知。

如何用不到 2KB 的 JavaScript 代碼寫一個(gè) 3D 賽車游戲?

分享:
相關(guān)文章

編輯

數(shù)據(jù)漫游天地間。 聯(lián)系可通過上方郵箱或WeChat(請注明身份、姓名、來意,thx)
當(dāng)月熱門文章
最新文章
請?zhí)顚懮暾埲速Y料
姓名
電話
郵箱
微信號
作品鏈接
個(gè)人簡介
為了您的賬戶安全,請驗(yàn)證郵箱
您的郵箱還未驗(yàn)證,完成可獲20積分喲!
請驗(yàn)證您的郵箱
立即驗(yàn)證
完善賬號信息
您的賬號已經(jīng)綁定,現(xiàn)在您可以設(shè)置密碼以方便用郵箱登錄
立即設(shè)置 以后再說