Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
個(gè)月前,JS1k游戲制作節(jié)(JS1K game jam)傳出不再舉辦消息后,許多游戲迷開始哀嚎。
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è)這樣的游戲。
他給這個(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)。”
做完這個(gè)游戲后,F(xiàn)rank 將包含了游戲的全部 JavaScript 代碼都發(fā)布在他的個(gè)人博客上,其中用到的軟件主要也是免費(fèi)或開源軟件的。游戲代碼發(fā)布在CodePen,可以在 iframe 中試玩,有興趣的朋友可以去看看。
以下是原博內(nèi)容,AI源創(chuàng)評論進(jìn)行了不改變原意的編譯:
因?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ù)一些東西,最終目的就是為了壓縮。
其實(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è)更小的最終版本… 但是,在開發(fā)過程中,我不喜歡用這個(gè)壓縮設(shè)置,因?yàn)榇a存儲在一個(gè)字符串中,所以編輯器不能正確地高亮顯示語法。
有許多常量在各方面控制著游戲。當(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)是唯一的輸入系統(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;
這個(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+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 new Vec3( this.x + v.x, this.y + v.y, this.z + v.z ));
Multiply=(v)=>(
v=v new Vec3( this.x * v.x, this.y * v.y, this.z * v.z ));
}
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
}
首先,我們必須生成完整的軌道,而且準(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 {
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 {
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++ && 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 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
}
在渲染之前,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);
空氣背景是用全屏的 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);
}
山脈是通過在地平線上畫50個(gè)三角形,然后根據(jù)程序自己生成的。
因?yàn)橛昧斯饩€照明,山脈在面對太陽時(shí)會更暗,因?yàn)樗鼈兲幱陉幱爸小4送猓浇纳矫}顏色越暗,我想以此來模擬霧氣。這里我有個(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 {
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
}
現(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 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
// 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 70 : 7) + light));
// dashed lines if wide and close enough
if ((segment1.w > 300) && (s+i)%9==0 && i DrawPoly(p1.x, p1.y, p1.z*dashLineWidth,
p2.x, p2.y, p2.z*dashLineWidth,
LSHA(70 + light));
// save this segment
segment2=segment1;
}
游戲有兩種不同類型的物體:樹和石頭。首先,我們通過使用 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
{
// 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) && Math.abs(position.z-(s+i)*segmentLength) && position.y-height {
// 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));
}
}
}
}
游戲的標(biāo)題、時(shí)間和距離是用一個(gè)非常基礎(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
這就是整個(gè)游戲啦!下方的一小段代碼就是壓縮后的最終結(jié)果,我用不同的顏色標(biāo)注了不同的部分。完成所有這些工作后,你能感受到我在2KB內(nèi)就做完了整個(gè)游戲是多么讓我滿意了嗎?而這還是在zip之前的工作,zip還可以進(jìn)一步壓縮大小。
當(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)
文: https://www.w3cplus.com/javascript/javascript-tips.html
? w3cplus.com
任何一門技術(shù)在實(shí)際中都會有一些屬于自己的小技巧。同樣的,在使用JavaScript時(shí)也有一些自己的小技巧,只不過很多時(shí)候有可能容易被大家忽略。而在互聯(lián)網(wǎng)上,時(shí)不時(shí)的有很多同行朋友會總結(jié)(或收集)一些這方面的小技巧。作為一位JavaScript的菜鳥級的同學(xué),更應(yīng)該要留意這些小技巧,因?yàn)檫@些小技巧可以在實(shí)際業(yè)務(wù)的開發(fā)中幫助我們解決問題,而且會很容易的解決問題。在這篇文章中,會整理一些大家熟悉或不熟悉的有關(guān)于JavaScript的小技巧。
先來看使用數(shù)組中常用的一些小技巧。
ES6提供了幾種簡潔的數(shù)組去重的方法,但該方法并不適合處理非基本類型的數(shù)組。對于基本類型的數(shù)組去重,可以使用... new Set()來過濾掉數(shù)組中重復(fù)的值,創(chuàng)建一個(gè)只有唯一值的新數(shù)組。
const array=[1, 1, 2, 3, 5, 5, 1]
const uniqueArray=[...new Set(array)];
console.log(uniqueArray);
> Result:(4) [1, 2, 3, 5]
這是ES6中的新特性,在ES6之前,要實(shí)現(xiàn)同樣的效果,我們需要使用更多的代碼。該技巧適用于包含基本類型的數(shù)組:undefined、null、boolean、string和number。如果數(shù)組中包含了一個(gè)object,function或其他數(shù)組,那就需要使用另一種方法。
除了上面的方法之外,還可以使用Array.from(new Set())來實(shí)現(xiàn):
const array=[1, 1, 2, 3, 5, 5, 1]
Array.from(new Set(array))
> Result:(4) [1, 2, 3, 5]
另外,還可以使用Array的.filter及indexOf()來實(shí)現(xiàn):
const array=[1, 1, 2, 3, 5, 5, 1]
array.filter((arr, index)=> array.indexOf(arr)===index)
> Result:(4) [1, 2, 3, 5]
注意,indexOf()方法將返回?cái)?shù)組中第一個(gè)出現(xiàn)的數(shù)組項(xiàng)。這就是為什么我們可以在每次迭代中將indexOf()方法返回的索引與當(dāng)索索引進(jìn)行比較,以確定當(dāng)前項(xiàng)是否重復(fù)。
在處理網(wǎng)格結(jié)構(gòu)時(shí),如果原始數(shù)據(jù)每行的長度不相等,就需要重新創(chuàng)建該數(shù)據(jù)。為了確保每行的數(shù)據(jù)長度相等,可以使用Array.fill來處理:
let array=Array(5).fill('');
console.log(array);
> Result: (5) ["", "", "", "", ""]
不使用Array.map來映射數(shù)組值的方法。
const array=[
{
name: '大漠',
email: 'w3cplus@hotmail.com'
},
{
name: 'Airen',
email: 'airen@gmail.com'
}
]
const name=Array.from(array, ({ name })=> name)
> Result: (2) ["大漠", "Airen"]
如果你想從數(shù)組末尾刪除值(刪除數(shù)組中的最后一項(xiàng)),有比使用splice()更快的替代方法。
例如,你知道原始數(shù)組的大小,可以重新定義數(shù)組的length屬性的值,就可以實(shí)現(xiàn)從數(shù)組末尾刪除值:
let array=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
console.log(array.length)
> Result: 10
array.length=4
console.log(array)
> Result: (4) [0, 1, 2, 3]
這是一個(gè)特別簡潔的解決方案。但是,slice()方法運(yùn)行更快,性能更好:
let array=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
array=array.slice(0, 4);
console.log(array);
> Result: [0, 1, 2, 3]
如果你想過濾數(shù)組中的falsy值,比如0、undefined、null、false,那么可以通過map和filter方法實(shí)現(xiàn):
const array=[0, 1, '0', '1', '大漠', 'w3cplus.com', undefined, true, false, null, 'undefined', 'null', NaN, 'NaN', '1' + 0]
array.map(item=> {
return item
}).filter(Boolean)
> Result: (10) [1, "0", "1", "大漠", "w3cplus.com", true, "undefined", "null", "NaN", "10"]
數(shù)組的slice()取值為正值時(shí),從數(shù)組的開始處截取數(shù)組的項(xiàng),如果取值為負(fù)整數(shù)時(shí),可以從數(shù)組末屬開始獲取數(shù)組項(xiàng)。
let array=[1, 2, 3, 4, 5, 6, 7]
const firstArrayVal=array.slice(0, 1)
> Result: [1]
const lastArrayVal=array.slice(-1)
> Result: [7]
console.log(array.slice(1))
> Result: (6) [2, 3, 4, 5, 6, 7]
console.log(array.slice(array.length))
> Result: []
正如上面示例所示,使用array.slice(-1)獲取數(shù)組的最后一項(xiàng),除此之外還可以使用下面的方式來獲取數(shù)組的最后一項(xiàng):
console.log(array.slice(array.length - 1))
> Result: [7]
你可能有一個(gè)很多名字組成的列表,需要過濾掉重復(fù)的名字并按字母表將其排序。
在我們的例子里準(zhǔn)備用不同版本語言的JavaScript 保留字的列表,但是你能發(fā)現(xiàn),有很多重復(fù)的關(guān)鍵字而且它們并沒有按字母表順序排列。所以這是一個(gè)完美的字符串列表(數(shù)組)來測試我們的JavaScript小知識。
var keywords=['do', 'if', 'in', 'for', 'new', 'try', 'var', 'case', 'else', 'enum', 'null', 'this', 'true', 'void', 'with', 'break', 'catch', 'class', 'const', 'false', 'super', 'throw', 'while', 'delete', 'export', 'import', 'return', 'switch', 'typeof', 'default', 'extends', 'finally', 'continue', 'debugger', 'function', 'do', 'if', 'in', 'for', 'int', 'new', 'try', 'var', 'byte', 'case', 'char', 'else', 'enum', 'goto', 'long', 'null', 'this', 'true', 'void', 'with', 'break', 'catch', 'class', 'const', 'false', 'final', 'float', 'short', 'super', 'throw', 'while', 'delete', 'double', 'export', 'import', 'native', 'public', 'return', 'static', 'switch', 'throws', 'typeof', 'boolean', 'default', 'extends', 'finally', 'package', 'private', 'abstract', 'continue', 'debugger', 'function', 'volatile', 'interface', 'protected', 'transient', 'implements', 'instanceof', 'synchronized', 'do', 'if', 'in', 'for', 'let', 'new', 'try', 'var', 'case', 'else', 'enum', 'eval', 'null', 'this', 'true', 'void', 'with', 'break', 'catch', 'class', 'const', 'false', 'super', 'throw', 'while', 'yield', 'delete', 'export', 'import', 'public', 'return', 'static', 'switch', 'typeof', 'default', 'extends', 'finally', 'package', 'private', 'continue', 'debugger', 'function', 'arguments', 'interface', 'protected', 'implements', 'instanceof', 'do', 'if', 'in', 'for', 'let', 'new', 'try', 'var', 'case', 'else', 'enum', 'eval', 'null', 'this', 'true', 'void', 'with', 'await', 'break', 'catch', 'class', 'const', 'false', 'super', 'throw', 'while', 'yield', 'delete', 'export', 'import', 'public', 'return', 'static', 'switch', 'typeof', 'default', 'extends', 'finally', 'package', 'private', 'continue', 'debugger', 'function', 'arguments', 'interface', 'protected', 'implements', 'instanceof'];
因?yàn)槲覀儾幌敫淖兾覀兊脑剂斜恚晕覀儨?zhǔn)備用高階函數(shù)叫做filter,它將基于我們傳遞的回調(diào)方法返回一個(gè)新的過濾后的數(shù)組。回調(diào)方法將比較當(dāng)前關(guān)鍵字在原始列表里的索引和新列表中的索引,僅當(dāng)索引匹配時(shí)將當(dāng)前關(guān)鍵字push到新數(shù)組。
最后我們準(zhǔn)備使用sort方法排序過濾后的列表,sort只接受一個(gè)比較方法作為參數(shù),并返回按字母表排序后的列表。
在ES6下使用箭頭函數(shù)看起來更簡單:
const filteredAndSortedKeywords=keywords
.filter((keyword, index)=> keywords.lastIndexOf(keyword)===index)
.sort((a, b)=> a < b ? -1 : 1);
這是最后過濾和排序后的JavaScript保留字列表:
console.log(filteredAndSortedKeywords);
> Result: ['abstract', 'arguments', 'await', 'boolean', 'break', 'byte', 'case', 'catch', 'char', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do', 'double', 'else', 'enum', 'eval', 'export', 'extends', 'false', 'final', 'finally', 'float', 'for', 'function', 'goto', 'if', 'implements', 'import', 'in', 'instanceof', 'int', 'interface', 'let', 'long', 'native', 'new', 'null', 'package', 'private', 'protected', 'public', 'return', 'short', 'static', 'super', 'switch', 'synchronized', 'this', 'throw', 'throws', 'transient', 'true', 'try', 'typeof', 'var', 'void', 'volatile', 'while', 'with', 'yield']
如果你定義了一個(gè)數(shù)組,然后你想清空它。通常,你會這樣做:
let array=[1, 2, 3, 4];
function emptyArray() {
array=[];
}
emptyArray();
但是,這有一個(gè)效率更高的方法來清空數(shù)組。你可以這樣寫:
let array=[1, 2, 3, 4];
function emptyArray() {
array.length=0;
}
emptyArray();
使用...運(yùn)算符,將多維數(shù)組拍平:
const arr=[1, [2, '大漠'], 3, ['blog', '1', 2, 3]]
const flatArray=[].concat(...arr)
console.log(flatArray)
> Result: (8) [1, 2, "大漠", 3, "blog", "1", 2, 3]
不過上面的方法只適用于二維數(shù)組。不過通過遞歸調(diào)用,可以使用它適用于二維以下的數(shù)組:
function flattenArray(arr) {
const flattened=[].concat(...arr);
return flattened.some(item=> Array.isArray(item)) ? flattenArray(flattened) : flattened;
}
const array=[1, [2, '大漠'], 3, [['blog', '1'], 2, 3]]
const flatArr=flattenArray(array)
console.log(flatArr)
> Result: (8) [1, 2, "大漠", 3, "blog", "1", 2, 3]
可以使用Math.max和Math.min取出數(shù)組中的最大小值和最小值:
const numbers=[15, 80, -9, 90, -99]
const maxInNumbers=Math.max.apply(Math, numbers)
const minInNumbers=Math.min.apply(Math, numbers)
console.log(maxInNumbers)
> Result: 90
console.log(minInNumbers)
> Result: -99
另外還可以使用ES6的...運(yùn)算符來完成:
const numbers=[1, 2, 3, 4];
Math.max(...numbers)
> Result: 4
Math.min(...numbers)
> > Result: 1
在操作對象時(shí)也有一些小技巧。
同樣使用ES的...運(yùn)算符可以替代人工操作,合并對象或者合并數(shù)組中的對象。
// 合并對象
const obj1={
name: '大漠',
url: 'w3cplus.com'
}
const obj2={
name: 'airen',
age: 30
}
const mergingObj={...obj1, ...obj2}
> Result: {name: "airen", url: "w3cplus.com", age: 30}
// 合并數(shù)組中的對象
const array=[
{
name: '大漠',
email: 'w3cplus@gmail.com'
},
{
name: 'Airen',
email: 'airen@gmail.com'
}
]
const result=array.reduce((accumulator, item)=> {
return {
...accumulator,
[item.name]: item.email
}
}, {})
> Result: {大漠: "w3cplus@gmail.com", Airen: "airen@gmail.com"}
不再需要根據(jù)一個(gè)條件創(chuàng)建兩個(gè)不同的對象,以使它具有特定的屬性。為此,使用...操作符是最簡單的。
const getUser=(emailIncluded)=> {
return {
name: '大漠',
blog: 'w3cplus',
...emailIncluded && {email: 'w3cplus@hotmail.com'}
}
}
const user=getUser(true)
console.log(user)
> Result: {name: "大漠", blog: "w3cplus", email: "w3cplus@hotmail.com"}
const userWithoutEmail=getUser(false)
console.log(userWithoutEmail)
> Result: {name: "大漠", blog: "w3cplus"}
你可以在使用數(shù)據(jù)的時(shí)候,把所有數(shù)據(jù)都放在一個(gè)對象中。同時(shí)想在這個(gè)數(shù)據(jù)對象中獲取自己想要的數(shù)據(jù)。在這里可以使用ES6的Destructuring特性來實(shí)現(xiàn)。比如你想把下面這個(gè)obj中的數(shù)據(jù)分成兩個(gè)部分:
const obj={
name: '大漠',
blog: 'w3cplus',
email: 'w3cplus@hotmail.com',
joined: '2019-06-19',
followers: 45
}
let user={}, userDetails={}
({name: user.name, email: user.email, ...userDetails}=obj)
> {name: "大漠", blog: "w3cplus", email: "w3cplus@hotmail.com", joined: "2019-06-19", followers: 45}
console.log(user)
> Result: {name: "大漠", email: "w3cplus@hotmail.com"}
console.log(userDetails)
> Result: {blog: "w3cplus", joined: "2019-06-19", followers: 45}
在過去,我們首先必須聲明一個(gè)對象,然后在需要?jiǎng)討B(tài)屬性名的情況下分配一個(gè)屬性。在以前,這是不可能以聲明的方式實(shí)現(xiàn)的。不過在ES6中,我們可以實(shí)現(xiàn):
const dynamicKey='email'
let obj={
name: '大漠',
blog: 'w3cplus',
[dynamicKey]: 'w3cplus@hotmail.com'
}
console.log(obj)
> Result: {name: "大漠", blog: "w3cplus", email: "w3cplus@hotmail.com"}
使用Object.prototype.toString配合閉包來實(shí)現(xiàn)對象數(shù)據(jù)類型的判斷:
const isType=type=> target=> `[object ${type}]`===Object.prototype.toString.call(target)
const isArray=isType('Array')([1, 2, 3])
console.log(isArray)
> Result: true
上面的代碼相當(dāng)于:
function isType(type){
return function (target) {
return `[object ${type}]`===Object.prototype.toString.call(target)
}
}
isType('Array')([1,2,3])
> Result: true
或者:
const isType=type=> target=> `[object ${type}]`===Object.prototype.toString.call(target)
const isString=isType('String')
const res=isString(('1'))
console.log(res)
> Result: true
當(dāng)你需要檢查某屬性是否存在于一個(gè)對象,你可能會這樣做:
var obj={
name: '大漠'
};
if (obj.name) {
console.log(true) // > Result: true
}
這是可以的,但是你需要知道有兩種原生方法可以解決此類問題。in 操作符 和 Object.hasOwnProperty,任何繼承自O(shè)bject的對象都可以使用這兩種方法。
var obj={
name: '大漠'
};
obj.hasOwnProperty('name'); // > true
'name' in obj; // > true
obj.hasOwnProperty('valueOf'); // > false, valueOf 繼承自原型鏈
'valueOf' in obj; // > true
兩者檢查屬性的深度不同,換言之hasOwnProperty只在本身有此屬性時(shí)返回true,而in操作符不區(qū)分屬性來自于本身或繼承自原型鏈。
這是另一個(gè)例子:
var myFunc=function() {
this.name='大漠';
};
myFunc.prototype.age='10 days';
var user=new myFunc();
user.hasOwnProperty('name');
> Result: true
user.hasOwnProperty('age');
> Result: false, 因?yàn)閍ge來自于原型鏈
使用Object.create(null)可以創(chuàng)建一個(gè)純對象,它不會從Object類繼承任何方法(例如:構(gòu)造函數(shù)、toString() 等):
const pureObject=Object.create(null);
console.log(pureObject); //=> {}
console.log(pureObject.constructor); //=> undefined
console.log(pureObject.toString); //=> undefined
console.log(pureObject.hasOwnProperty); //=> undefined
JavaScript中數(shù)據(jù)類型有Number、String、Boolean、Object、Array和Function等,在實(shí)際使用時(shí)會碰到數(shù)據(jù)類型的轉(zhuǎn)換。在轉(zhuǎn)換數(shù)據(jù)類型時(shí)也有一些小技巧。
布爾值除了true和false之外,JavaScript還可以將所有其他值視為“真實(shí)的”或“虛假的”。除非另有定義,JavaScript中除了0、''、null、undefined、NaN和false之外的值都是真實(shí)的。
我們可以很容易地在真和假之間使用!運(yùn)算符進(jìn)行切換,它也會將類型轉(zhuǎn)換為Boolean。比如:
const isTrue=!0;
const isFasle=!1;
const isFasle=!!0 // !0=> true,true的反即是false
console.log(isTrue)
> Result: true
console.log(typeof isTrue)
> Result: 'boolean'
這種類型的轉(zhuǎn)換在條件語句中非常方便,比如將!1當(dāng)作false。
我們可以使用運(yùn)算符+后緊跟一組空的引號''快速地將數(shù)字或布爾值轉(zhuǎn)為字符串:
const val=1 + ''
const val2=false + ''
console.log(val)
> Result: "1"
console.log(typeof val)
> Result: "string"
console.log(val2)
> Result: "false"
console.log(typeof val2)
> Result: "string"
上面我們看到了,使用+緊跟一個(gè)空的字符串''就可以將數(shù)值轉(zhuǎn)換為字符串。相反的,使用加法運(yùn)算符+可以快速實(shí)現(xiàn)相反的效果。
let int='12'
int=+int
console.log(int)
> Result: 12
console.log(typeof int)
> Result: 'number'
用同樣的方法可以將布爾值轉(zhuǎn)換為數(shù)值:
console.log(+true)
> Return: 1
console.log(+false)
> Return: 0
在某些上下文中,+會被解釋為連接操作符,而不是加法運(yùn)算符。當(dāng)這種情況發(fā)生時(shí),希望返回一個(gè)整數(shù),而不是浮點(diǎn)數(shù),那么可以使用兩個(gè)波浪號~~。雙波浪號~~被稱為按位不運(yùn)算符,它和-n - 1等價(jià)。例如, ~15=-16。這是因?yàn)? (-n - 1) - 1=n + 1 - 1=n。換句話說,~ - 16=15。
我們也可以使用~~將數(shù)字字符串轉(zhuǎn)換成整數(shù)型:
const int=~~'15'
console.log(int)
> Result: 15
console.log(typeof int)
> Result: 'number'
同樣的,NOT操作符也可以用于布爾值: ~true=-2,~false=-1。
平常都會使用Math.floor()、Math.ceil()或Math.round()將浮點(diǎn)數(shù)轉(zhuǎn)換為整數(shù)。在JavaScript中還有一種更快的方法,即使用|(位或運(yùn)算符)將浮點(diǎn)數(shù)截?cái)酁檎麛?shù)。
console.log(23.9 | 0);
> Result: 23
console.log(-23.9 | 0);
> Result: -23
|的行為取決于處理的是正數(shù)還是負(fù)數(shù),所以最好只在確定的情況下使用這個(gè)快捷方式。
如果n是正數(shù),則n | 0有效地向下舍入。如果n是負(fù)數(shù),它有效地四舍五入。更準(zhǔn)確的說,該操作刪除小數(shù)點(diǎn)后的內(nèi)容,將浮點(diǎn)數(shù)截?cái)酁檎麛?shù)。還可以使用~~來獲得相同的舍入效果,如上所述,實(shí)際上任何位操作符都會強(qiáng)制浮點(diǎn)數(shù)為整數(shù)。這些特殊操作之所以有效,是因?yàn)橐坏?qiáng)制為整數(shù),值就保持不變。
|還可以用于從整數(shù)的末尾刪除任意數(shù)量的數(shù)字。這意味著我們不需要像下面這樣來轉(zhuǎn)換類型:
let str="1553";
Number(str.substring(0, str.length - 1));
> Result: 155
我們可以像下面這樣使用|運(yùn)算符來替代:
console.log(1553 / 10 | 0)
> Result: 155
console.log(1553 / 100 | 0)
> Result: 15
console.log(1553 / 1000 | 0)
> Result: 1
有時(shí)候我們需要對一個(gè)變量查檢其是否存在或者檢查值是否有一個(gè)有效值,如果存在就返回true值。為了做這樣的驗(yàn)證,我們可以使用!!操作符來實(shí)現(xiàn)是非常的方便與簡單。對于變量可以使用!!variable做檢測,只要變量的值為:0、null、" "、undefined或者NaN都將返回的是false,反之返回的是true。比如下面的示例:
function Account(cash) {
this.cash=cash;
this.hasMoney=!!cash;
}
var account=new Account(100.50);
console.log(account.cash);
> Result: 100.50
console.log(account.hasMoney);
> Result: true
var emptyAccount=new Account(0);
console.log(emptyAccount.cash);
> Result: 0
console.log(emptyAccount.hasMoney);
> Result: false
在這個(gè)示例中,只要account.cash的值大于0,那么account.hasMoney返回的值就是true。
還可以使用!!操作符將truthy或falsy值轉(zhuǎn)換為布爾值:
!!"" // > false
!!0 // > false
!!null // > false
!!undefined // > false
!!NaN // > false
!!"hello" // > true
!!1 // > true
!!{} // > true
!![] // > true
文章主要收集和整理了一些有關(guān)于JavaScript使用的小技巧。既然是技巧在必要的時(shí)候能幫助我們快速的解決一些問題。如果你有這方面的相關(guān)積累,歡迎在下面的評論中與我們一起分享。后續(xù)將會持續(xù)更新,希望對大家有所幫助。
2024 年旋風(fēng)式巡演期間,繼專輯《Open Channel》發(fā)行之后,電子樂界著名的 Claude VonStroke(克勞德-馮-斯特羅克)通過他的新軟件和采樣公司 Imperial Dust 推出了一款尖端數(shù)字音頻工作站插件 Nasty Channel。該插件旨在為制作人提供一種非常特殊的添加失真風(fēng)格。
Barclay Crenshaw 講述了這個(gè)插件的起源故事:
“在我孩子高中的一次社交活動中,我看到一個(gè)家伙躲在房間的角落里。他穿著一件會員專用夾克,獨(dú)來獨(dú)往。他的樣子很吸引人,我就過去打招呼。后來我才知道,他是上世紀(jì) 80 年代末 IBM 某個(gè)秘密項(xiàng)目的前程序員。他因?yàn)閻焊憷习宥唤夤停F(xiàn)在正在為某個(gè)他討厭的交友軟件編寫廉價(jià)的腳本以換取現(xiàn)金。我告訴他我是做什么工作的,他問我有沒有想過做軟件。我說想。他讓我解釋所有的功能。我當(dāng)時(shí)沒多想,但十天后,他給我發(fā)了一封郵件,把我在聚會上告訴他的想法原封不動地告訴了我,并給了我一個(gè)粗糙的測試版。”
"Nasty Channel "提供了一套簡單的功能,可將聲音轉(zhuǎn)化為清脆糟糕的音頻。添加功能的聲音調(diào)色板由 Crenshaw 自己的模擬錄音室設(shè)備制作,旨在為制作人提供多功能的添加失真功能。
這次發(fā)布與最近發(fā)行的 "Open Channel "專輯不謀而合。Crenshaw 在 "The Rebel "和 "Big In the Game "等曲目中使用了新插件的測試版來完成低音失真音效。Nasty Channel 的面板也與丹尼爾-馬丁-迪亞茲(Daniel Martin Diaz)在《Open Channel》黑膠唱片封面上創(chuàng)作的圖像相呼應(yīng)。
Nasty Channel 售價(jià) 49 美元,支持 VST3 和 AU 格式插件
Barclay Crenshaw 發(fā)布加法失真插件 Nasty Channel
https://www.audioapp.cn/bbs/thread-224505-1-1.html
(出處: 音頻應(yīng)用)
*請認(rèn)真填寫需求信息,我們會在24小時(shí)內(nèi)與您取得聯(lián)系。