十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Unity实战:角色加入队伍系统设计与实现

Unity实战:角色加入队伍系统设计与实现 最近在整理《博德之门坠入阿弗纳斯》战役第三阶段的时候我遇到一个很典型的场景中狗正式加入了大狗和小狗的队伍。表面上看这只是一句轻描淡写的剧情描述但真正落地到游戏开发或跑团工具里背后其实牵着一整套角色状态、队伍数据、触发事件和跟随 AI 的联动逻辑。这篇文章围绕“角色加入队伍”这个事件展开用大狗、小狗、中狗三个角色作为示例完整拆解一个可供 CRPG、Unity 小游戏或跑团辅助工具复用的队伍系统实现方案。无论你是正在做独立游戏还是想把线下跑团流程做成数字工具这套设计都能直接借鉴。1. 背景与核心概念角色加入队伍到底在做什么1.1 一次“加入队伍”背后发生了什么在大多数角色扮演游戏里“加入队伍”不是一个普通的对话动作而是一次状态转移。玩家和 NPC 对话完成后NPC 会从一个“场景中的独立角色”变成“玩家队伍中的成员”从那一刻起它的行为方式、交互逻辑、数据归属都发生了变化。具体来说至少包含这几件事角色档案从场景 NPC 数据中脱离进入队伍成员列表角色不再受普通待机 AI 控制而是切换成“跟随玩家”的 AIUI 上要出现该角色的头像、状态、装备或技能入口存档系统要记录“该角色已经在队伍里”这个事实避免重进场景后角色又出现在原地剧情系统要能感知到队伍变化从而解锁新的对话或任务分支。这些环节如果都写在同一个剧情脚本里前期确实省事后面会越来越难维护。尤其像《博德之门坠入阿弗纳斯》这种流程长、角色多、分支复杂的项目角色加入事件会反复出现。如果没有一套统一的队伍管理模块每个加入事件都各自实现一套后续加存档、加 UI、加多角色跟随时会非常痛苦。1.2 常见应用场景不只是 CRPG很多项目都会遇到类似需求剧情伙伴加入主线 NPC 因为剧情需要加入队伍比如“中狗加入了大狗和小狗的队伍”宠物/召唤物系统玩家通过道具或技能召唤一只宠物跟随自己策略类游戏单位管理新单位解锁后出现在编队中跑团数字工具TRPG 主持人把一张新的角色卡拖入当前玩家列表。这套逻辑的核心只有一个把“加入”这个行为统一为一个可复用的模块让后续任何角色加入时只需要配置数据不需要重复写代码。1.3 本文示例中的三个角色为了避免涉及原版模组的复杂剧情我用一组代称来说明角色初始状态加入方式说明大狗玩家队伍成员初始在场作为队伍中的前排角色小狗玩家队伍成员初始在场作为队伍中的后排角色中狗场景 NPC与玩家对话后加入本次要实现的加入事件主体这个模型很干净大狗和小狗已经在队伍中玩家在一个场景中遇到中狗对话后中狗变成队伍成员并开始跟随玩家在地图中移动。2. 环境准备与版本说明2.1 开发环境本文的代码示例以 Unity 2021.3 LTS 和 C# 为例。之所以选择 Unity是因为它的组件式开发非常适合演示“队伍系统”这种模块化设计。如果你用的是 Godot、Unreal 或者纯后端服务核心思想是一样的只需要把 API 换成对应引擎的写法。建议环境如下Unity 2021.3 LTS 或更高版本IDEVisual Studio 2022 或 Rider不需要第三方插件示例项目使用 URP 或内置渲染管线均可。版本不需要严格一致。重点是代码里的设计思路大家按照自己项目的实际版本来即可。2.2 项目目录设计为了不把代码写成一团乱麻建议先规划目录Assets/ ├── Scripts/ │ ├── Team/ │ │ ├── TeamManager.cs │ │ └── CharacterProfile.cs │ ├── NPC/ │ │ ├── InteractableNPC.cs │ │ └── FollowTarget.cs │ └── UI/ │ └── TeamPanel.cs └── Data/ ├── BigDog.asset ├── SmallDog.asset └── MidDog.assetScripts/Team存放队伍核心逻辑Scripts/NPC存放 NPC 交互与跟随逻辑Scripts/UI负责队伍界面刷新Data目录放角色配置资产。这样每个模块的职责都比较清晰。3. 核心设计角色数据与队伍状态3.1 角色数据模型在开发之前先定义“角色”到底是什么。一个角色不仅有一个名字还需要唯一 ID、显示名、是否永久成员、加入描述等信息。在 Unity 中用ScriptableObject管理这种配置数据非常合适方便策划在不改代码的情况下配置新角色。文件路径Assets/Scripts/Team/CharacterProfile.csusing UnityEngine; [CreateAssetMenu(fileName CharacterProfile, menuName Team/CharacterProfile)] public class CharacterProfile : ScriptableObject { [Header(基础信息)] public string characterId; public string displayName; [Header(队伍属性)] public bool isPermanentMember true; [Header(叙事信息)] [TextArea] public string joinDescription; }这个类本身不包含任何运行时的“当前状态”它只是角色的静态配置。比如大狗、小狗、中狗的 ID、名称、加入描述都在各自的CharacterProfile资产里配置。那运行时状态放哪里放在TeamManager维护的列表里。这样同一个角色配置资产可以在多个场景中复用不会因为一个存档的进度不同而污染资产本身。3.2 队伍管理器谁在队伍里TeamManager是队伍系统的核心负责维护当前队伍成员列表提供“加入队伍”“是否已在队伍”“获取全部成员”等操作。文件路径Assets/Scripts/Team/TeamManager.csusing System; using System.Collections.Generic; using UnityEngine; public class TeamManager : MonoBehaviour { public static TeamManager Instance { get; private set; } [SerializeField] private ListCharacterProfile teamMembers new ListCharacterProfile(); public IReadOnlyListCharacterProfile TeamMembers teamMembers; public event ActionCharacterProfile OnMemberJoined; private void Awake() { if (Instance ! null Instance ! this) { Destroy(gameObject); return; } Instance this; DontDestroyOnLoad(gameObject); } public bool IsInTeam(string characterId) { foreach (var member in teamMembers) { if (member.characterId characterId) { return true; } } return false; } public bool AddMember(CharacterProfile profile) { if (profile null) { Debug.LogWarning(AddMember 失败角色配置为空。); return false; } if (IsInTeam(profile.characterId)) { Debug.Log(${profile.displayName} 已经在队伍中。); return false; } teamMembers.Add(profile); OnMemberJoined?.Invoke(profile); Debug.Log(${profile.displayName} 加入了队伍。); return true; } }这里有几个关键点使用单例模式是为了方便 NPC 交互脚本快速调用但代码里也做了重复实例保护DontDestroyOnLoad让队伍状态在场景切换时保留OnMemberJoined是事件UI 和剧情系统可以监听它而不是每帧去轮询队伍列表IReadOnlyList防止外部直接修改队伍数据所有修改都通过AddMember进入。为什么不做成静态类因为静态类虽然简单但不容易在 Inspector 中看到运行状态也不方便保存和扩展。用MonoBehaviour挂在场景中的GameManager对象上既能实时调试又能配合存档系统。3.3 从“场景 NPC”到“队员”的状态切换在角色没有加入队伍前它由场景中的普通 NPC 脚本控制。加入后它需要变成“跟随者”。最简单的做法是在InteractableNPC中加入成功后禁用 NPC 自己的独立 AI 脚本启用FollowTarget脚本并把它的leader指向玩家。这种状态切换用代码表达就是npcController.enabled false; FollowTarget follow npcGameObject.GetComponentFollowTarget(); follow.enabled true; follow.leader playerTransform;后面完整示例中会把这套逻辑整合进触发过程。这里的重点是加入队伍不是一个标签而是一组组件状态的切换。4. 完整实战中狗加入大狗和小狗的队伍4.1 创建示例场景在 Unity 中新建一个场景包含以下物体Player玩家角色带有CharacterController或RigidbodyBigDog已经在队伍中的角色使用FollowTarget跟随玩家SmallDog已经在队伍中的角色使用FollowTarget跟随玩家MidDog场景中的 NPC带有InteractableNPC脚本GameManager挂载TeamManager。为了让“中狗加入”更有仪式感可以给中狗一个可交互的提示比如玩家靠近后按 E 键触发对话。4.2 创建角色配置资产在Assets/Data目录下创建三个CharacterProfileBigDog.assetcharacterId big_dogdisplayName 大狗SmallDog.assetcharacterId small_dogdisplayName 小狗MidDog.assetcharacterId mid_dogdisplayName 中狗joinDescription 中狗在路边遇到大狗和小狗决定一起行动。在TeamManager的teamMembers列表中默认把大狗和小狗配置进去代表初始队伍状态。中狗不要放在初始列表里它要等玩家触发后加入。4.3 编写可交互 NPC触发加入文件路径Assets/Scripts/NPC/InteractableNPC.csusing UnityEngine; public class InteractableNPC : MonoBehaviour { [Header(角色配置)] public CharacterProfile profile; [Header(加入后状态切换)] public MonoBehaviour npcController; public FollowTarget followTarget; public Transform playerTransform; [Header(交互按键)] public KeyCode interactKey KeyCode.E; private bool hasJoined false; private void Update() { if (hasJoined) { return; } if (IsPlayerNearby() Input.GetKeyDown(interactKey)) { TryJoinTeam(); } } private bool IsPlayerNearby() { if (playerTransform null) { return false; } float distance Vector3.Distance(transform.position, playerTransform.position); return distance 2.5f; } private void TryJoinTeam() { bool success TeamManager.Instance.AddMember(profile); if (!success) { Debug.Log(加入失败角色可能已经在队伍中。); return; } hasJoined true; if (npcController ! null) { npcController.enabled false; } if (followTarget ! null) { followTarget.enabled true; followTarget.leader playerTransform; } Debug.Log(${profile.displayName} 的加入流程已执行完毕。); } }在这个脚本里npcController是普通 NPC 的 AI 控制组件followTarget是跟随组件。中狗加入后原来的 NPC 行为被禁用跟随行为被启用这就完成了从“世界角色”到“队伍角色”的切换。需要说明的是npcController的类型我故意写成MonoBehaviour而不是具体某个 AI 类。这样任何继承自MonoBehaviour的控制脚本都可以直接拖进去不会有强耦合。4.4 实现跟随 AI文件路径Assets/Scripts/NPC/FollowTarget.csusing UnityEngine; public class FollowTarget : MonoBehaviour { [Header(跟随目标)] public Transform leader; [Header(移动参数)] public float followSpeed 4f; public float stoppingDistance 1.5f; public float rotationSpeed 8f; private void Update() { if (leader null) { return; } float distance Vector3.Distance(transform.position, leader.position); if (distance stoppingDistance) { return; } Vector3 direction (leader.position - transform.position).normalized; transform.position direction * followSpeed * Time.deltaTime; if (direction.sqrMagnitude 0.001f) { Quaternion lookRotation Quaternion.LookRotation(direction); transform.rotation Quaternion.Slerp(transform.rotation, lookRotation, Time.deltaTime * rotationSpeed); } } }这个跟随实现比较基础适合做一个可运行的最小原型。大狗、小狗、中狗都挂同一个FollowTarget只是各自的leader指向玩家。如果多个角色都跟随玩家会出现一个问题它们会挤在同一个stoppingDistance的位置互相重叠。更完整的方案应该给每个跟随者一个偏移位置比如public Vector3 followOffset; private void Update() { Vector3 targetPosition leader.position followOffset; transform.position Vector3.MoveTowards(transform.position, targetPosition, followSpeed * Time.deltaTime); }偏移量可以在 Inspector 中按角色分别调整让大狗站在左侧小狗站在右侧中狗站在后方看起来更有队伍感。4.5 队伍面板 UI当角色加入队伍后界面最好能立刻反馈。下面是一个最简单的 UI 刷新脚本它监听TeamManager.OnMemberJoined事件实时更新面板文字。文件路径Assets/Scripts/UI/TeamPanel.csusing System.Text; using UnityEngine; using UnityEngine.UI; public class TeamPanel : MonoBehaviour { public Text teamListText; private void OnEnable() { if (TeamManager.Instance ! null) { TeamManager.Instance.OnMemberJoined OnMemberJoined; } } private void OnDisable() { if (TeamManager.Instance ! null) { TeamManager.Instance.OnMemberJoined - OnMemberJoined; } } private void Start() { RefreshPanel(); } private void OnMemberJoined(CharacterProfile profile) { RefreshPanel(); } private void RefreshPanel() { if (teamListText null) { return; } var builder new StringBuilder(); builder.AppendLine(当前队伍); foreach (var member in TeamManager.Instance.TeamMembers) { builder.AppendLine($- {member.displayName}); } teamListText.text builder.ToString(); } }在场景中创建一个 Canvas挂上Text组件然后把TeamPanel挂到同一个物体上并把teamListText拖入。运行游戏后初始队伍会显示大狗和小狗玩家与中狗对话后UI 会立刻多出一项“中狗”。4.6 运行与验证把大狗、小狗、中狗的CharacterProfile配置好给中狗挂上InteractableNPC给大狗和小狗挂上FollowTarget将玩家角色拖入对应的playerTransform和leader字段。运行场景后预期流程如下画面中的队伍面板显示大狗、小狗玩家靠近中狗按下 E 键控制台输出“中狗 加入了队伍。”中狗原来的 NPC 控制被禁用随后跟随玩家移动队伍面板新增一项“中狗”。如果这一套流程能跑通说明队伍加入的核心链路已经完成。接下来可以在这个基础上扩展剧情分支、对话系统、存档系统和阵型控制。5. 常见问题与排查思路在实现这套系统时比较容易遇到下面几个问题。问题现象常见原因解决思路按下交互键没有反应playerTransform未赋值或距离判断太远检查场景引用适当调大IsPlayerNearby的距离角色重复加入只判断了hasJoined没判断TeamManager.IsInTeam在TryJoinTeam中增加二次校验加入后角色仍然站在原地FollowTarget未启用或leader为空确认followTarget.enabled true和leader已赋值场景切换后队伍丢失TeamManager未设置DontDestroyOnLoad在Awake中调用DontDestroyOnLoad多个跟随者重叠在一起没有设置偏移量给每个跟随角色配置followOffsetScriptableObject 数据改动后无变化运行时修改资产退出后未保存在编辑模式下修改资产并保存项目UI 列表不刷新没有监听OnMemberJoined事件在OnEnable注册在OnDisable注销5.1 按下交互键没有反应这个问题最常见的不是代码逻辑而是引用没有拖对。尤其是playerTransform很多人在场景中忘记把这个字段拖到玩家身上。建议在Update中临时设置断点先确认IsPlayerNearby()返回的是不是true再确认Input.GetKeyDown(KeyCode.E)是否真的触发了。如果你在测试时焦点不在游戏窗口中键盘输入可能不会生效。5.2 角色加入后被其他脚本覆盖部分 NPC 在加入后原来的 AI 还是会尝试控制角色移动。比如某些项目使用了CharacterController.Move而跟随逻辑也用transform.position直接赋值二者会产生冲突。解决办法是给 NPC 的移动控制组件加一个总开关也就是上面用到的npcController.enabled false。如果原来的 AI 是写在Update中的禁用它即可如果写在FixedUpdate中同样有效。5.3 存档后角色状态丢失队伍系统没有接入存档之前每次进游戏都要重新加入这对实际项目来说是不可接受的。建议在保存数据时把当前teamMembers中的角色 ID 序列化为字符串列表加载时再根据 ID 反向找到对应的CharacterProfile。这样就不需要把整个ScriptableObject存档只需要存 ID 即可。6. 最佳实践与工程建议6.1 数据驱动不要在代码里硬编码角色名角色名称、对话、加入描述都应该放在配置文件里。现在代码中虽然已经用了CharacterProfile但在实际项目中还可以进一步把它扩展成包含头像、职业、等级、初始装备等字段。策划只需要配置一份资产不需要改代码。6.2 用事件解耦队伍系统与 UI/剧情TeamManager不应该直接调用 UI 的刷新方法而是通过OnMemberJoined事件通知其他模块。这样如果以后想让角色加入时播放一段演出、解锁一个新技能、弹出提示窗口只需要监听同一个事件即可不需要改动TeamManager本身。6.3 避免单例滥用虽然示例用了单例但单例并不是唯一选择。在大型项目中有些人会用依赖注入容器管理TeamManager或者通过事件总线传递队伍变更消息。如果你的项目已经使用了成熟的框架建议优先遵循项目现有的架构不需要为了一个小功能强行引入单例。6.4 跟随 AI 的若干优化点本文的FollowTarget是最简单的移动方式适合演示。实际项目中可以优化使用NavMeshAgent做寻路避免角色卡在障碍物上引入阵型系统为大狗、小狗、中狗分别配置站位当队伍成员之间有碰撞时使用松散的跟随规则而不是完全“贴”在玩家身后如果场景中有大量跟随者可以降低跟随 AI 的更新频率提高性能。6.5 注意叙事与系统的同步“中狗加入大狗和小狗的队伍”在剧情里是一个非常自然的瞬间但在系统中一定要保证先更新队伍数据再触发演出最后让角色切换为跟随状态。顺序反了可能会出现 UI 已经显示中狗入队但玩家转过头发现中狗还站在原地的情况。6.6 生产环境下的存档与备份如果你在开发一个长期的 RPG 项目队伍数据属于玩家核心数据必须谨慎处理。建议在存档时带上版本号方便以后做数据迁移。不要把存档数据直接写到玩家本地目录而不做校验否则一旦存档损坏玩家丢的不只是队伍还有整个游戏进度。7. 总结与后续扩展通过“中狗加入大狗和小狗的队伍”这个案例我们完整实现了一套可复用的队伍加入流程角色配置用CharacterProfile管理队伍状态由TeamManager统一维护NPC 交互触发加入事件加入后角色从普通 NPC 切换为跟随者UI 通过事件刷新。这套逻辑可以继续扩展到更多场景。接下来你可以试着完善这几个方向加入队伍时的演出系统让中狗先走一段路再自动归队阵型跟随大狗左侧、小狗右侧、中狗后方角色切换在队伍面板中点击角色头像控制不同角色带队剧情条件限制玩家完成某个任务后才能触发中狗的入队事件。实际项目中优先关注队伍数据的一致性和存档安全。只要这两点做扎实了后续加再多玩法都会很顺畅。如果你正在做 CRPG 或者跑团辅助工具建议先把今天这套最小流程跑通再逐步加功能。队伍系统看起来简单真正做好却需要不断打磨。希望这篇文章对你有帮助也欢迎在实际开发后回来交流你踩过的坑。
返回列表