几个月前,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 调用,得到了一个更小的最终版本…< body style = margin:0 onload = " code _ goes _ here " > < canvas id = c >但是,在开发过程中,我不喜欢用这个压缩设置,因为代码存储在一个字符串中,所以编辑器不能正确地高亮显示语法。
 
<body 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 < roadEnd*2; ++i )          // build road past end
 
{
 
if (roadGenLength++ > roadGenLengthMax) // is end of section?
 
{
 
// calculate difficulty percent
 
d = Math.min(1, i/maxDifficultySegment);
 
// randomize road settings
 
roadGenWidth = roadWidth*R(1-d*.7,3-2*d);        // road width
 
roadGenFreqX = R(Lerp(d,.01,.02));               // X curves
 
roadGenFreqY = R(Lerp(d,.01,.03));               // Y bumps
 
roadGenScaleX = i>roadEnd ? 0 : R(Lerp(d,.2,.6));// X scale
 
roadGenScaleY = R(Lerp(d,1e3,2e3));              // Y scale
 
// apply taper and move back
 
roadGenTaper = R(99, 1e3)|0;                 // random taper
 
roadGenLengthMax = roadGenTaper + R(99,1e3); // random length
 
roadGenLength = 0;                           // reset length
 
i -= roadGenTaper;                           // subtract taper
 
}
 
// make a wavy road
 
x = Math.sin(i*roadGenFreqX) * roadGenScaleX;
 
y = Math.sin(i*roadGenFreqY) * roadGenScaleY;
 
road[i] = road[i]? road[i] : {x:x, y:y, w:roadGenWidth};
 
// apply taper from last section and lerp values
 
p = Clamp(roadGenLength / roadGenTaper, 0, 1);
 
road[i].x = Lerp(p, road[i].x, x);
 
road[i].y = Lerp(p, road[i].y, y);
 
road[i].w = i > roadEnd ? 0 : Lerp(p, road[i].w, roadGenWidth);
 
// calculate road pitch angle
 
road[i].a = road[i-1] ?
 
Math.atan2(road[i-1].y-road[i].y, segmentLength) : 0;
 
}
 
启动游戏
 
现在跑道就绪,我们只需要预置一些变量就可以开始游戏了。
 
// 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展示相机的俯仰角以给玩家动态运动的感觉。此外,当玩家驾车翻越山丘或跳跃时,相机还会随着道路倾斜而倾斜。