如何在类似街机的游戏中实现环绕效果?
How can I achieve a wraparound effect in an arcade-like game?
我正在制作游戏的克隆 Spacewar!。在游戏中,船只可以行驶到地图的边缘并环绕到另一侧,有时会分成两半,屏幕的每一侧各一半(当船只进入角落时,它们会分成四部分)。您可以玩游戏 here。
现在我正在使用取模运算符将 div
环绕在屏幕周围,但它并没有像我希望的那样将块分成两半。在 JavaScript 或 jQuery 中有什么方法可以做到这一点?
这取决于您如何渲染飞船。我假设你正在使用精灵。让:
xs,ys
为目标屏幕分辨率
txs,tys
精灵分辨率
x0,y0
是精灵位置(左上角)
x
-轴向右,y
-轴向下,(0,0)
是展台精灵和屏幕的左上角
我知道有两种基本方法:
直接在精灵渲染中包裹坐标。
这是最简单的方法,但您需要访问精灵渲染器。找到它们应该如下所示的渲染循环:
for (y=y0,ty=0;ty<tys;ty++,y++)
for (x=x0,tx=0;tx<txs;tx++,x++)
{
color=sprite[ty][tx];
if (color!=color_transparent) screen[y][x]=color;
}
所以只需添加 x,y
包装即可:
while (x0>=xs) x0-=xs; // just normalize start position
while (x0< 0) x0+=xs;
while (y0>=ys) y0-=ys;
while (y0< 0) y0+=ys;
for (y=y0,ty=0;ty<tys;ty++,y++)
{
if (y>=ys) y=0; // wrap y
for (x=x0,tx=0;tx<txs;tx++,x++)
{
if (x>=xs) x=0; // wrap x
color=sprite[ty][tx];
if (color!=color_transparent) screen[y][x]=color;
}
}
当然,渲染循环中的这些条件会稍微减慢速度。因此,您可以通过将代码复制到 4 个实例中进行优化,每个实例都在其自己的一组坐标上,这样 if
就不再位于循环内。
分割包裹的精灵
这种方法要快一些(在渲染循环中不需要额外的 if
s)并且不需要对渲染代码进行任何篡改。这个想法是像往常一样渲染非包裹的精灵,如果检测到包裹渲染 2/4
包裹位置的精灵副本
所以在代码中应该是这样的:
// just normalize start position
while (x0>=xs) x0-=xs;
while (x0< 0) x0+=xs;
while (y0>=ys) y0-=ys;
while (y0< 0) y0+=ys;
render_sprite(x0,y0,sprite);
// compute copies coordinates
x1=x0; if (x1+txs>xs) x1-=xs;
y1=y0; if (y1+tys>ys) y1-=ys;
// render copies
if (x0!=x1) render_sprite(x1,y0,sprite);
if (y0!=y1) render_sprite(x0,y1,sprite);
if ((x0!=x1)&&(y0!=y1)) render_sprite(x1,y1,sprite);
顺便说一句。如果您正在考虑使用这种方法,可以在几何 GLSL 着色器中完成。
包裹精灵
您提供的 link 很有趣。他们已经实施了完整的 CPU 仿真和 运行 汇编编写的游戏。
提高模数
无论如何,如果您使用 canvas 来渲染 sprite(图像),最简单的方法是使用简单的模数并进行修改以处理负值。
当使用负值时,正常模数会失效。
x = 100 % 200; // 100
x = 300 % 200; // 100
x = -100 % 200; // -100 the wrong sign should be 100
x = -50 % 200; // -50 wrong sign wrong direction should be 150
您需要模数以始终 return 正确方向上的正值。要处理负数,请两次取模,第一个将在您想要的范围内,但在 +/- 范围内。然后通过添加范围使负为正。然后再做一次模运算。
var range = 200;
var x = 150;
x = ((x % range) + range) % range; // result 150
x = -50;
x = ((x % range) + range) % range; // result 150 correct.
简单包装
使用上面的模算法,检查边界并根据需要渲染精灵是一件简单的事情。
// assuming ctx is canvas context 2D
// canvas is the canvas.
// img is a HTMLImageElement
var canW = canvas.width; // the canvas size
var canH = canvas.height;
// draw a sprite that is wrapped around the edges of the canvas
function drawWrappedSprite(img,x,y){
x1 = ((x % canW) + canW) % canW; // wrap around correcting for negative values
y1 = ((y % canH) + canH) % canH;
x2 = x1 + img.width; // get right and bottom
y2 = y1 + img.height;
ctx.drawImage(img, x1, y1); // draw first copy
if(x2 > canW){ // check if touching right side
ctx.drawImage(img, x1 - canW, y1); // draw left copy
if(y2 > canH){ // check if touching bottom
ctx.drawImage(img, x1 - canW, y1 - canH); // draw top copy
}
}
if(y2 > canH){
ctx.drawImage(img, x1 , y1 - canH); // draw top copy
}
}
包裹旋转精灵
由于游戏中有旋转的精灵,上述功能将不起作用,因为旋转会改变精灵的大小。要处理旋转的精灵,您需要检查它的最大尺寸。这是精灵对角线的长度,可以用 sqrt(width * width + height * height)
找到
假设您希望精灵围绕其中心旋转,您可以通过在 x,y 中心位置减去和添加对角线的一半来找到精灵最大范围(顶部、底部、左侧和右侧)。与第一个功能一样,进行取模并根据需要绘制精灵。
在某些情况下,即使精灵不可见,也会在对面绘制精灵。如果您正在绘制大量精灵(超过 100 个),您可能希望获得准确范围而不是最大范围,但是您必须至少变换精灵的一个角以获得水平和垂直范围。然后只需使用这些值而不是函数中的 diag
。
// assuming ctx is canvas context 2D
// canvas is the canvas.
// img is a HTMLImageElement
var canW = canvas.width; // the canvas size
var canH = canvas.height;
// draws sprite rotated about its center by r
function drawWrappedRotatedSprite(img,x,y,r){ // x,y center pos, r rotation in radians
var diag = Math.sqrt(img.width * img.width + img.height * img.height); // get diagonal size
diag /= 2; // half the diagonal
x1 = (((x - diag) % canW) + canW) % canW; // get left extent position and
y1 = (((y - diag) % canH) + canH) % canH; // wrap around correcting for negative values
var w = - img.width / 2; // get image width height
var h = - img.height / 2 // offset in rotated space
x2 = x1 + diag * 2; // get right and bottom max extent
y2 = y1 + diag * 2;
ctx.setTransform(1,0,0,1,x1 + diag, y1 + diag); // set origin
ctx.rotate(r); // set rotation
ctx.drawImage(img, w, h); // draw image rotated around its center
if(x2 > canW){ // check if touching right side
ctx.setTransform(1,0,0,1,x1 + diag - canW, y1 + diag); // set origin
ctx.rotate(r); // set rotation
ctx.drawImage(img,w,h); // draw image rotated around its center
if(y2 > canH){ // check if touching bottom
ctx.setTransform(1,0,0,1,x1 + diag - canW, y1 + diag - canH); // set origin
ctx.rotate(r); // set rotation
ctx.drawImage(img,w,h); // draw image rotated around its center
}
}
if(y2 > canH){
ctx.setTransform(1,0,0,1,x1 + diag, y1 + diag - canH); // set origin
ctx.rotate(r); // set rotation
ctx.drawImage(img,w,h); // draw top image rotated around its center
}
ctx.setTransform(1,0,0,1,0,0); // reset the transform (should only do this after all sprites
// using this function have been drawn
}
我正在制作游戏的克隆 Spacewar!。在游戏中,船只可以行驶到地图的边缘并环绕到另一侧,有时会分成两半,屏幕的每一侧各一半(当船只进入角落时,它们会分成四部分)。您可以玩游戏 here。
现在我正在使用取模运算符将 div
环绕在屏幕周围,但它并没有像我希望的那样将块分成两半。在 JavaScript 或 jQuery 中有什么方法可以做到这一点?
这取决于您如何渲染飞船。我假设你正在使用精灵。让:
xs,ys
为目标屏幕分辨率txs,tys
精灵分辨率x0,y0
是精灵位置(左上角)x
-轴向右,y
-轴向下,(0,0)
是展台精灵和屏幕的左上角
我知道有两种基本方法:
直接在精灵渲染中包裹坐标。
这是最简单的方法,但您需要访问精灵渲染器。找到它们应该如下所示的渲染循环:
for (y=y0,ty=0;ty<tys;ty++,y++) for (x=x0,tx=0;tx<txs;tx++,x++) { color=sprite[ty][tx]; if (color!=color_transparent) screen[y][x]=color; }
所以只需添加
x,y
包装即可:while (x0>=xs) x0-=xs; // just normalize start position while (x0< 0) x0+=xs; while (y0>=ys) y0-=ys; while (y0< 0) y0+=ys; for (y=y0,ty=0;ty<tys;ty++,y++) { if (y>=ys) y=0; // wrap y for (x=x0,tx=0;tx<txs;tx++,x++) { if (x>=xs) x=0; // wrap x color=sprite[ty][tx]; if (color!=color_transparent) screen[y][x]=color; } }
当然,渲染循环中的这些条件会稍微减慢速度。因此,您可以通过将代码复制到 4 个实例中进行优化,每个实例都在其自己的一组坐标上,这样
if
就不再位于循环内。分割包裹的精灵
这种方法要快一些(在渲染循环中不需要额外的
if
s)并且不需要对渲染代码进行任何篡改。这个想法是像往常一样渲染非包裹的精灵,如果检测到包裹渲染2/4
包裹位置的精灵副本所以在代码中应该是这样的:
// just normalize start position while (x0>=xs) x0-=xs; while (x0< 0) x0+=xs; while (y0>=ys) y0-=ys; while (y0< 0) y0+=ys; render_sprite(x0,y0,sprite); // compute copies coordinates x1=x0; if (x1+txs>xs) x1-=xs; y1=y0; if (y1+tys>ys) y1-=ys; // render copies if (x0!=x1) render_sprite(x1,y0,sprite); if (y0!=y1) render_sprite(x0,y1,sprite); if ((x0!=x1)&&(y0!=y1)) render_sprite(x1,y1,sprite);
顺便说一句。如果您正在考虑使用这种方法,可以在几何 GLSL 着色器中完成。
包裹精灵
您提供的 link 很有趣。他们已经实施了完整的 CPU 仿真和 运行 汇编编写的游戏。
提高模数
无论如何,如果您使用 canvas 来渲染 sprite(图像),最简单的方法是使用简单的模数并进行修改以处理负值。
当使用负值时,正常模数会失效。
x = 100 % 200; // 100
x = 300 % 200; // 100
x = -100 % 200; // -100 the wrong sign should be 100
x = -50 % 200; // -50 wrong sign wrong direction should be 150
您需要模数以始终 return 正确方向上的正值。要处理负数,请两次取模,第一个将在您想要的范围内,但在 +/- 范围内。然后通过添加范围使负为正。然后再做一次模运算。
var range = 200;
var x = 150;
x = ((x % range) + range) % range; // result 150
x = -50;
x = ((x % range) + range) % range; // result 150 correct.
简单包装
使用上面的模算法,检查边界并根据需要渲染精灵是一件简单的事情。
// assuming ctx is canvas context 2D
// canvas is the canvas.
// img is a HTMLImageElement
var canW = canvas.width; // the canvas size
var canH = canvas.height;
// draw a sprite that is wrapped around the edges of the canvas
function drawWrappedSprite(img,x,y){
x1 = ((x % canW) + canW) % canW; // wrap around correcting for negative values
y1 = ((y % canH) + canH) % canH;
x2 = x1 + img.width; // get right and bottom
y2 = y1 + img.height;
ctx.drawImage(img, x1, y1); // draw first copy
if(x2 > canW){ // check if touching right side
ctx.drawImage(img, x1 - canW, y1); // draw left copy
if(y2 > canH){ // check if touching bottom
ctx.drawImage(img, x1 - canW, y1 - canH); // draw top copy
}
}
if(y2 > canH){
ctx.drawImage(img, x1 , y1 - canH); // draw top copy
}
}
包裹旋转精灵
由于游戏中有旋转的精灵,上述功能将不起作用,因为旋转会改变精灵的大小。要处理旋转的精灵,您需要检查它的最大尺寸。这是精灵对角线的长度,可以用 sqrt(width * width + height * height)
假设您希望精灵围绕其中心旋转,您可以通过在 x,y 中心位置减去和添加对角线的一半来找到精灵最大范围(顶部、底部、左侧和右侧)。与第一个功能一样,进行取模并根据需要绘制精灵。
在某些情况下,即使精灵不可见,也会在对面绘制精灵。如果您正在绘制大量精灵(超过 100 个),您可能希望获得准确范围而不是最大范围,但是您必须至少变换精灵的一个角以获得水平和垂直范围。然后只需使用这些值而不是函数中的 diag
。
// assuming ctx is canvas context 2D
// canvas is the canvas.
// img is a HTMLImageElement
var canW = canvas.width; // the canvas size
var canH = canvas.height;
// draws sprite rotated about its center by r
function drawWrappedRotatedSprite(img,x,y,r){ // x,y center pos, r rotation in radians
var diag = Math.sqrt(img.width * img.width + img.height * img.height); // get diagonal size
diag /= 2; // half the diagonal
x1 = (((x - diag) % canW) + canW) % canW; // get left extent position and
y1 = (((y - diag) % canH) + canH) % canH; // wrap around correcting for negative values
var w = - img.width / 2; // get image width height
var h = - img.height / 2 // offset in rotated space
x2 = x1 + diag * 2; // get right and bottom max extent
y2 = y1 + diag * 2;
ctx.setTransform(1,0,0,1,x1 + diag, y1 + diag); // set origin
ctx.rotate(r); // set rotation
ctx.drawImage(img, w, h); // draw image rotated around its center
if(x2 > canW){ // check if touching right side
ctx.setTransform(1,0,0,1,x1 + diag - canW, y1 + diag); // set origin
ctx.rotate(r); // set rotation
ctx.drawImage(img,w,h); // draw image rotated around its center
if(y2 > canH){ // check if touching bottom
ctx.setTransform(1,0,0,1,x1 + diag - canW, y1 + diag - canH); // set origin
ctx.rotate(r); // set rotation
ctx.drawImage(img,w,h); // draw image rotated around its center
}
}
if(y2 > canH){
ctx.setTransform(1,0,0,1,x1 + diag, y1 + diag - canH); // set origin
ctx.rotate(r); // set rotation
ctx.drawImage(img,w,h); // draw top image rotated around its center
}
ctx.setTransform(1,0,0,1,0,0); // reset the transform (should only do this after all sprites
// using this function have been drawn
}