如何在2D游戏中进行物体排序?

3
我正在使用JavaScript开发一种基于透视原理的2D/3D游戏。
如下图所示,我有一个X轴和一个Y轴。
(Note: The image is not provided)
对于我的问题:我在地图上有一堆带有属性的对象(标记为“1”和“2”),例如:
  • positionX / positionY
  • sizeX / sizeY

在图像中,对象“1”获得坐标 x:3, y:2,而对象“2”获得坐标 x:5, y:4。 对于这两个对象,SizeX和sizeY都是w:1, h:1

我想做的是根据对象的位置和大小按升序对所有对象进行排序,以便在3D中了解哪些对象属于另一个对象,然后将所有已排序的对象绘制到画布中(前景/背景中的对象=“图层”)。


img

注意:相机必须固定位置——假设相机具有相同的X和Y值,因此在计算中不能使用相机位置CameraX = CameraY
我尝试过的方法:

let objects = [
  {
    name: "objectA",
    x: 8,
    y: 12,
    w: 2,
    h: 2
  }, 
  {
    name: "objectB",
    x: 3,
    y: 5,
    w: 2,
    h: 2
  },
  {
    name: "objectC",
    x: 6,
    y: 2,
    w: 1,
    h: 3
  }
]


let sortObjects = (objects) => {
  return objects.sort((a, b)=> {
    let distanceA = Math.sqrt(a.x**2 + a.y**2);
    let distanceB = Math.sqrt(b.x**2 + b.y**2);
    return distanceA - distanceB;
  });
}


let sortedObjects = sortObjects(objects);
console.log(sortedObjects);

// NOTE in 3d: first Object drawn first, second Object drawn second and so on...

对上面的代码片段进行编辑:

我尝试根据对象的x/y坐标对其进行排序,但似乎在计算时还必须使用宽度和高度参数以避免错误。

我该如何使用宽度/高度?说实话,我一点头绪都没有,所以任何帮助都将不胜感激。


如果你只是想为了渲染目的按照从后往前的顺序对对象进行排序(所谓的画家算法),那么为什么不实现BSP树呢?BSP树可以保证完美的排序,但你需要权衡一些东西... - Mauricio Cele Lopez Belon
5个回答

1

好的,让我们开始吧!我们需要按照遮挡而不是距离来排序这些对象。也就是说,对于两个对象A和B,A可以明显遮挡B,B可以明显遮挡A,或者两者都不相互遮挡。如果A遮挡B,我们将想要先画B,反之亦然。为了解决这个问题,我们需要能够确定A是否遮挡了B,或者反过来。

这是我想出的解决方法。我只有有限的测试能力,可能仍然存在缺陷,但思路是正确的。

第一步。将每个对象映射到其边界,并保存原始对象以供以后使用:

let step1 = objects.map(o => ({
  original: o,
  xmin: o.x,
  xmax: o.x + o.w,
  ymin: o.y,
  ymax: o.y + o.h
}));

步骤2:将每个对象映射到两个角落,当在它们之间画一条线时,形成对相机视野最大的遮挡物:
let step2 = step1.map(o => {
  const [closestX, farthestX] = [o.xmin, o.xmax].sort((a, b) => Math.abs(camera.x - a) - Math.abs(camera.x - b));
  const [closestY, farthestY] = [o.ymin, o.ymax].sort((a, b) => Math.abs(camera.y - a) - Math.abs(camera.y - b));

  return {
    original: o.original,
    x1: closestX,
    y1: o.xmin <= camera.x && camera.x <= o.xmax ? closestY : farthestY,
    x2: o.ymin <= camera.y && camera.y <= o.ymax ? closestX : farthestX,
    y2: closestY
  };
});

步骤三。对物体进行排序。从相机到一个物体的每个端点绘制一条线段。如果另一个物体的端点之间的线段相交,则该物体更靠近,必须在其后绘制。

let step3 = step2.sort((a, b) => {
  const camSegmentA1 = {
    x1: camera.x,
    y1: camera.y,
    x2: a.x1,
    y2: a.y1
  };
  const camSegmentA2 = {
    x1: camera.x,
    y1: camera.y,
    x2: a.x2,
    y2: a.y2
  };
  const camSegmentB1 = {
    x1: camera.x,
    y1: camera.y,
    x2: b.x1,
    y2: b.y1
  };
  const camSegmentB2 = {
    x1: camera.x,
    y1: camera.y,
    x2: b.x2,
    y2: b.y2
  };

  // Intersection function taken from here: https://dev59.com/_2ox5IYBdhLWcg3wr2Lo#24392281
  function intersects(seg1, seg2) {
    const a = seg1.x1, b = seg1.y1, c = seg1.x2, d = seg1.y2,
          p = seg2.x1, q = seg2.y1, r = seg2.x2, s = seg2.y2;
    const det = (c - a) * (s - q) - (r - p) * (d - b);
    if (det === 0) {
      return false;
    } else {
      lambda = ((s - q) * (r - a) + (p - r) * (s - b)) / det;
      gamma = ((b - d) * (r - a) + (c - a) * (s - b)) / det;
      return (0 < lambda && lambda < 1) && (0 < gamma && gamma < 1);
    }
  }

  function squaredDistance(pointA, pointB) {
    return Math.pow(pointB.x - pointA.x, 2) + Math.pow(pointB.y - pointA.y, 2);
  }

  if (intersects(camSegmentA1, b) || intersects(camSegmentA2, b)) {
    return -1;
  } else if (intersects(camSegmentB1, a) || intersects(camSegmentB2, a)) {
    return 1;
  } else {
    return Math.max(squaredDistance(camera, {x: b.x1, y: b.y1}), squaredDistance(camera, {x: b.x2, y: b.y2})) - Math.max(squaredDistance(camera, {x: a.x1, y: a.y1}), squaredDistance(camera, {x: a.x2, y: a.y2}));
  }
});

