文章目录
- 先来看看最终效果
- 前言
- 配置商店系统数据
- 创建另一个NPC
- 绘制商店UI
- 控制商店开关
- 列出商品
- 添加和删除物品功能
- 添加商品到购物车
- 购买商品
- 购物车删除物品
- 商店预览效果
- 购买和出售切换
- 出售功能
- 保存商店数据
- 快捷栏物品切换和使用
- 完结
先来看看最终效果
前言
本期也是最好一期,紧跟着上期,继续来完善我们的库存系统,实现商店系统和快捷栏的切换功能
配置商店系统数据
新增ShopItemList ,配置商店物品列表信息
[CreateAssetMenu(menuName = "商店系统/商店物品列表")]
public class ShopItemList : ScriptableObject
{
// 商店物品列表
[SerializeField] private List<ShopInventoryItem> _items;
// 商店允许的最大金币数
[SerializeField] private int _maxAllowedGold;
// 商店出售物品的加价比例
[SerializeField] private float _sellMarkUp;
// 商店购买物品的加价比例
[SerializeField] private float _buyMarkUp;
public List<ShopInventoryItem> Items => _items;
public int MaxAllowedGold => _maxAllowedGold;
public float SellMarkUp => _sellMarkUp;
public float BuyMarkUp => _buyMarkUp;
// 商店库存物品结构体
[System.Serializable]
public struct ShopInventoryItem
{
// 物品数据
public InventoryItemData ItemData;
// 物品数量
public int Amount;
}
}
新增ItemSlot,物品槽的抽象基类
public abstract class ItemSlot : ISerializationCallbackReceiver
{
// 对数据的引用,使用[NonSerialized]属性表示在序列化时不包含该字段
[NonSerialized] protected InventoryItemData itemData;
// 用于序列化的字段
[SerializeField] protected int _itemID = -1;
[SerializeField] protected int stackSize;
// 对外暴露的物品数据和堆叠大小属性
public InventoryItemData ItemData => itemData;
public int StackSize => stackSize;
// 清空物品槽
public void ClearSlot()
{
itemData = null;
_itemID = -1;
stackSize = 0;
}
// 分配一个物品给物品槽
public void AssignItem(InventorySlot invSlot)
{
if (itemData == invSlot.ItemData)
{
AddToStack(invSlot.StackSize);
}
else
{
itemData = invSlot.ItemData;
_itemID = itemData.ID;
stackSize = 0;
AddToStack(invSlot.StackSize);
}
}
// 将当前背包的物品信息复制到克隆的背包系统中
public void AssignItem(InventoryItemData data, int amount){
if (itemData == data) AddToStack(amount);
else
{
itemData = data;
_itemID = data.ID;
stackSize = 0;
AddToStack(amount);
}
}
// 将物品堆叠数量增加一定数量
public void AddToStack(int amount)
{
stackSize += amount;
}
// 从物品堆叠中移除一定数量的物品
public void RemoveFromStack(int amount)
{
stackSize -= amount;
}
// 在序列化之前的回调函数
public void OnBeforeSerialize()
{
// 这里可以添加在序列化之前需要处理的逻辑
}
// 在反序列化之后的回调函数
public void OnAfterDeserialize()
{
if (_itemID == -1) return;
var db = Resources.Load<Database>("Database");
itemData = db.GetItem(_itemID);
}
}
重构InventorySlot,继承前面的ItemSlot物品槽的抽象基类
//用于表示背包系统中的一个物品槽位
[System.Serializable]
public class InventorySlot : ItemSlot
{
// 构造函数,用于创建一个带有物品和堆叠数量的槽
public InventorySlot(InventoryItemData source, int amount)
{
itemData = source;
_itemID = itemData.ID;
stackSize = amount;
}
// 默认构造函数,用于创建一个空槽
public InventorySlot()
{
ClearSlot();
}
// 检查是否有足够的堆叠空间,并返回剩余的可堆叠数量
public bool EnoughRoomLeftInStack(int amountToAdd, out int amountRemaining)
{
amountRemaining = itemData.MaxStackSize - stackSize; // 计算剩余的可堆叠数量
return EnoughRoomLeftInStack(amountToAdd);
}
// 检查是否有足够的堆叠空间
public bool EnoughRoomLeftInStack(int amountToAdd)
{
// 如果当前堆叠数量加上要添加的数量小于等于最大堆叠数量
if (itemData == null || itemData != null && stackSize + amountToAdd <= itemData.MaxStackSize)
return true;
else
return false;
}
//更新库存槽的数据
public void UpdateInventorySlot(InventoryItemData data, int amount)
{
itemData = data;
_itemID = itemData.ID;// 通过传入的物品数据获取物品ID
stackSize = amount;
}
/// <summary>
/// 将物品堆叠一分为二。
/// </summary>
/// <param name="splitStack">拆分后的新物品堆叠。</param>
/// <returns>是否成功拆分。</returns>
public bool SplitStack(out InventorySlot splitStack)
{
// 如果物品堆叠为空或堆叠数量小于1,则无法拆分
if (stackSize < 1)
{
splitStack = null;
return false;
}
// 计算需要拆分出的物品堆叠数量(约为原始堆叠数量的一半)
int halfStack = Mathf.RoundToInt(stackSize / 2f);
// 从原始堆叠中移除一部分物品
RemoveFromStack(halfStack);
// 创建一个新的物品堆叠作为拆分后的一半
splitStack = new InventorySlot(itemData, halfStack);
return true;
}
}
新增ShopSlot,基础ItemSlot,对槽位进行了初始化操作
[System.Serializable]
public class ShopSlot: ItemSlot
{
public ShopSlot()
{
ClearSlot();
}
}
新增ShopSystem,定义了一个商店系统类,包含了商店的物品槽列表、金币数量、购买和出售的加价率等属性,以及相应的初始化和设置方法。这些属性和方法可以用于管理商店的状态和行为。
[System.Serializable]
public class ShopSystem
{
// 商店的物品槽列表
[SerializeField] private List<ShopSlot> _shopInventory;
public List<ShopSlot> ShopInventory => _shopInventory;
// 商店的可用金币数量
[SerializeField] private int _availableGold;
public int AvailableGold => _availableGold;
// 商品的购买加价率和出售加价率
[SerializeField] private float _buyMarkUp;
public float BuyMarkUp => _buyMarkUp;
[SerializeField] private float _sellMarkUp;
public float S => _sellMarkUp;
// 构造方法,初始化商店的大小、金币数量以及购买和出售的加价率
public ShopSystem(int size, int gold, float buyMarkUp, float sellMarkUp)
{
_availableGold = gold;
_buyMarkUp = buyMarkUp;
_sellMarkUp = sellMarkUp;
SetShopSize(size);
}
// 设置商店的大小
private void SetShopSize(int size)
{
_shopInventory = new List<ShopSlot>(size);// 创建一个指定大小的物品槽列表
for (int i = 0; i < size; i++)
{
_shopInventory.Add(new ShopSlot());// 将新创建的物品槽添加到列表中
}
}
// 向商店添加物品
public void AddToShop(InventoryItemData data, int amount)
{
if (ContainsItem(data, out ShopSlot shopSlot))
{
shopSlot.AddToStack(amount); // 如果商店已经存在相同的物品,则将物品堆叠数量增加
}
else
{
var freeSlot = GetFreeSlot(); // 获取一个空闲的物品槽
freeSlot.AssignItem(data, amount); // 在空闲的物品槽中添加新的物品
}
}
// 获取一个空闲的物品槽
private ShopSlot GetFreeSlot()
{
var freeSlot = _shopInventory.FirstOrDefault(i => i.ItemData == null); // 查找第一个物品槽中没有物品的槽
if (freeSlot == null)
{
freeSlot = new ShopSlot(); // 如果没有空闲槽,则创建一个新的物品槽
_shopInventory.Add(freeSlot); // 将新创建的物品槽添加到列表中
}
return freeSlot;
}
// 检查商店是否已经存在某个物品,并返回对应的物品槽
public bool ContainsItem(InventoryItemData itemToAdd, out ShopSlot shopSlot)
{
shopSlot = _shopInventory.Find(i => i.ItemData == itemToAdd); // 查找物品槽列表中是否存在相同的物品
return shopSlot != null;
}
}
新增ShopKeeper,定义了一个商店管理员类,该类包含了商店所持有的物品列表、商店系统以及与玩家交互的方法和事件。你需要根据具体需求来编写交互逻辑和结束交互的实现。
// 需要附加UniqueID组件方可使用
[RequireComponent(typeof(UniqueID))]
public class ShopKeeper : MonoBehaviour, IInteractable
{
// 商店所持有的物品列表
[SerializeField] private ShopItemList _shopItemsHeld;
[SerializeField] private ShopSystem _shopSystem;
public static UnityAction<ShopSystem, PlayerInventoryHolder> OnShopWindowRequested;
private void Awake(){
_shopSystem = new ShopSystem(_shopItemsHeld.Items.Count, _shopItemsHeld.MaxAllowedGold, _shopItemsHeld.BuyMarkUp, _shopItemsHeld.SellMarkUp);
foreach (var item in _shopItemsHeld.Items){
//打印测试
Debug.Log($"{item.ItemData.DisplayName}:{item.Amount}");
_shopSystem.AddToShop(item.ItemData, item.Amount);//向商店添加物品
}
}
// 当交互完成时触发的事件
public UnityAction<IInteractable> OnInteractionComplete { get; set; }
// 当与玩家进行交互时调用
public void Interact(Interactor interactor, out bool interactSuccessful)
{
var playerInv = interactor.GetComponent<PlayerInventoryHolder>();
if (playerInv != null)
{
OnShopWindowRequested?.Invoke(_shopSystem, playerInv);
interactSuccessful = true;
}
else
{
interactSuccessful = false;
Debug.LogError("没找到PlayerInventoryHolder");
}
}
// 结束与玩家的交互
public void EndInteraction()
{
//
}
}
运行查看是否还正常
设置物品价格
配置店铺数据,配置对应的价格比例,100的物品我们只能卖75,因为店铺要收25%的利润
添加商品库存
修改UniqueID,可以通过菜单初始化ID
[ContextMenu("生成ID")]
private void Generate()
{
//。。。
}
配置NPC脚本,记得前面生成ID方法生成ID
打印
界面数据
创建另一个NPC
工具NPC
数据
绘制商店UI
绘制商店UI,这里我就不多介绍了,按自己喜欢绘制就行
控制商店开关
新增ShopKeeperDisplay 和ShoppingCartItemUI脚本
public class ShopKeeperDisplay : MonoBehaviour
{}
public class ShoppingCartItemUI: MonoBehaviour
{}
挂载脚本
模仿之前的InventoryUIController,新增UIController,控制商店窗口的显示隐藏
public class UIController : MonoBehaviour
{
[SerializeField] private ShopKeeperDisplay _shopKeeperDisplay;
private void Awake() {
_shopKeeperDisplay.gameObject.SetActive(false);
}
void Update()
{
//activeInHierarchy 检查该对象是否处于活动状态
if (_shopKeeperDisplay.gameObject.activeInHierarchy && Input.GetKeyDown(KeyCode.Escape))
{
// 按下 ESC 键关闭物品界面
_shopKeeperDisplay.gameObject.SetActive(false);
}
}
// 注册事件监听器,在脚本启用时调用
private void OnEnable()
{
ShopKeeper.OnShopWindowRequested += DisplayShopWindow;
}
// 取消事件监听器,在脚本禁用时调用
private void OnDisable()
{
ShopKeeper.OnShopWindowRequested -= DisplayShopWindow;
}
// 显示商店窗口的方法,响应ShopKeeper.OnShopWindowRequested事件
private void DisplayShopWindow(ShopSystem shopSystem, PlayerInventoryHolder playerInventory)
{
_shopKeeperDisplay.gameObject.SetActive(true);
}
}
挂载脚本UI
新增NPC,添加配置,记得图层进行修改,因为前面我配置打开的图层是Box,所以这里偷懒也用Box,你也可以换成别的(可以Inventory图层比较通用)
效果,打开关闭商店
列出商品
修改ShopSlotUI ,控制商品槽UI
public class ShopSlotUI : MonoBehaviour
{
[Header("商品图标组件")]
[SerializeField] private Image _itemSprite;
[Header("商品名称的组件")]
[SerializeField] private TextMeshProUGUI _itemName;
[Header("商品数量的组件")]
[SerializeField] private TextMeshProUGUI _itemCount;
[Header("该UI对应的商店物品槽")]
[SerializeField] private ShopSlot _assignedItemSlot;
public ShopSlot AssignedItemSlot => _assignedItemSlot;
[Header("将商品添加到购物车的按钮")]
[SerializeField] private Button _addItemToCartButton;
[Header("从购物车中移除商品的按钮")]
[SerializeField] private Button _removeItemFromCartButton;
// 声明关联的商店窗口显示对象和价格调整比例
public ShopKeeperDisplay ParentDisplay { get; private set; }
public float Markup { get; private set; }//加价比例
private void Awake()
{
// 初始化UI元素
_itemSprite.sprite = null;
_itemSprite.preserveAspect = true;
_itemSprite.color = Color.clear;
_itemName.text = "";
_itemCount.text = "";
// 添加按钮点击监听事件
_addItemToCartButton?.onClick.AddListener(AddItemToCart);
_removeItemFromCartButton?.onClick.AddListener(RemoveItemFromCart);
// 获取关联的商店窗口显示对象
ParentDisplay = transform.parent.GetComponentInParent<ShopKeeperDisplay>();
}
// 初始化商店物品槽UI
public void Init(ShopSlot slot, float markup)
{
_assignedItemSlot = slot;
Markup = markup;
UpdateUISlot();
}
// 更新商店物品槽UI的显示
private void UpdateUISlot()
{
if (_assignedItemSlot.ItemData != null)
{
// 显示物品图标、名称、数量和价格
_itemSprite.sprite = _assignedItemSlot.ItemData.Icon;
_itemSprite.color = Color.white;
_itemCount.text = _assignedItemSlot.StackSize.ToString();
_itemName.text = $"{_assignedItemSlot.ItemData.DisplayName} - {_assignedItemSlot.ItemData.GoldValue}G";
}
else
{
// 如果物品为空,则清空显示
_itemSprite.sprite = null;
_itemSprite.color = Color.clear;
_itemName.text = "";
_itemCount.text = "";
}
}
// 从购物车中移除物品
private void RemoveItemFromCart()
{
Debug.Log("从购物车中移除物品");
}
// 将物品添加到购物车中
private void AddItemToCart()
{
Debug.Log("向购物车中添加商品");
}
}
挂载脚本,配置参数
修改ShopKeeperDisplay
public class ShopKeeperDisplay : MonoBehaviour
{
[Header("商店槽预制体")]
[SerializeField] private ShopSlotUI _shopSlotPrefab;
[Header("购物车物品预制体")]
[SerializeField] private ShoppingCartItemUI _shoppingCartItemPrefab;
[Header("购买标签按钮")]
[SerializeField] private Button _buyTabButton;
[Header("出售标签按钮")]
[SerializeField] private Button _sellTabButton;
[Space]
[Header("购物车")]
[Header("购物车总价文本")]
[SerializeField] private TextMeshProUGUI _basketTotalText;
[Header("玩家金币数文本")]
[SerializeField] private TextMeshProUGUI _playerGoldText;
[Header("商店金币数文本")]
[SerializeField] private TextMeshProUGUI _shopGoldText;
[Header("购买按钮")]
[SerializeField] private Button _buyButton;
[Header("购买按钮文本")]
[SerializeField] private TextMeshProUGUI _buyButtonText;
[Space]
[Header("物品预览区域")]
[Header("物品预览图像")]
[SerializeField] private Image _itemPreviewSprite;
[Header("物品名称文本")]
[SerializeField] private TextMeshProUGUI _itemPreviewName;
[Header("物品描述文本")]
[SerializeField] private TextMeshProUGUI _itemPreviewDescription;
[Header("物品列表内容面板")]
[SerializeField] private GameObject _itemListContentPanel;
[Header("购物车物品列表内容面板")]
[SerializeField] private GameObject _shoppingCartContentPanel;
private int _basketTotal;// 购物车总价格
private ShopSystem _shopSystem;// 商店系统组件
private PlayerInventoryHolder _playerInventoryHolder;
private Dictionary<InventoryItemData, int> _shoppingCart = new Dictionary<InventoryItemData, int>();// 购物车字典
private Dictionary<InventoryItemData, ShoppingCartItemUI> _shoppingCartUI = new Dictionary<InventoryItemData, ShoppingCartItemUI>();// 显示购物车物品信息的字典
public void DisplayShopWindow(ShopSystem shopSystem, PlayerInventoryHolder playerInventoryHolder)
{
_shopSystem = shopSystem;
_playerInventoryHolder = playerInventoryHolder;
RefreshDisplay();
}
private void RefreshDisplay()
{
ClearSlots();
_basketTotalText.enabled = false;
_buyButton.gameObject.SetActive(false);
_basketTotal = 0;
_playerGoldText.text = $"PLayer Gold:{_playerInventoryHolder.PrimaryInventorySystem.Gold}";
_shopGoldText.text = $"Shop Gold:{_shopSystem.AvailableGold}";
DisplayShopInventory();
}
private void ClearSlots()
{
_shoppingCart = new Dictionary<InventoryItemData, int>();
_shoppingCartUI = new Dictionary<InventoryItemData, ShoppingCartItemUI>();
// 清空物品列表内容面板和购物车物品列表内容面板中的所有子物体
foreach (var item in _itemListContentPanel.transform.Cast<Transform>())
{
Destroy(item.gameObject);
}
foreach (var item in _shoppingCartContentPanel.transform.Cast<Transform>())
{
Destroy(item.gameObject);
}
}
private void DisplayShopInventory()
{
// 遍历商店物品列表,显示每个物品槽
foreach (var item in _shopSystem.ShopInventory)
{
if (item.ItemData == null) continue;
var shopSlot = Instantiate(_shopSlotPrefab, _itemListContentPanel.transform);
shopSlot.Init(item, _shopSystem.BuyMarkUp);
}
}
}
修改InventorySystem
[SerializeField] private int _gold;
public int Gold => _gold;
//用于创建一个具有指定大小的背包系统
public InventorySystem(int size)
{
_gold = 0;
CreateInventory(size);
}
public InventorySystem(int size, int gold)
{
_gold = gold;
CreateInventory(size);
}
private void CreateInventory(int size)
{
//根据指定的大小创建对应数量的空物品槽位
inventorySlots = new List<InventorySlot>(size);
for (int i = 0; i < size; i++)
{
inventorySlots.Add(new InventorySlot());
}
}
修改UIController
//显示商店窗口的方法,响应ShopKeeper.OnShopWindowRequested事件
private void DisplayShopWindow(ShopSystem shopSystem, PlayerInventoryHolder playerInventory)
{
_shopKeeperDisplay.gameObject.SetActive(true);
_shopKeeperDisplay.DisplayShopWindow(shopSystem, playerInventory);
}
挂载脚本,配置参数
效果
添加和删除物品功能
修改ShopSlotUI
// 记录临时数量,初始化为物品槽的堆叠数量
private int _tempAmount;
public void Init(ShopSlot slot, float markup)
{
_assignedItemSlot = slot;
Markup = markup;
_tempAmount = slot.StackSize;// 将临时数量初始化为物品槽的堆叠数量
UpdateUISlot();
}
// 从购物车中移除物品
private void RemoveItemFromCart()
{
Debug.Log("从购物车中移除物品");
// 如果临时数量等于物品槽堆叠数量,则不做任何操作,直接返回
if (_tempAmount ==_assignedItemSlot.StackSize)return;
_tempAmount++;
ParentDisplay.RemoveItemFromCart(this);// 从购物车中移除该物品槽对应的物品
_itemCount.text =_tempAmount.ToString();// 更新物品数量的文本显示
}
// 将物品添加到购物车中
private void AddItemToCart()
{
Debug.Log("向购物车中添加商品");
// 如果临时数量小于等于 0,则不做任何操作,直接返回
if (_tempAmount <= 0) return;
_tempAmount--;
ParentDisplay.AddItemToCart(this);// 将该物品槽对应的物品添加到购物车中
_itemCount.text = _tempAmount.ToString();// 更新物品数量的文本显示
}
修改ShopKeeperDisplay
// 从购物车中移除该物品槽对应的物品
public void RemoveItemFromCart(ShopSlotUI shopSlotUI)
{
//
}
// 将该物品槽对应的物品添加到购物车中
public void AddItemToCart(ShopSlotUI shopSlotUI)
{
//
}
效果
添加商品到购物车
修改ShoppingCartItemUI,控制购物车插槽的UI
public class ShoppingCartItemUI : MonoBehaviour
{
[SerializeField] private TextMeshProUGUI _itemText;
//设置物品信息文本
public void SetItemText(string newString)
{
_itemText.text = newString;
}
}
挂载脚本
修改ShopKeeperDisplay
private bool _isSelling;// 是否为出售模式
// 将该物品槽对应的物品添加到购物车中
public void AddItemToCart(ShopSlotUI shopSlotUI)
{
// 获取物品数据
var data = shopSlotUI.AssignedItemSlot.ItemData;
// 更新物品预览
UpdateItemPreview(shopSlotUI);
var price = GetModifiedPrice(data, 1, shopSlotUI.MarkUp);// 获取修改后的价格
if (_shoppingCart.ContainsKey(data))
{
_shoppingCart[data]++;
var newString = $"{data.DisplayName} ({price}G)x{_shoppingCart[data]}";// 如果购物车中已经有该物品,增加其数量
_shoppingCartUI[data].SetItemText(newString);// 更新显示购物车物品信息的文本组件
}
else
{
_shoppingCart.Add(data, 1);// 否则,在购物车中添加该物品
var shoppingCartTextObj = Instantiate(_shoppingCartItemPrefab, _shoppingCartContentPanel.transform);// 创建一个新的显示购物车物品信息的文本组件
var newString = $"{data.DisplayName} ({price}G)x1";
shoppingCartTextObj.SetItemText(newString);
_shoppingCartUI.Add(data, shoppingCartTextObj); // 添加到显示购物车物品信息的字典中
}
_basketTotal += price;
_basketTotalText.text = $"Total:{_basketTotal}G";// 更新购物车总价格的文本显示
if (_basketTotal > 0 && !_basketTotalText.IsActive())
{
_basketTotalText.enabled = true;// 如果购物车总价大于 0 且文本组件是激活状态,启用文本组件
_buyButton.gameObject.SetActive(true);// 激活“购买”按钮
}
CheckCartVsAvailableGold();
}
// 更新物品预览
private void UpdateItemPreview(ShopSlotUI shopSlotUI) {}
/// <summary>
/// 获取修改后的价格
/// </summary>
/// <param name="data">物品数据</param>
/// <param name="amount">物品数量</param>
/// <param name="markUp">加价比例</param>
/// <returns>修改后的价格</returns>
private static int GetModifiedPrice(InventoryItemData data, int amount, float markUp)
{
// 计算基础价格,即物品单价乘以数量
var baseValue = data.GoldValue * amount;
// 计算调整后的价格,即基础价格加上涨价比例所得到的价格, 结果向下取整
return Mathf.FloorToInt(baseValue + baseValue * markUp);
}
//检查购物车与可用金币数之间的比较
private void CheckCartVsAvailableGold()
{
// 如果是出售模式,获取商店可用金币数,否则获取玩家背包的金币数
var goldToCheck = _isSelling ? _shopSystem.AvailableGold : _playerInventoryHolder.PrimaryInventorySystem.Gold;
// 如果购物车总价大于可用金币数,将文本颜色设置为红色,否则为白色
_basketTotalText.color = _basketTotal > goldToCheck ? Color.red : Color.white;
// 如果是出售模式或者玩家背包还有足够的空间,则直接返回
if (_isSelling || _playerInventoryHolder.PrimaryInventorySystem.CheckInventoryRemaining(_shoppingCart)) return;
//库存不足
_basketTotalText.text = "Insufficient inventory";
_basketTotalText.color = Color.red;
}
修改InventorySystem
/// <summary>
/// 检查背包剩余空间是否足够容纳购物车中的物品
/// </summary>
/// <param name="shoppingCart">购物车字典,键为物品数据,值为物品数量</param>
/// <returns>如果背包剩余空间足够容纳购物车中的物品,则返回true;否则返回false</returns>
public bool CheckInventoryRemaining(Dictionary<InventoryItemData, int> shoppingCart)
{
var clonedSystem = new InventorySystem(InventorySize);// 克隆一个背包系统
for (int i = 0; i < InventorySize; i++)
{
// 将当前背包的物品信息复制到克隆的背包系统中
clonedSystem.InventorySlots[i].AssignItem(InventorySlots[i].ItemData, InventorySlots[i].StackSize);
}
foreach (var kvp in shoppingCart)// 遍历购物车中的物品
{
for (int i = 0; i < kvp.Value; i++)// 遍历购物车中每种物品的数量
{
// 尝试将物品添加到克隆的背包系统中,如果添加失败(即背包空间不足),则返回false
if (!clonedSystem.AddToInventory(kvp.Key, 1)) return false;
}
}
// 所有物品都能成功添加到背包中,则返回true
return true;
}
效果,添加物品,购物车自动计算价格,如果玩家金币不足则会显示红色
购买商品
修改ShopSlotUI
// 更新商店物品槽UI的显示
private void UpdateUISlot()
{
if (_assignedItemSlot.ItemData != null)
{
// 。。。
// 获取调整后的物品价格
var modifiedPrice = ShopKeeperDisplay.GetModifiedPrice(_assignedItemSlot.ItemData, 1, MarkUp);
// 显示物品名称和价格
_itemName.text = $"{_assignedItemSlot.ItemData.DisplayName} - {modifiedPrice}G";
}
else
{
// 。。。
}
}
修改ShopKeeperDisplay
private void RefreshDisplay()
{
if (_buyButton != null)
{
// 设置购买按钮的文字和点击事件
_buyButtonText.text = _isSelling ? "Sell":"Buy";
_buyButton.onClick.RemoveAllListeners();
if (_isSelling) _buyButton.onClick.AddListener(SellItems);
else _buyButton.onClick.AddListener(BuyItems);
}
//。。。
}
//出售点击事件
private void SellItems()
{
//
}
//购买点击事件
private void BuyItems()
{
// 检查玩家是否有足够的金币购买所有物品
if (_playerInventoryHolder.PrimaryInventorySystem.Gold < _basketTotal){
Debug.Log("玩家金币不足");
return;
}
// 检查玩家背包是否有足够的空间存放购物车中的物品
if (!_playerInventoryHolder.PrimaryInventorySystem.CheckInventoryRemaining(_shoppingCart)){
Debug.Log("玩家背包位置不足");
return;
}
// 逐个购买购物车中的物品并添加到玩家背包中
foreach (var kvp in _shoppingCart)
{
_shopSystem.PurchaseItem(kvp.Key, kvp.Value);
for (int i = 0; i < kvp.Value; i++)
{
_playerInventoryHolder.PrimaryInventorySystem.AddToInventory(kvp.Key, 1);
}
}
// 商店获得金币,玩家失去金币
_shopSystem.GainGold(_basketTotal);
_playerInventoryHolder.PrimaryInventorySystem.SpendGold(_basketTotal);
// 刷新商店界面显示
RefreshDisplay();
}
修改ShopSystem
public void PurchaseItem(InventoryItemData data, int amount)
{
// 检查商店是否包含这种物品,如果不包含,直接返回
if (!ContainsItem(data, out ShopSlot slot)) return;
// 从物品槽中移除指定数量的物品
slot.RemoveFromStack(amount);
}
public void GainGold(int basketTotal)
{
// 将购物车总额添加到可用金币中
_availableGold += basketTotal;
}
修改InventorySystem
public void SpendGold(int basketTotal)
{
// 从玩家金币中扣除购物车总额
_gold -= basketTotal;
}
修改InventoryHolder
[SerializeField] protected int _gold;//玩家金币量
protected virtual void Awake()
{
SaveLoad.OnLoadGame += LoadInventory;// 注册加载游戏事件
//创建一个具有指定大小的背包系统
inventorySystem = new InventorySystem(inventorySize, _gold);
}
为了测试,先给玩家一些钱
效果,购买后玩家金额减少,商店金额增加
购物车删除物品
修改ShopKeeperDisplay,完善购物车删除物品事件
// 从购物车中移除该物品槽对应的物品
public void RemoveItemFromCart(ShopSlotUI shopSlotUI)
{
var data = shopSlotUI.AssignedItemSlot.ItemData;
var price = GetModifiedPrice(data, 1, shopSlotUI.MarkUp);// 获取物品的价格
if (_shoppingCart.ContainsKey(data))// 如果购物车中存在该物品
{
_shoppingCart[data]--;// 将物品数量减1
// 更新该物品UI的显示
var newString = $"{data.DisplayName}({price}G) x{_shoppingCart[data]}";
_shoppingCartUI[data].SetItemText(newString);
// 如果移除该物品后,该物品数量为0
if (_shoppingCart[data] <= 0)
{
// 从购物车和UI字典中移除该物品
_shoppingCart.Remove(data);
var tempobj =_shoppingCartUI[data].gameObject;// 获取该物品UI对象
_shoppingCartUI.Remove(data);
Destroy(tempobj);// 销毁该物品UI对象
}
}
_basketTotal -= price; // 减去该物品的价格
_basketTotalText.text = $"Total:{_basketTotal}G";// 更新购物车总价UI的文本显示
// 如果购物车总价小于等于0且购物车总价UI处于激活状态
if (_basketTotal <= 0 && _basketTotalText.IsActive())
{
_basketTotalText.enabled = false;// 关闭购物车总价UI的显示
_buyButton.gameObject.SetActive(false);// 隐藏购买按钮
ClearItemPreview();// 清空物品预览
return;
}
// 检查购物车总价和玩家拥有的金币数是否匹配
CheckCartVsAvailableGold();
}
// 清空物品预览
private void ClearItemPreview()
{
//
}
效果
商店预览效果
修改ShopKeeperDisplay
private void RefreshDisplay()
{
//...
ClearItemPreview();// 清空物品预览
if (_isSelling) DisplayPlayerInventory(); // 如果是卖家,显示玩家的库存
else DisplayShopInventory();// 否则显示商店的库存
}
//显示玩家的库存
private void DisplayPlayerInventory()
{
}
// 清空物品预览
private void ClearItemPreview()
{
_itemPreviewSprite.sprite = null;
_itemPreviewSprite.color = Color.clear;
_itemPreviewName.text = "";
_itemPreviewDescription.text = "";
}
// 更新物品预览
private void UpdateItemPreview(ShopSlotUI shopSlotUI) {
var data = shopSlotUI.AssignedItemSlot.ItemData;
_itemPreviewSprite.sprite = data.Icon;
_itemPreviewSprite.color = Color.white;
_itemPreviewName.text = data.DisplayName;
_itemPreviewDescription.text = data.Description;
}
效果
购买和出售切换
修改ShopKeeperDisplay,完善显示玩家的库存事件,并添加切换事件
//显示玩家的库存
private void DisplayPlayerInventory()
{
foreach (var item in _playerInventoryHolder.PrimaryInventorySystem.GetAllItemsHeld())
{
var tempSlot = new ShopSlot();
tempSlot.AssignItem(item.Key,item.Value);// 分配物品
var shopSlot = Instantiate(_shopSlotPrefab,_itemListContentPanel.transform);
shopSlot.Init(tempSlot, _shopSystem.SellMarkUp);// 初始化槽位
}
}
public void OnBuyTabPressed()
{
_isSelling = false;
RefreshDisplay();
}
public void OnSellTabPressed()
{
_isSelling = true;
RefreshDisplay();
}
修改InventorySystem
//获取玩家库存中所有持有的物品及其数量的方法
public Dictionary<InventoryItemData, int> GetAllItemsHeld()
{
var distinctItems = new Dictionary<InventoryItemData, int>();
foreach (var item in inventorySlots)
{
if (item.ItemData == null) continue;// 如果物品数据为空,跳过当前循环
if (!distinctItems.ContainsKey(item.ItemData)) distinctItems.Add(item.ItemData, item.StackSize);// 如果字典中不存在该物品数据,则添加到字典中
else distinctItems[item.ItemData] += item.StackSize; // 否则,增加物品数量
}
return distinctItems;
}
绑定点击事件
效果
出售功能
修改ShopKeeperDisplay
// 出售点击事件
private void SellItems()
{
// 如果商店可用金币少于购物车中物品的总价值,则无法出售
if (_shopSystem.AvailableGold < _basketTotal)
return;
// 遍历购物车中的物品
foreach (var kvp in _shoppingCart)
{
// 获取物品的调整价格
var price = GetModifiedPrice(kvp.Key, kvp.Value, _shopSystem.SellMarkUp);
// 在商店中出售物品,并减少商店金额
_shopSystem.SellItem(kvp.Key, kvp.Value, price);
// 玩家增加金币
_playerInventoryHolder.PrimaryInventorySystem.GainGold(price);
// 从玩家库存系统中移除出售的物品
_playerInventoryHolder.PrimaryInventorySystem.RemoveItemsFromInventory(kvp.Key, kvp.Value);
}
// 刷新显示
RefreshDisplay();
}
修改ShopSystem
//出售物品
public void SellItem(InventoryItemData itemData, int quantity, int price)
{
// 将物品添加到商店中
AddToShop(itemData, quantity);
// 减少商店金额
ReduceGold(price);
}
private void ReduceGold(int price){
// 减少商店金额
_availableGold -= price;
}
修改InventorySystem
// 玩家增加金币
public void GainGold(int price)
{
_gold += price;
}
//从库存中移除物品
public void RemoveItemsFromInventory(InventoryItemData data, int amount)
{
// 检查库存中是否包含指定物品,并获取包含该物品的所有库存槽
if (ContainsItem(data, out List<InventorySlot> inventorySlots))
{
foreach (var slot in inventorySlots)
{
var stackSize = slot.StackSize;
if (stackSize > amount)
{
// 如果库存槽中的堆叠大小 大于 要移除的数量,则从堆叠中移除指定数量的物品
slot.RemoveFromStack(amount);
// 触发库存槽变化事件
OnInventorySlotChanged?.Invoke(slot);
return;
}
else
{
// 否则则从堆叠中移除全部物品,并更新剩余数量
slot.RemoveFromStack(stackSize);
amount -= stackSize;
// 触发库存槽变化事件
OnInventorySlotChanged?.Invoke(slot);
}
}
}
}
修改ItemSlot
// 从物品堆叠中移除一定数量的物品
public void RemoveFromStack(int amount)
{
stackSize -= amount;
// 如果堆叠大小小于等于0,则清空槽位
if(stackSize <= 0) ClearSlot();
}
效果
保存商店数据
修改ShopKeeper
private string _id;// 商店管理员的唯一标识符
private ShopSaveData _shopSaveData;// 商店保存数据
private void Awake(){
//。。。
_id= GetComponent<UniqueID>().ID;// 获取商店管理员的唯一标识符
_shopSaveData = new ShopSaveData(_shopSystem);// 创建商店保存数据对象
}
private void Start()
{
// 如果存档管理器中不包含该商店管理员的数据,则将其添加到存档管理器中
if (!SaveGameManager.data._shopKeeperDictionary.ContainsKey(_id))
SaveGameManager.data._shopKeeperDictionary.Add(_id, _shopSaveData);
}
private void OnEnable()
{
// 注册加载游戏事件时,调用加载物品方法
SaveLoad.OnLoadGame += LoadInventory;
}
private void OnDisable()
{
// 取消注册加载游戏事件时,调用加载物品方法
SaveLoad.OnLoadGame -= LoadInventory;
}
private void LoadInventory(SaveData data)
{
// 如果存档中不包含该商店管理员的数据,则直接返回
if (!data._shopKeeperDictionary.TryGetValue(_id, out ShopSaveData shopSaveData)) return;
// 将存档中的商店保存数据赋值给当前商店管理员
_shopSaveData = shopSaveData;
// 更新商店系统
_shopSystem = _shopSaveData.ShopSystem;
}
[System.Serializable]
public class ShopSaveData
{
public ShopSystem ShopSystem;// 商店系统
public ShopSaveData(ShopSystem shopSystem)
{
ShopSystem = shopSystem;
}
}
修改SaveData
//用于保存商店的数据
public SerializableDictionary<string, ShopSaveData> _shopKeeperDictionary;
_shopKeeperDictionary = new SerializableDictionary<string, ShopSaveData>();
效果,我先购买了5个木头保存,在重新运行游戏,加载商店数据
快捷栏物品切换和使用
修改InventorySlot_UI,实现切换快捷栏颜色
private Image image;
//选中和不选中颜色
public Color selectedColor, notSelectedColor;
private void Awake()
{
//。。。
image = GetComponent<Image>();
Deselect();// 初始化时取消选中
}
//选择该槽位颜色修改
public void Select()
{
image.color = selectedColor;
}
//取消选择该槽位颜色修改
public void Deselect()
{
image.color = notSelectedColor;
}
配置参数
修改StaticInventoryDisplay
int selectedSlot = -1;//快捷栏索引
private Dictionary<KeyCode, int> slotIndexMap = new Dictionary<KeyCode, int>()
{
{ KeyCode.Alpha1, 0 },
{ KeyCode.Alpha2, 1 },
{ KeyCode.Alpha3, 2 },
{ KeyCode.Alpha4, 3 },
{ KeyCode.Alpha5, 4 },
{ KeyCode.Alpha6, 5 },
{ KeyCode.Alpha7, 6 },
{ KeyCode.Alpha8, 7 },
{ KeyCode.Alpha9, 8 },
{ KeyCode.Alpha0, 9 }
};
protected override void Start()
{
//。。。
ChangeSelectedSlot(0);//默认选中第一个槽
}
private void Update()
{
//快捷栏数字切换
foreach (var kvp in slotIndexMap)
{
if (Input.GetKeyDown(kvp.Key))
{
ChangeSelectedSlot(kvp.Value);
break;
}
}
//使用物品测试
if (Input.GetKeyDown(KeyCode.Space))
{
InventorySlot_UI slot = GetSelectedItem();
if (slot && slot.AssignedInventorySlot.ItemData)
{
Debug.Log("使用了物品:" + slot.AssignedInventorySlot.ItemData.DisplayName);
// 从物品堆叠中移除一定数量的物品
slot.AssignedInventorySlot.RemoveFromStack(1);
slot.UpdateUISlot();// 更新物品槽的显示
}
else
{
Debug.Log("没有物品可是使用!");
}
}
}
//切换快捷栏选择
void ChangeSelectedSlot(int newValue)
{
if (selectedSlot >= 0)
{
slots[selectedSlot].Deselect();// 取消之前选中的槽位
}
slots[newValue].Select();// 选择新的槽位
selectedSlot = newValue;// 更新选中的槽位索引
}
// 获取当前选中快捷栏信息
public InventorySlot_UI GetSelectedItem()
{
if (slots.Length > 0)
{
InventorySlot_UI slot = slots[selectedSlot];// 获取当前选中槽位
if (slot != null) return slot;// 如果有物品,则返回物品
}
return null;//如果没有选中物品则返回null
}
效果
完结
赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注
,以便我第一时间收到反馈,你的每一次支持
都是我不断创作的最大动力。点赞越多,更新越快哦!当然,如果你发现了文章中存在错误
或者有更好的解决方法
,也欢迎评论私信告诉我哦!
好了,我是向宇
,https://xiangyu.blog.csdn.net
一位在小公司默默奋斗的开发者,出于兴趣爱好,于是最近才开始自习unity。如果你遇到任何问题,也欢迎你评论私信找我, 虽然有些问题我可能也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~