迭代拼装玩法与素材
This commit is contained in:
@@ -3,6 +3,7 @@ using DG.Tweening;
|
||||
using Definition.Enum;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace UI
|
||||
{
|
||||
@@ -16,16 +17,14 @@ namespace UI
|
||||
|
||||
[SerializeField] private CombinePartType _partType = CombinePartType.Dou;
|
||||
|
||||
[SerializeField] private string _partDisplayName = "Dou";
|
||||
|
||||
[SerializeField] [TextArea(2, 4)] private string _mechanicsExplanation = string.Empty;
|
||||
|
||||
[SerializeField] private bool _lockAfterPlaced = true;
|
||||
|
||||
[SerializeField] private RectTransform _rectTransform;
|
||||
|
||||
[SerializeField] private CanvasGroup _canvasGroup;
|
||||
|
||||
[SerializeField] private Image _partImage;
|
||||
|
||||
[SerializeField] private float _defaultScaleMultiplier = 0.8f;
|
||||
|
||||
[SerializeField] private float _hoverScaleMultiplier = 1f;
|
||||
@@ -80,8 +79,6 @@ namespace UI
|
||||
|
||||
public CombinePartType PartType => _partType;
|
||||
|
||||
public string MechanicsExplanation => _mechanicsExplanation;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Unity Lifecycle
|
||||
@@ -120,17 +117,23 @@ namespace UI
|
||||
_controller = controller;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用运行时数据初始化部件配置。
|
||||
/// </summary>
|
||||
public void Initialize(CombinePartContext data)
|
||||
public void ConfigureRuntime(CombinePartType partType, Sprite sprite)
|
||||
{
|
||||
_partType = data.PartType;
|
||||
_partDisplayName = string.IsNullOrEmpty(data.PartDisplayName)
|
||||
? data.PartType.ToString()
|
||||
: data.PartDisplayName;
|
||||
_mechanicsExplanation = data.MechanicsExplanation ?? string.Empty;
|
||||
_lockAfterPlaced = data.LockAfterPlaced;
|
||||
_partType = partType;
|
||||
|
||||
if (_partImage == null)
|
||||
{
|
||||
_partImage = GetComponentInChildren<Image>(true);
|
||||
}
|
||||
|
||||
if (_partImage != null)
|
||||
{
|
||||
_partImage.sprite = sprite;
|
||||
if (sprite != null)
|
||||
{
|
||||
_partImage.SetNativeSize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -4,6 +4,7 @@ using Event;
|
||||
using GameFramework.Event;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace UI
|
||||
@@ -15,23 +16,14 @@ namespace UI
|
||||
{
|
||||
#region Property
|
||||
|
||||
[SerializeField] private CombineComponent _controller;
|
||||
|
||||
[SerializeField] private RectTransform _slotRoot;
|
||||
|
||||
[SerializeField] private RectTransform _partRoot;
|
||||
|
||||
[SerializeField] private CombineSlot _slotPrefab;
|
||||
|
||||
[SerializeField] private CombineDraggablePart _partPrefab;
|
||||
|
||||
[SerializeField] private TMP_Text _guideText;
|
||||
|
||||
[SerializeField] private TMP_Text _explanationText;
|
||||
|
||||
[SerializeField] private TMP_Text _progressText;
|
||||
|
||||
[SerializeField] private string _progressFormat = "{0}/{1}";
|
||||
[FormerlySerializedAs("_progressText")] [SerializeField]
|
||||
private TMP_Text _hintText;
|
||||
|
||||
[SerializeField] private Vector2 _partStartAnchoredPosition = new(0f, -40f);
|
||||
|
||||
@@ -45,6 +37,8 @@ namespace UI
|
||||
|
||||
[SerializeField] [Range(0f, 1f)] private float _partSpawnMaxYNormalized = 0.9f;
|
||||
|
||||
private CombineComponent _controller;
|
||||
|
||||
private readonly List<GameObject> _runtimeNodes = new();
|
||||
|
||||
#endregion
|
||||
@@ -71,32 +65,24 @@ namespace UI
|
||||
{
|
||||
_controller = GameEntry.Combine;
|
||||
}
|
||||
|
||||
|
||||
if (userData is CombineFormContext context)
|
||||
{
|
||||
if (context.Slots.Count == 0)
|
||||
if (!CanBuildContext(context))
|
||||
{
|
||||
Log.Warning("CombineMvcForm open failed. Slot model is empty.");
|
||||
Log.Warning("CombineMvcForm open failed. StructurePrefab/PartSprites is invalid.");
|
||||
return;
|
||||
}
|
||||
|
||||
Build(context);
|
||||
_controller.BindRuntimeContext(_slotRoot, _partRoot);
|
||||
|
||||
GameEntry.Event.Subscribe(CombineGuideMessageEventArgs.EventId, OnGuideMessageChanged);
|
||||
GameEntry.Event.Subscribe(CombinePartMessageEventArgs.EventId, OnPartExplained);
|
||||
GameEntry.Event.Subscribe(CombineProgressEventArgs.EventId, OnProgressChanged);
|
||||
GameEntry.Event.Subscribe(CombineCompletedEventArgs.EventId, OnPuzzleCompleted);
|
||||
GameEntry.Event.Subscribe(CombineFeedbackEventArgs.EventId, OnFeedbackChanged);
|
||||
|
||||
if (context.AutoStart)
|
||||
{
|
||||
_controller.StartPuzzle();
|
||||
}
|
||||
else
|
||||
{
|
||||
GameEntry.Event.Fire(this,
|
||||
CombineProgressEventArgs.Create(_controller.CurrentStep, _controller.TotalStep));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -104,12 +90,21 @@ namespace UI
|
||||
}
|
||||
}
|
||||
|
||||
private static bool CanBuildContext(CombineFormContext context)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool hasStructurePrefab = context.StructurePrefab != null;
|
||||
bool hasPartSprites = context.PartSprites != null && context.PartSprites.Count > 0;
|
||||
return hasStructurePrefab && hasPartSprites;
|
||||
}
|
||||
|
||||
protected override void OnClose(bool isShutdown, object userData)
|
||||
{
|
||||
GameEntry.Event.Unsubscribe(CombineGuideMessageEventArgs.EventId, OnGuideMessageChanged);
|
||||
GameEntry.Event.Unsubscribe(CombinePartMessageEventArgs.EventId, OnPartExplained);
|
||||
GameEntry.Event.Unsubscribe(CombineProgressEventArgs.EventId, OnProgressChanged);
|
||||
GameEntry.Event.Unsubscribe(CombineCompletedEventArgs.EventId, OnPuzzleCompleted);
|
||||
GameEntry.Event.Unsubscribe(CombineFeedbackEventArgs.EventId, OnFeedbackChanged);
|
||||
|
||||
if (_controller != null)
|
||||
{
|
||||
@@ -180,58 +175,131 @@ namespace UI
|
||||
|
||||
_runtimeNodes.Clear();
|
||||
}
|
||||
|
||||
|
||||
private void ResetTexts()
|
||||
{
|
||||
GameEntry.Event.Fire(this, CombineGuideMessageEventArgs.Create(string.Empty));
|
||||
GameEntry.Event.Fire(this, CombinePartMessageEventArgs.Create(string.Empty));
|
||||
GameEntry.Event.Fire(this, CombineProgressEventArgs.Create(0, 0));
|
||||
if (_hintText != null)
|
||||
{
|
||||
_hintText.text = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private void Build(CombineFormContext context)
|
||||
{
|
||||
ClearRuntimeNodes();
|
||||
|
||||
foreach (var slotData in context.Slots)
|
||||
List<CombineSlot> runtimeSlots = BuildSlots(context);
|
||||
if (runtimeSlots.Count == 0)
|
||||
{
|
||||
CombineSlot slot = Instantiate(_slotPrefab, _slotRoot, false);
|
||||
slot.Initialize(slotData);
|
||||
slot.SetSnapPoint(slot.transform as RectTransform);
|
||||
_runtimeNodes.Add(slot.gameObject);
|
||||
Log.Warning("CombineMvcForm build failed. No CombineSlot found in structure prefab.");
|
||||
return;
|
||||
}
|
||||
|
||||
List<CombinePartContext> partContexts = BuildPartContextList(context);
|
||||
for (int i = 0; i < partContexts.Count; i++)
|
||||
List<Sprite> partSprites = ClonePartSprites(context.PartSprites);
|
||||
BuildParts(partSprites, runtimeSlots);
|
||||
}
|
||||
|
||||
private List<CombineSlot> BuildSlots(CombineFormContext context)
|
||||
{
|
||||
var runtimeSlots = new List<CombineSlot>();
|
||||
if (context.StructurePrefab == null)
|
||||
{
|
||||
CombinePartContext partData = partContexts[i];
|
||||
return runtimeSlots;
|
||||
}
|
||||
|
||||
GameObject structureNode = Instantiate(context.StructurePrefab, _slotRoot, false);
|
||||
CenterStructureNode(structureNode);
|
||||
_runtimeNodes.Add(structureNode);
|
||||
|
||||
CombineStageOverlay stageOverlay = structureNode.GetComponentInChildren<CombineStageOverlay>(true);
|
||||
if (stageOverlay != null)
|
||||
{
|
||||
List<CombineSlot> managedSlots = stageOverlay.GetManagedSlots();
|
||||
if (managedSlots.Count > 0)
|
||||
{
|
||||
return managedSlots;
|
||||
}
|
||||
}
|
||||
|
||||
CombineSlot[] structureSlots = structureNode.GetComponentsInChildren<CombineSlot>(true);
|
||||
for (int i = 0; i < structureSlots.Length; i++)
|
||||
{
|
||||
CombineSlot slot = structureSlots[i];
|
||||
if (slot != null)
|
||||
{
|
||||
runtimeSlots.Add(slot);
|
||||
}
|
||||
}
|
||||
|
||||
return runtimeSlots;
|
||||
}
|
||||
|
||||
private void BuildParts(List<Sprite> partSprites, List<CombineSlot> runtimeSlots)
|
||||
{
|
||||
if (_partPrefab == null)
|
||||
{
|
||||
Log.Warning("CombineMvcForm build failed. Part prefab is missing.");
|
||||
return;
|
||||
}
|
||||
|
||||
List<CombineSlot> orderedSlots = new List<CombineSlot>(runtimeSlots);
|
||||
orderedSlots.Sort((a, b) => a.BuildOrder.CompareTo(b.BuildOrder));
|
||||
|
||||
int spawnCount = Mathf.Min(partSprites.Count, orderedSlots.Count);
|
||||
if (partSprites.Count != orderedSlots.Count)
|
||||
{
|
||||
Log.Warning("CombineMvcForm build mismatch. partSprites={0}, slots={1}.", partSprites.Count.ToString(),
|
||||
orderedSlots.Count.ToString());
|
||||
}
|
||||
|
||||
for (int i = 0; i < spawnCount; i++)
|
||||
{
|
||||
CombineSlot slot = orderedSlots[i];
|
||||
if (slot == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
CombineDraggablePart part = Instantiate(_partPrefab, _partRoot, false);
|
||||
part.Initialize(partData);
|
||||
part.ConfigureRuntime(slot.RequiredPartType, partSprites[i]);
|
||||
SetPartSpawnPosition(part, i);
|
||||
_runtimeNodes.Add(part.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
private List<CombinePartContext> BuildPartContextList(CombineFormContext context)
|
||||
private static List<Sprite> ClonePartSprites(List<Sprite> source)
|
||||
{
|
||||
if (context.Parts != null && context.Parts.Count > 0)
|
||||
var result = new List<Sprite>();
|
||||
if (source == null)
|
||||
{
|
||||
return context.Parts;
|
||||
return result;
|
||||
}
|
||||
|
||||
List<CombinePartContext> fallbackParts = new List<CombinePartContext>(context.Slots.Count);
|
||||
for (int i = 0; i < context.Slots.Count; i++)
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
CombineSlotContext slot = context.Slots[i];
|
||||
fallbackParts.Add(new CombinePartContext
|
||||
{
|
||||
PartType = slot.RequiredPartType,
|
||||
PartDisplayName = slot.RequiredPartType.ToString(),
|
||||
MechanicsExplanation = slot.MechanicsExplanation,
|
||||
LockAfterPlaced = true
|
||||
});
|
||||
result.Add(source[i]);
|
||||
}
|
||||
|
||||
return fallbackParts;
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void CenterStructureNode(GameObject structureNode)
|
||||
{
|
||||
if (structureNode == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RectTransform rectTransform = structureNode.transform as RectTransform;
|
||||
if (rectTransform != null)
|
||||
{
|
||||
rectTransform.anchoredPosition = Vector2.zero;
|
||||
rectTransform.localScale = Vector3.one;
|
||||
return;
|
||||
}
|
||||
|
||||
structureNode.transform.localPosition = Vector3.zero;
|
||||
structureNode.transform.localScale = Vector3.one;
|
||||
}
|
||||
|
||||
private void SetPartSpawnPosition(CombineDraggablePart part, int index)
|
||||
@@ -249,10 +317,14 @@ namespace UI
|
||||
return;
|
||||
}
|
||||
|
||||
float minX = Mathf.Lerp(rootRect.xMin, rootRect.xMax, Mathf.Min(_partSpawnMinXNormalized, _partSpawnMaxXNormalized));
|
||||
float maxX = Mathf.Lerp(rootRect.xMin, rootRect.xMax, Mathf.Max(_partSpawnMinXNormalized, _partSpawnMaxXNormalized));
|
||||
float minY = Mathf.Lerp(rootRect.yMin, rootRect.yMax, Mathf.Min(_partSpawnMinYNormalized, _partSpawnMaxYNormalized));
|
||||
float maxY = Mathf.Lerp(rootRect.yMin, rootRect.yMax, Mathf.Max(_partSpawnMinYNormalized, _partSpawnMaxYNormalized));
|
||||
float minX = Mathf.Lerp(rootRect.xMin, rootRect.xMax,
|
||||
Mathf.Min(_partSpawnMinXNormalized, _partSpawnMaxXNormalized));
|
||||
float maxX = Mathf.Lerp(rootRect.xMin, rootRect.xMax,
|
||||
Mathf.Max(_partSpawnMinXNormalized, _partSpawnMaxXNormalized));
|
||||
float minY = Mathf.Lerp(rootRect.yMin, rootRect.yMax,
|
||||
Mathf.Min(_partSpawnMinYNormalized, _partSpawnMaxYNormalized));
|
||||
float maxY = Mathf.Lerp(rootRect.yMin, rootRect.yMax,
|
||||
Mathf.Max(_partSpawnMinYNormalized, _partSpawnMaxYNormalized));
|
||||
|
||||
partRect.anchoredPosition = new Vector2(
|
||||
Random.Range(minX, maxX),
|
||||
@@ -263,39 +335,12 @@ namespace UI
|
||||
|
||||
#region Event Handlers
|
||||
|
||||
private void OnGuideMessageChanged(object sender, GameEventArgs e)
|
||||
private void OnFeedbackChanged(object sender, GameEventArgs e)
|
||||
{
|
||||
if (!(e is CombineGuideMessageEventArgs args)) return;
|
||||
if (_guideText != null)
|
||||
if (!(e is CombineFeedbackEventArgs args)) return;
|
||||
if (_hintText != null)
|
||||
{
|
||||
_guideText.text = args.Message;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPartExplained(object sender, GameEventArgs e)
|
||||
{
|
||||
if (!(e is CombinePartMessageEventArgs args)) return;
|
||||
if (_explanationText != null)
|
||||
{
|
||||
_explanationText.text = args.Message;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnProgressChanged(object sender, GameEventArgs e)
|
||||
{
|
||||
if (!(e is CombineProgressEventArgs args)) return;
|
||||
if (_progressText != null)
|
||||
{
|
||||
_progressText.text = string.Format(_progressFormat, args.CurrentStep, args.TotalSteps);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPuzzleCompleted(object sender, GameEventArgs e)
|
||||
{
|
||||
if (!(e is CombineCompletedEventArgs args)) return;
|
||||
if (_progressText != null)
|
||||
{
|
||||
_progressText.text = string.Format(_progressFormat, _controller.TotalStep, _controller.TotalStep);
|
||||
_hintText.text = args.Message;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,16 +32,6 @@ namespace UI
|
||||
/// </summary>
|
||||
[SerializeField] private RectTransform _snapPoint;
|
||||
|
||||
/// <summary>
|
||||
/// 放置成功后的力学说明文案。
|
||||
/// </summary>
|
||||
[SerializeField] [TextArea(2, 4)] private string _mechanicsExplanation = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 部件不匹配时的自定义提示文案。
|
||||
/// </summary>
|
||||
[SerializeField] [TextArea(2, 4)] private string _mismatchHint = string.Empty;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Runtime State
|
||||
@@ -80,16 +70,6 @@ namespace UI
|
||||
/// </summary>
|
||||
public bool IsOccupied => _occupiedPart != null;
|
||||
|
||||
/// <summary>
|
||||
/// 获取槽位的力学说明文案。
|
||||
/// </summary>
|
||||
public string MechanicsExplanation => _mechanicsExplanation;
|
||||
|
||||
/// <summary>
|
||||
/// 获取槽位不匹配提示文案。
|
||||
/// </summary>
|
||||
public string MismatchHint => _mismatchHint;
|
||||
|
||||
/// <summary>
|
||||
/// 获取槽位吸附点。
|
||||
/// </summary>
|
||||
@@ -107,25 +87,6 @@ namespace UI
|
||||
_controller = controller;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用运行时数据初始化槽位配置。
|
||||
/// </summary>
|
||||
public void Initialize(CombineSlotContext data)
|
||||
{
|
||||
_requiredPartType = data.RequiredPartType;
|
||||
_buildOrder = data.BuildOrder;
|
||||
_requireStrictOrder = data.RequireStrictOrder;
|
||||
_mechanicsExplanation = data.MechanicsExplanation ?? string.Empty;
|
||||
_mismatchHint = data.MismatchHint ?? string.Empty;
|
||||
|
||||
RectTransform rectTransform = transform as RectTransform;
|
||||
if (rectTransform != null)
|
||||
{
|
||||
rectTransform.anchoredPosition = data.AnchoredPosition;
|
||||
rectTransform.sizeDelta = data.SizeDelta;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置槽位吸附点,为空时回退到槽位自身。
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
using System.Collections.Generic;
|
||||
using Event;
|
||||
using GameFramework.Event;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Stage overlay for combine silhouette prefab.
|
||||
/// Sprite index matches assemble step index (step1 -> index0).
|
||||
/// </summary>
|
||||
public class CombineStageOverlay : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private Image _overlayImage;
|
||||
|
||||
[SerializeField] private List<Sprite> _stageSprites = new List<Sprite>();
|
||||
|
||||
[SerializeField] private List<CombineSlot> _slots = new List<CombineSlot>();
|
||||
|
||||
[SerializeField] private bool _autoCollectSlotsWhenEmpty = true;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_overlayImage == null)
|
||||
{
|
||||
_overlayImage = GetComponent<Image>();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (GameEntry.Event != null)
|
||||
{
|
||||
GameEntry.Event.Subscribe(CombineProgressEventArgs.EventId, OnCombineProgressChanged);
|
||||
}
|
||||
|
||||
ApplyOverlayByStep(0);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (GameEntry.Event != null)
|
||||
{
|
||||
GameEntry.Event.Unsubscribe(CombineProgressEventArgs.EventId, OnCombineProgressChanged);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCombineProgressChanged(object sender, GameEventArgs e)
|
||||
{
|
||||
if (!(e is CombineProgressEventArgs args))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyOverlayByStep(args.CurrentStep);
|
||||
}
|
||||
|
||||
public List<CombineSlot> GetManagedSlots()
|
||||
{
|
||||
var result = new List<CombineSlot>();
|
||||
|
||||
if (_slots != null && _slots.Count > 0)
|
||||
{
|
||||
for (int i = 0; i < _slots.Count; i++)
|
||||
{
|
||||
CombineSlot slot = _slots[i];
|
||||
if (slot != null && !result.Contains(slot))
|
||||
{
|
||||
result.Add(slot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result.Count == 0 && _autoCollectSlotsWhenEmpty)
|
||||
{
|
||||
CombineSlot[] discoveredSlots = GetComponentsInChildren<CombineSlot>(true);
|
||||
for (int i = 0; i < discoveredSlots.Length; i++)
|
||||
{
|
||||
CombineSlot slot = discoveredSlots[i];
|
||||
if (slot != null)
|
||||
{
|
||||
result.Add(slot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void ApplyOverlayByStep(int currentStep)
|
||||
{
|
||||
if (_overlayImage == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentStep <= 0 || _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)
|
||||
{
|
||||
_overlayImage.SetNativeSize();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a9073bd22a6f4675b117bd711b68a031
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user