Add in-game paint HUD bound to the mesh painter

Add a code-driven HUD that appears in paint mode and binds live to the
active MeshPainter: four channel toggles, HSV pickers for albedo and
emission, sliders for metallic/smoothness/occlusion, emission intensity
and brush size. The HUD self-assembles its own overlay canvas and event
system at runtime.

Add a ModeChanged event to GameModeManager so the HUD can follow the
active mode, and guard MeshPainter against painting through the UI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 4.8 committed 2026-06-24 08:26:56 +02:00
1 parent 6555db1487
commit 04c22f0ac1
8 files changed
+594

No files matched your search

+5
View File
@@ -41,6 +41,10 @@ public class GameModeManager : MonoBehaviour
public MeshPainter Painter => painter;
public InsectController Insect => insect;
/// <summary>Raised after a mode switch finishes (the new mode's Enter has already run).
/// Argument = the now-active mode. Used by UI (e.g. the paint HUD) to follow the mode.</summary>
public event System.Action<GameMode> ModeChanged;
private readonly List<GameMode> _modes = new List<GameMode>();
private int _activeIndex = -1;
@@ -105,6 +109,7 @@ public class GameModeManager : MonoBehaviour
ActiveMode.Enter();
Debug.Log($"[GameModeManager] Mode -> {ActiveMode.DisplayName}", this);
ModeChanged?.Invoke(ActiveMode);
}
private bool WasSwitchPressed()
+124
View File
@@ -0,0 +1,124 @@
using System;
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// Compact runtime HSV colour picker: a saturation/value square, a hue bar and a live
/// swatch. Builds its own UI as children when <see cref="Build"/> is called and raises
/// <see cref="Changed"/> on user interaction. The square is a pure-hue base with two
/// static gradient overlays on top (white-out -> saturation, black-out -> value), so
/// only the base tint changes per hue - no per-frame texture rebuilds. Hue is never
/// lost while dragging the square (the square only edits S/V, the bar only edits H).
/// </summary>
public class HsvColorPicker : MonoBehaviour
{
/// <summary>Raised with the new colour when the user moves the square or hue bar.</summary>
public event Action<Color> Changed;
private float _h, _s, _v, _a = 1f;
private Image _hueBase; // pure-hue background of the SV square
private RectTransform _svHandle;
private RectTransform _hueHandle;
private Image _swatch;
/// <summary>Current colour (HSV -> RGB, with the preserved alpha).</summary>
public Color Value
{
get
{
Color c = Color.HSVToRGB(_h, _s, _v);
c.a = _a;
return c;
}
}
/// <summary>Shows <paramref name="c"/> without raising <see cref="Changed"/> (external set).</summary>
public void SetColor(Color c)
{
Color.RGBToHSV(c, out _h, out _s, out _v);
_a = c.a;
Refresh();
}
/// <summary>Builds the picker (square + hue bar + swatch) under this object, top-left aligned.</summary>
public void Build(float squareSize, float hueHeight)
{
// Saturation / value square: base hue + two gradient overlays, top-left of this rect.
var sv = PaintHud.NewRect("SV", transform);
TopLeft(sv, new Vector2(0f, 0f), new Vector2(squareSize, squareSize));
_hueBase = PaintHud.AddImage(sv.gameObject, Color.red); // raycast target for the drag relay
PaintHud.AddOverlay(sv, PaintHud.SaturationSprite());
PaintHud.AddOverlay(sv, PaintHud.ValueSprite());
sv.gameObject.AddComponent<UiPointerSurface>().Picked += OnSvPicked;
_svHandle = PaintHud.NewRect("Handle", sv);
_svHandle.sizeDelta = new Vector2(12f, 12f);
var svHandleImg = PaintHud.AddImage(_svHandle.gameObject, Color.white);
svHandleImg.raycastTarget = false;
// Hue bar below the square.
var hue = PaintHud.NewRect("Hue", transform);
TopLeft(hue, new Vector2(0f, -(squareSize + 6f)), new Vector2(squareSize, hueHeight));
var hueImg = hue.gameObject.AddComponent<Image>();
hueImg.sprite = PaintHud.HueSprite();
hueImg.type = Image.Type.Simple;
hue.gameObject.AddComponent<UiPointerSurface>().Picked += OnHuePicked;
_hueHandle = PaintHud.NewRect("Handle", hue);
_hueHandle.sizeDelta = new Vector2(4f, hueHeight + 4f);
var hueHandleImg = PaintHud.AddImage(_hueHandle.gameObject, Color.white);
hueHandleImg.raycastTarget = false;
// Swatch, to the right of the square.
var sw = PaintHud.NewRect("Swatch", transform);
TopLeft(sw, new Vector2(squareSize + 10f, 0f), new Vector2(34f, 34f));
_swatch = PaintHud.AddImage(sw.gameObject, Color.white);
_swatch.raycastTarget = false;
Refresh();
}
private void OnSvPicked(Vector2 p)
{
_s = p.x;
_v = p.y;
Refresh();
Changed?.Invoke(Value);
}
private void OnHuePicked(Vector2 p)
{
_h = p.x;
Refresh();
Changed?.Invoke(Value);
}
private void Refresh()
{
if (_hueBase) _hueBase.color = Color.HSVToRGB(_h, 1f, 1f);
if (_svHandle)
{
_svHandle.anchorMin = _svHandle.anchorMax = new Vector2(_s, _v);
_svHandle.anchoredPosition = Vector2.zero;
}
if (_hueHandle)
{
_hueHandle.anchorMin = _hueHandle.anchorMax = new Vector2(_h, 0.5f);
_hueHandle.anchoredPosition = Vector2.zero;
}
if (_swatch) _swatch.color = Color.HSVToRGB(_h, _s, _v);
}
/// <summary>Anchors a child to this rect's top-left corner with the given offset and size.</summary>
private static void TopLeft(RectTransform rt, Vector2 offset, Vector2 size)
{
rt.anchorMin = rt.anchorMax = new Vector2(0f, 1f);
rt.pivot = new Vector2(0f, 1f);
rt.anchoredPosition = offset;
rt.sizeDelta = size;
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2a35dda49401e4045ba2165e15116a5f
+13
View File
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
#if ENABLE_INPUT_SYSTEM
using UnityEngine.InputSystem;
#endif
@@ -578,6 +579,11 @@ public partial class MeshPainter : MonoBehaviour
if (!TryGetMouse(out bool pressed, out Vector2 screenPos))
return;
// Don't paint through the in-game UI: clicking a HUD slider/toggle must not
// also stamp the mesh behind it.
if (pressed && IsPointerOverUi())
pressed = false;
// Brush preview (size + mask) at the hover point - independent of painting,
// so you see the brush before pressing the button.
if (previewMode != BrushPreviewMode.Off && _previewMat && paintCamera)
@@ -741,6 +747,13 @@ public partial class MeshPainter : MonoBehaviour
#endif
}
/// <summary>True if the pointer is currently over an interactive UI element (raycast target).</summary>
private static bool IsPointerOverUi()
{
var es = EventSystem.current;
return es && es.IsPointerOverGameObject();
}
private void OnDestroy()
{
if (_bakedMesh) Destroy(_bakedMesh);
+407
View File
@@ -0,0 +1,407 @@
using System;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
#if ENABLE_INPUT_SYSTEM
using UnityEngine.InputSystem.UI;
#endif
/// <summary>
/// In-game paint UI. Builds its own Screen Space - Overlay canvas (and an EventSystem
/// if the scene has none) entirely in code and binds the widgets directly to the live
/// <see cref="MeshPainter"/> fields, so changes apply immediately. The panel is shown
/// only while the active <see cref="GameMode"/> is <see cref="PaintMode"/> (it follows
/// the manager's ModeChanged event).
///
/// First, simple pass: channel toggles, an HSV colour picker for albedo, metallic /
/// smoothness / occlusion sliders, an HSV picker + intensity for emission, and brush
/// size. Attach to an empty GameObject; everything else is generated at runtime.
/// </summary>
[DisallowMultipleComponent]
public class PaintHud : MonoBehaviour
{
[Tooltip("Mode manager driving visibility. Empty = found automatically.")]
public GameModeManager modeManager;
[Tooltip("Painter the HUD edits. Empty = the manager's painter, else found automatically.")]
public MeshPainter painter;
private static readonly Color PanelColor = new Color(0.09f, 0.09f, 0.11f, 0.9f);
private static readonly Color Accent = new Color(0.40f, 0.75f, 1f, 1f);
private static readonly Color BoxColor = new Color(0.20f, 0.20f, 0.24f, 1f);
private GameObject _panel;
private Font _font;
private void Awake()
{
if (!modeManager) modeManager = FindFirstObjectByType<GameModeManager>();
}
private void OnEnable()
{
if (modeManager) modeManager.ModeChanged += OnModeChanged;
}
private void OnDisable()
{
if (modeManager) modeManager.ModeChanged -= OnModeChanged;
}
private void Start()
{
if (!painter) painter = modeManager ? modeManager.Painter : null;
if (!painter) painter = FindFirstObjectByType<MeshPainter>();
_font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
if (!_font) _font = Resources.GetBuiltinResource<Font>("Arial.ttf");
EnsureEventSystem();
BuildUi();
// Set initial visibility (the manager's first SwitchTo may have fired before this Start).
OnModeChanged(modeManager ? modeManager.ActiveMode : null);
}
private void OnModeChanged(GameMode mode)
{
if (_panel) _panel.SetActive(mode is PaintMode);
}
// --- UI construction ---
private void BuildUi()
{
var canvas = GetComponent<Canvas>();
if (!canvas) canvas = gameObject.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvas.sortingOrder = 100;
var scaler = GetComponent<CanvasScaler>();
if (!scaler) scaler = gameObject.AddComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1920f, 1080f);
scaler.matchWidthOrHeight = 0.5f;
if (!GetComponent<GraphicRaycaster>()) gameObject.AddComponent<GraphicRaycaster>();
// Panel (this is what gets shown/hidden per mode).
var panel = NewRect("Panel", transform);
_panel = panel.gameObject;
panel.anchorMin = panel.anchorMax = new Vector2(0f, 1f);
panel.pivot = new Vector2(0f, 1f);
panel.anchoredPosition = new Vector2(16f, -16f);
panel.sizeDelta = new Vector2(296f, 0f); // width fixed; height from the ContentSizeFitter
AddImage(panel.gameObject, PanelColor);
var vlg = panel.gameObject.AddComponent<VerticalLayoutGroup>();
vlg.padding = new RectOffset(12, 12, 12, 12);
vlg.spacing = 8f;
vlg.childAlignment = TextAnchor.UpperLeft;
vlg.childControlWidth = true;
vlg.childControlHeight = true;
vlg.childForceExpandWidth = true;
vlg.childForceExpandHeight = false;
var fitter = panel.gameObject.AddComponent<ContentSizeFitter>();
fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
AddSectionLabel(panel, "PAINT", 15, FontStyle.Bold);
// Channels.
AddSectionLabel(panel, "Channels", 13, FontStyle.Bold);
AddToggle(panel, "Albedo", painter && painter.paintAlbedo, v => { if (painter) painter.paintAlbedo = v; });
AddToggle(panel, "Metallic + Smoothness", painter && painter.paintMetallicSmoothness, v => { if (painter) painter.paintMetallicSmoothness = v; });
AddToggle(panel, "Occlusion", painter && painter.paintOcclusion, v => { if (painter) painter.paintOcclusion = v; });
AddToggle(panel, "Emission", painter && painter.paintEmission, v => { if (painter) painter.paintEmission = v; });
// Albedo colour.
AddSectionLabel(panel, "Albedo Color", 13, FontStyle.Bold);
var albedo = AddColorPicker(panel);
albedo.SetColor(painter ? painter.brushColor : Color.red);
albedo.Changed += c => { if (painter) painter.brushColor = new Color(c.r, c.g, c.b, painter.brushColor.a); };
// Metallic / smoothness / occlusion.
AddSlider(panel, "Metallic", 0f, 1f, false, painter ? painter.metallic : 0f, v => { if (painter) painter.metallic = v; });
AddSlider(panel, "Smoothness", 0f, 1f, false, painter ? painter.smoothness : 0.5f, v => { if (painter) painter.smoothness = v; });
AddSlider(panel, "Occlusion", 0f, 1f, false, painter ? painter.occlusion : 0.5f, v => { if (painter) painter.occlusion = v; });
// Emission.
AddSectionLabel(panel, "Emission Color", 13, FontStyle.Bold);
var emission = AddColorPicker(panel);
emission.SetColor(painter ? painter.emissionColor : Color.black);
emission.Changed += c => { if (painter) painter.emissionColor = new Color(c.r, c.g, c.b, painter.emissionColor.a); };
AddSlider(panel, "Emission Intensity", 0f, 8f, false, painter ? painter.emissionIntensity : 1f, v => { if (painter) painter.emissionIntensity = v; });
// Brush size.
AddSlider(panel, "Brush Size", 1f, 256f, true, painter ? painter.brushSize : 16f, v => { if (painter) painter.brushSize = Mathf.RoundToInt(v); });
}
private void EnsureEventSystem()
{
if (FindFirstObjectByType<EventSystem>()) return;
var es = new GameObject("EventSystem", typeof(EventSystem));
#if ENABLE_INPUT_SYSTEM
var module = es.AddComponent<InputSystemUIInputModule>();
if (!module.actionsAsset) module.AssignDefaultActions();
#else
es.AddComponent<StandaloneInputModule>();
#endif
}
// --- Widgets ---
private void AddSectionLabel(RectTransform panel, string text, int size, FontStyle style)
{
var rt = NewRect("Label", panel);
var le = rt.gameObject.AddComponent<LayoutElement>();
le.minHeight = le.preferredHeight = 18f;
var t = MakeText(rt.gameObject, text, size, style, new Color(0.85f, 0.85f, 0.9f, 1f), TextAnchor.MiddleLeft);
t.raycastTarget = false;
}
private void AddToggle(RectTransform panel, string label, bool value, Action<bool> onChanged)
{
var rt = NewRect("Toggle", panel);
var le = rt.gameObject.AddComponent<LayoutElement>();
le.minHeight = le.preferredHeight = 22f;
var toggle = rt.gameObject.AddComponent<Toggle>();
var box = NewRect("Box", rt);
box.anchorMin = box.anchorMax = new Vector2(0f, 0.5f);
box.pivot = new Vector2(0f, 0.5f);
box.anchoredPosition = Vector2.zero;
box.sizeDelta = new Vector2(18f, 18f);
var boxImg = AddImage(box.gameObject, BoxColor);
var check = NewRect("Check", box);
check.anchorMin = Vector2.zero;
check.anchorMax = Vector2.one;
check.offsetMin = new Vector2(3f, 3f);
check.offsetMax = new Vector2(-3f, -3f);
var checkImg = AddImage(check.gameObject, Accent);
checkImg.raycastTarget = false;
var lbl = NewRect("Label", rt);
lbl.anchorMin = Vector2.zero;
lbl.anchorMax = Vector2.one;
lbl.offsetMin = new Vector2(24f, 0f);
lbl.offsetMax = Vector2.zero;
var t = MakeText(lbl.gameObject, label, 14, FontStyle.Normal, Color.white, TextAnchor.MiddleLeft);
t.raycastTarget = false;
toggle.targetGraphic = boxImg;
toggle.graphic = checkImg;
toggle.isOn = value;
toggle.onValueChanged.AddListener(v => onChanged(v));
}
private void AddSlider(RectTransform panel, string label, float min, float max, bool whole, float value, Action<float> onChanged)
{
var rt = NewRect("Slider", panel);
var le = rt.gameObject.AddComponent<LayoutElement>();
le.minHeight = le.preferredHeight = 34f;
// Caption row (label left, value right), pinned to the top of the cell.
var lbl = NewRect("Label", rt);
lbl.anchorMin = new Vector2(0f, 1f);
lbl.anchorMax = new Vector2(1f, 1f);
lbl.pivot = new Vector2(0f, 1f);
lbl.offsetMin = new Vector2(0f, -16f);
lbl.offsetMax = new Vector2(-48f, 0f);
var lt = MakeText(lbl.gameObject, label, 13, FontStyle.Normal, new Color(0.8f, 0.8f, 0.85f, 1f), TextAnchor.MiddleLeft);
lt.raycastTarget = false;
var valRt = NewRect("Value", rt);
valRt.anchorMin = new Vector2(1f, 1f);
valRt.anchorMax = new Vector2(1f, 1f);
valRt.pivot = new Vector2(1f, 1f);
valRt.anchoredPosition = Vector2.zero;
valRt.sizeDelta = new Vector2(56f, 16f);
var vt = MakeText(valRt.gameObject, "", 13, FontStyle.Normal, Color.white, TextAnchor.MiddleRight);
vt.raycastTarget = false;
// Track, pinned to the bottom of the cell.
var track = NewRect("Track", rt);
track.anchorMin = new Vector2(0f, 0f);
track.anchorMax = new Vector2(1f, 0f);
track.pivot = new Vector2(0.5f, 0f);
track.anchoredPosition = Vector2.zero;
track.sizeDelta = new Vector2(0f, 16f);
var slider = track.gameObject.AddComponent<Slider>();
var bg = NewRect("Background", track);
bg.anchorMin = new Vector2(0f, 0.5f);
bg.anchorMax = new Vector2(1f, 0.5f);
bg.pivot = new Vector2(0.5f, 0.5f);
bg.anchoredPosition = Vector2.zero;
bg.sizeDelta = new Vector2(0f, 5f);
AddImage(bg.gameObject, BoxColor); // raycast target so clicking the bar jumps the value
var fillArea = NewRect("Fill Area", track);
fillArea.anchorMin = new Vector2(0f, 0.5f);
fillArea.anchorMax = new Vector2(1f, 0.5f);
fillArea.pivot = new Vector2(0.5f, 0.5f);
fillArea.anchoredPosition = Vector2.zero;
fillArea.sizeDelta = new Vector2(-16f, 5f);
var fill = NewRect("Fill", fillArea);
fill.anchorMin = Vector2.zero;
fill.anchorMax = Vector2.one;
fill.offsetMin = Vector2.zero;
fill.offsetMax = Vector2.zero;
var fillImg = AddImage(fill.gameObject, Accent);
fillImg.raycastTarget = false;
var handleArea = NewRect("Handle Slide Area", track);
handleArea.anchorMin = Vector2.zero;
handleArea.anchorMax = Vector2.one;
handleArea.offsetMin = new Vector2(8f, 0f);
handleArea.offsetMax = new Vector2(-8f, 0f);
var handle = NewRect("Handle", handleArea);
handle.sizeDelta = new Vector2(14f, 0f);
var handleImg = AddImage(handle.gameObject, Color.white);
slider.fillRect = fill;
slider.handleRect = handle;
slider.targetGraphic = handleImg;
slider.direction = Slider.Direction.LeftToRight;
slider.minValue = min;
slider.maxValue = max;
slider.wholeNumbers = whole;
slider.value = value;
Action<float> setText = v => vt.text = whole
? Mathf.RoundToInt(v).ToString()
: v.ToString("0.00");
setText(slider.value); // show the actual (clamped) value, not the unclamped input
slider.onValueChanged.AddListener(v => { setText(v); onChanged(v); });
}
private HsvColorPicker AddColorPicker(RectTransform panel)
{
const float square = 120f;
const float hueH = 14f;
var rt = NewRect("ColorPicker", panel);
var le = rt.gameObject.AddComponent<LayoutElement>();
le.minHeight = le.preferredHeight = square + 6f + hueH;
var picker = rt.gameObject.AddComponent<HsvColorPicker>();
picker.Build(square, hueH);
return picker;
}
private Text MakeText(GameObject go, string text, int size, FontStyle style, Color color, TextAnchor anchor)
{
var t = go.AddComponent<Text>();
t.font = _font;
t.fontSize = size;
t.fontStyle = style;
t.color = color;
t.alignment = anchor;
t.text = text;
t.horizontalOverflow = HorizontalWrapMode.Overflow;
t.verticalOverflow = VerticalWrapMode.Truncate;
return t;
}
// --- Static UI factory (shared with HsvColorPicker) ---
public static RectTransform NewRect(string name, Transform parent)
{
var go = new GameObject(name, typeof(RectTransform));
go.transform.SetParent(parent, false);
return (RectTransform)go.transform;
}
public static Image AddImage(GameObject go, Color color)
{
var img = go.AddComponent<Image>();
img.color = color;
return img;
}
/// <summary>Adds a full-rect, non-raycasting overlay image with the given sprite.</summary>
public static Image AddOverlay(RectTransform parent, Sprite sprite)
{
var rt = NewRect("Overlay", parent);
rt.anchorMin = Vector2.zero;
rt.anchorMax = Vector2.one;
rt.offsetMin = Vector2.zero;
rt.offsetMax = Vector2.zero;
var img = rt.gameObject.AddComponent<Image>();
img.sprite = sprite;
img.type = Image.Type.Simple;
img.raycastTarget = false;
return img;
}
// Cached gradient sprites for the colour picker.
private static Sprite _satSprite, _valSprite, _hueSprite;
/// <summary>Horizontal white-out: opaque white (left) -> transparent (right) = saturation.</summary>
public static Sprite SaturationSprite()
{
if (_satSprite) return _satSprite;
const int n = 64;
var tex = NewGradientTex(n, 1);
for (int x = 0; x < n; x++)
{
byte a = (byte)Mathf.RoundToInt(255f * (1f - x / (float)(n - 1)));
tex.SetPixel(x, 0, new Color32(255, 255, 255, a));
}
tex.Apply(false);
_satSprite = ToSprite(tex);
return _satSprite;
}
/// <summary>Vertical black-out: opaque black (bottom) -> transparent (top) = value.</summary>
public static Sprite ValueSprite()
{
if (_valSprite) return _valSprite;
const int n = 64;
var tex = NewGradientTex(1, n);
for (int y = 0; y < n; y++)
{
byte a = (byte)Mathf.RoundToInt(255f * (1f - y / (float)(n - 1)));
tex.SetPixel(0, y, new Color32(0, 0, 0, a));
}
tex.Apply(false);
_valSprite = ToSprite(tex);
return _valSprite;
}
/// <summary>Horizontal full-saturation hue ramp 0..1.</summary>
public static Sprite HueSprite()
{
if (_hueSprite) return _hueSprite;
const int n = 64;
var tex = NewGradientTex(n, 1);
for (int x = 0; x < n; x++)
tex.SetPixel(x, 0, Color.HSVToRGB(x / (float)(n - 1), 1f, 1f));
tex.Apply(false);
_hueSprite = ToSprite(tex);
return _hueSprite;
}
private static Texture2D NewGradientTex(int w, int h)
{
return new Texture2D(w, h, TextureFormat.RGBA32, false, false) // sRGB sampling for correct colours
{
name = "PaintHud_Gradient",
wrapMode = TextureWrapMode.Clamp,
filterMode = FilterMode.Bilinear,
hideFlags = HideFlags.HideAndDontSave
};
}
private static Sprite ToSprite(Texture2D tex)
{
var s = Sprite.Create(tex, new Rect(0f, 0f, tex.width, tex.height), new Vector2(0.5f, 0.5f), 100f);
s.hideFlags = HideFlags.HideAndDontSave;
return s;
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d0635b02a5454ec408df3a69b04f85a7
+39
View File
@@ -0,0 +1,39 @@
using System;
using UnityEngine;
using UnityEngine.EventSystems;
/// <summary>
/// Turns a pointer press/drag on a UI graphic into a normalized (x, y) position in
/// [0,1] within its RectTransform (origin bottom-left). Added in code to the colour
/// picker's saturation/value field and hue bar so they react to both clicking and
/// dragging. The graphic on the same object must be a raycast target.
/// </summary>
[RequireComponent(typeof(RectTransform))]
public class UiPointerSurface : MonoBehaviour, IPointerDownHandler, IDragHandler
{
/// <summary>Fired on press and while dragging with the normalized (x, y) in [0,1].</summary>
public event Action<Vector2> Picked;
private RectTransform _rect;
private void Awake() => _rect = (RectTransform)transform;
public void OnPointerDown(PointerEventData eventData) => Report(eventData);
public void OnDrag(PointerEventData eventData) => Report(eventData);
private void Report(PointerEventData eventData)
{
// pressEventCamera is null for a Screen Space - Overlay canvas, which is the
// correct camera argument there.
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(
_rect, eventData.position, eventData.pressEventCamera, out Vector2 local))
return;
Rect r = _rect.rect;
if (r.width <= 0f || r.height <= 0f) return;
float x = Mathf.Clamp01((local.x - r.xMin) / r.width);
float y = Mathf.Clamp01((local.y - r.yMin) / r.height);
Picked?.Invoke(new Vector2(x, y));
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3aefb85fd241856408808aa5401c9d64