649 lines
18 KiB
C#
649 lines
18 KiB
C#
using System.Collections.Generic;
|
||
using Event;
|
||
using UI;
|
||
using UnityEngine;
|
||
using UnityEngine.Events;
|
||
using UnityGameFramework.Runtime;
|
||
|
||
namespace CustomComponent
|
||
{
|
||
/// <summary>
|
||
/// 核心玩法A控制器,负责拼装规则校验、进度统计与完成判定。
|
||
/// </summary>
|
||
[DisallowMultipleComponent]
|
||
public class CombineComponent : GameFrameworkComponent
|
||
{
|
||
private const int MaxCombineRoundCount = 2;
|
||
|
||
#region Inspector Config
|
||
|
||
/// <summary>
|
||
/// 组件启用时是否自动开始拼装。
|
||
/// </summary>
|
||
[SerializeField] private bool _autoStartOnEnable = false;
|
||
|
||
/// <summary>
|
||
/// 是否对所有槽位启用全局严格顺序。
|
||
/// </summary>
|
||
[SerializeField] private bool _strictGlobalOrder = true;
|
||
|
||
/// <summary>
|
||
/// 拖拽时的临时父节点。
|
||
/// </summary>
|
||
[SerializeField] private Transform _dragRoot = null;
|
||
|
||
/// <summary>
|
||
/// 拼装节点根对象。
|
||
/// </summary>
|
||
[SerializeField] private Transform _puzzleRoot = null;
|
||
|
||
/// <summary>
|
||
/// Combine 展示关卡序列(当前固定两次,按顺序消费)。
|
||
/// </summary>
|
||
[SerializeField] private List<GameObject> _levelStructurePrefabs = new List<GameObject>();
|
||
|
||
[SerializeField] [TextArea(1, 3)] private string _successHint = "拼接成功";
|
||
|
||
[SerializeField] [TextArea(1, 3)] private string _orderErrorHint = "顺序错误";
|
||
|
||
[SerializeField] [TextArea(1, 3)] private string _positionErrorHint = "位置错误";
|
||
|
||
/// <summary>
|
||
/// 拼装完成事件。
|
||
/// </summary>
|
||
[SerializeField] private UnityEvent _onPuzzleCompleted = new UnityEvent();
|
||
|
||
#endregion
|
||
|
||
#region Runtime State
|
||
|
||
private CombineFormContext _formContext;
|
||
private CombineFormController _formController;
|
||
|
||
/// <summary>
|
||
/// 当前拼装场景中收集到的槽位(运行态)。
|
||
/// </summary>
|
||
private readonly List<CombineSlot> _runtimeSlots = new List<CombineSlot>();
|
||
|
||
/// <summary>
|
||
/// 当前拼装场景中收集到的拖拽部件(运行态)。
|
||
/// </summary>
|
||
private readonly List<CombineDraggablePart> _runtimeParts = new List<CombineDraggablePart>();
|
||
|
||
/// <summary>
|
||
/// 按顺序排序后的槽位缓存。
|
||
/// </summary>
|
||
private readonly List<CombineSlot> _orderedSlots = new List<CombineSlot>();
|
||
|
||
/// <summary>
|
||
/// 当前期望放置的槽位索引。
|
||
/// </summary>
|
||
private int _nextOrderSlotIndex = 0;
|
||
|
||
/// <summary>
|
||
/// 当前已放置部件数量。
|
||
/// </summary>
|
||
private int _placedCount = 0;
|
||
|
||
/// <summary>
|
||
/// 拼装是否已开始。
|
||
/// </summary>
|
||
private bool _isStarted = false;
|
||
|
||
/// <summary>
|
||
/// 拼装是否已完成。
|
||
/// </summary>
|
||
private bool _isCompleted = false;
|
||
|
||
/// <summary>
|
||
/// 拼装是否处于暂停状态。
|
||
/// </summary>
|
||
private bool _isPaused = false;
|
||
|
||
/// <summary>
|
||
/// 下一次启动要使用的展示关卡索引。
|
||
/// </summary>
|
||
private int _nextLevelStructureIndex = 0;
|
||
|
||
#endregion
|
||
|
||
#region Public Query
|
||
|
||
/// <summary>
|
||
/// 获取拼装是否完成。
|
||
/// </summary>
|
||
public bool IsCompleted => _isCompleted;
|
||
|
||
/// <summary>
|
||
/// 获取当前步数。
|
||
/// </summary>
|
||
public int CurrentStep => _placedCount;
|
||
|
||
/// <summary>
|
||
/// 获取总步数。
|
||
/// </summary>
|
||
public int TotalStep => _orderedSlots.Count;
|
||
|
||
#endregion
|
||
|
||
#region Unity Lifecycle
|
||
|
||
/// <summary>
|
||
/// 组件启用时可选自动开始拼装。
|
||
/// </summary>
|
||
private void OnEnable()
|
||
{
|
||
if (_autoStartOnEnable)
|
||
{
|
||
StartPuzzle();
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Level Lifecycle
|
||
|
||
/// <summary>
|
||
/// 使用关卡数据启动关卡,并打开玩法UI。
|
||
/// </summary>
|
||
public int? StartLevel(CombineFormContext context)
|
||
{
|
||
if (!TryBuildRuntimeFormContext(context, out CombineFormContext runtimeContext))
|
||
{
|
||
return null;
|
||
}
|
||
|
||
ClearRuntimeContext();
|
||
SetFormContext(runtimeContext);
|
||
EnsureFormController();
|
||
|
||
// 先关闭上一次UI,避免重复打开。
|
||
_formController.CloseUI();
|
||
|
||
int? serialId = _formController.OpenUI(runtimeContext);
|
||
if (!serialId.HasValue)
|
||
{
|
||
Log.Warning("CoreGameplayA start failed. OpenUI returned null.");
|
||
return null;
|
||
}
|
||
|
||
_nextLevelStructureIndex++;
|
||
return serialId;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 停止当前关卡并清理运行态。
|
||
/// </summary>
|
||
public void StopLevel()
|
||
{
|
||
_formController?.CloseUI();
|
||
ClearRuntimeContext();
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Puzzle Flow
|
||
|
||
/// <summary>
|
||
/// 启动拼装流程。
|
||
/// </summary>
|
||
public void StartPuzzle()
|
||
{
|
||
CollectChildren();
|
||
|
||
BuildOrderedSlotList();
|
||
PrepareSlots();
|
||
PrepareParts();
|
||
|
||
_placedCount = 0;
|
||
_nextOrderSlotIndex = 0;
|
||
_isStarted = true;
|
||
_isCompleted = false;
|
||
_isPaused = false;
|
||
|
||
GameEntry.Event.Fire(this, CombineProgressEventArgs.Create(_placedCount, _orderedSlots.Count));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 暂停拼装输入。
|
||
/// </summary>
|
||
public void PausePuzzle()
|
||
{
|
||
_isPaused = true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 恢复拼装输入。
|
||
/// </summary>
|
||
public void ResumePuzzle()
|
||
{
|
||
_isPaused = false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 重置并重新开始拼装。
|
||
/// </summary>
|
||
public void ResetPuzzle()
|
||
{
|
||
StartPuzzle();
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Placement
|
||
|
||
/// <summary>
|
||
/// 尝试将部件放置到目标槽位。
|
||
/// </summary>
|
||
public bool TryPlacePart(CombineDraggablePart part, CombineSlot slot)
|
||
{
|
||
if (!CanPlacePart(part, slot))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (!ValidateSlotAvailability(part, slot))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (!ValidatePartType(part, slot))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (!ValidateOrder(part, slot))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
ApplyPlacement(part, slot);
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 放置前通用状态校验。
|
||
/// </summary>
|
||
private bool CanPlacePart(CombineDraggablePart part, CombineSlot slot)
|
||
{
|
||
return _isStarted && !_isCompleted && !_isPaused && part != null && slot != null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 校验槽位是否可放置。
|
||
/// </summary>
|
||
private bool ValidateSlotAvailability(CombineDraggablePart part, CombineSlot slot)
|
||
{
|
||
if (!slot.IsOccupied)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
RejectPlace(part, CombineFeedbackType.PositionError);
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 校验部件类型是否匹配槽位。
|
||
/// </summary>
|
||
private bool ValidatePartType(CombineDraggablePart part, CombineSlot slot)
|
||
{
|
||
if (slot.RequiredPartType == part.PartType)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
RejectPlace(part, CombineFeedbackType.PositionError);
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 校验放置顺序是否符合规则。
|
||
/// </summary>
|
||
private bool ValidateOrder(CombineDraggablePart part, CombineSlot slot)
|
||
{
|
||
if (!NeedValidateOrder(slot) || IsExpectedOrderSlot(slot))
|
||
{
|
||
return true;
|
||
}
|
||
|
||
RejectPlace(part, CombineFeedbackType.OrderError);
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 应用成功放置结果,并推进进度。
|
||
/// </summary>
|
||
private void ApplyPlacement(CombineDraggablePart part, CombineSlot slot)
|
||
{
|
||
slot.SetOccupiedPart(part);
|
||
part.PlaceToSlot(slot);
|
||
HidePlacedPair(slot, part);
|
||
|
||
_placedCount++;
|
||
AdvanceOrderCursor();
|
||
PublishFeedback(CombineFeedbackType.Success);
|
||
|
||
GameEntry.Event.Fire(this, CombineProgressEventArgs.Create(_placedCount, _orderedSlots.Count));
|
||
TryCompletePuzzle();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查是否完成全部步骤并触发完成事件。
|
||
/// </summary>
|
||
private void TryCompletePuzzle()
|
||
{
|
||
if (_placedCount < _orderedSlots.Count)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_isCompleted = true;
|
||
_onPuzzleCompleted.Invoke();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理放置失败:部件回弹并提示。
|
||
/// </summary>
|
||
private void RejectPlace(CombineDraggablePart part, CombineFeedbackType feedbackType)
|
||
{
|
||
part.ReturnToSpawnAnimated();
|
||
PublishFeedback(feedbackType);
|
||
}
|
||
|
||
private void PublishFeedback(CombineFeedbackType feedbackType)
|
||
{
|
||
string message = string.Empty;
|
||
switch (feedbackType)
|
||
{
|
||
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));
|
||
}
|
||
|
||
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
|
||
|
||
/// <summary>
|
||
/// 绑定运行时上下文根节点。
|
||
/// </summary>
|
||
/// <param name="puzzleRoot">拼装根节点。</param>
|
||
/// <param name="dragRoot">拖拽根节点。</param>
|
||
public void BindRuntimeContext(Transform puzzleRoot, Transform dragRoot = null)
|
||
{
|
||
_puzzleRoot = puzzleRoot;
|
||
if (dragRoot != null)
|
||
{
|
||
_dragRoot = dragRoot;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置当前UI上下文数据。
|
||
/// </summary>
|
||
public void SetFormContext(CombineFormContext context)
|
||
{
|
||
_formContext = context;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取当前UI上下文数据。
|
||
/// </summary>
|
||
public CombineFormContext GetFormContext()
|
||
{
|
||
return _formContext;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 清理运行时上下文与状态。
|
||
/// </summary>
|
||
public void ClearRuntimeContext()
|
||
{
|
||
_formContext = null;
|
||
_puzzleRoot = null;
|
||
_dragRoot = null;
|
||
_runtimeSlots.Clear();
|
||
_runtimeParts.Clear();
|
||
_orderedSlots.Clear();
|
||
_nextOrderSlotIndex = 0;
|
||
_placedCount = 0;
|
||
_isStarted = false;
|
||
_isCompleted = false;
|
||
_isPaused = false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取当前拖拽根节点。
|
||
/// </summary>
|
||
public Transform GetDragRoot()
|
||
{
|
||
return _dragRoot;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 确保表单控制器已创建。
|
||
/// </summary>
|
||
private void EnsureFormController()
|
||
{
|
||
if (_formController == null)
|
||
{
|
||
_formController = new CombineFormController(this);
|
||
}
|
||
}
|
||
|
||
private bool TryBuildRuntimeFormContext(CombineFormContext context, out CombineFormContext runtimeContext)
|
||
{
|
||
runtimeContext = context ?? new CombineFormContext();
|
||
if (!TryGetNextLevelStructurePrefab(out GameObject structurePrefab))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
runtimeContext.StructurePrefab = structurePrefab;
|
||
return true;
|
||
}
|
||
|
||
private bool TryGetNextLevelStructurePrefab(out GameObject prefab)
|
||
{
|
||
prefab = null;
|
||
|
||
if (_levelStructurePrefabs == null || _levelStructurePrefabs.Count == 0)
|
||
{
|
||
Log.Warning("CoreGameplayA start failed. level structure list is empty.");
|
||
return false;
|
||
}
|
||
|
||
int maxPlayableCount = Mathf.Min(MaxCombineRoundCount, _levelStructurePrefabs.Count);
|
||
if (_nextLevelStructureIndex < 0 || _nextLevelStructureIndex >= maxPlayableCount)
|
||
{
|
||
Log.Warning("CoreGameplayA start skipped. No remaining level structure prefab. used={0}, total={1}.",
|
||
_nextLevelStructureIndex.ToString(), maxPlayableCount.ToString());
|
||
return false;
|
||
}
|
||
|
||
prefab = _levelStructurePrefabs[_nextLevelStructureIndex];
|
||
if (prefab != null)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
Log.Warning("CoreGameplayA start failed. level structure prefab at index {0} is null.",
|
||
_nextLevelStructureIndex.ToString());
|
||
return false;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Collection And Preparation
|
||
|
||
/// <summary>
|
||
/// 收集拼装所需的槽位与部件。
|
||
/// </summary>
|
||
private void CollectChildren()
|
||
{
|
||
_runtimeSlots.Clear();
|
||
_runtimeParts.Clear();
|
||
|
||
Transform collectRoot = _puzzleRoot != null ? _puzzleRoot : transform;
|
||
CombineSlot[] foundSlots = collectRoot.GetComponentsInChildren<CombineSlot>(true);
|
||
for (int i = 0; i < foundSlots.Length; i++)
|
||
{
|
||
CombineSlot slot = foundSlots[i];
|
||
if (slot != null)
|
||
{
|
||
_runtimeSlots.Add(slot);
|
||
}
|
||
}
|
||
|
||
CollectUniqueParts(collectRoot);
|
||
if (_dragRoot != null && !ReferenceEquals(_dragRoot, collectRoot))
|
||
{
|
||
CollectUniqueParts(_dragRoot);
|
||
}
|
||
|
||
if (_runtimeSlots.Count == 0 || _runtimeParts.Count == 0)
|
||
{
|
||
Log.Warning("CoreGameplayA collect failed. slots={0}, parts={1}.", _runtimeSlots.Count.ToString(),
|
||
_runtimeParts.Count.ToString());
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从指定根节点收集不重复的可拖拽部件。
|
||
/// </summary>
|
||
private void CollectUniqueParts(Transform root)
|
||
{
|
||
if (root == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
CombineDraggablePart[] parts = root.GetComponentsInChildren<CombineDraggablePart>(true);
|
||
for (int i = 0; i < parts.Length; i++)
|
||
{
|
||
CombineDraggablePart part = parts[i];
|
||
if (part == null || _runtimeParts.Contains(part))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
_runtimeParts.Add(part);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 构建按 BuildOrder 排序后的槽位列表。
|
||
/// </summary>
|
||
private void BuildOrderedSlotList()
|
||
{
|
||
_orderedSlots.Clear();
|
||
|
||
for (int i = 0; i < _runtimeSlots.Count; i++)
|
||
{
|
||
CombineSlot slot = _runtimeSlots[i];
|
||
if (slot != null)
|
||
{
|
||
_orderedSlots.Add(slot);
|
||
}
|
||
}
|
||
|
||
_orderedSlots.Sort((a, b) => a.BuildOrder.CompareTo(b.BuildOrder));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 初始化槽位控制器绑定与占用状态。
|
||
/// </summary>
|
||
private void PrepareSlots()
|
||
{
|
||
for (int i = 0; i < _orderedSlots.Count; i++)
|
||
{
|
||
CombineSlot slot = _orderedSlots[i];
|
||
slot.gameObject.SetActive(true);
|
||
slot.BindController(this);
|
||
slot.ResetSlot();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 初始化部件控制器绑定与出生点状态。
|
||
/// </summary>
|
||
private void PrepareParts()
|
||
{
|
||
for (int i = 0; i < _runtimeParts.Count; i++)
|
||
{
|
||
CombineDraggablePart part = _runtimeParts[i];
|
||
if (part == null)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
part.gameObject.SetActive(true);
|
||
part.BindController(this);
|
||
part.CacheSpawnState();
|
||
part.ResetToSpawn();
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Rule Helpers
|
||
|
||
/// <summary>
|
||
/// 是否需要校验严格顺序。
|
||
/// </summary>
|
||
private bool NeedValidateOrder(CombineSlot slot)
|
||
{
|
||
return _strictGlobalOrder || slot.RequireStrictOrder;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 当前槽位是否是下一步期望槽位。
|
||
/// </summary>
|
||
private bool IsExpectedOrderSlot(CombineSlot slot)
|
||
{
|
||
if (_nextOrderSlotIndex < 0 || _nextOrderSlotIndex >= _orderedSlots.Count)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
return ReferenceEquals(_orderedSlots[_nextOrderSlotIndex], slot);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 推进顺序游标到下一个未占用槽位。
|
||
/// </summary>
|
||
private void AdvanceOrderCursor()
|
||
{
|
||
while (_nextOrderSlotIndex < _orderedSlots.Count && _orderedSlots[_nextOrderSlotIndex].IsOccupied)
|
||
{
|
||
_nextOrderSlotIndex++;
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
}
|
||
}
|