计算在基于瓦片的游戏中哪些瓦片被点亮(“光线追踪”)

Posted

技术标签:

【中文标题】计算在基于瓦片的游戏中哪些瓦片被点亮(“光线追踪”)【英文标题】:Calculating which tiles are lit in a tile-based game ("raytracing") 【发布时间】:2010-09-15 12:52:49 【问题描述】:

我正在编写一个基于图块的小游戏,我想为此支持光源。但是我的算法fu太弱了,所以来找你帮忙。

情况是这样的:有一个基于瓦片的地图(以二维数组的形式保存),包含一个光源和几个站在周围的物品。我想计算哪些瓷砖被光源照亮,哪些在阴影中。

大概的样子的视觉帮助。 L 是光源,Xs 是阻挡光线的物品,0s 是点亮的瓷砖,-s 是阴影中的瓷砖。

0 0 0 0 0 0 - - 0
0 0 0 0 0 0 - 0 0
0 0 0 0 0 X 0 0 0
0 0 0 0 0 0 0 0 0
0 0 0 0 L 0 0 0 0
0 0 0 0 0 0 0 0 0
0 0 0 X X X X 0 0
0 0 0 - - - - - 0
0 0 - - - - - - -

当然,分数系统会更好,其中由于部分被遮挡,瓷砖可能处于半阴影中。该算法不必是完美的 - 只是没有明显错误并且相当快。

(当然会有多个光源,但这只是一个循环。)

有接受者吗?

【问题讨论】:

感谢您的所有回答。回到家后,我将详细介绍它们并实施/发布算法。 您在这方面取得了进一步的进展吗?我很想听听你的进展。 【参考方案1】:

我知道这是多年前的问题,但对于任何寻找这种风格的东西的人,我想提供一个我曾经用于我自己的 roguelike 的解决方案;手动“预先计算”的 FOV。如果你的光源视野有一个最大的外部距离,那么手绘由阻挡物体产生的阴影真的不是很努力。你只需要画1/8的圆(加上直线和对角线方向);您可以将对称性用于其他八分之一。您将拥有与 1/8 圆中的正方形一样多的阴影贴图。然后根据对象将它们组合在一起。

这方面的三个主要优点是: 1.如果实施得当,速度非常快 2. 你可以决定应该如何投射阴影,而不是比较哪种算法处理哪种情况最好 3. 没有奇怪的算法导致你必须以某种方式修复的边缘情况

缺点是你并没有真正实现一个有趣的算法。

【讨论】:

【参考方案2】:

我已经在单个 C 函数中实现了基于 tile 的视野。这里是: https://gist.github.com/zloedi/9551625

【讨论】:

【参考方案3】:

这是一种非常简单但相当有效的方法,它在屏幕上的图块数量中使用线性时间。每个图块要么是不透明的,要么是透明的(这是给我们的),每个图块都可以是可见的或有阴影的(这是我们试图计算的)。

我们首先将头像本身标记为“可见”。

然后我们应用此递归规则来确定剩余图块的可见性。

    如果磁贴与头像位于同一行或同一列,则仅当靠近头像的相邻磁贴可见且透明时才可见。 如果图块位于与头像成 45 度角的对角线上,则仅当相邻的对角图块(朝向头像)可见且透明时,它才可见。 在所有其他情况下,请考虑比相关图块更靠近化身的三个相邻图块。例如,如果此图块位于 (x,y) 并且位于头像的上方和右侧,则要考虑的三个图块是 (x-1, y)、(x, y-1) 和 (x- 1, y-1)。如果这三个图块中的任何可见且透明,则相关图块可见。

为了完成这项工作,必须按特定顺序检查图块,以确保已经计算了递归情况。这是一个工作排序的示例,从 0(即头像本身)开始并向上计数:

9876789
8543458
7421247
6310136
7421247
8543458
9876789

编号相同的图块可以按任意顺序相互检查。

结果不是漂亮的阴影投射,而是计算出可信的图块可见性。

【讨论】:

【参考方案4】:

roguelike 开发社区有点痴迷于视线、视野算法。

以下是关于该主题的 roguelike wiki 文章的链接: http://roguebasin.roguelikedevelopment.org/index.php?title=Field_of_Vision

