557 lines
No EOL
26 KiB
C#
557 lines
No EOL
26 KiB
C#
// Assets/_Project/Scripts/Gameplay/LevelLoader.cs
|
||
using UnityEngine;
|
||
using TD.Core;
|
||
using TD.Levels;
|
||
|
||
namespace TD.Gameplay
|
||
{
|
||
/// <summary>
|
||
/// Match-time loader for a baked <see cref="LevelData"/> asset. Owns the
|
||
/// runtime representation of the level: the buildable-plane physics
|
||
/// collider that click-to-tile raycasts target, and tile-query accessors
|
||
/// for the rest of the gameplay code.
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// Plain <c>MonoBehaviour</c>, not <c>NetworkBehaviour</c>. The baked
|
||
/// LevelData is identical on every peer (same asset, same content), so
|
||
/// there's no value in syncing it through Netcode. Each peer loads
|
||
/// locally on scene-load.
|
||
///
|
||
/// Mutable runtime state (walkability changes as towers are placed) lives
|
||
/// here for now. When server-authoritative tower placement is implemented,
|
||
/// the mutable walkability grid will move to a dedicated
|
||
/// <c>MatchState</c> NetworkBehaviour alongside lives, wave info, etc.
|
||
/// Today's goal is the minimum loader surface area needed for everything
|
||
/// that comes next.
|
||
///
|
||
/// Singleton access: <see cref="Instance"/> is set in <c>Awake</c> and
|
||
/// cleared in <c>OnDestroy</c>. Consumers null-check before use.
|
||
/// </remarks>
|
||
public class LevelLoader : MonoBehaviour
|
||
{
|
||
// ----- Singleton --------------------------------------------------
|
||
|
||
/// <summary>
|
||
/// The currently loaded LevelLoader. Null before any scene with a
|
||
/// LevelLoader has Awake'd, and during scene transitions. Always
|
||
/// null-check before use.
|
||
/// </summary>
|
||
public static LevelLoader Instance { get; private set; }
|
||
|
||
// ----- Inspector fields -------------------------------------------
|
||
|
||
[Header("Level")]
|
||
|
||
[Tooltip("Baked LevelData asset to load. Required.")]
|
||
[SerializeField] private LevelData level;
|
||
|
||
[Tooltip("Name of the physics layer used by the buildable-plane collider. " +
|
||
"Click-to-tile raycasts should target this layer. Layer must exist " +
|
||
"in Project Settings → Tags and Layers.")]
|
||
[SerializeField] private string buildablePlaneLayerName = "BuildablePlane";
|
||
|
||
[Header("Debug Gizmos")]
|
||
|
||
[Tooltip("Draw the grid's bounding rectangle (always visible).")]
|
||
[SerializeField] private bool drawGridBounds = true;
|
||
|
||
[Tooltip("Draw a translucent overlay showing walkable vs. non-walkable tiles.")]
|
||
[SerializeField] private bool drawWalkability = true;
|
||
|
||
[Tooltip("Draw the buildable-plane collider's footprint.")]
|
||
[SerializeField] private bool drawBuildablePlane = true;
|
||
|
||
[Tooltip("Draw owner-grid tile borders. Only visible when LevelLoader is selected.")]
|
||
[SerializeField] private bool drawOwnerBorders = true;
|
||
|
||
// ----- Runtime state ----------------------------------------------
|
||
|
||
/// <summary>The loaded LevelData asset, or null if loading failed.</summary>
|
||
public LevelData LevelData => level;
|
||
|
||
/// <summary>True if the loader successfully initialized at match start.</summary>
|
||
public bool IsLoaded { get; private set; }
|
||
|
||
// The mutable walkability grid. Initialized from LevelData.WalkabilityGrid
|
||
// and mutated by tower placement at runtime. Stays in lockstep with the
|
||
// baked grid until towers are placed.
|
||
//
|
||
// Consumers query through IsWalkable(Vector2Int) and mutate through
|
||
// SetWalkable(Vector2Int, bool), which hide the flat-array indexing.
|
||
//
|
||
// NOTE: This grid will move to MatchState when that NetworkBehaviour is
|
||
// implemented, so server-authoritative mutation is co-located with other
|
||
// match-wide state. For now it lives here because no consumer needed it
|
||
// to live elsewhere.
|
||
private bool[] runtimeWalkability;
|
||
|
||
// Per-tile tower occupancy. True when a tower's footprint covers this tile.
|
||
// Distinct from runtimeWalkability because:
|
||
// (a) spawner and leak-exit tiles are walkable but can never be occupied
|
||
// by a tower — tracking them separately avoids ambiguity.
|
||
// (b) the placement ghost needs to detect tile conflicts independently
|
||
// of walkability (a tile is walkable until the tower is placed,
|
||
// but the ghost should turn red as soon as a prior placement
|
||
// reserves it).
|
||
//
|
||
// Initialized to all-false on Awake. Mutated via SetOccupied; queried
|
||
// via IsOccupied. Will move to MatchState alongside runtimeWalkability.
|
||
private bool[] runtimeOccupied;
|
||
|
||
// The buildable-plane GameObject we instantiated as our child.
|
||
// Cached for inspector debugging and future destruction.
|
||
private GameObject buildablePlaneGO;
|
||
private BoxCollider buildablePlaneCollider;
|
||
|
||
// ----- Lifecycle --------------------------------------------------
|
||
|
||
private void Awake()
|
||
{
|
||
if (Instance != null && Instance != this)
|
||
{
|
||
// Two LevelLoaders in one scene is a setup error -- the bake
|
||
// pipeline assumes one map per scene, and the singleton doesn't
|
||
// make sense with multiples. Log loudly and let the second one
|
||
// keep going inert (Instance still points at the first).
|
||
Debug.LogError(
|
||
$"[LevelLoader] Multiple LevelLoader instances in scene. " +
|
||
$"Existing: '{Instance.gameObject.name}'. New: '{gameObject.name}'. " +
|
||
$"This loader will not initialize.");
|
||
return;
|
||
}
|
||
Instance = this;
|
||
|
||
if (!ValidateInputs())
|
||
{
|
||
IsLoaded = false;
|
||
return;
|
||
}
|
||
|
||
InitializeRuntimeWalkability();
|
||
InitializeRuntimeOccupied();
|
||
SpawnBuildablePlane();
|
||
|
||
IsLoaded = true;
|
||
LogLoadSummary();
|
||
}
|
||
|
||
private void OnDestroy()
|
||
{
|
||
if (Instance == this)
|
||
{
|
||
Instance = null;
|
||
}
|
||
// The buildable plane GameObject is our child, so Unity will
|
||
// destroy it automatically when this object is destroyed.
|
||
}
|
||
|
||
// ----- Loading steps ----------------------------------------------
|
||
|
||
private bool ValidateInputs()
|
||
{
|
||
if (level == null)
|
||
{
|
||
Debug.LogError(
|
||
$"[LevelLoader] '{gameObject.name}': LevelData reference is not assigned. " +
|
||
$"Assign a baked LevelData asset in the inspector.");
|
||
return false;
|
||
}
|
||
|
||
// Grid arrays must be present and consistent. An unbaked LevelData
|
||
// has empty arrays; a corrupted one might have mismatched sizes.
|
||
int expectedLength = level.GridSize.x * level.GridSize.y;
|
||
if (level.GridSize.x <= 0 || level.GridSize.y <= 0 || expectedLength <= 0)
|
||
{
|
||
Debug.LogError(
|
||
$"[LevelLoader] '{level.name}' has invalid grid size {level.GridSize}. " +
|
||
$"Re-bake the level.");
|
||
return false;
|
||
}
|
||
if (level.WalkabilityGrid == null || level.WalkabilityGrid.Length != expectedLength ||
|
||
level.PlacementGrid == null || level.PlacementGrid.Length != expectedLength ||
|
||
level.OwnerGrid == null || level.OwnerGrid.Length != expectedLength)
|
||
{
|
||
Debug.LogError(
|
||
$"[LevelLoader] '{level.name}' has inconsistent grid arrays. " +
|
||
$"Expected each grid to have {expectedLength} entries " +
|
||
$"(GridSize {level.GridSize.x}×{level.GridSize.y}). Re-bake the level.");
|
||
return false;
|
||
}
|
||
|
||
// Physics layer must exist. NameToLayer returns -1 for unknown
|
||
// names; that's the failure signal.
|
||
int layerIndex = LayerMask.NameToLayer(buildablePlaneLayerName);
|
||
if (layerIndex < 0)
|
||
{
|
||
Debug.LogError(
|
||
$"[LevelLoader] Physics layer '{buildablePlaneLayerName}' does not exist. " +
|
||
$"Create it in Project Settings → Tags and Layers, then assign it to the " +
|
||
$"buildablePlaneLayerName field if you used a different name.");
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
private void InitializeRuntimeWalkability()
|
||
{
|
||
// Copy the baked walkability into a runtime array. We don't reuse
|
||
// level.WalkabilityGrid directly because (a) the runtime grid will
|
||
// mutate as towers are placed, and we don't want to mutate the
|
||
// ScriptableObject asset; (b) ScriptableObject array fields are
|
||
// shared across the editor and runtime, so mutating it would persist
|
||
// tower placements across Play sessions.
|
||
runtimeWalkability = new bool[level.WalkabilityGrid.Length];
|
||
System.Array.Copy(
|
||
level.WalkabilityGrid, runtimeWalkability, level.WalkabilityGrid.Length);
|
||
}
|
||
|
||
private void InitializeRuntimeOccupied()
|
||
{
|
||
// All tiles start unoccupied. bool[] default-initializes to false,
|
||
// so Array.Clear is redundant but makes intent explicit.
|
||
runtimeOccupied = new bool[level.WalkabilityGrid.Length];
|
||
}
|
||
|
||
private void SpawnBuildablePlane()
|
||
{
|
||
// Compute the world-space center and size of the grid.
|
||
//
|
||
// The grid covers tiles from GridOriginTile (inclusive, SW corner)
|
||
// to GridOriginTile + GridSize - (1,1) (inclusive, NE corner).
|
||
// Each tile is TILE_SIZE wide and centered on its integer coords.
|
||
//
|
||
// World extent on X:
|
||
// left = (GridOriginTile.x - 0.5) * TILE_SIZE
|
||
// right = (GridOriginTile.x + GridSize.x - 0.5) * TILE_SIZE
|
||
// width = GridSize.x * TILE_SIZE
|
||
// centerX = (left + right) / 2 = (GridOriginTile.x + (GridSize.x - 1) / 2) * TILE_SIZE
|
||
//
|
||
// Same shape on Z (grid-y maps to world-z).
|
||
float worldCenterX =
|
||
(level.GridOriginTile.x + (level.GridSize.x - 1) * 0.5f) * GridCoordinates.TILE_SIZE;
|
||
float worldCenterZ =
|
||
(level.GridOriginTile.y + (level.GridSize.y - 1) * 0.5f) * GridCoordinates.TILE_SIZE;
|
||
float worldSizeX = level.GridSize.x * GridCoordinates.TILE_SIZE;
|
||
float worldSizeZ = level.GridSize.y * GridCoordinates.TILE_SIZE;
|
||
|
||
buildablePlaneGO = new GameObject("__BuildablePlane");
|
||
buildablePlaneGO.transform.SetParent(transform, worldPositionStays: false);
|
||
buildablePlaneGO.transform.localPosition = Vector3.zero;
|
||
buildablePlaneGO.transform.localRotation = Quaternion.identity;
|
||
buildablePlaneGO.transform.localScale = Vector3.one;
|
||
buildablePlaneGO.layer = LayerMask.NameToLayer(buildablePlaneLayerName);
|
||
|
||
buildablePlaneCollider = buildablePlaneGO.AddComponent<BoxCollider>();
|
||
// Position the collider center in WORLD space coords by setting
|
||
// BoxCollider.center after the GameObject is at origin. Since the
|
||
// GO transform is identity at origin, local == world.
|
||
buildablePlaneCollider.center = new Vector3(
|
||
worldCenterX, GridCoordinates.BUILDABLE_PLANE_Y, worldCenterZ);
|
||
// Y size: a thin sliver. We give it a non-zero height so raycasts
|
||
// from above and below both register, and so floating-point
|
||
// imprecision in raycast origin doesn't make hits fail at the
|
||
// exact Y=0 plane. 0.1 world units is plenty.
|
||
buildablePlaneCollider.size = new Vector3(worldSizeX, 0.1f, worldSizeZ);
|
||
// Default isTrigger=false; raycasts hit it as a solid collider.
|
||
// If we ever want it to ignore physics simulation while still
|
||
// being raycastable, isTrigger=true also works for Raycast (with
|
||
// QueryTriggerInteraction.Collide).
|
||
}
|
||
|
||
private void LogLoadSummary()
|
||
{
|
||
Vector3 c = buildablePlaneCollider.center;
|
||
Vector3 s = buildablePlaneCollider.size;
|
||
Debug.Log(
|
||
$"[LevelLoader] Loaded '{level.MapName}' " +
|
||
$"(playerCount={level.PlayerCount}, " +
|
||
$"grid {level.GridSize.x}×{level.GridSize.y} from origin {level.GridOriginTile}). " +
|
||
$"Buildable plane at world ({c.x:F2}, {c.y:F2}, {c.z:F2}) " +
|
||
$"size ({s.x:F2}, {s.y:F2}, {s.z:F2}) on layer '{buildablePlaneLayerName}'.");
|
||
}
|
||
|
||
// ----- Public tile queries ----------------------------------------
|
||
//
|
||
// All queries take WORLD-TILE coordinates (the same coordinates
|
||
// GridCoordinates.WorldToGrid returns). Internally we translate by
|
||
// GridOriginTile to index into the flat arrays. Consumers should
|
||
// never see grid-array indices.
|
||
|
||
/// <summary>True if <paramref name="tile"/> is within the loaded grid's bounds.</summary>
|
||
public bool InBounds(Vector2Int tile)
|
||
{
|
||
if (!IsLoaded) return false;
|
||
int x = tile.x - level.GridOriginTile.x;
|
||
int y = tile.y - level.GridOriginTile.y;
|
||
return x >= 0 && x < level.GridSize.x && y >= 0 && y < level.GridSize.y;
|
||
}
|
||
|
||
/// <summary>
|
||
/// True if <paramref name="tile"/> is part of the playable map area (inside any
|
||
/// MapAreaVolume at bake time). Returns false for out-of-bounds tiles and for in-bounds
|
||
/// "void" tiles outside the map area. This is the outermost gate — gameplay queries
|
||
/// (IsWalkable, GetPlacement, GetOwner) are only meaningful where IsInMap is true.
|
||
///
|
||
/// Use this for: builder movement clamp, camera pan clamp, minimap rendering bounds.
|
||
/// </summary>
|
||
public bool IsInMap(Vector2Int tile)
|
||
{
|
||
if (!TryFlatIndex(tile, out int idx)) return false;
|
||
// Defensive: existing maps that haven't been re-baked since MapAreaGrid was added
|
||
// will have a null array. Treat that as "not in map" so callers don't false-positive.
|
||
if (level.MapAreaGrid == null || level.MapAreaGrid.Length == 0) return false;
|
||
return level.MapAreaGrid[idx];
|
||
}
|
||
|
||
/// <summary>
|
||
/// True if <paramref name="tile"/> is currently walkable. Returns
|
||
/// false for out-of-bounds tiles. Reflects the runtime walkability
|
||
/// grid (which will mutate as towers are placed in future work).
|
||
/// </summary>
|
||
public bool IsWalkable(Vector2Int tile)
|
||
{
|
||
if (!TryFlatIndex(tile, out int idx)) return false;
|
||
return runtimeWalkability[idx];
|
||
}
|
||
|
||
/// <summary>
|
||
/// Placement state for <paramref name="tile"/>. Returns
|
||
/// <see cref="PlacementState.Outside"/> for out-of-bounds tiles.
|
||
/// Reflects the baked placement grid; this does NOT change at runtime.
|
||
/// </summary>
|
||
public PlacementState GetPlacement(Vector2Int tile)
|
||
{
|
||
if (!TryFlatIndex(tile, out int idx)) return PlacementState.Outside;
|
||
return level.PlacementGrid[idx];
|
||
}
|
||
|
||
/// <summary>
|
||
/// Owning player for <paramref name="tile"/>. Returns
|
||
/// <see cref="PlayerSlot.None"/> for out-of-bounds tiles or tiles not
|
||
/// inside any player zone.
|
||
/// </summary>
|
||
public PlayerSlot GetOwner(Vector2Int tile)
|
||
{
|
||
if (!TryFlatIndex(tile, out int idx)) return PlayerSlot.None;
|
||
return level.OwnerGrid[idx];
|
||
}
|
||
|
||
/// <summary>
|
||
/// True if <paramref name="tile"/> is currently occupied by a placed tower footprint.
|
||
/// Returns false for out-of-bounds tiles. Unlike <see cref="IsWalkable"/>, this grid
|
||
/// starts all-false and only becomes true when a tower is successfully placed.
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// The placement ghost uses this (alongside <see cref="GetPlacement"/> and
|
||
/// <see cref="GetOwner"/>) to decide whether to render as valid (white) or invalid (red).
|
||
/// The server uses it as part of the tile-availability check before path validation.
|
||
/// </remarks>
|
||
public bool IsOccupied(Vector2Int tile)
|
||
{
|
||
if (!TryFlatIndex(tile, out int idx)) return false;
|
||
return runtimeOccupied[idx];
|
||
}
|
||
|
||
// ----- Runtime mutators -------------------------------------------
|
||
//
|
||
// Called by TowerPlacementManager (server-side) when a tower placement
|
||
// is accepted. Both mutators must be called for every tile in the tower's
|
||
// footprint so the two grids stay in sync.
|
||
//
|
||
// These are not RPCs — LevelLoader is a plain MonoBehaviour, not a
|
||
// NetworkBehaviour. The server calls these directly after authoritative
|
||
// validation; clients learn about the change when the TowerInstance
|
||
// NetworkObject spawns and its Start/OnNetworkSpawn stamps its own
|
||
// footprint locally.
|
||
|
||
/// <summary>
|
||
/// Fired on every peer whenever <see cref="SetWalkable"/> changes a tile's
|
||
/// walkability. <see cref="TD.Gameplay.PathfindingService"/> subscribes to
|
||
/// invalidate cached paths so in-flight enemies reroute after a tower is
|
||
/// placed or removed.
|
||
/// </summary>
|
||
public event System.Action OnWalkabilityChanged;
|
||
|
||
/// <summary>
|
||
/// Sets the runtime walkability of <paramref name="tile"/>. Called by
|
||
/// <c>TowerPlacementManager</c> on the server when a tower is accepted (pass
|
||
/// <c>false</c>) and when a tower is sold/destroyed (pass <c>true</c>).
|
||
/// No-ops silently for out-of-bounds tiles.
|
||
/// </summary>
|
||
public void SetWalkable(Vector2Int tile, bool walkable)
|
||
{
|
||
if (!TryFlatIndex(tile, out int idx)) return;
|
||
if (runtimeWalkability[idx] == walkable) return; // no change — don't fire event
|
||
runtimeWalkability[idx] = walkable;
|
||
OnWalkabilityChanged?.Invoke();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Sets the runtime occupancy of <paramref name="tile"/>. Called alongside
|
||
/// <see cref="SetWalkable"/> — always update both grids together so they
|
||
/// stay in sync. Pass <c>true</c> when a tower is placed, <c>false</c> when
|
||
/// it is sold or destroyed.
|
||
/// No-ops silently for out-of-bounds tiles.
|
||
/// </summary>
|
||
public void SetOccupied(Vector2Int tile, bool occupied)
|
||
{
|
||
if (!TryFlatIndex(tile, out int idx)) return;
|
||
runtimeOccupied[idx] = occupied;
|
||
}
|
||
|
||
// Translates world-tile coordinates to a flat-array index, returning
|
||
// false if the tile is out of bounds. Used by all query methods.
|
||
private bool TryFlatIndex(Vector2Int tile, out int idx)
|
||
{
|
||
idx = 0;
|
||
if (!IsLoaded) return false;
|
||
int x = tile.x - level.GridOriginTile.x;
|
||
int y = tile.y - level.GridOriginTile.y;
|
||
if (x < 0 || x >= level.GridSize.x || y < 0 || y >= level.GridSize.y) return false;
|
||
idx = y * level.GridSize.x + x;
|
||
return true;
|
||
}
|
||
|
||
// ----- Gizmos -----------------------------------------------------
|
||
//
|
||
// Gizmos run in both edit mode and play mode. In edit mode we use the
|
||
// baked walkability/owner grids from the LevelData asset directly,
|
||
// because runtimeWalkability hasn't been initialized yet. In play mode
|
||
// we use runtimeWalkability so the visualization reflects tower stamps.
|
||
// Owner and placement grids are immutable, so we read them from the
|
||
// asset in both modes.
|
||
|
||
private void OnDrawGizmos()
|
||
{
|
||
if (level == null) return;
|
||
if (level.GridSize.x <= 0 || level.GridSize.y <= 0) return;
|
||
|
||
if (drawGridBounds) DrawGridBoundsGizmo();
|
||
if (drawBuildablePlane) DrawBuildablePlaneGizmo();
|
||
if (drawWalkability) DrawWalkabilityGizmo();
|
||
}
|
||
|
||
private void OnDrawGizmosSelected()
|
||
{
|
||
if (level == null) return;
|
||
if (level.GridSize.x <= 0 || level.GridSize.y <= 0) return;
|
||
|
||
if (drawOwnerBorders) DrawOwnerBordersGizmo();
|
||
}
|
||
|
||
private void DrawGridBoundsGizmo()
|
||
{
|
||
// One outlined wire box covering the entire grid extent.
|
||
float halfTile = GridCoordinates.TILE_SIZE * 0.5f;
|
||
Vector3 sw = new Vector3(
|
||
level.GridOriginTile.x * GridCoordinates.TILE_SIZE - halfTile,
|
||
GridCoordinates.BUILDABLE_PLANE_Y,
|
||
level.GridOriginTile.y * GridCoordinates.TILE_SIZE - halfTile);
|
||
Vector3 ne = new Vector3(
|
||
(level.GridOriginTile.x + level.GridSize.x) * GridCoordinates.TILE_SIZE - halfTile,
|
||
GridCoordinates.BUILDABLE_PLANE_Y,
|
||
(level.GridOriginTile.y + level.GridSize.y) * GridCoordinates.TILE_SIZE - halfTile);
|
||
|
||
Gizmos.color = new Color(1f, 1f, 1f, 0.9f); // bright white outline
|
||
Vector3 nw = new Vector3(sw.x, sw.y, ne.z);
|
||
Vector3 se = new Vector3(ne.x, sw.y, sw.z);
|
||
Gizmos.DrawLine(sw, nw);
|
||
Gizmos.DrawLine(nw, ne);
|
||
Gizmos.DrawLine(ne, se);
|
||
Gizmos.DrawLine(se, sw);
|
||
}
|
||
|
||
private void DrawBuildablePlaneGizmo()
|
||
{
|
||
// In play mode the collider exists; draw it directly. In edit mode
|
||
// we don't have a collider yet, but we can draw the rectangle that
|
||
// the loader WOULD instantiate, so designers can preview it.
|
||
float worldCenterX =
|
||
(level.GridOriginTile.x + (level.GridSize.x - 1) * 0.5f) * GridCoordinates.TILE_SIZE;
|
||
float worldCenterZ =
|
||
(level.GridOriginTile.y + (level.GridSize.y - 1) * 0.5f) * GridCoordinates.TILE_SIZE;
|
||
float worldSizeX = level.GridSize.x * GridCoordinates.TILE_SIZE;
|
||
float worldSizeZ = level.GridSize.y * GridCoordinates.TILE_SIZE;
|
||
|
||
Gizmos.color = new Color(0.3f, 0.6f, 1f, 0.10f); // translucent blue fill
|
||
Gizmos.DrawCube(
|
||
new Vector3(worldCenterX, GridCoordinates.BUILDABLE_PLANE_Y, worldCenterZ),
|
||
new Vector3(worldSizeX, 0.02f, worldSizeZ));
|
||
}
|
||
|
||
private void DrawWalkabilityGizmo()
|
||
{
|
||
// Per-tile translucent fill: green tint for walkable, red tint
|
||
// for non-walkable. Edit mode reads from the baked asset; play
|
||
// mode reads from the runtime grid.
|
||
bool[] walk = (Application.isPlaying && runtimeWalkability != null)
|
||
? runtimeWalkability
|
||
: level.WalkabilityGrid;
|
||
if (walk == null || walk.Length != level.GridSize.x * level.GridSize.y) return;
|
||
|
||
Color walkable = new Color(0.2f, 0.9f, 0.2f, 0.10f);
|
||
Color blocked = new Color(0.9f, 0.2f, 0.2f, 0.20f);
|
||
|
||
float tile = GridCoordinates.TILE_SIZE;
|
||
// Slight Y offset so this sits above the buildable-plane gizmo
|
||
// rather than z-fighting with it.
|
||
float drawY = GridCoordinates.BUILDABLE_PLANE_Y + 0.005f;
|
||
Vector3 size = new Vector3(tile * 0.95f, 0.001f, tile * 0.95f);
|
||
|
||
for (int y = 0; y < level.GridSize.y; y++)
|
||
{
|
||
for (int x = 0; x < level.GridSize.x; x++)
|
||
{
|
||
int idx = y * level.GridSize.x + x;
|
||
Gizmos.color = walk[idx] ? walkable : blocked;
|
||
Vector3 c = new Vector3(
|
||
(level.GridOriginTile.x + x) * tile,
|
||
drawY,
|
||
(level.GridOriginTile.y + y) * tile);
|
||
Gizmos.DrawCube(c, size);
|
||
}
|
||
}
|
||
}
|
||
|
||
private void DrawOwnerBordersGizmo()
|
||
{
|
||
// One thin outlined square per tile that has an owner, colored
|
||
// with that owner's player color. Drawn only when LevelLoader is
|
||
// selected to keep the scene view from getting too noisy.
|
||
if (level.OwnerGrid == null ||
|
||
level.OwnerGrid.Length != level.GridSize.x * level.GridSize.y) return;
|
||
|
||
float tile = GridCoordinates.TILE_SIZE;
|
||
float halfTile = tile * 0.5f;
|
||
float drawY = GridCoordinates.BUILDABLE_PLANE_Y + 0.010f;
|
||
|
||
for (int y = 0; y < level.GridSize.y; y++)
|
||
{
|
||
for (int x = 0; x < level.GridSize.x; x++)
|
||
{
|
||
int idx = y * level.GridSize.x + x;
|
||
PlayerSlot owner = level.OwnerGrid[idx];
|
||
if (owner == PlayerSlot.None) continue;
|
||
|
||
Vector3 c = new Vector3(
|
||
(level.GridOriginTile.x + x) * tile,
|
||
drawY,
|
||
(level.GridOriginTile.y + y) * tile);
|
||
|
||
Gizmos.color = PlayerColors.Get(owner);
|
||
// Draw four edges as a wire square. We could DrawWireCube
|
||
// but it would also draw vertical edges we don't want.
|
||
Vector3 sw = new Vector3(c.x - halfTile, drawY, c.z - halfTile);
|
||
Vector3 nw = new Vector3(c.x - halfTile, drawY, c.z + halfTile);
|
||
Vector3 ne = new Vector3(c.x + halfTile, drawY, c.z + halfTile);
|
||
Vector3 se = new Vector3(c.x + halfTile, drawY, c.z - halfTile);
|
||
Gizmos.DrawLine(sw, nw);
|
||
Gizmos.DrawLine(nw, ne);
|
||
Gizmos.DrawLine(ne, se);
|
||
Gizmos.DrawLine(se, sw);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} |