pos機(jī)p3代碼,如何用2 KB代碼實(shí)現(xiàn)3D賽車(chē)游戲

 新聞資訊2  |   2023-05-26 09:48  |  投稿人:pos機(jī)之家

網(wǎng)上有很多關(guān)于pos機(jī)p3代碼,如何用2 KB代碼實(shí)現(xiàn)3D賽車(chē)游戲的知識(shí),也有很多人為大家解答關(guān)于pos機(jī)p3代碼的問(wèn)題,今天pos機(jī)之家(m.afbey.com)為大家整理了關(guān)于這方面的知識(shí),讓我們一起來(lái)看下吧!

本文目錄一覽:

1、pos機(jī)p3代碼

pos機(jī)p3代碼

選自frankforce

作者:Frank

機(jī)器之心編譯

參與:王子嘉、Geek AI

控制復(fù)雜度一直是軟件開(kāi)發(fā)的核心問(wèn)題之一,一代代的計(jì)算機(jī)從業(yè)者紛紛貢獻(xiàn)著自己的智慧,試圖降低程序的計(jì)算復(fù)雜度。然而,將一款 3D 賽車(chē)游戲的代碼壓縮到 2KB 以?xún)?nèi),聽(tīng)起來(lái)是不是太夸張了?本文作者 Frank 是一名資深游戲開(kāi)發(fā)者,在本文中,他詳細(xì)介紹了如何靈活運(yùn)用代碼壓縮、編譯、隨機(jī)數(shù)生成、代碼復(fù)用、設(shè)計(jì)模式等十八般武藝僅僅通過(guò) 2KB 的代碼就能實(shí)現(xiàn)一款強(qiáng)大的 3D 賽車(chē)游戲。

幾個(gè)月前,當(dāng)我聽(tīng)說(shuō)傳奇 JS1K 游戲編程競(jìng)賽將不再舉辦時(shí),當(dāng)即把這件事告訴了其他開(kāi)發(fā)者,最后我們決定在 itch 上搞一個(gè) 2KB 版的編程競(jìng)賽以彌補(bǔ)這一遺憾,我們將其稱(chēng)之為「2kPlus Jam」。這個(gè)競(jìng)賽的主要目標(biāo)是制作一個(gè)只需要 2KB 壓縮文件就可以容納的游戲。如果你知道一個(gè) 3.5 英寸軟盤(pán)可以存超過(guò) 700 個(gè)這樣的游戲,你也就知道這有多小了。

我的作品(Hue Jumper)是對(duì) 80 年代賽車(chē)游戲渲染技術(shù)的致敬。這里的 3D 圖像和物理引擎是我純粹地使用 JavaScript 從零開(kāi)始實(shí)現(xiàn)的,同時(shí)我還花了大量時(shí)間去調(diào)整游戲玩法和視覺(jué)效果。

游戲編程競(jìng)賽強(qiáng)調(diào)「變化」(shift),所以每當(dāng)玩家通過(guò)關(guān)卡時(shí),我就會(huì)改變整個(gè)世界的色調(diào)。我想在玩家通過(guò)關(guān)卡時(shí),會(huì)感覺(jué)到像進(jìn)入了一個(gè)色調(diào)不同的新的維度,這就是我為它取名為「Hue Jumper」的原因。

