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元素来控制游戏的进展和操作。