如果最近你在用 Unity 做 RPG 类型项目,大概会有一种感觉:单个功能做出来都不难,难的是把它们组合起来之后,代码还能不能继续往下加。角色移动、状态切换、交互、背包、对话、战斗、存档,每增加一个系统,之前“能跑”的脚本就开始互相挤占,最后变成一团只有你自己才看得懂的 spaghetti。
这里真正容易踩坑的地方是,大多数入门教程只讲了“怎么让角色动起来”“怎么写一个检测碰撞的代码”,却没有把 RPG 项目的骨架讲透。尤其是 Unity 6 发布之后,渲染管线、包管理和输入系统的使用方式都有变化,旧教程里的很多配置步骤已经不能照抄了。如果你直接在 2025 年还在用五六年前的插件和写法,项目越到后面越难维护。
这篇《Unity 6 RPG 游戏开发终极指南(上)》先把玩家的核心纵向切片做完:项目架构、移动控制、状态机、第三人称相机、交互系统和背包系统。我的核心判断并不复杂:RPG 的技术难度从来不在某个具体功能,而在于你怎么避免功能之间的状态耦合。Unity 6 真正的价值,不是多了几个华丽新特性,而是它已经能够用现代 C# 工程方式去组织一个长期迭代的游戏项目了。
这篇文章会覆盖一套可以直接在 Unity 6 中实操的思路,同时也讲清楚每个系统背后的设计原因。读完你可以跑通一个带交互和物品拾取的第三人称 RPG 玩家原型,并且知道下一步接任务、对话和战斗时应该把代码放在哪里。如果你正打算用 Unity 6 开始 RPG 项目,或者已经在做但感觉系统之间开始失控,这篇文章应该能给你一个更稳的起点。
1. 这篇文章要解决的核心问题
先回答一个很多开发者不好意思问的问题:为什么别人的 RPG Demo 能一直加系统,而我的项目加一个任务系统就要重构一次角色控制?
多数情况下,问题不在于某个功能写得不好,而在于你没有一个清晰的代码分层。控制角色的 MonoBehaviour 里既处理按键,又处理动画,还处理攻击判定,甚至直接修改背包数据。一旦需求变化,你就只能靠 if 分支去兼容各种状态。这种写法最明显的症状是:每次加需求都会让角色控制器脚本越来越长,自己改起来都不敢动,怕把前面的逻辑弄坏。
用 Unity 做 RPG 时,正确的处理方式是把系统拆开。玩家控制是一个模块,交互是一个模块,物品与背包是一个模块,UI 显示又是一个模块。模块与模块之间通过接口、事件或数据对象通信,而不是互相直接引用。这样才能保证一个系统内部的 Bug 不会像多米诺骨牌一样倒向整个项目。
这篇文章重点解决的就是这类工程组织问题。适合的读者不只是准备做大型 RPG 的团队,也包括个人开发者。哪怕你的项目只有几百行代码,提前建立这两个习惯也会受益很多:第一,不要让一个脚本承担所有职责;第二,能用数据定义做的事情,不要写死在代码里。
Unity 6 并不是一个让你不需要思考架构的“神器”。新版本确实让渲染、光照、工具链变得更好用了,但它不会替你把脚本组织好。架构设计依旧要靠你。所以这篇文章不打算把每个 API 都罗列一遍,而是用玩家原型这条主线,演示一套可复用的工程思路。
2. Unity 6 项目底座:版本、渲染与输入系统
在真正开始建项目之前,一定要先弄清楚 Unity 6 和旧版本的差异。Unity 6 是 2024 年 10 月发布的以 6000.0.x 为版本号的新一代主版本。它不再沿用 2022 LTS、2023 LTS 这种以年份命名的体系,这一点会直接影响你查找文档、选择插件兼容版本时的判断。
对 RPG 项目来说,Unity 6 涉及最多的变化集中在三块。第一,渲染管线和渲染后端;Unity 6 内置的 URP 和 HDRP 都做了较明显的底层重构。第二,新输入系统;新项目模板对新输入系统的支持和默认配置状态比旧版本更友好,刚接触的人容易在这里踩配置坑。第三,编辑器工具链;例如更完善的场景模板、更好的照明工具和烘焙速度,都会影响实际项目效率,但也意味着你在网上搜到的“老版本怎么调”的回答未必完全适用。
在版本选择上,我的建议比较保守:做 RPG,如果团队没有强 3A 画面需求,优先选带 URP 模板的 Unity 6 项目。URP 既能满足大多数 3D RPG 的视觉表现,又有相对低的适配成本,对移动端和 PC 跨平台也更友好。HDRP 适合需要高保真光照的项目,但它的性能开销更大,调试复杂度也更高,不建议新手第一个项目就直接挑战。
另一个务实的建议是,在 Player Settings 的 Active Input Handling 里把输入模式调成 Both,或者明确使用 Input System。下面表格简单列一下几种输入配置的选择逻辑:
| 配置方式 | 适合场景 | 注意事项 |
|---|---|---|
| 仅旧版 Input Manager | 教学项目、原型验证 | 不支持新输入系统的复杂按键映射,代码简单但扩展有限 |
| 仅 Input System | 正式项目、多端发布 | 需要学习和配置 Action Asset,初期繁琐但长期收益明显 |
| Both | 过渡阶段、插件兼容测试 | 会出现两套输入代码并存,记得统一规范,避免团队混用 |
这里真正容易出错的地方是,你新建了 URP 项目,但导入旧资源包时发现材质颜色偏暗或者渲染效果不对。这是因为 Built-in 管线的 Shader 和 URP 不完全兼容。面对这种情况,要么把旧资源升级到 URP Shader,要么尽量寻找支持 URP 的插件版本。后面我们在创建项目时会用 URP 模板来规避这个问题。
看完这一节,你可以得到的结论是:不要因为 Unity 6 新就频繁更换项目模板和输入方案,选一套主流的 URP + Input System(或 Both)方案,然后用足够的项目周期去跑通它。技术的价值来自持续使用,而不是反复切换。
3. RPG 系统的顶层拆分:先把代码地图画出来
在动手写第一条移动代码之前,我建议你先在纸上画一画 RPG 项目的模块地图。这不是形式主义,是为了让你以后每次加功能时都有一个固定的“放代码的地方”。如果连文件放哪儿都是随机的,协作和复盘都会很痛苦。
一个典型的 Unity RPG 项目,从功能上看通常包含这些模块:角色控制与状态、交互、物品与背包、任务、对话、战斗、NPC AI、UI、存档、音频。完整实现它们是一个庞大的内容工程,但它们在代码结构上相对独立。把它们全部硬编码进 PlayerController 里一定会爆炸。正确做法是每个模块一个独立文件夹,再通过事件和接口把它们连接起来。
这里提供一个实用的目录规划示例:
Assets/ _Project/ Art/ Characters/ Environment/ UI/ Audio/ Prefabs/ Scenes/ Scripts/ Core/ Player/ PlayerController.cs States/ Camera/ Interaction/ Inventory/ Quest/ UI/ Settings/ Resources/用_Project作为项目根目录是个常见做法,它把第三方插件包和自己的代码区隔开。你不需要在这里机械照抄目录名,但有一个原则需要坚持:什么类型的文件就放在什么目录,核心资源不要全部堆积在 Assets 根目录下。
从系统关系看,RPG 中最容易出问题的三对关联是:玩家状态与动画、交互检测与 UI、物品数据与存档。这三对关联都需要轻量连接。玩家状态机只告诉动画系统“现在处于移动状态”,不直接操纵角色全身;交互检测只负责找出当前可交互对象,不负责弹对话;物品定义只保存静态数据,不关心物品在背包里的坑位。
下面用一个表格总结 RPG 各个系统在代码上的常见落点:
| 系统 | 常见代码落点 | 最容易失控的位置 |
|---|---|---|
| 角色控制 | CharacterController、状态机 | 把移动、动画、攻击全部堆在 Update 里 |
| 交互 | IInteractable、检测器 | 高频 Find 和直接依赖具体按钮 |
| 背包 | ScriptableObject、ItemStack | 数据与 UI 强耦合 |
| 对话 | DialogData、DialogManager | 在剧情脚本里直接改全局状态 |
| 任务 | QuestDefinition、QuestTracker | 任务条件写在 UI 逻辑里 |
| 存档 | SaveData、序列化服务 | 直接存 MonoBehaviour 引用 |
这幅“地图”是这篇文章后面每一节的组织依据。我们不会一口气做完所有系统,上半篇先把玩家控制、状态机、相机、交互、背包这些地基打完。地基稳了,任务、对话、战斗系统后续才有地方挂。
4. 创建 Unity 6 URP 项目与环境配置
Unity 6 环境配置的第一步是选择正确的项目模板。打开 Unity Hub,在 New Project 页面选择 Universal 3D,也就是 URP 模板,而不是普通的 3D 模板。Unity 6 自带的 URP 已经集成好渲染管线资产,省去了手动从包管理器安装和配置的麻烦。给项目命名时,建议使用不带空格的英文名,例如 RPGGuide,避免后续做版本管理和打包时出现路径坑。
模板创建完成之后,Unity 会自动打开编辑器。第一次启动时会出现编译和资源导入的过程,需要耐心等待。建议你先把默认的 SampleScene 改名成 Game_Main 并保存到一个固定的 Scenes 目录,避免后续场景越建越多却分不清哪个是入口场景。
接下来检查两个关键配置。第一个是输入系统配置。如果你打算使用 Input System 新方案,需要通过 Window > Package Manager 确认 Input System 包已经安装,并在 Player Settings > Active Input Handling 里选择 Input System Package 或 Both。如果你希望用最简单的 Input.GetAxis 先跑通逻辑,至少也要确认当前项目允许旧版输入管理器工作。
第二个是通过 Project Settings 的 Graphics 确认使用的 Render Pipeline Asset 是 URP 资产。URP 模板创建后通常已经配置正确,但导入旧项目或升级项目时容易出现默认渲染管线为空的情况。如果你的游戏画面完全没有光照效果,先去这里看,而不是怀疑自己的 Shader 代码写错了。
在做正式版本管理时,建议创建项目的第一时间就初始化一个 Git 仓库,并配置好 Unity 项目的 .gitignore,避免把 Library 和 Temp 目录提交进仓库。这是工程习惯,但很多新手会在项目进行到一两个月后才补 Git,每次回滚都变得非常痛苦。Unity 6 项目体积通常不小,尽早投入版本管理是成本最低的保障。
环境准备完成后,我们不需要在这一步引入任何复杂插件。Cinemachine 会在后面的相机章节再安装,Input System 也可以等你有需要时再接。先保持最小依赖,让项目尽量干净。
5. 跑通玩家移动:角色控制器的最小实现
在 Unity 中让角色移动,比较成熟的方案是使用 CharacterController 组件。它处理了常见的碰撞、斜坡和重力相关逻辑,比直接使用 Rigidbody 写角色移动要简单也更稳。如果你是从 2D 项目转来做 3D RPG 的,需要先记住一个区别:CharacterController 是移动组件的“胶囊体”,不是物理刚体,它需要配合 transform 的旋转和 Move 方法使用。
先创建一个空物体命名为 Player,给它挂上 CharacterController,再设置 Capsule 的半径和高度,使其贴合你的角色模型。然后创建以下脚本。这里先用旧输入方式写一个最小版本,便于理解核心逻辑:
// Assets/_Project/Scripts/Player/PlayerController.cs using UnityEngine; [RequireComponent(typeof(CharacterController))] public class PlayerController : MonoBehaviour { [Header("移动参数")] [SerializeField] private float moveSpeed = 5f; [SerializeField] private float turnSmoothTime = 0.08f; [SerializeField] private float gravity = -9.81f; private CharacterController _controller; private Transform _cameraTransform; private float _turnSmoothVelocity; private float _verticalVelocity; private void Awake() { _controller = GetComponent<CharacterController>(); if (Camera.main != null) { _cameraTransform = Camera.main.transform; } } private void Update() { Vector2 input = new Vector2(Input.GetAxisRaw("Horizontal"), Input.GetAxisRaw("Vertical")); HandleGravity(); ApplyMove(input); } private void ApplyMove(Vector2 input) { if (_cameraTransform == null) { return; } Vector3 forward = Vector3.ProjectOnPlane(_cameraTransform.forward, Vector3.up).normalized; Vector3 right = Vector3.ProjectOnPlane(_cameraTransform.right, Vector3.up).normalized; Vector3 moveDirection = (forward * input.y + right * input.x).normalized; if (moveDirection.sqrMagnitude > 0.01f) { float targetAngle = Mathf.Atan2(moveDirection.x, moveDirection.z) * Mathf.Rad2Deg; float smoothedAngle = Mathf.SmoothDampAngle( transform.eulerAngles.y, targetAngle, ref _turnSmoothVelocity, turnSmoothTime); transform.rotation = Quaternion.Euler(0f, smoothedAngle, 0f); _controller.Move(moveDirection * moveSpeed * Time.deltaTime); } } private void HandleGravity() { if (_controller.isGrounded && _verticalVelocity < 0f) { _verticalVelocity = -2f; } _verticalVelocity += gravity * Time.deltaTime; Vector3 verticalMove = new Vector3(0f, _verticalVelocity, 0f) * Time.deltaTime; _controller.Move(verticalMove); } }这段脚本里有三个值得解释的点。第一,输入方向必须从屏幕坐标系转到世界坐标系。开发 RPG 时我们通常希望角色按 WASD 时,朝“摄像机视角中的前方”移动,而不是朝世界坐标 Z 轴移动,所以代码里用摄像机的 forward 和 right 重新组合了输入。第二,Mathf.Atan2(moveDirection.x, moveDirection.z) * Mathf.Rad2Deg将移动向量转成世界旋转角度,这比用LookRotation更可控。第三,重力没有让人物直接从屏幕掉下去,而是通过CharacterController.Move每一帧作用累计值。这是防止角色在斜坡上卡住的标准写法。
运行后,把 Player 放到一个带地面的场景里,用 WASD 测试移动。如果角色平移而没有碰撞,说明 CharacterController 没有正确挂在 Player 上;如果角色只转头不动,大概率是摄像机没有正确赋值,或者_cameraTransform在 Awake 时还没有找到主摄像机。遇到这个问题,先在场景里确认只有一个 Main Camera。
这个最小版本已经能跑,但它把所有逻辑都塞在了一个 MonoBehaviour 里。下一次你要加跳跃、攻击、翻滚、受伤,Update 方法里的 if 会越来越多,这正是我们要在前面强调的“状态膨胀”问题。解决它的下一步,就是引入有限状态机。
6. 用有限状态机管理“角色状态膨胀”
先看一个大家都很熟悉的反面案例:角色控制器里靠布尔值和 if 判断来区分状态。isAttacking、isRolling、isDead互相组合,甚至还要区分“是否能移动”“是否播放受伤动画”“是否允许切换攻击”。这类代码在状态数量只有两三个时还能忍受,一旦状态超过五个,修改一个状态就可能让其他状态变得不可控。
有限状态机的思路是把每个状态封装成一个独立对象,让状态之间通过入口和退出方法来切换。从普通 Update 代码切到状态机,新手往往会觉得前两三天反而更慢,这是因为多写了几个类。但这个成本换来的是长期的代码秩序。RPG 角色经常会有 Idle、Move、Attack、Skill、Hit、Roll、Dead 这类状态,用状态机管理后,每个状态只关心自己的进入条件和退出条件。
下面是一个精简的状态机核心结构:
// Assets/_Project/Scripts/Player/PlayerStateBase.cs using UnityEngine; public abstract class PlayerStateBase { protected PlayerController Player { get; private set; } protected PlayerStateMachine Machine { get; private set; } public virtual void Enter(PlayerController player, PlayerStateMachine machine) { Player = player; Machine = machine; } public virtual void Exit() { } public virtual void Tick() { } }// Assets/_Project/Scripts/Player/PlayerStateMachine.cs using System; using System.Collections.Generic; public class PlayerStateMachine { private readonly Dictionary<Type, PlayerStateBase> _states = new Dictionary<Type, PlayerStateBase>(); private PlayerStateBase _currentState; public void AddState(PlayerStateBase state) { _states[state.GetType()] = state; } public void ChangeState<T>() where T : PlayerStateBase { if (_currentState is T) { return; } Type nextType = typeof(T); if (!_states.ContainsKey(nextType)) { return; } _currentState?.Exit(); _currentState = _states[nextType]; _currentState.Enter(Player, this); } public void Tick() { _currentState?.Tick(); } }同一个文件里的 Player 还需要把输入交给状态机。它的核心职责变成:采集输入,提供给状态机判断,而不直接写具体的攻击翻滚逻辑。IdleState 和 MoveState 的职责非常清晰:
// Assets/_Project/Scripts/Player/States/IdleState.cs using UnityEngine; public class IdleState : PlayerStateBase { public override void Tick() { if (Player.HasMoveInput) { Machine.ChangeState<MoveState>(); } } }// Assets/_Project/Scripts/Player/States/MoveState.cs using UnityEngine; public class MoveState : PlayerStateBase { public override void Tick() { if (!Player.HasMoveInput) { Machine.ChangeState<IdleState>(); return; } Player.HandleMove(); } }状态机的代码看起来比原来的简单移动脚本多了很多类,但好处在于,以后需要新增 Sprint、Attack 状态时,不需要去改动 IdleState 和 MoveState 之外的业务代码。比如新增冲刺状态,只需要创建一个 SprintState,然后在 IdleState 或 MoveState 中按下 Shift 时切换。
在管理状态机时有几个容易踩的坑。第一,状态 Enter 和 Exit 里不要做耗时操作,比如加载资源、读取配置,它们每秒钟可能会执行多次。第二,不要把动画名写死在状态内部,尽量使用 Animator 参数名,比如SetBool("IsMoving", true),否则以后重命名动画参数会导致隐藏错误。第三,如果状态切换瞬间需要播放动画,建议在状态机上再加一个“按百分比切换打断”的规则,也就是上一状态还没到可打断帧,拒绝切换,这是战斗系统精细化之后要考虑的一点。
状态切换需要小心的一点是,使用泛型 ChangeState 虽然阅读方便,但在代码里对目标状态的引用依赖字符串之外的强类型。只要你状态类的命名稳定,这套方案非常容易理解。继续把项目做大后,可以考虑为状态机增加“历史栈”,用于实现从翻滚、受击状态退出后回到原状态的逻辑。这在动作 RPG 中会很有用。
7. 用 Cinemachine 搭一个第三人称相机
RPG 项目里,主角移动只是开始,镜头的跟随方案会直接影响手感。Unity 6 URP 项目里推荐使用 Cinemachine 来搭建镜头系统。它比手动在 LateUpdate 里写position = target.position + offset要稳,因为在旋转、碰撞、平滑跟随等场景中,手写代码要考虑很多边界问题,而 Cinemachine 已经把这些能力工具化了。
先在 Package Manager 里安装 Cinemachine。随后在场景里确认场景中有一个摄像机。选中主摄像机,在菜单 Component > Cinemachine 里添加 CinemachineBrain,这是主摄像机处理相机混合的关键组件。接着创建一个 Virtual Camera,把 Player 拖到它的 Follow 和 Look At 字段。为了让镜头跟随在一个第三人称位置,你可以适当调整 Body 组件的参数,比如把 Screen X、Screen Y 设置为 0.5,让玩家显示在屏幕中央附近。
这里真正的一个新手误区是,同时存在多个 Virtual Camera 时,没有 CinemachineBrain 的主摄像机会不知所措,表现为画面不断抖动或不跟随主角。一定要确保主摄像机有 CinemachineBrain,并且场景中只有一个主相机 Culling Mask 能看见 Player。
Cinemachine 的 Aim 组件选择 Do Nothing 通常适合 RPG 中“玩家面向移动方向”的方案。如果你的镜头希望跟随鼠标和右摇杆旋转视角,则要考虑在 Virtual Camera 的 Aim 上使用 POV,并且由玩家输入来控制水平角与垂直角。这里有一个镜头碰撞问题需要提前设计:野外 RPG 场景很可能出现悬崖和墙壁,镜头穿墙会严重破坏沉浸感。Cinemachine 的 Body 组件里有碰撞检测参数,需要在场景中给角色、环境和镜头配置好对应的物理 Layer,否则碰撞过滤会变得非常麻烦。
镜头手感调试需要结合你的角色移动手感一起进行。移动速度过快而镜头平滑过硬,玩家会感觉眩晕;速度太慢则会让 RPG 探索变得无聊。建议在真机上反复调节,而不是只看编辑器里的静止画面。
从架构角度看,镜头系统应该独立于输入和角色移动。不要让 PlayerController 直接控制跟随目标旋转。正确的连接方式是:玩家输入产生“期望视角方向”,角色移动消费它,镜头只负责呈现它。这样做的好处是以后新增锁定目标、过场动画、死亡回放时,不需要改写角色控制逻辑。
8. 场景交互:从“碰撞检测”到“接口设计”
RPG 场景中有大量可交互对象:宝箱、NPC、书籍、门。最糟糕的写法是在玩家脚本里挨个判断 GameObject 的名字或者 Tag。因为场景里的内容会不断增长,用标签和名称判断的做法会让玩家控制器变成所有策划需求的堆砌地。真正的解耦做法是:交互对象自己声明“我可以被交互”,玩家侧只需要检测谁满足了交互条件。
在 C# 中,这个“声明”可以用接口来完成。定义一个 IInteractable 接口,让所有可交互对象实现它。这样,玩家的检测器不需要知道面前是宝箱还是 NPC,它只需要知道对象实现了这个接口,并且把对象暴露给 UI 显示提示即可。来看核心代码:
// Assets/_Project/Scripts/Interaction/IInteractable.cs using UnityEngine; public interface IInteractable { string Prompt { get; } void Interact(GameObject interactor); }接口定义只做两件事:读取提示文本,执行交互。真正的 NPC 对话逻辑、宝箱开启逻辑,都放在各自对象的实现里。例如一个可开启的宝箱:
// Assets/_Project/Scripts/Interaction/Chest.cs using UnityEngine; public class Chest : MonoBehaviour, IInteractable { [SerializeField] private string prompt = "打开宝箱"; [SerializeField] private bool isOpen; public string Prompt => isOpen ? string.Empty : prompt; public void Interact(GameObject interactor) { if (isOpen) { return; } isOpen = true; Debug.Log("宝箱开启"); // 在这里调用背包系统添加物品,或播放动画与音效 } }玩家的交互检测器每帧在角色周围做 OverlapSphere 检测,找出最近的 IInteractable,然后通过 UI 显示交互提示。当玩家按下交互键时,就调用当前对象的 Interact 方法。这样的好处是,添加新交互对象完全不需要改动玩家脚本。
检测器的核心逻辑是高频非物理更新,它适合放在 Update 而不是 FixedUpdate,因为交互提示不需要和物理模拟同频。同时在每一帧都做 OverlapSphere 会产生一定开销,但控制半径在 2 到 3 米、检测频率 0.2 秒一次就足够了。你可以使用协程或计时器来降低检测频率。
这里有一个容易忽略的细节:如果交互提示 UI 需要显示“按 F 打开宝箱”还是“按 F 与 NPC 对话”,那么 UI 不应该自己偷偷去分类。正确做法是让 UI 只读取 IInteractable.Prompt,对话和宝箱文字由实现者返回。这就是面向接口设计的价值。
物理检测没有命中 Collider 的接口对象时,会出现提示 UI 一直残留的情况。建议检测器在退出交互范围时显式地把当前对象置空,并触发 UI 隐藏事件。如果你用 Layer 过滤所有 Detectable 对象,而不是对所有 Collider 都执行GetComponent<IInteractable>,性能会更好。
从这一步开始,你已经体验到“玩家脚本”逐渐瘦身的过程。角色控制器负责移动,状态机负责行为切换,交互检测器只负责寻找可交互对象。后续把任务、对话接到 NPC 上时,只需要在 NPC 实现里调用对话系统,不需要玩家脚本再去判断“这是不是任务 NPC”。
9. 用 ScriptableObject 重构背包物品数据
RPG 开发中,物品数据的组织方式决定了后期你做内容填充时是否痛苦。最原始的方式是在代码里写死物品属性,比如一个方法返回一堆金币和一把剑,但一旦数量到了几十上百,你就要为每件物品改代码。正确做法是把物品定义资源化,让策划在编辑器里直接创建物品资产,而代码只需要读取。
Unity 的 ScriptableObject 非常适合表达这类静态数据。每个物品做成一个资产文件,资产内部保存名称、图标、类型、说明、最大堆叠数等字段。Inventory 系统里保存的只是“持有哪个物品定义、持有数量”,而不是复制每个物品的所有属性。这样做有很多好处:修改一把剑的基础伤害时不用改玩家的存档;给物品换图标时,所有引用它的地方自动更新。
下面是一个物品定义的示例:
// Assets/_Project/Scripts/Inventory/ItemType.cs public enum ItemType { Consumable, Equipment, Material, Quest }// Assets/_Project/Scripts/Inventory/ItemDefinition.cs using UnityEngine; [CreateAssetMenu(fileName = "Item", menuName = "RPG/Item Definition")] public class ItemDefinition : ScriptableObject { [Header("基础信息")] public string itemName; [TextArea] public string description; public ItemType itemType; public Sprite icon; public int maxStack = 99; }创建资产时,在 Project 面板右键选择 Create > RPG > Item Definition,就能生成一个物品资产。你不需要把它拖到场景里,它只是配置数据。背包持有的是物品堆:
// Assets/_Project/Scripts/Inventory/ItemStack.cs using System; [Serializable] public class ItemStack { public ItemDefinition Definition; public int Count; public ItemStack(ItemDefinition definition, int count) { Definition = definition; Count = count; } }Inventory 管理这些堆的增删改