Эффективный способ расчета «конусов зрения» на 2D-карте тайлов?

13

Я пытаюсь вычислить, какие плитки может «видеть» конкретный юнит, если смотреть на определенное направление на карте плиток (в пределах определенного диапазона и угла наклона). Самым простым способом было бы нарисовать определенное количество плиток наружу и лучевую трансляцию к каждой плитке. Однако я надеюсь на что-то более эффективное. Картина говорит тысячу слов:

конусы зрения

Красная точка - это единица (которая обращена вверх). Моя цель - подсчитать желтые плитки. Зеленые блоки - это стены (стены между плитками, и легко проверить, можно ли пройти между двумя плитками). Синяя линия представляет собой что-то вроде метода «лучевого вещания», о котором я говорил, но я бы предпочел не делать этого.

РЕДАКТИРОВАТЬ: Единицы могут быть направлены только на север / юг / восток / запад (0, 90, 180 или 270 градусов), а FoV всегда 90 градусов. Следует упростить некоторые расчеты. Я думаю, что есть какой-то алгоритм рекурсивного ish / stack-based / queue-based, но я не могу понять это.

Благодарность!

Роберт Фрейзер
источник
Может ли направление движения быть любым углом или просто 0,45,90, ... и т. Д.?
Wondra
Также FoV всегда 90?
J_F_B_M
@wondra Only NSEW (0, 90, 180, 270).
Роберт Фрейзер
@Larethian - Да, FoV всегда 90. Это должно помочь упростить вещи. Я думаю, что может быть способ, основанный на поиске в глубину, но я не могу понять это.
Роберт Фрейзер
Это похоже на алгоритмы видения, используемые в roguelikes . Это может быть источником вдохновения. Я нашел страницу со списком различных подходов к проблеме.
Анко

Ответы:

13

Yay я нашел исследовательскую работу!

С точки зрения вычислительной стоимости Shadow Mapping кажется довольно явным победителем.

введите описание изображения здесь

