当前位置: 首页 > news >正文

Unity Demo-3DFarm详解-其二

我们接着一的内容来讲解这几个部分:

角色与玩家互动

物品与背包

存档和进度管理

用户界面系统

角色与玩家互动

角色与玩家互动系统是游戏中连接玩家输入与游戏世界的核心机制,它允许玩家通过点击、移动等操作与游戏中的各种对象(如NPC、物品、环境元素)进行交互,实现诸如对话、采集、使用物品、战斗等核心游戏玩法。

交互逻辑实现

Selectable 组件(Selectable.cs)是所有可交互对象的基础,它定义了对象的交互类型、范围和可用动作:

// 可选类型枚举
public enum SelectableType
{Interact = 0,       // 与物体的中心交互InteractBound = 5,  // 与碰撞体包围盒内最近的位置交互InteractSurface = 10,  // 表面交互CantInteract = 20,  // 可以点击或悬停,但无法交互CantSelect = 30,    // 无法点击或悬停
}/// <summary>
/// 玩家可以与之交互的任何物体都是可选对象
/// 大多数对象都是可选的(玩家可以点击的任何东西)。
/// 可选对象可以包含动作。
/// 当距离摄像机太远时,可选对象将被停用,以提升游戏性能。
/// </summary>
public class Selectable : MonoBehaviour
{public SelectableType type;  // 可选对象的类型public float use_range = 2f; // 使用范围[Header("动作")]public SAction[] actions;   // 动作数组// ... 其他代码 ...// 当角色与此可选对象交互时,检查所有动作,看看是否有任何应该触发的动作。public void Use(PlayerCharacter character, Vector3 pos){if (enabled){PlayerUI ui = PlayerUI.Get(character.player_id);ItemSlot slot = ui?.GetSelectedSlot();MAction maction = slot?.GetItem()?.FindMergeAction(this);AAction aaction = FindAutoAction(character);if (maction != null && maction.CanDoAction(character, slot, this)){maction.DoAction(character, slot, this);PlayerUI.Get(character.player_id)?.CancelSelection();}else if (aaction != null && aaction.CanDoAction(character, this)){aaction.DoAction(character, this);}else if (actions.Length > 0){ActionSelector.Get(character.player_id)?.Show(character, this, pos);}if (onUse != null)onUse.Invoke(character);}}// ... 其他代码 ...
}

实现了一个游戏中的可交互物体系统,通过 Selectable 类为场景中的物体赋予交互能力,其中 SelectableType 枚举定义了五种交互类型(从完全交互到完全不可交互),并通过 use_range 控制交互距离;当玩家与物体交互时,系统会按优先级顺序执行动作:首先检查玩家手持物品是否支持合并操作(如钥匙开门),若可行则触发合并动作并清空玩家选择状态,否则寻找自动触发的动作(如自动拾取),若两者均不满足则弹出动作选择菜单供玩家手动选择(如打开箱子),同时通过 onUse 事件通知外部系统响应交互行为,整个设计通过动态停用远距离物体优化性能,并支持多人游戏中基于玩家ID的独立交互逻辑。 

PlayerCharacter 类定义了玩家角色的属性和行为,包括移动、交互等:

public enum PlayerInteractBehavior
{MoveAndInteract = 0, // 当点击对象时,角色将自动移动到对象位置,然后与之交互InteractOnly = 10, // 当点击对象时,只有在交互范围内才会进行交互(不会自动移动)
}/// <summary>
/// 主角角色脚本,包含了移动和玩家控制/命令的代码
/// </summary>
public class PlayerCharacter : MonoBehaviour
{[Header("Interact")]public PlayerInteractBehavior interact_type = PlayerInteractBehavior.MoveAndInteract; // 交互类型public float interact_range = 0f; // 添加到可选使用范围中的交互范围public float interact_offset = 0f; // 不要与角色中心交互,而是与前方的偏移量进行交互// ... 其他代码 ...private void Start(){PlayerControlsMouse mouse_controls = PlayerControlsMouse.Get(); // 获取鼠标控制实例mouse_controls.onClickFloor += OnClickFloor; // 注册点击地面事件mouse_controls.onClickObject += OnClickObject; // 注册点击对象事件mouse_controls.onClick += OnClick; // 注册点击事件mouse_controls.onRightClick += OnRightClick; // 注册右键点击事件// ... 其他代码 ...}// ... 其他代码 ...
}