对于我的 roguelike 游戏,我在 Python 中实现了阴影投射算法 (http://roguebasin.roguelikedevelopment.org/index.php?title=Shadow_casting)。组装起来有点复杂,但运行效率相当高(即使在纯 Python 中)并产生了不错的结果。

“许可视野”似乎也越来越受欢迎: http://roguebasin.roguelikedevelopment.org/index.php?title=Permissive_Field_of_View

【讨论】:

+1 用于阴影投射。我在我的 roguelike 中使用了它,一旦你让它工作,它就很棒,而且超级快。我不喜欢宽容的视野,我认为它太宽容了。 这不仅仅是胭脂。 FOV 和 LOS 是制造能够正确模拟视觉约束的 AI 传感器所需的基本几何测试,即是否可以进行任何类型的隐身。在 2D 物理系统中,LOS 可以通过段查询来完成,LOS 通过检查面向单位向量与目标方向向量的点积来完成。【参考方案5】:

实际上,我最近才将此功能写入我的一个项目中。

void Battle::CheckSensorRange(Unit* unit,bool fog)
    int sensorRange = 0;
    for(int i=0; i < unit->GetSensorSlots(); i++)
        if(unit->GetSensorSlot(i)->GetSlotEmpty() == false)
            sensorRange += unit->GetSensorSlot(i)->GetSensor()->GetRange()+1;
        
    
    int originX = unit->GetUnitX();
    int originY = unit->GetUnitY();

    float lineLength;
    vector <Place> maxCircle;

    //get a circle around the unit
    for(int i = originX - sensorRange; i < originX + sensorRange; i++)
        if(i < 0)
            continue;
        
        for(int j = originY - sensorRange; j < originY + sensorRange; j++)
            if(j < 0)
                continue;
            
            lineLength = sqrt( (float)((originX - i)*(originX - i)) + (float)((originY - j)*(originY - j)));
            if(lineLength < (float)sensorRange)
                Place tmp;
                tmp.x = i;
                tmp.y = j;
                maxCircle.push_back(tmp);
            
        
    

    //if we're supposed to fog everything we don't have to do any fancy calculations
    if(fog)
        for(int circleI = 0; circleI < (int) maxCircle.size(); circleI++)
            Map->GetGrid(maxCircle[circleI].x,maxCircle[circleI].y)->SetFog(fog);
        
    else

        bool LOSCheck = true;
        vector <bool> placeCheck;

        //have to check all of the tiles to begin with 
        for(int circleI = 0; circleI < (int) maxCircle.size(); circleI++)
            placeCheck.push_back(true);
        

        //for all tiles in the circle, check LOS
        for(int circleI = 0; circleI < (int) maxCircle.size(); circleI++)
            vector<Place> lineTiles;
            lineTiles = line(originX, originY, maxCircle[circleI].x, maxCircle[circleI].y);

            //check each tile in the line for LOS
            for(int lineI = 0; lineI < (int) lineTiles.size(); lineI++)
                if(false == CheckPlaceLOS(lineTiles[lineI], unit))
                    LOSCheck = false;

                    //mark this tile not to be checked again
                    placeCheck[circleI] = false;
                
                if(false == LOSCheck)
                    break;
                
            

            if(LOSCheck)
                Map->GetGrid(maxCircle[circleI].x,maxCircle[circleI].y)->SetFog(fog);
            else
                LOSCheck = true;
            
        
    


如果您将其改编为自己使用,则其中有一些额外的东西是您不需要的。为了方便起见,Place 类型只是定义为 x 和 y 位置。

line 函数取自***,做了非常小的修改。我没有打印出 x y 坐标,而是将其更改为返回包含线上所有点的位置向量。 CheckPlaceLOS 函数仅根据图块上是否有对象返回 true 或 false。可以使用此功能进行更多优化,但这对我的需求来说很好。

【讨论】:

【参考方案6】:

这只是为了好玩:

如果您首先执行将图块转换为线条的步骤,则可以在 2D 中复制 Doom 3 方法。例如,

- - - - -
- X X X -
- X X - -
- X - - -
- - - - L

...将被缩减为三条线,将立体对象的角连接成三角形。

然后,做 Doom 3 引擎所做的事情:从光源的角度,考虑每一个面向光的“墙”。 (在这个场景中,只考虑对角线。)对于每条这样的线,将其投影成一个梯形,其前边缘是原始线,其边位于从光源到每个端点的线上,其背面是远远的,过去的整个场景。所以,它是一个“指向”光的梯形。它包含了墙壁投下阴影的所有空间。用黑暗填充这个梯形中的每一块瓷砖。

遍历所有这些线条,您最终会得到一个“模板”,其中包含从光源可见的所有图块。用浅色填充这些瓷砖。当您远离光源(“衰减”)或做其他花哨的事情时,您可能希望减少一点瓷砖的亮度。

对场景中的每个光源重复此操作。

【讨论】:

【参考方案7】:

又快又脏:

(取决于数组的大小)

循环遍历每个图块 为光画一条线 如果行的任何部分命中 X,则它在阴影中 (可选):计算线穿过的 X 的数量并做一些花哨的数学来确定阴影中瓷砖的比例。注意:这可以通过在阈值处理过程中对瓦片和灯光之间的线进行抗锯齿处理(因此沿着返回光源的路线查看其他瓦片)来完成,这些将显示为小的异常。根据所使用的逻辑,您可能会确定该图块处于阴影中的程度(如果有的话)。

您还可以跟踪已测试的像素,因此稍微优化解决方案,而不是重新测试两次像素。

如果线条是半透明的并且 X 块又是半透明的,则可以通过使用图像处理和在像素(图块)之间绘制直线来很好地制作圆顶。您可以对图像设置阈值以确定该线是否与“X”相交

如果您可以选择使用第 3 方工具,那么我可能会接受它。从长远来看,它可能会更快,但您对游戏的了解会更少。

【讨论】:

【参考方案8】:

您可以通过计算遮挡等来处理各种复杂问题,或者您可以采用简单的蛮力方法:对于每个单元格,使用诸如Bresenham Line Algorithm 之类的线条绘制算法来检查当前单元格之间的每个单元格和光源。如果任何已填充的单元格或(如果您只有一个光源)已经测试并发现处于阴影中的单元格,则您的单元格处于阴影中。如果你遇到一个已知被点亮的牢房,你的牢房同样会被点亮。对此的一个简单优化是将沿线遇到的任何单元格的状态设置为最终结果。

这或多或少是我在2004 IOCCC winning entry 中使用的。不过,显然这并不是很好的示例代码。 ;)

