Unity简单攻击及受击实现
通用开发思路
在开发任何复杂系统(如战斗系统)时,建议遵循以下通用的三步走思路:
- 构建信息和数据的容器 :思考我们需要哪些数据?(例如:攻击力、生命值、技能列表)。
- 确定获取数据的方法 :这些数据从哪里来?(例如:数据库、配置文件、或者硬编码在脚本中)。
- 使用数据实现需求 :如何利用这些数据来执行逻辑?(例如:攻击时如何计算伤害 = 攻击力 - 防御力)。
基础设施搭建 (Data Layer)
无论做实时还是回合制,角色背后的数值模型往往是通用的。我们先构建这层“地基”。
首先,我们需要一个类来描述“技能”。
- 属性 :技能名称、技能威力。
[System.Serializable]public class CombatSkill{ public string skillName; public int attackPower;
public CombatSkill(string name, int power) { this.skillName = name; this.attackPower = power; }}这是一个纯数据类(不继承 MonoBehaviour),用于存储角色的所有属性。它就像一张“角色卡”。
using System.Collections.Generic;
[System.Serializable]public class CombatCharacterInfo{ public string name; public float maxHp; public float currentHp; public int attackPower; public int defensePower; public List<CombatSkill> skillList;
public CombatCharacterInfo(string name, float hp, int atk, int def) { this.name = name; this.maxHp = hp; this.currentHp = hp; this.attackPower = atk; this.defensePower = def; this.skillList = new List<CombatSkill>(); }}为了方便测试,我们创建一个静态类来“伪造”数据库,提供初始化数据的方法。
using System.Collections.Generic;
public class ItemInfoCombat{ private static Dictionary<int, CombatCharacterInfo> characterDatabase;
static ItemInfoCombat() { characterDatabase = new Dictionary<int, CombatCharacterInfo>();
// ID 0: 玩家 (高血量,低攻) CombatCharacterInfo player = new CombatCharacterInfo("Alex", 200, 10, 5); player.skillList.Add(new CombatSkill("Slash", 20)); characterDatabase.Add(0, player);
// ID 1: 敌人 (低血量,高攻) CombatCharacterInfo enemy = new CombatCharacterInfo("Monster", 100, 25, 0); enemy.skillList.Add(new CombatSkill("Bite", 30)); characterDatabase.Add(1, enemy); }
public static CombatCharacterInfo GetCharacterInfo(int id) { if (characterDatabase.ContainsKey(id)) { // 教学提示:实际项目中应返回 Deep Copy (深拷贝),防止修改源数据 return characterDatabase[id]; } return null; }}实时战斗系统
实时攻击的核心是:
攻击的生效时机由动画的某一帧决定,而不是由玩家按键决定。同时更注重物理碰撞和时机。
也就是说,按下攻击键 ≠ 立刻造成伤害
真正的伤害时刻通常发生在动画中刀挥出去的某个瞬间。
为了实现这个,我们必须用到 动画事件(Animation Event) 。
从最简单的开始:
按下某个键触发攻击。
在 Update 中写:
if (Input.GetKeyDown(KeyCode.J)){ Attack();}Attack() 只是一个触发,不负责做攻击判定。
它只做一件事: 播放攻击动画 。
我们在 Animator 中准备一个 Attack 动画, 并使用 Trigger 参数 来控制它播放。
为什么要用 Trigger?
因为攻击动画播放一次就结束,不需要维持状态。
代码:
animator.SetTrigger("Attack");这里要强调一个概念:
我们并不是在
Attack()中去判断敌人的碰撞,而是在动画中指定合适的攻击帧,再调用判定函数。
打开攻击动画,把时间轴拖到“刀挥出去那一瞬间”。
在 Inspector 中添加一个动画事件,调用一个函数,比如 DealDamage()。
这个事件是极其重要的,它决定了攻击的“出招帧”。
为什么要用动画事件?
- 如果在按键瞬间就造成伤害,会出现“动画还没打出去就命中了”的问题
- 动画事件可以确保攻击判定与动画动作完全同步
- 专业游戏也都是使用动画事件来触发攻击逻辑的
到这里,我们已经做好了“攻击的时机”。
接下来,我们要讲“攻击的范围”。
近战攻击最标准的做法是 Hitbox + OverlapCircle 。
我们给角色加一个空物体 AttackPoint,作为攻击的位置。
然后用一个范围,比如一个圆形,作为攻击范围。
代码:
Collider2D[] hitEnemies = Physics2D.OverlapCircleAll( attackPoint.position, attackRange, enemyLayer);这个函数返回范围内所有敌人,然后我们依次对他们造成伤害。
foreach (Collider2D enemy in hitEnemies){ enemy.GetComponent<Enemy>().TakeDamage(attackDamage);}重点是:
- 攻击不是碰撞器撞上敌人
- 攻击是我们主动检测一个区域内的敌人
这是所有动作游戏最基本的攻击方式。
既然敌人可以被攻击,那他一定得有个血条
同时他还得能被攻击到
public void TakeDamage(int damage){ currentHP -= damage; animator.SetTrigger("Hurt");if (currentHP <= 0) Die();}说明:
- 扣血
- 播放受击动画
- 播放死亡动画或销毁对象
如果你想让攻击更有“手感”,可以加一些伪物理效果,比如击退(Knockback)。
回合制战斗系统
回合制战斗(如《宝可梦》、《崩坏:星穹铁道》)的核心在于流程控制和UI交互。
- 回合管理 :谁先动?谁后动?
- UI 驱动 :玩家通过点击菜单来行动,而不是按键。
- 状态流转 :等待输入 -> 执行动画 -> 结算伤害 -> 切换回合。
这是最复杂的脚本,它需要管理整个战场的状态。
2.1 定义辅助结构
public enum AttackDirection { PlayerToEnemy, EnemyToPlayer }2.2 管理器核心逻辑
using UnityEngine;using UnityEngine.UI;using System.Collections;
public class CombatManagerTurnBased : MonoBehaviour{ public static CombatManagerTurnBased Instance;
[Header("UI 引用")] public Transform skillFrameContainer; // 技能按钮的父物体 public GameObject skillFramePrefab; // 技能按钮预制体 public Slider playerHpBar; public Slider enemyHpBar;
[Header("运行时数据")] public CombatCharacterInfo playerInfo; public CombatCharacterInfo enemyInfo;
private AttackDirection currentTurn; // 当前回合
private void Awake() { Instance = this; }
private void Start() { // 游戏开始,初始化一场 ID 0 vs ID 1 的战斗 InitializeBattle(0, 1); }
public void InitializeBattle(int playerId, int enemyId) { playerInfo = ItemInfoCombat.GetCharacterInfo(playerId); enemyInfo = ItemInfoCombat.GetCharacterInfo(enemyId);
InitializeUI();
currentTurn = AttackDirection.PlayerToEnemy; }
// ... (InitializeUI 代码省略,主要是实例化按钮和设置Slider) ...
// 玩家点击技能按钮后调用此方法 public void ExecuteAttack(CombatSkill skill, AttackDirection direction) { if (direction != currentTurn) return; // 不是你的回合不能动
StartCoroutine(ProcessTurn(skill, direction)); }
// 使用协程来处理回合流程(攻击 -> 动画 -> 结算 -> 延迟 -> 换人) private IEnumerator ProcessTurn(CombatSkill skill, AttackDirection direction) { var attacker = (direction == AttackDirection.PlayerToEnemy) ? playerInfo : enemyInfo; var defender = (direction == AttackDirection.PlayerToEnemy) ? enemyInfo : playerInfo;
int damage = Mathf.Max(0, (skill.attackPower + attacker.attackPower) - defender.defensePower); defender.currentHp -= damage;
Debug.Log($"{attacker.name} 使用 {skill.skillName} 造成了 {damage} 点伤害!");
UpdateHpBars();
if (defender.currentHp <= 0) { Debug.Log("战斗结束!"); yield break; }
SwitchTurn(); }
private void SwitchTurn() { if (currentTurn == AttackDirection.PlayerToEnemy) { currentTurn = AttackDirection.EnemyToPlayer; // 轮到敌人:启动AI协程 StartCoroutine(EnemyTurnRoutine()); } else { currentTurn = AttackDirection.PlayerToEnemy; // 轮到玩家:等待点击,无需操作 } }
// 简单的敌人AI private IEnumerator EnemyTurnRoutine() { yield return new WaitForSeconds(1.5f); // 模拟思考/发呆
// 随机选一个技能打玩家 var skill = enemyInfo.skillList[Random.Range(0, enemyInfo.skillList.Count)]; ExecuteAttack(skill, AttackDirection.EnemyToPlayer); }}每个技能按钮上挂载的脚本,用于连接数据和点击事件。
public class SkillFrame : MonoBehaviour{ private CombatSkill mySkill;
public void Setup(CombatSkill skill) { mySkill = skill; // 更新 Text 显示技能名和伤害... }
public void OnClick() { // 通知管理器:玩家想用这个技能攻击 CombatManagerTurnBased.Instance.ExecuteAttack(mySkill, AttackDirection.PlayerToEnemy); }}