本文包含了這個(gè)游戲的完整 JavaScript 代碼,所以可能會(huì)有點(diǎn)長(zhǎng)。不過(guò)代碼的注釋很友好,所以我不打算一行一行解讀,也不要求你現(xiàn)在就通讀所有代碼。我的目的是向你解釋它的工作原理,還有為什么我要這樣做,以及這個(gè)項(xiàng)目的整個(gè)結(jié)構(gòu)。你也可以在 CodePen上找到這份代碼(https://codepen.io/KilledByAPixel/pen/poJdLwB),并進(jìn)行現(xiàn)場(chǎng)調(diào)試。

那么,系好安全帶,坐穩(wěn),我們要開(kāi)始啦!

靈感來(lái)源

我的靈感主要來(lái)源于 80 年代的經(jīng)典賽車(chē)游戲,比如《Out Run》。使用相似的技術(shù),他們能夠在非常早期的硬件上實(shí)現(xiàn)實(shí)時(shí)三維圖形。我最近也在玩一些現(xiàn)代的賽車(chē)游戲,比如《Distance》和《Lonely Mountains: Downhill》,這些游戲也對(duì)我的視覺(jué)設(shè)計(jì)和游戲體驗(yàn)有所幫助。

Jake Gordon 用 JavaScript 寫(xiě)的一個(gè)偽 3D 賽車(chē)的項(xiàng)目(https://GitHub.com/jakesgordon/javascript-racer/)幫我減輕了很多負(fù)擔(dān)。他還為此寫(xiě)了一系列解釋其工作原理的博文。盡管我的項(xiàng)目是從零開(kāi)始的,但是他的代碼助我解決了遇到的包括數(shù)學(xué)在內(nèi)的一些問(wèn)題。

我還看了 Chris Glover 開(kāi)發(fā)的一款名為「Moto1kross」的 JS1k 游戲(https://js1k.com/2019-x/demo/4006)。這款簡(jiǎn)單的 1KB 賽車(chē)游戲給了我一個(gè)參考,讓我知道什么是可能實(shí)現(xiàn)的?,F(xiàn)在我有額外的 1KB 可用空間,因此我得遠(yuǎn)遠(yuǎn)超過(guò)它。

總體策略

由于游戲大小有嚴(yán)格的限制,程序的架構(gòu)就顯得尤為重要。我的總體策略是讓一切盡可能的簡(jiǎn)單,以實(shí)現(xiàn)創(chuàng)造一款視覺(jué)感受和游戲體驗(yàn)都很棒的游戲的最終目標(biāo)。

為了壓縮代碼,我用 Google Closure Compiler (https://closure-compiler.appspot.com/home) 來(lái)運(yùn)行它。這個(gè)編譯器會(huì)刪掉所有空白,把變量重命名為 1 個(gè)字母的字符,并且做了一些簡(jiǎn)單的優(yōu)化。你可以通過(guò)下面的鏈接使用這個(gè)編譯器:https://clocompiler.appspot.com/home。

不過(guò),這個(gè)編譯器還做了一些無(wú)用的事,比如替換模板字符串、缺省參數(shù)和其它有助于節(jié)省空間的 ES6 特性。所以我需要手動(dòng)撤銷(xiāo)某些無(wú)用的工作,并預(yù)先準(zhǔn)備一些更「冒險(xiǎn)」的壓縮技術(shù),以節(jié)省每一個(gè)字節(jié)。但這并不是最主要的成功之處,大部分文件體積的壓縮還是歸功于代碼本身的架構(gòu)。

代碼需要被壓縮到 2KB 以?xún)?nèi)。如果你不想選用上一種方案,還有一個(gè)類(lèi)似的、但功能較弱的工具——RegPack,它可以在嚴(yán)格遵守規(guī)定的情況下編譯 JavaScript。無(wú)論哪種方式,策略都是一樣的,盡可能使用重復(fù)的代碼,然后在壓縮的時(shí)候壓縮它們。例如,某些字符串經(jīng)常出現(xiàn),因此它們的壓縮比很大。「c.width="360px",height="auto" />

CodePen

下面我們將給出一款在 Codepen 上運(yùn)行的游戲。你可以在 iframe 上玩這個(gè)游戲,但是為了獲得最佳的游戲體驗(yàn),我建議你使用鏈接(https://codepen.io/KilledByAPixel/pen/poJdLwB)打開(kāi)它,這樣你還可以編輯或是創(chuàng)建代碼分支。

HTML

我的游戲使用到 html 的部分很少,因?yàn)樗饕腔?JavaScript 開(kāi)發(fā)的。JavaScript 創(chuàng)建全屏畫(huà)布的方法和與后面將畫(huà)布大小設(shè)置為窗口內(nèi)部大小的代碼都是最節(jié)省空間的。我不能確定為什么 CodePen 中需要將「overflow:hidden」添加到「body」標(biāo)簽中,但是直接打開(kāi)應(yīng)該也可以正常工作。

最終的壓縮版本使用了更小的設(shè)置——把 JavaScript 包在一個(gè)「onload」事件的「call」方法里()。但是,我不喜歡在開(kāi)發(fā)的時(shí)候用這種壓縮的設(shè)置,因?yàn)榇a是以字符串形式存儲(chǔ)的,這樣編譯器也就無(wú)法正常地強(qiáng)調(diào)語(yǔ)法。

常量

游戲中的很多東西都是由常量來(lái)控制的。當(dāng)我們用類(lèi)似 Google Closure 這樣的工具來(lái)進(jìn)行壓縮時(shí),這些常量就會(huì)被替換成類(lèi)似于 C++ 中的「#define」的形式。將它們放在開(kāi)頭能夠更快地調(diào)整游戲玩法。

// draw settingsconst context = c.getContext`2d`; // canvas contextconst drawDistance = 800; // how far ahead to drawconst cameraDepth = 1; // FOV of cameraconst segmentLength = 100; // length of each road segmentconst roadwidth="360px",height="auto" />

// player settingsconst height = 150; // high of player above groundconst maxSpeed = 300; // limit max player speedconst playerAccel = 1; // player forward accelerationconst playerBrake = -3; // player breaking accelerationconst turnControl = .2; // player turning rateconst jumpAccel = 25; // z speed added for jumpconst springConstant = .01; // spring players pitchconst collisionSlow = .1; // slow down from collisionsconst pitchLerp = .1; // rate camera pitch changesconst pitchSpringDamp = .9; // dampen the pitch springconst elasticity = 1.2; // bounce elasticityconst centrifugal = .002; // how much turns pull playerconst forwardDamp = .999; // dampen player z speedconst lateralDamp = .7; // dampen player x speedconst offRoadDamp = .98; // more damping when off roadconst gravity = -1; // gravity to apply in y axisconst cameraTurnScale = 2; // how much to rotate cameraconst worldRotateScale = .00005; // how much to rotate world // level settingsconst maxTime = 20; // time to startconst checkPointTime = 10; // add time at checkpointsconst checkPointDistance = 1e5; // how far between checkpointsconst maxDifficultySegment = 9e3; // how far until max difficultyconst roadEnd = 1e4; // how far until end of road

鼠標(biāo)控制

輸入系統(tǒng)是非常簡(jiǎn)單的,只用到了鼠標(biāo)。使用下面這段代碼,我們可以跟蹤鼠標(biāo)點(diǎn)擊和水平光標(biāo)位置,并將其表示為 -1 到 1 之間的值。雙擊是通過(guò)「mouseUpFrames」實(shí)現(xiàn)的?!竚ousePressed」變量只在玩家第一次點(diǎn)擊開(kāi)始游戲時(shí)使用一次。

mouseDown =mousePressed =mouseUpFrames =mouseX = 0; onmouseup =e=> mouseDown = 0;onmousedown =e=> mousePressed ? mouseDown = 1 : mousePressed = 1;onmousemove =e=> mouseX = e.x/window.innerwidth="360px",height="auto" />

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

這個(gè)游戲使用了一些函數(shù)來(lái)簡(jiǎn)化代碼并減少冗余。一些標(biāo)準(zhǔn)的數(shù)學(xué)函數(shù)可以用來(lái)對(duì)值進(jìn)行限定(Clamp)并進(jìn)行線(xiàn)性差值操作(Lerp)?!窩lampAngle」函數(shù)就非常有用,因?yàn)楹芏嘤螒蛐枰獙⒔嵌认拗圃?-PI 和 PI 之間,而這個(gè)函數(shù)就可以做到。

