1. 跑通第一章全部流程 2. 优化拼装玩法体验
This commit is contained in:
@@ -318,6 +318,7 @@ namespace CustomComponent
|
||||
{
|
||||
slot.SetOccupiedPart(part);
|
||||
part.PlaceToSlot(slot);
|
||||
HidePlacedPair(slot, part);
|
||||
|
||||
_placedCount++;
|
||||
AdvanceOrderCursor();
|
||||
@@ -338,8 +339,6 @@ namespace CustomComponent
|
||||
}
|
||||
|
||||
_isCompleted = true;
|
||||
|
||||
GameEntry.Event.Fire(this, CombineCompletedEventArgs.Create());
|
||||
_onPuzzleCompleted.Invoke();
|
||||
}
|
||||
|
||||
@@ -371,6 +370,19 @@ namespace CustomComponent
|
||||
GameEntry.Event.Fire(this, CombineFeedbackEventArgs.Create(feedbackType, message));
|
||||
}
|
||||
|
||||
private static void HidePlacedPair(CombineSlot slot, CombineDraggablePart part)
|
||||
{
|
||||
if (slot != null && slot.gameObject != null)
|
||||
{
|
||||
slot.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
if (part != null && part.gameObject != null)
|
||||
{
|
||||
part.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Runtime Context
|
||||
@@ -569,6 +581,7 @@ namespace CustomComponent
|
||||
for (int i = 0; i < _orderedSlots.Count; i++)
|
||||
{
|
||||
CombineSlot slot = _orderedSlots[i];
|
||||
slot.gameObject.SetActive(true);
|
||||
slot.BindController(this);
|
||||
slot.ResetSlot();
|
||||
}
|
||||
@@ -587,6 +600,7 @@ namespace CustomComponent
|
||||
continue;
|
||||
}
|
||||
|
||||
part.gameObject.SetActive(true);
|
||||
part.BindController(this);
|
||||
part.CacheSpawnState();
|
||||
part.ResetToSpawn();
|
||||
|
||||
@@ -73,6 +73,8 @@ namespace UI
|
||||
|
||||
private Tween _returnTween;
|
||||
|
||||
private Vector2 _dragPointerOffset = Vector2.zero;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Query
|
||||
@@ -185,10 +187,11 @@ namespace UI
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
MoveToDragRoot();
|
||||
_rectTransform.SetAsLastSibling();
|
||||
CacheDragOffset(eventData);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 拖拽中:按指针增量更新锚点位置。
|
||||
/// 拖拽中:按屏幕坐标映射到父节点局部坐标更新位置。
|
||||
/// </summary>
|
||||
public void OnDrag(PointerEventData eventData)
|
||||
{
|
||||
@@ -197,6 +200,13 @@ namespace UI
|
||||
return;
|
||||
}
|
||||
|
||||
if (TryGetDragParent(out RectTransform dragParent) &&
|
||||
TryGetPointerLocalPosition(eventData, dragParent, out Vector2 pointerLocalPosition))
|
||||
{
|
||||
_rectTransform.anchoredPosition = pointerLocalPosition + _dragPointerOffset;
|
||||
return;
|
||||
}
|
||||
|
||||
_rectTransform.anchoredPosition += eventData.delta;
|
||||
}
|
||||
|
||||
@@ -216,6 +226,11 @@ namespace UI
|
||||
|
||||
if (!_isPlaced)
|
||||
{
|
||||
if (ShouldKeepDroppedInPartArea(eventData))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ReturnToSpawnAnimated();
|
||||
}
|
||||
}
|
||||
@@ -273,6 +288,138 @@ namespace UI
|
||||
}
|
||||
}
|
||||
|
||||
private void CacheDragOffset(PointerEventData eventData)
|
||||
{
|
||||
_dragPointerOffset = Vector2.zero;
|
||||
|
||||
if (!TryGetDragParent(out RectTransform dragParent))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (TryGetPointerLocalPosition(eventData, dragParent, out Vector2 pointerLocalPosition))
|
||||
{
|
||||
_dragPointerOffset = _rectTransform.anchoredPosition - pointerLocalPosition;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryGetDragParent(out RectTransform dragParent)
|
||||
{
|
||||
dragParent = _rectTransform.parent as RectTransform;
|
||||
return dragParent != null;
|
||||
}
|
||||
|
||||
private bool TryGetPointerLocalPosition(PointerEventData eventData, RectTransform targetRect,
|
||||
out Vector2 localPosition)
|
||||
{
|
||||
localPosition = Vector2.zero;
|
||||
if (eventData == null || targetRect == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Camera eventCamera = ResolveEventCamera(eventData);
|
||||
return RectTransformUtility.ScreenPointToLocalPointInRectangle(targetRect, eventData.position, eventCamera,
|
||||
out localPosition);
|
||||
}
|
||||
|
||||
private Camera ResolveEventCamera(PointerEventData eventData)
|
||||
{
|
||||
if (eventData != null)
|
||||
{
|
||||
if (eventData.pressEventCamera != null)
|
||||
{
|
||||
return eventData.pressEventCamera;
|
||||
}
|
||||
|
||||
if (eventData.enterEventCamera != null)
|
||||
{
|
||||
return eventData.enterEventCamera;
|
||||
}
|
||||
}
|
||||
|
||||
Canvas canvas = GetComponentInParent<Canvas>();
|
||||
if (canvas != null && canvas.renderMode != RenderMode.ScreenSpaceOverlay)
|
||||
{
|
||||
return canvas.worldCamera;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool ShouldKeepDroppedInPartArea(PointerEventData eventData)
|
||||
{
|
||||
if (eventData == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryGetDragArea(out RectTransform dragArea))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Camera eventCamera = ResolveEventCamera(eventData);
|
||||
if (!RectTransformUtility.RectangleContainsScreenPoint(dragArea, eventData.position, eventCamera))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ClampInsideDragArea(dragArea);
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryGetDragArea(out RectTransform dragArea)
|
||||
{
|
||||
dragArea = null;
|
||||
if (_controller == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
dragArea = _controller.GetDragRoot() as RectTransform;
|
||||
return dragArea != null;
|
||||
}
|
||||
|
||||
private void ClampInsideDragArea(RectTransform dragArea)
|
||||
{
|
||||
if (dragArea == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2 current = _rectTransform.anchoredPosition;
|
||||
Rect areaRect = dragArea.rect;
|
||||
Rect partRect = _rectTransform.rect;
|
||||
Vector2 pivot = _rectTransform.pivot;
|
||||
Vector3 scale = _rectTransform.localScale;
|
||||
|
||||
float width = Mathf.Abs(partRect.width * scale.x);
|
||||
float height = Mathf.Abs(partRect.height * scale.y);
|
||||
float minX = areaRect.xMin + width * Mathf.Clamp01(pivot.x);
|
||||
float maxX = areaRect.xMax - width * (1f - Mathf.Clamp01(pivot.x));
|
||||
float minY = areaRect.yMin + height * Mathf.Clamp01(pivot.y);
|
||||
float maxY = areaRect.yMax - height * (1f - Mathf.Clamp01(pivot.y));
|
||||
|
||||
if (minX > maxX)
|
||||
{
|
||||
float centerX = (areaRect.xMin + areaRect.xMax) * 0.5f;
|
||||
minX = centerX;
|
||||
maxX = centerX;
|
||||
}
|
||||
|
||||
if (minY > maxY)
|
||||
{
|
||||
float centerY = (areaRect.yMin + areaRect.yMax) * 0.5f;
|
||||
minY = centerY;
|
||||
maxY = centerY;
|
||||
}
|
||||
|
||||
current.x = Mathf.Clamp(current.x, minX, maxX);
|
||||
current.y = Mathf.Clamp(current.y, minY, maxY);
|
||||
_rectTransform.anchoredPosition = current;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Placement
|
||||
@@ -307,13 +454,7 @@ namespace UI
|
||||
_currentSlot = slot;
|
||||
_isPlaced = true;
|
||||
_isLocked = _lockAfterPlaced;
|
||||
|
||||
RectTransform snapPoint = slot.SnapPoint;
|
||||
_rectTransform.SetParent(snapPoint != null ? snapPoint : slot.transform, false);
|
||||
_rectTransform.anchoredPosition = Vector2.zero;
|
||||
_rectTransform.localRotation = Quaternion.identity;
|
||||
_rectTransform.localScale = Vector3.one;
|
||||
_canvasGroup.blocksRaycasts = !_isLocked;
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using CustomComponent;
|
||||
using Event;
|
||||
@@ -37,8 +38,20 @@ namespace UI
|
||||
|
||||
[SerializeField] [Range(0f, 1f)] private float _partSpawnMaxYNormalized = 0.9f;
|
||||
|
||||
[SerializeField] private float _completeToFinalStageDelay = 0.35f;
|
||||
|
||||
[SerializeField] private float _finalStageToCloseDelay = 0.35f;
|
||||
|
||||
private CombineComponent _controller;
|
||||
|
||||
private CombineStageOverlay _stageOverlay;
|
||||
|
||||
private Coroutine _completeFlowCoroutine;
|
||||
|
||||
private bool _completionFlowStarted;
|
||||
|
||||
private bool _fireCompletedEventOnClose;
|
||||
|
||||
private readonly List<GameObject> _runtimeNodes = new();
|
||||
|
||||
#endregion
|
||||
@@ -78,6 +91,10 @@ namespace UI
|
||||
_controller.BindRuntimeContext(_slotRoot, _partRoot);
|
||||
|
||||
GameEntry.Event.Subscribe(CombineFeedbackEventArgs.EventId, OnFeedbackChanged);
|
||||
GameEntry.Event.Subscribe(CombineProgressEventArgs.EventId, OnCombineProgressChanged);
|
||||
|
||||
_completionFlowStarted = false;
|
||||
_fireCompletedEventOnClose = false;
|
||||
|
||||
if (context.AutoStart)
|
||||
{
|
||||
@@ -104,6 +121,15 @@ namespace UI
|
||||
protected override void OnClose(bool isShutdown, object userData)
|
||||
{
|
||||
GameEntry.Event.Unsubscribe(CombineFeedbackEventArgs.EventId, OnFeedbackChanged);
|
||||
GameEntry.Event.Unsubscribe(CombineProgressEventArgs.EventId, OnCombineProgressChanged);
|
||||
|
||||
StopCompleteFlow();
|
||||
|
||||
if (_fireCompletedEventOnClose)
|
||||
{
|
||||
_fireCompletedEventOnClose = false;
|
||||
GameEntry.Event.Fire(this, CombineCompletedEventArgs.Create());
|
||||
}
|
||||
|
||||
if (_controller != null)
|
||||
{
|
||||
@@ -163,6 +189,8 @@ namespace UI
|
||||
|
||||
private void ClearRuntimeNodes()
|
||||
{
|
||||
_stageOverlay = null;
|
||||
|
||||
for (int i = _runtimeNodes.Count - 1; i >= 0; i--)
|
||||
{
|
||||
GameObject node = _runtimeNodes[i];
|
||||
@@ -213,6 +241,7 @@ namespace UI
|
||||
CombineStageOverlay stageOverlay = structureNode.GetComponentInChildren<CombineStageOverlay>(true);
|
||||
if (stageOverlay != null)
|
||||
{
|
||||
_stageOverlay = stageOverlay;
|
||||
List<CombineSlot> managedSlots = stageOverlay.GetManagedSlots();
|
||||
if (managedSlots.Count > 0)
|
||||
{
|
||||
@@ -313,6 +342,7 @@ namespace UI
|
||||
if (rootRect.width <= 0f || rootRect.height <= 0f)
|
||||
{
|
||||
partRect.anchoredPosition = _partStartAnchoredPosition + Vector2.down * (_partVerticalSpacing * index);
|
||||
ClampAnchoredPositionToPartRoot(partRect);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -325,9 +355,69 @@ namespace UI
|
||||
float maxY = Mathf.Lerp(rootRect.yMin, rootRect.yMax,
|
||||
Mathf.Max(_partSpawnMinYNormalized, _partSpawnMaxYNormalized));
|
||||
|
||||
Vector2 size = GetPartSize(partRect);
|
||||
Vector2 xRange = BuildSafeAxisRange(minX, maxX, rootRect.xMin, rootRect.xMax, size.x, partRect.pivot.x);
|
||||
Vector2 yRange = BuildSafeAxisRange(minY, maxY, rootRect.yMin, rootRect.yMax, size.y, partRect.pivot.y);
|
||||
|
||||
partRect.anchoredPosition = new Vector2(
|
||||
Random.Range(minX, maxX),
|
||||
Random.Range(minY, maxY));
|
||||
Random.Range(xRange.x, xRange.y),
|
||||
Random.Range(yRange.x, yRange.y));
|
||||
}
|
||||
|
||||
private static Vector2 BuildSafeAxisRange(float desiredMin, float desiredMax, float containerMin, float containerMax,
|
||||
float partSize, float pivot)
|
||||
{
|
||||
float min = Mathf.Min(desiredMin, desiredMax);
|
||||
float max = Mathf.Max(desiredMin, desiredMax);
|
||||
|
||||
float size = Mathf.Max(0f, partSize);
|
||||
float safeMin = containerMin + size * Mathf.Clamp01(pivot);
|
||||
float safeMax = containerMax - size * (1f - Mathf.Clamp01(pivot));
|
||||
|
||||
if (safeMin > safeMax)
|
||||
{
|
||||
float center = (containerMin + containerMax) * 0.5f;
|
||||
safeMin = center;
|
||||
safeMax = center;
|
||||
}
|
||||
|
||||
min = Mathf.Clamp(min, safeMin, safeMax);
|
||||
max = Mathf.Clamp(max, safeMin, safeMax);
|
||||
|
||||
if (min > max)
|
||||
{
|
||||
float temp = min;
|
||||
min = max;
|
||||
max = temp;
|
||||
}
|
||||
|
||||
return new Vector2(min, max);
|
||||
}
|
||||
|
||||
private static Vector2 GetPartSize(RectTransform partRect)
|
||||
{
|
||||
Rect rect = partRect.rect;
|
||||
Vector3 scale = partRect.localScale;
|
||||
return new Vector2(
|
||||
Mathf.Abs(rect.width * scale.x),
|
||||
Mathf.Abs(rect.height * scale.y));
|
||||
}
|
||||
|
||||
private void ClampAnchoredPositionToPartRoot(RectTransform partRect)
|
||||
{
|
||||
if (partRect == null || _partRoot == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Rect rootRect = _partRoot.rect;
|
||||
Vector2 anchored = partRect.anchoredPosition;
|
||||
Vector2 size = GetPartSize(partRect);
|
||||
|
||||
Vector2 xRange = BuildSafeAxisRange(anchored.x, anchored.x, rootRect.xMin, rootRect.xMax, size.x, partRect.pivot.x);
|
||||
Vector2 yRange = BuildSafeAxisRange(anchored.y, anchored.y, rootRect.yMin, rootRect.yMax, size.y, partRect.pivot.y);
|
||||
|
||||
partRect.anchoredPosition = new Vector2(xRange.x, yRange.x);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -343,6 +433,60 @@ namespace UI
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCombineProgressChanged(object sender, GameEventArgs e)
|
||||
{
|
||||
if (!(e is CombineProgressEventArgs args))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.TotalSteps <= 0 || args.CurrentStep < args.TotalSteps || _completionFlowStarted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_completionFlowStarted = true;
|
||||
StopCompleteFlow();
|
||||
_completeFlowCoroutine = StartCoroutine(PlayCompleteFlow());
|
||||
}
|
||||
|
||||
private IEnumerator PlayCompleteFlow()
|
||||
{
|
||||
float delayToFinalStage = Mathf.Max(0f, _completeToFinalStageDelay);
|
||||
if (delayToFinalStage > 0f)
|
||||
{
|
||||
yield return new WaitForSecondsRealtime(delayToFinalStage);
|
||||
}
|
||||
|
||||
if (_stageOverlay != null && _controller != null)
|
||||
{
|
||||
// For N parts, progress displays sprite[N-1]; completion tail uses sprite[N].
|
||||
int finalStageIndex = _controller.TotalStep;
|
||||
_stageOverlay.ApplyOverlayByIndex(finalStageIndex);
|
||||
}
|
||||
|
||||
float delayToClose = Mathf.Max(0f, _finalStageToCloseDelay);
|
||||
if (delayToClose > 0f)
|
||||
{
|
||||
yield return new WaitForSecondsRealtime(delayToClose);
|
||||
}
|
||||
|
||||
_fireCompletedEventOnClose = true;
|
||||
_completeFlowCoroutine = null;
|
||||
Close();
|
||||
}
|
||||
|
||||
private void StopCompleteFlow()
|
||||
{
|
||||
if (_completeFlowCoroutine == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
StopCoroutine(_completeFlowCoroutine);
|
||||
_completeFlowCoroutine = null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,19 +96,35 @@ namespace UI
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentStep <= 0 || _stageSprites == null || _stageSprites.Count == 0)
|
||||
// Keep overlay's initial sprite/layout untouched before any placement.
|
||||
if (currentStep <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_stageSprites == null || _stageSprites.Count == 0)
|
||||
{
|
||||
_overlayImage.sprite = null;
|
||||
return;
|
||||
}
|
||||
|
||||
int spriteIndex = Mathf.Clamp(currentStep - 1, 0, _stageSprites.Count - 1);
|
||||
Sprite sprite = _stageSprites[spriteIndex];
|
||||
_overlayImage.sprite = sprite;
|
||||
if (sprite != null)
|
||||
ApplyOverlayByIndex(spriteIndex);
|
||||
}
|
||||
|
||||
public bool ApplyOverlayByIndex(int spriteIndex)
|
||||
{
|
||||
if (_overlayImage == null || _stageSprites == null || _stageSprites.Count == 0)
|
||||
{
|
||||
_overlayImage.SetNativeSize();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (spriteIndex < 0 || spriteIndex >= _stageSprites.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_overlayImage.sprite = _stageSprites[spriteIndex];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user