This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
using System;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace ShrinkTutorial
|
||||
{
|
||||
public class ShrinkTutorialDialog : MonoBehaviour
|
||||
{
|
||||
private RectTransform _rectTransform;
|
||||
private RectTransform _arrowRectTransform;
|
||||
private TextMeshProUGUI _contentText;
|
||||
private GameObject _skipButtonObject;
|
||||
private Action _skipAction;
|
||||
|
||||
public void Initialize(Transform parent, Action skipAction)
|
||||
{
|
||||
_skipAction = skipAction;
|
||||
_rectTransform = GetComponent<RectTransform>();
|
||||
if (!_rectTransform)
|
||||
_rectTransform = gameObject.AddComponent<RectTransform>();
|
||||
_rectTransform.SetParent(parent, false);
|
||||
_rectTransform.sizeDelta = new Vector2(360f, 180f);
|
||||
_rectTransform.anchorMin = new Vector2(0.5f, 0.5f);
|
||||
_rectTransform.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
_rectTransform.pivot = new Vector2(0.5f, 0.5f);
|
||||
|
||||
var background = gameObject.AddComponent<Image>();
|
||||
background.color = new Color(0.08f, 0.1f, 0.14f, 0.96f);
|
||||
background.raycastTarget = true;
|
||||
|
||||
var content = CreateText("Content", "Tutorial", 24, TextAlignmentOptions.TopLeft);
|
||||
var contentRect = content.rectTransform;
|
||||
contentRect.SetParent(_rectTransform, false);
|
||||
contentRect.anchorMin = new Vector2(0f, 0f);
|
||||
contentRect.anchorMax = new Vector2(1f, 1f);
|
||||
contentRect.offsetMin = new Vector2(24f, 64f);
|
||||
contentRect.offsetMax = new Vector2(-24f, -24f);
|
||||
_contentText = content;
|
||||
|
||||
var skipButton = new GameObject("SkipButton", typeof(RectTransform), typeof(Image), typeof(Button));
|
||||
var skipRect = skipButton.GetComponent<RectTransform>();
|
||||
skipRect.SetParent(_rectTransform, false);
|
||||
skipRect.anchorMin = new Vector2(1f, 0f);
|
||||
skipRect.anchorMax = new Vector2(1f, 0f);
|
||||
skipRect.pivot = new Vector2(1f, 0f);
|
||||
skipRect.anchoredPosition = new Vector2(-16f, 16f);
|
||||
skipRect.sizeDelta = new Vector2(96f, 36f);
|
||||
|
||||
var skipImage = skipButton.GetComponent<Image>();
|
||||
skipImage.color = new Color(0.22f, 0.34f, 0.5f, 1f);
|
||||
|
||||
var skipButtonComponent = skipButton.GetComponent<Button>();
|
||||
skipButtonComponent.onClick.AddListener(() => _skipAction?.Invoke());
|
||||
_skipButtonObject = skipButton;
|
||||
|
||||
var skipLabel = CreateText("Label", "Skip", 20, TextAlignmentOptions.Center);
|
||||
var skipLabelRect = skipLabel.rectTransform;
|
||||
skipLabelRect.SetParent(skipRect, false);
|
||||
skipLabelRect.anchorMin = Vector2.zero;
|
||||
skipLabelRect.anchorMax = Vector2.one;
|
||||
skipLabelRect.offsetMin = Vector2.zero;
|
||||
skipLabelRect.offsetMax = Vector2.zero;
|
||||
|
||||
var arrow = new GameObject("Arrow", typeof(RectTransform), typeof(Image));
|
||||
_arrowRectTransform = arrow.GetComponent<RectTransform>();
|
||||
_arrowRectTransform.SetParent(_rectTransform, false);
|
||||
_arrowRectTransform.sizeDelta = new Vector2(20f, 20f);
|
||||
_arrowRectTransform.localRotation = Quaternion.Euler(0f, 0f, 45f);
|
||||
var arrowImage = arrow.GetComponent<Image>();
|
||||
arrowImage.color = background.color;
|
||||
arrowImage.raycastTarget = false;
|
||||
}
|
||||
|
||||
public void Show(string text, Rect? targetRect, ShrinkTutorialDialogAnchor anchor, Vector2 offset, bool showArrow)
|
||||
{
|
||||
if (!_rectTransform)
|
||||
return;
|
||||
|
||||
gameObject.SetActive(true);
|
||||
_contentText.text = string.IsNullOrWhiteSpace(text) ? " " : text;
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate(_rectTransform);
|
||||
|
||||
if (!targetRect.HasValue)
|
||||
{
|
||||
_rectTransform.anchoredPosition = offset;
|
||||
_arrowRectTransform.gameObject.SetActive(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var canvasRect = _rectTransform.parent as RectTransform;
|
||||
var targetCenterScreen = targetRect.Value.center;
|
||||
RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRect, targetCenterScreen, null, out var targetCenterLocal);
|
||||
var localTargetRect = ConvertScreenRectToLocalRect(canvasRect, targetRect.Value);
|
||||
|
||||
var resolvedAnchor = ResolveAnchor(localTargetRect, anchor, canvasRect.rect);
|
||||
var panelSize = _rectTransform.rect.size;
|
||||
var margin = 24f;
|
||||
var position = targetCenterLocal + offset;
|
||||
|
||||
switch (resolvedAnchor)
|
||||
{
|
||||
case ShrinkTutorialDialogAnchor.Top:
|
||||
position += new Vector2(0f, localTargetRect.height * 0.5f + panelSize.y * 0.5f + margin);
|
||||
break;
|
||||
case ShrinkTutorialDialogAnchor.Bottom:
|
||||
position += new Vector2(0f, -(localTargetRect.height * 0.5f + panelSize.y * 0.5f + margin));
|
||||
break;
|
||||
case ShrinkTutorialDialogAnchor.Left:
|
||||
position += new Vector2(-(localTargetRect.width * 0.5f + panelSize.x * 0.5f + margin), 0f);
|
||||
break;
|
||||
case ShrinkTutorialDialogAnchor.Right:
|
||||
position += new Vector2(localTargetRect.width * 0.5f + panelSize.x * 0.5f + margin, 0f);
|
||||
break;
|
||||
}
|
||||
|
||||
var halfCanvas = canvasRect.rect.size * 0.5f;
|
||||
position.x = Mathf.Clamp(position.x, -halfCanvas.x + panelSize.x * 0.5f + 12f, halfCanvas.x - panelSize.x * 0.5f - 12f);
|
||||
position.y = Mathf.Clamp(position.y, -halfCanvas.y + panelSize.y * 0.5f + 12f, halfCanvas.y - panelSize.y * 0.5f - 12f);
|
||||
_rectTransform.anchoredPosition = position;
|
||||
|
||||
ConfigureArrow(resolvedAnchor, showArrow);
|
||||
}
|
||||
|
||||
public void Hide()
|
||||
{
|
||||
if (gameObject.activeSelf)
|
||||
gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
public bool ContainsScreenPoint(Vector2 screenPoint)
|
||||
{
|
||||
return _rectTransform && gameObject.activeInHierarchy &&
|
||||
RectTransformUtility.RectangleContainsScreenPoint(_rectTransform, screenPoint, null);
|
||||
}
|
||||
|
||||
public void SetSkipVisible(bool visible)
|
||||
{
|
||||
if (_skipButtonObject)
|
||||
_skipButtonObject.SetActive(visible);
|
||||
}
|
||||
|
||||
private void ConfigureArrow(ShrinkTutorialDialogAnchor anchor, bool showArrow)
|
||||
{
|
||||
_arrowRectTransform.gameObject.SetActive(showArrow);
|
||||
if (!showArrow)
|
||||
return;
|
||||
|
||||
switch (anchor)
|
||||
{
|
||||
case ShrinkTutorialDialogAnchor.Top:
|
||||
_arrowRectTransform.anchorMin = new Vector2(0.5f, 0f);
|
||||
_arrowRectTransform.anchorMax = new Vector2(0.5f, 0f);
|
||||
_arrowRectTransform.anchoredPosition = new Vector2(0f, -10f);
|
||||
break;
|
||||
case ShrinkTutorialDialogAnchor.Bottom:
|
||||
_arrowRectTransform.anchorMin = new Vector2(0.5f, 1f);
|
||||
_arrowRectTransform.anchorMax = new Vector2(0.5f, 1f);
|
||||
_arrowRectTransform.anchoredPosition = new Vector2(0f, 10f);
|
||||
break;
|
||||
case ShrinkTutorialDialogAnchor.Left:
|
||||
_arrowRectTransform.anchorMin = new Vector2(1f, 0.5f);
|
||||
_arrowRectTransform.anchorMax = new Vector2(1f, 0.5f);
|
||||
_arrowRectTransform.anchoredPosition = new Vector2(10f, 0f);
|
||||
break;
|
||||
case ShrinkTutorialDialogAnchor.Right:
|
||||
_arrowRectTransform.anchorMin = new Vector2(0f, 0.5f);
|
||||
_arrowRectTransform.anchorMax = new Vector2(0f, 0.5f);
|
||||
_arrowRectTransform.anchoredPosition = new Vector2(-10f, 0f);
|
||||
break;
|
||||
default:
|
||||
_arrowRectTransform.gameObject.SetActive(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static ShrinkTutorialDialogAnchor ResolveAnchor(Rect targetRect, ShrinkTutorialDialogAnchor preferredAnchor, Rect canvasRect)
|
||||
{
|
||||
if (preferredAnchor != ShrinkTutorialDialogAnchor.Auto)
|
||||
return preferredAnchor;
|
||||
|
||||
var topSpace = canvasRect.yMax - targetRect.yMax;
|
||||
var bottomSpace = targetRect.yMin - canvasRect.yMin;
|
||||
if (topSpace >= 240f)
|
||||
return ShrinkTutorialDialogAnchor.Top;
|
||||
if (bottomSpace >= 240f)
|
||||
return ShrinkTutorialDialogAnchor.Bottom;
|
||||
|
||||
var rightSpace = canvasRect.xMax - targetRect.xMax;
|
||||
var leftSpace = targetRect.xMin - canvasRect.xMin;
|
||||
return rightSpace >= leftSpace
|
||||
? ShrinkTutorialDialogAnchor.Right
|
||||
: ShrinkTutorialDialogAnchor.Left;
|
||||
}
|
||||
|
||||
private static Rect ConvertScreenRectToLocalRect(RectTransform canvasRect, Rect screenRect)
|
||||
{
|
||||
RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRect,
|
||||
new Vector2(screenRect.xMin, screenRect.yMin), null, out var minLocal);
|
||||
RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRect,
|
||||
new Vector2(screenRect.xMax, screenRect.yMax), null, out var maxLocal);
|
||||
|
||||
return Rect.MinMaxRect(
|
||||
Mathf.Min(minLocal.x, maxLocal.x),
|
||||
Mathf.Min(minLocal.y, maxLocal.y),
|
||||
Mathf.Max(minLocal.x, maxLocal.x),
|
||||
Mathf.Max(minLocal.y, maxLocal.y));
|
||||
}
|
||||
|
||||
private static TextMeshProUGUI CreateText(string objectName, string text, float fontSize, TextAlignmentOptions alignment)
|
||||
{
|
||||
var textObject = new GameObject(objectName, typeof(RectTransform), typeof(TextMeshProUGUI));
|
||||
var tmp = textObject.GetComponent<TextMeshProUGUI>();
|
||||
tmp.text = text;
|
||||
tmp.fontSize = fontSize;
|
||||
tmp.alignment = alignment;
|
||||
tmp.color = Color.white;
|
||||
tmp.enableWordWrapping = true;
|
||||
return tmp;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9380f8798cf316c4d9a7d9d743037688
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,135 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace ShrinkTutorial
|
||||
{
|
||||
public class ShrinkTutorialMask : MaskableGraphic, ICanvasRaycastFilter
|
||||
{
|
||||
private Rect _holeRect;
|
||||
private ShrinkTutorialMaskShape _shape;
|
||||
private bool _hasHole;
|
||||
|
||||
public void SetHighlight(Rect screenRect, ShrinkTutorialMaskShape shape, float padding)
|
||||
{
|
||||
enabled = true;
|
||||
raycastTarget = true;
|
||||
|
||||
var camera = GetEventCamera();
|
||||
var minScreen = new Vector2(screenRect.xMin - padding, screenRect.yMin - padding);
|
||||
var maxScreen = new Vector2(screenRect.xMax + padding, screenRect.yMax + padding);
|
||||
|
||||
RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, minScreen, camera, out var minLocal);
|
||||
RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, maxScreen, camera, out var maxLocal);
|
||||
|
||||
_holeRect = Rect.MinMaxRect(
|
||||
Mathf.Min(minLocal.x, maxLocal.x),
|
||||
Mathf.Min(minLocal.y, maxLocal.y),
|
||||
Mathf.Max(minLocal.x, maxLocal.x),
|
||||
Mathf.Max(minLocal.y, maxLocal.y));
|
||||
_shape = shape;
|
||||
_hasHole = shape != ShrinkTutorialMaskShape.None;
|
||||
SetVerticesDirty();
|
||||
}
|
||||
|
||||
public void ClearHighlight()
|
||||
{
|
||||
_hasHole = false;
|
||||
_shape = ShrinkTutorialMaskShape.None;
|
||||
_holeRect = default;
|
||||
raycastTarget = false;
|
||||
SetVerticesDirty();
|
||||
enabled = false;
|
||||
}
|
||||
|
||||
public bool IsRaycastLocationValid(Vector2 screenPoint, Camera eventCamera)
|
||||
{
|
||||
if (!_hasHole)
|
||||
return false;
|
||||
|
||||
RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, screenPoint, eventCamera, out var localPoint);
|
||||
if (_shape == ShrinkTutorialMaskShape.Circle)
|
||||
{
|
||||
var radius = Mathf.Min(_holeRect.width, _holeRect.height) * 0.5f;
|
||||
return Vector2.SqrMagnitude(localPoint - _holeRect.center) > radius * radius;
|
||||
}
|
||||
|
||||
return !_holeRect.Contains(localPoint);
|
||||
}
|
||||
|
||||
protected override void OnPopulateMesh(VertexHelper vh)
|
||||
{
|
||||
vh.Clear();
|
||||
|
||||
var outer = rectTransform.rect;
|
||||
if (!_hasHole)
|
||||
return;
|
||||
|
||||
var hole = ClampRect(_holeRect, outer);
|
||||
|
||||
if (hole.yMax < outer.yMax)
|
||||
{
|
||||
AddQuad(vh,
|
||||
new Vector2(outer.xMin, hole.yMax),
|
||||
new Vector2(outer.xMin, outer.yMax),
|
||||
new Vector2(outer.xMax, outer.yMax),
|
||||
new Vector2(outer.xMax, hole.yMax));
|
||||
}
|
||||
|
||||
if (hole.yMin > outer.yMin)
|
||||
{
|
||||
AddQuad(vh,
|
||||
new Vector2(outer.xMin, outer.yMin),
|
||||
new Vector2(outer.xMin, hole.yMin),
|
||||
new Vector2(outer.xMax, hole.yMin),
|
||||
new Vector2(outer.xMax, outer.yMin));
|
||||
}
|
||||
|
||||
if (hole.xMin > outer.xMin)
|
||||
{
|
||||
AddQuad(vh,
|
||||
new Vector2(outer.xMin, hole.yMin),
|
||||
new Vector2(outer.xMin, hole.yMax),
|
||||
new Vector2(hole.xMin, hole.yMax),
|
||||
new Vector2(hole.xMin, hole.yMin));
|
||||
}
|
||||
|
||||
if (hole.xMax < outer.xMax)
|
||||
{
|
||||
AddQuad(vh,
|
||||
new Vector2(hole.xMax, hole.yMin),
|
||||
new Vector2(hole.xMax, hole.yMax),
|
||||
new Vector2(outer.xMax, hole.yMax),
|
||||
new Vector2(outer.xMax, hole.yMin));
|
||||
}
|
||||
}
|
||||
|
||||
private static Rect ClampRect(Rect source, Rect bounds)
|
||||
{
|
||||
return Rect.MinMaxRect(
|
||||
Mathf.Clamp(source.xMin, bounds.xMin, bounds.xMax),
|
||||
Mathf.Clamp(source.yMin, bounds.yMin, bounds.yMax),
|
||||
Mathf.Clamp(source.xMax, bounds.xMin, bounds.xMax),
|
||||
Mathf.Clamp(source.yMax, bounds.yMin, bounds.yMax));
|
||||
}
|
||||
|
||||
private void AddQuad(VertexHelper vh, Vector2 bottomLeft, Vector2 topLeft, Vector2 topRight, Vector2 bottomRight)
|
||||
{
|
||||
var startIndex = vh.currentVertCount;
|
||||
vh.AddVert(bottomLeft, color, Vector2.zero);
|
||||
vh.AddVert(topLeft, color, Vector2.up);
|
||||
vh.AddVert(topRight, color, Vector2.one);
|
||||
vh.AddVert(bottomRight, color, Vector2.right);
|
||||
vh.AddTriangle(startIndex, startIndex + 1, startIndex + 2);
|
||||
vh.AddTriangle(startIndex, startIndex + 2, startIndex + 3);
|
||||
}
|
||||
|
||||
private Camera GetEventCamera()
|
||||
{
|
||||
var currentCanvas = canvas ? canvas : GetComponentInParent<Canvas>();
|
||||
if (!currentCanvas || currentCanvas.renderMode == RenderMode.ScreenSpaceOverlay)
|
||||
return null;
|
||||
|
||||
return currentCanvas.worldCamera;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d5cadf3e241805441be468ff8eec081a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user