隨機(jī)測(cè)試樣例

R 函數(shù)的工作原理就像魔法——生成種子隨機(jī)數(shù)。它先取當(dāng)前隨機(jī)種子的正弦值,乘以一個(gè)很大的數(shù),然后小數(shù)部分就是最終的隨機(jī)數(shù)。有很多方法可以做到這一點(diǎn),但這是最節(jié)約空間的方法之一。我不建議使用這項(xiàng)技術(shù)來(lái)做賭博軟件,但在我們的項(xiàng)目里,它的隨機(jī)性已經(jīng)足夠了。我們將使用這個(gè)隨機(jī)生成器在不需要保存任何數(shù)據(jù)的情況下創(chuàng)建各種程序。例如,山脈、巖石和樹(shù)木的變化并不儲(chǔ)存在內(nèi)存里。但我們這里的目標(biāo)不是減少內(nèi)存,而是消除存儲(chǔ)和檢索數(shù)據(jù)所需的代碼。

由于這是一個(gè)「真 3D」游戲,一個(gè) 3D 向量類(lèi)就顯得極為有用了,而且它還能讓代碼容量更小。該類(lèi)只包含這個(gè)游戲所必需的基本要素——一個(gè)帶有加法和乘法函數(shù)的構(gòu)造函數(shù),它的參數(shù)既可以是標(biāo)量,也可以是向量。要確定是否傳入了一個(gè)標(biāo)量,只需檢查它是否小于一個(gè)大數(shù)。使用「isNan」或是檢查它的類(lèi)型是否是「Vec3」當(dāng)然更好,但它們需要更多的空間。