第四步。最后一步--获取排序后的原始对象,按照远到近的顺序:

let results = step3.map(o => o.original);

现在,把它们全部结合起来:
results = objects.map(o => {
  const xmin = o.x,
        xmax = o.x + o.w,
        ymin = o.y,
        ymax = o.y + o.h;

  const [closestX, farthestX] = [xmin, xmax].sort((a, b) => Math.abs(camera.x - a) - Math.abs(camera.x - b));
  const [closestY, farthestY] = [ymin, ymax].sort((a, b) => Math.abs(camera.y - a) - Math.abs(camera.y - b));

  return {
    original: o,
    x1: closestX,
    y1: xmin <= camera.x && camera.x <= xmax ? closestY : farthestY,
    x2: ymin <= camera.y && camera.y <= ymax ? closestX : farthestX,
    y2: closestY
  };
}).sort((a, b) => {
  const camSegmentA1 = {
    x1: camera.x,
    y1: camera.y,
    x2: a.x1,
    y2: a.y1
  };
  const camSegmentA2 = {
    x1: camera.x,
    y1: camera.y,
    x2: a.x2,
    y2: a.y2
  };
  const camSegmentB1 = {
    x1: camera.x,
    y1: camera.y,
    x2: b.x1,
    y2: b.y1
  };
  const camSegmentB2 = {
    x1: camera.x,
    y1: camera.y,
    x2: b.x2,
    y2: b.y2
  };

  // Intersection function taken from here: https://dev59.com/_2ox5IYBdhLWcg3wr2Lo#24392281
  function intersects(seg1, seg2) {
    const a = seg1.x1, b = seg1.y1, c = seg1.x2, d = seg1.y2,
          p = seg2.x1, q = seg2.y1, r = seg2.x2, s = seg2.y2;
    const det = (c - a) * (s - q) - (r - p) * (d - b);
    if (det === 0) {
      return false;
    } else {
      lambda = ((s - q) * (r - a) + (p - r) * (s - b)) / det;
      gamma = ((b - d) * (r - a) + (c - a) * (s - b)) / det;
      return (0 < lambda && lambda < 1) && (0 < gamma && gamma < 1);
    }
  }

  function squaredDistance(pointA, pointB) {
    return Math.pow(pointB.x - pointA.x, 2) + Math.pow(pointB.y - pointA.y, 2);
  }

  if (intersects(camSegmentA1, b) || intersects(camSegmentA2, b)) {
    return -1;
  } else if (intersects(camSegmentB1, a) || intersects(camSegmentB2, a)) {
    return 1;
  }
  return Math.max(squaredDistance(camera, {x: b.x1, y: b.y1}), squaredDistance(camera, {x: b.x2, y: b.y2})) - Math.max(squaredDistance(camera, {x: a.x1, y: a.y1}), squaredDistance(camera, {x: a.x2, y: a.y2}));
}).map(o => o.original);

如果那个有效,请告诉我!


@jonas00 我明白问题所在了:我之前测试的是相机线段是否与另一个线段相交,但实际上应该测试它是否与另一条直线相交。我已经修改了代码,现在可以正常工作了吗? - JasonR
@jonas00 已修复!我的新方法寻找交点存在一个错误,所以我回到了之前使用线段交点的方式,并添加了对处理不正确的情况的检查。我还通过与您的JSFiddle玩耍意识到对象并没有居中于它们的坐标上。坐标是左上角。我在我的答案中编辑了我的代码,也编辑了您的JSFiddle:https://jsfiddle.net/4z9bcu7o/ 相机到对象的线条仅用于测试目的。请注意,您可能需要调整相机的位置。 - JasonR
@jonas00 谢谢!很高兴能帮到你! - JasonR
Jupyy。这个例子完美地运行了 - 干得好,兄弟。但是现在我已经在我的游戏中测试了你的代码 - 它没有按照预期工作。无论如何,非常感谢你的帮助 - 即使现在我注意到还有一些错误,我仍然授予你赏金。嗯。 :) - user3596335
@jonas00 现在有什么错误?哪些对象的排列被绘制不正确?我很好奇我没有预料到的情况以及如何修复我的代码。 - JasonR

