在 如何设计一个背包的控制逻辑 中我们实现了背包的所有处理逻辑下面需要绑定 ui 了,ui 上逻辑就比较简单了,我们需要简单的做一个数据流程图去思考一下怎么实现数据的流转。
我选择将 ui 层分为 4 块功能:
- BagContainer 这个是需要被导出为自定义 node 的节点,用来标记 bag 显示在哪里,随后 bag 会自动显示在目标位置,并适配其宽高
- BagInnerContainer 处理具体的背包逻辑
- ItemSlot 用来实现显示 ItemSlot 的功能
- MouseSlot 用来显示被鼠标拿住的物品,这个阶段先不实现
注意 BagContainer 和 BagInnerContainer 本身没有较大的界限,我个人比较喜欢分开但是实际上,是实现在一起会更方便一点。
背包功能逻辑
然后我们就可以简单的画出上面这个图,对于整个 GUI 组件而言,对于 BagContainer 并不关心里面存放的内容,他只需要设定背包的大小(从 godot 编辑器里面拖拽),设定行、列,随后设置一个全局唯一的 BagName 即可。
BagInnerContainer 是一个活比较重的组件,他需要初始化 ItemSlot,设定宽高等等。
最后是 ItemSlot 他是一个核心组件,用于显示背包的数量和图标,当然未来还有拖拽事件,所以还需要绑定一些复杂的事件。
扩展实现 BagResourceManager
public partial class BagResourceManager
{
BagResourceManager()
{
// 方便做测试,这里的注册功能应该通过 csv 或是 excel 或是 sqlite 的方式自动读取
Init();
}
private static BagResourceManager _bagResourceManager;
public static BagResourceManager Instance => _bagResourceManager ??= new BagResourceManager();
public BagItemResource GetItem(string name) => Items[name];
public readonly Dictionary<string, BagItemResource> Items = new();
// 给一个 init 函数方便做测试
public void Init()
{
Items.Add("banana",
new BagItemResource
{
Name = "banana", Icon = "res://assets/pictures/banana.png", MaxCount = 100
});
}
}
扩展 BagItem
为了方便使用,我们在 BagItem 中扩展一个字段用来获取他对应的 resource 数据, 他还可以帮助我们获取目标的 Texture,可能算一种变异的 享元模式
public class BagItem
{
// ...略去内容
// 扩展了一个对外方法
public BagItemResource Resource => BagResourceManager.Instance.GetItem(Name);
// ...略去内容
}
实际上这可能并不是一个好的选择,因为当你可以获得 BagItem 的时候,你已经可以通过 Name 变相的从 BagResourceManager 中获取 Item 的元信息了,这样做可以完全的分离相互间的耦合性。
实现 ItemSlot
我们从下层逐渐向上实现是比较好的组织方式,首先 ItemSlot 的组织架构大致如下:
这里我只修改了CountLabel 的名字其余都一致样子大致是这样的,会有一个默认的 margin。
下面我需要编写对应的代码:
public partial class ItemSlot : Control
{
[Export] public Label CountLabel;
[Export] public TextureRect Icon { get; set; }
private int _x;
private int _y;
private string _bagName;
// 方便上级初始化的时候添加数据,我还没找到合适的 constructor 方式创建。
public void AddItem(string bagName, int x, int y)
{
_x = x;
_y = y;
_bagName = bagName;
}
// 方便后续使用
private BagItem Item => BagController.Instance.GetBagItem(_bagName, _x, _y);
private void ClearRender()
{
Icon.Texture = null;
CountLabel.Text = "";
}
public override void _Process(double delta)
{
// 保底策略,当 Item 不可用的时候需要清除基础的渲染
if (Item.IsEmpty || Item is null)
{
ClearRender();
return;
}
CountLabel.Text = Item.Count.ToString();
Icon.Texture = Item.Resource.Texture;
}
}
这样就完成 ItemSlot 的基本逻辑,下面要通过 BagInnerContainer 创建 ItemSlot
BagInnerContainer
BagInnerContainer 他的 UI 和代码逻辑都很简单的,具体的组件只有三个用来控制宽度的外层、滚动的 scroll container、还有 Grid:
我们只需要简单的 scroll container 设置为下面这样就可以了,这样当高度不够的时候他就会转换为滚动来显示 ui:
下面我们要开始编写代码部分:
public partial class BagInnerContainer : Control
{
public string BagName;
[Export] public GridContainer Grid;
public BagContainer Parent;
public void InitBagGrid()
{
// 获取当前 bag 的信息,并向 grid 里面添加对应的格子
if (BagController.Instance.Bags.ContainsKey(BagName))
{
var bag = BagController.Instance.Bags[BagName];
Grid.Columns = bag.ColumnCount;
// 计算宽度
var width = Size.X;
// 还需要减去边距
// 还需要减去 scroll 宽度
var cellWidth = width / bag.ColumnCount;
var pack = GD.Load<PackedScene>("res://<path to ItemSlot>");
for (int i = 0; i < bag.ColumnCount; i++)
{
for (int j = 0; j < bag.RowCount; j++)
{
// 计算宽度
var cellHeight = cellWidth;
var cell = pack.Instantiate<ItemSlot>();
cell.Name = $"ItemSlot_{i}_{j}";
cell.CustomMinimumSize = new Vector2(cellWidth, cellHeight);
cell.AddItem(BagName, i, j);
Grid.AddChild(cell);
}
}
}
}
}
我们需要对外暴露一个 InitBagGrid 的方法,并向内添加对应的格子这里实现上并不难做,初始化以后调用 AddItem 即可在内部已经实现了。
BagContainer
BagContainer 组件虽然逻辑复杂一点,但是组件上反而是更简单了,他只有用来标记大小的 ColorRect 组件。
随便拖拽一个大小即可。
下面是代码部分:
public partial class BagContainer : Control
{
/// <summary>
/// 背包名称,全局唯一
/// </summary>
[Export] public string BagName = "Default";
[Export] public int ColumnCount = 5;
[Export] public int RowCount = 40;
/// <summary>
/// 是否在退出时从控制器中移除,用来实现一次性的背包或者容器
/// </summary>
[Export] public bool ExitFreeFromController = true;
/// <summary>
/// 是否在 Ready 时初始化背包
/// </summary>
[Export] public bool InitOnReady;
private BagInnerContainer _innerContainer;
private const string BAG_CONTAINER_PATH = "<path to BagInnerContainer>";
public async void InitBag()
{
// 创建背包
BagController.Instance.AddBag(BagName, ColumnCount, RowCount);
var bagInnerContainerPack = GD.Load<PackedScene>(BAG_CONTAINER_PATH);
_innerContainer = bagInnerContainerPack.Instantiate<BagInnerContainer>();
_innerContainer.BagName = BagName;
_innerContainer.Parent = this;
// 延迟确保 size 正确
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame);
_innerContainer.Size = Size;
AddChild(_innerContainer);
_innerContainer.InitBagGrid();
}
public override void _Ready()
{
if (InitOnReady)
{
// 如果勾选了自动初始化就直接初始化,如果没有说明需要手动初始化
InitBag();
}
}
public override void _ExitTree()
{
if (ExitFreeFromController)
{
BagController.Instance.RemoveBag(BagName);
}
}
public override void _Process(double delta)
{
if (_innerContainer is not null)
{
_innerContainer.GlobalPosition = GlobalPosition;
}
}
}
实验一下
下面我们简单的建一个背包实验一下:
public partial class BagNode : Control
{
[Export] public string BagName = "Test";
[Export] public Label BagNameLabel;
[Export] public BagContainer BagContainer;
public void OnClose()
{
Visible = false;
}
public override void _Ready()
{
BagNameLabel.Text = BagName;
BagContainer.BagName = BagName;
BagContainer.InitBag();
BagController.Instance.AddItemToBag("Test", "banana", 10);
}
}
效果还不错,下面需要实现拖拽系统。
本文标题:实现背包GUI
永久链接:https://iceprosurface.com/godot/bag-system/gui/
作者授权:本文由 icepro 原创编译并授权刊载发布。
版权声明:本文使用「署名-非商业性使用-相同方式共享 4.0 国际」创作共享协议,转载或使用请遵守署名协议。