我们接着一的内容来讲解这几个部分:
角色与玩家互动
物品与背包
存档和进度管理
用户界面系统
角色与玩家互动
角色与玩家互动系统是游戏中连接玩家输入与游戏世界的核心机制,它允许玩家通过点击、移动等操作与游戏中的各种对象(如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;
#endif
namespace FarmingEngine
{
/// <summary>
/// 对接 DialogueQuests 的包装类
/// </summary>
public class DialogueQuestsWrap : MonoBehaviour
{
#if DIALOGUE_QUESTS
private 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; // 物品ID
public int quantity; // 数量
public float durability; // 耐久度
public string uid; // 唯一ID
public 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; // 唯一ID
public 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 UI
public 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 floor
public 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 sec
if (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 inventory
ItemTakeFX.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;
else
Destroy(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();
else
PauseGame();
}
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元素来控制游戏的进展和操作。