Unity TEngine5实战:用它的UI模块和事件系统,快速搭建一个战斗界面(含代码)
Unity TEngine5实战:从零构建战斗UI系统全流程解析
在游戏开发中,一个高效、响应迅速的战斗界面系统往往决定了玩家的核心体验。本文将基于TEngine5框架,手把手带你实现一个完整的战斗UI系统,涵盖UI构建、事件驱动和数据绑定三大核心模块。
1. 环境准备与项目初始化
首先确保你的Unity项目已经集成TEngine5框架。这个开源框架提供了完整的UI工作流和事件系统,能显著提升开发效率。新建Unity项目后,通过Package Manager导入TEngine5,或者直接从GitHub仓库克隆最新版本。
关键依赖检查清单:
- Unity 2021 LTS或更新版本
- TextMeshPro基础包(用于高质量文本渲染)
- TEngine5核心模块(UI、Event、Resource)
// 初始化TEngine框架的典型入口代码 public class GameLaunch : MonoBehaviour { void Start() { // 初始化框架模块 GameModule.CreateModules(); // 启动游戏逻辑 StartGameLogic().Forget(); } private async UniTaskVoid StartGameLogic() { // 加载必要资源 await GameModule.Resource.LoadAssetsAsync("UI/Battle"); // 进入战斗场景 GameModule.Scene.LoadScene("BattleScene").Forget(); } }2. 战斗UI界面架构设计
一个标准的战斗UI通常包含以下核心组件:
| 组件类型 | 功能描述 | 实现方式 |
|---|---|---|
| 血条系统 | 显示玩家/敌人生命值 | Slider + 事件监听 |
| 分数显示 | 实时更新战斗得分 | TextMeshPro + 数据绑定 |
| 技能按钮 | 触发角色技能 | Button + 事件派发 |
| 暂停菜单 | 游戏状态控制 | 弹出式UI窗口 |
UI层级规划建议:
- Background Layer:背景元素(非交互)
- Gameplay Layer:核心战斗信息(血条、分数)
- Interaction Layer:按钮和可操作元素
- Popup Layer:暂停菜单等临时界面
3. 使用UIWindow构建基础界面
TEngine5的UIWindow基类提供了完整的生命周期管理。我们首先创建战斗主界面:
[Window(UILayer.Gameplay)] public class UIBattleMain : UIWindow { #region 自动生成的组件引用 private Slider m_hpSlider; private TMP_Text m_scoreText; private Button m_skillButton; private Button m_pauseButton; protected override void ScriptGenerator() { m_hpSlider = FindChildComponent<Slider>("HPBar"); m_scoreText = FindChildComponent<TMP_Text>("ScoreText"); m_skillButton = FindChildComponent<Button>("SkillBtn"); m_pauseButton = FindChildComponent<Button>("PauseBtn"); m_skillButton.onClick.AddListener(OnSkillButtonClick); m_pauseButton.onClick.AddListener(OnPauseButtonClick); } #endregion private void OnSkillButtonClick() { GameEvent.Send(BattleEventDefine.PlayerCastSkill); } private void OnPauseButtonClick() { GameModule.UI.ShowWindow<UIPauseMenu>(); } }UIWidget的巧妙应用: 对于可复用的UI元素如血条,建议封装为UIWidget:
public class UIHealthBar : UIWidget { private Slider m_slider; private Entity m_bindEntity; public void BindEntity(Entity entity) { m_bindEntity = entity; // 初始值设置 UpdateHealth(entity.CurrentHealth, entity.MaxHealth); // 监听血量变化事件 entity.OnHealthChanged += UpdateHealth; } private void UpdateHealth(float current, float max) { m_slider.value = current / max; } public override void OnDestroy() { if(m_bindEntity != null) m_bindEntity.OnHealthChanged -= UpdateHealth; } }4. 事件系统的深度集成
TEngine5的GameEvent系统是UI与游戏逻辑解耦的关键。我们需要定义战斗相关事件:
public static class BattleEventDefine { // 分数变化事件(携带int参数) public static readonly int ScoreUpdate = StringId.StringToHash("Battle.ScoreUpdate"); // 玩家释放技能事件 public static readonly int PlayerCastSkill = StringId.StringToHash("Battle.CastSkill"); // 游戏暂停事件 public static readonly int GamePaused = StringId.StringToHash("Battle.GamePaused"); }事件监听与响应的最佳实践:
// 在UIWindow中注册事件监听 protected override void RegisterEvent() { AddUIEvent<int>(BattleEventDefine.ScoreUpdate, OnScoreUpdated); AddUIEvent(BattleEventDefine.GamePaused, OnGamePaused); } private void OnScoreUpdated(int newScore) { m_scoreText.text = $"SCORE: {newScore}"; // 添加分数变化动画 PlayScoreAnimation(); } private void OnGamePaused() { m_pauseButton.interactable = false; }在游戏逻辑层派发事件:
public class BattleSystem : BehaviourSingleton<BattleSystem> { private int m_currentScore; public void AddScore(int points) { m_currentScore += points; GameEvent.Send(BattleEventDefine.ScoreUpdate, m_currentScore); } public void TogglePause() { bool isPaused = Time.timeScale == 0; Time.timeScale = isPaused ? 1 : 0; GameEvent.Send(BattleEventDefine.GamePaused); } }5. 数据驱动视图的高级技巧
现代游戏UI越来越倾向于数据驱动的设计模式。TEngine5虽然没有内置的MVVM框架,但我们可以实现轻量级的数据绑定:
public class ObservableProperty<T> { private T m_value; public event Action<T> OnValueChanged; public T Value { get => m_value; set { if(!Equals(m_value, value)) { m_value = value; OnValueChanged?.Invoke(value); } } } } // 在UI中的使用示例 public class UIPlayerStatus : UIWindow { private ObservableProperty<float> m_health = new(); private Slider m_healthSlider; protected override void OnCreate() { m_healthSlider = FindChildComponent<Slider>("HealthBar"); m_health.OnValueChanged += UpdateHealthBar; } private void UpdateHealthBar(float value) { m_healthSlider.value = value; } public void SetHealth(float current, float max) { m_health.Value = current / max; } }性能优化建议:
- 对于高频更新的数据(如帧数显示),使用UniTask的每帧检测而非事件驱动
- 批量更新UI元素时,先禁用Canvas渲染,更新完成后再启用
- 使用对象池管理频繁创建/销毁的UI元素
// 高性能分数更新方案 private async UniTaskVoid UpdateScorePerFrame() { while (true) { if (m_lastScore != m_currentScore) { m_scoreText.text = m_currentScore.ToString(); m_lastScore = m_currentScore; } await UniTask.Yield(PlayerLoopTiming.Update); } }6. 实战:构建完整的战斗UI流程
让我们把这些技术整合到一个实际案例中:
- 场景加载阶段:
private async UniTaskVoid EnterBattle() { // 显示加载界面 await GameModule.UI.ShowWindowAsync<UILoading>(); // 加载战斗场景 await GameModule.Scene.LoadScene("BattleScene"); // 初始化战斗系统 BattleSystem.Instance.Initialize(); // 显示主界面 GameModule.UI.ShowWindow<UIBattleMain>(); // 隐藏加载界面 GameModule.UI.CloseWindow<UILoading>(); }- 战斗进行阶段的典型事件流:
玩家攻击敌人 -> EnemyDefeated事件 -> 分数系统处理 -> ScoreUpdate事件 -> UI更新 玩家点击暂停 -> PauseGame事件 -> 时间系统暂停 -> 显示暂停菜单- 暂停菜单实现:
[Window(UILayer.Popup)] public class UIPauseMenu : UIWindow { private Button m_resumeButton; private Button m_quitButton; protected override void ScriptGenerator() { m_resumeButton = FindChildComponent<Button>("ResumeBtn"); m_quitButton = FindChildComponent<Button>("QuitBtn"); m_resumeButton.onClick.AddListener(OnResume); m_quitButton.onClick.AddListener(OnQuit); } private void OnResume() { BattleSystem.Instance.ResumeGame(); Close(); } private void OnQuit() { GameModule.UI.ShowMessageBox("确认退出战斗吗?", () => { BattleSystem.Instance.EndBattle(); GameModule.Scene.LoadScene("MainMenu").Forget(); }); } }7. 调试与性能分析
TEngine5提供了强大的调试工具:
- UI层级查看器:
// 在控制台查看当前UI堆栈 GameModule.UI.DebugPrintWindowStack();- 事件监控:
// 跟踪特定事件 GameEvent.AddDebugListener(BattleEventDefine.ScoreUpdate, (score) => Debug.Log($"Score updated: {score}"));- 性能统计:
// 在UIWindow中标记性能关键代码 TProfiler.BeginSample("HealthBarUpdate"); UpdateHealthBar(); TProfiler.EndSample();常见问题解决方案:
- UI点击无响应:检查Canvas的Raycaster设置和UI层级顺序
- 事件未触发:确认事件ID一致性和监听器生命周期
- 内存泄漏:确保所有事件监听在OnDestroy中移除
8. 进阶技巧:动态UI与特效集成
对于需要动态生成的UI元素(如伤害数字),推荐使用TEngine5的对象池系统:
public class DamageNumberManager : MonoBehaviour { public void ShowDamage(Vector3 worldPos, int damage) { var uiDamage = GameModule.UI.CreateWidget<UIDamageNumber>(); // 转换世界坐标到屏幕空间 var screenPos = Camera.main.WorldToScreenPoint(worldPos); uiDamage.ShowDamage(damage, screenPos); } } [Window(UILayer.Effect)] public class UIDamageNumber : UIWindow { private TMP_Text m_text; private RectTransform m_rect; public void ShowDamage(int damage, Vector2 screenPos) { m_text.text = damage.ToString(); m_rect.anchoredPosition = screenPos; // 播放动画 PlayAnimation().Forget(); } private async UniTaskVoid PlayAnimation() { // 向上飘动动画 await m_rect.DOAnchorPosY(100, 0.5f) .SetRelative() .ToUniTask(); // 回收实例 GameModule.UI.DestroyWindow(this); } }特效与UI的配合技巧:
- 使用Shader实现UI元素的特殊效果(如血条预警闪烁)
- 将粒子系统渲染到UI层需要调整RenderTexture设置
- 对于复杂的UI动画,推荐使用Dotween配合UniTask
// 血条危险状态特效Shader Shader "UI/HealthBarWarning" { Properties { _MainTex ("Texture", 2D) = "white" {} _WarningColor ("Warning Color", Color) = (1,0,0,1) _WarningThreshold ("Warning Threshold", Range(0,1)) = 0.3 _FlashSpeed ("Flash Speed", Float) = 2.0 } // ... shader代码实现闪烁效果 }9. 多平台适配与响应式布局
针对不同设备屏幕尺寸,TEngine5提供了多种适配方案:
Canvas Scaler配置:
- UI Scale Mode: Scale With Screen Size
- Reference Resolution: 1920x1080(根据项目基准设置)
- Screen Match Mode: Expand(保持比例同时适应屏幕)
锚点与布局组:
- 关键UI元素应设置适当的锚点
- 使用Horizontal/Vertical Layout Group自动排列元素
- Content Size Fitter实现动态尺寸调整
设备特定适配:
protected override void OnAdaption() { // 移动设备隐藏部分特效 if(Application.isMobilePlatform) { FindChild("HighEndEffects").SetActive(false); } // 超宽屏调整布局 float aspect = Screen.width / (float)Screen.height; if(aspect > 2.1f) { m_sidePanels.anchoredPosition = new Vector2(100, 0); } }响应式设计模式:
[Window(UILayer.Gameplay)] public class UIBattleAdaptive : UIWindow { private RectTransform m_leftPanel; private RectTransform m_rightPanel; protected override void OnRectTransformDimensionsChange() { base.OnRectTransformDimensionsChange(); UpdateLayout(); } private void UpdateLayout() { float safePadding = Screen.safeArea.width * 0.05f; m_leftPanel.anchoredPosition = new Vector2(safePadding, 0); m_rightPanel.anchoredPosition = new Vector2(-safePadding, 0); } }10. 项目架构建议与扩展思路
基于TEngine5构建大型项目时,推荐采用以下架构:
BattleSystem (游戏逻辑) ↑ ↓ GameEvent (事件系统) ↑ ↓ UISystem (界面呈现) ↑ ↓ ResourceManager (资源加载)可扩展方向:
- UI主题系统:通过ScriptableObject配置不同风格的UI主题
- 多语言支持:集成TEngine5的本地化模块
- UI自动化测试:基于事件系统模拟用户操作
- 编辑器扩展:自定义UI脚本生成模板
// 主题系统示例 [CreateAssetMenu] public class UITheme : ScriptableObject { public Color primaryColor; public Color dangerColor; public TMP_FontAsset mainFont; public Sprite buttonSprite; } // 在UI中的应用 public class ThemedUI : UIWindow { [SerializeField] private UITheme m_theme; protected override void OnRefresh() { ApplyTheme(m_theme); } private void ApplyTheme(UITheme theme) { m_button.image.sprite = theme.buttonSprite; m_text.font = theme.mainFont; m_warningText.color = theme.dangerColor; } }在实际项目中,我们曾遇到一个棘手问题:当快速切换界面时,由于异步加载未完成就发起新请求,导致UI状态混乱。解决方案是引入操作队列系统:
public class UIOperationQueue { private Queue<Func<UniTask>> m_operations = new(); private bool m_isProcessing; public void Enqueue(Func<UniTask> operation) { m_operations.Enqueue(operation); if(!m_isProcessing) ProcessNext().Forget(); } private async UniTaskVoid ProcessNext() { m_isProcessing = true; try { while(m_operations.Count > 0) { var op = m_operations.Dequeue(); await op(); } } finally { m_isProcessing = false; } } } // 使用示例 UIOperationQueue queue = new(); queue.Enqueue(async () => { await GameModule.UI.ShowWindowAsync<UIWindow1>(); }); queue.Enqueue(async () => { await GameModule.UI.CloseWindow<UIWindow1>(); await GameModule.UI.ShowWindowAsync<UIWindow2>(); });