Files
chameleon/Assets/Scripts/PaintHud.cs
T
adminandClaude Opus 4.8 e04868f9c0 Fix first-person camera target and paint HUD rebind after networked spawn
Point the first-person camera at the dedicated FirstPersonTarget anchor
(eye height) instead of the shared CameraTarget, in both the runtime
SetPlayer rebind and the editor ResolveReferences fallback.

Rebind the paint HUD to the locally spawned avatar's MeshPainter via a
new GameModeManager.PainterChanged event. Since the spawn-on-connect
refactor, no player exists at PaintHud.Start(), so the HUD bound to a
null painter (no brush masks shown, color/settings were no-ops).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 15:22:23 +02:00

597 lines
25 KiB
C#

using System;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
#if ENABLE_INPUT_SYSTEM
using UnityEngine.InputSystem.UI;
#endif
/// <summary>
/// In-game paint UI. Shows its panels only while the active <see cref="GameMode"/> is
/// <see cref="PaintMode"/> (it follows the manager's ModeChanged event) and binds its
/// widgets directly to the live <see cref="MeshPainter"/> fields, so edits apply immediately.
///
/// The layout is a transparent root holding one independently positioned sub-panel per channel
/// (Albedo, Metallic+Smoothness, Occlusion, Emission) plus a global Brush card, each with a title
/// bar and draggable from any of its non-widget surface. The layout comes from one of two sources:
/// - an authored prefab assigned to <see cref="layoutPrefab"/> (instantiated, then bound), or
/// - generated in code (the fallback when no prefab is set).
/// Either way the *bindings* (listeners, picker gradient sprites, painter values) live in code
/// and are (re)applied at runtime by <see cref="WirePanel"/>. A prefab stores only the visual
/// layout, never the behaviour, so both paths run the exact same wiring against the same stable
/// widget names - there is no separate "dead" prefab path.
///
/// Widgets: 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.
/// </summary>
[DisallowMultipleComponent]
public partial 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;
[Tooltip("Authored panel layout. If set it is instantiated and bound; empty = build the panel in code.")]
public GameObject layoutPrefab;
// Stable widget names shared by the code-gen builder, the baked prefab and the binder.
private const string PanelSpray = "Panel_Spray";
private const string PanelAlbedo = "Panel_Albedo";
private const string PanelMetallicSmoothness = "Panel_MetallicSmoothness";
private const string PanelOcclusion = "Panel_Occlusion";
private const string PanelEmission = "Panel_Emission";
private const string PanelBrushSize = "Panel_BrushSize";
private const string ToggleAlbedo = "Toggle_Albedo";
private const string ToggleMetallicSmoothness = "Toggle_MetallicSmoothness";
private const string ToggleOcclusion = "Toggle_Occlusion";
private const string ToggleEmission = "Toggle_Emission";
private const string ToggleSpray = "Toggle_Spray";
private const string TogglePipette = "Toggle_Pipette";
private const string PickerAlbedo = "Picker_Albedo";
private const string PickerEmission = "Picker_Emission";
private const string SliderMetallic = "Slider_Metallic";
private const string SliderSmoothness = "Slider_Smoothness";
private const string SliderMetallicSmoothnessAlpha = "Slider_MetallicSmoothnessAlpha";
private const string SliderOcclusion = "Slider_Occlusion";
private const string SliderOcclusionAlpha = "Slider_OcclusionAlpha";
private const string SliderEmissionIntensity = "Slider_EmissionIntensity";
private const string SliderBrushSize = "Slider_BrushSize";
private const string SliderSprayDots = "Slider_SprayDots";
private const string SliderSprayDotSizeMin = "Slider_SprayDotSizeMin";
private const string SliderSprayDotSizeMax = "Slider_SprayDotSizeMax";
private const string SliderSprayDotAlphaMin = "Slider_SprayDotAlphaMin";
private const string SliderSprayDotAlphaMax = "Slider_SprayDotAlphaMax";
private const string MaskSelector = "MaskSelector";
private const string MaskSwatchPrefix = "MaskSwatch_";
// Panels made draggable at runtime - grabbable from any non-widget surface (both prefab and code-gen paths).
private static readonly string[] DraggablePanels =
{
PanelAlbedo,
PanelMetallicSmoothness,
PanelOcclusion,
PanelEmission,
PanelBrushSize,
PanelSpray
};
private GameObject _panel;
private Font _font;
private void Awake()
{
if (!modeManager) modeManager = FindFirstObjectByType<GameModeManager>();
}
private void OnEnable()
{
if (modeManager)
{
modeManager.ModeChanged += OnModeChanged;
modeManager.PainterChanged += OnPainterChanged;
}
}
private void OnDisable()
{
if (modeManager)
{
modeManager.ModeChanged -= OnModeChanged;
modeManager.PainterChanged -= OnPainterChanged;
}
}
private void Start()
{
_font = LoadFont();
EnsureEventSystem();
EnsureCanvas();
if (layoutPrefab)
{
var go = Instantiate(layoutPrefab, transform);
go.name = layoutPrefab.name; // strip the "(Clone)" suffix
_panel = go;
}
// Bind to a painter as soon as one exists. With networked spawn-on-connect there is none
// at scene start; BindLocalPlayer then raises PainterChanged when the local avatar spawns
// and OnPainterChanged wires the widgets. In a non-networked scene the painter is already
// present and binds here immediately.
var p = painter;
if (!p) p = modeManager ? modeManager.Painter : null;
if (!p) p = FindFirstObjectByType<MeshPainter>();
if (p) BindPainter(p);
// Set initial visibility (the manager's first SwitchTo may have fired before this Start).
OnModeChanged(modeManager ? modeManager.ActiveMode : null);
}
private void OnPainterChanged(MeshPainter newPainter)
{
if (newPainter) BindPainter(newPainter);
}
/// <summary>
/// (Re)binds the HUD's widgets to a live painter. Called from Start when one already exists
/// (non-networked scene) or from the manager's PainterChanged event when the local networked
/// avatar spawns. Binds exactly once per painter: re-binding the same painter is a no-op, so
/// the deferred network path and the immediate scene path never double-add widget listeners.
/// </summary>
private void BindPainter(MeshPainter newPainter)
{
if (!newPainter || newPainter == painter || !_panel) return;
if (painter) painter.Picked -= SyncFromPainter;
painter = newPainter;
WirePanel(_panel.transform);
// Re-sync every widget when the pipette adopts a sampled material (painter -> HUD).
painter.Picked += SyncFromPainter;
// Restore the saved panel positions + widget values now that the painter exists.
RestoreHudState();
}
private void OnDestroy()
{
if (painter) painter.Picked -= SyncFromPainter;
SaveHudOnTeardown();
}
private void OnModeChanged(GameMode mode)
{
if (_panel) _panel.SetActive(mode is PaintMode);
}
private static Font LoadFont()
{
var f = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
if (!f) f = Resources.GetBuiltinResource<Font>("Arial.ttf");
return f;
}
// --- Canvas ---
private void EnsureCanvas()
{
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.screenMatchMode = CanvasScaler.ScreenMatchMode.Expand;
if (!GetComponent<GraphicRaycaster>()) gameObject.AddComponent<GraphicRaycaster>();
}
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
}
// --- Wiring (behaviour; shared by the code-gen and prefab paths) ---
private void WirePanel(Transform panel)
{
EnsureFonts(panel); // builtin font references may not survive prefab serialization
WireToggle(panel, ToggleAlbedo, painter && painter.paintAlbedo, v => { if (painter) painter.paintAlbedo = v; });
WireToggle(panel, ToggleMetallicSmoothness, painter && painter.paintMetallicSmoothness, v => { if (painter) painter.paintMetallicSmoothness = v; });
WireToggle(panel, ToggleOcclusion, painter && painter.paintOcclusion, v => { if (painter) painter.paintOcclusion = v; });
WireToggle(panel, ToggleEmission, painter && painter.paintEmission, v => { if (painter) painter.paintEmission = v; });
WireToggle(panel, ToggleSpray, painter && painter.spray, v => { if (painter) painter.spray = v; });
WireToggle(panel, TogglePipette, painter && painter.pipette, v => { if (painter) painter.pipette = v; });
// Dim a channel panel to 0.5 alpha while its toggle is off, full alpha while on.
WirePanelFade(panel, PanelAlbedo, ToggleAlbedo);
WirePanelFade(panel, PanelMetallicSmoothness, ToggleMetallicSmoothness);
WirePanelFade(panel, PanelOcclusion, ToggleOcclusion);
WirePanelFade(panel, PanelEmission, ToggleEmission);
WirePanelFade(panel, PanelSpray, ToggleSpray);
WirePicker(panel, PickerAlbedo, painter ? painter.brushColor : Color.red,
c => { if (painter) painter.brushColor = c; });
WireSlider(panel, SliderMetallic, painter ? painter.metallic : 0f, v => { if (painter) painter.metallic = v; });
WireSlider(panel, SliderSmoothness, painter ? painter.smoothness : 0.5f, v => { if (painter) painter.smoothness = v; });
WireSlider(panel, SliderMetallicSmoothnessAlpha, painter ? painter.metallicSmoothnessOpacity : 1f, v => { if (painter) painter.metallicSmoothnessOpacity = v; });
WireSlider(panel, SliderOcclusion, painter ? painter.occlusion : 0.5f, v => { if (painter) painter.occlusion = v; });
WireSlider(panel, SliderOcclusionAlpha, painter ? painter.occlusionOpacity : 1f, v => { if (painter) painter.occlusionOpacity = v; });
WirePicker(panel, PickerEmission, painter ? painter.emissionColor : Color.black,
c => { if (painter) painter.emissionColor = c; });
WireSlider(panel, SliderEmissionIntensity, painter ? painter.emissionIntensity : 1f, v => { if (painter) painter.emissionIntensity = v; });
WireSlider(panel, SliderBrushSize, painter ? painter.brushSize : 16f, v => { if (painter) painter.brushSize = Mathf.RoundToInt(v); });
WireMaskSelector(panel);
WireSlider(panel, SliderSprayDots, painter ? painter.sprayDots : 24f, v => { if (painter) painter.sprayDots = Mathf.RoundToInt(v); });
WireSlider(panel, SliderSprayDotSizeMin, painter ? painter.sprayDotSizeMin : .05f, v => { if (painter) painter.sprayDotSizeMin = v; });
WireSlider(panel, SliderSprayDotSizeMax, painter ? painter.sprayDotSizeMax : .18f, v => { if (painter) painter.sprayDotSizeMax = v; });
WireSlider(panel, SliderSprayDotAlphaMin, painter ? painter.sprayDotAlphaMin : .25f, v => { if (painter) painter.sprayDotAlphaMin = v; });
WireSlider(panel, SliderSprayDotAlphaMax, painter ? painter.sprayDotAlphaMax : 1f, v => { if (painter) painter.sprayDotAlphaMax = v; });
foreach (var name in DraggablePanels)
MakeDraggable(panel, name);
}
/// <summary>
/// Pushes the painter's current values back into every widget (painter -> HUD), e.g. after a
/// pipette pick changed them outside the UI. No listeners are added here: setting a toggle/slider
/// value fires its existing change listener, which keeps the painter, value labels and panel-fade
/// alphas in sync; the colour pickers update silently via SetColor (it does not raise Changed).
/// </summary>
private void SyncFromPainter()
{
if (!_panel || !painter) return;
var panel = _panel.transform;
SetToggle(panel, ToggleAlbedo, painter.paintAlbedo);
SetToggle(panel, ToggleMetallicSmoothness, painter.paintMetallicSmoothness);
SetToggle(panel, ToggleOcclusion, painter.paintOcclusion);
SetToggle(panel, ToggleEmission, painter.paintEmission);
SetToggle(panel, ToggleSpray, painter.spray);
SetToggle(panel, TogglePipette, painter.pipette);
SetPicker(panel, PickerAlbedo, painter.brushColor);
SetPicker(panel, PickerEmission, painter.emissionColor);
SetSlider(panel, SliderMetallic, painter.metallic);
SetSlider(panel, SliderSmoothness, painter.smoothness);
SetSlider(panel, SliderMetallicSmoothnessAlpha, painter.metallicSmoothnessOpacity);
SetSlider(panel, SliderOcclusion, painter.occlusion);
SetSlider(panel, SliderOcclusionAlpha, painter.occlusionOpacity);
SetSlider(panel, SliderEmissionIntensity, painter.emissionIntensity);
SetSlider(panel, SliderBrushSize, painter.brushSize);
SetSlider(panel, SliderSprayDots, painter.sprayDots);
SetSlider(panel, SliderSprayDotSizeMin, painter.sprayDotSizeMin);
SetSlider(panel, SliderSprayDotSizeMax, painter.sprayDotSizeMax);
SetSlider(panel, SliderSprayDotAlphaMin, painter.sprayDotAlphaMin);
SetSlider(panel, SliderSprayDotAlphaMax, painter.sprayDotAlphaMax);
RefreshMaskHighlights();
}
private static void SetToggle(Transform panel, string name, bool value)
{
var toggle = Find<Toggle>(panel, name);
if (toggle) toggle.isOn = value; // only fires onValueChanged when it actually changes
}
private static void SetSlider(Transform panel, string name, float value)
{
var cell = FindDeep(panel, name);
var slider = cell ? cell.GetComponentInChildren<Slider>() : null;
if (slider) slider.value = value; // listener refreshes the value label + painter
}
private static void SetPicker(Transform panel, string name, Color color)
{
var picker = Find<HsvColorPicker>(panel, name);
if (picker) picker.SetColor(color);
}
// Active = accent frame, inactive = subtle frame. Selection lives on the painter (activeBrushIndex);
// the swatch click only sets it - the painter resolves the new mask on its next Update.
private static readonly Color MaskFrameSelected = new Color(0.30f, 0.62f, 1f, 1f);
private static readonly Color MaskFrameUnselected = new Color(0.18f, 0.18f, 0.21f, 1f);
/// <summary>
/// Binds the baked mask swatches (one per <c>MaskSwatch_i</c>) to the painter's brush masks:
/// each swatch previews <c>painter.brushMasks[i]</c> and selects it on click. A swatch with no
/// matching mask is hidden, so the prefab can carry more swatches than the painter has masks.
/// </summary>
private void WireMaskSelector(Transform panel)
{
var selector = FindDeep(panel, MaskSelector);
if (!selector) return;
var masks = painter ? painter.brushMasks : null;
for (int i = 0; ; i++)
{
var swatch = FindDeep(selector, MaskSwatchPrefix + i);
if (!swatch) break;
bool has = masks != null && i < masks.Count && masks[i];
swatch.gameObject.SetActive(has);
if (!has) continue;
var preview = swatch.Find("Preview");
var previewImg = preview ? preview.GetComponent<Image>() : null;
if (previewImg) previewImg.sprite = MaskSprite(masks[i]);
var btn = swatch.GetComponent<Button>();
if (btn)
{
int idx = i; // capture per swatch
btn.onClick.RemoveAllListeners();
btn.onClick.AddListener(() => SelectMask(idx));
}
}
RefreshMaskHighlights();
}
private void SelectMask(int index)
{
if (painter) painter.activeBrushIndex = index;
RefreshMaskHighlights();
}
private void RefreshMaskHighlights()
{
if (!_panel || !painter) return;
var selector = FindDeep(_panel.transform, MaskSelector);
if (!selector) return;
for (int i = 0; ; i++)
{
var swatch = FindDeep(selector, MaskSwatchPrefix + i);
if (!swatch) break;
var frame = swatch.GetComponent<Image>();
if (frame) frame.color = i == painter.activeBrushIndex ? MaskFrameSelected : MaskFrameUnselected;
}
}
// One sprite per mask texture, reused across rewires (display only - no CPU readback needed).
private static readonly System.Collections.Generic.Dictionary<Texture2D, Sprite> _maskSprites
= new System.Collections.Generic.Dictionary<Texture2D, Sprite>();
private static Sprite MaskSprite(Texture2D tex)
{
if (!tex) return null;
if (_maskSprites.TryGetValue(tex, out var cached) && cached) return cached;
var s = Sprite.Create(tex, new Rect(0f, 0f, tex.width, tex.height), new Vector2(0.5f, 0.5f), 100f);
s.hideFlags = HideFlags.HideAndDontSave;
_maskSprites[tex] = s;
return s;
}
/// <summary>
/// Attaches (idempotently) a <see cref="DraggablePanel"/> to the whole panel so it can be dragged
/// from any of its non-interactive surface (header, padding, labels, background); the component
/// ignores presses that start on a widget (toggle, slider, colour picker).
/// </summary>
private static void MakeDraggable(Transform root, string panelName)
{
var panel = FindDeep(root, panelName) as RectTransform;
if (!panel) return;
var drag = panel.GetComponent<DraggablePanel>();
if (!drag) drag = panel.gameObject.AddComponent<DraggablePanel>();
drag.target = panel;
}
private void EnsureFonts(Transform panel)
{
foreach (var t in panel.GetComponentsInChildren<Text>(true))
if (!t.font) t.font = _font;
}
private static void WireToggle(Transform panel, string name, bool value, Action<bool> onChanged)
{
var toggle = Find<Toggle>(panel, name);
if (!toggle) return;
toggle.isOn = value;
toggle.onValueChanged.AddListener(v => onChanged(v));
}
/// <summary>
/// Ensures a <see cref="CanvasGroup"/> on the channel panel and drives its alpha from the toggle:
/// 1 while the channel is enabled, 0.5 while disabled. Call after <see cref="WireToggle"/> so the
/// initial alpha reflects the toggle's painter-synced state. The group stays interactive (default
/// interactable/blocksRaycasts), so a dimmed panel can still be clicked to re-enable its channel.
/// </summary>
private static void WirePanelFade(Transform root, string panelName, string toggleName)
{
var panel = FindDeep(root, panelName);
if (!panel) return;
var group = panel.GetComponent<CanvasGroup>();
if (!group) group = panel.gameObject.AddComponent<CanvasGroup>();
var toggle = Find<Toggle>(panel, toggleName);
if (!toggle) { group.alpha = 1f; return; }
group.alpha = toggle.isOn ? 1f : 0.5f;
toggle.onValueChanged.AddListener(v => group.alpha = v ? 1f : 0.5f);
}
private static void WireSlider(Transform panel, string name, float value, Action<float> onChanged)
{
var cell = FindDeep(panel, name);
if (!cell) return;
var slider = cell.GetComponentInChildren<Slider>();
if (!slider) return;
var valueRt = cell.Find("Value");
var valueText = valueRt ? valueRt.GetComponent<TMPro.TMP_Text>() : null;
bool whole = slider.wholeNumbers;
slider.value = value;
Action<float> setText = v =>
{
if (valueText) valueText.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 void WirePicker(Transform panel, string name, Color color, Action<Color> onChanged)
{
var picker = Find<HsvColorPicker>(panel, name);
if (!picker) return;
picker.Rebind(); // re-find children, re-assign runtime gradient sprites, re-hook drag relays
picker.SetColor(color);
picker.Changed += onChanged;
}
private static T Find<T>(Transform panel, string name) where T : Component
{
var child = FindDeep(panel, name);
return child ? child.GetComponent<T>() : null;
}
/// <summary>Depth-first search by name (widgets now live nested inside per-channel sub-panels).</summary>
private static Transform FindDeep(Transform root, string name)
{
if (root.name == name) return root;
for (int i = 0; i < root.childCount; i++)
{
var found = FindDeep(root.GetChild(i), name);
if (found) return found;
}
return null;
}
// --- 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 (sprite assigned by the caller).</summary>
public static Image AddOverlay(RectTransform parent, string name)
{
var rt = NewRect(name, parent);
rt.anchorMin = Vector2.zero;
rt.anchorMax = Vector2.one;
rt.offsetMin = Vector2.zero;
rt.offsetMax = Vector2.zero;
var img = rt.gameObject.AddComponent<Image>();
img.type = Image.Type.Simple;
img.raycastTarget = false;
return img;
}
// Cached gradient sprites for the colour picker.
private static Sprite _satSprite, _valSprite, _hueSprite, _alphaSprite;
/// <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 white transparency ramp: transparent (left) -> opaque (right) = alpha.
/// Tinted to the current colour by the picker so it reads as a colour-over-backdrop fade.</summary>
public static Sprite AlphaSprite()
{
if (_alphaSprite) return _alphaSprite;
const int n = 64;
var tex = NewGradientTex(n, 1);
for (int x = 0; x < n; x++)
{
byte a = (byte)Mathf.RoundToInt(255f * (x / (float)(n - 1)));
tex.SetPixel(x, 0, new Color32(255, 255, 255, a));
}
tex.Apply(false);
_alphaSprite = ToSprite(tex);
return _alphaSprite;
}
/// <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;
}
}