1

我不确定你的意思是什么:

注意:相机必须固定位置——假设相机具有相同的X和Y值,因此在计算CameraX = CameraY时不能使用相机位置。

所以这里提供一个一般情况下的解决方案。

您必须按照它们到相机的最近距离对对象进行排序。这取决于对象的尺寸以及其相对位置。

该算法可以通过以下JS实现:

// If e.g. horizontal distance > width / 2, subtract width / 2; same for vertical
let distClamp = (dim, diff) => {
    let dist = Math.abs(diff);
    return (dist > 0.5 * dim) ? (dist - 0.5 * dim) : dist;
}

// Closest distance to the camera
let closestDistance = (obj, cam) => {
    let dx = distClamp(obj.width, obj.x - cam.x);
    let dy = distClamp(obj.height, obj.y - cam.y);
    return Math.sqrt(dx * dx + dy * dy);
}

// Sort using this as the metric
let sortObject = (objects, camera) => {
    return objects.sort((a, b) => {
        return closestDistance(a, camera) - closestDistance(b, camera);
    });
}

编辑 这个解决方案也不可行,因为它做出了幼稚的假设,很快会更新或删除。


如果我站在一个物体(大小X:1,大小Y:2)的右侧,我会被绘制成在物体后面而不是前面的玩家。在其他方面似乎没有任何问题。 - user3596335
@jonas00,你能给出玩家和物体的坐标和尺寸,以及相机的坐标吗?我不明白为什么会出现这个问题。 - meowgoesthedog
当然可以!玩家{x:7.16, y: 10.25, width: 0.2, height: 0.2 }物体{x: 6.66, y: 11.44, width: 0.3, height: 1.6}。我将我的相机定位在 x: 100, y: 100。无论我的相机在哪里,因为 x 和 y 始终相同,所以 x: 200, y: 200 也是有效的。 - user3596335
@jonas00 啊,我现在意识到出了什么问题了,非常抱歉。我完全忘记了这样简单的方法行不通,因为它没有考虑物体平面的方向。我会重新发布或删除。 - meowgoesthedog
好的。非常感谢您的提前帮助。 - user3596335
显示剩余3条评论

0

也许你可以在这里找到一些有用的东西(请使用Firefox并检查演示

在我的情况下,深度基本上是pos.x + pos.y [assetHelper.js -> get depth() {...],正如第一个答案所描述的那样。然后排序是一个简单的比较[canvasRenderer -> depthSortAssets() {...]


0
在您的图表中,考虑每个单元格的x+y值。

diagram with X+Y for each cell

要从上到下对单元格进行排序,您可以简单地根据x+y的值进行排序。


这正是我的代码所做的,但 OP 声称它不起作用。 - Dillon Davis
1
@DillonDavis:我知道...希望展示一个图表能够让这更清晰。 - 6502
但是一个对象可以从(x:0,y:0)移动到(x:1,y:2),所以我不能像@DillonDavis那样按它们的x / y位置对对象进行排序,因为这是绝对错误的。X / Y参数代表区域的中心点。 - user3596335
@jonas00:你应该问自己的问题是...“在标有3的正方形上建造的任何东西是否可以隐藏在标有4的正方形上?”...(或者另一个标有3的正方形) - 6502

0
问题在于您使用欧几里得距离来测量对象距离 (0, 0) 有多远,试图测量到y = -x 线的距离。然而这种方法行不通,但曼哈顿距离可以解决这个问题。
let sortObjects = (objects) => {
  return objects.sort((a, b)=> {
    let distanceA = a.x + a.y;
    let distanceB = b.x + b.y;
    return distanceA - distanceB;
  });
}

这将在您旋转的坐标系中垂直地排列您的对象。


我一定误解了问题。对象可以有不同的大小吗?如果可以,图像锚定在哪里(中心,左上角等)? - Dillon Davis
@jonas00,只要 cameraX == cameraY,并且(假设)所有对象都具有固定的高度/宽度,我很难看出这种方法为什么行不通。 - Dillon Davis
是的。但在我的旋转坐标系中,图层与对象的宽度和高度有些连接。所有对象可能具有不同的大小,这将破坏您/我的排序算法。而且我很确定这个解决方案并没有完全按照预期工作 :( - user3596335
@jonas00 我可能现在理解问题了。你是说一个对象可能跨越矩形 (x: 0, y: 0) 到 (x: 1, y: 2),因此它将根据 (x: (0+1)/2, y: (0+2)/2) = (0.5, 1) 定位,但这可能会导致刚好在其下方(并且向左或向右)的对象被排序到它上面。 - Dillon Davis
如果是这种情况,使用您当前的方法不可能解决。不存在比较函数可以进行基于比较的排序来对这些元素进行排序,因为它会违反三角不等式。例如:正方形(x:0,y:5)、矩形(x:1,y:5)到(x:2,y:5)、正方形(x:3,y:0)。第一个在第二个之上,第二个在第三个之上,但第三个又在第一个之上。您需要完全重新考虑您的算法。 - Dillon Davis
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接