原文:http://keithclark.co.uk/articles/js1k-2015-defender/
译者:Lenville
2015年3月16号
去年,我带着一个名为Thrust的基于8位机的游戏第一次参加JS1K。我参加2015大赛的作品是另一个复古游戏 —— 这一次的灵感来源于街机上的射击游戏 Defender。
将这样一个标志性的游戏压缩到1K并且还忠于原作,这简直是一个巨大的挑战,1024字节的限制意味着我不能加入原作的所有元素,但是我确实在不断努力尝试,保证作品的核心部分尽可能的接近原作。下面是一些我打算实现的功能:
注意:这个版本使用了一个自定义的shim通过CSS来创建一个完整的viewport,修正过的长宽比的canvas,这里的JavaScript与我在JS1K官方放置的作品使用的是同一个。如果要在官方的shim里运行这个作品,请前往JS1K的网站上体验。
正如去年一样,我非常想写一个代码没有被打包器(packer)压缩过(我就是喜欢看看这小东西是怎样工作的)的作品,并且仅用1024个字节就能够容纳这些代码。我打算用这种方式写一个能玩儿的Defender,但是看着这个小东西我并不怎么开心,因为我总想着添加更多的功能,所以我又重新开始了——这次我打算用打包器来助我一臂之力。
为了更有效率地利用打包器进行开发,我需要一个构建工具,所以我写了一个简单的grunt task来运行Uglify,把输出的结果再处理(增加我自己的优化选项),然后再将结果传递给RegPack。最终,打包好的输出被注入到2015的shim中并且保存成一个文件。这个grunt task也可以监控源代码的改变,当文件被保存的时候触发构建过程。
工具准备之后,我开始在我的原型中精挑细选,重新格式化并且展开代码让它”对打包器更友好“。紧接着,我逐步地增加新功能,例如:彩色图形、突变体,并且不断改进AI,直到代码刚好到达1K的限制。当我对我的作品感到比较满意的时候,我禁用了Uglify,并且使用编译过的输出作为我的源代码,开始亲手缩减代码规模。
写简洁的代码与写完美压缩的代码是完全不同的挑战——当你用到打包器时,一切优化都与“重复”有关。
举个例子,在原型中,我使用了一个逻辑触发器来控制主(for)循环计数器,允许为为敌人侵略者运行两遍碰撞和运动逻辑,在on的状态时,代码被用来检查碰撞,并且向玩家开火。在off状态时,同样的代码被用来捕捉人类。在最终的针对打包器优化的版本中,我放弃使用触发器从而复制整个代码块来引入重复,并且生成一个更小的输出文件
为了让RegPack输出最好的代码,我需要理解它如何加工我的代码,所以我创造了一个工具来对它所使用的压缩模式进行可视化。这个工具给RegPack(还有JSCrush)的解压循环打了一个补丁,强迫它对每一个元素中解压过的子串进行包装,然后运行打包器并把产生的结果渲染到DOM上。最终,我用一个很小的CSS来完成这么多字符串的可视化呈现,并且展示出它们是如何嵌套的。
Defender 源代码的打包模式
识别压缩好的子串使得我能够重构源代码来利用这些模式,并且可以更进一步减少文件的体积。一些简单的改动,例如在表达式中交换运算符的顺序或者尝试放置一个=0的赋值所以它进展成为一个逗号,为每一个表达式节省一到两个字节。节省下来的字节积少成多也能够带来可观的代码压缩量。
其中一个最大的重复模式是像素渲染器,它被用来绘制地面,星星,精灵和激光像素,同样的一套表达式被用来剪切,缩放,上色和绘制像素矩形,我精心打造渲染表达式来获得一组相同的特征,所以它们可以被很好地压缩。(总之就是让一段代码同时干好几件事情——译者注)
X<255&&c.fillRect(X<<1,Y<<1,2,2,c.fillStyle="hsl("+C+",99%,50%)")
我的作品里包含5个单一颜色的精灵,每个7像素宽,7像素高,限制每一个精灵为单一颜色可以将每行7像素的图像数据被存储在一个单字节中,使用它的唯一比特来决定一个像素是否应该被绘制。精灵被设计成7像素宽,是为了防止触及第8比特扩容限制——当设置为8比特或更大时会导致值被存储为16位比特。(也就是7位意味着可以用一个ASCII字符来保存一行——译者注)
按照这种方式存储数据意味着,在全部1024个字节中,我只使用了7个字节就存储了每一个精灵,但是它带来一个问题,一些图片数据包含了这些特定的比特组合,当转换成一个字符串的时候产生了不可打印的字符,为了绕过这个问题,我使用修正值69(取决于精灵生成工具)异或每一个字节来确保每一个字节包含足够高的比特来生成一个可打印的字符。
在异或处理之前和之后的敌人侵略者精灵
精灵被编码好了以后,生成的字符串看起来就像这样:
var myEncodedSprites = '[y}}U]UMA8Z8AMEMYYMEE {V:V{ {V:V{ ';
编码好的精灵数据被使用在最终的作品中
注意:我故意复制了一份侵略精灵当作突变体,在原始游戏中两个敌人长得非常像,我直接复制这个精灵可以让打包的文件更小(当然重复也会更多),这可比我写代码共享这个精灵节省了不少的字节。
demo中使用的精灵是用他们的行列数据转置器生成的,所以他们看起来像是旋转过了一样,我这样做来满足可变宽的精灵(原始游戏中有一个更宽的玩家太空船)来避免去处理8比特所带来的问题。但是不幸的是,我并没有预留足够的空间来这样做。
绘制精灵是一个简单地应用异或来移除掩码的例子,位移比特然后绘制矩形。这里是一个绘制玩家飞船的例子:
for(l=55;X=l&7,Y=l>>3,l--;)(69^'[y}}U]U'.charCodeAt(Y))>>X&1&&c.fillRect(Y,X,1,1)
精灵的颜色取决于用 hsl(i, 100%, 50%) 设置的 fillStyle 属性,在这里 色调值 i 代表了4种不同的精灵(0-4),乘以99(也是一个针对打包器优化过的值),第四个精灵产生的色调值比最大值360大,但是这不成问题,因为色调是一个角度值,所以值可以隐式地循环(因为角度是一个周期函数——译者注)。
创造一个类似于敌人爆炸和合成的可视效果,或者当玩家的精灵改变方向的时候将它镜像化是相当简单的。当每一个精灵被渲染的时候,所有的效果通过缩放的他们的坐标来实现,爆炸效果同时缩放X值和Y值,镜像效果则将X坐标乘-1来实现。当我准备好绘制每一个精灵像素的时候,在渲染他们的时候缩放需要一点额外的努力,这里是一个效果实现的例子:
// t = 0 // Sprite (0 = player, 1 = human, 2 = missle)
// d = 1 // Direction (-1 = left, 1 = right)
// w = 1 // Warp / Explosion factor (1 = normal)
// x = 100 // X position
// y = 100 // Y position
for(l=55;U=l&7,V=l>>3,X=x+d*(V-3)*w,Y=y+(U-3)*w,l--;)
(69^'[y}}U]UMA8Z8AMEMYYMEE'.charCodeAt(t*7+V))>>U&1&&c.fillRect(X,Y,1,1)
一个动态的敌人侵略者在水平面合成
在最终的demo中你可能注意到,精灵在爆炸的时候快速翻动,这是因为当爆炸的时候合成因数是一个负值,并且我需要权衡计算绝对值来节省字节。
Defender中的背景图包含了一个崎岖的波浪起伏的地面和一个视差滚动的星域,然而这些元素只是纯粹的装饰品,将他们中的每一个从我的作品中抽离出去都会使得这个游戏死气沉沉的,所以我需要将他们加入到这个游戏中去。
地面是由一个简单的余弦曲线生成的,当前的迭代循环计算进行比特掩码处理,然后传递给Math.cos(),得出的结果与之前的迭代结果进行叠加,生成一个波浪起伏的山脉。
寻找一个完美的比特掩码需要进行不断地试错,地面必须无缝地(估计作者是想说seamlessly)拼接,因为玩家可以始终在同一个方向上持续飞行,所以所有的地面生成公式必须产生相同的开始和结束值,我发现Math.cos(iteration / 5 & -11) 这个公式在无限合成的时候能够生成一个看起来合理的崎岖山脉:
for(Y=127,X=1023;X;Y+=Math.cos(X--/5&-11))c.fillRect(X,Y|0,1,1)
无缝连接的山脉地形
这个星球的宽度事实上是一个用低10位比特设置高的比特掩码(是的,又是一个掩码),宽由Math.pow(2, 10) - 1生成(作者可能拼错了,应该是derived),这个公式能够生成1023个值,这个掩码通过逻辑与的操作,来保持最终值始终在这个世界范围内,我在模量上用一比特位的操作符因为拆开比特就防止了负值的产生。(移位操作规则在最高位为“1”的时候会产生负数——译者注)
对于地面的值计算同样被应用到绘制视差星际上,X的值减少一半,生成一个更慢的滚动速度,并且Y值被扩大,与1023进行逻辑与计算,按照这种方式缩放出来的Y的位置能让绝大多数星星在画布外消失。同时也节省了额外的逻辑(和字节)来把他们沿着X轴散布开来。
这里是我的作品最终打包的源码:
for(_='s=KC=GL=B&&Q<4QP,X=`W[_=1=Math.cos(0%)")25)):c.fill,onkey99 s. {V:V{ -H+127+||(w--<1&A,Y),0,(q=-511+(E[d])Rect(x-x+511&Ar==function(k){E[0]._k.which-32]=,Style="hsl("+C+", %,-y,psqrt(q*q+r*rnatan2(r,qunvsin(nX<5QX<,Y<,2,25H=$=J=D,A023,E=[]down1}up0},setInterval(Wfor(F27,BGd=A,A;d--;){if(G`d-H=F+d/5&-11,G-F,X/=2,Y *F< Q,JQ(G5*d`(16-z)*d*Dx=y,d<Qwith(for(l=55,x+=u*g,y+=v*g,zQz--,w?(KK<-Q(A*=t>E.splice(d,1)yp<5?t>1Q:JQ-rPrPp30Qq*DQ,t?t<2?(v=s?-1:y4yQ(B5,t<3?zu+v?w=-A:(z= (zB3,O=x,N=y,z= +dsQ!wQ!(sQs!=?y-8pQ(x=x,y-=r,K):KtPE[$%d],t=K0(H=x+u*2,I=!_5]-!_7],D=I||D,u-=I?u*I<Q-I/2:u/,y+=_8]?y40:yQ-!!_6],zz6*_0]J=z>9));U=l&7,V=l>>3`x(U-3)*D*++w=y+*(V-3G *t,l--;(69^"[y}}U]UMA8Z8AMEMYYMEE".charCodeAt(7*t+V))>>U&1Q);dB$1?2-!$:$% Q4,N=70*L%24O= *$LQE.push({g:L/4,t:L-1,w:L>3Q,s:Bu:v:z:x:O,y:N})}$++},16)';g=/[- -_`PQBGK]/.exec(_);)with(_.split(g))_=join(shift());eval(_)
…当然了这里有全部注释的源代码:
/*
* Defender: 1024 by Keith Clark
*
* A 1K game inspired by the classic arcade shooter Defender. Written
* for the 2015 JS1K competition.
*
* Save the humans from alien abduction! Pilot your ship and fire its
* laser at enemy landers before they abduct and transform humans into
* mutants programmed to hunt you down!
*
* Use the arrow keys to fly and space to fire.
*
* web: keithclark.co.uk | tweet: @keithclarkcouk
*/
// Initialise any globals
H = // Camera position
$ = // Game frame ticks
J = 0, // Player fire flag
D = 1, // Player direction
A = 1023, // World width
E = [], // Entity stack
// Handle user input
// W[0] = fire [space bar]
// W[5] = thrust left [left arrow]
// W[7] = thrust right [right arrow]
// W[6] = climb [up arrow]
// W[8] = dive [down arrow]
onkeydown = function(k) { // key down state handler
W[k.which - 32] = 1; // set key flag
},
onkeyup = function(k) { // key up state handler
W[k.which - 32] = 0; // clear key flag
},
// Game loop
setInterval(W = function(k) {
for (
F = 127, // mountain range Y start position
c.fillRect(L = 0, C = 0,d = A, A, // clear the screen
c.fillStyle = "hsl(" + C + ",99%,0%)"
)
;
d-- // for 1023 cycles...
;
) {
if (
// Draw a mountain pixel
C = 25, // set colour to brown
X = d - H & A, // set pixel X position
Y = F += Math.cos(d / 5 & -11), // set pixel Y position
X < 255 && // if the pixel is in the viewport
c.fillRect(X << 1, Y << 1, 2, 2, // plot the pixel
c.fillStyle = "hsl(" + C + ",99%,50%)"
),
// Draw a starfield pixel
C = -F, // set colour to dark blue
X /= 2, // half mountain X pixel (simple parallax)
Y = 199 * F & A, // set pixel Y position
Y < 99 && X < 255 && // if the pixel is in the viewport
c.fillRect(X << 1, Y << 1, 2, 2, // plot the pixel
c.fillStyle = "hsl(" + C + ",99%,50%)"
),
// Draw a player lazer pixel
J && (
C = 5 * d, // set colour spread from red to green
X = (16 - E[0].z) * d * D - H + 127 + E[0].x & A, // set X position
Y = E[0].y, // set Y position matches player Y position
d < 25 && X < 255 && // if the pixel is in the viewport
c.fillRect(X << 1, Y << 1, 2, 2, // plot the pixel
c.fillStyle = "hsl(" + C + ",99%,50%)"
)
),
E[d] // check we have an entity to process
// Game logic
) with (E[d]) for (
l = 55, // entity sprite has is (8 * 7 - 1) bits
x += u * g, // increment entity X position
y += v * g, // increment entity Y position
z && z--, // decrement entity timer (used for fire rate / bullet life etc.)
w ? // if this entity warping (w>0) or exploding (w<0)
(
s = s.s = 0, // clear link with any other entity (lander -> human -> lander)
w-- < -25 && ( // decrement warp / explosion counter. If explosion has ended...
A *= t > 0, // stop the game if the entity is the player
E.splice(d, 1) // remove the dead entity from the game
)
)
: // the entity is out of warp and "in play"
(
q = -511 + (E[0].x - x + 511 & A), // get x delta to player (-511 to +511)
r = E[0].y - y, // get y delta to player
p = Math.sqrt(q * q + r * r), // get distance to player
n = Math.atan2(r, q), // get angle to player
p < 5 ? // if entity has hit player
t > 1 && E[0].w-- // if entity is not a human, explode player
:
J && -r < 4 && r < 4 && // if player is firing and entity inline with player
p < 130 && q * D < 1 && w--, // if entity is in front of player, explode entity
t ?
t < 2 ? // if entity is a HUMAN
(
v = s ? // if grabbed by a lander
-1 // make human climb (the lander will actually chase it)
: // if not grabbed by a lander
y < 140, // if human is in free air, fall to earth
y < 1 && (L = 5, w--) // if human was captured, kill it and spawn... a... MUTANT!
)
:
t < 3 ? // if entity is a BULLET
z || ( // if entity timer has reached 0
u + v ? // and it's moving
w = -A // instantly destroy it
:
( // if it's not moving, it's a new bullet
u = Math.cos(n), // set X direction to track player
v = Math.sin(n), // set Y direction to track player
z = 99 // set bullet life
)
)
:
( // if entity is a LANDER or a MUTANT
u = Math.cos(n), // set X direction to track player
v = Math.sin(n), // set X direction to track player
z || ( // if the timer has reached 0
L = 3, // spawn a BULLET
O = x, // spawn at entity X position
N = y, // spawn at entity Y position
z = 99 + d // reset the entity firing timer
),
s && !s.w && !(s.s && s.s != E[d]) ? // if tracking a human (a lander)
(
q = -511 + (s.x - x + 511 & A), // get x delta to human (-511 to +511)
r = s.y - 8 - y, // get y delta to human
p = Math.sqrt(q * q + r * r), // get distance to human
n = Math.atan2(r, q), // get angle to human
u = Math.cos(n), // set X direction to track human
v = Math.sin(n), // set Y direction to track human
p < 1 && ( // if lander is grabbing human
x = s.x, // align x values (prevents rounding issues)
y -= r, // align y values (prevents rounding issues)
s.s = E[d] // link the human to the lander
)
)
:
s = t < 4 && E[$ % d], // if not tracking and not a mutant - pick an entity...
s.t == 1 || (s = 0) // if it's a human, track it
)
:
( // if entity is the PLAYER
H = x + u * 2, // set the camera
I = !W[5] - !W[7], // determine X input
D = I || D, // set player direction
u -= I ? u * I < 25 && -I / 2 : u / 25, // set player X velocity
y += W[8] ? y < 140 : y && -!!W[6], // set player Y position
z || (z = 16 * W[0]), // is player able to fire?
J = z > 9 // set player firing flag
)
)
;
// Entity rendering
U = l & 7, // get sprite bit column
V = l >> 3, // get sprite bit row
X = x - H + 127 + (V - 3) * D * ++w & A, // pixel X position
Y = y + w-- * (U - 3), // pixel Y position
C = 99 * t, // set entity colour
l-- // decrement counter, ready for next bit
;
(69 ^ "[y}}U]UMA8Z8AMEMYYMEE {V:V{ {V:V{ ".charCodeAt(7 * t + V)) >> U & 1 &&
X < 255 && c.fillRect(X << 1, Y << 1, 2, 2,
c.fillStyle = "hsl(" + C + ",99%,50%)"
)
)
;
// Entity spawning
d || (
L = $ < 11 ? // first 11 cycles add player / humans
2 - !$ // player if first cycle, or 9 humans
:
$ % 99 < 1 && 4, // every 99 cycles add a lander
N = 70 * L % 240, // the spawn X position
O = 99 * $ // the spawn Y position
),
L && E.push({ // If new entity flag is set, add it
g: L / 4, // entity speed
t: L - 1, // entity type
w: L > 3 && 25, // entity warp / explosion frame
s: L = 0, // entity target (and clear entity flag)
u: 0, // entity X velocitiy
v: 0, // entity Y velocitiy
z: 0, // entity timer
x: O, // entity X position
y: N // entity Y position
})
}
$++; // increment frame counter
}, 16)
扫码关注w3ctech微信公众号
月影大大好棒!
哈哈,不是我翻译的,我只负责整理和发表,Lenville童鞋翻译得非常好~
共收到2条回复