尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

从零构建游戏角色PVE战斗演示:以Unity技能系统与AI设计为例

从零构建游戏角色PVE战斗演示:以Unity技能系统与AI设计为例 在游戏开发与角色设计领域为特定角色构建一套清晰、可玩且富有深度的PVE玩家对环境战斗演示是验证角色机制、技能循环和战斗体验的关键环节。本文将以虚构的“朗基努斯”预设快反小队队长——“盖茵特”为例系统性地拆解如何从零开始设计并实现一个完整的PVE战斗演示。无论你是独立游戏开发者、游戏策划还是对角色动作与技能系统感兴趣的爱好者都能通过本文获得从设计思路到技术实现的全流程实战经验。1. 角色“盖茵特”的核心概念与设计目标在进行任何代码编写之前我们必须明确“盖茵特”作为“快反小队队长”的定位。这决定了其技能、属性和战斗风格的设计方向。1.1 角色定位与核心玩法“快反小队”通常意味着高机动性、快速部署和应对突发威胁。因此“盖茵特”的设计应围绕以下几点展开高机动性拥有冲刺、闪避或短距离突进技能能够快速接近或脱离敌人。爆发伤害作为队长需要具备在短时间内倾泻高额伤害的能力以快速削减精英或首领敌人的血量。战术指挥可选可以设计为能为队友或自身召唤物提供增益效果体现其领导职能。攻防一体在激进进攻的同时需要有一定的生存或自我保护手段如护盾、格挡或吸血。基于此我们为“盖茵特”预设一套基础技能组普通攻击快速的三连击最后一击附带小幅击退。技能一迅捷突刺向指定方向快速突进对路径上的敌人造成伤害。技能二能量护盾生成一个持续数秒的护盾吸收一定量的伤害。技能三朗基努斯裁决终极技能对锁定目标发动一次高额伤害的强力刺击对低血量目标有额外伤害加成。1.2 PVE演示场景设计目标一个优秀的PVE演示不仅仅是展示技能特效它需要验证技能循环的流畅性技能之间衔接是否自然冷却时间CD和资源消耗是否合理。战斗策略的有效性玩家是否需要根据敌人类型选择技能释放顺序。数值平衡的直观感受伤害数字、敌人血条变化是否能给玩家带来正反馈。视听表现的冲击力动作、音效、特效是否贴合角色设定。我们的演示将创建一个简单的战斗场景包含几波不同类型的敌人最终以一个Boss战结束全面测试“盖茵特”的各项能力。2. 开发环境与项目结构准备我们将使用一个通用的游戏开发框架思路进行说明其概念适用于UnityC#、Unreal EngineC/蓝图乃至一些2D游戏引擎。本文将以伪代码和设计模式为主重点阐述逻辑而非绑定于特定引擎API。2.1 基础环境假设游戏引擎Unity 2022.3 LTS 或类似版本思路通用。编程语言C#。开发工具Visual Studio 2022 或 Rider。目标平台Windows PC用于演示。2.2 项目核心结构规划一个清晰的项目结构是高效开发的基础。我们为“盖茵特”演示项目规划如下目录Assets/ ├── Scripts/ │ ├── Characters/ │ │ ├── CharacterBase.cs // 角色基础类生命值、移动等 │ │ ├── PlayerController.cs // 玩家输入控制 │ │ └── Gaient/ // 盖茵特专属脚本 │ │ ├── GaientController.cs // 盖茵特逻辑主控制器 │ │ ├── Skills/ // 技能脚本目录 │ │ │ ├── SkillBase.cs │ │ │ ├── Skill_QuickThrust.cs │ │ │ ├── Skill_EnergyShield.cs │ │ │ └── Skill_FinalJudgment.cs │ │ └── Data/ │ │ └── GaientData_SO.cs // ScriptableObject 存储角色数据 │ ├── AI/ │ │ ├── EnemyBase.cs │ │ ├── States/ // 敌人AI状态机 │ │ └── EnemyTypes/ // 不同敌人类型 │ └── Managers/ │ ├── GameManager.cs // 游戏流程控制 │ ├── WaveManager.cs // 敌人波次管理 │ └── UIManager.cs // UI管理 ├── Prefabs/ // 预制体 │ ├── Characters/ │ │ ├── Gaient.prefab │ │ └── Enemies/ │ └── VFX/ // 视觉特效 ├── Animations/ │ ├── Gaient/ │ └── Enemies/ └── Scenes/ └── Gaient_PVE_Demo.unity // 主演示场景3. 核心系统拆解角色与技能框架3.1 角色基础属性系统所有战斗单位的基类负责管理生命值、状态等通用属性。// 文件路径Assets/Scripts/Characters/CharacterBase.cs using UnityEngine; public abstract class CharacterBase : MonoBehaviour { [Header(基础属性)] [SerializeField] protected float maxHealth 100f; protected float currentHealth; [SerializeField] protected bool isInvincible false; // 无敌状态用于技能期间 public delegate void OnHealthChanged(float current, float max); public event OnHealthChanged HealthChanged; public delegate void OnCharacterDefeated(GameObject character); public event OnCharacterDefeated CharacterDefeated; protected virtual void Start() { currentHealth maxHealth; HealthChanged?.Invoke(currentHealth, maxHealth); } public virtual void TakeDamage(float damage) { if (isInvincible) return; currentHealth - damage; currentHealth Mathf.Clamp(currentHealth, 0, maxHealth); HealthChanged?.Invoke(currentHealth, maxHealth); if (currentHealth 0) { Die(); } } public virtual void Heal(float amount) { currentHealth amount; currentHealth Mathf.Clamp(currentHealth, 0, maxHealth); HealthChanged?.Invoke(currentHealth, maxHealth); } protected virtual void Die() { Debug.Log(gameObject.name 被击败。); CharacterDefeated?.Invoke(gameObject); // 后续可播放死亡动画、触发掉落等 Destroy(gameObject, 2f); // 2秒后销毁对象 } }3.2 技能基类设计采用面向对象的设计所有技能继承自同一个基类便于统一管理和扩展。// 文件路径Assets/Scripts/Characters/Gaient/Skills/SkillBase.cs using UnityEngine; public abstract class SkillBase : MonoBehaviour { [Header(技能基础配置)] public string skillName; public KeyCode shortcutKey; // 快捷键 public float cooldownTime 3f; // 冷却时间 public float manaCost 20f; // 能量/魔法值消耗 [TextArea] public string description; // 技能描述 [Header(状态)] protected float currentCooldown 0f; protected bool isSkillActive false; protected GaientController owner; // 技能持有者 public bool IsReady currentCooldown 0f; public void Initialize(GaientController gaient) { owner gaient; } void Update() { if (currentCooldown 0) { currentCooldown - Time.deltaTime; } OnSkillUpdate(); } public bool TryActivate() { // 检查条件冷却、能量、是否正在释放其他技能等 if (!IsReady || owner.CurrentMana manaCost || isSkillActive) { Debug.Log($技能 {skillName} 无法释放。冷却{!IsReady}能量{owner.CurrentMana manaCost}激活中{isSkillActive}); return false; } owner.ConsumeMana(manaCost); currentCooldown cooldownTime; isSkillActive true; OnSkillActivate(); return true; } // 子类必须实现的方法 protected abstract void OnSkillActivate(); // 技能激活时调用 protected virtual void OnSkillUpdate() { } // 每帧更新用于持续技能 protected virtual void OnSkillEnd() // 技能结束时调用 { isSkillActive false; } }4. “盖茵特”完整实现与PVE演示搭建4.1 盖茵特主控制器这是“盖茵特”的大脑集成移动、技能管理和数据。// 文件路径Assets/Scripts/Characters/Gaient/GaientController.cs using UnityEngine; using System.Collections.Generic; public class GaientController : CharacterBase { [Header(移动设置)] public float moveSpeed 8f; public float rotationSpeed 720f; private CharacterController characterController; private Vector3 moveDirection; [Header(能量系统)] public float maxMana 100f; private float currentMana; public float manaRegenRate 10f; // 每秒回复量 [Header(技能系统)] public ListSkillBase skills new ListSkillBase(); private SkillBase ultimateSkill; // 特殊引用终极技能 [Header(数据引用)] public GaientData_SO characterData; private void Awake() { characterController GetComponentCharacterController(); currentMana maxMana; InitializeSkills(); } void InitializeSkills() { foreach (var skill in skills) { skill.Initialize(this); // 假设第三个技能是终极技能 if (skill is Skill_FinalJudgment) { ultimateSkill skill; } } } void Update() { HandleMovement(); HandleSkillInput(); RegenerateMana(); } void HandleMovement() { float horizontal Input.GetAxis(Horizontal); float vertical Input.GetAxis(Vertical); moveDirection new Vector3(horizontal, 0, vertical).normalized; if (moveDirection.magnitude 0.1f) { // 移动 characterController.Move(moveDirection * moveSpeed * Time.deltaTime); // 转向 Quaternion toRotation Quaternion.LookRotation(moveDirection, Vector3.up); transform.rotation Quaternion.RotateTowards(transform.rotation, toRotation, rotationSpeed * Time.deltaTime); } } void HandleSkillInput() { // 遍历技能列表检测快捷键 foreach (var skill in skills) { if (Input.GetKeyDown(skill.shortcutKey)) { skill.TryActivate(); } } // 终极技能特殊键位例如 R 键 if (Input.GetKeyDown(KeyCode.R) ultimateSkill ! null) { ultimateSkill.TryActivate(); } } void RegenerateMana() { if (currentMana maxMana) { currentMana manaRegenRate * Time.deltaTime; currentMana Mathf.Clamp(currentMana, 0, maxMana); // 可以在这里触发UI更新事件 } } public void ConsumeMana(float amount) { currentMana - amount; currentMana Mathf.Clamp(currentMana, 0, maxMana); } public float CurrentMana currentMana; public float MaxMana maxMana; }4.2 技能具体实现以“迅捷突刺”为例展示一个位移攻击技能的实现。// 文件路径Assets/Scripts/Characters/Gaient/Skills/Skill_QuickThrust.cs using UnityEngine; public class Skill_QuickThrust : SkillBase { [Header(突刺技能参数)] public float thrustDistance 8f; public float thrustDuration 0.3f; // 突刺动作持续时间 public float damage 30f; public LayerMask enemyLayer; private Vector3 thrustDirection; private float thrustTimer; private bool isThrusting false; protected override void OnSkillActivate() { Debug.Log(释放技能迅捷突刺); // 获取鼠标或摇杆方向这里简化为角色面朝方向 thrustDirection owner.transform.forward; thrustTimer thrustDuration; isThrusting true; // 播放动画和音效此处为伪代码 // owner.GetComponentAnimator().SetTrigger(QuickThrust); // AudioManager.Instance.PlaySFX(ThrustSound); } protected override void OnSkillUpdate() { if (isThrusting) { thrustTimer - Time.deltaTime; // 在突刺期间强制移动角色并检测碰撞 owner.transform.Translate(thrustDirection * (thrustDistance / thrustDuration) * Time.deltaTime, Space.World); // 简单球形检测前方敌人 RaycastHit[] hits Physics.SphereCastAll(owner.transform.position, 1.5f, thrustDirection, 2f, enemyLayer); foreach (var hit in hits) { CharacterBase enemy hit.collider.GetComponentCharacterBase(); if (enemy ! null) { enemy.TakeDamage(damage); // 避免一帧内多次伤害 } } if (thrustTimer 0) { EndThrust(); } } } void EndThrust() { isThrusting false; OnSkillEnd(); } }4.3 敌人波次管理系统用于控制PVE演示中敌人的生成节奏。// 文件路径Assets/Scripts/Managers/WaveManager.cs using System.Collections; using System.Collections.Generic; using UnityEngine; [System.Serializable] public class Wave { public string waveName; public ListGameObject enemyPrefabs; // 这波敌人的预制体列表 public float spawnInterval 1f; // 生成间隔 public int totalEnemiesToSpawn 5; // 总生成数量 } public class WaveManager : MonoBehaviour { public static WaveManager Instance; public ListWave waves; public Transform[] spawnPoints; public float timeBetweenWaves 5f; private int currentWaveIndex 0; private int enemiesAlive 0; private bool isSpawning false; void Awake() { if (Instance null) Instance this; } void Start() { StartCoroutine(StartNextWave()); } IEnumerator StartNextWave() { if (currentWaveIndex waves.Count) { Debug.Log(所有波次已完成准备Boss战。); // 触发Boss生成事件 yield break; } yield return new WaitForSeconds(timeBetweenWaves); Debug.Log($开始波次{waves[currentWaveIndex].waveName}); StartCoroutine(SpawnWave(waves[currentWaveIndex])); } IEnumerator SpawnWave(Wave wave) { isSpawning true; for (int i 0; i wave.totalEnemiesToSpawn; i) { if (spawnPoints.Length 0) yield break; Transform spawnPoint spawnPoints[Random.Range(0, spawnPoints.Length)]; GameObject enemyPrefab wave.enemyPrefabs[Random.Range(0, wave.enemyPrefabs.Count)]; GameObject enemy Instantiate(enemyPrefab, spawnPoint.position, spawnPoint.rotation); enemy.GetComponentCharacterBase().CharacterDefeated OnEnemyDefeated; enemiesAlive; yield return new WaitForSeconds(wave.spawnInterval); } isSpawning false; } void OnEnemyDefeated(GameObject enemy) { enemiesAlive--; // 可选增加玩家经验、金币等 // 如果当前波次敌人全部死亡且不在生成中则开始下一波 if (enemiesAlive 0 !isSpawning) { currentWaveIndex; StartCoroutine(StartNextWave()); } } }4.4 构建演示场景与配置场景搭建在Unity中创建一个地形或平面作为战场放置一些简单的障碍物。创建“盖茵特”预制体创建一个空对象命名为Gaient。添加CharacterController组件。添加GaientController脚本。在GaientController的Skills列表中拖入三个技能脚本组件Skill_QuickThrust,Skill_EnergyShield,Skill_FinalJudgment并分别配置快捷键如 Q, E, R。配置角色数据生命值、能量、速度等。配置敌人创建几种敌人预制体如近战兵、远程兵挂载EnemyBase脚本和简单的AI如寻路攻击玩家。配置波次在场景中创建一个空对象WaveManager挂载WaveManager脚本。在Inspector面板中定义2-3波敌人并指定生成点。设置摄像机与灯光将主摄像机设置为跟随“盖茵特”布置基础光照。运行测试点击Play使用WASD移动按Q/E/R释放技能击败不断涌来的敌人。5. 常见问题与调试技巧在实现上述系统的过程中你可能会遇到一些典型问题。问题现象可能原因排查与解决思路技能按下无反应1. 快捷键冲突或被其他UI元素拦截。2. 技能条件不满足冷却中、能量不足。3.TryActivate()方法返回false。1. 检查Unity的Input Manager或事件系统。2. 在TryActivate()方法内添加Debug.Log打印检查失败的具体原因。3. 确保技能脚本已正确添加到GaientController的技能列表中并被初始化。角色移动卡顿或穿墙1.CharacterController与碰撞体交互问题。2. 移动速度过快单帧位移过大。3. 技能位移未考虑碰撞。1. 调整CharacterController的Slope Limit、Step Offset和Skin Width。2. 使用Time.deltaTime平滑移动或使用CharacterController.Move()并处理碰撞结果。3. 在技能位移时可考虑使用Physics.SphereCast进行预检测。敌人不攻击或行为异常1. AI状态机逻辑错误。2. 导航网格NavMesh未烘焙或设置不正确。3. 玩家图层Layer未被敌人检测层包含。1. 使用Debug.DrawRay或可视化工具检查敌人的视线和攻击范围。2. 确保场景已烘焙导航网格且敌人拥有NavMeshAgent组件。3. 检查敌人AI脚本中关于目标查找的代码确认其Layer Mask包含玩家所在层。伤害计算异常或多次触发1. 伤害检测在一帧内多次触发如SphereCast在持续技能中每帧检测。2. 伤害数值未从数据配置中读取而是硬编码。1. 为敌人添加一个短暂的“受伤无敌时间”或使用一个HashSet记录本帧已受到伤害的敌人避免重复计算。2. 将伤害值、冷却时间等配置数据抽离到ScriptableObject中便于平衡调整。UI显示不更新1. UI脚本未订阅角色属性变化的事件。2. 事件在角色销毁时未正确取消订阅可能导致空引用。1. 在CharacterBase和GaientController中定义事件如OnHealthChanged,OnManaChanged让UI脚本在Start或OnEnable时订阅。2. 在UI脚本的OnDisable中取消订阅事件。6. 工程优化与进阶实践一个可演示的版本完成后可以从以下方面提升其完整性和工程化水平。6.1 使用ScriptableObject进行数据驱动将角色的属性、技能的参数剥离成可配置的资源文件使策划能独立调整数值无需修改代码。// 文件路径Assets/Scripts/Characters/Gaient/Data/GaientData_SO.cs using UnityEngine; [CreateAssetMenu(fileName NewGaientData, menuName Character Data/Gaient)] public class GaientData_SO : ScriptableObject { [Header(基础属性)] public float baseHealth 100f; public float baseMana 100f; public float baseMoveSpeed 8f; public float baseManaRegen 10f; [Header(技能数据)] public SkillData quickThrustData; public SkillData energyShieldData; public SkillData finalJudgmentData; } [System.Serializable] public class SkillData { public float cooldown; public float manaCost; public float damage; public float range; public float duration; // 对于持续技能 }然后在GaientController和各个技能脚本中引用此ScriptableObject从数据中读取数值。6.2 实现更复杂的技能系统技能连招记录上一次释放的技能根据条件触发不同的连招效果。技能天赋/升级为技能设计多个等级或分支通过消耗资源解锁动态修改SkillData。技能效果系统设计一个Buff/Debuff系统技能可以施加“灼烧”、“减速”、“破甲”等状态效果。6.3 完善敌人AI与Boss战状态机FSM为敌人实现Idle闲置、Patrol巡逻、Chase追逐、Attack攻击、Flee逃跑等状态。行为树Behavior Tree对于更复杂的Boss行为可以考虑使用行为树插件如Node Canvas来设计多阶段、条件触发的技能。Boss阶段转换在Boss的EnemyBase脚本中监控血量百分比到达阈值时切换阶段改变攻击模式和技能。6.4 性能优化考虑对象池对于频繁生成和销毁的敌人、子弹、特效使用对象池进行管理避免频繁的Instantiate和Destroy调用造成的GC垃圾回收压力。动画事件将伤害检测、特效生成等逻辑绑定到动画关键帧事件上使动作与逻辑更同步而非单纯依赖Update计时。物理查询优化使用Physics.SphereCastNonAlloc或OverlapSphereNonAlloc替代SphereCastAll和OverlapSphere避免GC分配。通过以上步骤你不仅完成了一个角色“盖茵特”的PVE演示更搭建了一个可扩展的小型动作游戏战斗框架。这个框架的核心思想——基于组件的角色设计、数据驱动的技能系统、事件驱动的通信——是构建更复杂游戏系统的坚实基础。
返回列表