编辑:正如 loren 指出的那样,通过这些优化,您只需要选择沿地图边缘的像素进行追踪。

【讨论】:

【参考方案9】:

在我看来,这里介绍的算法所做的计算比我认为需要的要多。我尚未对此进行测试,但我认为它会起作用:

最初,将所有像素标记为点亮。

对于地图边缘的每个像素:正如 Arachnid 建议的那样,使用 Bresenham 追踪从像素到灯光的线。如果该线碰到障碍物,则将从边缘到障碍物之外的所有像素标记为处于阴影中。

【讨论】:

只需要沿边缘使用像素的好处。我认为这实际上是我使用的 - 时间太长了,我无法清楚地回忆起来。 :) 纯 bresenham 光线追踪往往会在光半径边缘留下伪影。它往往会错过应该被点亮的方块。 您可能需要重新遍历整个世界以拾取任何“未经测试”的图块。因此,简单地遍历数组应该具有几乎相同的效果(假设您有已经测试过的图块的记录)。【参考方案10】:

TK 的解决方案是您通常用于此类事情的解决方案。

对于部分照明场景,您可以使用它,以便如果某个图块处于阴影中,则该图块会被分成 4 个图块,并且每个图块都经过测试。然后,您可以根据需要拆分多少?

编辑:

您也可以通过不测试与灯光相邻的任何图块来优化它 - 我猜当您有多个光源时这样做会更重要...

【讨论】:

【参考方案11】:

如果您不想花时间重新发明/重新实现这一点,那么市面上有很多游戏引擎。 Ogre3D 是一个完全支持灯光、声音和游戏控制的开源游戏引擎。

【讨论】:

【参考方案12】:

要检查瓷砖是否处于阴影中,您需要将一条直线画回光源。如果该线与另一个被占用的图块相交,则您正在测试的图块处于阴影中。光线追踪算法对视图中的每个对象(在您的情况下为图块)执行此操作。

***上的Raytracing article 有伪代码。

【讨论】:

以上是关于计算在基于瓦片的游戏中哪些瓦片被点亮(“光线追踪”)的主要内容,如果未能解决你的问题,请参考以下文章

瓦片合并算法2048游戏

Tilemaps 地图创建 1

基于cesium的百度、腾讯、高德数据访问

数据结构 2d/3d 瓦片数组 C++

Unity 2D 游戏学习笔记

多时相地图瓦片简单设想