Clamp =(v, a, b) => Math.min(Math.max(v, a), b);ClampAngle=(a) => (a+PI) % (2*PI) + (a+PILerp =(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 ));}

渲染函數(shù)

LSHA 使用模板字符串生成一個(gè)標(biāo)準(zhǔn)的 HSLA(色相、飽和度、光度、透明度)顏色,并且剛剛重新排序,以便將更多經(jīng)常使用的組件排列在前面。在關(guān)卡處發(fā)生的全局色調(diào)變化也是在這里發(fā)生的。

DrawPoly 可以繪制梯形,它也會(huì)被用于渲染場(chǎng)景中的所有東西。使用「|0」將 Y 分量轉(zhuǎn)換為整數(shù),以確保道路多邊形完全連接。如果進(jìn)行這項(xiàng)操作,在路段之間就會(huì)有一條細(xì)線(xiàn)。出于同樣的原因,這種渲染技術(shù)必須在對(duì)角線(xiàn)圖形的組件幫助下處理相機(jī)的滾動(dòng)。

DrawText 則被用來(lái)渲染顯示時(shí)間、距離和游戲標(biāo)題的概述文本。

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

// draw a trapazoid shaped polyDrawPoly=(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 textDrawText=(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="360px",height="auto" />

通過(guò)過(guò)程生成來(lái)構(gòu)建軌道

在游戲開(kāi)始之前,我們必須首先生成整個(gè)賽道,而每個(gè)游戲的賽道都不同。為此,我們構(gòu)建一個(gè)路段列表,它存儲(chǔ)了道路在軌道上每個(gè)點(diǎn)的位置和寬度。

軌道發(fā)生器很簡(jiǎn)單,它只是在不同頻率、振幅和寬度的部分之間逐漸變細(xì)。賽道長(zhǎng)度決定了這段賽道的難度。

這里的道路俯仰角是使用「atan2」函數(shù)計(jì)算出來(lái)的,它被用于用于物理效果和照明。

程序化的軌道生成器的示例結(jié)果。

roadGenLengthMax = // end of sectionroadGenLength = // distance leftroadGenTaper = // length of taperroadGenFreqX = // X wave frequencyroadGenFreqY = // Y wave frequencyroadGenScaleX = // X wave amplituderoadGenScaleY = 0; // Y wave amplituderoadGenwidth="360px",height="auto" />

// generate the roadfor( 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="360px",height="auto" />

開(kāi)始游戲

現(xiàn)在軌道有了,剩下的啟動(dòng)過(guò)程就很簡(jiǎn)單了。我們只需要初始化幾個(gè)變量。

// reset everythingvelocity = new Vec3 ( pitchSpring = pitchSpringSpeed = pitchRoad = hueShift = 0 ); position = new Vec3(0, height); // set player start posnextCheckPoint = checkPointDistance; // init next checkpointtime = maxTime; // set the start timeheading = randSeed; // random world heading

更新玩家狀態(tài)

本節(jié)將介紹主要的更新函數(shù),它可以處理游戲中所有內(nèi)容的更新和渲染!通常,在代碼寫(xiě)一個(gè)超大的函數(shù)并不是一個(gè)好習(xí)慣,我們需要將其分解成子函數(shù)。因此,為了方便理解,下面的敘述將其分為幾部分。

首先我們需要了解玩家所在位置的道路信息。為了使物理效果和渲染感覺(jué)平滑,在當(dāng)前路段和下一個(gè)路段之間進(jìn)行了插值操作。

玩家的位置和速度是 3D 向量,并通過(guò)動(dòng)力學(xué)進(jìn)行更新以體現(xiàn)重力,阻尼和其他因素。如果玩家在道路下方,位置將被固定在地面上,并且速度會(huì)相對(duì)于法線(xiàn)反射。同樣,在地面上時(shí)會(huì)施加加速度,并且越野行駛時(shí)相機(jī)會(huì)震動(dòng)。經(jīng)過(guò)游戲測(cè)試后,我決定允許玩家在空降時(shí)仍可以進(jìn)行調(diào)整。

