Grid cell and tags types for an A* nav grid. CellTags is a copy-on-write tag container safe for single-writer / multi-reader use. Cell implements geometry, connectivity, generation helpers (traces/tests for walkability, steps, clearance), neighbor/jump queries, occupancy checking and debug drawing.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace GridAStar;
/// <summary>
/// Tags on a grid cell. COPY-ON-WRITE + a reference type: tags are written on the MAIN thread (Door.OccupyCells,
/// NPC.AssignNearbyTags) while the A* pathfinder reads/enumerates them on background worker threads. Mutations
/// build a NEW list and swap the reference, so readers always enumerate a stable (immutable) snapshot - this
/// fixes the "Collection was modified; enumeration operation may not execute" crash in ComputePathInternal.
/// Single-writer (main) / multi-reader (path threads), so no lock is needed; a reference read/write is atomic,
/// and a momentarily stale snapshot is harmless (the NPC re-paths next tick). NOTE: s&box's code whitelist
/// forbids the `volatile` keyword, so we rely on plain reference atomicity rather than volatile visibility.
/// </summary>
public class CellTags
{
// Shared immutable empty snapshot, returned for reads when `all` is (transiently) null.
private static readonly List<string> Empty = new();
// Field initializer (runs for every ctor) + null-guards below: cells are built on background generation
// threads, so the main thread can momentarily observe the Tags reference before this inner list write is
// visible. Treating null as empty makes every operation safe regardless.
private List<string> all = new();
/// <summary>Current snapshot. Safe to enumerate from any thread; do NOT mutate it directly (use Add/Remove).</summary>
public List<string> All => all ?? Empty;
public CellTags()
{
all = new List<string>();
}
public CellTags( List<string> tags )
{
all = tags is null ? new List<string>() : new List<string>( tags );
}
public bool Has( string tag ) => (all ?? Empty).Contains( tag );
public bool Has( params string[] tags )
{
var snapshot = all ?? Empty;
foreach ( string tag in tags )
if ( !snapshot.Contains( tag ) )
return false;
return true;
}
public bool Has( List<string> tags )
{
var snapshot = all ?? Empty;
foreach ( string tag in tags )
if ( snapshot.Contains( tag ) )
return true;
return false;
}
public bool Has( IEnumerable<string> tags )
{
var snapshot = all ?? Empty;
foreach ( string tag in tags )
if ( snapshot.Contains( tag ) )
return true;
return false;
}
public void Add( string tag )
{
var current = all;
if ( current is not null && current.Contains( tag ) )
return;
all = current is null ? new List<string> { tag } : new List<string>( current ) { tag };
}
public void Remove( string tag )
{
var current = all;
if ( current is null || !current.Contains( tag ) )
return;
var copy = new List<string>( current );
copy.Remove( tag );
all = copy;
}
public void Clear()
{
all = new List<string>();
}
}
public struct CellConnection
{
public Cell Cell { get; private set; }
public string ConnectionTag { get; private set; } = string.Empty;
public CellConnection( Cell cell )
{
Cell = cell;
}
public CellConnection( Cell cell, string tag )
{
Cell = cell;
ConnectionTag = tag;
}
}
public partial class Cell : IEquatable<Cell>, IValid
{
/// <summary>The parent grid</summary>
public Grid Grid { get; set; }
public Rotation Rotation => Grid.AxisAligned ? new Rotation() : Grid.Rotation;
public Vector3 Position { get; set; }
public IntVector2 GridPosition { get; set; }
/// <summary>
/// 0 = Bottom Left, 1 = Bottom Right, 2 = Top Left, 3 = Top Right
/// </summary>
public float[] Vertices = new float[4];
public Vector3 BottomLeft => Position.WithZ( Vertices[0] ) + new Vector3( -Grid.CellSize / 2, -Grid.CellSize / 2, 0f ) * Rotation;
public Vector3 BottomRight => Position.WithZ( Vertices[1] ) + new Vector3( -Grid.CellSize / 2, Grid.CellSize / 2, 0f ) * Rotation;
public Vector3 TopLeft => Position.WithZ( Vertices[2] ) + new Vector3( Grid.CellSize / 2, -Grid.CellSize / 2, 0f ) * Rotation;
public Vector3 TopRight => Position.WithZ( Vertices[3] ) + new Vector3( Grid.CellSize / 2, Grid.CellSize / 2, 0f ) * Rotation;
public float Height => Vertices.Max() - Vertices.Min();
public Vector3 Bottom => Position.WithZ( Vertices.Min() );
public BBox Bounds => new BBox( new Vector3( -Grid.WidthClearance, -Grid.WidthClearance, 0f ), new Vector3( Grid.WidthClearance, Grid.WidthClearance, Grid.HeightClearance ) );
public BBox WorldBounds => new BBox( (Position + Bounds.Mins).WithZ( Vertices.Min() ), Position + Bounds.Maxs );
public CellTags Tags { get; private set; }
public List<AStarNode> CellConnections { get; private set; } = new();
private List<AStarNode> connectedCells = new();
public bool Occupied
{
get => Tags.Has( "occupied" );
set
{
if ( value )
Tags.Add( "occupied" );
else
Tags.Remove( "occupied" );
}
}
public Component OccupyingEntity { get; set; } = null;
internal Transform currentOccupyingTransform { get; set; } = Transform.Zero;
bool IValid.IsValid { get; }
// --- Generation diagnostics (reset in Level.GenerateGrid, logged after) ---
public static int DebugRejectCoords;
public static int DebugRejectClearance;
public static int DebugCreated;
/// <summary>
/// Try to create a new cell with the given position and the max standing angle
/// </summary>
public static Cell TryCreate( Grid grid, Vector3 position )
{
float[] validCoordinates = new float[4];
var height = position.z - validCoordinates.Min();
var coordinatesAndStairs = TraceCoordinates( grid, position, ref validCoordinates );
if ( !coordinatesAndStairs.Item1 )
{
DebugRejectCoords++;
return null;
}
if ( !TestForClearance( grid, position, height ) )
{
DebugRejectClearance++;
return null;
}
var cell = new Cell( grid, position, validCoordinates );
if ( coordinatesAndStairs.Item2 )
cell.Tags.Add( "step" );
DebugCreated++;
return cell;
}
//(IsWalkable, IsSteps)
private static (bool, bool) TraceCoordinates( Grid grid, Vector3 position, ref float[] validCoordinates )
{
Vector3[] testCoordinates = new Vector3[4] {
new Vector3( -grid.CellSize / 2, -grid.CellSize / 2 ) * grid.AxisRotation,
new Vector3( -grid.CellSize / 2, grid.CellSize / 2 ) * grid.AxisRotation,
new Vector3( grid.CellSize / 2, -grid.CellSize / 2 ) * grid.AxisRotation,
new Vector3( grid.CellSize / 2, grid.CellSize / 2 ) * grid.AxisRotation
};
var maxHeight = Math.Max( grid.CellSize * MathF.Tan( MathX.DegreeToRadian( grid.StandableAngle ) ), grid.StepSize );
for ( int i = 0; i < 4; i++ )
{
var centerDir = testCoordinates[i].Normal;
// Scene-System fix: start the corner ray a step-height ABOVE the floor. Starting it ON the surface
// (legacy did) reports StartedSolid in the Scene physics and rejected ~98% of floor cells.
var startTestPos = position + testCoordinates[i] + Vector3.Up * grid.StepSize - centerDir * grid.Tolerance;
var endTestPos = position + testCoordinates[i].WithZ( -maxHeight * 2f ) - centerDir * grid.Tolerance;
var testTrace = grid.Scene.Trace.Ray( startTestPos, endTestPos )
.WithGridSettings( grid.Settings );
var testResult = testTrace.Run();
if ( testResult.StartedSolid ) return (false, false);
if ( !testResult.Hit ) return (false, false);
if ( testResult.HitPosition.z > position.z + grid.StepSize ) return (false, false); // corner significantly higher than the cell centre
validCoordinates[i] = testResult.HitPosition.z;
testCoordinates[i] = testResult.HitPosition;
}
var orderedByHeight = testCoordinates.OrderBy( x => x.z );
var lowest = orderedByHeight.First();
var highest = orderedByHeight.Last();
if ( IsCliff( grid, lowest, highest ) || IsCliff( grid, lowest, position ) )
return (false, false);
return TestForSteps( grid, position, testCoordinates );
}
private static bool IsCliff( Grid grid, Vector3 from, Vector3 to )
{
var trace = grid.Scene.Trace.Ray( from, to )
.WithGridSettings( grid.Settings );
var result = trace.Run();
// A ray that starts embedded in the surface (Scene physics reports this when from/to sit on the
// floor) isn't detecting a cliff edge - don't reject the cell for it.
if ( result.StartedSolid )
return false;
if ( result.Hit )
if ( Vector3.GetAngle( Vector3.Up, result.Normal ) > 90 )
return true;
return false;
}
private static bool TestForClearance( Grid grid, Vector3 position, float height )
{
var clearanceBBox = new BBox( new Vector3( -grid.WidthClearance / 2f, -grid.WidthClearance / 2f, 0f ), new Vector3( grid.WidthClearance / 2f, grid.WidthClearance / 2f, 1f ) );
var startPos = position + Vector3.Up * grid.HeightClearance;
var clearanceTrace = grid.Scene.Trace.Box( clearanceBBox, startPos, position + Vector3.Up * grid.StepSize )
.WithGridSettings( grid.Settings );
var clearanceResult = clearanceTrace.Run();
var heightDifference = clearanceResult.EndPosition.z - (position.z - height);
return heightDifference <= grid.StepSize + height;
}
//(IsWalkable, IsSteps)
private static (bool, bool) TestForSteps( Grid grid, Vector3 position, Vector3[] testCoordinates )
{
if ( grid.StepSize <= 0.1f )
return (true, true);
var lowestToHighest = testCoordinates
.OrderBy( x => x.z )
.ToArray();
var stepTestMin = TestForStep( grid, lowestToHighest[0], lowestToHighest[3], position, lowestToHighest[0] );
if ( !stepTestMin.Item1 )
return (false, stepTestMin.Item2);
var stepTestMid = TestForStep( grid, lowestToHighest[1], lowestToHighest[3], position, lowestToHighest[1] );
if ( !stepTestMid.Item1 )
return (false, stepTestMid.Item2);
return (true, stepTestMin.Item2 || stepTestMid.Item2);
}
//(IsWalkable, IsSteps)
private static (bool, bool) TestForStep( Grid grid, Vector3 startPosition, Vector3 endPosition, Vector3 highestPosition, Vector3 lowestPosition )
{
var stepsTried = 0;
var maxSteps = (int)Math.Max( (Math.Abs( highestPosition.z - lowestPosition.z ) / (grid.StepSize / 2f)) + 1, 3 );
var stepDistances = new float[maxSteps];
if ( highestPosition.z - lowestPosition.z <= grid.StepSize / 2 )
return (true, false);
while ( stepsTried < maxSteps )
{
var tolerance = 0.01f;
var stepPositionStart = startPosition + Vector3.Up * (grid.StepSize / 4f + grid.StepSize / 2f * stepsTried + tolerance);
var stepPositionEnd = endPosition.WithZ( stepPositionStart.z );
var stepDirection = (stepPositionEnd - stepPositionStart).Normal;
var stepDistance = stepPositionStart.Distance( stepPositionEnd );
var stepTrace = grid.Scene.Trace.Ray( stepPositionStart, stepPositionStart + stepDirection * (stepDistance + tolerance * 2f) )
.Size( new Vector3( grid.StepSize / 2f ) )
.WithGridSettings( grid.Settings );
var stepResult = stepTrace.Run();
var stepAngle = Vector3.GetAngle( Vector3.Up, stepResult.Normal );
if ( stepsTried == 0 )
if ( stepResult.EndPosition.Distance( endPosition ) <= tolerance * 3f )
return (true, false);
if ( stepResult.Hit && stepAngle > grid.StandableAngle && stepAngle < 89.9f )
return (false, false);
if ( stepResult.Hit && stepAngle < grid.StandableAngle )
return (true, false);
var distanceFromStart = startPosition.Distance( stepResult.EndPosition.WithZ( startPosition.z ) );
if ( stepsTried >= 2 )
{
var distanceDifference = Math.Abs( distanceFromStart - stepDistances[stepsTried - 2] );
if ( distanceDifference < tolerance )
return (false, true);
}
stepDistances[stepsTried] = distanceFromStart;
stepsTried++;
}
return (true, true);
}
public AStarNode AddConnection( Cell other, string tag = "" )
{
var node = new AStarNode( other, new AStarNode( this ), tag == "" ? string.Empty : tag );
CellConnections.Add( node );
other.connectedCells.Add( node );
return node;
}
public void RemoveConnection( AStarNode connection )
{
CellConnections.Remove( connection );
connection.Current.connectedCells.Remove( connection );
}
public void RemoveConnections( Cell other )
{
var foundConnections = CellConnections.Where( x => x.Current == other ).ToList();
foreach ( var connection in foundConnections )
RemoveConnection( connection );
}
public IEnumerable<AStarNode> GetConnections( string movementTag ) => CellConnections.Where( x => x.MovementTag == movementTag );
public void SetOccupant( Component entity )
{
OccupyingEntity = entity;
currentOccupyingTransform = entity.WorldTransform;
}
public void RemoveOccupant()
{
OccupyingEntity = null;
currentOccupyingTransform = Transform.Zero;
}
public bool TestForOccupancy( string tag )
{
if ( OccupyingEntity != null && OccupyingEntity.WorldTransform == currentOccupyingTransform ) return Occupied;
var occupyTrace = Grid.Scene.Trace.Box( Bounds, Position, Position )
.IgnoreStatic()
.WithTag( tag );
var occupyResult = occupyTrace.Run();
if ( occupyResult.Component != null )
SetOccupant( occupyResult.Component );
return occupyResult.Hit;
}
public Cell( Grid grid, Vector3 position, float[] vertices, List<string> tags = null )
{
Grid = grid;
Position = position;
GridPosition = (Position - Grid.WorldBounds.Mins - grid.CellSize / 2).ToIntVector2( grid.CellSize );
Vertices = vertices;
if ( tags != null && tags.Count() > 0 )
Tags = new CellTags( tags );
else
Tags = new CellTags();
}
public void Delete( bool deleteConnections = true )
{
if ( deleteConnections )
{
var connections = connectedCells.ToList();
foreach ( var connectedCell in connections )
connectedCell.Parent.Current.RemoveConnections( this );
connectedCells.Clear();
CellConnections.Clear();
}
Grid.CellStacks[GridPosition].Remove( this );
}
internal static Dictionary<IntVector2, List<IntVector2>> CompareVertices = new()
{
[new IntVector2( -1, -1 )] = new List<IntVector2>() { new IntVector2( 0, 3 ) },
[new IntVector2( -1, 0 )] = new List<IntVector2>() { new IntVector2( 1, 3 ), new IntVector2( 0, 2 ) },
[new IntVector2( -1, 1 )] = new List<IntVector2>() { new IntVector2( 1, 2 ) },
[new IntVector2( 0, -1 )] = new List<IntVector2>() { new IntVector2( 0, 1 ), new IntVector2( 2, 3 ) },
[new IntVector2( 0, 1 )] = new List<IntVector2>() { new IntVector2( 1, 0 ), new IntVector2( 3, 2 ) },
[new IntVector2( 1, -1 )] = new List<IntVector2>() { new IntVector2( 2, 1 ) },
[new IntVector2( 1, 0 )] = new List<IntVector2>() { new IntVector2( 3, 1 ), new IntVector2( 2, 0 ) },
[new IntVector2( 1, 1 )] = new List<IntVector2>() { new IntVector2( 3, 0 ) },
};
public bool IsNeighbour( Cell cell )
{
var xDistance = cell.GridPosition.x - GridPosition.x;
var yDistance = cell.GridPosition.y - GridPosition.y;
if ( xDistance < -1 || xDistance > 1 || yDistance < -1 || yDistance > 1 ) return false;
if ( cell == this ) return true;
if ( xDistance == 0 && yDistance == 0 ) return false;
var verticesToCompare = CompareVertices[new IntVector2( xDistance, yDistance )];
foreach ( var comparePair in verticesToCompare )
{
var heightDifference = Math.Abs( Vertices[comparePair[0]] - cell.Vertices[comparePair[1]] );
if ( heightDifference > Grid.StepSize ) return false;
}
return true;
}
public IEnumerable<Cell> GetNeighbours( bool ignoreHeight = false )
{
var height = ignoreHeight ? float.MaxValue : Position.z;
for ( int y = -1; y <= 1; y++ )
{
for ( int x = -1; x <= 1; x++ )
{
if ( x == 0 && y == 0 ) continue;
var cellFound = Grid.GetCell( new IntVector2( GridPosition.x + x, GridPosition.y + y ), height );
if ( cellFound == null ) continue;
if ( IsNeighbour( cellFound ) )
yield return cellFound;
}
}
}
public Cell GetClosestNeighbour( Vector3 position ) => GetNeighbours().OrderBy( x => x.Position.Distance( position ) ).FirstOrDefault();
public Cell GetClosestNeighbourAndConnection( Vector3 position ) => GetNeighbourAndConnections().OrderBy( x => x.Current.Position.Distance( position ) ).FirstOrDefault()?.Current ?? null;
public IEnumerable<AStarNode> GetNeighbourAndConnections( bool ignoreHeight = false ) => GetNeighbourConnections( ignoreHeight ).Concat( CellConnections );
public IEnumerable<AStarNode> GetNeighbourConnections( bool ignoreHeight = false ) => GetNeighbours( ignoreHeight ).Select( x => new AStarNode( x ) );
/// <summary>
/// Return the first cell below spaces where a neighbour is missing
/// </summary>
public Cell GetFirstValidDroppable( int minCellDistance = 1, int maxCellsDistance = 3, float maxHeightDistance = GridSettings.DEFAULT_DROP_HEIGHT )
{
for ( int y = 0; y <= maxCellsDistance * 2; y++ )
{
var spiralY = MathAStar.SpiralPattern( y );
for ( int x = 0; x <= maxCellsDistance * 2; x++ )
{
var spiralX = MathAStar.SpiralPattern( x );
if ( spiralX == 0 && spiralY == 0 ) continue;
if ( Math.Abs( spiralX ) <= minCellDistance && Math.Abs( spiralY ) <= minCellDistance ) continue;
var cellFound = Grid.GetCell( new IntVector2( GridPosition.x + spiralX, GridPosition.y + spiralY ), Position.z );
if ( cellFound == null ) continue;
if ( cellFound == this ) continue;
if ( IsNeighbour( cellFound ) ) continue;
var verticalDistance = Position.z - cellFound.Position.z;
if ( verticalDistance > maxHeightDistance ) continue;
var horizontalDistance = new Vector2( spiralX, spiralY ).Length - 1f;
if ( verticalDistance < Grid.StepSize * horizontalDistance ) continue;
if ( Grid.LineOfSight( this, cellFound ) ) continue;
var clearanceBBox = new BBox( new Vector3( -Grid.WidthClearance / 2f, -Grid.WidthClearance / 2f, 0f ), new Vector3( Grid.WidthClearance / 2f, Grid.WidthClearance / 2f, Grid.HeightClearance - Grid.StepSize ) );
var horizontalClearanceTrace = Grid.Scene.Trace.Box( clearanceBBox, Position + Vector3.Up * Grid.StepSize, cellFound.Position.WithZ( Position.z + Grid.StepSize ) )
.WithGridSettings( Grid.Settings )
.Run();
if ( horizontalClearanceTrace.Hit ) continue;
var verticalClearanceTrace = Grid.Scene.Trace.Box( clearanceBBox, cellFound.Position.WithZ( Position.z + Grid.StepSize ), cellFound.Position + Vector3.Up * Grid.StepSize )
.WithGridSettings( Grid.Settings )
.Run();
if ( verticalClearanceTrace.Hit ) continue;
return cellFound;
}
}
return null;
}
public IEnumerable<Cell> GetValidJumpables( JumpDefinition definition, float maxHeightDistance = GridSettings.DEFAULT_DROP_HEIGHT, bool ignoreConnections = false, bool ignoreLOS = false )
{
var jumpableCells = new List<Cell>();
for ( int side = 0; side < definition.SidesToCheck; side++ )
{
var directionToCheck = Rotation.FromYaw( definition.AngleOffset + 360 / definition.SidesToCheck * side ).Forward;
var horizontalVelocity = directionToCheck * definition.HorizontalSpeed;
var endPosition = Grid.TraceParabola( Position, horizontalVelocity, definition.VerticalSpeed, definition.Gravity, maxHeightDistance );
var cell = Grid.GetCellInArea( endPosition, Grid.WidthClearance );
if ( cell == null || cell == this ) continue;
if ( ignoreLOS || !Grid.IsDirectlyWalkable( this, cell, withConnections: !ignoreConnections ) && (ignoreConnections ? true : !Grid.IsDirectlyWalkable( this, cell, withConnections: false )) )
if ( ignoreLOS || !jumpableCells.Any( otherCell => Grid.IsDirectlyWalkable( otherCell, cell, withConnections: !ignoreConnections ) ) && (ignoreConnections ? true : !jumpableCells.Any( otherCell => Grid.IsDirectlyWalkable( otherCell, cell, withConnections: false ) )) )
if ( ignoreLOS || !CellConnections.Any( otherNode => Grid.IsDirectlyWalkable( otherNode.Current, cell, withConnections: !ignoreConnections ) ) && (ignoreConnections ? true : !CellConnections.Any( otherNode => Grid.IsDirectlyWalkable( otherNode.Current, cell, withConnections: false ) )) )
{
var clearanceBBox = new BBox( new Vector3( -Grid.WidthClearance / 2f, -Grid.WidthClearance / 2f, Grid.StepSize ), new Vector3( Grid.WidthClearance / 2f, Grid.WidthClearance / 2f, Grid.HeightClearance ) );
var jumpTrace = Grid.Scene.Trace.Box( clearanceBBox, endPosition, cell.Position )
.WithGridSettings( Grid.Settings )
.Run();
if ( !jumpTrace.Hit )
jumpableCells.Add( cell );
}
if ( jumpableCells.Count() >= definition.MaxPerCell )
break;
}
return jumpableCells;
}
public Cell GetValidJumpable( JumpDefinition definition, Vector3 directionToCheck, float maxHeightDistance = GridSettings.DEFAULT_DROP_HEIGHT, bool ignoreConnections = false, bool ignoreLOS = false )
{
var horizontalVelocity = directionToCheck * definition.HorizontalSpeed;
var endPosition = Grid.TraceParabola( Position, horizontalVelocity, definition.VerticalSpeed, definition.Gravity, maxHeightDistance );
var cell = Grid.GetCellInArea( endPosition, Grid.WidthClearance );
if ( cell == null ) return null;
if ( ignoreLOS || !Grid.IsDirectlyWalkable( this, cell, withConnections: !ignoreConnections ) && (ignoreConnections ? true : !Grid.IsDirectlyWalkable( this, cell, withConnections: false )) )
if ( ignoreLOS || !CellConnections.Any( otherNode => Grid.IsDirectlyWalkable( otherNode.Current, cell, withConnections: !ignoreConnections ) ) && (ignoreConnections ? true : !CellConnections.Any( otherNode => Grid.IsDirectlyWalkable( otherNode.Current, cell, withConnections: false ) )) )
{
var clearanceBBox = new BBox( new Vector3( -Grid.WidthClearance / 2f, -Grid.WidthClearance / 2f, Grid.StepSize ), new Vector3( Grid.WidthClearance / 2f, Grid.WidthClearance / 2f, Grid.HeightClearance ) );
var jumpTrace = Grid.Scene.Trace.Box( clearanceBBox, endPosition, cell.Position )
.WithGridSettings( Grid.Settings )
.Run();
if ( !jumpTrace.Hit )
return cell;
}
return null;
}
/// <summary>Draw this cell's outline (debug). Scene-System port routes through Scene.DebugOverlay.</summary>
public void Draw( Color color, float duration = 0f, bool depthTest = true, bool drawCenter = false, bool drawCross = false, bool drawCoordinates = false )
{
var overlay = Grid?.Scene?.DebugOverlay;
if ( overlay is null ) return;
overlay.Line( BottomLeft, BottomRight, color, duration, default, !depthTest );
overlay.Line( BottomRight, TopRight, color, duration, default, !depthTest );
overlay.Line( TopRight, TopLeft, color, duration, default, !depthTest );
overlay.Line( TopLeft, BottomLeft, color, duration, default, !depthTest );
if ( drawCross )
{
overlay.Line( BottomLeft, TopRight, color, duration, default, !depthTest );
overlay.Line( TopLeft, BottomRight, color, duration, default, !depthTest );
}
if ( drawCenter )
overlay.Sphere( new Sphere( Position, 5f ), color, duration, default, !depthTest );
}
public void Draw( float duration = 0f, bool depthTest = true, bool drawCenter = false, bool drawCross = false )
=> Draw( Occupied ? Color.Red : Color.White, duration, depthTest, drawCenter, drawCross );
public override bool Equals( object obj )
{
return Equals( obj as Cell );
}
public bool Equals( Cell obj )
{
return obj != null && obj.GetHashCode() == this.GetHashCode();
}
public override int GetHashCode()
{
var gridHash = Grid.GetHashCode();
var positionHash = Position.GetHashCode();
return gridHash + positionHash;
}
}