迭代拼装玩法与素材

This commit is contained in:
2026-04-22 10:54:24 +08:00
parent a1b6b4a823
commit 204eb6e4d0
35 changed files with 3471 additions and 848 deletions
@@ -1,6 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using Definition.Enum;
using Event;
using UI;
using UnityEngine;
@@ -42,40 +41,11 @@ namespace CustomComponent
/// </summary>
[SerializeField] private Transform _puzzleRoot = null;
/// <summary>
/// 拼装开始时提示文本。
/// </summary>
[SerializeField]
[TextArea(2, 4)]
private string _startHint = "Assemble dougong from lower-to-upper and inner-to-outer.";
[SerializeField] [TextArea(1, 3)] private string _successHint = "拼接成功";
/// <summary>
/// 放置顺序错误提示文本。
/// </summary>
[SerializeField]
[TextArea(2, 4)]
private string _wrongOrderHint = "Wrong order. Place lower or inner parts first.";
[SerializeField] [TextArea(1, 3)] private string _orderErrorHint = "顺序错误";
/// <summary>
/// 部件不匹配提示文本。
/// </summary>
[SerializeField]
[TextArea(2, 4)]
private string _wrongPartHint = "Part does not match this slot.";
/// <summary>
/// 槽位已占用提示文本。
/// </summary>
[SerializeField]
[TextArea(2, 4)]
private string _slotOccupiedHint = "This slot is already occupied.";
/// <summary>
/// 拼装完成提示文本。
/// </summary>
[SerializeField]
[TextArea(2, 4)]
private string _completeHint = "Dougong assembly completed.";
[SerializeField] [TextArea(1, 3)] private string _positionErrorHint = "位置错误";
/// <summary>
/// 当前参与拼装的槽位列表。
@@ -229,11 +199,6 @@ namespace CustomComponent
_isPaused = false;
GameEntry.Event.Fire(this, CombineProgressEventArgs.Create(_placedCount, _orderedSlots.Count));
if (!string.IsNullOrEmpty(_startHint))
{
GameEntry.Event.Fire(this, CombineGuideMessageEventArgs.Create(_startHint));
}
}
/// <summary>
@@ -311,7 +276,7 @@ namespace CustomComponent
return true;
}
RejectPlace(part, _slotOccupiedHint);
RejectPlace(part, CombineFeedbackType.PositionError);
return false;
}
@@ -325,8 +290,7 @@ namespace CustomComponent
return true;
}
string hint = string.IsNullOrEmpty(slot.MismatchHint) ? _wrongPartHint : slot.MismatchHint;
RejectPlace(part, hint);
RejectPlace(part, CombineFeedbackType.PositionError);
return false;
}
@@ -340,7 +304,7 @@ namespace CustomComponent
return true;
}
RejectPlace(part, _wrongOrderHint);
RejectPlace(part, CombineFeedbackType.OrderError);
return false;
}
@@ -354,7 +318,7 @@ namespace CustomComponent
_placedCount++;
AdvanceOrderCursor();
PublishExplanation(part, slot);
PublishFeedback(CombineFeedbackType.Success);
GameEntry.Event.Fire(this, CombineProgressEventArgs.Create(_placedCount, _orderedSlots.Count));
TryCompletePuzzle();
@@ -372,11 +336,6 @@ namespace CustomComponent
_isCompleted = true;
if (!string.IsNullOrEmpty(_completeHint))
{
GameEntry.Event.Fire(this, CombineGuideMessageEventArgs.Create(_completeHint));
}
GameEntry.Event.Fire(this, CombineCompletedEventArgs.Create());
_onPuzzleCompleted.Invoke();
}
@@ -384,14 +343,29 @@ namespace CustomComponent
/// <summary>
/// 处理放置失败:部件回弹并提示。
/// </summary>
private void RejectPlace(CombineDraggablePart part, string hint)
private void RejectPlace(CombineDraggablePart part, CombineFeedbackType feedbackType)
{
part.ReturnToSpawnAnimated();
PublishFeedback(feedbackType);
}
if (!string.IsNullOrEmpty(hint))
private void PublishFeedback(CombineFeedbackType feedbackType)
{
string message = string.Empty;
switch (feedbackType)
{
GameEntry.Event.Fire(this, CombineGuideMessageEventArgs.Create(hint));
case CombineFeedbackType.Success:
message = _successHint;
break;
case CombineFeedbackType.OrderError:
message = _orderErrorHint;
break;
case CombineFeedbackType.PositionError:
message = _positionErrorHint;
break;
}
GameEntry.Event.Fire(this, CombineFeedbackEventArgs.Create(feedbackType, message));
}
#endregion
@@ -603,50 +577,6 @@ namespace CustomComponent
}
}
/// <summary>
/// 发布当前放置对应的力学说明文案。
/// </summary>
private void PublishExplanation(CombineDraggablePart part, CombineSlot slot)
{
string explanation = slot.MechanicsExplanation;
if (string.IsNullOrEmpty(explanation))
{
explanation = part.MechanicsExplanation;
}
if (string.IsNullOrEmpty(explanation))
{
explanation = GetDefaultExplanation(part.PartType);
}
if (!string.IsNullOrEmpty(explanation))
{
GameEntry.Event.Fire(this, CombinePartMessageEventArgs.Create(explanation));
}
}
/// <summary>
/// 兜底力学说明文案。
/// </summary>
private static string GetDefaultExplanation(CombinePartType partType)
{
switch (partType)
{
case CombinePartType.Dou:
return "Dou transfers upper load and works as the base node.";
case CombinePartType.Sheng:
return "Sheng raises layer height to form the bracket hierarchy.";
case CombinePartType.Gong:
return "Gong spreads force laterally through overhang.";
case CombinePartType.Qiao:
return "Qiao continues force transfer to the outer side.";
case CombinePartType.Ang:
return "Ang uses leverage to redirect eave load inward.";
default:
return string.Empty;
}
}
#endregion
}
}
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using UI;
using UnityEngine;
using UnityEngine.Serialization;
namespace CustomComponent
{
@@ -8,7 +9,7 @@ namespace CustomComponent
public sealed class StoryCombineConfig
{
public bool AutoStart = true;
public List<CombineSlotContext> Slots = new List<CombineSlotContext>();
public List<CombinePartContext> Parts = new List<CombinePartContext>();
public GameObject StructurePrefab;
[FormerlySerializedAs("PartPrefabs")] public List<Sprite> PartSprites = new List<Sprite>();
}
}
@@ -5,8 +5,6 @@ namespace CustomComponent
[CreateAssetMenu(menuName = "Story/Directive/Start Combine", fileName = "Directive_StartCombine")]
public sealed class StoryStartCombineDirectiveAsset : StoryDirectiveAsset
{
[SerializeField] private bool _useDefaultConfig = true;
[SerializeField] private StoryCombineConfig _combineConfig = new StoryCombineConfig();
public override string ActionName => "StartCombine";
@@ -18,8 +16,7 @@ namespace CustomComponent
return;
}
StoryCombineConfig config = _useDefaultConfig ? null : _combineConfig;
director.ExecuteStartCombine(config);
director.ExecuteStartCombine(_combineConfig);
}
}
}
@@ -1,6 +1,5 @@
using System.Collections;
using System.Collections.Generic;
using Definition.Enum;
using Event;
using UI;
using UnityEngine;
@@ -293,149 +292,33 @@ namespace CustomComponent
private static CombineFormContext BuildCombineFormContext(StoryCombineConfig config)
{
if (config != null && config.Slots != null && config.Slots.Count > 0)
if (config == null)
{
return new CombineFormContext
{
Slots = CloneSlots(config.Slots),
Parts = CloneParts(config.Parts),
AutoStart = config.AutoStart
};
return null;
}
return BuildDefaultCombineContext();
}
private static List<CombineSlotContext> CloneSlots(List<CombineSlotContext> source)
{
var result = new List<CombineSlotContext>();
if (source == null)
{
return result;
}
for (int i = 0; i < source.Count; i++)
{
CombineSlotContext slot = source[i];
if (slot == null)
{
continue;
}
result.Add(new CombineSlotContext
{
RequiredPartType = slot.RequiredPartType,
BuildOrder = slot.BuildOrder,
RequireStrictOrder = slot.RequireStrictOrder,
AnchoredPosition = slot.AnchoredPosition,
SizeDelta = slot.SizeDelta,
MechanicsExplanation = slot.MechanicsExplanation,
MismatchHint = slot.MismatchHint
});
}
return result;
}
private static List<CombinePartContext> CloneParts(List<CombinePartContext> source)
{
var result = new List<CombinePartContext>();
if (source == null)
{
return result;
}
for (int i = 0; i < source.Count; i++)
{
CombinePartContext part = source[i];
if (part == null)
{
continue;
}
result.Add(new CombinePartContext
{
PartType = part.PartType,
PartDisplayName = part.PartDisplayName,
MechanicsExplanation = part.MechanicsExplanation,
LockAfterPlaced = part.LockAfterPlaced
});
}
return result;
}
private static CombineFormContext BuildDefaultCombineContext()
{
List<CombineSlotContext> slots = new List<CombineSlotContext>
{
new CombineSlotContext
{
RequiredPartType = CombinePartType.Dou,
BuildOrder = 0,
RequireStrictOrder = true,
AnchoredPosition = new Vector2(-320f, -160f),
SizeDelta = new Vector2(120f, 120f),
MechanicsExplanation = "Dou transfers upper load and works as the base node."
},
new CombineSlotContext
{
RequiredPartType = CombinePartType.Sheng,
BuildOrder = 1,
RequireStrictOrder = true,
AnchoredPosition = new Vector2(-320f, -20f),
SizeDelta = new Vector2(120f, 120f),
MechanicsExplanation = "Sheng raises layer height to form the bracket hierarchy."
},
new CombineSlotContext
{
RequiredPartType = CombinePartType.Gong,
BuildOrder = 2,
RequireStrictOrder = true,
AnchoredPosition = new Vector2(-320f, 120f),
SizeDelta = new Vector2(120f, 120f),
MechanicsExplanation = "Gong spreads force laterally through overhang."
},
new CombineSlotContext
{
RequiredPartType = CombinePartType.Qiao,
BuildOrder = 3,
RequireStrictOrder = true,
AnchoredPosition = new Vector2(-160f, 120f),
SizeDelta = new Vector2(120f, 120f),
MechanicsExplanation = "Qiao continues force transfer to the outer side."
},
new CombineSlotContext
{
RequiredPartType = CombinePartType.Ang,
BuildOrder = 4,
RequireStrictOrder = true,
AnchoredPosition = new Vector2(0f, 120f),
SizeDelta = new Vector2(120f, 120f),
MechanicsExplanation = "Ang uses leverage to redirect eave load inward."
}
};
List<CombinePartContext> parts = new List<CombinePartContext>
{
new CombinePartContext
{ PartType = CombinePartType.Dou, PartDisplayName = "Dou", LockAfterPlaced = true },
new CombinePartContext
{ PartType = CombinePartType.Sheng, PartDisplayName = "Sheng", LockAfterPlaced = true },
new CombinePartContext
{ PartType = CombinePartType.Gong, PartDisplayName = "Gong", LockAfterPlaced = true },
new CombinePartContext
{ PartType = CombinePartType.Qiao, PartDisplayName = "Qiao", LockAfterPlaced = true },
new CombinePartContext
{ PartType = CombinePartType.Ang, PartDisplayName = "Ang", LockAfterPlaced = true }
};
return new CombineFormContext
{
Slots = slots,
Parts = parts,
AutoStart = true
StructurePrefab = config.StructurePrefab,
PartSprites = ClonePartSprites(config.PartSprites),
AutoStart = config.AutoStart
};
}
private static List<Sprite> ClonePartSprites(List<Sprite> source)
{
var result = new List<Sprite>();
if (source == null)
{
return result;
}
for (int i = 0; i < source.Count; i++)
{
result.Add(source[i]);
}
return result;
}
}
}
@@ -0,0 +1,45 @@
using GameFramework;
using GameFramework.Event;
namespace Event
{
public enum CombineFeedbackType : byte
{
None = 0,
Success = 1,
OrderError = 2,
PositionError = 3
}
public class CombineFeedbackEventArgs : GameEventArgs
{
public static readonly int EventId = typeof(CombineFeedbackEventArgs).GetHashCode();
public override int Id => EventId;
public CombineFeedbackType FeedbackType { get; private set; }
public string Message { get; private set; }
public CombineFeedbackEventArgs()
{
FeedbackType = CombineFeedbackType.None;
Message = string.Empty;
}
public static CombineFeedbackEventArgs Create(CombineFeedbackType feedbackType, string message)
{
var args = ReferencePool.Acquire<CombineFeedbackEventArgs>();
args.FeedbackType = feedbackType;
args.Message = message ?? string.Empty;
return args;
}
public override void Clear()
{
FeedbackType = CombineFeedbackType.None;
Message = string.Empty;
}
}
}
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 82f303529f5b4b040845223f97a66cfa
guid: 760eb8da887f4ab9918f01a43ca40b59
MonoImporter:
externalObjects: {}
serializedVersion: 2
@@ -1,32 +0,0 @@
using GameFramework;
using GameFramework.Event;
namespace Event
{
public class CombineGuideMessageEventArgs : GameEventArgs
{
public static readonly int EventId = typeof(CombineGuideMessageEventArgs).GetHashCode();
public override int Id => EventId;
public string Message { get; private set; }
public CombineGuideMessageEventArgs()
{
Message = "";
}
public static CombineGuideMessageEventArgs Create(string message)
{
var args = ReferencePool.Acquire<CombineGuideMessageEventArgs>();
args.Message = message;
return args;
}
public override void Clear()
{
Message = "";
}
}
}
@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 7d6ccdc983514de981ac731659f6fa0e
timeCreated: 1770510601
@@ -1,32 +0,0 @@
using GameFramework;
using GameFramework.Event;
namespace Event
{
public class CombinePartMessageEventArgs : GameEventArgs
{
public static readonly int EventId = typeof(CombinePartMessageEventArgs).GetHashCode();
public override int Id => EventId;
public string Message { get; private set; }
public CombinePartMessageEventArgs()
{
Message = "";
}
public static CombinePartMessageEventArgs Create(string message)
{
var args = ReferencePool.Acquire<CombinePartMessageEventArgs>();
args.Message = message;
return args;
}
public override void Clear()
{
Message = "";
}
}
}
@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: fd83ff85d4a8443db7a9cba10c02529f
timeCreated: 1770510848
@@ -1,8 +1,5 @@
using Definition.Enum;
using System.Collections.Generic;
using CustomComponent;
using UI;
using UnityEngine;
using ProcedureOwner = GameFramework.Fsm.IFsm<GameFramework.Procedure.IProcedureManager>;
namespace Procedure
@@ -75,84 +72,5 @@ namespace Procedure
#endregion
#region Other Methods
/// <summary>
/// 构建测试用关卡上下文(槽位、部件与自动开始配置)。
/// </summary>
private static CombineFormContext BuildTestOpenData()
{
List<CombineSlotContext> slots = new List<CombineSlotContext>
{
new CombineSlotContext
{
RequiredPartType = CombinePartType.Dou,
BuildOrder = 0,
RequireStrictOrder = true,
AnchoredPosition = new Vector2(-320f, -160f),
SizeDelta = new Vector2(120f, 120f),
MechanicsExplanation = "Dou transfers upper load and works as the base node."
},
new CombineSlotContext
{
RequiredPartType = CombinePartType.Sheng,
BuildOrder = 1,
RequireStrictOrder = true,
AnchoredPosition = new Vector2(-320f, -20f),
SizeDelta = new Vector2(120f, 120f),
MechanicsExplanation = "Sheng raises layer height to form the bracket hierarchy."
},
new CombineSlotContext
{
RequiredPartType = CombinePartType.Gong,
BuildOrder = 2,
RequireStrictOrder = true,
AnchoredPosition = new Vector2(-320f, 120f),
SizeDelta = new Vector2(120f, 120f),
MechanicsExplanation = "Gong spreads force laterally through overhang."
},
new CombineSlotContext
{
RequiredPartType = CombinePartType.Qiao,
BuildOrder = 3,
RequireStrictOrder = true,
AnchoredPosition = new Vector2(-160f, 120f),
SizeDelta = new Vector2(120f, 120f),
MechanicsExplanation = "Qiao continues force transfer to the outer side."
},
new CombineSlotContext
{
RequiredPartType = CombinePartType.Ang,
BuildOrder = 4,
RequireStrictOrder = true,
AnchoredPosition = new Vector2(0f, 120f),
SizeDelta = new Vector2(120f, 120f),
MechanicsExplanation = "Ang uses leverage to redirect eave load inward."
}
};
List<CombinePartContext> parts = new List<CombinePartContext>
{
new CombinePartContext
{ PartType = CombinePartType.Dou, PartDisplayName = "Dou", LockAfterPlaced = true },
new CombinePartContext
{ PartType = CombinePartType.Sheng, PartDisplayName = "Sheng", LockAfterPlaced = true },
new CombinePartContext
{ PartType = CombinePartType.Gong, PartDisplayName = "Gong", LockAfterPlaced = true },
new CombinePartContext
{ PartType = CombinePartType.Qiao, PartDisplayName = "Qiao", LockAfterPlaced = true },
new CombinePartContext
{ PartType = CombinePartType.Ang, PartDisplayName = "Ang", LockAfterPlaced = true }
};
return new CombineFormContext
{
Slots = slots,
Parts = parts,
AutoStart = true
};
}
#endregion
}
}
@@ -1,4 +1,3 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
@@ -10,18 +9,18 @@ namespace UI
public sealed class CombineFormContext : UIContext
{
/// <summary>
/// Slot definitions. Position and order are provided externally.
/// Assembled structure/silhouette prefab. Slots are read directly from this prefab.
/// </summary>
public List<CombineSlotContext> Slots;
public GameObject StructurePrefab;
/// <summary>
/// Optional part definitions. When null/empty, parts are derived from slots.
/// Part sprites spawned on the right panel.
/// </summary>
public List<CombinePartContext> Parts;
public List<Sprite> PartSprites = new List<Sprite>();
/// <summary>
/// Auto start puzzle after runtime nodes are generated.
/// </summary>
public bool AutoStart = true;
}
}
}
@@ -1,21 +0,0 @@
using System;
using Definition.Enum;
using UnityEngine;
namespace UI
{
/// <summary>
/// Runtime part data passed from external system.
/// </summary>
[Serializable]
public sealed class CombinePartContext
{
public CombinePartType PartType = CombinePartType.Dou;
public string PartDisplayName = "Dou";
public string MechanicsExplanation = string.Empty;
public bool LockAfterPlaced = true;
}
}
@@ -1,28 +0,0 @@
using System;
using Definition.Enum;
using UnityEngine;
namespace UI
{
/// <summary>
/// Runtime slot data passed from external system.
/// </summary>
[Serializable]
public sealed class CombineSlotContext
{
public CombinePartType RequiredPartType = CombinePartType.Dou;
public int BuildOrder = 0;
public bool RequireStrictOrder = true;
public Vector2 AnchoredPosition = Vector2.zero;
public Vector2 SizeDelta = new Vector2(120f, 120f);
public string MechanicsExplanation = string.Empty;
public string MismatchHint = string.Empty;
}
}
@@ -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>
+134 -89
View File
@@ -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();
}
}
}
}
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 03894567063ea7d42a417545043e32c6
guid: a9073bd22a6f4675b117bd711b68a031
MonoImporter:
externalObjects: {}
serializedVersion: 2