¿Forma eficiente de calcular "conos de visión" en el mapa de mosaico 2D?

13

Estoy tratando de calcular qué fichas puede "ver" una unidad en particular si se enfrenta a una determinada dirección en un mapa de fichas (dentro de un cierto rango y ángulo de orientación). La forma más fácil sería dibujar un cierto número de fichas hacia afuera y emitir rayos a cada ficha. Sin embargo, espero algo un poco más eficiente. Una imagen dice más que mil palabras:

conos de visión

El punto rojo es la unidad (que está mirando hacia arriba). Mi objetivo es calcular las fichas amarillas. Los bloques verdes son paredes (las paredes están entre las fichas, y es fácil comprobar si puedes pasar entre dos fichas). La línea azul representa algo así como el método de "emisión de rayos" del que estaba hablando, pero prefiero no tener que hacer esto.

EDITAR: Las unidades solo pueden estar orientadas al norte / sur / este / oeste (0, 90, 180 o 270 grados) y el campo de visión siempre es de 90 grados. Debería simplificar algunos cálculos. Estoy pensando que hay algún tipo de algoritmo recursivo-ish / basado en pila / basado en cola, pero no puedo entenderlo.

¡Gracias!

Robert Fraser
fuente
¿Puede la dirección de orientación ser cualquier ángulo o solo 0,45,90, etc.?
wondra
¿También es el FoV siempre 90?
J_F_B_M
@wondra Solo NSEW (0, 90, 180, 270).
Robert Fraser
@Larethian: Sí, el campo de visión siempre es 90. Esto debería ayudar a simplificar las cosas. Estoy pensando que podría haber una manera basada en la búsqueda en profundidad, pero no puedo entenderlo.
Robert Fraser
Esto suena como los algoritmos de visión utilizados en roguelikes . Esos podrían ser un poco de inspiración. Encontré una página que enumera varios enfoques para el problema.
Anko

Respuestas:

13

¡Sí, encontré un trabajo de investigación!

En términos de costo computacional, Shadow Mapping parece un ganador bastante claro.

ingrese la descripción de la imagen aquí

El algoritmo utilizado se puede encontrar aquí y una implementación de C # se puede encontrar aquí , el bit relevante a continuación.

    #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
Trueno clásico
fuente
¡Gracias! Parece que puede ser complicado hacer que esto funcione con paredes entre celdas, pero lo intentaré y veré cómo funciona.
Robert Fraser
Iría con uno de los algoritmos permisivos, vea la tabla en la octava página del documento
Gustavo Maciel
Excelente documento, aunque mi conclusión sería diferente, que no existe una diferencia práctica de velocidad. Los resultados muestran que para los mapas típicos de pícaro, la mayoría de los algoritmos se ejecutan dentro de los 50 microsegundos, lo que significa que en la práctica, es mejor elegir según otros criterios que no sean la velocidad.
congusbongus