Click here to Skip to main content
15,885,366 members
Articles / Programming Languages / C# 4.0

A Complex - Yet Quite Intelligible - Pathfinding in C#

Rate me:
Please Sign up or sign in to vote.
4.96/5 (41 votes)
30 Aug 2013CPOL21 min read 59.5K   2.5K   88  
A semi-analytic 2D path-finding for large dynamic scenarios.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Linq;
using YinYang.CodeProject.Projects.SimplePathfinding.Helpers;

namespace YinYang.CodeProject.Projects.SimplePathfinding.Scenarios
{
    public abstract class BasePathScenario : IPathScenario
    {
        #region | Constants |

        protected readonly Int32 BlockColorValue = Color.Black.ToArgb();

        #endregion

        #region | Fields |

        protected Int32 Width;
        protected Int32 Height;

        protected BitArray Cache;
        protected UInt16[] DistanceMap;
        protected List<DirectionType> Directions;
        
        protected Boolean IsFirstRun;
        protected BlockMethodType ObstacleDetectionMethod;
        protected Boolean AreHollowAreasMinimized;

        #endregion

        #region | Properties |

        /// <summary>
        /// Determines whether the cache is used.
        /// </summary>
        protected virtual Boolean UseCache
        {
            get { return false; }
        }

        #endregion

        #region | Indexers |

        public UInt16 this[Int32 x, Int32 y]
        {
            get { return DistanceMap[x + y*Width]; }
            set { DistanceMap[x + y*Width] = value; }
        }

        #endregion

        #region | Constructors |

        /// <summary>
        /// Initializes a new instance of the <see cref="BasePathScenario" /> class.
        /// </summary>
        /// <param name="width">The width.</param>
        /// <param name="height">The height.</param>
        protected BasePathScenario(Int32 width, Int32 height)
        {
            Width = width;
            Height = height;

            Int32 volume = Width*Height;

            Directions = DirectionHelper.GetValues().ToList();
            Cache = UseCache ? new BitArray(volume) : null;
            DistanceMap = new UInt16[volume];

            IsFirstRun = true;
            AreHollowAreasMinimized = true;
            ObstacleDetectionMethod = BlockMethodType.Precise;
        }

        #endregion

        #region | Methods |

        /// <summary>
        /// Creates the default image, that will be used as a reset image.
        /// </summary>
        /// <param name="obstacleDetectionMethod">Type of the method used to determine whether point is blocked (speed/quality).</param>
        /// <param name="generateNew">Whether to generate new variant of scenario (where applicable).</param>
        /// <param name="minimizeHollowAreas">Whether to minimize hollow areas, or not.</param>
        /// <param name="updateDistanceMap">Whether to recreate the distance map.</param>
        /// <param name="showDistanceMap">Whether the distance map should be visualized in the target image.</param>
        public Bitmap CreateDefaultImage(BlockMethodType obstacleDetectionMethod = BlockMethodType.Precise,
                                         Boolean generateNew = true, 
                                         Boolean minimizeHollowAreas = false, 
                                         Boolean updateDistanceMap = true,
                                         Boolean showDistanceMap = true)
        {
            ObstacleDetectionMethod = obstacleDetectionMethod;
            AreHollowAreasMinimized = minimizeHollowAreas;

            return OnCreateDefaultImage(generateNew, updateDistanceMap, showDistanceMap);
        }

        #endregion

        #region | Bit field methods |

        private void ResetCache()
        {
            if (Cache == null) Cache = new BitArray(Width*Height);

            Cache.SetAll(false);
        }

        protected Int32 GetOffset(Int32 x, Int32 y)
        {
            return x + y*Width;
        }

        protected Boolean GetCacheBit(Int32 x, Int32 y)
        {
            Int32 offset = GetOffset(x, y);
            return Cache.Get(offset);
        }

        protected void SetCacheBit(Int32 x, Int32 y)
        {
            Int32 offset = GetOffset(x, y);
            Cache.Set(offset, true);
        }

        #endregion

        #region | Distance map methods |

        private static void DrawDistanceIntensity(Bitmap bitmap, Int32 x, Int32 y, UInt16 distance)
        {
            Int32 intensity = 255 - (distance << 1) % 254;
            Color color = Color.FromArgb(255, 255, 255, intensity);
            bitmap.SetPixel(x, y, color);
        }

        private void GenerateDistanceMap(Bitmap bitmap, Boolean showDistanceMap)
        {
            // cannot create map for invalid image, just skip it
            if (bitmap == null) return;

            // prepares for the processing
            List<Point> nextRound = new List<Point>();

            // resets the distance map to Empty = 0, Blocked = 1
            for (Int32 y = 0; y < Height; y++)
            for (Int32 x = 0; x < Width; x++)
            {
                Point point = new Point(x, y);

                // determines whether the pixel is blocked, or on the boundaries (disables off-screen pathfinding)
                if (x == 0 || y == 0 || x == Width - 1 || y == Height - 1 || OnIsBlocked(x, y))
                {
                    this[x, y] = 1;
                    nextRound.Add(point);
                }
                else
                {
                    this[x, y] = 0;
                }
            }

            // round to does actual classification
            UInt16 round = 2;

            do // perform rounds if there is still at least one pixel to process
            {
                // switches the next round for current round
                List<Point> thisRound = nextRound;
                nextRound = new List<Point>();

                // processes all the neighbor points along the border of already classified pixels
                foreach (Point point in thisRound)
                foreach (DirectionType direction in Directions)
                {
                    Point neighborPoint = DirectionHelper.GetNextStep(point, direction);

                    // checks whether it is within valid bounds
                    if (neighborPoint.X >= 0 && neighborPoint.Y >= 0 &&
                        neighborPoint.X < Width && neighborPoint.Y < Height)
                    {
                        UInt16 value = this[neighborPoint.X, neighborPoint.Y];

                        // if this neighbor is still unclassified, do it, and add it for the next round
                        if (value == 0)
                        {
                            if (showDistanceMap)
                            {
                                DrawDistanceIntensity(bitmap, neighborPoint.X, neighborPoint.Y, round);
                            }

                            this[neighborPoint.X, neighborPoint.Y] = round;
                            nextRound.Add(neighborPoint);
                        }
                    }
                }

                round++;
            }
            while (nextRound.Count > 0);
        }