Используемый алгоритм можно найти здесь, а реализацию C # можно найти здесь , соответствующий бит ниже.

    #region FOV algorithm

    //  Octant data
    //
    //    \ 1 | 2 /
    //   8 \  |  / 3
    //   -----+-----
    //   7 /  |  \ 4
    //    / 6 | 5 \
    //
    //  1 = NNW, 2 =NNE, 3=ENE, 4=ESE, 5=SSE, 6=SSW, 7=WSW, 8 = WNW

    /// <summary>
    /// Start here: go through all the octants which surround the player to
    /// determine which open cells are visible
    /// </summary>
    public void GetVisibleCells()
    {
        VisiblePoints = new List<Point>();
        foreach (int o in VisibleOctants)
            ScanOctant(1, o, 1.0, 0.0);

    }

    /// <summary>
    /// Examine the provided octant and calculate the visible cells within it.
    /// </summary>
    /// <param name="pDepth">Depth of the scan</param>
    /// <param name="pOctant">Octant being examined</param>
    /// <param name="pStartSlope">Start slope of the octant</param>
    /// <param name="pEndSlope">End slope of the octance</param>
    protected void ScanOctant(int pDepth, int pOctant, double pStartSlope, double pEndSlope)
    {

        int visrange2 = VisualRange * VisualRange;
        int x = 0;
        int y = 0;

        switch (pOctant)
        {

            case 1: //nnw
                y = player.Y - pDepth;
                if (y < 0) return;

                x = player.X - Convert.ToInt32((pStartSlope * Convert.ToDouble(pDepth)));
                if (x < 0) x = 0;

                while (GetSlope(x, y, player.X, player.Y, false) >= pEndSlope)
                {
                    if (GetVisDistance(x, y, player.X, player.Y) <= visrange2)
                    {
                        if (map[x, y] == 1) //current cell blocked
                        {
                            if (x - 1 >= 0 && map[x - 1, y] == 0) //prior cell within range AND open...
                                //...incremenet the depth, adjust the endslope and recurse
                                ScanOctant(pDepth + 1, pOctant, pStartSlope, GetSlope(x - 0.5, y + 0.5, player.X, player.Y, false));
                        }
                        else
                        {

                            if (x - 1 >= 0 && map[x - 1, y] == 1) //prior cell within range AND open...
                                //..adjust the startslope
                                pStartSlope = GetSlope(x - 0.5, y - 0.5, player.X, player.Y, false);

                                VisiblePoints.Add(new Point(x, y));
                        }                            
                    }
                    x++;
                }
                x--;
                break;

            case 2: //nne

                y = player.Y - pDepth;
                if (y < 0) return;                  

                x = player.X + Convert.ToInt32((pStartSlope * Convert.ToDouble(pDepth)));
                if (x >= map.GetLength(0)) x = map.GetLength(0) - 1;

                while (GetSlope(x, y, player.X, player.Y, false) <= pEndSlope)
                {
                    if (GetVisDistance(x, y, player.X, player.Y) <= visrange2)
                    {
                        if (map[x, y] == 1)
                        {
                            if (x + 1 < map.GetLength(0) && map[x + 1, y] == 0)
                                ScanOctant(pDepth + 1, pOctant, pStartSlope, GetSlope(x + 0.5, y + 0.5, player.X, player.Y, false));
                        }
                        else
                        {
                            if (x + 1 < map.GetLength(0) && map[x + 1, y] == 1)
                                pStartSlope = -GetSlope(x + 0.5, y - 0.5, player.X, player.Y, false);

                            VisiblePoints.Add(new Point(x, y));
                        }                            
                    }
                    x--;
                }
                x++;
                break;

            case 3:

                x = player.X + pDepth;
                if (x >= map.GetLength(0)) return;

                y = player.Y - Convert.ToInt32((pStartSlope * Convert.ToDouble(pDepth))); 
                if (y < 0) y = 0;

                while (GetSlope(x, y, player.X, player.Y, true) <= pEndSlope)
                {

                    if (GetVisDistance(x, y, player.X, player.Y) <= visrange2)
                    {

                        if (map[x, y] == 1)
                        {
                            if (y - 1 >= 0 && map[x, y - 1] == 0)
                                ScanOctant(pDepth + 1, pOctant, pStartSlope, GetSlope(x - 0.5, y - 0.5, player.X, player.Y, true));
                        }
                        else
                        {
                            if (y - 1 >= 0 && map[x, y - 1] == 1)
                                pStartSlope = -GetSlope(x + 0.5, y - 0.5, player.X, player.Y, true);

                            VisiblePoints.Add(new Point(x, y));
                        }                           
                    }
                    y++;
                }
                y--;
                break;

            case 4:

                x = player.X + pDepth;
                if (x >= map.GetLength(0)) return;

                y = player.Y + Convert.ToInt32((pStartSlope * Convert.ToDouble(pDepth)));
                if (y >= map.GetLength(1)) y = map.GetLength(1) - 1;

                while (GetSlope(x, y, player.X, player.Y, true) >= pEndSlope)
                {

                    if (GetVisDistance(x, y, player.X, player.Y) <= visrange2)
                    {

                        if (map[x, y] == 1)
                        {
                            if (y + 1 < map.GetLength(1)&& map[x, y + 1] == 0)
                                ScanOctant(pDepth + 1, pOctant, pStartSlope, GetSlope(x - 0.5, y + 0.5, player.X, player.Y, true));
                        }
                        else
                        {
                            if (y + 1 < map.GetLength(1) && map[x, y + 1] == 1)
                                pStartSlope = GetSlope(x + 0.5, y + 0.5, player.X, player.Y, true);

                             VisiblePoints.Add(new Point(x, y));
                        }                          
                    }
                    y--;
                }
                y++;
                break;

            case 5:

                y = player.Y + pDepth;
                if (y >= map.GetLength(1)) return;

                x = player.X + Convert.ToInt32((pStartSlope * Convert.ToDouble(pDepth)));
                if (x >= map.GetLength(0)) x = map.GetLength(0) - 1;

                while (GetSlope(x, y, player.X, player.Y, false) >= pEndSlope)
                {
                    if (GetVisDistance(x, y, player.X, player.Y) <= visrange2)
                    {

                        if (map[x, y] == 1)
                        {
                            if (x + 1 < map.GetLength(1) && map[x+1, y] == 0)
                                ScanOctant(pDepth + 1, pOctant, pStartSlope, GetSlope(x + 0.5, y - 0.5, player.X, player.Y, false));
                        }
                        else
                        {
                            if (x + 1 < map.GetLength(1)
                                    && map[x + 1, y] == 1)
                                pStartSlope = GetSlope(x + 0.5, y + 0.5, player.X, player.Y, false);

                            VisiblePoints.Add(new Point(x, y));
                        }
                    }
                    x--;
                }
                x++;
                break;

            case 6:

                y = player.Y + pDepth;
                if (y >= map.GetLength(1)) return;                  

                x = player.X - Convert.ToInt32((pStartSlope * Convert.ToDouble(pDepth)));
                if (x < 0) x = 0;

                while (GetSlope(x, y, player.X, player.Y, false) <= pEndSlope)
                {
                    if (GetVisDistance(x, y, player.X, player.Y) <= visrange2)
                    {

                        if (map[x, y] == 1)
                        {
                            if (x - 1 >= 0 && map[x - 1, y] == 0)
                                ScanOctant(pDepth + 1, pOctant, pStartSlope, GetSlope(x - 0.5, y - 0.5, player.X, player.Y, false));
                        }
                        else
                        {
                            if (x - 1 >= 0
                                    && map[x - 1, y] == 1)
                                pStartSlope = -GetSlope(x - 0.5, y + 0.5, player.X, player.Y, false);

                            VisiblePoints.Add(new Point(x, y));
                        }
                    }
                    x++;
                }
                x--;
                break;

            case 7:

                x = player.X - pDepth;
                if (x < 0) return;

                y = player.Y + Convert.ToInt32((pStartSlope * Convert.ToDouble(pDepth)));                    
                if (y >= map.GetLength(1)) y = map.GetLength(1) - 1;

                while (GetSlope(x, y, player.X, player.Y, true) <= pEndSlope)
                {

                    if (GetVisDistance(x, y, player.X, player.Y) <= visrange2)
                    {

                        if (map[x, y] == 1)
                        {
                            if (y + 1 < map.GetLength(1) && map[x, y+1] == 0)
                                ScanOctant(pDepth + 1, pOctant, pStartSlope, GetSlope(x + 0.5, y + 0.5, player.X, player.Y, true));
                        }
                        else
                        {
                            if (y + 1 < map.GetLength(1) && map[x, y + 1] == 1)
                                pStartSlope = -GetSlope(x - 0.5, y + 0.5, player.X, player.Y, true);

                            VisiblePoints.Add(new Point(x, y));
                        }
                    }
                    y--;
                }
                y++;
                break;

            case 8: //wnw

                x = player.X - pDepth;
                if (x < 0) return;

                y = player.Y - Convert.ToInt32((pStartSlope * Convert.ToDouble(pDepth)));
                if (y < 0) y = 0;

                while (GetSlope(x, y, player.X, player.Y, true) >= pEndSlope)
                {

                    if (GetVisDistance(x, y, player.X, player.Y) <= visrange2)
                    {

                        if (map[x, y] == 1)
                        {
                            if (y - 1 >=0 && map[x, y - 1] == 0)
                                ScanOctant(pDepth + 1, pOctant, pStartSlope, GetSlope(x + 0.5, y - 0.5, player.X, player.Y, true));

                        }
                        else
                        {
                            if (y - 1 >= 0 && map[x, y - 1] == 1)
                                pStartSlope = GetSlope(x - 0.5, y - 0.5, player.X, player.Y, true);

                            VisiblePoints.Add(new Point(x, y));
                        }
                    }
                    y++;
                }
                y--;
                break;
        }


        if (x < 0)
            x = 0;
        else if (x >= map.GetLength(0))
            x = map.GetLength(0) - 1;

        if (y < 0)
            y = 0;
        else if (y >= map.GetLength(1))
            y = map.GetLength(1) - 1;

        if (pDepth < VisualRange & map[x, y] == 0)
            ScanOctant(pDepth + 1, pOctant, pStartSlope, pEndSlope);

    }

    /// <summary>
    /// Get the gradient of the slope formed by the two points
    /// </summary>
    /// <param name="pX1"></param>
    /// <param name="pY1"></param>
    /// <param name="pX2"></param>
    /// <param name="pY2"></param>
    /// <param name="pInvert">Invert slope</param>
    /// <returns></returns>
    private double GetSlope(double pX1, double pY1, double pX2, double pY2, bool pInvert)
    {
        if (pInvert)
            return (pY1 - pY2) / (pX1 - pX2);
        else
            return (pX1 - pX2) / (pY1 - pY2);
    }


    /// <summary>
    /// Calculate the distance between the two points
    /// </summary>
    /// <param name="pX1"></param>
    /// <param name="pY1"></param>
    /// <param name="pX2"></param>
    /// <param name="pY2"></param>
    /// <returns>Distance</returns>
    private int GetVisDistance(int pX1, int pY1, int pX2, int pY2)
    {
        return ((pX1 - pX2) * (pX1 - pX2)) + ((pY1 - pY2) * (pY1 - pY2));
    }

    #endregion
ClassicThunder
источник
Благодарность! Похоже, может быть сложно заставить это работать со стенами между ячейками, но я попробую и посмотрю, как это работает!
Роберт Фрейзер
Я бы пошел с одним из разрешающих алгоритмов, см. Таблицу на 8-й странице статьи
Густаво Масиэль
Отличная статья, хотя мой вывод будет другим, что практической разницы в скорости нет. Результаты показывают, что для типичных мошеннических карт большинство алгоритмов работают в течение 50 микросекунд, что означает, что на практике вам лучше выбирать, основываясь на критериях, отличных от скорости.
congusbongus