实现了一个玩家角色交互控制系统,通过 PlayerInteractBehavior 枚举定义了两种交互模式:MoveAndInteract(点击对象时角色自动移动到目标位置并触发交互)和 InteractOnly(仅当对象在交互范围内时才直接交互,不自动移动),并在 PlayerCharacter 类中通过 interact_type 字段动态配置当前模式,同时通过 interact_range 扩展默认交互距离、interact_offset 设定交互点偏移(避免与角色中心重叠);此外,角色在初始化时(Start 方法)注册了鼠标控制事件(如点击地面、对象、右键等),将用户输入(如 onClickObject)与后续的移动逻辑、范围判定及对象交互行为绑定,形成一套基于事件驱动的玩家操作响应机制。 

ActionSelector 类处理交互时的动作选择面板:

/// <summary>
/// ActionSelector 是一个面板,当点击可选择的对象时弹出,允许选择一个操作。
/// </summary>
public class ActionSelector : UISlotPanel
{// ... 其他代码 ...public void Show(PlayerCharacter character, Selectable select, Vector3 pos){if (select != null && character != null){if (!IsVisible() || this.select != select || this.character != character){this.select = select;this.character = character;RefreshSelector(); // 刷新面板上的按钮animator.Rebind(); // 重新绑定动画transform.position = pos;interact_pos = pos;gameObject.SetActive(true); // 显示面板selection_index = 0;Show();}}}// ... 其他代码 ...
}

 实现了一个动作选择面板(ActionSelector)​,在玩家点击可交互物体时弹出,用于展示并选择该物体支持的操作:当调用 Show(PlayerCharacter character, Selectable select, Vector3 pos) 方法时,系统会校验传入的玩家角色(character)和可交互对象(select)是否有效,若面板当前未显示、或本次调用的对象/角色与上次不同,则更新面板绑定的目标(this.select 和 this.character),通过 RefreshSelector() 刷新面板按钮内容,重置动画状态(animator.Rebind()),并将面板位置(transform.position)设定到交互发生点(pos),最后激活面板(gameObject.SetActive(true))并初始化选项索引(selection_index = 0),为用户提供直观的操作选择界面。

整个系统的底层逻辑如下:

- 交互检测 :

- 系统通过鼠标点击或游戏手柄输入检测玩家的交互意图
- 当玩家点击对象时,系统会检查该对象是否为 Selectable 类型
- 如果是,则根据玩家的交互类型( MoveAndInteract 或 InteractOnly )决定是否移动角色到交互位置
- 动作执行 :

- 当角色到达交互位置或已经在交互范围内时,系统会调用 Selectable 的 Use 方法
- Use 方法会检查是否有自动动作可以执行,或者显示动作选择面板让玩家选择
- 执行动作后,系统会触发相应的事件和反馈
- 优化机制 :

- 为了提高性能, Selectable 对象在距离摄像机太远时会被停用
- 系统会自动管理 Selectable 对象的激活状态,确保只处理可见范围内的对象

工作流程如下:

- 玩家输入 :

- 玩家通过鼠标点击或游戏手柄选择游戏世界中的对象
- 系统检测到点击,并确定点击的对象是否为 Selectable 类型
- 角色移动 :

- 如果交互类型为 MoveAndInteract ,角色会自动移动到对象的交互范围内
- 如果交互类型为 InteractOnly ,只有当角色已经在交互范围内时才会进行交互
- 交互执行 :

- 当角色到达交互位置时,系统会调用 Selectable 的 Use 方法
- 系统检查是否有可以自动执行的动作(如物品拾取)
- 如果没有自动动作或有多个可能的动作,系统会显示动作选择面板
- 玩家选择动作后,系统执行相应的动作
- 反馈与结果 :

- 动作执行后,系统会提供视觉和听觉反馈(如动画、音效)
- 系统会更新游戏状态(如物品被拾取、任务进度更新等)
- 交互完成后,角色可以进行下一次交互

具体NPC实现

在这个项目中目前只实现了商店的NPC和对话的NPC,但是具体的数据框架是已经定义好了的。

ShopNPC.cs

[RequireComponent(typeof(Selectable))]  // 需要挂载Selectable组件
public class ShopNPC : MonoBehaviour
{public string title;  // 商店标题[Header("Buy")]  // 购买项public ItemData[] items;  // 购买物品列表[Header("Sell")]  // 出售项public GroupData sell_group;  // 出售物品的群组,如果为null,则可以出售任何物品// 打开商店给特定的玩家角色public void OpenShop(PlayerCharacter player){List<ItemData> buy_items = new List<ItemData>(items);  // 创建购买物品列表的副本ShopPanel.Get().ShowShop(player, title, buy_items, sell_group);  // 显示商店界面}
}

- 可以展示商店标题
- 提供物品购买功能(有固定的物品列表)
- 提供物品出售功能(可以限制出售物品的群组)
- 与玩家交互时会打开商店界面