在此處理輸入以控制加速,剎車(chē),跳躍和轉(zhuǎn)彎。通過(guò)「mouseUpFrames」也可以檢測(cè)到雙擊。有一些代碼可以跟蹤玩家在空中停留了多少幀,以便在玩家仍然可以跳躍的時(shí)候有一個(gè)短暫的寬限期。

相機(jī)的俯仰角使用了一個(gè)簡(jiǎn)單的彈簧系統(tǒng),在玩家加速、剎車(chē)和跳躍時(shí)給人一種動(dòng)態(tài)的感覺(jué)。當(dāng)玩家駕車(chē)翻越山丘以及跳躍時(shí),攝像機(jī)也會(huì)根據(jù)道路角度傾斜。

Update=()=>{

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

// get lerped values between last and current road segmentroadX = 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 velocitylastVelocity = velocity.Add(0);velocity.y += gravity;velocity.x *= lateralDamp;velocity.z = Math.max(0, time?forwardDamp*velocity.z:0);

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

// check if on groundif (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 forceturn = Lerp(velocity.z/maxSpeed, mouseX * turnControl, 0);velocity.x += velocity.z * turn - velocity.z ** 2 * centrifugal * roadX;

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

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

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

// update headingheading = 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ù)渲染

在渲染之前,可以通過(guò)設(shè)置畫(huà)布的寬度和高度來(lái)清除畫(huà)布。這也適用于用畫(huà)布填充窗口。

我們還計(jì)算了用于將世界點(diǎn)轉(zhuǎn)換為畫(huà)布空間的投影比例。「cameraDepth」值表示攝像機(jī)的視野(FOV),本游戲中其視野為 90 度。計(jì)算公式為「1/Math.tan((fovRadians/2))」,對(duì)于 90 度的 FOV 來(lái)說(shuō),其結(jié)果正好是 1。為了保持縱橫比,投影按「c.width="360px",height="auto" />

// clear the screen and set sizec.width="360px",height="auto" />

// calculate projection scale, flip yprojectScale = (new Vec3(1,-1,1)).Multiply(c.width="360px",height="auto" />

畫(huà)出天空、太陽(yáng)和月亮

背景氛圍是通過(guò)全屏線(xiàn)性漸變繪制的,它會(huì)根據(jù)太陽(yáng)的方向更改顏色。

為了節(jié)省空間,我們使用具有透明度的全屏徑向漸變?cè)谕粋€(gè) for 循環(huán)中繪制太陽(yáng)和月亮。

線(xiàn)性和徑向漸變相結(jié)合,構(gòu)成了一個(gè)完全環(huán)繞場(chǎng)景的天空。

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

// create linear gradient for skyg = 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 polyDrawPoly(c.width="360px",height="auto" />

// draw sun and moon (0=sun, 1=moon)for( i = 2 ; i--; ){ // create radial gradient g = context.createRadialGradient( x = c.width="360px",height="auto" />

畫(huà)出山和地平線(xiàn)

山是通過(guò)在地平線(xiàn)上繪制 50 個(gè)三角形來(lái)程序化地生成的。當(dāng)我們面向太陽(yáng)時(shí),由于山處于陰影中,所以山的光線(xiàn)會(huì)更暗。此外,附近的山更暗,以模擬霧的效果。這里真正的訣竅是調(diào)整大小和顏色的隨機(jī)值以獲得良好的結(jié)果。

繪制背景的最后一部分是繪制地平線(xiàn),并用純綠色填充地平線(xiàn)的下方。

// set random seed for mountainsrandSeed = startRandSeed;

// draw mountainsfor( i = mountainCount; i--; ){ angle = ClampAngle(heading+R(19)); light = Math.cos(angle-heading); DrawPoly( x = c.width="360px",height="auto" />

// draw horizonDrawPoly( c.width="360px",height="auto" />

將路段投影到畫(huà)布空間

