Minimap!
This commit is contained in:
parent
f7720a9915
commit
6c37e569ab
18 changed files with 1169 additions and 323 deletions
502
Assets/_Project/Scripts/UI/Minimap/MinimapView.cs
Normal file
502
Assets/_Project/Scripts/UI/Minimap/MinimapView.cs
Normal file
|
|
@ -0,0 +1,502 @@
|
|||
// Assets/_Project/Scripts/UI/Minimap/MinimapView.cs
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using TD.Core;
|
||||
using TD.Gameplay;
|
||||
using TD.Levels;
|
||||
|
||||
namespace TD.UI.Minimap
|
||||
{
|
||||
/// <summary>
|
||||
/// UI Toolkit minimap controller. Manages two stacked sub-elements (terrain + entities)
|
||||
/// inside a host <see cref="VisualElement"/>, handles click-to-jump, drag-to-pan,
|
||||
/// right-click-to-move-builder, and scroll-wheel zoom against a <see cref="CameraController"/>
|
||||
/// and the local <see cref="SelectionState"/>. Refreshes the entity overlay at a throttled
|
||||
/// rate so 500+ enemies don't dominate frame time.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para><b>Coordinate spaces.</b> Three are in play:
|
||||
/// <list type="bullet">
|
||||
/// <item><b>World</b> — XZ plane positions of game-side entities. Z+ is north.</item>
|
||||
/// <item><b>UI local</b> — pixel coordinates within the minimap container. Y+ is DOWN.</item>
|
||||
/// <item><b>Texture</b> — handled entirely by the baker; not exposed here.</item>
|
||||
/// </list>
|
||||
/// World↔UI conversion uses the currently-visible world rectangle, which is the full map
|
||||
/// at zoom 1 and a smaller rect centered on <see cref="viewCenter"/> at zoom > 1.</para>
|
||||
///
|
||||
/// <para><b>Zoom model.</b> Zoom is anchored to the cursor position: the world point under
|
||||
/// the cursor before the scroll stays under the cursor after, so the user can dial in on a
|
||||
/// region by hovering over it and scrolling. Zoom defaults to 1 (fully zoomed out) and is
|
||||
/// clamped to <see cref="MaxZoom"/>. When zoom returns to 1 the visible rect snaps back to
|
||||
/// the full map.</para>
|
||||
///
|
||||
/// <para><b>Terrain rendering.</b> The terrain sub-element is sized and positioned so the
|
||||
/// currently-visible world rect maps to the container's contentRect. At zoom 1 the element
|
||||
/// fills the container exactly; at higher zoom it grows (in pixels) past the container and
|
||||
/// the container's <c>overflow: hidden</c> clips the off-view portion. The texture itself
|
||||
/// is baked once and never re-baked.</para>
|
||||
/// </remarks>
|
||||
public class MinimapView
|
||||
{
|
||||
// ----- Tuning -----------------------------------------------------
|
||||
|
||||
/// <summary>Entity overlay refresh rate. 20Hz handles 500+ enemies comfortably without
|
||||
/// burning the frame budget on Painter2D. Revisit if testing shows it's not enough.</summary>
|
||||
private const float EntityRepaintHz = 20f;
|
||||
|
||||
private const float MinZoom = 1f;
|
||||
private const float MaxZoom = 4f;
|
||||
/// <summary>Multiplicative factor applied per scroll-wheel tick. 1.2 ≈ 6 ticks to span
|
||||
/// 1x → 4x, which feels neither sluggish nor twitchy in playtest.</summary>
|
||||
private const float ZoomStepFactor = 1.2f;
|
||||
|
||||
// Pixel-size floor for point-like icons (builders, enemies). Ensures they stay visible
|
||||
// even at full zoom-out on a large map. Towers use their actual footprint instead and
|
||||
// do not need a floor — they're always at least a couple of pixels by virtue of being
|
||||
// 2×2 or larger.
|
||||
private const float BuilderMinRadiusPx = 2.4f;
|
||||
private const float EnemyMinRadiusPx = 1.5f;
|
||||
|
||||
// Outline added to builder icons so they read against same-color zone fill.
|
||||
private static readonly Color BuilderOutline = new Color(1f, 1f, 1f, 0.85f);
|
||||
|
||||
// ----- Refs -------------------------------------------------------
|
||||
|
||||
private readonly VisualElement container;
|
||||
private readonly VisualElement terrainLayer;
|
||||
private readonly VisualElement entityLayer;
|
||||
private readonly CameraController cameraController;
|
||||
|
||||
// ----- Bake state -------------------------------------------------
|
||||
|
||||
// World-space rectangle the texture covers (the full map). Cached once at bake.
|
||||
private Vector2 worldMin;
|
||||
private Vector2 worldMax;
|
||||
|
||||
// Owned texture — kept so we can destroy it cleanly. Null until first successful bake.
|
||||
private Texture2D bakedTerrain;
|
||||
|
||||
// True once terrain has been baked successfully.
|
||||
private bool ready;
|
||||
|
||||
// ----- View state -------------------------------------------------
|
||||
|
||||
// Current zoom in [MinZoom, MaxZoom]. 1 = full map visible.
|
||||
private float zoom = 1f;
|
||||
|
||||
// World-XZ point the visible rect is centered on. At zoom 1 this is forced to the
|
||||
// world center; at higher zoom the user's scrolling determines it (clamped so the
|
||||
// visible rect never leaves the map).
|
||||
private Vector2 viewCenter;
|
||||
|
||||
// Visible world rect, derived from zoom + viewCenter in ApplyView().
|
||||
private Vector2 visibleWorldMin;
|
||||
private Vector2 visibleWorldMax;
|
||||
|
||||
// ----- Input state ------------------------------------------------
|
||||
|
||||
private bool isDragging;
|
||||
private int dragPointerId;
|
||||
|
||||
// ----- Repaint throttle -------------------------------------------
|
||||
|
||||
private float lastEntityRepaintTime;
|
||||
|
||||
// ----- Construction -----------------------------------------------
|
||||
|
||||
public MinimapView(VisualElement container, CameraController cameraController)
|
||||
{
|
||||
this.container = container;
|
||||
this.cameraController = cameraController;
|
||||
|
||||
// Two stacked sub-elements. Terrain is painted via background-image; the entity
|
||||
// overlay uses generateVisualContent + Painter2D. Both are PickingMode.Ignore so
|
||||
// pointer events bubble back to the container, where we capture them.
|
||||
terrainLayer = new VisualElement { name = "minimap-terrain" };
|
||||
terrainLayer.AddToClassList("minimap-terrain");
|
||||
terrainLayer.pickingMode = PickingMode.Ignore;
|
||||
container.Add(terrainLayer);
|
||||
|
||||
entityLayer = new VisualElement { name = "minimap-entities" };
|
||||
entityLayer.AddToClassList("minimap-entities");
|
||||
entityLayer.pickingMode = PickingMode.Ignore;
|
||||
entityLayer.generateVisualContent += DrawEntities;
|
||||
container.Add(entityLayer);
|
||||
|
||||
// Pointer events on the container drive click-to-jump, drag-to-pan, right-click-move.
|
||||
container.RegisterCallback<PointerDownEvent>(OnPointerDown);
|
||||
container.RegisterCallback<PointerMoveEvent>(OnPointerMove);
|
||||
container.RegisterCallback<PointerUpEvent>(OnPointerUp);
|
||||
container.RegisterCallback<PointerCaptureOutEvent>(OnPointerCaptureOut);
|
||||
container.RegisterCallback<WheelEvent>(OnWheel);
|
||||
|
||||
// Recompute terrain layout when the container resizes — the absolute pixel
|
||||
// positions we set on terrainLayer are container-relative.
|
||||
container.RegisterCallback<GeometryChangedEvent>(_ => { if (ready) ApplyView(); });
|
||||
}
|
||||
|
||||
// ----- Lifecycle --------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Called from <c>HUDController.Update</c>. Performs lazy bake on the first frame
|
||||
/// after <see cref="LevelLoader"/> finishes loading, then triggers throttled entity
|
||||
/// overlay repaints.
|
||||
/// </summary>
|
||||
public void Tick()
|
||||
{
|
||||
if (!ready)
|
||||
{
|
||||
TryBake();
|
||||
if (!ready) return;
|
||||
}
|
||||
|
||||
float now = Time.unscaledTime;
|
||||
if (now - lastEntityRepaintTime >= 1f / EntityRepaintHz)
|
||||
{
|
||||
lastEntityRepaintTime = now;
|
||||
entityLayer.MarkDirtyRepaint();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Releases the baked texture. Call from <c>HUDController.OnDestroy</c>.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (bakedTerrain != null)
|
||||
{
|
||||
Object.Destroy(bakedTerrain);
|
||||
bakedTerrain = null;
|
||||
}
|
||||
ready = false;
|
||||
}
|
||||
|
||||
// ----- Bake -------------------------------------------------------
|
||||
|
||||
private void TryBake()
|
||||
{
|
||||
var loader = LevelLoader.Instance;
|
||||
if (loader == null || !loader.IsLoaded) return;
|
||||
|
||||
bakedTerrain = MinimapTerrainBaker.Bake(loader);
|
||||
if (bakedTerrain == null) return;
|
||||
|
||||
terrainLayer.style.backgroundImage =
|
||||
new StyleBackground(Background.FromTexture2D(bakedTerrain));
|
||||
|
||||
// World extents of the baked rectangle. Tile (n) covers world n - 0.5 to n + 0.5.
|
||||
var data = loader.LevelData;
|
||||
float halfTile = GridCoordinates.TILE_SIZE * 0.5f;
|
||||
float minX = data.GridOriginTile.x * GridCoordinates.TILE_SIZE - halfTile;
|
||||
float maxX = (data.GridOriginTile.x + data.GridSize.x) * GridCoordinates.TILE_SIZE - halfTile;
|
||||
float minZ = data.GridOriginTile.y * GridCoordinates.TILE_SIZE - halfTile;
|
||||
float maxZ = (data.GridOriginTile.y + data.GridSize.y) * GridCoordinates.TILE_SIZE - halfTile;
|
||||
worldMin = new Vector2(minX, minZ);
|
||||
worldMax = new Vector2(maxX, maxZ);
|
||||
|
||||
// Default view: centered, fully zoomed out.
|
||||
zoom = MinZoom;
|
||||
viewCenter = (worldMin + worldMax) * 0.5f;
|
||||
ApplyView();
|
||||
|
||||
ready = true;
|
||||
}
|
||||
|
||||
// ----- View math --------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Recomputes <see cref="visibleWorldMin"/>/<see cref="visibleWorldMax"/> from the
|
||||
/// current <see cref="zoom"/> and <see cref="viewCenter"/>, clamps the view to stay
|
||||
/// within the map, and resizes/positions the terrain sub-element so its rendered
|
||||
/// region matches the visible world rect.
|
||||
/// </summary>
|
||||
private void ApplyView()
|
||||
{
|
||||
var rect = container.contentRect;
|
||||
if (rect.width <= 0f || rect.height <= 0f) return;
|
||||
|
||||
// Half-extents of the visible world rect.
|
||||
float halfWorldX = (worldMax.x - worldMin.x) * 0.5f / zoom;
|
||||
float halfWorldZ = (worldMax.y - worldMin.y) * 0.5f / zoom;
|
||||
|
||||
// Clamp viewCenter so the visible rect doesn't leave the map. At zoom 1 the
|
||||
// clamp range collapses to a single point (the world center), forcing the view.
|
||||
viewCenter.x = Mathf.Clamp(viewCenter.x,
|
||||
worldMin.x + halfWorldX, worldMax.x - halfWorldX);
|
||||
viewCenter.y = Mathf.Clamp(viewCenter.y,
|
||||
worldMin.y + halfWorldZ, worldMax.y - halfWorldZ);
|
||||
|
||||
visibleWorldMin = new Vector2(viewCenter.x - halfWorldX, viewCenter.y - halfWorldZ);
|
||||
visibleWorldMax = new Vector2(viewCenter.x + halfWorldX, viewCenter.y + halfWorldZ);
|
||||
|
||||
// Position the terrain element so visibleWorldMin maps to container (0, 0) — in
|
||||
// UI coords where y is top-down and the texture is flipped to put north at top.
|
||||
// Terrain at zoom z is rect.size * z big, and we offset it so the visible window
|
||||
// lands inside the container.
|
||||
float worldRangeX = worldMax.x - worldMin.x;
|
||||
float worldRangeZ = worldMax.y - worldMin.y;
|
||||
|
||||
float terrainW = rect.width * zoom;
|
||||
float terrainH = rect.height * zoom;
|
||||
float left = -(visibleWorldMin.x - worldMin.x) / worldRangeX * terrainW;
|
||||
// Top: container's top corresponds to visibleWorldMax.z. Distance from texture's
|
||||
// top (worldMax.z) to visibleWorldMax.z, as a fraction of world range, times height.
|
||||
float top = -(worldMax.y - visibleWorldMax.y) / worldRangeZ * terrainH;
|
||||
|
||||
terrainLayer.style.width = terrainW;
|
||||
terrainLayer.style.height = terrainH;
|
||||
terrainLayer.style.left = left;
|
||||
terrainLayer.style.top = top;
|
||||
terrainLayer.style.right = StyleKeyword.Auto;
|
||||
terrainLayer.style.bottom = StyleKeyword.Auto;
|
||||
|
||||
// Entity overlay needs a redraw to reflect the new visible rect.
|
||||
entityLayer.MarkDirtyRepaint();
|
||||
}
|
||||
|
||||
// ----- Pointer handling -------------------------------------------
|
||||
|
||||
private void OnPointerDown(PointerDownEvent evt)
|
||||
{
|
||||
if (!ready) return;
|
||||
|
||||
if (evt.button == 0)
|
||||
{
|
||||
// Left button: jump camera + start drag-to-pan.
|
||||
if (cameraController == null) return;
|
||||
isDragging = true;
|
||||
dragPointerId = evt.pointerId;
|
||||
container.CapturePointer(evt.pointerId);
|
||||
cameraController.BeginDrag();
|
||||
cameraController.JumpTo(UIToWorld(evt.localPosition));
|
||||
evt.StopPropagation();
|
||||
}
|
||||
else if (evt.button == 1)
|
||||
{
|
||||
// Right button: move-and-pause command on the selected builder. Fires
|
||||
// immediately; no drag/capture semantics (single-shot like an RTS).
|
||||
HandleRightClickMove(evt.localPosition);
|
||||
evt.StopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPointerMove(PointerMoveEvent evt)
|
||||
{
|
||||
if (!isDragging || evt.pointerId != dragPointerId) return;
|
||||
cameraController.JumpTo(UIToWorld(evt.localPosition));
|
||||
evt.StopPropagation();
|
||||
}
|
||||
|
||||
private void OnPointerUp(PointerUpEvent evt)
|
||||
{
|
||||
if (!isDragging || evt.pointerId != dragPointerId) return;
|
||||
EndDragging(evt.pointerId);
|
||||
evt.StopPropagation();
|
||||
}
|
||||
|
||||
// Lost capture — end drag cleanly so we don't leave CameraController stuck.
|
||||
private void OnPointerCaptureOut(PointerCaptureOutEvent evt)
|
||||
{
|
||||
if (!isDragging || evt.pointerId != dragPointerId) return;
|
||||
EndDragging(evt.pointerId);
|
||||
}
|
||||
|
||||
private void EndDragging(int pointerId)
|
||||
{
|
||||
isDragging = false;
|
||||
cameraController?.EndDrag();
|
||||
if (container.HasPointerCapture(pointerId))
|
||||
container.ReleasePointer(pointerId);
|
||||
}
|
||||
|
||||
private void HandleRightClickMove(Vector2 uiLocal)
|
||||
{
|
||||
var selection = SelectionState.Instance;
|
||||
if (selection == null || !selection.HasSelection) return;
|
||||
|
||||
Vector3 worldTarget = UIToWorld(uiLocal);
|
||||
// Same RPC the world right-click uses; server validates and side-effects the queue.
|
||||
selection.SelectedBuilder.RequestMoveAndPauseRpc(worldTarget);
|
||||
}
|
||||
|
||||
// ----- Zoom -------------------------------------------------------
|
||||
|
||||
private void OnWheel(WheelEvent evt)
|
||||
{
|
||||
if (!ready) return;
|
||||
|
||||
// Cursor-anchored zoom: capture the world point under the cursor before the
|
||||
// zoom change, then move viewCenter so that same world point lands at the same
|
||||
// UI position afterward. Result: the cursor "drills in" on whatever it's over.
|
||||
Vector3 cursorWorldBefore = UIToWorld(evt.localMousePosition);
|
||||
|
||||
// WheelEvent.delta.y: positive = scroll down (typically zoom out), negative = up.
|
||||
// Multiplicative steps feel natural and keep the rate consistent across zoom levels.
|
||||
float steps = -evt.delta.y;
|
||||
float newZoom = Mathf.Clamp(zoom * Mathf.Pow(ZoomStepFactor, steps), MinZoom, MaxZoom);
|
||||
if (Mathf.Approximately(newZoom, zoom))
|
||||
{
|
||||
evt.StopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
zoom = newZoom;
|
||||
|
||||
// Compute viewCenter that keeps cursorWorldBefore under the cursor at the new zoom.
|
||||
// visibleWorldMin = viewCenter - halfWorld; visibleWorldMax = viewCenter + halfWorld
|
||||
// UIToWorld(uiLocal) = visibleWorldMin + frac * (visibleWorldMax - visibleWorldMin)
|
||||
// = viewCenter - halfWorld + frac * 2 * halfWorld
|
||||
// = viewCenter + (2*frac - 1) * halfWorld
|
||||
// Solve viewCenter from desired cursorWorldBefore at the cursor's frac.
|
||||
var rect = container.contentRect;
|
||||
float fx = Mathf.Clamp01(evt.localMousePosition.x / rect.width);
|
||||
float fyTopDown = Mathf.Clamp01(evt.localMousePosition.y / rect.height);
|
||||
float fzBottomUp = 1f - fyTopDown;
|
||||
|
||||
float halfWorldX = (worldMax.x - worldMin.x) * 0.5f / zoom;
|
||||
float halfWorldZ = (worldMax.y - worldMin.y) * 0.5f / zoom;
|
||||
viewCenter.x = cursorWorldBefore.x - (2f * fx - 1f) * halfWorldX;
|
||||
viewCenter.y = cursorWorldBefore.z - (2f * fzBottomUp - 1f) * halfWorldZ;
|
||||
|
||||
ApplyView(); // clamps viewCenter and updates terrain element
|
||||
evt.StopPropagation();
|
||||
}
|
||||
|
||||
// ----- Coordinate transforms --------------------------------------
|
||||
|
||||
// UI local (y down) → world (z up). Clamps to visible map rect so dragging past the
|
||||
// edge doesn't fly the camera off the map.
|
||||
private Vector3 UIToWorld(Vector2 uiLocal)
|
||||
{
|
||||
var rect = container.contentRect;
|
||||
if (rect.width <= 0f || rect.height <= 0f) return Vector3.zero;
|
||||
|
||||
float fx = Mathf.Clamp01(uiLocal.x / rect.width);
|
||||
float fyTopDown = Mathf.Clamp01(uiLocal.y / rect.height);
|
||||
float fzBottomUp = 1f - fyTopDown;
|
||||
|
||||
float worldX = Mathf.Lerp(visibleWorldMin.x, visibleWorldMax.x, fx);
|
||||
float worldZ = Mathf.Lerp(visibleWorldMin.y, visibleWorldMax.y, fzBottomUp);
|
||||
return new Vector3(worldX, GridCoordinates.BUILDABLE_PLANE_Y, worldZ);
|
||||
}
|
||||
|
||||
// World → UI local. Used for placing entity icons.
|
||||
//
|
||||
// IMPORTANT: this must NOT clamp. Mathf.InverseLerp clamps to [0,1], which would
|
||||
// pin off-screen entities to the minimap edge — making zoomed-in views show
|
||||
// ghost icons stuck against every border. We compute the fraction manually so
|
||||
// out-of-view entities get UI coords outside the container's rect, where the
|
||||
// bounds check in DrawOneEntity culls them.
|
||||
private Vector2 WorldToUI(Vector3 world)
|
||||
{
|
||||
var rect = container.contentRect;
|
||||
float rangeX = visibleWorldMax.x - visibleWorldMin.x;
|
||||
float rangeZ = visibleWorldMax.y - visibleWorldMin.y;
|
||||
if (rangeX <= 0.0001f || rangeZ <= 0.0001f) return Vector2.zero;
|
||||
|
||||
float fx = (world.x - visibleWorldMin.x) / rangeX;
|
||||
float fzBottomUp = (world.z - visibleWorldMin.y) / rangeZ;
|
||||
float fyTopDown = 1f - fzBottomUp;
|
||||
return new Vector2(fx * rect.width, fyTopDown * rect.height);
|
||||
}
|
||||
|
||||
/// <summary>Pixels per world unit at the current zoom. Used to scale entity icons so
|
||||
/// e.g. a 2×2 tower footprint reads as a 2-tile square on the minimap.</summary>
|
||||
private float PixelsPerWorldUnit
|
||||
{
|
||||
get
|
||||
{
|
||||
var rect = container.contentRect;
|
||||
float visibleWorldWidth = visibleWorldMax.x - visibleWorldMin.x;
|
||||
return visibleWorldWidth > 0.0001f ? rect.width / visibleWorldWidth : 1f;
|
||||
}
|
||||
}
|
||||
|
||||
// ----- Entity overlay drawing -------------------------------------
|
||||
|
||||
private void DrawEntities(MeshGenerationContext mgc)
|
||||
{
|
||||
if (!ready) return;
|
||||
var painter = mgc.painter2D;
|
||||
float pxPerWorld = PixelsPerWorldUnit;
|
||||
|
||||
// The local player's builder always draws on top. Cache the reference once
|
||||
// (Builder.Local does a dictionary lookup) and cast to the interface so the
|
||||
// ReferenceEquals compare against registry entries works without boxing.
|
||||
IMinimapEntity localBuilder = Builder.Local;
|
||||
|
||||
// Pass 1: every entity except the local builder. Order within this pass is
|
||||
// arbitrary (registry is a HashSet); revisit if cross-entity layering between
|
||||
// non-local entities ever matters.
|
||||
MinimapEntityRegistry.ForEach(e =>
|
||||
{
|
||||
if (ReferenceEquals(e, localBuilder)) return;
|
||||
DrawOneEntity(painter, e, pxPerWorld);
|
||||
});
|
||||
|
||||
// Pass 2: local builder on top. Skipped if the local builder isn't spawned
|
||||
// (e.g., on a dedicated server or before the local client's builder arrives).
|
||||
if (localBuilder != null)
|
||||
DrawOneEntity(painter, localBuilder, pxPerWorld);
|
||||
}
|
||||
|
||||
private void DrawOneEntity(Painter2D p, IMinimapEntity entity, float pxPerWorld)
|
||||
{
|
||||
if (entity == null) return;
|
||||
|
||||
Vector2 ui = WorldToUI(entity.WorldPosition);
|
||||
|
||||
// Skip if obviously off-screen — overflow:hidden clips it anyway, but skipping
|
||||
// saves Painter2D work for entities far outside the zoomed-in window.
|
||||
var rect = container.contentRect;
|
||||
if (ui.x < -8f || ui.y < -8f || ui.x > rect.width + 8f || ui.y > rect.height + 8f) return;
|
||||
|
||||
Color col = entity.MinimapColor; col.a = 1f;
|
||||
p.fillColor = col;
|
||||
|
||||
switch (entity.IconKind)
|
||||
{
|
||||
case MinimapIconKind.Enemy:
|
||||
{
|
||||
float r = Mathf.Max(entity.MinimapWorldSize * 0.5f * pxPerWorld, EnemyMinRadiusPx);
|
||||
DrawCircle(p, ui, r);
|
||||
break;
|
||||
}
|
||||
case MinimapIconKind.Tower:
|
||||
{
|
||||
// Tower size follows footprint exactly — no pixel floor. Adjacent towers
|
||||
// visually touch on the minimap because their drawn squares span their
|
||||
// full footprint extent in world units.
|
||||
float halfPx = entity.MinimapWorldSize * 0.5f * pxPerWorld;
|
||||
DrawSquare(p, ui, halfPx);
|
||||
break;
|
||||
}
|
||||
case MinimapIconKind.Builder:
|
||||
{
|
||||
float r = Mathf.Max(entity.MinimapWorldSize * 0.5f * pxPerWorld, BuilderMinRadiusPx);
|
||||
DrawCircle(p, ui, r);
|
||||
p.strokeColor = BuilderOutline;
|
||||
p.lineWidth = 1f;
|
||||
p.Stroke();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawCircle(Painter2D p, Vector2 center, float radius)
|
||||
{
|
||||
p.BeginPath();
|
||||
p.Arc(center, radius, new Angle(0f, AngleUnit.Degree), new Angle(360f, AngleUnit.Degree));
|
||||
p.Fill();
|
||||
}
|
||||
|
||||
private static void DrawSquare(Painter2D p, Vector2 center, float halfSize)
|
||||
{
|
||||
p.BeginPath();
|
||||
p.MoveTo(new Vector2(center.x - halfSize, center.y - halfSize));
|
||||
p.LineTo(new Vector2(center.x + halfSize, center.y - halfSize));
|
||||
p.LineTo(new Vector2(center.x + halfSize, center.y + halfSize));
|
||||
p.LineTo(new Vector2(center.x - halfSize, center.y + halfSize));
|
||||
p.ClosePath();
|
||||
p.Fill();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue