(client) feat:添加基地界面到游玩界面的过程,添加存档管理,技能树变得可用 (#58)

Co-authored-by: m0_75251201 <m0_75251201@noreply.gitcode.com>
Reviewed-on: http://47.107.252.169:3000/Roguelite-Game-Developing-Team/Gen_Hack-and-Slash-Roguelite/pulls/58
This commit is contained in:
2025-10-03 00:31:34 +08:00
parent aff747be17
commit dd9d90439d
134 changed files with 10322 additions and 4872 deletions

View File

@@ -1,4 +1,5 @@
using System;
using Entity;
using TMPro;
using UnityEngine;
@@ -7,10 +8,21 @@ namespace UI
public class CoinCountUI:MonoBehaviour
{
public TMP_Text text;
public int coinCount;
private void Start()
{
text.text = coinCount.ToString();
}
private void Update()
{
var playerEntity = Program.Instance.FocusedEntity as Character;
if (!playerEntity || playerEntity.Coin == null)
return;
if (coinCount == playerEntity.Coin.Quantity) return;
coinCount = playerEntity.Coin.Quantity;
text.text = coinCount.ToString();
}
}
}

View File

@@ -176,9 +176,11 @@ namespace UI
private void GenerateEntityCallback(EntityDef entityDef)
{
if(entityDef==null)
return;
entityPlacementUI.currentAction = () =>
{
Managers.EntityManager.Instance.GenerateEntity(Program.Instance.FocusedDimensionId, entityDef, Utils.MousePosition.GetWorldPosition());
Managers.EntityManager.Instance.GenerateCharacterEntity(Program.Instance.FocusedDimensionId, entityDef, Utils.MousePosition.GetWorldPosition());
};
entityPlacementUI.Prompt = $"当前生成器:\n名称{entityDef.label}\n描述{entityDef.description}";
entityPlacementUI.snapEnabled = false;
@@ -186,6 +188,8 @@ namespace UI
}
private void GenerateMonsterEntityCallback(MonsterDef monsterDef)
{
if(monsterDef==null)
return;
entityPlacementUI.currentAction = () =>
{
Managers.EntityManager.Instance.GenerateMonsterEntity(Program.Instance.FocusedDimensionId, monsterDef, Utils.MousePosition.GetWorldPosition());
@@ -196,6 +200,8 @@ namespace UI
}
private void GenerateBuildingCallback(BuildingDef def)
{
if(def==null)
return;
entityPlacementUI.currentAction = () =>
{
Managers.EntityManager.Instance.GenerateBuildingEntity(Program.Instance.FocusedDimensionId, def, Utils.MousePosition.GetSnappedWorldPosition());
@@ -206,6 +212,8 @@ namespace UI
}
private void GeneratePickupCallback(ItemDef itemDef)
{
if(itemDef==null)
return;
entityPlacementUI.currentAction = () =>
{
Managers.EntityManager.Instance.GeneratePickupEntity(Program.Instance.FocusedDimensionId, itemDef, Utils.MousePosition.GetWorldPosition());

View File

@@ -22,8 +22,6 @@ namespace UI
}
public void TickUI()
{
if (!IsVisible)
return;
if (Input.GetKeyDown(KeyCode.Escape))
{
Base.UIInputControl.Instance.Hide(this);
@@ -48,12 +46,14 @@ namespace UI
{
base.Show();
focusBox.SetActive(true);
Clock.AddTickUI(this);
}
override public void Hide()
public override void Hide()
{
base.Hide();
focusBox.SetActive(false);
Clock.RemoveTickUI(this);
}
}
}

View File

@@ -37,9 +37,9 @@ namespace UI
Program.Instance.OnFocusedEntityChanged -= FocusEntityChanged;
}
// 如果聚焦实体及其背包存在,则取消订阅背包改变事件。
if (focusedEntity != null && focusedEntity.Inventory != null)
if (focusedEntity != null && focusedEntity.PlayerInventory != null)
{
focusedEntity.Inventory.OnInventoryChanged -= UpdateUI;
focusedEntity.PlayerInventory.OnInventoryChanged -= UpdateUI;
}
Clock.RemoveTick(this);
}
@@ -72,7 +72,7 @@ namespace UI
// 如果存在旧的聚焦实体,则先取消订阅其背包改变事件。
if (focusedEntity)
{
focusedEntity.Inventory.OnInventoryChanged -= UpdateUI;
focusedEntity.PlayerInventory.OnInventoryChanged -= UpdateUI;
}
// 设置新的聚焦实体,并尝试将其转换为 Character 类型。
@@ -81,7 +81,7 @@ namespace UI
// 如果新的聚焦实体存在且是 Character 类型,则订阅其背包改变事件。
if (focusedEntity)
{
focusedEntity.Inventory.OnInventoryChanged += UpdateUI;
focusedEntity.PlayerInventory.OnInventoryChanged += UpdateUI;
}
// 聚焦实体改变后更新 UI 显示。
@@ -102,7 +102,7 @@ namespace UI
// 遍历角色的前三个装备槽假设物品栏只显示前3个槽位
for (var i = 0; i < 3; i++)
{
var slot = focusedEntity.Inventory.GetSlot(i);
var slot = focusedEntity.PlayerInventory.GetSlot(i);
if (i == currentSelectedIndex)
{

View File

@@ -15,7 +15,6 @@ namespace UI
private void OnEnable()
{
Clock.AddTickUI(this);
}
private void OnDisable()
@@ -25,8 +24,6 @@ namespace UI
public virtual void TickUI()
{
if(!IsVisible)
return;
if (Input.GetKeyDown(KeyCode.Escape) || Input.GetKeyDown(actionButton))
UIInputControl.Instance.Hide(this);
}

View File

@@ -0,0 +1,24 @@
using Data;
using Managers;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace UI
{
public class MapViewUI:MonoBehaviour
{
public TMP_Text text;
public Image icon;
public DimensionDef DimensionDefine;
public void Init(DimensionDef dimensionDef)
{
if (dimensionDef == null) return;
text.text = $"<color=yellow>{dimensionDef.label}</color>\n{dimensionDef.description}";
if (!string.IsNullOrEmpty(dimensionDef.icon))
icon.sprite = PackagesImageManager.Instance.GetSprite(dimensionDef.icon);
DimensionDefine=dimensionDef;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a17823520b234a3297fea4642b69a4d7
timeCreated: 1759363251

View File

@@ -6,7 +6,7 @@ using UnityEngine;
namespace UI
{
public class PlayerStateUI : MonoBehaviour, Base.ITick
public class PlayerStateUI : MonoBehaviour, ITick
{
[SerializeField] private BarUI focusedEntityHP;
[SerializeField] private BarUI lastEntityHP;
@@ -17,14 +17,14 @@ namespace UI
[SerializeField] private BuffIconListUI focuseEntityBuffIconList;
[SerializeField] private BuffIconListUI lastEntityBuffIconList;
[SerializeField] private AttackModeUI attackMode;
public void Tick()
{
var focusedEntity = Program.Instance.FocusedEntity;
if (focusedEntity && focusedEntity.entityDef != null)
{
focusedEntityHP.Progress =
(float)focusedEntity.attributes.health / focusedEntity.entityDef.attributes.health;
(float)focusedEntity.AttributesNow.health / focusedEntity.entityDef.attributes.health;
}
}
@@ -39,6 +39,8 @@ namespace UI
focuseEntityBuffIconList.gameObject.SetActive(true);
lastEntityBuffIconList.gameObject.SetActive(true);
attackMode.gameObject.SetActive(true);
Clock.AddTick(this);
}
public void Hide()
@@ -52,11 +54,14 @@ namespace UI
focuseEntityBuffIconList.gameObject.SetActive(false);
lastEntityBuffIconList.gameObject.SetActive(false);
attackMode.gameObject.SetActive(false);
Clock.RemoveTick(this);
}
private void Start()
{
UIInputControl.Instance.OnWindowVisibilityChanged += UIChange;
Clock.AddTick(this);
}
private void OnDestroy()

View File

@@ -238,7 +238,7 @@ namespace UI
}
gameObject.SetActive(false);
Program.Instance.PlayGame(SelectedCharacter);
Program.Instance.SelectCharacterPlayGame(SelectedCharacter);
}
}
}

View File

@@ -0,0 +1,62 @@
using System.Linq;
using Base;
using Data;
using Managers;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace UI
{
public class SkillTreeNodeInformationUI:UIBase
{
public TMP_Text label;
public TMP_Text description;
public TMP_Text coin;
public Button purchaseButton;
public SkillTreeNodeUI currentLink;
public SkillTreeDef SkillTreeDef => currentLink.SkillTreeDefine;
public bool CanUnlock
{
get
{
if (SkillTreeDef.cost > Program.Instance.CoinCount)
return false;
var parent = SkillTreeManager.Instance.GetAllDirectParents(SkillTreeDef);
return parent.All(p => SkillTreeManager.Instance.IsSkillTreeUnlocked(p.defName));
}
}
public void Init(SkillTreeNodeUI nodeUI)
{
currentLink = nodeUI;
label.text = SkillTreeDef.label;
description.text = SkillTreeDef.description;
coin.text = $"{SkillTreeDef.cost}/{Program.Instance.CoinCount}";
if (CanUnlock)
{
coin.color = Color.white;
purchaseButton.interactable = true;
}
else
{
coin.color = Color.red;
purchaseButton.interactable = false;
}
}
public void Purchase()
{
SkillTreeManager.Instance.UnlockSkillTree(SkillTreeDef.defName);
OnHide();
currentLink.Refresh();
}
public void OnHide()
{
UIInputControl.Instance.Hide(this);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9440a85a9d4d43aab42d798468760e13
timeCreated: 1759416081

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using Base;
using Data;
using Managers;
using UnityEngine;
@@ -52,7 +53,7 @@ namespace UI
[SerializeField] private Color unlockedColor;
[SerializeField] private Color lockedColor;
[SerializeField] private GameObject shader;
public void Init(SkillTreeDef skillTreeDef)
{
@@ -163,16 +164,14 @@ namespace UI
public void OnPointerClick(PointerEventData eventData)
{
if (eventData.button == PointerEventData.InputButton.Left)
if (eventData.button != PointerEventData.InputButton.Left) return;
var window = UIInputControl.Instance.GetWindow("SkillTreeNodeInformation") as SkillTreeNodeInformationUI;
if (!window)
{
SkillTreeManager.Instance.UnlockSkillTree(SkillTreeDefine.defName);
Refresh();
}
else if (eventData.button == PointerEventData.InputButton.Right)
{
SkillTreeManager.Instance.LockSkillTree(SkillTreeDefine.defName);
Refresh();
return;
}
UIInputControl.Instance.Show(window);
window.Init(this);
}
}
}

View File

@@ -524,7 +524,6 @@ namespace UI
foreach (var node in layerNodes)
{
node.rectTransform.anchoredPosition = new Vector2(layerX, nodeY);
Debug.Log($"{layerX},{nodeY}");
nodeY += NODE_VERTICAL_SPACING +
node.GetRequiredNodeHeight(Managers.SkillTreeManager.Instance
.GetAllDirectParents(node.SkillTreeDefine).Count);

View File

@@ -1,175 +1,70 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Managers;
using UnityEngine;
namespace UI
{
public class SkillTreeUI : FullScreenUI
public class SkillTreeUI : FullScreenUI
{
public SkillTreePageUI skillTreePageUIPrefab;
public Transform skillTreePageUIParent; // 用于承载 SkillTreePageUI 实例的 Transform
private List<SkillTreePageUI> _skillTreePageUIs = new List<SkillTreePageUI>();
private int _currentPageIndex = 0; // 当前显示的页面索引
// 动画相关
private bool _isAnimating = false;
private float _animationProgress = 0f; // 0到1的进度
[SerializeField] private float _animationDuration = 0.3f; // 动画持续时间
private int _animationDirection = 0; // -1:左翻页, 1:右翻页 (或0表示无动画)
private SkillTreePageUI _currentMovingPage; // 当前正在移动的页面
private SkillTreePageUI _targetMovingPage; // 正在移入的页面
private float _pageWidth; // 页面的标准宽度,用于计算滑动位置
public List<SkillTreePageUI> skillTreePages = new List<SkillTreePageUI>();
private int _index = 0;
public int Index
{
get => _index;
set
{
_index = value;
if (skillTreePages.Count == 0)
return;
while (_index < 0)
{
_index += skillTreePages.Count;
}
while (_index >= skillTreePageUIParent.childCount)
{
_index -= skillTreePageUIParent.childCount;
}
}
}
public SkillTreePageUI FocusedSkillTreePage => skillTreePages.Any() ? skillTreePages[Index] : null;
private void Start()
{
// 获取所有技能树标签
var allTags = Managers.SkillTreeManager.Instance.GetAllTag();
if (allTags == null || allTags.Length == 0)
{
Debug.LogWarning("No skill tree tags found. SkillTreeUI will be empty.");
// 可以在这里禁用翻页按钮或显示提示
var pageList = SkillTreeManager.Instance.GetAllTag();
if (pageList == null)
return;
}
// [改进] 动态获取页面的宽度
// 为了准确获取 prefab 的尺寸,最好在编辑器中设置好 prefab 的 RectTransform
// 或者在这里实例化一个临时对象来获取其 RectTransform 信息。
var prefabRect = skillTreePageUIPrefab.GetComponent<RectTransform>();
if (prefabRect != null)
foreach (var page in pageList)
{
_pageWidth = prefabRect.rect.width;
// 如果父级有LayoutGroup或者Canvas Scaler可能影响实际尺寸需注意
// 如果 prefab 是撑满 FullScreenUI 的,那 _pageWidth 应该等于 FullScreenUI 的宽度
// 这里假设 SkillTreePageUI 会充满其父级,或者具有固定宽度。
// 简单起见,我们也可以假设它充满父级,那么 _pageWidth = parent.GetComponent<RectTransform>().rect.width;
if (skillTreePageUIParent != null)
{
_pageWidth = skillTreePageUIParent.GetComponent<RectTransform>().rect.width;
}
}
else
{
Debug.LogError("SkillTreePageUI Prefab does not have a RectTransform!");
_pageWidth = 1920; // fallback default
}
// 为每个标签实例化并初始化 SkillTreePageUI
foreach (var tag in allTags)
{
var newPage = Instantiate(skillTreePageUIPrefab, skillTreePageUIParent);
newPage.name = $"SkillTreePage_{tag}"; // 方便在 Hierarchy 中识别
// 设置 RectTransform 属性,确保页面正确布局
var pageRectTransform = newPage.GetComponent<RectTransform>();
pageRectTransform.anchorMin = Vector2.zero;
pageRectTransform.anchorMax = Vector2.one;
pageRectTransform.pivot = new Vector2(0.5f, 0.5f); // 中心
pageRectTransform.anchoredPosition = Vector2.zero;
pageRectTransform.sizeDelta = Vector2.zero; // 撑满父级
newPage.GenerateAndLayoutAllSkillTrees(tag);
_skillTreePageUIs.Add(newPage);
// 初始状态下所有页面都先设为激活,通过位置控制显示
newPage.gameObject.SetActive(true);
}
// 根据 _currentPageIndex 设置所有页面的初始位置
// 只有当前页面在中央 (0,0),其他页面在屏幕外
for (var i = 0; i < _skillTreePageUIs.Count; i++)
{
_skillTreePageUIs[i].GetComponent<RectTransform>().anchoredPosition =
new Vector2(_pageWidth * (i - _currentPageIndex), 0);
}
// 确保当前页面在层级的最上方,防止被其他未隐藏的页面遮挡
if (_skillTreePageUIs.Count > 0)
{
_skillTreePageUIs[_currentPageIndex].transform.SetAsLastSibling();
var pageUI = Instantiate(skillTreePageUIPrefab, skillTreePageUIParent);
pageUI.GenerateAndLayoutAllSkillTrees(page);
skillTreePages.Add(pageUI);
pageUI.gameObject.SetActive(false);
}
FocusedSkillTreePage.gameObject.SetActive(true);
}
// 实现翻页动画
public override void TickUI()
public void OnLeft()
{
base.TickUI();
if (_isAnimating)
{
_animationProgress += Time.deltaTime / _animationDuration;
_animationProgress = Mathf.Clamp01(_animationProgress);
// 计算当前页面的目标位置:从 0 移动到 -_pageWidth * _animationDirection
var currentX = Mathf.Lerp(0, -_pageWidth * _animationDirection, _animationProgress);
_currentMovingPage.GetComponent<RectTransform>().anchoredPosition = new Vector2(currentX, 0);
// 计算目标页面的目标位置:从 _pageWidth * _animationDirection 移动到 0
var targetX = Mathf.Lerp(_pageWidth * _animationDirection, 0, _animationProgress);
_targetMovingPage.GetComponent<RectTransform>().anchoredPosition = new Vector2(targetX, 0);
if (_animationProgress >= 1f)
{
// 动画结束
_isAnimating = false;
_animationDirection = 0; // 重置动画方向
// 更新当前页面索引 (使用 _targetMovingPage 的索引)
_currentPageIndex = _skillTreePageUIs.IndexOf(_targetMovingPage);
// 确保新的当前页面在正确位置 (0,0)
_targetMovingPage.GetComponent<RectTransform>().anchoredPosition = Vector2.zero;
// 隐藏所有非当前页面,将其放置到屏幕外,减少渲染负担
for (var i = 0; i < _skillTreePageUIs.Count; i++)
{
if (i != _currentPageIndex)
{
// 将非当前页面放置到正确的位置,以便下次翻页时能从正确位置移入
_skillTreePageUIs[i].GetComponent<RectTransform>().anchoredPosition =
new Vector2(_pageWidth * (i - _currentPageIndex), 0);
}
}
// 确保新的当前页面在层级最上方
_skillTreePageUIs[_currentPageIndex].transform.SetAsLastSibling();
}
}
FocusedSkillTreePage.gameObject.SetActive(false);
Index--;
FocusedSkillTreePage.gameObject.SetActive(true);
}
/// <summary>
/// 控制切换到上一页。
/// </summary>
public void TurnPageLeft()
public void OnRight()
{
if (_isAnimating) return; // 动画进行中,忽略新的翻页请求
// 检查是否已经到达第一页
if (_currentPageIndex <= 0)
{
Debug.Log("SkillTreeUI: Already at the first page.");
return;
}
_animationDirection = -1; // 左翻页方向
StartPageTurnAnimation(_currentPageIndex - 1);
FocusedSkillTreePage.gameObject.SetActive(false);
Index++;
FocusedSkillTreePage.gameObject.SetActive(true);
}
/// <summary>
/// 控制切换到下一页。
/// </summary>
public void TurnPageRight()
{
if (_isAnimating) return; // 动画进行中,忽略新的翻页请求
// 检查是否已经到达最后一页
if (_currentPageIndex >= _skillTreePageUIs.Count - 1)
{
Debug.Log("SkillTreeUI: Already at the last page.");
return;
}
_animationDirection = 1; // 右翻页方向
StartPageTurnAnimation(_currentPageIndex + 1);
}
/// <summary>
/// 启动翻页动画的内部方法。
/// </summary>
/// <param name="targetPageIndex">目标页面的索引。</param>
private void StartPageTurnAnimation(int targetPageIndex)
{
_isAnimating = true;
_animationProgress = 0f; // 重置动画进度
_currentMovingPage = _skillTreePageUIs[_currentPageIndex];
_targetMovingPage = _skillTreePageUIs[targetPageIndex];
// 确保参与动画的两个页面都是激活状态
_currentMovingPage.gameObject.SetActive(true);
_targetMovingPage.gameObject.SetActive(true);
// 将目标页面初始位置设置在当前页的左侧或右侧
// 例如,如果向右翻页 (_animationDirection = 1),目标页从右边 (_pageWidth) 滑入
_targetMovingPage.GetComponent<RectTransform>().anchoredPosition =
new Vector2(_pageWidth * _animationDirection, 0);
// 确保目标页面在当前页面之上,以便在滑动时覆盖旧页面
_targetMovingPage.transform.SetAsLastSibling();
}
// 可以添加一个 GoToPage(int pageIndex) 方法来直接跳转到某一页
// 这里为了简化,只实现了左右翻页
}
}

View File

@@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using Data;
using Managers;
using UnityEngine;
namespace UI
{
public class StartPlayUI:FullScreenUI
{
public Transform iconList;
private int _currentIndex;
public int CurrentDimensionIndex
{
get => _currentIndex;
set
{
_currentIndex = value;
if (_currentIndex < 0)
{
_currentIndex += mapViewUIList.Count;
}
else if (_currentIndex >= mapViewUIList.Count)
{
_currentIndex %= mapViewUIList.Count;
}
}
}
public DimensionDef CurrentDimension => mapViewUIList[CurrentDimensionIndex].DimensionDefine;
public MapViewUI mapViewUIPrefab;
public List<MapViewUI> mapViewUIList = new List<MapViewUI>();
private void Start()
{
var dimensionDefs = DefineManager.Instance.QueryDefinesByType<DimensionDef>();
if(dimensionDefs==null)
return;
foreach (var d in dimensionDefs)
{
if(!d.canSelect)
continue;
var newObj=Instantiate(mapViewUIPrefab,iconList);
mapViewUIList.Add(newObj);
newObj.Init(d);
newObj.gameObject.SetActive(false);
newObj.transform.localPosition = Vector3.zero;
}
UpdateUI();
}
public void UpdateUI()
{
if (mapViewUIList == null)
return;
for (var i = 0; i < mapViewUIList.Count; i++)
{
mapViewUIList[i].gameObject.SetActive(i == CurrentDimensionIndex);
}
}
public void OnStartGame()
{
Program.Instance.StartPlayGame(CurrentDimension);
}
public void OnLeft()
{
CurrentDimensionIndex-=1;
UpdateUI();
}
public void OnRight()
{
CurrentDimensionIndex+=1;
UpdateUI();
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 39c9e270bf6946c0a9fc963ce048ee32
timeCreated: 1759334581

View File

@@ -0,0 +1,55 @@
using System;
using TMPro;
using UnityEngine;
namespace UI
{
public class WaitStartUI:MonoBehaviour
{
public TMP_Text text;
public string baseText;
public int dotCount = 3;
public int dotCounter = 0;
public float updateTime = 1;
private float timer = 0;
private void Start()
{
if(string.IsNullOrEmpty(baseText))
{
baseText = text.text;
}
else
{
text.text = baseText;
}
}
private void Update()
{
timer+=Time.deltaTime;
if (this.timer >= updateTime)
{
timer-=updateTime;
var dots = "";
for (var i = 0; i < dotCounter; i++)
{
dots += ".";
}
text.text = baseText + dots;
dotCounter += 1;
if (dotCounter > dotCount)
{
dotCounter = 0;
}
}
}
public void Hide()
{
gameObject.SetActive(false);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8526602f31724cc39ebdf089255189da
timeCreated: 1759368694