gameplay refine

1. 优化 ResolutionAdapter 的分辨率适配问题
2. 调整 SpriteCache 的资源路径拼接
3. 引入 StoryDirector 做各玩法的胶水
4. 导入和改名资源便于代码查询路径
This commit is contained in:
2026-04-20 15:26:36 +08:00
parent cc5d8edec4
commit 0f928f6167
56 changed files with 1218 additions and 621 deletions
@@ -377,6 +377,7 @@ namespace CustomComponent
GameEntry.Event.Fire(this, CombineGuideMessageEventArgs.Create(_completeHint));
}
GameEntry.Event.Fire(this, CombineCompletedEventArgs.Create());
_onPuzzleCompleted.Invoke();
}
@@ -338,11 +338,20 @@ namespace CustomComponent
private void EndDialogInternal()
{
int chapterId = _formContext != null ? _formContext.ChapterId : _currentChapterId;
int dialogId = _formContext != null ? _formContext.DialogId : 0;
int lineId = _formContext != null ? _formContext.CurrentLineId : 0;
_isPlaying = false;
_currentLineIndex = -1;
_formController.OnDialogEnded(_formContext);
_formController.CloseUI();
if (dialogId > 0)
{
GameEntry.Event.Fire(this, DialogCompletedEventArgs.Create(chapterId, dialogId, lineId));
}
}
private void ApplyLineToContext(DRDialogLine lineRow, int lineIndex, int totalLines)
@@ -16,6 +16,7 @@ namespace GeometryTD.CustomComponent
[SerializeField] private Vector2 _referenceResolution = new Vector2(2560f, 1600f);
[SerializeField] private bool _adaptUiCanvasToViewport = true;
[SerializeField] private bool _enableBlackMask = true;
[SerializeField] private bool _autoCollectSceneCanvases = true;
[SerializeField] private List<Transform> _uiRoots = new List<Transform>();
private readonly List<Canvas> _canvasBuffer = new List<Canvas>(32);
@@ -29,6 +30,7 @@ namespace GeometryTD.CustomComponent
private bool _canvasCacheDirty = true;
private bool _uiEventSubscribed;
private bool _sceneEventSubscribed;
private bool _missingRootWarned;
private Canvas _maskCanvas;
@@ -163,18 +165,6 @@ namespace GeometryTD.CustomComponent
return;
}
if (_uiRoots == null || _uiRoots.Count == 0)
{
if (!_missingRootWarned)
{
_missingRootWarned = true;
Log.Warning(
"ResolutionAdapterComponent missing injected roots. Assign UI/HPBar root transforms in scene.");
}
return;
}
if (_canvasCacheDirty)
{
RebuildCanvasCache();
@@ -200,9 +190,49 @@ namespace GeometryTD.CustomComponent
_trackedCanvases.Clear();
_trackedCanvasSet.Clear();
foreach (var root in _uiRoots)
bool hasValidRoot = false;
if (_uiRoots != null)
{
CollectCanvases(root);
foreach (var root in _uiRoots)
{
if (root == null)
{
continue;
}
hasValidRoot = true;
CollectCanvases(root);
}
}
if (_autoCollectSceneCanvases || !hasValidRoot)
{
Canvas[] canvases = Resources.FindObjectsOfTypeAll<Canvas>();
foreach (Canvas canvas in canvases)
{
if (canvas == null || canvas == _maskCanvas || canvas.renderMode == RenderMode.WorldSpace)
{
continue;
}
GameObject gameObject = canvas.gameObject;
if (!gameObject.scene.IsValid() || !gameObject.scene.isLoaded)
{
continue;
}
if (_trackedCanvasSet.Add(canvas))
{
_trackedCanvases.Add(canvas);
}
}
}
if (!hasValidRoot && !_autoCollectSceneCanvases && !_missingRootWarned)
{
_missingRootWarned = true;
Log.Warning(
"ResolutionAdapterComponent missing injected roots. Assign UI/HPBar root transforms in scene or enable auto collection.");
}
}
@@ -270,27 +300,46 @@ namespace GeometryTD.CustomComponent
private void TryEnsureUiEventSubscribed()
{
if (_uiEventSubscribed || GameEntry.Event == null)
if (GameEntry.Event == null)
{
return;
}
GameEntry.Event.Subscribe(OpenUIFormSuccessEventArgs.EventId, OnUIFormChanged);
GameEntry.Event.Subscribe(CloseUIFormCompleteEventArgs.EventId, OnUIFormChanged);
_uiEventSubscribed = true;
_canvasCacheDirty = true;
if (!_uiEventSubscribed)
{
GameEntry.Event.Subscribe(OpenUIFormSuccessEventArgs.EventId, OnUIFormChanged);
GameEntry.Event.Subscribe(CloseUIFormCompleteEventArgs.EventId, OnUIFormChanged);
_uiEventSubscribed = true;
_canvasCacheDirty = true;
}
if (!_sceneEventSubscribed)
{
GameEntry.Event.Subscribe(LoadSceneSuccessEventArgs.EventId, OnSceneChanged);
_sceneEventSubscribed = true;
_canvasCacheDirty = true;
}
}
private void UnsubscribeUiEvents()
{
if (!_uiEventSubscribed || GameEntry.Event == null)
if (GameEntry.Event == null)
{
return;
}
GameEntry.Event.Unsubscribe(OpenUIFormSuccessEventArgs.EventId, OnUIFormChanged);
GameEntry.Event.Unsubscribe(CloseUIFormCompleteEventArgs.EventId, OnUIFormChanged);
_uiEventSubscribed = false;
if (_uiEventSubscribed)
{
GameEntry.Event.Unsubscribe(OpenUIFormSuccessEventArgs.EventId, OnUIFormChanged);
GameEntry.Event.Unsubscribe(CloseUIFormCompleteEventArgs.EventId, OnUIFormChanged);
_uiEventSubscribed = false;
}
if (_sceneEventSubscribed)
{
GameEntry.Event.Unsubscribe(LoadSceneSuccessEventArgs.EventId, OnSceneChanged);
_sceneEventSubscribed = false;
}
}
private void OnUIFormChanged(object sender, GameEventArgs e)
@@ -298,6 +347,12 @@ namespace GeometryTD.CustomComponent
_canvasCacheDirty = true;
}
private void OnSceneChanged(object sender, GameEventArgs e)
{
_canvasCacheDirty = true;
_missingRootWarned = false;
}
private void UpdateMaskLayout()
{
if (!_enableBlackMask)
@@ -442,4 +497,4 @@ namespace GeometryTD.CustomComponent
maskRect.sizeDelta = new Vector2(0f, height);
}
}
}
}
@@ -33,7 +33,7 @@ namespace CustomComponent
{
_resource.LoadAsset
(
AssetUtility.GetUIDialogAsset(assetName),
AssetUtility.GetUISpriteAsset(assetName),
Constant.AssetPriority.UIFormAsset,
new LoadAssetCallbacks(
(resourcePath, asset, duration, userData) =>
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 99c1d60910f24e37ba0cbd8109e63bd7
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,22 @@
using UnityEngine;
namespace CustomComponent
{
[CreateAssetMenu(menuName = "Story/Directive/Change Background", fileName = "Directive_ChangeBackground")]
public sealed class StoryChangeBackgroundDirectiveAsset : StoryDirectiveAsset
{
[SerializeField] private string _backgroundAssetName = string.Empty;
public override string ActionName => "ChangeBackground";
public override void Execute(StoryDirectorComponent director)
{
if (director == null)
{
return;
}
director.ExecuteChangeBackground(_backgroundAssetName);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2d4b16a5d76140a1b952ad48774dd89a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using UI;
namespace CustomComponent
{
[Serializable]
public sealed class StoryCombineConfig
{
public bool AutoStart = true;
public List<CombineSlotContext> Slots = new List<CombineSlotContext>();
public List<CombinePartContext> Parts = new List<CombinePartContext>();
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4c53bd314c194677b10a2e7fa754fdbb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,39 @@
using UnityEngine;
namespace CustomComponent
{
public abstract class StoryDirectiveAsset : ScriptableObject
{
[SerializeField] private bool _enabled = true;
[SerializeField] private StoryTriggerType _triggerType = StoryTriggerType.DialogCompleted;
[SerializeField] [Tooltip("DialogCompleted 时为对话 Id;<=0 表示该触发类型下的任意 Id。")]
private int _triggerId = 0;
public bool IsEnabled => _enabled;
public StoryTriggerType TriggerType => _triggerType;
public int TriggerId => _triggerId;
public virtual string ActionName => GetType().Name;
public bool IsMatch(StoryTriggerType triggerType, int triggerId)
{
if (!_enabled || _triggerType != triggerType)
{
return false;
}
if (triggerType == StoryTriggerType.DialogCompleted && _triggerId > 0 && _triggerId != triggerId)
{
return false;
}
return true;
}
public abstract void Execute(StoryDirectorComponent director);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2deed2f965a04cbab1c18d2e9b5d5845
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,22 @@
using UnityEngine;
namespace CustomComponent
{
[CreateAssetMenu(menuName = "Story/Directive/End Chapter", fileName = "Directive_EndChapter")]
public sealed class StoryEndChapterDirectiveAsset : StoryDirectiveAsset
{
[SerializeField] [Tooltip("<=0 则使用当前章节。")] private int _chapterId = 0;
public override string ActionName => "EndChapter";
public override void Execute(StoryDirectorComponent director)
{
if (director == null)
{
return;
}
director.ExecuteEndChapter(_chapterId);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2b63fb6eb96e4d3193dd26b443f856c7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,25 @@
using UnityEngine;
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";
public override void Execute(StoryDirectorComponent director)
{
if (director == null)
{
return;
}
StoryCombineConfig config = _useDefaultConfig ? null : _combineConfig;
director.ExecuteStartCombine(config);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 19a1150cd0fb4b648ad8232de4e38baa
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,22 @@
using UnityEngine;
namespace CustomComponent
{
[CreateAssetMenu(menuName = "Story/Directive/Start Dialog", fileName = "Directive_StartDialog")]
public sealed class StoryStartDialogDirectiveAsset : StoryDirectiveAsset
{
[SerializeField] private int _dialogId = 0;
public override string ActionName => "StartDialog";
public override void Execute(StoryDirectorComponent director)
{
if (director == null)
{
return;
}
director.ExecuteStartDialog(_dialogId);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8e4367863f5042e1b5b0b3b3b7f62f5e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
namespace CustomComponent
{
public enum StoryTriggerType
{
DialogCompleted = 0,
CombineCompleted = 1
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2b9d79013962410681f2f29343902599
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,358 @@
using System.Collections.Generic;
using Definition.Enum;
using Event;
using UI;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace CustomComponent
{
[DisallowMultipleComponent]
public class StoryDirectorComponent : GameFrameworkComponent
{
[SerializeField] [Tooltip("按顺序执行。每个元素是一个 StoryDirectiveAsset 资产引用。")]
private List<StoryDirectiveAsset> _directives = new List<StoryDirectiveAsset>();
[SerializeField] private bool _allowRepeatTrigger = false;
[SerializeField] private bool _verboseLog = true;
[SerializeField] private string _backgroundAssetNamePrefix = string.Empty;
private readonly HashSet<string> _consumedDirectiveTokens = new HashSet<string>();
private BgFormController _bgFormController;
private int _bgRequestVersion;
private bool _isExecuting;
private void Start()
{
GameEntry.Event.Subscribe(DialogCompletedEventArgs.EventId, OnDialogCompleted);
GameEntry.Event.Subscribe(CombineCompletedEventArgs.EventId, OnCombineCompleted);
}
private void OnDestroy()
{
GameEntry.Event.Unsubscribe(DialogCompletedEventArgs.EventId, OnDialogCompleted);
GameEntry.Event.Unsubscribe(CombineCompletedEventArgs.EventId, OnCombineCompleted);
_bgFormController?.CloseUI();
_bgFormController = null;
}
public void ResetConsumedDirectives()
{
_consumedDirectiveTokens.Clear();
}
private void OnDialogCompleted(object sender, GameFramework.Event.GameEventArgs e)
{
if (!(e is DialogCompletedEventArgs args))
{
return;
}
ExecuteDirectives(StoryTriggerType.DialogCompleted, args.DialogId);
}
private void OnCombineCompleted(object sender, GameFramework.Event.GameEventArgs e)
{
if (!(e is CombineCompletedEventArgs))
{
return;
}
ExecuteDirectives(StoryTriggerType.CombineCompleted, 0);
}
private void ExecuteDirectives(StoryTriggerType triggerType, int triggerId)
{
if (_isExecuting)
{
return;
}
_isExecuting = true;
try
{
for (int i = 0; i < _directives.Count; i++)
{
StoryDirectiveAsset directive = _directives[i];
if (directive == null || !directive.IsMatch(triggerType, triggerId))
{
continue;
}
string directiveToken = BuildDirectiveToken(directive, i);
if (!_allowRepeatTrigger && _consumedDirectiveTokens.Contains(directiveToken))
{
continue;
}
ExecuteDirective(directive, triggerType, triggerId);
if (!_allowRepeatTrigger)
{
_consumedDirectiveTokens.Add(directiveToken);
}
}
}
finally
{
_isExecuting = false;
}
}
private static string BuildDirectiveToken(StoryDirectiveAsset directive, int directiveIndex)
{
return $"{directive.GetInstanceID()}:{directiveIndex}";
}
private void ExecuteDirective(StoryDirectiveAsset directive, StoryTriggerType triggerType, int triggerId)
{
directive.Execute(this);
if (_verboseLog)
{
Log.Info("StoryDirector executed action '{0}' by trigger '{1}' ({2}).",
directive.ActionName, triggerType.ToString(), triggerId.ToString());
}
}
public void ExecuteStartDialog(int dialogId)
{
if (dialogId <= 0)
{
Log.Warning("StoryDirector start dialog skipped. dialogId must be positive.");
return;
}
if (GameEntry.Dialog == null)
{
Log.Warning("StoryDirector start dialog failed. Dialog component is missing.");
return;
}
GameEntry.Dialog.StartDialog(dialogId);
}
public void ExecuteStartCombine(StoryCombineConfig config)
{
if (GameEntry.Combine == null)
{
Log.Warning("StoryDirector start combine failed. Combine component is missing.");
return;
}
CombineFormContext context = BuildCombineFormContext(config);
int? formSerialId = GameEntry.Combine.StartLevel(context);
if (!formSerialId.HasValue)
{
Log.Warning("StoryDirector start combine failed. Open combine UI failed.");
return;
}
}
public void ExecuteChangeBackground(string backgroundAssetName)
{
string assetName = string.IsNullOrWhiteSpace(backgroundAssetName)
? string.Empty
: backgroundAssetName.Trim();
if (string.IsNullOrEmpty(assetName))
{
Log.Warning("StoryDirector change background skipped. assetName is empty.");
return;
}
if (GameEntry.SpriteCache == null)
{
Log.Warning("StoryDirector change background failed. SpriteCache component is missing.");
return;
}
int requestVersion = ++_bgRequestVersion;
GameEntry.SpriteCache.GetSprite(_backgroundAssetNamePrefix + assetName,
sprite => { OnBackgroundLoaded(requestVersion, assetName, sprite); });
}
private void OnBackgroundLoaded(int requestVersion, string assetName, Sprite sprite)
{
if (requestVersion != _bgRequestVersion)
{
return;
}
if (sprite == null)
{
Log.Warning("StoryDirector change background failed. Loaded sprite is null for '{0}'.", assetName);
return;
}
if (_bgFormController == null)
{
_bgFormController = new BgFormController();
}
_bgFormController.OpenUI(new BgFormContext
{
Sprite = sprite
});
}
public void ExecuteEndChapter(int chapterId)
{
int finalChapterId = chapterId > 0
? chapterId
: (GameEntry.Dialog != null ? GameEntry.Dialog.CurrentChapterId : 0);
GameEntry.Event.Fire(this, StoryChapterEndedEventArgs.Create(finalChapterId));
}
private static CombineFormContext BuildCombineFormContext(StoryCombineConfig config)
{
if (config != null && config.Slots != null && config.Slots.Count > 0)
{
return new CombineFormContext
{
Slots = CloneSlots(config.Slots),
Parts = CloneParts(config.Parts),
AutoStart = config.AutoStart
};
}
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
};
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d899fa8b1598c1846bc3a03d11c83d04
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: