控制复杂度一直是软件开发的核心问题之一,一代代的计算机从业者纷纷贡献着自己的智慧,试图降低程序的计算复杂度。然而,将一款 3D 赛车游戏的代码压缩到 2KB 以内,听起来是不是太夸张了?本文作者 Frank 是一名资深游戏开发者,在本文中,他详细介绍了如何灵活运用代码压缩、编译、随机数生成、代码复用、设计模式等十八般武艺仅仅通过 2KB 的代码就能实现一款强大的 3D 赛车游戏。
<body style=margin:0><canvas id=c><script>
// 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 = 500; // how wide is roadconst curbWidth = 150; // with of warning trackconst dashLineWidth = 9; // width of the dashed lineconst maxPlayerX = 2e3; // limit player offsetconst mountainCount = 30; // how many mountains are thereconst timeDelta = 1/60; // inverse frame rateconst PI = Math.PI; // shorthand for Math.PI// 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
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 =(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 ));}
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 sizecontext.fillStyle = LSHA(99,0,0,.5); // set font colorcontext.fillText(text, posX, 129); // fill textcontext.lineWidth = 3; // line widthcontext.strokeText(text, posX, 129); // outline text}
roadGenLengthMax = // end of sectionroadGenLength = // distance leftroadGenTaper = // length of taperroadGenFreqX = // X wave frequencyroadGenFreqY = // Y wave frequencyroadGenScaleX = // X wave amplituderoadGenScaleY = 0; // Y wave amplituderoadGenWidth = roadWidth; // starting road widthstartRandSeed = randSeed = Date.now(); // set random seedroad = []; // clear road// generate the roadfor( i = 0; i < roadEnd*2; ++i ) // build road past end{if (roadGenLength++ > roadGenLengthMax) // is end of section?{// calculate difficulty percentd = Math.min(1, i/maxDifficultySegment);// randomize road settingsroadGenWidth = roadWidth*R(1-d*.7,3-2*d); // road widthroadGenFreqX = R(Lerp(d,.01,.02)); // X curvesroadGenFreqY = R(Lerp(d,.01,.03)); // Y bumpsroadGenScaleX = i>roadEnd ? 0 : R(Lerp(d,.2,.6));// X scaleroadGenScaleY = R(Lerp(d,1e3,2e3)); // Y scale// apply taper and move backroadGenTaper = R(99, 1e3)|0; // random taperroadGenLengthMax = roadGenTaper + R(99,1e3); // random lengthroadGenLength = 0; // reset lengthi -= roadGenTaper; // subtract taper}// make a wavy roadx = 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 valuesp = 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 angleroad[i].a = road[i-1] ?Math.atan2(road[i-1].y-road[i].y, segmentLength) : 0;}
// 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
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 planeairFrame = 0; // reset air frames// get the dot product of the ground normal and the velocitydp = Math.cos(roadA)*velocity.y + Math.sin(roadA)*velocity.z;// bounce velocity against ground normalvelocity = new Vec3(0, Math.cos(roadA), Math.sin(roadA)).Multiply(-elasticity * dp).Add(velocity);// apply player brake and accelvelocity.z +=mouseDown? playerBrake :Lerp(velocity.z/maxSpeed, mousePressed*playerAccel, 0);// check if off roadif (Math.abs(position.x) > road[s].w){velocity.z *= offRoadDamp; // slow downpitchSpring += 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++<6 && time&& mouseDown && mouseUpFrames && mouseUpFrames<9){velocity.y += jumpAccel; // apply jump velocityairFrame = 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 timenextCheckPoint += checkPointDistance; // set next checkpointhueShift += 36; // shift hue}
// clear the screen and set sizec.width = window.innerWidth, c.height = window.innerHeight;// calculate projection scale, flip yprojectScale = (new Vec3(1,-1,1)).Multiply(c.width/2/cameraDepth);
// 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/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 gradientg = 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 polyDrawPoly(c.width/2,0,c.width/2,c.width/2,c.height,c.width/2,g);}
// 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*(.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 horizonDrawPoly(c.width/2, horizon, c.width/2, c.width/2, c.height, c.width/2,LSHA(25, 30, 95));
for( x = w = i = 0; i < drawDistance+1; ){p = new Vec3(x+=w+=road[s+i].x, // sum local road offsetsroad[s+i].y, (s+i)*segmentLength) // road y and z pos.Add(position.Multiply(-1)); // get local camera space// apply camera headingp.x = p.x*Math.cos(cameraHeading) - p.z*Math.sin(cameraHeading);// tilt camera pitch and invert zz = 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 spaceroad[s+i++].p = // projected road pointp.Multiply(new Vec3(z, z, 1)) // projection.Multiply(projectScale) // scale.Add(new Vec3(c.width/2,c.height/2)); // center on canvas}
let segment2 = road[s+drawDistance]; // store the last segmentfor( i = drawDistance; i--; ) // iterate in reverse{// get projected road pointssegment1 = road[s+i];p1 = segment1.p;p2 = segment2.p;// random seed and lightingrandSeed = startRandSeed + s + i;light = Math.sin(segment1.a) * Math.cos(heading) * 99;// check near and far clipif (p1.z < 1e5 && p1.z > 0){// fade in road resolution over distanceif (i % (Lerp(i/drawDistance,1,9)|0) == 0){// groundDrawPoly(c.width/2, p1.y, c.width/2,c.width/2, p2.y, c.width/2,LSHA(25 + light, 30, 95));// curb if wide enoughif (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 markerDrawPoly(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 enoughif ((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 segmentsegment2 = segment1;}
if (R()<.2 && s+i>29) // is there an object?{// player object collision checkx = 2*roadWidth * R(10,-10) * R(9); // choose object posconst objectHeight = (R(2)|0) * 400; // choose tree or rockif (!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 hitvelocity = velocity.Multiply(segment1.h = collisionSlow);}// draw road objectconst alpha = Lerp(i/drawDistance, 4, 0); // fade in objectif (objectHeight){// tree trunkDrawPoly(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 leavesDrawPoly(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{// rockDrawPoly(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));}}}}
if (mousePressed){time = Clamp(time - timeDelta, 0, maxTime); // update timeDrawText(Math.ceil(time), 9); // show timecontext.textAlign = 'right'; // right alignmentDrawText(0|position.z/1e3, c.width-9); // show distance}else{context.textAlign = 'center'; // center alignmentDrawText('HUE JUMPER', c.width/2); // draw title text}requestAnimationFrame(Update); // kick off next frame} // end of update function
Update(); // kick off update loop</script>
HTML – Red
函数 – Orange
设置– Yellow
玩家更新 – Green
背景渲染 – Cyan
道路渲染 – Purple
对象渲染 – Pink
HUD 渲染 – Brown
GitHub地址:https://github.com/KilledByAPixel/HueJumper2k
4 月 30 日,机器之心联合华为昇腾学院开设的线上公开课《轻松上手开源框架 MindSpore》第四课将正式开讲,主题为「k8s MindSpore Operator 介绍」,欢迎读者报名学习。