        protected void UpdateDistanceMap(Bitmap bitmap, Boolean generateNew, Boolean updateDistanceMap, Boolean showDistanceMap)
        {
            if (generateNew || updateDistanceMap || IsFirstRun)
            {
                GenerateDistanceMap(bitmap, showDistanceMap);
            }
            else if (showDistanceMap) // if we're not showing the map just skip this at all, otherwise redraw it
            {
                for (Int32 y = 0; y < Height; y++)
                for (Int32 x = 0; x < Width; x++)
                {
                    UInt16 distance = this[x, y];

                    // draw only non-blocking spaces
                    if (distance > 1)
                    {
                        DrawDistanceIntensity(bitmap, x, y, distance);
                    }
                }
            }
        }

        #endregion

        #region | Virtual/abstract methods |

        /// <summary>
        /// Called when a default image is about to be created.
        /// </summary>
        /// <returns></returns>
        protected virtual Bitmap OnCreateDefaultImage(Boolean generateNew, 
                                                      Boolean updateDistanceMap, 
                                                      Boolean showDistanceMap)
        {
            Bitmap result = new Bitmap(Width, Height, PixelFormat.Format32bppArgb);

            using (Graphics graphics = Graphics.FromImage(result))
            {
                if (UseCache) ResetCache();

                graphics.Clear(SystemColors.Control);
                graphics.SmoothingMode = SmoothingMode.HighQuality;
                graphics.CompositingQuality = CompositingQuality.HighQuality;
                graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;

                // generates scenario, if needed
                OnBuild(generateNew || IsFirstRun);

                // creates distance map, or at least updates it
                UpdateDistanceMap(result, generateNew, updateDistanceMap, showDistanceMap);

                // draws the main layout of the scenario
                OnDraw(graphics);
            }

            IsFirstRun = false;
            return result;
        }

        /// <summary>
        /// Called when a scenario is about to be built.
        /// </summary>
        /// <param name="generateNew">Indicates that a new configuration should be generated if possible.</param>
        protected virtual void OnBuild(Boolean generateNew)
        {
            // not implemented here (empty scenario)
        }

        /// <summary>
        /// Called when scenario is about to be drawn to an image.
        /// </summary>
        /// <param name="graphics">The graphics.</param>
        /// <param name="?">The ?.</param>
        protected virtual void OnDraw(Graphics graphics)
        {
            // not implemented here (empty scenario)
        }

        /// <summary>
        /// Determines whether the point is blocked or not. There are two variants: fast, or precise (self-explanatory).
        /// </summary>
        /// <param name="x">The x.</param>
        /// <param name="y">The y.</param>
        /// <returns></returns>
        protected abstract Boolean OnIsBlocked(Int32 x, Int32 y);

        #endregion

        #region | Helper methods |

        /// <summary>
        /// Conforms start and end point to the bounds of [0,0] - [width, height]
        /// </summary>
        public static void CheckBounds(ref Point start, ref Point end, Int32 width, Int32 height)
        {
            Int32 x1 = Math.Max(0, Math.Min(width - 1, start.X));
            Int32 y1 = Math.Max(0, Math.Min(height - 1, start.Y));
            Int32 x2 = Math.Max(0, Math.Min(width - 1, end.X));
            Int32 y2 = Math.Max(0, Math.Min(height - 1, end.Y));

            start = new Point(x1, y1);
            end = new Point(x2, y2);
        }

        public static void ClearScreen(Graphics graphics, Int32 width, Int32 height)
        {
            graphics.Clear(SystemColors.Control);
        }

        #endregion

        #region << IPathScenario >>

        /// <summary>
        /// See <see cref="IPathScenario.IsBlocked"/> for more details.
        /// </summary>
        public Boolean IsBlocked(Int32 x, Int32 y, Int32 diameter)
        {
            Boolean result = true;

            // tests if the point is even within the bounds
            if (x >= 0 && y >= 0 && x < Width && y < Height)
            {
                // retrieves distance (allowed radius) from the nearest obstacle; otherwise assume worst case scenario (1)
                UInt16 radius = this[x, y];
   
                // determines whether the object will fit
                Boolean radiusCondition = radius <= (diameter + 1) >> 1;
                result = radiusCondition || OnIsBlocked(x, y);
            }

            return result;
        }

        #endregion
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Czech Republic Czech Republic
Contacts: EMAIL - smartk8@gmail.com

Comments and Discussions