欧美vvv,亚洲第一成人在线,亚洲成人欧美日韩在线观看,日本猛少妇猛色XXXXX猛叫

新聞資訊

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

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

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

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

    他給這個游戲取名 Hue Jumper。關于名字的由來,Frank 表示,游戲的核心操作是移動。當玩家通過一個關卡時,游戲世界就會換一個顏色色調。“在我想象中,每通過過一個關卡,玩家都會跳轉到另一個維度,有著完全不同的色調。”

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

    以下是原博內容,AI源創評論進行了不改變原意的編譯:

    確定最高目標

    因為嚴格的大小限制,我需要非常仔細對待我的程序。我的總體策略是盡可能保持一切簡單,為最終目標服務。

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

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

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

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

    HTML

    其實我的游戲很少使用 html ,因為它主要用到的是 JavaScript 。但這是創建全屏畫布 Canvas ,也能將畫布 Canvas 設為窗口內部大小的代碼最小方法。我不知道為什么在 CodePen 上有必要添加 overflow:hiddento the body,當直接打開時按理說也可以運行。

    我將 JavaScript 封裝在一個 onload 調用,得到了一個更小的最終版本… 但是,在開發過程中,我不喜歡用這個壓縮設置,因為代碼存儲在一個字符串中,所以編輯器不能正確地高亮顯示語法。

    常量

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

    // 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

    鼠標控制

    鼠標是唯一的輸入系統。通過這段代碼,我們可以跟蹤鼠標點擊和光標位置,位置顯示為-1到1之間的值。

    雙擊是通過 mouseUpFrames 實現的。mousePressed 變量只在玩家第一次點擊開始游戲時使用這么一次。

    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;

    數學函數

    這個游戲使用了一些函數來簡化代碼和減少重復,一些標準的數學函數用于 Clamp 和 Lerp 值。 ClampAngle 是有用的,因為它在 -PI 和 PI 之間 wrap angles,在許多游戲中已經廣泛應用。

    R函數就像個魔術師,因為它生成隨機數,通過取當前隨機數種子的正弦,乘以一個大數字,然后看分數部分來實現的。其實有很多方法可以做到,但這是最小的方法之一,而且對我們來說也是足夠隨機。

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

    因為這是一個“真正的3D”游戲,所以有一個 3D vector class 非常有用,它也能減少代碼量。這個 class 只包含這個游戲必需的基本元素,一個帶有加法和乘法函數的 constructor 可以接受標量或向量參數。為了確定標量是否被傳入,我們只需檢查它是否小于一個大數。更正確的方法是使用 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 ));

    }

    Render Functions渲染函數

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

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

    DrawText 則用于顯示時間、距離和游戲標題等文本渲染。

    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

    }

    設計軌道

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

    atan2 函數可以用來計算道路俯仰角,據此來設計物理運動和光線。

    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;

    }

    啟動游戲

    現在跑道就緒,我們只需要預置一些變量就可以開始游戲了。

    // 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

    更新玩家

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

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

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

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

    當玩家加速、剎車和跳躍時,我通過spring system展示相機的俯仰角以給玩家動態運動的感覺。此外,當玩家駕車翻越山丘或跳躍時,相機還會隨著道路傾斜而傾斜。

    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 每當高度或寬度被重設時,畫布內容就會被清空。這也適用于自適應窗口的畫布。

    我們還計算了將世界點轉換到畫布的投影比例。cameraDepth 值代表攝像機的視場(FOV)。這個游戲是90度。計算結果是 1/Math.tan(fovRadians/2) ,FOV 是90度的時候,計算結果正好是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 (徑向漸變)繪制的,它還會根據太陽的位置改變顏色。

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

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

    // 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個三角形,然后根據程序自己生成的。

    因為用了光線照明,山脈在面對太陽時會更暗,因為它們處于陰影中。此外,越近的山脈顏色越暗,我想以此來模擬霧氣。這里我有個訣竅,就是微調大小和顏色的隨機值。

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

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

    將路段投影到畫布空間

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

    一旦我們有了世界空間位置,我們就可以從道路位置中知道玩家的位置,從而得到本地攝像機空間位置。代碼的其余部分,首先通過旋轉標題、俯仰角來應用變換,然后通過投影變換,做到近大遠小的效果,最后將其移動到畫布空間。

    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

    }

    繪制路段

    現在我們有了每個路段的畫布空間點,渲染就相當簡單了。我們需要從后向前畫出每一個路段,或者更具體地說,連接上一路段的梯形多邊形。

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

    有必要檢查路段是在近還是遠剪輯范圍,以防止渲染出現 bug 。此外,還有一個很好的優化方法是,當道路變得很窄時,可以通過 distance 來減小道路的分辨率。如此,不僅減少了 draw count 一半以上,而且沒有明顯的質量損失,這是一次性能勝利。

    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 函數來確定是否加一個對象。這是隨機數和隨機數種子特別有意思的地方。我們還將使用 R 為對象隨機添加不同的形狀和顏色。

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

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

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

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

    畫上 HUD,更新時間,請求下一次更新

    游戲的標題、時間和距離是用一個非常基礎的字體渲染系統顯示出來的,就是之前設置的 DrawText 函數。在玩家點擊鼠標之前,它會在屏幕中央顯示標題。

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

    在這個 massive Update function 結束后,它調用 requestAnimationFrame (Update) 來觸發下一次更新。

    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 需要一個結束腳本標簽來讓所有的代碼能夠跑起來。

    Update; // kick off update loop

    壓縮

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

    警告 Caveats

    當然,還有很多其他 3D 渲染方法可以同時保證性能和視覺效果。如果我有更多的可用空間,我會更傾向于使用一個 WebGL API 比如 three.js ,我在去年制作的一個類似游戲“Bogus Roads”中用過這個框架。此外,因為它使用的是 requestAnimationFrame ,所以需要一些額外的代碼來確保幀速率不超過60 fps,增強版本中我會這么用,盡管我更喜歡使用 requestAnimationFrame 而不是 setInterval ,因為它是垂直同期的(VSyn,VerticalSynchronization),所以渲染更絲滑。這種代碼的一個主要好處是它非常兼容,可以在任何設備上運行,盡管在我舊 iPhone 上運行有點慢。

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

    后記

    雷鋒網

    作者:mathe,騰訊QQ音樂前端開發工程師

    正則表達式具有偉大技術發明的一切特點,它簡單、優美、功能強大、妙用無窮。對于很多實際工作來講,正則表達式簡直是靈丹妙藥,能夠成百倍地提高開發效率和程序質量。

    1. 正則常見規則

    1.1 字符匹配

    字符說明\轉義符\d[0-9]。表示是一位數字。\D[^0-9]。表示除數字外的任意字符。\w[0-9a-zA-Z_]。表示數字、大小寫字母和下劃線。\W[^0-9a-zA-Z_]。非單詞字符。\s[\t\v\n\r\f]。表示空白符,包括空格、水平制表符、
    垂直制表符、換行符、回車符、換頁符。
    \S[^\t\v\n\r\f]。非空白符。.[^\n\r]。通配符,表示幾乎任意字符。
    換行符、回車符、行分隔符和段分隔符除外。
    \uxxxx查找以十六進制數 xxxx 規定的 Unicode 字符。\f匹配一個換頁符 (U+000C)。\n匹配一個換行符 (U+000A)。\r匹配一個回車符 (U+000D)。\t匹配一個水平制表符 (U+0009)。\v匹配一個垂直制表符 (U+000B)。>\0匹配 NULL(U+0000)字符, 不要在這后面跟其它小數,因為 \0是一個<匹配 null(u+0000)字符,="" 不要在這后面跟其它小數,因為="">\0匹配 NULL(U+0000)字符, 不要在這后面跟其它小數,因為 \0是一個<>
    八進制轉義序列。[\b]匹配一個退格(U+0008)。(不要和\b 混淆了。)[abc]any of a, b, or c[^abc]not a, b, or c[a-g]character between a & g

    1.2 位置匹配

    字符說明\b是單詞邊界,具體就是\w 和\W 之間的位置,也包括\w 和 ^ 之間的位置,
    也包括\w 和之間的位置。具體說來就是與、與、與,與之間的位置。\B是\b 的反面的意思,非單詞邊界。例如在字符串中所有位置中,扣掉\b,
    剩下的都是\B 的。
    ^abc$字符串開始、結束的位置

    1.3 組

    字符說明(abc)capture group,捕獲組\nbackreference to group #n,分組引用,引用第 n 個捕獲組匹配的內容,
    其中 n 是正整數
    (?:abc)non-capturing group,非捕獲組

    1.4 先行斷言

    字符說明a(?=b)positive lookahead,先行斷言,a 只有在 b 前面才匹配a(?!b)negative lookahead,先行否定斷言,a 只有不在 b 前面才匹配

    1.5 后行斷言

    字符說明(?<=b)apositive lookbehind,后行斷言,a 只有在 b 后面才匹配(?<!b)anegative lookbehind,后行否定斷言,a 只有不在 b 后面才匹配

    1.6 量詞和分支

    字符說明a*0 or morea+1 or morea?0 or 1a{5}exactly fivea{2,}two or morea{1,3}between one & threea+?
    a{2,}?match as few as possible,惰性匹配,就是盡可能少的匹配

    以下都是惰性匹配:
    {m,n}?
    {m,}?
    ??
    +?
    *?

    1.7 分支

    字符說明ab|cdmatch ab or cd,匹配'ab'或者'cd'字符子串

    1.8 修飾符

    字符說明i執行對大小寫不敏感的匹配。g執行全局匹配(查找所有匹配而非在找到第一個匹配后停止)。m執行多行匹配。u開啟"Unicode 模式",用來正確處理大于\uFFFF 的 Unicode 字符。也就是說,會正確處理四個字節的 UTF-16 編碼。s允許 . 匹配換行符。yy 修飾符的作用與 g 修飾符類似,也是全局匹配,后一次匹配都從上一次匹配成功的下一個位置開始。不同之處在于,g 修飾符只要剩余位置中存在匹配就可,而 y 修飾符確保匹配必須從剩余的第一個位置開始,這也就是"粘連"的涵義

    2. 運算符優先級

    運算符描述\轉義符(), (?:), (?=), []圓括號和方括號*, +, ?, {n}, {n,}, {n,m}限定符^, $, \任何元字符、任何字符定位點和序列(即:位置和順序)|替換,"或"操作
    字符具有高于替換運算符的優先級,使得"m|food"匹配"m"或"food"。若要匹配"mood"或"food",請使用括號創建子表達式,從而產生"(m|f)ood"。

    3. 正則回溯

    3.1 什么是回溯算法

    以下是來自摘自維基百科的部分解釋:

    回溯法是一種通用的計算機算法,用于查找某些計算問題的所有(或某些)解決方案,特別是約束滿足問題,逐步構建候選解決方案,并在確定候選不可能時立即放棄候選("回溯")完成有效的解決方案。

    回溯法通常用最簡單的遞歸方法來實現,在反復重復上述的步驟后可能出現兩種情況:

    找到一個可能存在的正確的答案

    在嘗試了所有可能的分步方法后宣告該問題沒有答案

    在最壞的情況下,回溯法會導致一次復雜度為指數時間的計算。

    3.2 什么是正則回溯

    正則引擎主要可以分為兩大類:一種是 DFA(Deterministic finite automaton 確定型有窮自動機),另一種是 NFA(NFA Non-deterministic finite automaton  非確定型有窮自動機)。NFA 速度較 DFA 更慢,并且實現復雜,但是它又有著比 DFA 強大的多的功能,比如支持反向引用等。像 javaScript、java、php、python、c#等語言的正則引擎都是 NFA 型,NFA 正則引擎的實現過程中使用了回溯。

    3.2.1 沒有回溯的正則

    舉一個網上常見的例子,正則表達式/ab{1,3}c/g 去匹配文本'abbc',我們接下來會通過 RegexBuddy 分析其中的匹配過程,后續的一個章節有關于 RegexBuddy 的使用介紹。

    如上圖所示,讓我們一步一步分解匹配過程:

    1. 正則引擎先匹配 a。
    2. 正則引擎盡可能多地(貪婪)匹配 b。
    3. 正則引擎匹配 c,完成匹配。

    在這之中,匹配過程都很順利,并沒發生意外(回溯)。

    3.2.2 有正則回溯的正則

    讓我們把上面的正則修改一下,/ab{1,3}c/g 改成/ab{1,3}bc/g,接下再通過 RegexBuddy 查看分析結果。

    我們再一步一步分解匹配過程:

    1. 正則引擎先匹配 a。
    2. 正則引擎盡可能多地(貪婪)匹配 b{1,3}中的 b。
    3. 正則引擎去匹配 b,發現沒 b 了,糟糕!趕緊回溯!
    4. 返回 b{1,3}這一步,不能這么貪婪,少匹配個 b。
    5. 正則引擎去匹配 b。
    6. 正則引擎去匹配 c,完成匹配。

    以上,就是一個簡單的回溯過程。

    3.3 正則回溯的幾種常見形式

    從上面發生正則回溯的例子可以看出來,正則回溯的過程就是一個試錯的過程,這也是回溯算法的精髓所在。回溯會增加匹配的步驟,勢必會影響文本匹配的性能,所以,要想提升正則表達式的匹配性能,了解回溯出現的場景(形式)是非常關鍵的。

    3.3.1 貪婪量詞

    在 NFA 正則引擎中,量詞默認都是貪婪的。當正則表達式中使用了下表所示的量詞,正則引擎一開始會盡可能貪婪的去匹配滿足量詞的文本。當遇到匹配不下去的情況,就會發生回溯,不斷試錯,直至失敗或者成功。

    量詞說明a*0 or morea+1 or morea?0 or 1a{5}exactly fivea{2,}two or morea{1,3}between one & three

    當多個貪婪量詞挨著存在,并相互有沖突時,秉持的是"先到先得"的原則,如下所示:

    let string = "12345";
    
    let regex = /(\d{1,3})(\d{1,3})/;
    console.log( string.match(regex) );
    // => ["12345", "123", "45", index: 0, input: "12345"]
    

    3.3.2 惰性量詞

    貪婪是導致回溯的重要原因,那我們盡量以懶惰匹配的方式去匹配文本,是否就能避免回溯了呢?答案是否定的。

    讓我們還是看回最初的例子,/ab{1,3}c/g 去匹配 abbc。接下來,我們再把正則修改一下,改成/ab{1,3}?c/g 去匹配 abbc,以懶惰匹配的方式去匹配文本,RegexBuddy 執行步驟如下圖所示:

    1. 正則引擎先匹配 a。
    2. 正則引擎盡可能少地(懶惰)匹配 b{1,3}中的 b。
    3. 正則引擎去匹配 c,糟糕!怎么有個 b 擋著,匹配不了 c 啊!趕緊回溯!
    4. 返回 b{1,3}這一步,不能這么懶惰,多匹配個 b。
    5. 正則引擎再去匹配 c,糟糕!怎么還有 b 擋著,匹配不了 c 啊!趕緊回溯!
    6. 返回 b{1,3}這一步,不能這么懶惰,再多匹配個 b。
    7. 正則引擎再去匹配 c,匹配成功,棒棒噠!

    本來是好端端不會發生回溯的正則,因為使用了惰性量詞進行懶惰匹配后,反而產生了回溯了。所以說,惰性量詞也不能瞎用,關鍵還是要看場景。

    3.3.3 分組

    分支的匹配規則是:按照分支的順序逐個匹配,當前面的分支滿足要求了,則舍棄后面的分支。

    舉個簡單的分支栗子,使用正則表達式去匹配 /abcde|abc/g 文本 abcd,還是通過 RegexBuddy 查看執行步驟:

    1. 正則引擎匹配 a。
    2. 正則引擎匹配 b。
    3. 正則引擎匹配 c。
    4. 正則引擎匹配 d。
    5. 正則引擎匹配 e,糟糕!下一個并不是 e,趕緊回溯
    6. 上一個分支走不通,切換分支,第二個分支正則引擎匹配 a。
    7. 第二個分支正則引擎匹配 b。
    8. 第二個分支正則引擎匹配 c,匹配成功!

    由此,可以看出,分組匹配的過程,也是個試錯的過程,中間是可能產生回溯的。

    4. 正則的分析與調試

    RegexBuddy 是個十分強大的正則表達式學習、分析及調試工具。RegexBuddy 支持 C++、Java、JavaScript、Python 等十幾種主流編程語言。通過 RegexBuddy,能看到正則一步步創建的過程。結合測試文本,你能看到正則一步步執行匹配的過程,這對于理解正則回溯和對正則進行進一步優化,都有極大的幫助。

    4.1 安裝分析調試工具

    可以在 RegexBuddy 的官方網站下載及獲取 RegexBuddy。

    下載完后,一步步點擊安裝即可。

    4.2 工具界面介紹

    下圖便是 RegexBuddy 界面的各個面板及相關功能。

    4.3 創建正則

    為了方便使用,可以在布局設置那里將布局設置成 Side by Side Layout。

    在正則輸入區輸入你的正則 regex1,查看 Create 面板,就會發現面板上顯示了正則的創建過程(或者說是匹配規則),在 Test 面板區域輸入你的測試文本,滿足 regex1 匹配規則的部分會高亮顯示,如下圖所示。

    4.4 使用 RegexBuddy 的 Debug 功能

    選中測試文本,點擊 debug 就可以進入 RegexBuddy 的 debug 模式,個人覺得這是 RegexBuddy 最強大地方,因為它可以讓你清楚地知道你輸入的正則對測試文本的匹配過程,執行了多少步,哪里發生了回溯,哪里需要優化,你都能一目了然。

    4.5 使用 RegexBuddy 的 Library 功能

    RegexBuddy 的正則庫內置了很多常用正則,日常編碼過程中需要的很多正則表達式都能在該正則庫中找到。

    4.6 更多工具推薦

    • 正則可視化-regexper
    • 正則可視化-regulex
    • 正則在線調試

    5. 正則性能優化

    正則是個很好用的利器,如果使用得當,如有神助,能省掉大量代碼。當如果使用不當,則是處處埋坑。所以,本章節的重點就是總結如何寫一個高性能的正則表達式。

    5.1 避免量詞嵌套

    舉個簡單的例子對比:

    我們使用正則表達式/a*b/去匹配字符串 aaaaa,看下圖 RegexBuddy 的執行過程:

    我們將以上正則修改成/(a*)*b/去匹配字符串 aaaaa,再看看 RegexBuddy 的執行結果過程:

    以上兩個正則的基本執行步驟可以簡單認為是:

    1. 貪婪匹配
    2. 回溯
    3. 直至發現匹配失敗

    但令人驚奇的是,第一個正則的從開始匹配到匹配失敗這個過程只有 14 步。而第二個正則卻有 128 步之多。可想而知,嵌套量詞會大大增加正則的執行過程。因為這其中進行了兩層回溯,這個執行步驟增加的過程就如同算法復雜度從 O(n)上升到 O(n^2)的過程一般。

    所以,面對量詞嵌套,我們需作出適當的轉化消除這些嵌套:

    (a*)* <=> (a+)* <=> (a*)+ <=> a*
    (a+)+ <=> a+
    

    5.2 使用非捕獲組

    NFA 正則引擎中的括號主要有兩個作用:

    1. 主流功能,提升括號中內容的運算優先級
    2. 反向引用

    反向引用這個功能很強大,強大的代價是消耗性能。所以,當我們如果不需要用到括號反向引用的功能時,我們應該盡量使用非捕獲組,也就是:

    // 捕獲組與非捕獲組
    () => (?:)
    

    5.3 分支優化

    分支也是導致正則回溯的重要原因,所以,針對正則分支,我們也需要作出必要的優化。

    5.3.1 減少分支數量

    首先,需要減少分支數量。比如不少正則在匹配 http 和 https 的時候喜歡寫成:

    /^http|https/
    

    其實上面完全可以優化成:

    /^https?/
    

    這樣就能減少沒必要的分支回溯

    5.3.2 縮小分支內的內容

    縮小分支中的內容也是很有必要的,例如我們需要匹配 this 和 that ,我們也許會寫成:

    /this|that/
    

    但上面其實完全可以優化成

    /th(?:is|at)/
    

    有人可能認為以上沒啥區別,實踐出真知,讓我們用以上兩個正則表達式去匹配一下 that。

    我們會發現第一個正則的執行步驟比第一個正則多兩步,那是因為第一個正則的回溯路徑比第二個正則的回溯路徑更長了,最終導致執行步驟變長。

    5.4 錨點優化

    在能使用錨點的情況下盡量使用錨點。大部分正則引擎會在編譯階段做些額外分析, 判斷是否存在成功匹配必須的字符或者字符串。類似^、$ 這類錨點匹配能給正則引擎更多的優化信息。

    例如正則表達式 hello(hi)?$ 在匹配過程中只可能從字符串末尾倒數第 7 個字符開始, 所以正則引擎能夠分析跳到那個位置, 略過目標字符串中許多可能的字符, 大大提升匹配速度。

    6. 結語

    曾經有一次因為寫一個性能惡劣的正則表達式,導致代碼執行過程因為性能問題掛掉。于是下定決心要把正則表達式搞明白,看了不少文章書籍,做了不少練習之后,總算摸到了些門道,也真真切切體會到正則表達式的優美和強大。寫下此文,記錄下一些學習心得和總結,望批評指正,共同進步。

    7. 參考

    • 正則表達式中的悲觀回溯
    • 小心別落入正則回溯陷阱
    • 正則匹配原理解析
    • learncodethehardway
    • 正則表達式系列總結
    • wikipedia Backtracking
    • 精通正則表達式
網站首頁   |    關于我們   |    公司新聞   |    產品方案   |    用戶案例   |    售后服務   |    合作伙伴   |    人才招聘   |   

友情鏈接: 餐飲加盟

地址:北京市海淀區    電話:010-     郵箱:@126.com

備案號:冀ICP備2024067069號-3 北京科技有限公司版權所有