Park/GridObject.cs
using System;
using HC3.Rides;
using HC3.Terrain;
namespace HC3;
#nullable enable
[Flags]
public enum GridObjectFlags
{
IsWalkable = 1,
BlocksConstruction = 2
// TODO: walkable from different directions
}
public interface IGridObjectEvent : ISceneEvent<IGridObjectEvent>
{
/// <summary>
/// Called if any neighbouring cells changed this update.
/// </summary>
/// <param name="cell">Which cell, occupied by this object, had a changing neighbor.</param>
void NeighborsChanged( GridCell cell ) { }
}
/// <summary>
/// Objects tagged with this component live on the grid.
/// </summary>
public sealed class GridObject : Component
{
private RectInt? _previousBounds;
private GridObjectFlags _flags;
/// <summary>
/// Tile bounds relative to this object's position when not rotated.
/// TODO: more complex bounds for e.g., L-shaped stuff.
/// </summary>
[Property]
public RectInt LocalBounds { get; set; } = new RectInt( 0, 1 );
[Property]
public GridObjectFlags GridFlags
{
get => _flags;
set
{
if ( _flags == value ) return;
var wasWalkable = (_flags & GridObjectFlags.IsWalkable) != 0;
_flags = value;
var isWalkable = (_flags & GridObjectFlags.IsWalkable) != 0;
if ( wasWalkable != isWalkable )
{
if ( isWalkable )
GridManager.Instance?.RegisterWalkable( this );
else
GridManager.Instance?.UnregisterWalkable( this );
}
ForceUpdateNeighbors();
}
}
public bool IsWalkable
{
get => (GridFlags & GridObjectFlags.IsWalkable) != 0;
set => GridFlags = (value ? GridFlags | GridObjectFlags.IsWalkable : GridFlags & ~GridObjectFlags.IsWalkable);
}
public bool BlocksConstruction
{
get => (GridFlags & GridObjectFlags.BlocksConstruction) != 0;
set => GridFlags = (value ? GridFlags | GridObjectFlags.BlocksConstruction : GridFlags & ~GridObjectFlags.BlocksConstruction);
}
public RectInt GridBounds { get; private set; }
/// <summary>
/// How much space does this object take up vertically, in multiples of <see cref="GridManager.HeightStep"/>?
/// </summary>
[Property]
public int Height { get; set; } = 2;
/// <summary>
/// The vertical position of this object in grid space
/// </summary>
public int Level => GridManager.FloorWorldHeightToGrid( WorldPosition.z );
[Property, ReadOnly]
public int RegionId { get; set; } = -1;
protected override void OnAwake()
{
Transform.OnTransformChanged += TransformChanged;
}
protected override void OnEnabled()
{
UpdatePlacement();
if ( IsWalkable )
GridManager.Instance?.RegisterWalkable( this );
}
protected override void OnDisabled()
{
UpdatePlacement();
GridManager.Instance?.UnregisterWalkable( this );
}
protected override void OnDestroy()
{
UpdatePlacement( null );
GridManager.Instance?.UnregisterWalkable( this );
}
private void TransformChanged()
{
UpdatePlacement();
}
private RectInt CalculateBounds()
{
if ( !IsValid ) return default;
return BoundsToWorld( LocalBounds, WorldTransform );
}
public static RectInt BoundsToWorld( RectInt localBounds, Transform worldTransform )
{
var rotation = (int)MathF.Round( worldTransform.Rotation.Yaw() / 90f );
var worldToGrid = GridManager.WorldToGridPosition( worldTransform.Position );
var bounds = RotateBoundsAroundCellCenter( localBounds, rotation );
return new RectInt( bounds.Position + worldToGrid, bounds.Size );
}
public static RectInt BoundsToLocal( RectInt worldBounds, Transform worldTransform )
{
var rotation = (int)MathF.Round( worldTransform.Rotation.Yaw() / 90f );
var worldToGrid = GridManager.WorldToGridPosition( worldTransform.Position );
var bounds = new RectInt( worldBounds.Position - worldToGrid, worldBounds.Size );
return RotateBoundsAroundCellCenter( bounds, -rotation );
}
/// <summary>
/// Rotate grid bounds, while making sure odd-number-sized bounds don't get offset.
/// </summary>
private static RectInt RotateBoundsAroundCellCenter( RectInt bounds, int clockwise90Steps )
{
// For odd numbered side lengths, bias by half a grid cell
// so (0, 1) becomes (-0.5, 0.5) and we get a nice rotation.
var bias = new Vector2Int( bounds.Size.x & 1, bounds.Size.y & 1 );
// We're using int vectors so we can't use 0.5, let's do the rotation
// with everything doubled instead, so the bias is 1 instead of 0.5
bounds *= 2;
bounds.Position -= bias;
bounds = bounds.Rotate( clockwise90Steps );
bounds.Position += (clockwise90Steps & 1) == 0 ? bias : new Vector2Int( bias.y, bias.x );
// Halve everything since we doubled it earlier
return new RectInt( bounds.Position / 2, bounds.Size / 2 );
}
public BBox GetWorldBounds()
{
var bounds = CalculateBounds();
var min = new Vector3( bounds.Position * GridManager.GridSize, WorldPosition.z );
var max = new Vector3( (bounds.Position + bounds.Size) * GridManager.GridSize, min.z + Height * GridManager.HeightStep );
return new BBox( min, max );
}
protected override void DrawGizmos()
{
Gizmo.Transform = global::Transform.Zero;
var worldBounds = GetWorldBounds();
Gizmo.Draw.Color = Color.Yellow;
Gizmo.Draw.LineBBox( worldBounds );
if ( !Scene.IsEditor )
{
Gizmo.Draw.Text( $"R: {RegionId}", new Transform( WorldPosition ) );
}
}
public void UpdatePlacement()
{
GridBounds = CalculateBounds();
RectInt? newBounds = IsValid && Active
? GridBounds
: null;
UpdatePlacement( newBounds );
}
private void UpdatePlacement( RectInt? newBounds, bool forceFlagUpdate = true )
{
if ( Scene.GetAllComponents<GridManager>().FirstOrDefault() is not { } grid ) return;
HashSet<Vector2Int> affected = new();
if ( _previousBounds is { } prevBounds )
{
foreach ( var pos in prevBounds.AsEnumerable() )
{
grid.RemoveInternal( pos, this );
affected.Add( pos );
}
}
_previousBounds = newBounds;
if ( newBounds is { } bounds )
{
foreach ( var pos in bounds.AsEnumerable() )
{
grid.AddInternal( pos, this, forceFlagUpdate );
affected.Add( pos );
}
}
foreach ( var pos in affected )
{
foreach ( var neighbour in grid.GetNeighbors( pos ) )
{
foreach ( var x in neighbour )
{
grid.AddInternal( neighbour.Position, x, forceFlagUpdate );
}
}
}
}
public void ForceUpdateNeighbors() =>
UpdatePlacement( _previousBounds, true );
protected override void OnUpdate()
{
if ( !GridManager.DebugDraw ) return;
var tilePosition = GridManager.WorldToGridPosition3D( WorldPosition );
Gizmo.Draw.Color = Color.White;
Gizmo.Draw.Text( $"{tilePosition}\n(Region = {RegionId})", new Transform( WorldPosition ) );
Gizmo.Transform = new Transform( new Vector3( 0, 0, GridManager.HeightStep ) );
if ( IsWalkable )
{
foreach ( var edge in GridManager.AllEdges )
{
var neighborPos = tilePosition + edge.GetDirection();
var neighbor = GridManager.Instance.GetCell( new Vector2Int( neighborPos ) );
if ( neighbor is null )
continue;
var oppositeEdge = edge.GetOpposite();
bool walkableOut = GridNavigation.Instance.IsWalkable( tilePosition, edge, out var pos1 );
bool walkableIn = GridNavigation.Instance.IsWalkable( pos1, edge.GetOpposite(), out _ );
if ( !walkableOut && !walkableIn )
continue;
var delta = (WorldPosition + GridManager.GridToWorldPosition( pos1 ) + GridManager.CentreOffset) / 2.0f;
Gizmo.Draw.Color = (walkableIn && walkableOut ? Color.Cyan : Color.Orange).WithAlpha( 0.8f );
Gizmo.Draw.Arrow( delta, WorldPosition );
}
}
}
}