在渲染道路之前,我們必須首先獲取投影后的道路點(diǎn)。第一部分比較復(fù)雜,因?yàn)槲覀兊牡缆返?x 值需要轉(zhuǎn)換為世界空間位置。為了使道路看起來(lái)是彎曲的,我們將 x 值作為二階導(dǎo)數(shù)。這就是奇怪的代碼「x+=w+=」的作用。由于這種工作方式,路段并沒(méi)有固定的世界空間位置,而是基于玩家的位置重新計(jì)算每一幀。

有了世界空間位置后,我們便能夠用道路位置減去玩家位置以獲得當(dāng)前的攝像頭空間位置。代碼的其余部分實(shí)現(xiàn)了不同的變換,首先旋轉(zhuǎn)航向、俯仰角,然后進(jìn)行投影變換,使更遠(yuǎn)的東西看起來(lái)更小,最后將其映射到畫(huà)布空間。

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="360px",height="auto" />

繪制路段

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

為了創(chuàng)建道路,我們需要在每個(gè)路段上進(jìn)行 4 層渲染:地面,條紋路緣,道路本身和虛線(xiàn)白線(xiàn)。根據(jù)道路線(xiàn)段的坡度和方向?yàn)槊總€(gè)陰影著色,并根據(jù)該圖層的外觀添加一些額外的邏輯。

我們需要檢查路段是否在近/遠(yuǎn)剪輯范圍中,以防止出現(xiàn)怪異的渲染偽像。此外,還有一個(gè)很好的優(yōu)化方法,可以在道路變得很細(xì)時(shí)按距離縮小道路分辨率。這樣就在沒(méi)有明顯的質(zhì)量損失的情況下,將繪圖次數(shù)減少了一半以上,從而獲得了巨大的性能提升。

線(xiàn)框輪廓顯示了每一個(gè)被渲染的多邊形。

let segment2 = road[s+drawDistance]; // store the last segmentfor( 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="360px",height="auto" />

// curb if wide enough if (segment1.w > 400) DrawPoly(p1.x, p1.y, p1.z*(segment1.w+curbwidth="360px",height="auto" />

// save this segment segment2 = segment1; }

繪制道路上的樹(shù)和石頭

這個(gè)游戲只有兩種不同類(lèi)型的物體:樹(shù)和石頭,它們是被渲染在道路上的。首先,我們使用「R()」函數(shù)來(lái)確定是否存在對(duì)象。這是種子隨機(jī)數(shù)厲害的地方之一。我們還將使用「R()」為對(duì)象添加隨機(jī)形狀和顏色變化。

一開(kāi)始我想要其他的車(chē)輛,但如果不進(jìn)行大幅裁剪,就不能滿(mǎn)足空間限制,所以我使用風(fēng)景作為障礙。這些風(fēng)景的位置是隨機(jī)的,而且傾向于接近道路,否則他們就會(huì)變得很稀疏,而且很容易通過(guò)。為了節(jié)省空間,對(duì)象的高度也決定了對(duì)象的類(lèi)型。

在這里可以通過(guò)比較玩家和物體在 3D 空間中的位置來(lái)檢查它們之間的碰撞。當(dāng)一個(gè)物體被擊中時(shí),玩家會(huì)放慢速度,并將該物體標(biāo)記為擊中,這樣它就可以安全地通過(guò)。

為了防止物體突然出現(xiàn)在地平線(xiàn)上,透明效果會(huì)隨著距離的增加而減弱。由于我前面提到的神奇的種子隨機(jī)函數(shù),對(duì)象的形狀和顏色使用了帶有變化的梯形繪制函數(shù)。

if (R()29) // is there an object? { // player object collision check x = 2*roadwidth="360px",height="auto" />

// 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í)間,請(qǐng)求下一個(gè)更新

游戲的標(biāo)題、時(shí)間和距離是通過(guò)一個(gè)非常簡(jiǎn)單的字體渲染系統(tǒng)顯示的,該系統(tǒng)使用了我們之前設(shè)置的 DrawText 函數(shù)。在玩家點(diǎn)擊鼠標(biāo)之前,它會(huì)將標(biāo)題顯示在屏幕中央。這是我非常自豪的部分——能夠顯示游戲標(biāo)題并使用粗體的「impact」字體。如果我面臨的空間上的要求更緊一些,這些東西會(huì)是第一個(gè)被我刪掉的。