所有NPC都基于 Character.cs 类实现,具有以下核心功能:

/// <summary>
/// Characters是可以给予移动或执行动作命令的盟友或NPC。
/// </summary>
[RequireComponent(typeof(Rigidbody))]
[RequireComponent(typeof(Selectable))]
[RequireComponent(typeof(Destructible))]
[RequireComponent(typeof(UniqueID))]
public class Character : Craftable
{[Header("Character")]public CharacterData data;                           // 角色数据[Header("Move")]public bool move_enabled = true;                     // 是否启用移动public float move_speed = 2f;                        // 移动速度// ... 其他移动相关参数 ...[Header("Attack")]public bool attack_enabled = true;                    // 是否启用攻击public int attack_damage = 10;                        // 攻击伤害// ... 其他攻击相关参数 ...[Header("Action")]public float follow_distance = 3f;                    // 跟随距离public UnityAction onAttack;                          // 攻击时触发的事件public UnityAction onDamaged;                         // 受伤时触发的事件public UnityAction onDeath;                           // 死亡时触发的事件// ... 其他代码 ...
}

Character.cs定义了一系列角色通用的内容,比如-移动能力(可设置移动速度、旋转速度等、障碍物 avoidance、地面检测和下落机制、攻击能力(包括近战和远程攻击)、跟随功能、受伤和死亡机制、事件系统(攻击、受伤、死亡时触发事件)。

NPC通过 Selectable 组件实现与玩家的交互,并通过动作系统触发相应的功能:

/// <summary>
/// 商店动作,用于与商店NPC交互
/// </summary>
[CreateAssetMenu(fileName = "Action", menuName = "FarmingEngine/Actions/Shop", order = 50)]
public class ActionShop : AAction
{public override void DoAction(PlayerCharacter character, Selectable select){ShopNPC shop = select.GetComponent<ShopNPC>(); // 获取选择对象上的商店NPC组件if (shop != null)shop.OpenShop(character); // 打开商店界面,让玩家与商店NPC交互}public override bool CanDoAction(PlayerCharacter character, Selectable select){ShopNPC shop = select.GetComponent<ShopNPC>(); // 获取选择对象上的商店NPC组件return shop != null; // 如果选择对象有商店NPC组件,则可以执行该动作}
}

定义了一个名为 ActionShop 的游戏动作类(继承自 AAction),专门用于处理玩家与商店 NPC 的交互逻辑:当玩家对可交互对象(Selectable)执行该动作时,系统会检查该对象是否挂载了 ShopNPC 组件;若存在该组件,则调用其 OpenShop 方法打开商店界面,实现商品交易功能,并通过 CanDoAction 方法预先验证交互的可行性(仅当目标对象包含 ShopNPC 组件时才允许执行该动作)。这种设计将商店交互逻辑封装为独立的可配置资源(通过 CreateAssetMenu 特性可在 Unity 编辑器菜单中创建实例),符合模块化原则,便于复用和扩展商店功能。

对话NPC

对话NPC的实现主要通过 DialogueQuestsWrap.cs 文件完成,这是一个对接DialogueQuests系统的包装类:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;#if DIALOGUE_QUESTS
using DialogueQuests;
#endifnamespace FarmingEngine
{/// <summary>/// 对接 DialogueQuests 的包装类/// </summary>public class DialogueQuestsWrap : MonoBehaviour{
#if DIALOGUE_QUESTSprivate HashSet<Actor> inited_actors = new HashSet<Actor>(); // 已初始化的角色集合private float timer = 1f; // 计时器,用于慢速更新// 静态构造函数,注册事件处理程序static DialogueQuestsWrap(){TheGame.afterLoad += ReloadDQ; // 在加载后重新加载对话任务TheGame.afterNewGame += NewDQ; // 在新游戏开始后初始化对话任务}void Awake(){PlayerData.LoadLast(); // 确保游戏已加载TheGame the_game = FindObjectOfType<TheGame>();NarrativeManager narrative = FindObjectOfType<NarrativeManager>();if (narrative != null){narrative.onPauseGameplay += OnPauseGameplay; // 游戏暂停时的事件处理narrative.onUnpauseGameplay += OnUnpauseGameplay; // 游戏继续时的事件处理narrative.onPlaySFX += OnPlaySFX; // 播放音效的事件处理narrative.onPlayMusic += OnPlayMusic; // 播放音乐的事件处理narrative.onStopMusic += OnStopMusic; // 停止音乐的事件处理narrative.getTimestamp += GetTimestamp; // 获取时间戳的委托narrative.use_custom_audio = true; // 使用自定义音频}else{Debug.LogError("Dialogue Quests: 集成失败 - 确保在场景中添加了 DQManager");}if (the_game != null){the_game.beforeSave += SaveDQ; // 保存游戏前的事件处理LoadDQ(); // 加载对话任务数据}}private void Start(){Actor player = Actor.GetPlayerActor();if (player == null){Debug.LogError("Dialogue Quests: 集成失败 - 确保在 PlayerCharacter 上添加了 Actor 脚本,并且 ActorData 的 is_player 设置为 true");}}private void Update(){timer += Time.deltaTime;if (timer > 1f){timer = 0f;SlowUpdate(); // 慢速更新,处理角色初始化等}}private void SlowUpdate(){foreach (Actor actor in Actor.GetAll()){if (!inited_actors.Contains(actor)){inited_actors.Add(actor);InitActor(actor); // 初始化角色}}}private void InitActor(Actor actor){if (actor != null){Selectable select = actor.GetComponent<Selectable>();if (select != null){actor.auto_interact_enabled = false; // 禁用角色的自动交互select.onUse += (PlayerCharacter character) =>{character.StopMove(); // 停止角色移动character.FaceTorward(actor.transform.position); // 面向角色位置actor.Interact(character.GetComponent<Actor>()); // 角色与角色交互};}}}// 在 Awake 中不要调用此方法(因为在获取 NarrativeManager 之前无法工作)private static void ReloadDQ(){NarrativeData.Unload(); // 卸载对话数据LoadDQ(); // 重新加载对话任务数据}private static void NewDQ(){PlayerData pdata = PlayerData.Get();if (pdata != null){NarrativeData.Unload(); // 卸载对话数据NarrativeData.NewGame(pdata.filename); // 新建游戏,根据指定的文件名}}private static void LoadDQ(){PlayerData pdata = PlayerData.Get();if (pdata != null){NarrativeData.AutoLoad(pdata.filename); // 自动加载对话数据}}private void SaveDQ(string filename){if (NarrativeData.Get() != null && !string.IsNullOrEmpty(filename)){NarrativeData.Save(filename, NarrativeData.Get()); // 保存对话数据}}private void OnPauseGameplay(){TheGame.Get().PauseScripts(); // 暂停脚本执行}private void OnUnpauseGameplay(){TheGame.Get().UnpauseScripts(); // 恢复脚本执行}private void OnPlaySFX(string channel, AudioClip clip, float vol = 0.8f){TheAudio.Get().PlaySFX(channel, clip, vol); // 播放音效}private void OnPlayMusic(string channel, AudioClip clip, float vol = 0.4f){TheAudio.Get().PlayMusic(channel, clip, vol); // 播放音乐}private void OnStopMusic(string channel){TheAudio.Get().StopMusic(channel); // 停止音乐}private float GetTimestamp(){return TheGame.Get().GetTimestamp(); // 获取时间戳}#endif}
}

 该桥接类通过静态构造函数注册游戏全局事件​(如加载存档 afterLoad、新建游戏afterNewGame),在关键节点触发对话数据的重载(ReloadDQ)或初始化(NewDQ),确保对话状态与游戏进程同步;在 Awake 阶段绑定 NarrativeManager 的核心事件回调,实现跨模块联动:游戏暂停时(onPauseGameplay)冻结脚本逻辑,恢复时(onUnpauseGameplay)解冻,并将插件的音效(onPlaySFX)与音乐控制(onPlayMusic/onStopMusic)转发至游戏音频系统 TheAudio,同时通过 getTimestamp 委托同步游戏内时间戳;通过慢速更新(SlowUpdate)动态初始化场景中的 Actor 角色,禁用其自动交互(auto_interact_enabled=false)避免冲突,并重写点击逻辑——玩家点击角色时强制停止移动、转向目标位置,再触发 actor.Interact() 以启动对话;在游戏保存时(beforeSave)将对话分支与任务进度写入存档文件(NarrativeData.Save())​,加载时(LoadDQ)根据存档名恢复对话状态,保证叙事进度与游戏存档严格一致;启动时校验关键组件,如检查玩家角色是否挂载 Actor 脚本,未找到 NarrativeManager 时报错提示配置缺失,确保集成可靠性

我想我得先介绍一下DialogueQuests系统:

关于这个插件的内容都够我们再重新多写一篇博客了,这里先按下不表,主要学习我们这个桥接层做了哪些东西。

 在 Unity 游戏框架中,桥接层代码(如 DialogueQuestsWrap)通过组合关系而非继承实现了 NPC 对话功能的动态集成:它将游戏引擎的物理交互(点击 NPC)​重定向至 DialogueQuests 插件的对话接口 actor.Interact(),并通过事件绑定同步游戏状态(如对话时暂停非对话逻辑、转发音频请求至游戏音频系统),同时依托静态构造函数注册全局事件(存档加载/保存),确保对话分支进度与游戏存档数据持久化同步,最终在保障性能(慢速更新检测 NPC)和可靠性(组件校验、防重复初始化)的前提下,实现“点击 NPC → 触发对话 → 存档继承”的无缝流程。

物品和背包

物品和背包系统是游戏中的核心系统之一,它允许玩家拾取、存储、使用和管理游戏中的各种物品,包括消耗品、装备、材料等,为玩家提供了与游戏世界互动的重要方式。

物品和背包系统的实现主要涉及以下几个文件:

Item.cs

// ... existing code ...
public class Item : Craftable
{[Header("Item")]public ItemData data; // 物品数据public int quantity = 1; // 数量[Header("FX")]public float auto_collect_range = 0f; // 当在范围内时将自动被收集public bool snap_to_ground = true; // 如果为真,物品将自动放置在地面上而不是浮空public AudioClip take_audio; // 收取时的音频public GameObject take_fx; // 收取时的特效// ... existing code ...private void OnUse(PlayerCharacter character){// 收取物品character.Inventory.TakeItem(this);}public void TakeItem(){if (onTake != null)onTake.Invoke();DestroyItem();TheAudio.Get().PlaySFX("item", take_audio);if (take_fx != null)Instantiate(take_fx, transform.position, Quaternion.identity);}// ... existing code ...
}
// ... existing code ...

实现了一个游戏中的可拾取物品系统,其核心逻辑围绕物品数据管理、交互触发与拾取反馈展开:通过 ItemData 存储物品基础属性(如名称、图标),quantity 记录堆叠数量,并继承 Craftable 支持合成系统;交互上支持玩家主动点击拾取(调用 OnUse 触发角色背包的 TakeItem 方法)或通过 auto_collect_range 实现自动收集(需外部逻辑配合);拾取时触发 onTake 事件通知其他模块(如任务系统),销毁物品实体(DestroyItem),并播放 take_audio 音效及生成 take_fx 粒子特效以增强沉浸感,同时通过 snap_to_ground 控制物品生成时自动吸附地面避免悬空。

InventoryData.cs

// ... existing code ...
public enum InventoryType
{None = 0,           // 无Inventory = 5,      // 背包Equipment = 10,     // 装备Storage = 15,       // 存储Bag = 20,           // 袋子
}[System.Serializable]
public class InventoryItemData
{public string item_id;       // 物品IDpublic int quantity;         // 数量public float durability;     // 耐久度public string uid;           // 唯一IDpublic InventoryItemData(string id, int q, float dura, string uid) { item_id = id; quantity = q; durability = dura; this.uid = uid; }public ItemData GetItem() { return ItemData.Get(item_id); } // 获取物品数据
}[System.Serializable]
public class InventoryData
{public Dictionary<int, InventoryItemData> items;    // 物品字典public InventoryType type;                          // 库存类型public string uid;                                  // 唯一IDpublic int size = 99;                               // 大小// ... existing code ...// 添加物品public int AddItem(string item_id, int quantity, float durability, string uid){if (!string.IsNullOrEmpty(item_id) && quantity > 0){ItemData idata = ItemData.Get(item_id);int max = idata != null ? idata.inventory_max : 999;int slot = GetFirstItemSlot(item_id, max - quantity);if (slot >= 0){AddItemAt(item_id, slot, quantity, durability, uid);}return slot;}return -1;}// ... existing code ...
}
// ... existing code ...

 实现了一个游戏中的模块化库存系统,通过 InventoryType 枚举划分背包、装备栏、仓库等不同类型的库存区域(如 Inventory=5 代表背包),并在 InventoryItemData 类中封装单件物品的核心属性(包括物品ID item_id 关联配置数据、堆叠数量 quantity、耐久度 durability 和全局唯一标识 uid),同时通过 GetItem() 方法动态获取物品配置实现数据解耦;而 InventoryData 类则负责库存的动态管理,以字典结构 items 存储槽位与物品的映射关系,通过 size 控制库存容量上限(默认99格),并在 AddItem() 方法中实现智能添加逻辑——先根据物品ID查询最大堆叠数(inventory_max),再寻找可堆叠槽位或空槽(GetFirstItemSlot),最终调用 AddItemAt() 完成添加,确保堆叠不超限,同时每个库存实例通过唯一 uid 支持多角色或多容器场景(如玩家背包与NPC商店并存)。

PlayerCharacterInventory.cs

// ... existing code ...
public class PlayerCharacterInventory : MonoBehaviour
{public int inventory_size = 15; //If you change this, make sure to change the UIpublic ItemData[] starting_items;public UnityAction<Item> onTakeItem;public UnityAction<Item> onDropItem;public UnityAction<ItemData> onGainItem;private PlayerCharacter character;private EquipAttach[] equip_attachments;private Dictionary<string, EquipItem> equipped_items = new Dictionary<string, EquipItem>();// ... existing code ...//Take an Item on the floorpublic void TakeItem(Item item){if (BagData != null && !InventoryData.CanTakeItem(item.data.id, item.quantity) && !item.data.IsBag()){TakeItem(BagData, item); //Take into bag}else{TakeItem(InventoryData, item); //Take into main inventory}}public void TakeItem(InventoryData inventory, Item item){if (item != null && !character.IsBusy() && inventory.CanTakeItem(item.data.id, item.quantity)){character.FaceTorward(item.transform.position);if (onTakeItem != null)onTakeItem.Invoke(item);character.TriggerBusy(0.4f, () =>{//Make sure wasnt destroyed during the 0.4 secif (item != null && inventory.CanTakeItem(item.data.id, item.quantity)){PlayerData pdata = PlayerData.Get();DroppedItemData dropped_item = pdata.GetDroppedItem(item.GetUID());float durability = dropped_item != null ? dropped_item.durability : item.data.durability;int slot = inventory.AddItem(item.data.id, item.quantity, durability, item.GetUID()); //Add to inventoryItemTakeFX.DoTakeFX(item.transform.position, item.data, inventory.type, slot);item.TakeItem(); //Destroy item}});}}// ... existing code ...
}
// ... existing code ...

 实现了一个玩家角色的库存管理系统,其中核心逻辑围绕 ​物品拾取行为​ 展开,通过 PlayerCharacterInventory 类管理背包容量(inventory_size 默认15格)、初始物品配置(starting_items)以及事件委托(如 onTakeItem 通知外部拾取动作),并通过 TakeItem 方法处理物品拾取流程:当玩家调用 TakeItem(Item item) 时,系统优先判断物品是否可放入额外容器(如背包 BagData),若不可行则放入主背包 InventoryData;具体拾取操作由重载方法 TakeItem(InventoryData inventory, Item item) 执行,其中会检查角色是否处于空闲状态(!character.IsBusy())且库存可容纳物品(inventory.CanTakeItem),随后触发角色转向物品位置(character.FaceTorward)并调用 onTakeItem 事件,再通过 TriggerBusy(0.4f, ...) 强制角色进入0.4秒操作状态防止打断,期间验证物品有效性后调用 inventory.AddItem() 将物品数据(ID、数量、耐久度、唯一ID)添加至库存槽位,同时播放物品拾取特效(ItemTakeFX.DoTakeFX)并销毁场景中的物品实体(item.TakeItem())。

整个物品背包系统的工作流是这样的:

- 物品拾取流程

- 玩家点击物品或进入物品自动收集范围
- 触发 Item 类的 OnUse 方法
- 调用 PlayerCharacterInventory 类的 TakeItem 方法
- 角色面向物品,播放收取动画
- 物品被添加到背包,播放收取特效和音效
- 物品被销毁


- 物品使用流程

- 玩家从背包中选择物品
- 调用物品的使用方法
- 物品数量减少或耐久度降低
- 当物品数量为零或耐久度为零时,物品被从背包中移除


- 物品存储流程

- 玩家打开存储界面(如箱子、背包)
- 物品从角色背包转移到存储容器
- 或从存储容器转移到角色背包
- 库存数据被更新

存档和进度管理

存档和进度管理系统是游戏的核心机制之一,它允许玩家保存游戏进度、加载之前的保存以及开始新游戏。系统负责跟踪和存储玩家的所有游戏数据,包括玩家角色信息、物品库存、世界状态、建造物、种植物等,确保玩家可以随时暂停和恢复游戏。

具体实现代码如下:

SaveTool.cs

// ... existing code ...
public static T LoadFile<T>(string filename) where T : class
{// 从文件加载序列化的数据
}public static void SaveFile<T>(string filename, T data) where T : class
{// 将数据序列化后保存到文件
}public static void DeleteFile(string filename)
{// 删除指定文件
}public static List<string> GetAllSave(string extension = "")
{// 获取所有保存文件的列表
}
// ... existing code ...

这个文件提供了基本的文件操作功能,如保存、加载、删除文件和获取保存文件列表等。它使用二进制序列化来处理数据,确保数据可以被正确地保存和加载。

PlayerData.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;namespace FarmingEngine
{/// <summary>/// PlayerData 是主要的存档文件数据脚本。该脚本中包含的所有内容都将被保存。/// </summary>[System.Serializable]public class PlayerData{public string filename; // 存档文件名public string version; // 游戏版本号public DateTime last_save; // 上次保存时间public int world_seed = 0; // 世界随机种子public string current_scene = ""; // 当前加载的场景public int day = 0; // 游戏天数public float day_time = 0f; // 当天时间,0 = 午夜,24 = 一天结束public Dictionary<int, PlayerCharacterData> player_characters; // 玩家角色数据public Dictionary<string, InventoryData> inventories; // 物品库存数据// 其他游戏数据...public PlayerData(string name){filename = name; // 设置文件名version = Application.version; // 设置版本号last_save = DateTime.Now; // 设置当前时间为上次保存时间day = 1; // 从第一天开始day_time = 6f; // 游戏从早上6点开始new_day = true; // 新的一天}public static void Save(string filename, PlayerData data){if (!string.IsNullOrEmpty(filename) && data != null){data.filename = filename;data.last_save = DateTime.Now;data.version = Application.version;player_data = data;file_loaded = filename;SaveTool.SaveFile<PlayerData>(filename + extension, data);SetLastSave(filename);}}public static PlayerData Load(string filename){if (player_data == null || file_loaded != filename){player_data = SaveTool.LoadFile<PlayerData>(filename + extension);if (player_data != null){file_loaded = filename;player_data.FixData();}}return player_data;}// 其他方法...}
}

这个文件包含了游戏的所有可保存数据,如玩家角色、物品库存、世界状态等。它还提供了保存、加载和新游戏等功能,确保游戏数据可以被正确地管理。

TheGame.cs

// ... 其他代码 ...// 保存(不是静态的,因为需要加载场景和保存文件)
public void Save()
{Save(PlayerData.Get().filename); // 保存当前文件
}// 保存到指定文件
public bool Save(string filename)
{if (!SaveTool.IsValidFilename(filename))return false; // 失败foreach (PlayerCharacter player in PlayerCharacter.GetAll())player.SaveData.position = player.transform.position;PlayerData.Get().current_scene = SceneNav.GetCurrentScene();PlayerData.Get().current_entry_index = -1; // 根据当前位置保存数据if (beforeSave != null)beforeSave.Invoke(filename); // 调用保存前回调PlayerData.Save(filename, PlayerData.Get());return true;
}// 静态方法:加载游戏
public static void Load()
{Load(PlayerData.GetLastSave()); // 从上次保存的文件加载
}// 静态方法:从指定文件加载游戏
public static bool Load(string filename)
{if (!SaveTool.IsValidFilename(filename))return false; // 失败PlayerData.Unload(); // 确保先卸载PlayerData.AutoLoad(filename);// 其他加载逻辑...
}// ... 其他代码 ...

这个文件提供了游戏层面的保存和加载功能,它协调各个系统的数据保存和加载,确保游戏状态可以被正确地保存和恢复。

具体的工作流程如下:

- 保存流程

- 收集所有游戏数据,包括玩家角色信息、物品库存、世界状态等。
- 调用 PlayerData.Save 方法将数据序列化后写入文件。
- 更新 PlayerPrefs 中的最后一次保存的文件名。
- 加载流程

- 调用 PlayerData.Load 方法从文件读取序列化的数据。
- 反序列化为游戏对象,恢复游戏状态。
- 如果找不到保存文件,则开始新游戏。
- 新游戏流程

- 调用 PlayerData.NewGame 方法创建新的游戏数据。
- 初始化默认值,如游戏天数、时间、玩家角色等。
- 保存新的游戏数据。

用户界面系统

这个项目的UI系统是游戏与玩家交互的重要桥梁,它负责显示游戏状态、接收玩家输入、反馈游戏结果等,为玩家提供直观、便捷的操作界面,提升游戏的可玩性和用户体验。

在这个项目中,UI系统主要由 TheUI.cs 和 PlayerUI.cs 两个核心脚本实现。

TheUI.cs

private void Awake()
{if (instance == null)instance = this;elseDestroy(gameObject);canvas = GetComponent<Canvas>();canvas.renderMode = RenderMode.ScreenSpaceOverlay;canvas.sortingOrder = 100;ui_material = Resources.Load<Material>("UI/UI_Material");render_camera = Camera.main;pause_panel = transform.Find("PausePanel").GetComponent<GameObject>();gameover_panel = transform.Find("GameOverPanel").GetComponent<GameObject>();
}private void Update()
{if (Input.GetKeyDown(KeyCode.Escape)){if (is_paused)ResumeGame();elsePauseGame();}if (is_game_over){if (!gameover_panel.activeSelf)gameover_panel.SetActive(true);}else{if (gameover_panel.activeSelf)gameover_panel.SetActive(false);}
}

TheUI.cs 是项目中负责管理所有UI面板和基础功能的顶层脚本,它设置单例模式确保全局唯一,初始化Canvas、UI材质、渲染相机和各种UI面板,处理暂停/继续游戏、游戏手柄焦点管理、显示游戏结束面板等功能,同时提供坐标转换、射线检测等基础服务,确保UI系统能够正常工作并响应玩家的输入和游戏的状态变化。

PlayerUI.cs 

private void Start()
{TheUI.Instance.AddPanel(gameObject);build_mode_text = transform.Find("BuildModeText").GetComponent<Text>();ride_button = transform.Find("RideButton").GetComponent<Button>();player = GameObject.FindGameObjectWithTag("Player").GetComponent<Player>();player.onDamage += ShowDamageEffect;UpdateCoinText();
}private void Update()
{if (damage_effect_active){damage_effect_timer -= Time.deltaTime;if (damage_effect_timer <= 0){damage_effect_active = false;damage_text.gameObject.SetActive(false);}}if (player.is_build_mode){build_mode_text.gameObject.SetActive(true);build_mode_text.text = "建造模式";}else{build_mode_text.gameObject.SetActive(false);}if (Input.GetKeyDown(KeyCode.B)){ToggleCraftPanel();}
}

PlayerUI.cs 是玩家专用的游戏内UI面板,它负责显示玩家的信息和处理玩家的输入,将自身添加到UI面板列表中,初始化建造模式文本和骑乘按钮,获取玩家角色,注册伤害特效回调,更新金币显示,处理伤害特效、建造模式文本更新、控制输入等功能,确保玩家能够直观地了解自己的游戏状态并通过UI元素来控制游戏的进展和操作。 

UI系统的工作流程可以分为三个主要阶段,首先是初始化阶段,在游戏启动时, TheUI 会设置单例模式、Canvas组件、UI材质、渲染相机和UI面板等, PlayerUI 会将自身添加到UI面板列表中,初始化建造模式文本和骑乘按钮,获取玩家角色,注册伤害特效回调,更新金币显示等,确保UI系统能够正常工作;然后是更新阶段,在游戏运行过程中, TheUI 会处理暂停/继续游戏、游戏手柄焦点管理、显示游戏结束面板等功能, PlayerUI 会处理伤害特效、更新建造模式文本和第三人称视角光标、处理控制输入等功能,确保UI系统能够正确地响应玩家的输入和游戏的状态变化;最后是事件处理阶段,当玩家点击暂停按钮、工艺面板按钮等UI元素时, TheUI 和 PlayerUI 会处理这些事件,显示或隐藏相应的UI面板,执行相应的游戏逻辑,确保玩家能够通过UI元素来控制游戏的进展和操作。

http://www.dtcms.com/a/270720.html

相关文章:

  • 以太坊智能合约核心技术解析与应用实践
  • LLaMA-Omni 深度解析:打开通往无缝人机语音交互的大门
  • HCIP 认证可以做什么?如何选择合适的职业路径?
  • C++11 future、promise实现原理
  • AI生成交互式数据图表
  • 【c++八股文】Day5:const和constexpr,define
  • sql查询davinci看板数据
  • 【一起来学AI大模型】PyTorch DataLoader 实战指南
  • 极简相册管理ios app Tech Support
  • ARM汇编编程(AArch64架构)课程 - 第7章:SIMD与浮点运算
  • 2025杰理蓝牙芯片:各系列芯片特点及市场分析
  • 【手写 new 操作符实现 - 深入理解 JavaScript 对象创建机制】
  • 【Linux】权限的概念及理解
  • VR/AR在HMI中的创新应用:远程协作与维修的沉浸式体验
  • 类和对象拓展——日期类
  • 【实习篇】之Http头部字段之Disposition介绍
  • 使用 Docker 搭建 Rust Web 应用开发环境——AI教你学Docker
  • VR重现红军过雪山:一场穿越时空的精神洗礼​
  • MySQL 09 普通索引和唯一索引
  • MySQL 间隙锁
  • pytorch 自动微分
  • 半导体晶圆检测的基本知识
  • EGARCH
  • Linux C 目录流基本操作
  • Alloy VS Promtail:基于 Loki 的日志采集架构对比与选型指南
  • ECS由浅入深第四节:ECS 与 Unity 传统开发模式的结合?混合架构的艺术
  • Using Spring for Apache Pulsar:Publishing and Consuming Partitioned Topics
  • vue2 echarts中国地图、在地图上标注经纬度及标注点
  • AI应用实践:制作一个支持超长计算公式的计算器,计算内容只包含加减乘除算法,保存在一个HTML文件中
  • 「macOS 系统字体收集器 (C++17 实现)」