修复已知的 bug,规划后期的开发方向

This commit is contained in:
SepComet 2026-02-17 22:46:28 +08:00
parent 85b0205c73
commit e151619c69
24 changed files with 482 additions and 1313 deletions

4
.gitignore vendored
View File

@ -80,6 +80,4 @@ crashlytics-build.properties
/[Aa]ssets/[Ss]treamingAssets/aa/*
/UI参考
/DisplayItemInfoForm_Summary.md
/UI_Design_Summary.md
/AGENTS.md

View File

@ -1,44 +0,0 @@
# Repository Guidelines
## Project Structure & Module Organization
This is a Unity project (Unity 2022.3.62f3c1). Core game code and assets live under `Assets/`:
- `Assets/GameMain/` for game-specific scripts, scenes, and content.
- `Assets/GameFramework/` for shared framework code and editor tooling.
- `Assets/Plugins/` for third-party integrations (e.g., DOTween).
- `Assets/Resources/` and `Assets/StreamingAssets/` for runtime-loaded data.
- `Json/` and `数据表/` for data files and tables used by the game.
- `Tools/` for local utilities or pipelines.
Avoid editing generated folders: `Library/`, `Temp/`, `Logs/`, `obj/`, `ProjectSettings/` (unless configuration changes are intentional).
## Build, Test, and Development Commands
Primary workflow is through the Unity Editor:
- Open the project in Unity Hub, then press Play to run locally.
- Optional CLI launch: `Unity -projectPath .` (use your local Unity editor path).
- The solution file `VampireLike.sln` supports IDE navigation (Rider/VS/VS Code).
## Coding Style & Naming Conventions
- C# style: 4-space indentation, braces on the same line, one public type per file.
- Match filename to main type (e.g., `PlayerController.cs` defines `PlayerController`).
- Use `PascalCase` for public types/members, `camelCase` for locals/parameters.
- Prefer `SerializeField` for private Unity fields that must be editable in the Inspector.
## Testing Guidelines
`com.unity.test-framework` is included, but there is no dedicated `Assets/**/Tests` directory yet. When adding tests:
- Place under `Assets/Tests/` or `Assets/<Module>/Tests/`.
- Name files `*Tests.cs` and use NUnit-style `[Test]` methods.
- Run via Unity Test Runner (Window > General > Test Runner).
## Commit & Pull Request Guidelines
This repository does not include Git history, so no commit convention is enforced. Recommended default:
- Short, imperative subject (e.g., `Add enemy spawn tuning`).
- Include scope tags when helpful (e.g., `UI: Fix pause menu layout`).
For pull requests, include:
- A concise summary and testing notes.
- Linked issues or tasks when applicable.
- Screenshots or short clips for UI/visual changes.
## Configuration Tips
- Keep `Packages/manifest.json` and `ProjectSettings/` in sync when changing dependencies or project settings.
- Large binary assets should stay in `Assets/` with `.meta` files committed alongside.

View File

@ -93,6 +93,7 @@ namespace Entity
set
{
if (value == _enable) return;
_enable = value;
_movementComponent.SetMove(value);
_backpackComponent.SetWeaponState(value);
_inputComponent.SetListening(value);

View File

@ -6,12 +6,10 @@
//------------------------------------------------------------
using Components;
using Definition;
using Definition.DataStruct;
using Entity.EntityData;
using StarForce;
using Game.Utility;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace Entity
{

View File

@ -6,7 +6,7 @@ using Definition.DataStruct;
using Definition.Enum;
using DG.Tweening;
using Entity.EntityData;
using StarForce;
using Game.Utility;
using UnityEngine;
using UnityGameFramework.Runtime;

View File

@ -94,8 +94,6 @@ namespace Procedure
GameEntry.Entity.ShowPlayer(_currentPlayerData);
GameEntry.UIRouter.OpenUI(UIFormType.HudForm);
InitGameState();
}
protected override void OnUpdate(IFsm<IProcedureManager> procedureOwner, float elapseSeconds,

View File

@ -0,0 +1,183 @@
using Definition.Enum;
using GameFramework.Event;
using UnityGameFramework.Runtime;
namespace UI
{
public abstract class UIFormControllerCommonBase<TContext, TForm> : UIFormControllerBase<TContext>
where TContext : UIContext
where TForm : UGuiForm
{
private TContext _context;
private TForm _form;
private int? _formSerialId;
private bool _pendingRefresh;
private bool _isBindEvent;
protected TContext Context => _context;
protected TForm Form => _form;
protected int? FormSerialId => _formSerialId;
protected abstract UIFormType UIFormTypeId { get; }
protected abstract void RefreshUI(TForm form, TContext context);
protected virtual void SubscribeCustomEvents()
{
}
protected virtual void UnsubscribeCustomEvents()
{
}
protected virtual void CloseLoadedFormDirect(TForm form)
{
form.Close();
}
protected void SetContext(TContext context)
{
_context = context;
}
protected void RefreshCurrentUI()
{
if (_context == null)
{
return;
}
if (_form == null)
{
_pendingRefresh = true;
return;
}
RefreshUI(_form, _context);
_pendingRefresh = false;
}
protected override int? OpenUIInternal(TContext context)
{
if (context == null)
{
Log.Warning("{0}.OpenUI() context is null.", GetType().Name);
return null;
}
_context = context;
if (_form != null && _formSerialId.HasValue && GameEntry.UI.HasUIForm(_formSerialId.Value))
{
RefreshUI(_form, _context);
return _formSerialId;
}
CloseUI();
_pendingRefresh = true;
SubscribeEvents();
_formSerialId = GameEntry.UI.OpenUIForm(UIFormTypeId, _context);
return _formSerialId;
}
public override void CloseUI()
{
_pendingRefresh = false;
UnsubscribeEvents();
if (_formSerialId.HasValue)
{
if (GameEntry.UI.HasUIForm(_formSerialId.Value))
{
GameEntry.UI.CloseUIForm(_formSerialId.Value);
}
_form = null;
_formSerialId = null;
return;
}
if (_form != null)
{
CloseLoadedFormDirect(_form);
_form = null;
}
}
private void SubscribeEvents()
{
if (_isBindEvent)
{
return;
}
GameEntry.Event.Subscribe(OpenUIFormSuccessEventArgs.EventId, OpenUIFormSuccess);
GameEntry.Event.Subscribe(CloseUIFormCompleteEventArgs.EventId, CloseUIFormComplete);
SubscribeCustomEvents();
_isBindEvent = true;
}
private void UnsubscribeEvents()
{
if (!_isBindEvent)
{
return;
}
GameEntry.Event.Unsubscribe(OpenUIFormSuccessEventArgs.EventId, OpenUIFormSuccess);
GameEntry.Event.Unsubscribe(CloseUIFormCompleteEventArgs.EventId, CloseUIFormComplete);
UnsubscribeCustomEvents();
_isBindEvent = false;
}
private void OpenUIFormSuccess(object sender, GameEventArgs e)
{
if (!(e is OpenUIFormSuccessEventArgs args))
{
return;
}
if (!_formSerialId.HasValue)
{
return;
}
if (args.UIForm == null || args.UIForm.SerialId != _formSerialId.Value || args.UserData != _context)
{
return;
}
_form = args.UIForm.Logic as TForm;
if (_form == null)
{
Log.Warning("{0} open success but form logic is invalid.", GetType().Name);
return;
}
if (_pendingRefresh)
{
RefreshCurrentUI();
}
}
private void CloseUIFormComplete(object sender, GameEventArgs e)
{
if (!(e is CloseUIFormCompleteEventArgs args))
{
return;
}
if (args.SerialId != _formSerialId)
{
return;
}
_form = null;
_formSerialId = null;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 696c0314286f89c4082c2aa2eddaec2d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,60 +1,17 @@
using Definition.Enum;
using GameFramework.Event;
using UnityGameFramework.Runtime;
namespace UI
{
public class TUIFormController : UIFormControllerBase<UIContext>
public class TUIFormController : UIFormControllerCommonBase<UIContext, TUIForm>
{
private IUIUseCase _useCase;
private UIContext _context;
private TUIForm _tForm;
private int? _tFormSerialId;
private bool _pendingRefresh;
private bool _isBindEvent;
private void SubscribeEvents()
protected override UIFormType UIFormTypeId => UIFormType.TUIForm;
protected override void RefreshUI(TUIForm form, UIContext context)
{
if (_isBindEvent) return;
GameEntry.Event.Subscribe(OpenUIFormSuccessEventArgs.EventId, OpenUIFormSuccess);
GameEntry.Event.Subscribe(CloseUIFormCompleteEventArgs.EventId, CloseUIFormComplete);
_isBindEvent = true;
}
private void UnsubscribeEvents()
{
if (!_isBindEvent) return;
GameEntry.Event.Unsubscribe(OpenUIFormSuccessEventArgs.EventId, OpenUIFormSuccess);
GameEntry.Event.Unsubscribe(CloseUIFormCompleteEventArgs.EventId, CloseUIFormComplete);
_isBindEvent = false;
}
protected override int? OpenUIInternal(UIContext context)
{
if (context == null)
{
Log.Warning("TUIFormController open failed. context is null.");
return null;
}
_context = context;
if (_tForm != null && _tFormSerialId.HasValue &&
GameEntry.UI.HasUIForm(_tFormSerialId.Value))
{
_tForm.RefreshUI(_context);
return _tFormSerialId;
}
CloseUI();
_pendingRefresh = true;
SubscribeEvents();
_tFormSerialId = GameEntry.UI.OpenUIForm(UIFormType.TUIForm, context);
return _tFormSerialId;
form.RefreshUI(context);
}
public override int? OpenUI(object userData = null)
@ -70,110 +27,18 @@ namespace UI
return null;
}
return OpenUIInternal(_context);
}
public override void CloseUI()
{
_pendingRefresh = false;
UnsubscribeEvents();
if (_tFormSerialId.HasValue)
{
if (GameEntry.UI.HasUIForm(_tFormSerialId.Value))
{
GameEntry.UI.CloseUIForm(_tFormSerialId.Value);
}
_tForm = null;
_tFormSerialId = null;
return;
}
if (_tForm != null)
{
_tForm.Close();
_tForm = null;
}
return OpenUIInternal(Context);
}
public override void BindUseCase(IUIUseCase useCase)
{
if (!(useCase is IUIUseCase UIFormUseCase))
if (!(useCase is IUIUseCase uiFormUseCase))
{
Log.Error("LevelUpForm.BindUseCase() useCase is invalid.");
return;
}
_useCase = UIFormUseCase;
}
private void TryRefreshUI()
{
if (_context == null)
{
return;
}
if (_tForm == null)
{
_pendingRefresh = true;
return;
}
_tForm.RefreshUI(_context);
_pendingRefresh = false;
}
#region EventHanlders
private void OpenUIFormSuccess(object sender, GameEventArgs e)
{
if (!(e is OpenUIFormSuccessEventArgs args))
{
return;
}
if (!_tFormSerialId.HasValue)
{
return;
}
if (args.UIForm == null || args.UIForm.SerialId != _tFormSerialId.Value || args.UserData != _context)
{
return;
}
_tForm = args.UIForm.Logic as TUIForm;
if (_tForm == null)
{
Log.Warning("DialogFormController open success but form logic is invalid.");
return;
}
if (_pendingRefresh)
{
TryRefreshUI();
_useCase = uiFormUseCase;
}
}
private void CloseUIFormComplete(object sender, GameEventArgs e)
{
if (!(e is CloseUIFormCompleteEventArgs args))
{
return;
}
if (args.SerialId != _tFormSerialId)
{
return;
}
_tForm = null;
_tFormSerialId = null;
}
#endregion
}
}

View File

@ -18,11 +18,11 @@ namespace UI
private const float OnHoverAlpha = 0.7f;
private const float OnClickAlpha = 0.6f;
[SerializeField] private UnityEvent _onPointerEnterAction = null;
[SerializeField] private UnityEvent _onPointerEnterAction = new();
[SerializeField] private UnityEvent _onClickAction = null;
[SerializeField] private UnityEvent _onClickAction = new();
[SerializeField] private UnityEvent _onPointerExitAction = null;
[SerializeField] private UnityEvent _onPointerExitAction = new();
[SerializeField] private bool _enableFade = true;

View File

@ -1,39 +1,20 @@
using Definition.Enum;
using GameFramework.Event;
using UnityGameFramework.Runtime;
namespace UI
{
public class DisplayItemInfoFormController : UIFormControllerBase<DisplayItemInfoFormContext>
public class DisplayItemInfoFormController : UIFormControllerCommonBase<DisplayItemInfoFormContext, DisplayItemInfoForm>
{
private DisplayItemInfoFormContext _context;
protected override UIFormType UIFormTypeId => UIFormType.DisplayItemInfoForm;
private DisplayItemInfoForm _itemInfoForm;
private int? _itemInfoFormSerialId;
private bool _pendingRefresh;
private bool _isBindEvent = false;
private void SubscribeEvents()
protected override void RefreshUI(DisplayItemInfoForm form, DisplayItemInfoFormContext context)
{
if (_isBindEvent) return;
GameEntry.Event.Subscribe(OpenUIFormSuccessEventArgs.EventId, OpenUIFormSuccess);
GameEntry.Event.Subscribe(CloseUIFormCompleteEventArgs.EventId, CloseUIFormComplete);
_isBindEvent = true;
form.RefreshUI(context);
}
private void UnsubscribeEvents()
protected override void CloseLoadedFormDirect(DisplayItemInfoForm form)
{
if (!_isBindEvent) return;
GameEntry.Event.Unsubscribe(OpenUIFormSuccessEventArgs.EventId, OpenUIFormSuccess);
GameEntry.Event.Unsubscribe(CloseUIFormCompleteEventArgs.EventId, CloseUIFormComplete);
_isBindEvent = false;
GameEntry.UI.CloseUIForm(form);
}
private static DisplayItemInfoFormContext BuildContext(DisplayItemInfoFormRawData rawData)
@ -56,32 +37,6 @@ namespace UI
};
}
#region UI Methods
protected override int? OpenUIInternal(DisplayItemInfoFormContext context)
{
if (context == null)
{
Log.Warning("ItemInfoFormController open failed. context is null.");
return null;
}
_context = context;
if (_itemInfoForm != null && _itemInfoFormSerialId.HasValue &&
GameEntry.UI.HasUIForm(_itemInfoFormSerialId.Value))
{
_itemInfoForm.RefreshUI(_context);
return _itemInfoFormSerialId;
}
CloseUI();
_pendingRefresh = true;
SubscribeEvents();
_itemInfoFormSerialId = GameEntry.UI.OpenUIForm(UIFormType.DisplayItemInfoForm, context);
return _itemInfoFormSerialId;
}
public int? OpenUI(DisplayItemInfoFormRawData rawData)
{
DisplayItemInfoFormContext context = BuildContext(rawData);
@ -106,32 +61,7 @@ namespace UI
return null;
}
return OpenUIInternal(_context);
}
public override void CloseUI()
{
_pendingRefresh = false;
UnsubscribeEvents();
if (_itemInfoFormSerialId.HasValue)
{
if (GameEntry.UI.HasUIForm(_itemInfoFormSerialId.Value))
{
GameEntry.UI.CloseUIForm(_itemInfoFormSerialId.Value);
}
_itemInfoForm = null;
_itemInfoFormSerialId = null;
return;
}
if (_itemInfoForm != null)
{
GameEntry.UI.CloseUIForm(_itemInfoForm);
_itemInfoForm = null;
}
return OpenUIInternal(Context);
}
public override void BindUseCase(IUIUseCase useCase)
@ -139,78 +69,7 @@ namespace UI
if (!(useCase is DisplayItemInfoFormUseCase))
{
Log.Error("DisplayItemInfoForm.BindUseCase() useCase is invalid.");
return;
}
}
private void TryRefreshUI()
{
if (_context == null)
{
return;
}
if (_itemInfoForm == null)
{
_pendingRefresh = true;
return;
}
_itemInfoForm.RefreshUI(_context);
_pendingRefresh = false;
}
#endregion
#region EventHanlders
private void OpenUIFormSuccess(object sender, GameEventArgs e)
{
if (!(e is OpenUIFormSuccessEventArgs args))
{
return;
}
if (!_itemInfoFormSerialId.HasValue)
{
return;
}
if (args.UIForm == null || args.UIForm.SerialId != _itemInfoFormSerialId.Value || args.UserData != _context)
{
return;
}
_itemInfoForm = args.UIForm.Logic as DisplayItemInfoForm;
if (_itemInfoForm == null)
{
Log.Warning("DisplayItemInfoFormController open success but form logic is invalid.");
return;
}
if (_pendingRefresh)
{
TryRefreshUI();
}
}
private void CloseUIFormComplete(object sender, GameEventArgs e)
{
if (!(e is CloseUIFormCompleteEventArgs args))
{
return;
}
if (args.SerialId != _itemInfoFormSerialId)
{
return;
}
_itemInfoForm = null;
_itemInfoFormSerialId = null;
}
#endregion
}
}

View File

@ -1,39 +1,15 @@
using Definition.Enum;
using GameFramework.Event;
using UnityGameFramework.Runtime;
namespace UI
{
public class HudFormController : UIFormControllerBase<HudFormContext>
public class HudFormController : UIFormControllerCommonBase<HudFormContext, HudForm>
{
private HudFormContext _context;
protected override UIFormType UIFormTypeId => UIFormType.HudForm;
private HudForm _hudForm;
private int? _hudFormSerialId;
private bool _pendingRefresh;
private bool _isBindEvent;
private void SubscribeEvents()
protected override void RefreshUI(HudForm form, HudFormContext context)
{
if (_isBindEvent) return;
GameEntry.Event.Subscribe(OpenUIFormSuccessEventArgs.EventId, OpenUIFormSuccess);
GameEntry.Event.Subscribe(CloseUIFormCompleteEventArgs.EventId, CloseUIFormComplete);
_isBindEvent = true;
}
private void UnsubscribeEvents()
{
if (!_isBindEvent) return;
GameEntry.Event.Unsubscribe(OpenUIFormSuccessEventArgs.EventId, OpenUIFormSuccess);
GameEntry.Event.Unsubscribe(CloseUIFormCompleteEventArgs.EventId, CloseUIFormComplete);
_isBindEvent = false;
form.RefreshUI(context);
}
private static HudFormContext BuildHudFormContext()
@ -41,32 +17,6 @@ namespace UI
return new HudFormContext();
}
#region UI Methods
protected override int? OpenUIInternal(HudFormContext context)
{
if (context == null)
{
Log.Warning("HudFormController.OpenUI() context is null.");
return null;
}
_context = context;
if (_hudForm != null && _hudFormSerialId.HasValue &&
GameEntry.UI.HasUIForm(_hudFormSerialId.Value))
{
_hudForm.RefreshUI(_context);
return _hudFormSerialId;
}
CloseUI();
_pendingRefresh = true;
SubscribeEvents();
_hudFormSerialId = GameEntry.UI.OpenUIForm(UIFormType.HudForm, context);
return _hudFormSerialId;
}
public override int? OpenUI(object userData = null)
{
if (userData is HudFormContext context)
@ -88,105 +38,9 @@ namespace UI
return OpenUIInternal(context);
}
public override void CloseUI()
{
_pendingRefresh = false;
UnsubscribeEvents();
if (_hudFormSerialId.HasValue)
{
if (GameEntry.UI.HasUIForm(_hudFormSerialId.Value))
{
GameEntry.UI.CloseUIForm(_hudFormSerialId.Value);
}
_hudForm = null;
_hudFormSerialId = null;
return;
}
if (_hudForm != null)
{
_hudForm.Close();
_hudForm = null;
}
}
public override void BindUseCase(IUIUseCase useCase)
{
Log.Info("HudFormController doesn't need UseCase");
}
private void TryRefreshUI()
{
if (_context == null)
{
return;
}
if (_hudForm == null)
{
_pendingRefresh = true;
return;
}
_hudForm.RefreshUI(_context);
_pendingRefresh = false;
}
#endregion
#region Event Handlers
private void OpenUIFormSuccess(object sender, GameEventArgs e)
{
if (!(e is OpenUIFormSuccessEventArgs args))
{
return;
}
if (!_hudFormSerialId.HasValue)
{
return;
}
if (args.UIForm == null || args.UIForm.SerialId != _hudFormSerialId.Value ||
args.UserData != _context)
{
return;
}
_hudForm = args.UIForm.Logic as HudForm;
if (_hudForm == null)
{
Log.Warning("HudFormController open success but form logic is invalid.");
return;
}
if (_pendingRefresh)
{
TryRefreshUI();
}
}
private void CloseUIFormComplete(object sender, GameEventArgs e)
{
if (!(e is CloseUIFormCompleteEventArgs args))
{
return;
}
if (args.SerialId != _hudFormSerialId)
{
return;
}
_hudForm = null;
_hudFormSerialId = null;
}
#endregion
}
}

View File

@ -3,51 +3,40 @@ using CustomEvent;
using Definition.Enum;
using Game.Utility;
using GameFramework.Event;
using Procedure;
using UnityGameFramework.Runtime;
namespace UI
{
public class LevelUpFormController : UIFormControllerBase<LevelUpFormContext>
public class LevelUpFormController : UIFormControllerCommonBase<LevelUpFormContext, LevelUpForm>
{
private LevelUpFormUseCase _useCase;
private bool _pendingRefresh;
protected override UIFormType UIFormTypeId => UIFormType.LevelUpForm;
private int? _levelUpFormSerialId;
private LevelUpForm _levelUpForm;
private LevelUpFormContext _context;
private bool _isBindEvent;
private void SubscribeEvents()
protected override void RefreshUI(LevelUpForm form, LevelUpFormContext context)
{
if (_isBindEvent) return;
GameEntry.Event.Subscribe(OpenUIFormSuccessEventArgs.EventId, OpenUIFormSuccess);
GameEntry.Event.Subscribe(CloseUIFormCompleteEventArgs.EventId, CloseUIFormComplete);
GameEntry.Event.Subscribe(RefreshEventArgs.EventId, OnRefresh);
GameEntry.Event.Subscribe(LevelUpPropSelectedEventArgs.EventId, OnLevelUpPropSelected);
_isBindEvent = true;
form.RefreshUI(context);
}
private void UnsubscribeEvents()
protected override void SubscribeCustomEvents()
{
if (!_isBindEvent) return;
GameEntry.Event.Subscribe(RefreshEventArgs.EventId, OnRefresh);
GameEntry.Event.Subscribe(LevelUpPropSelectedEventArgs.EventId, OnLevelUpPropSelected);
}
GameEntry.Event.Unsubscribe(OpenUIFormSuccessEventArgs.EventId, OpenUIFormSuccess);
GameEntry.Event.Unsubscribe(CloseUIFormCompleteEventArgs.EventId, CloseUIFormComplete);
protected override void UnsubscribeCustomEvents()
{
GameEntry.Event.Unsubscribe(RefreshEventArgs.EventId, OnRefresh);
GameEntry.Event.Unsubscribe(LevelUpPropSelectedEventArgs.EventId, OnLevelUpPropSelected);
_isBindEvent = false;
}
private static LevelUpFormContext BuildContext(LevelUpFormRawData rawData)
{
if (rawData == null || rawData.Rewards == null)
{
return null;
}
List<LevelUpRewardItemContext> props = new List<LevelUpRewardItemContext>(rawData.Rewards.Count);
foreach (var reward in rawData.Rewards)
{
@ -72,33 +61,6 @@ namespace UI
};
}
#region UI Methods
protected override int? OpenUIInternal(LevelUpFormContext context)
{
if (context == null)
{
Log.Warning("LevelUpFormController.OpenUI() context is null.");
return null;
}
_context = context;
if (_levelUpForm != null && _levelUpFormSerialId.HasValue &&
GameEntry.UI.HasUIForm(_levelUpFormSerialId.Value))
{
_levelUpForm.RefreshUI(_context);
return _levelUpFormSerialId;
}
CloseUI();
_pendingRefresh = true;
SubscribeEvents();
_levelUpFormSerialId = GameEntry.UI.OpenUIForm(UIFormType.LevelUpForm, context);
return _levelUpFormSerialId;
}
public override int? OpenUI(object userData = null)
{
if (userData is LevelUpFormContext context)
@ -133,47 +95,6 @@ namespace UI
return OpenUIInternal(context);
}
private void TryRefreshUI()
{
if (_context == null)
{
return;
}
if (_levelUpForm == null)
{
_pendingRefresh = true;
return;
}
_levelUpForm.RefreshUI(_context);
_pendingRefresh = false;
}
public override void CloseUI()
{
_pendingRefresh = false;
UnsubscribeEvents();
if (_levelUpFormSerialId.HasValue)
{
if (GameEntry.UI.HasUIForm(_levelUpFormSerialId.Value))
{
GameEntry.UI.CloseUIForm(_levelUpFormSerialId.Value);
}
_levelUpForm = null;
_levelUpFormSerialId = null;
return;
}
if (_levelUpForm != null)
{
_levelUpForm.Close();
_levelUpForm = null;
}
}
public override void BindUseCase(IUIUseCase useCase)
{
if (!(useCase is LevelUpFormUseCase levelUpFormUseCase))
@ -185,10 +106,6 @@ namespace UI
_useCase = levelUpFormUseCase;
}
#endregion
#region Service
private void SelectReward(int selectedIndex)
{
if (_useCase == null)
@ -198,7 +115,6 @@ namespace UI
}
LevelUpFormRawData rawData = _useCase.SelectReward(selectedIndex);
if (rawData == null)
{
return;
@ -224,51 +140,6 @@ namespace UI
OpenUI(rawData);
}
#endregion
#region Event Handlers
private void OpenUIFormSuccess(object sender, GameEventArgs e)
{
if (!(e is OpenUIFormSuccessEventArgs args)) return;
if (!_levelUpFormSerialId.HasValue) return;
if (args.UIForm == null || args.UIForm.SerialId != _levelUpFormSerialId.Value || args.UserData != _context)
{
return;
}
_levelUpForm = args.UIForm.Logic as LevelUpForm;
if (_levelUpForm == null)
{
Log.Warning("LevelUpFormController open success but form logic is invalid.");
return;
}
if (_pendingRefresh)
{
TryRefreshUI();
}
}
private void CloseUIFormComplete(object sender, GameEventArgs e)
{
if (!(e is CloseUIFormCompleteEventArgs args))
{
return;
}
if (args.SerialId != _levelUpFormSerialId)
{
return;
}
_levelUpForm = null;
_levelUpFormSerialId = null;
}
private void OnRefresh(object sender, GameEventArgs e)
{
if (!(sender is LevelUpForm))
@ -293,7 +164,5 @@ namespace UI
SelectReward(args.SelectedId);
}
#endregion
}
}

View File

@ -9,50 +9,34 @@ using UnityGameFramework.Runtime;
namespace UI
{
public class ShopFormController : UIFormControllerBase<ShopFormContext>
public class ShopFormController : UIFormControllerCommonBase<ShopFormContext, ShopForm>
{
private ShopFormUseCase _useCase;
private bool _pendingRefresh;
private int? _shopFormSerialId;
private ShopForm _shopForm;
private ShopFormRawData _rawData;
private ShopFormContext _context;
protected override UIFormType UIFormTypeId => UIFormType.ShopForm;
private bool _isBindEvent;
private void SubscribeEvents()
protected override void RefreshUI(ShopForm form, ShopFormContext context)
{
if (_isBindEvent) return;
form.RefreshUI(context);
}
GameEntry.Event.Subscribe(OpenUIFormSuccessEventArgs.EventId, OpenUIFormSuccess);
GameEntry.Event.Subscribe(CloseUIFormCompleteEventArgs.EventId, CloseUIFormComplete);
protected override void SubscribeCustomEvents()
{
GameEntry.Event.Subscribe(RefreshEventArgs.EventId, Refresh);
GameEntry.Event.Subscribe(ShopPurchaseEventArgs.EventId, ShopPurchase);
GameEntry.Event.Subscribe(ShopContinueEventArgs.EventId, ShopContinue);
GameEntry.Event.Subscribe(DisplayItemShowEventArgs.EventId, DisplayItemShow);
GameEntry.Event.Subscribe(DisplayItemHideEventArgs.EventId, DisplayItemHide);
_isBindEvent = true;
}
private void UnsubscribeEvents()
protected override void UnsubscribeCustomEvents()
{
if (!_isBindEvent) return;
GameEntry.Event.Unsubscribe(OpenUIFormSuccessEventArgs.EventId, OpenUIFormSuccess);
GameEntry.Event.Unsubscribe(CloseUIFormCompleteEventArgs.EventId, CloseUIFormComplete);
GameEntry.Event.Unsubscribe(RefreshEventArgs.EventId, Refresh);
GameEntry.Event.Unsubscribe(ShopPurchaseEventArgs.EventId, ShopPurchase);
GameEntry.Event.Unsubscribe(ShopContinueEventArgs.EventId, ShopContinue);
GameEntry.Event.Unsubscribe(DisplayItemShowEventArgs.EventId, DisplayItemShow);
GameEntry.Event.Unsubscribe(DisplayItemHideEventArgs.EventId, DisplayItemHide);
_isBindEvent = false;
}
#region BuildContext
@ -72,17 +56,30 @@ namespace UI
RefreshPrice = rawData.RefreshPrice,
PlayerCoin = rawData.PlayerCoin,
GoodsItems = rawData.GoodsItems,
PropListContext = BuildDisplayListAreaContext("道具", rawData.PropItems, rawData.PropMaxCount),
WeaponListContext = BuildDisplayListAreaContext("武器", rawData.WeaponItems, rawData.WeaponMaxCount)
PropListContext = BuildDisplayListAreaContext(DisplayListAreaType.Prop, rawData.PropItems, rawData.PropMaxCount),
WeaponListContext = BuildDisplayListAreaContext(DisplayListAreaType.Weapon, rawData.WeaponItems, rawData.WeaponMaxCount)
};
}
private static DisplayListAreaContext BuildDisplayListAreaContext(string title, IReadOnlyList<object> items,
private static DisplayListAreaContext BuildDisplayListAreaContext(DisplayListAreaType listType, IReadOnlyList<object> items,
int maxCount)
{
DisplayItemContext[] itemContexts = new DisplayItemContext[items.Count];
if (title == "武器")
string title = GetDisplayListTitle(listType);
if (items == null)
{
return new DisplayListAreaContext
{
Title = title,
CurrentCount = 0,
MaxCount = maxCount,
ItemContexts = System.Array.Empty<DisplayItemContext>()
};
}
DisplayItemContext[] itemContexts = new DisplayItemContext[items.Count];
switch (listType)
{
case DisplayListAreaType.Weapon:
if (items is IReadOnlyList<WeaponBase> weapons)
{
for (int i = 0; i < weapons.Count; i++)
@ -92,9 +89,9 @@ namespace UI
itemContexts[i] = BuildWeaponItem(weapon);
}
}
}
else if (title == "道具")
{
break;
case DisplayListAreaType.Prop:
if (items is IReadOnlyList<PropItem> propItems)
{
for (int i = 0; i < propItems.Count; i++)
@ -104,6 +101,7 @@ namespace UI
itemContexts[i] = BuildPropItem(propItem);
}
}
break;
}
int currentCount = itemContexts.Length;
@ -116,6 +114,16 @@ namespace UI
};
}
private static string GetDisplayListTitle(DisplayListAreaType listType)
{
return listType switch
{
DisplayListAreaType.Weapon => "武器",
DisplayListAreaType.Prop => "道具",
_ => string.Empty
};
}
private static DisplayItemContext BuildPropItem(PropItem propItem)
{
string iconAssetName = null;
@ -135,7 +143,6 @@ namespace UI
};
}
private static DisplayItemContext BuildWeaponItem(WeaponBase weaponBase)
{
string iconAssetName = null;
@ -176,33 +183,8 @@ namespace UI
#endregion
#region UI Methods
protected override int? OpenUIInternal(ShopFormContext context)
{
if (context == null)
{
Log.Warning("ShopFormController.OpenUI() context is null.");
return null;
}
_context = context;
if (_shopForm != null && _shopFormSerialId.HasValue &&
GameEntry.UI.HasUIForm(_shopFormSerialId.Value))
{
_shopForm.RefreshUI(_context);
return _shopFormSerialId;
}
CloseUI();
_pendingRefresh = true;
SubscribeEvents();
_shopFormSerialId = GameEntry.UI.OpenUIForm(UIFormType.ShopForm, context);
return _shopFormSerialId;
}
public int? OpenUI(ShopFormRawData rawData)
{
ShopFormContext context = BuildContext(rawData);
@ -237,29 +219,6 @@ namespace UI
return OpenUI(rawData);
}
public override void CloseUI()
{
_pendingRefresh = false;
UnsubscribeEvents();
if (_shopFormSerialId.HasValue)
{
if (GameEntry.UI.HasUIForm(_shopFormSerialId.Value))
{
GameEntry.UI.CloseUIForm(_shopFormSerialId.Value);
}
_shopForm = null;
_shopFormSerialId = null;
return;
}
if (_shopForm != null)
{
_shopForm.Close();
_shopForm = null;
}
}
public override void BindUseCase(IUIUseCase useCase)
{
if (!(useCase is ShopFormUseCase shopFormUseCase))
@ -271,118 +230,60 @@ namespace UI
_useCase = shopFormUseCase;
}
private void TryRefreshUI()
{
if (_context == null)
{
return;
}
if (_shopForm == null)
{
_pendingRefresh = true;
return;
}
_shopForm.RefreshUI(_context);
_pendingRefresh = false;
}
#endregion
#region Service
private void RefreshGoodsItems(ShopRefreshResult result)
{
if (_context == null || result == null)
if (Context == null || result == null)
{
return;
}
_context.GoodsItems = result.GoodsItems;
_context.RefreshPrice = result.RefreshPrice;
Context.GoodsItems = result.GoodsItems;
Context.RefreshPrice = result.RefreshPrice;
if (_shopForm == null)
if (Form == null)
{
return;
}
_shopForm.RefreshGoodsItems(result.GoodsItems);
_shopForm.RefreshRefreshPrice(result.RefreshPrice);
Form.RefreshGoodsItems(result.GoodsItems);
Form.RefreshRefreshPrice(result.RefreshPrice);
}
private void ApplyGoodsPurchased(ShopPurchaseResult result)
{
if (_context == null || result == null)
if (Context == null || result == null)
{
return;
}
if (_context.GoodsItems != null && result.GoodsIndex >= 0 && result.GoodsIndex < _context.GoodsItems.Count)
if (Context.GoodsItems != null && result.GoodsIndex >= 0 && result.GoodsIndex < Context.GoodsItems.Count)
{
_context.GoodsItems[result.GoodsIndex] = null;
Context.GoodsItems[result.GoodsIndex] = null;
}
if (result.DisplayItem != null)
{
if (result.DisplayItem.IsWeapon)
{
AppendDisplayItemContext(_context.WeaponListContext, result.DisplayItem);
AppendDisplayItemContext(Context.WeaponListContext, result.DisplayItem);
}
else
{
AppendDisplayItemContext(_context.PropListContext, result.DisplayItem);
AppendDisplayItemContext(Context.PropListContext, result.DisplayItem);
}
}
_shopForm?.ApplyGoodsPurchased(result.GoodsIndex, result.DisplayItem);
Form?.ApplyGoodsPurchased(result.GoodsIndex, result.DisplayItem);
}
#endregion
#region Event Handlers
private void OpenUIFormSuccess(object sender, GameEventArgs e)
{
if (!(e is OpenUIFormSuccessEventArgs args)) return;
if (!_shopFormSerialId.HasValue) return;
if (args.UIForm == null || args.UIForm.SerialId != _shopFormSerialId.Value || args.UserData != _context)
{
return;
}
_shopForm = args.UIForm.Logic as ShopForm;
if (_shopForm == null)
{
Log.Warning("ShopFormController open success but form logic is invalid.");
return;
}
if (_pendingRefresh)
{
TryRefreshUI();
}
}
private void CloseUIFormComplete(object sender, GameEventArgs e)
{
if (!(e is CloseUIFormCompleteEventArgs args))
{
return;
}
if (args.SerialId != _shopFormSerialId)
{
return;
}
_shopForm = null;
_shopFormSerialId = null;
}
private void Refresh(object sender, GameEventArgs e)
{
if (!(sender is ShopForm))
@ -442,7 +343,7 @@ namespace UI
private void DisplayItemShow(object sender, GameEventArgs e)
{
if (!(e is DisplayItemShowEventArgs args)) return;
if (!(e is DisplayItemShowEventArgs args) || _rawData == null) return;
DisplayItemInfoFormRawData rawData = new();
rawData.TargetPos = args.TargetPos;
@ -472,7 +373,6 @@ namespace UI
GameEntry.UIRouter.OpenUI(UIFormType.DisplayItemInfoForm, rawData);
}
private void DisplayItemHide(object sender, GameEventArgs e)
{
if (!(e is DisplayItemHideEventArgs)) return;

View File

@ -242,6 +242,14 @@ namespace UI
};
}
if (goods.GoodsType == GoodsType.Weapon)
{
// TODO: Weapon purchase apply flow depends on the upcoming weapon system integration.
// Implement weapon creation/equip/add-to-inventory here when weapon runtime model is ready.
Log.Warning("ShopFormUseCase::ApplyGoodsPurchase: Weapon purchase flow is not implemented yet.");
return null;
}
return null;
}
}

View File

@ -8,6 +8,13 @@ using UnityGameFramework.Runtime;
namespace UI
{
public enum DisplayListAreaType : byte
{
None = 0,
Prop = 1,
Weapon = 2
}
public class DisplayListArea : MonoBehaviour
{
[SerializeField] private TMP_Text _titleText;
@ -72,7 +79,7 @@ namespace UI
public void OnDestroy()
{
_displayItemObjectPool.ReleaseAllUnused();
_displayItemObjectPool?.ReleaseAllUnused();
}
public DisplayItem AddItem(DisplayItemContext itemContext)

View File

@ -23,7 +23,8 @@ namespace UI
}
if (_context.Props == null) return;
for (int i = 0; i < _propItems.Length; i++)
int count = Mathf.Min(_propItems.Length, _context.Props.Count);
for (int i = 0; i < count; i++)
{
_propItems[i].gameObject.SetActive(true);
_propItems[i].Init(_context.Props[i]);

View File

@ -1,39 +1,20 @@
using Definition.Enum;
using GameFramework.Event;
using UnityGameFramework.Runtime;
namespace UI
{
public class DialogFormController : UIFormControllerBase<DialogFormContext>
public class DialogFormController : UIFormControllerCommonBase<DialogFormContext, DialogForm>
{
private DialogFormContext _context;
protected override UIFormType UIFormTypeId => UIFormType.DialogForm;
private DialogForm _dialogForm;
private int? _dialogFormSerialId;
private bool _pendingRefresh;
private bool _isBindEvent;
private void SubscribeEvents()
protected override void RefreshUI(DialogForm form, DialogFormContext context)
{
if (_isBindEvent) return;
GameEntry.Event.Subscribe(OpenUIFormSuccessEventArgs.EventId, OpenUIFormSuccess);
GameEntry.Event.Subscribe(CloseUIFormCompleteEventArgs.EventId, CloseUIFormComplete);
_isBindEvent = true;
form.RefreshUI(context);
}
private void UnsubscribeEvents()
protected override void CloseLoadedFormDirect(DialogForm form)
{
if (!_isBindEvent) return;
GameEntry.Event.Unsubscribe(OpenUIFormSuccessEventArgs.EventId, OpenUIFormSuccess);
GameEntry.Event.Unsubscribe(CloseUIFormCompleteEventArgs.EventId, CloseUIFormComplete);
_isBindEvent = false;
GameEntry.UI.CloseUIForm(form);
}
private static DialogFormContext BuildContext(DialogFormRawData rawData)
@ -59,30 +40,6 @@ namespace UI
};
}
protected override int? OpenUIInternal(DialogFormContext context)
{
if (context == null)
{
Log.Warning("DialogFormController.OpenUI() context is null.");
return null;
}
_context = context;
if (_dialogForm != null && _dialogFormSerialId.HasValue &&
GameEntry.UI.HasUIForm(_dialogFormSerialId.Value))
{
_dialogForm.RefreshUI(_context);
return _dialogFormSerialId;
}
CloseUI();
_pendingRefresh = true;
SubscribeEvents();
_dialogFormSerialId = GameEntry.UI.OpenUIForm(UIFormType.DialogForm, context);
return _dialogFormSerialId;
}
public int? OpenUI(DialogFormRawData rawData)
{
DialogFormContext context = BuildContext(rawData);
@ -101,42 +58,13 @@ namespace UI
return OpenUI(rawData);
}
if (userData is DialogFormRawData dialogParams)
{
return OpenUIInternal(BuildContext(dialogParams));
}
if (userData != null)
{
Log.Warning("DialogFormController.OpenUI() userData type is invalid.");
return null;
}
return OpenUIInternal(_context);
}
public override void CloseUI()
{
_pendingRefresh = false;
UnsubscribeEvents();
if (_dialogFormSerialId.HasValue)
{
if (GameEntry.UI.HasUIForm(_dialogFormSerialId.Value))
{
GameEntry.UI.CloseUIForm(_dialogFormSerialId.Value);
}
_dialogForm = null;
_dialogFormSerialId = null;
return;
}
if (_dialogForm != null)
{
GameEntry.UI.CloseUIForm(_dialogForm);
_dialogForm = null;
}
return OpenUIInternal(Context);
}
public override void BindUseCase(IUIUseCase useCase)
@ -146,70 +74,5 @@ namespace UI
Log.Warning("DialogFormController does not use a use case.");
}
}
private void TryRefreshUI()
{
if (_context == null)
{
return;
}
if (_dialogForm == null)
{
_pendingRefresh = true;
return;
}
_dialogForm.RefreshUI(_context);
_pendingRefresh = false;
}
private void OpenUIFormSuccess(object sender, GameEventArgs e)
{
if (!(e is OpenUIFormSuccessEventArgs args))
{
return;
}
if (!_dialogFormSerialId.HasValue)
{
return;
}
if (args.UIForm == null || args.UIForm.SerialId != _dialogFormSerialId.Value ||
args.UserData != _context)
{
return;
}
_dialogForm = args.UIForm.Logic as DialogForm;
if (_dialogForm == null)
{
Log.Warning("DialogFormController open success but form logic is invalid.");
return;
}
if (_pendingRefresh)
{
TryRefreshUI();
}
}
private void CloseUIFormComplete(object sender, GameEventArgs e)
{
if (!(e is CloseUIFormCompleteEventArgs args))
{
return;
}
if (args.SerialId != _dialogFormSerialId)
{
return;
}
_dialogForm = null;
_dialogFormSerialId = null;
}
}
}

View File

@ -100,7 +100,7 @@ namespace UI
/// <summary>
/// 用户自定义数据。
/// </summary>
public string UserData
public object UserData
{
get;
set;

View File

@ -5,44 +5,29 @@ using UnityGameFramework.Runtime;
namespace UI
{
public class SelectRoleFormController : UIFormControllerBase<SelectRoleFormContext>
public class SelectRoleFormController : UIFormControllerCommonBase<SelectRoleFormContext, SelectRoleForm>
{
private SelectRoleFormUseCase _useCase;
private SelectRoleFormContext _context;
protected override UIFormType UIFormTypeId => UIFormType.SelectRoleForm;
private SelectRoleForm _selectRoleForm;
private int? _selectRoleFormSerialId;
private bool _pendingRefresh;
private bool _isBindEvent;
private void SubscribeEvents()
protected override void RefreshUI(SelectRoleForm form, SelectRoleFormContext context)
{
if (_isBindEvent) return;
form.RefreshUI(context);
}
GameEntry.Event.Subscribe(OpenUIFormSuccessEventArgs.EventId, OpenUIFormSuccess);
GameEntry.Event.Subscribe(CloseUIFormCompleteEventArgs.EventId, CloseUIFormComplete);
protected override void SubscribeCustomEvents()
{
GameEntry.Event.Subscribe(MenuSelectRoleReturnEventArgs.EventId, OnMenuSelectRoleReturn);
GameEntry.Event.Subscribe(MenuSelectRoleSelectedEventArgs.EventId, OnMenuSelectRoleSelected);
GameEntry.Event.Subscribe(MenuSelectRoleConfirmEventArgs.EventId, OnMenuSelectRoleConfirm);
_isBindEvent = true;
}
private void UnsubscribeEvents()
protected override void UnsubscribeCustomEvents()
{
if (!_isBindEvent) return;
GameEntry.Event.Unsubscribe(OpenUIFormSuccessEventArgs.EventId, OpenUIFormSuccess);
GameEntry.Event.Unsubscribe(CloseUIFormCompleteEventArgs.EventId, CloseUIFormComplete);
GameEntry.Event.Unsubscribe(MenuSelectRoleReturnEventArgs.EventId, OnMenuSelectRoleReturn);
GameEntry.Event.Unsubscribe(MenuSelectRoleSelectedEventArgs.EventId, OnMenuSelectRoleSelected);
GameEntry.Event.Unsubscribe(MenuSelectRoleConfirmEventArgs.EventId, OnMenuSelectRoleConfirm);
_isBindEvent = false;
}
private static SelectRoleFormContext BuildContext(SelectRoleFormRawData rawData)
@ -85,32 +70,6 @@ namespace UI
};
}
#region UI Methods
protected override int? OpenUIInternal(SelectRoleFormContext context)
{
if (context == null)
{
Log.Warning("SelectRoleFormController.OpenUI() context is null.");
return null;
}
_context = context;
if (_selectRoleForm != null && _selectRoleFormSerialId.HasValue &&
GameEntry.UI.HasUIForm(_selectRoleFormSerialId.Value))
{
_selectRoleForm.RefreshUI(_context);
return _selectRoleFormSerialId;
}
CloseUI();
_pendingRefresh = true;
SubscribeEvents();
_selectRoleFormSerialId = GameEntry.UI.OpenUIForm(UIFormType.SelectRoleForm, context);
return _selectRoleFormSerialId;
}
public override int? OpenUI(object userData = null)
{
if (userData is SelectRoleFormContext selectRoleFormContext)
@ -124,35 +83,17 @@ namespace UI
return null;
}
if (_useCase == null)
{
Log.Error("SelectRoleFormController.OpenUI() useCase is null.");
return null;
}
SelectRoleFormRawData rawData = _useCase.CreateModel();
SelectRoleFormContext context = BuildContext(rawData);
return OpenUIInternal(context);
}
public override void CloseUI()
{
_pendingRefresh = false;
UnsubscribeEvents();
if (_selectRoleFormSerialId.HasValue)
{
if (GameEntry.UI.HasUIForm(_selectRoleFormSerialId.Value))
{
GameEntry.UI.CloseUIForm(_selectRoleFormSerialId.Value);
}
_selectRoleForm = null;
_selectRoleFormSerialId = null;
return;
}
if (_selectRoleForm != null)
{
_selectRoleForm.Close();
_selectRoleForm = null;
}
}
public override void BindUseCase(IUIUseCase useCase)
{
if (!(useCase is SelectRoleFormUseCase selectRoleUseCase))
@ -164,90 +105,14 @@ namespace UI
_useCase = selectRoleUseCase;
}
private void TryRefreshUI()
{
if (_context == null)
{
return;
}
if (_selectRoleForm == null)
{
_pendingRefresh = true;
return;
}
_selectRoleForm.RefreshUI(_context);
_pendingRefresh = false;
}
#endregion
#region Service
public void UpdateShowRole(RolePropertyAreaContext rolePropertyAreaContext)
{
if (_context != null)
if (Context != null)
{
_context.RolePropertyAreaContext = rolePropertyAreaContext;
Context.RolePropertyAreaContext = rolePropertyAreaContext;
}
if (_selectRoleForm != null)
{
_selectRoleForm.UpdateShowRole(rolePropertyAreaContext);
}
}
#endregion
#region Event Handlers
private void OpenUIFormSuccess(object sender, GameEventArgs e)
{
if (!(e is OpenUIFormSuccessEventArgs args))
{
return;
}
if (!_selectRoleFormSerialId.HasValue)
{
return;
}
if (args.UIForm == null || args.UIForm.SerialId != _selectRoleFormSerialId.Value ||
args.UserData != _context)
{
return;
}
_selectRoleForm = args.UIForm.Logic as SelectRoleForm;
if (_selectRoleForm == null)
{
Log.Warning("SelectRoleFormController open success but form logic is invalid.");
return;
}
if (_pendingRefresh)
{
TryRefreshUI();
}
}
private void CloseUIFormComplete(object sender, GameEventArgs e)
{
if (!(e is CloseUIFormCompleteEventArgs args))
{
return;
}
if (args.SerialId != _selectRoleFormSerialId)
{
return;
}
_selectRoleForm = null;
_selectRoleFormSerialId = null;
Form?.UpdateShowRole(rolePropertyAreaContext);
}
private void OnMenuSelectRoleReturn(object sender, GameEventArgs e)
@ -274,8 +139,8 @@ namespace UI
return;
}
_context = context;
UpdateShowRole(_context.RolePropertyAreaContext);
SetContext(context);
UpdateShowRole(context.RolePropertyAreaContext);
}
private void OnMenuSelectRoleConfirm(object sender, GameEventArgs e)
@ -285,9 +150,7 @@ namespace UI
return;
}
_useCase.ConfirmSelectedRole();
}
#endregion
_useCase?.ConfirmSelectedRole();
}
}
}

View File

@ -5,50 +5,33 @@ using UnityGameFramework.Runtime;
namespace UI
{
public class StartMenuFormController : UIFormControllerBase<StartMenuFormContext>
public class StartMenuFormController : UIFormControllerCommonBase<StartMenuFormContext, StartMenuForm>
{
private StartMenuFormContext _context;
protected override UIFormType UIFormTypeId => UIFormType.StartMenuForm;
private StartMenuForm _startMenuForm;
private int? _startMenuFormSerialId;
private bool _pendingRefresh;
private bool _isBindEvent;
private void SubscribeEvents()
protected override void RefreshUI(StartMenuForm form, StartMenuFormContext context)
{
if (_isBindEvent) return;
GameEntry.Event.Subscribe(OpenUIFormSuccessEventArgs.EventId, OpenUIFormSuccess);
GameEntry.Event.Subscribe(CloseUIFormCompleteEventArgs.EventId, CloseUIFormComplete);
form.RefreshUI(context);
}
protected override void SubscribeCustomEvents()
{
GameEntry.Event.Subscribe(MenuStartGameEventArgs.EventId, OnMenuStartGameButtonClick);
GameEntry.Event.Subscribe(MenuFileButtonClickEventArgs.EventId, OnMenuFileButtonClick);
GameEntry.Event.Subscribe(MenuGuideButtonClickEventArgs.EventId, OnMenuGuideButtonClick);
GameEntry.Event.Subscribe(MenuSettingButtonClickEventArgs.EventId, OnMenuSettingButtonClick);
GameEntry.Event.Subscribe(MenuQuitButtonClickEventArgs.EventId, OnMenuQuitButtonClick);
GameEntry.Event.Subscribe(MenuAboutButtonClickEventArgs.EventId, OnMenuAboutButtonClick);
_isBindEvent = true;
}
private void UnsubscribeEvents()
protected override void UnsubscribeCustomEvents()
{
if (!_isBindEvent) return;
GameEntry.Event.Unsubscribe(OpenUIFormSuccessEventArgs.EventId, OpenUIFormSuccess);
GameEntry.Event.Unsubscribe(CloseUIFormCompleteEventArgs.EventId, CloseUIFormComplete);
GameEntry.Event.Unsubscribe(MenuStartGameEventArgs.EventId, OnMenuStartGameButtonClick);
GameEntry.Event.Unsubscribe(MenuFileButtonClickEventArgs.EventId, OnMenuFileButtonClick);
GameEntry.Event.Unsubscribe(MenuGuideButtonClickEventArgs.EventId, OnMenuGuideButtonClick);
GameEntry.Event.Unsubscribe(MenuSettingButtonClickEventArgs.EventId, OnMenuSettingButtonClick);
GameEntry.Event.Unsubscribe(MenuQuitButtonClickEventArgs.EventId, OnMenuQuitButtonClick);
GameEntry.Event.Unsubscribe(MenuAboutButtonClickEventArgs.EventId, OnMenuAboutButtonClick);
_isBindEvent = false;
}
private static StartMenuFormContext BuildStartMenuFormContext()
@ -56,32 +39,6 @@ namespace UI
return new StartMenuFormContext();
}
#region UI Methods
protected override int? OpenUIInternal(StartMenuFormContext context)
{
if (context == null)
{
Log.Warning("StartMenuFormController.OpenUI() context is null.");
return null;
}
_context = context;
if (_startMenuForm != null && _startMenuFormSerialId.HasValue &&
GameEntry.UI.HasUIForm(_startMenuFormSerialId.Value))
{
_startMenuForm.RefreshUI(_context);
return _startMenuFormSerialId;
}
CloseUI();
_pendingRefresh = true;
SubscribeEvents();
_startMenuFormSerialId = GameEntry.UI.OpenUIForm(UIFormType.StartMenuForm, context);
return _startMenuFormSerialId;
}
public override int? OpenUI(object userData = null)
{
if (userData is StartMenuFormContext context)
@ -103,105 +60,11 @@ namespace UI
return OpenUIInternal(context);
}
public override void CloseUI()
{
_pendingRefresh = false;
UnsubscribeEvents();
if (_startMenuFormSerialId.HasValue)
{
if (GameEntry.UI.HasUIForm(_startMenuFormSerialId.Value))
{
GameEntry.UI.CloseUIForm(_startMenuFormSerialId.Value);
}
_startMenuForm = null;
_startMenuFormSerialId = null;
return;
}
if (_startMenuForm != null)
{
_startMenuForm.Close();
_startMenuForm = null;
}
}
public override void BindUseCase(IUIUseCase useCase)
{
Log.Info("StartMenuForm doesn't need UseCase");
}
private void TryRefreshUI()
{
if (_context == null)
{
return;
}
if (_startMenuForm == null)
{
_pendingRefresh = true;
return;
}
_startMenuForm.RefreshUI(_context);
_pendingRefresh = false;
}
#endregion
#region Event Handlers
private void OpenUIFormSuccess(object sender, GameEventArgs e)
{
if (!(e is OpenUIFormSuccessEventArgs args))
{
return;
}
if (!_startMenuFormSerialId.HasValue)
{
return;
}
if (args.UIForm == null || args.UIForm.SerialId != _startMenuFormSerialId.Value ||
args.UserData != _context)
{
return;
}
_startMenuForm = args.UIForm.Logic as StartMenuForm;
if (_startMenuForm == null)
{
Log.Warning("StartMenuFormController open success but form logic is invalid.");
return;
}
if (_pendingRefresh)
{
TryRefreshUI();
}
}
private void CloseUIFormComplete(object sender, GameEventArgs e)
{
if (!(e is CloseUIFormCompleteEventArgs args))
{
return;
}
if (args.SerialId != _startMenuFormSerialId)
{
return;
}
_startMenuForm = null;
_startMenuFormSerialId = null;
}
private void OnMenuStartGameButtonClick(object sender, GameEventArgs e)
{
if (!(sender is StartMenuForm) || !(e is MenuStartGameEventArgs))
@ -270,7 +133,5 @@ namespace UI
Log.Warning("Menu about button click is not implemented.");
}
#endregion
}
}

View File

@ -15,18 +15,16 @@ using UnityEngine;
using UnityGameFramework.Runtime;
using Random = UnityEngine.Random;
namespace StarForce
namespace Game.Utility
{
/// <summary>
/// AI 工具类。
/// </summary>
public static class AIUtility
{
private static Dictionary<CampPair, RelationType> s_CampPairToRelation =
new Dictionary<CampPair, RelationType>();
private static Dictionary<CampPair, RelationType> s_CampPairToRelation = new();
private static Dictionary<KeyValuePair<CampType, RelationType>, CampType[]> s_CampAndRelationToCamps =
new Dictionary<KeyValuePair<CampType, RelationType>, CampType[]>();
private static Dictionary<KeyValuePair<CampType, RelationType>, CampType[]> s_CampAndRelationToCamps = new();
static AIUtility()
{
@ -68,9 +66,7 @@ namespace StarForce
{
if (first > second)
{
CampType temp = first;
first = second;
second = temp;
(first, second) = (second, first);
}
RelationType relationType;
@ -92,8 +88,7 @@ namespace StarForce
public static CampType[] GetCamps(CampType camp, RelationType relation)
{
KeyValuePair<CampType, RelationType> key = new KeyValuePair<CampType, RelationType>(camp, relation);
CampType[] result = null;
if (s_CampAndRelationToCamps.TryGetValue(key, out result))
if (s_CampAndRelationToCamps.TryGetValue(key, out var result))
{
return result;
}
@ -194,7 +189,7 @@ namespace StarForce
}
int entityDamageHP = CalcDamageHP(weaponImpactData.AttackBase, weaponImpactData.AttackStat,
entityImpactData.DefenseStat, entityImpactData.DefenseStat);
entityImpactData.DefenseStat, entityImpactData.DodgeStat);
entity.ApplyDamage(weapon, entityDamageHP);
return;

View File

@ -1,40 +0,0 @@
# 3D 类土豆兄弟开发
## 流程设计
```mermaid
flowchart LR
A["开始菜单"]-->B["进入游戏"]
B-->C["选择角色(进阶)"]
C-->D
B-->D["选择初始武器"]
D-->E["进入关卡"]
E-->F["战斗,获取资源"]
F-->G["关卡结束,进入商店"]
G-->H["进入下一关"]
H-->E
F-->I["玩家死亡"]
I-->J["游戏结算"]
H-->K["完成所有关卡"]
K-->J
J-->A
```
## 开发需求
### 基础部分
#### UI 部分
- [x] 开始菜单 UI
- [ ] 局内 HUD
- [ ] 局内商店页面
- [ ] 设置页面
#### 游戏逻辑部分
- [ ] 玩家操作
- [ ] 玩家武器
- [ ] 自动攻击
- [ ] 等级制,多次获得同一把武器会升级
- [ ] 敌人:近战、远程……
- [ ] 关卡制,每关结束后进入商店购买武器和道具
### 进阶部分
- [ ] 武器词缀:每把高级武器会随机带有一个该等级的词缀,附带额外效果
- [ ] 多角色:每个角色初始属性不同,不同角色技能不同

119
TodoList.md Normal file
View File

@ -0,0 +1,119 @@
# 3D 类吸血鬼幸存者项目 TodoGameMain 侧规划)
> 范围说明:本清单基于当前 `Assets/GameMain` 代码现状制定,未涉及 `Assets/GameFramework` 底层实现。
## 0. 当前代码现状(已确认)
- [x] 已有完整流程骨架:`Menu -> Game(Battle/LevelUp/Shop)`以及基础实体系统Player/Enemy/Weapon/Drop/UI
- [x] 目前仍是传统 `MonoBehaviour + 每实体 OnUpdate` 驱动,暂无 Job System/Burst 实装。
- [x] 已有一个 Instancing Shader`Assets/GameMain/Materials/Shaders/SimpleInstancedFlash.shader`,但未接入运行时批量渲染管线。
- [x] 未发现代码热更新方案接入(如 HybridCLR/ILRuntime/xLua 等)。
## 1. P0 基线修正与性能基准
- [ ] 建立性能基准场景(建议复用 `Game.unity` + 压测参数):
- 指标:`1k / 3k / 5k` 敌人时的 FPS、CPU Main Thread、GC Alloc、Draw Calls。
- 输出:一份基线表格(开发机配置 + Unity Profiler 截图)。
- [x] 修正当前高风险逻辑问题(避免后续优化建立在不稳定行为上):
- `ProcedureGame.OnEnter()``_hudInitialized` 逻辑中有重复初始化状态机风险(`InitGameState()` 被调用两次)。
- `Player.Enable` setter 未更新 `_enable` 字段,状态切换语义不完整。
- `PlayerData` 构造中 `MaxHealthBase` 初始化异常(自赋值)。
- `AIUtility.PerformCollision()` 武器伤害计算参数里疑似把 `DodgeStat` 传成 `DefenseStat`
- [ ] 给关键战斗链路加最小回归测试PlayMode
- 伤害结算、掉落、回合切换Battle/LevelUp/Shop
**验收标准**
- 基线数据可复现。
- 以上问题修正后,核心流程可稳定连续跑 10 分钟无异常日志。
## 2. P1 Simulation 分层(为 Job/Burst 做结构准备)
- [ ] 新建 `Simulation` 层(建议目录:`Assets/GameMain/Scripts/Simulation`
- `SimulationWorld`:统一持有敌人/投射物/掉落物的纯数据容器。
- `EnemySimData / ProjectileSimData / PickupSimData`:结构化、连续内存友好的数据定义。
- `EntityBinding`:维护 `EntityId <-> SimulationIndex` 映射。
- [ ] 将“逻辑计算”和“表现层Transform/Animator/特效/UI”拆离
- 逻辑层输出 position/rotation/state。
- 表现层只消费结果做显示。
- [ ] 先保持现有 GameFramework 实体生命周期不变,仅替换更新路径。
**验收标准**
- 敌人移动/追踪由 Simulation 统一调度,不再逐个 Enemy MonoBehaviour 执行核心逻辑。
## 3. P2 Job System + Burst 落地(核心性能阶段)
- [ ] 引入并锁定依赖版本Unity 2022.3 对应):
- `com.unity.collections`
- `com.unity.jobs`
- `com.unity.burst`
- `com.unity.mathematics`
- [ ] 第一批 Job 化模块(优先级从高到低):
1. 敌人移动与朝向更新(`IJobParallelFor`)。
2. 目标选择加速(空间哈希/网格分桶,减少全量最近邻搜索)。
3. 投射物批量移动与寿命回收。
4. AOE/碰撞候选筛选(先 broad phase后精算
- [ ] Burst 编译策略:
- 热路径 Job 全部 `[BurstCompile]`
- 禁止在 Job 内使用托管分配、虚调用、LINQ。
- [ ] 主线程仅做输入采样、状态切换、UI同步、实体显隐。
**验收标准**
- 在 3k 敌人规模下CPU Main Thread 明显下降(目标 >= 30%)。
- Profiler 中战斗帧 GC Alloc 接近 0持续帧
## 4. P3 GPU Instancing 渲染管线(与 Job 并行推进)
- [ ] 先做“低风险版”批处理:
- 同 Mesh/Material 的敌人分组,使用 `Graphics.DrawMeshInstanced`(每批最多 1023
- [ ] 再升级“高上限版”:
- 使用 `Graphics.DrawMeshInstancedIndirect` + `ComputeBuffer` 管理实例矩阵/颜色/状态。
- [ ] 建立 `InstanceRendererComponent`
- 输入Simulation 输出的 transform/state。
- 输出:按 enemy archetype 的批量绘制。
- [ ] 将受击闪白、稀有度颜色等通过 `MaterialPropertyBlock` 或实例化属性下发(复用现有 Instanced Shader 思路)。
- [ ] 与现有碰撞体系解耦:
- 逻辑碰撞走 Simulation渲染不再依赖每敌人独立 GameObject Renderer。
**验收标准**
- 5k 敌人规模 Draw Calls 显著下降。
- 渲染主线程耗时可控,且视觉行为(受击、朝向、死亡)与逻辑一致。
## 5. P4 代码热更新(建议 HybridCLR
- [ ] 技术选型定稿:建议 `HybridCLR`Unity 2022 + C# 生态兼容更自然)。
- [ ] Assembly 拆分:
- `Main`:启动、资源更新、基础桥接(不可热更)。
- `Hotfix`:玩法规则、数值公式、技能与敌人行为树(可热更)。
- [ ] 运行时加载流程:
- 启动时通过现有资源更新流程拉取热更 DLL与版本号绑定
- 加载 AOT metadata + Hotfix DLL反射启动 `HotfixEntry`
- [ ] 建立热更边界规范:
- Hotfix 不直接依赖编辑器代码。
- 跨域调用统一走接口/Facade避免大量反射散落
- [ ] 回滚机制:
- DLL 校验失败时回退上一个稳定版本。
**验收标准**
- 不发整包即可替换一条技能逻辑并在设备上生效。
- 热更失败可自动回退,启动不中断。
## 6. P5 玩法目标对齐(与技术栈并行)
- [ ] 武器系统补完:
- 自动攻击、多武器并存、同武器升级/进化。
- `Shop` 武器购买流程补完(当前已有 TODO
- [ ] 敌人系统扩展:
- 近战/远程/精英/首领模板化,支持波次参数化。
- [ ] 关卡节奏:
- `DRLevel` 扩展为“时间轴+事件波次+奖励节点”。
- [ ] 数值可调试工具:
- 实时查看 Dps、受击、击杀效率、掉落速率。
**验收标准**
- 一局 10~20 分钟循环可闭环,且关卡难度曲线平滑。
## 7. 推荐执行顺序(避免返工)
- [ ] 里程碑 A`P0 -> P1`(稳定结构 + 可观测)
- [ ] 里程碑 B`P2`CPU 性能突破)
- [ ] 里程碑 C`P3`(渲染性能突破)
- [ ] 里程碑 D`P4`(线上快速迭代能力)
- [ ] 里程碑 E`P5`(内容量与可玩性扩展)
## 8. 交付物清单(每阶段都要有)
- [ ] 设计文档(接口、数据结构、生命周期)。
- [ ] Profiling 对比(改造前后同场景同参数)。
- [ ] 回归用例(至少战斗、关卡切换、商店、升级)。
- [ ] 风险与回滚说明(特别是热更新与渲染链路)。