按下鼠標(biāo)后,游戲就會(huì)開(kāi)始,HUD 顯示剩余的時(shí)間和當(dāng)前距離。在這個(gè)條件語(yǔ)句塊中,時(shí)間也會(huì)被更新,因?yàn)樗辉诒荣愰_(kāi)始后才會(huì)減少。

在這個(gè)龐大的更新函數(shù)的最后,它調(diào)用「requestAnimationFrame(Update)」來(lái)觸發(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="360px",height="auto" />

requestAnimationFrame(Update); // kick off next frame

} // end of update function

最后一點(diǎn)代碼

我們需要調(diào)用一次上面巨大的更新函數(shù),以啟動(dòng)更新循環(huán)。

此外,HTML 需要一個(gè)關(guān)閉腳本標(biāo)簽來(lái)讓所有代碼實(shí)際運(yùn)行。

Update(); // kick off update loop

極致壓縮

整個(gè)游戲的業(yè)務(wù)邏輯就是如此!以下是用彩色編碼將其壓縮以顯示不同部分后的最終結(jié)果。完成所有這些工作之后,可以想象,在這樣一小段代碼中看到我的整個(gè)游戲是多么令人滿(mǎn)足。之后的 zip 操作通過(guò)消除重復(fù)的代碼將文件大小幾乎又減少了一半。

HTML – Red

函數(shù) – Orange

設(shè)置– Yellow

玩家更新 – Green

背景渲染 – Cyan

道路渲染 – Purple

對(duì)象渲染 – Pink

HUD 渲染 – Brown

說(shuō)明

其他方法也可以實(shí)現(xiàn)同時(shí)提供性能和視覺(jué)效果的 3D 渲染。如果我有更多的空間,我更愿意使用像「three.js」這樣的 WebGL API,我在我去年制作的游戲《Bogus Roads》中就用到了它。此外,因?yàn)樗褂玫氖恰竢equestAnimationFrame」,所以確實(shí)需要一些額外的代碼來(lái)確保幀速率限制在 60 fps,我將它們添加到了增強(qiáng)版本中。我更喜歡使用「requestAnimationFrame」而不是「setInterval」,因?yàn)樗匿秩窘Y(jié)果更平滑,因?yàn)樗鼘⒈淮怪蓖剑ㄗ岋@卡的運(yùn)算和顯示器刷新率一致以穩(wěn)定輸出的畫(huà)面質(zhì)量)。這段代碼的一個(gè)主要有點(diǎn)是它的兼容性非常好,可以在任何設(shè)備上運(yùn)行,不過(guò)在我那臺(tái)老舊的 iPhone 上運(yùn)行速度有點(diǎn)慢。

結(jié)語(yǔ)

讀完本文,希望大家有所收獲。

這個(gè)游戲的代碼在 GPL-3.0 開(kāi)源協(xié)議下,在 GitHub 上已經(jīng)開(kāi)源了,你可以在自己的項(xiàng)目中隨意使用它。該 repo 還包含 2k 版本,該版本在發(fā)布時(shí)僅為 2031 字節(jié)!你也可以加入一些額外的功能如音樂(lè)和音效來(lái)實(shí)現(xiàn)「增強(qiáng)」版本。(https://killedbyapixel.itch.io/hue-jumper)

GitHub地址:https://github.com/KilledByAPixel/HueJumper2k

原文鏈接:http://frankforce.com/?p=7427

以上就是關(guān)于pos機(jī)p3代碼,如何用2 KB代碼實(shí)現(xiàn)3D賽車(chē)游戲的知識(shí),后面我們會(huì)繼續(xù)為大家整理關(guān)于pos機(jī)p3代碼的知識(shí),希望能夠幫助到大家!

轉(zhuǎn)發(fā)請(qǐng)帶上網(wǎng)址:http://m.afbey.com/newsone/57842.html

你可能會(huì)喜歡:

版權(quán)聲明:本文內(nèi)容由互聯(lián)網(wǎng)用戶(hù)自發(fā)貢獻(xiàn),該文觀點(diǎn)僅代表作者本人。本站僅提供信息存儲(chǔ)空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如發(fā)現(xiàn)本站有涉嫌抄襲侵權(quán)/違法違規(guī)的內(nèi)容, 請(qǐng)發(fā)送郵件至 babsan@163.com 舉報(bào),一經(jīng)查實(shí),本站將立刻刪除。