5. 战斗与技能系统
2026/4/15大约 7 分钟
5. 战斗与技能系统:让英雄释放技能
5.1 技能系统架构
技能系统是 MOBA 游戏最核心的部分。每个英雄有 4 个技能(被动 + 3 个主动),每个技能有自己的冷却时间、伤害类型、范围和特效。
技能数据定义
C#
// SkillData.cs
using Godot;
[GlobalClass]
public partial class SkillData : Resource
{
[Export] public string SkillId { get; set; } = "";
[Export] public string SkillName { get; set; } = "";
[Export] public string Description { get; set; } = "";
// 类型
[Export] public SkillType Type { get; set; } = SkillType.Active;
[Export] public DamageType Damage { get; set; } = DamageType.Physical;
[Export] public TargetType Target { get; set; } = TargetType.Enemy;
// 数值
[Export] public float[] BaseDamage { get; set; } = { 100 }; // 按技能等级
[Export] public float ManaCost { get; set; } = 50;
[Export] public float Cooldown { get; set; } = 8.0f;
[Export] public float Range { get; set; } = 10.0f;
[Export] public float Radius { get; set; } = 3.0f; // 范围技能的半径
// 弹道
[Export] public bool IsProjectile { get; set; } = false;
[Export] public float ProjectileSpeed { get; set; } = 20.0f;
// Buff/Debuff
[Export] public string BuffId { get; set; } = "";
[Export] public float BuffDuration { get; set; } = 0;
}
public enum SkillType { Passive, Active }
public enum DamageType { Physical, Magic, True }
public enum TargetType { Enemy, Ally, Self, Position, Direction }GDScript
# skill_data.gd
class_name SkillData
extends Resource
@export var skill_id: String = ""
@export var skill_name: String = ""
@export var description: String = ""
# 类型
enum SkillType { PASSIVE, ACTIVE }
enum DamageType { PHYSICAL, MAGIC, TRUE }
enum TargetType { ENEMY, ALLY, SELF, POSITION, DIRECTION }
@export var type: SkillType = SkillType.ACTIVE
@export var damage_type: DamageType = DamageType.PHYSICAL
@export var target_type: TargetType = TargetType.ENEMY
# 数值
@export var base_damage: Array[float] = [100] # 按技能等级
@export var mana_cost: float = 50
@export var cooldown: float = 8.0
@export var range: float = 10.0
@export var radius: float = 3.0 # 范围技能的半径
# 弹道
@export var is_projectile: bool = false
@export var projectile_speed: float = 20.0
# Buff/Debuff
@export var buff_id: String = ""
@export var buff_duration: float = 05.2 技能管理器
每个英雄身上挂一个技能管理器,管理技能的冷却、释放和升级:
C#
// SkillManager.cs
using Godot;
using System.Collections.Generic;
public partial class SkillManager : Node
{
private Dictionary<string, SkillData> _skills = new();
private Dictionary<string, float> _cooldowns = new();
private Dictionary<string, int> _skillLevels = new();
private int _skillPoints = 1; // 初始1个技能点
private HeroBase _hero;
public override void _Ready()
{
_hero = GetParent<HeroBase>();
}
// 初始化技能
public void InitSkills(HeroData heroData)
{
foreach (var skillId in heroData.SkillIds)
{
if (string.IsNullOrEmpty(skillId)) continue;
var skillData = GD.Load<SkillData>($"res://resources/skills/{skillId}.tres");
_skills[skillId] = skillData;
_cooldowns[skillId] = 0;
_skillLevels[skillId] = skillData.Type == SkillType.Passive ? 1 : 0;
}
}
// 尝试释放技能
public bool CastSkill(int slotIndex, Vector3 targetPos, Node3D targetUnit)
{
string skillId = GetSkillIdBySlot(slotIndex);
if (string.IsNullOrEmpty(skillId)) return false;
var skill = _skills[skillId];
// 检查条件
if (_skillLevels[skillId] <= 0) return false; // 未学习
if (_cooldowns[skillId] > 0) return false; // 冷却中
if (_hero.GetCurrentMp() < skill.ManaCost) return false; // 蓝不够
// 消耗蓝量
_hero.SpendMp(skill.ManaCost);
// 进入冷却
_cooldowns[skillId] = skill.Cooldown;
// 根据技能类型执行效果
ExecuteSkill(skill, _skillLevels[skillId] - 1, targetPos, targetUnit);
return true;
}
private void ExecuteSkill(SkillData skill, int skillLevel, Vector3 targetPos, Node3D targetUnit)
{
float damage = skill.BaseDamage[Mathf.Min(skillLevel, skill.BaseDamage.Length - 1)];
if (skill.IsProjectile)
{
// 弹道技能:创建飞行物体
SpawnProjectile(skill, damage, targetPos, targetUnit);
}
else if (skill.Radius > 0)
{
// 范围技能:对区域内所有目标造成伤害
ApplyAreaDamage(targetPos, skill.Radius, damage, skill.Damage);
}
else
{
// 单体技能:直接对目标造成伤害
if (targetUnit != null && targetUnit.HasMethod("TakeDamage"))
{
targetUnit.Call("TakeDamage", damage,
skill.Damage == DamageType.Physical ? "physical" : "magic", _hero);
}
}
// 应用 Buff/Debuff
if (!string.IsNullOrEmpty(skill.BuffId) && targetUnit != null)
{
ApplyBuff(skill.BuffId, skill.BuffDuration, targetUnit);
}
}
// 创建弹道
private void SpawnProjectile(SkillData skill, float damage, Vector3 targetPos, Node3D targetUnit)
{
var projectileScene = GD.Load<PackedScene>("res://scenes/effects/projectile.tscn");
var projectile = projectileScene.Instantiate<Node3D>();
projectile.Position = _hero.GlobalPosition + Vector3.Up;
GetTree().Root.AddChild(projectile);
// 设置弹道飞行方向和速度
if (projectile.HasMethod("Launch"))
{
projectile.Call("Launch", targetPos, skill.ProjectileSpeed, damage,
skill.Damage == DamageType.Physical ? "physical" : "magic",
_hero, _hero.TeamId);
}
}
// 范围伤害
private void ApplyAreaDamage(Vector3 center, float radius, float damage, DamageType damageType)
{
// 用物理查询找到范围内所有敌方单位
var spaceState = _hero.GetWorld3D().DirectSpaceState;
var shape = new SphereShape3D();
shape.Radius = radius;
var query = new PhysicsShapeQueryParameters3D();
query.Shape = shape;
query.Transform = new Transform3D(Basis.Identity, center);
query.CollisionMask = GetEnemyLayerMask();
var results = spaceState.IntersectShape(query);
foreach (var result in results)
{
var collider = result["collider"].AsGodotObject() as Node3D;
if (collider != null && collider.HasMethod("TakeDamage"))
{
collider.Call("TakeDamage", damage,
damageType == DamageType.Physical ? "physical" : "magic", _hero);
}
}
}
// 冷却计时
public override void _Process(double delta)
{
var keys = new List<string>(_cooldowns.Keys);
foreach (var skillId in keys)
{
if (_cooldowns[skillId] > 0)
{
_cooldowns[skillId] -= (float)delta;
if (_cooldowns[skillId] < 0) _cooldowns[skillId] = 0;
}
}
}
// 升级技能
public bool LevelUpSkill(int slotIndex)
{
if (_skillPoints <= 0) return false;
string skillId = GetSkillIdBySlot(slotIndex);
if (string.IsNullOrEmpty(skillId)) return false;
var skill = _skills[skillId];
if (skill.Type == SkillType.Passive) return false; // 被动不能升级
_skillLevels[skillId]++;
_skillPoints--;
return true;
}
// 英雄升级时获得技能点
public void OnHeroLevelUp()
{
_skillPoints++;
}
public float GetCooldownRemaining(string skillId) => _cooldowns.GetValueOrDefault(skillId, 0);
public int GetSkillLevel(string skillId) => _skillLevels.GetValueOrDefault(skillId, 0);
private string GetSkillIdBySlot(int slot) =>
slot < _hero.Data.SkillIds.Length ? _hero.Data.SkillIds[slot] : "";
private uint GetEnemyLayerMask() =>
_hero.TeamId == 0 ? 0b0000_0000_0000_0000_0000_0000_0001_1110u
: 0b0000_0000_0000_0000_0000_0000_0000_0010u;
}GDScript
# skill_manager.gd
extends Node
var skills: Dictionary = {} # skill_id -> SkillData
var cooldowns: Dictionary = {} # skill_id -> 剩余冷却
var skill_levels: Dictionary = {} # skill_id -> 技能等级
var skill_points: int = 1 # 初始1个技能点
var hero: Node3D
func _ready():
hero = get_parent()
func init_skills(hero_data: HeroData):
for skill_id in hero_data.skill_ids:
if skill_id == "":
continue
var skill_data = load("res://resources/skills/%s.tres" % skill_id)
skills[skill_id] = skill_data
cooldowns[skill_id] = 0
skill_levels[skill_id] = 1 if skill_data.type == SkillData.SkillType.PASSIVE else 0
func cast_skill(slot_index: int, target_pos: Vector3, target_unit: Node3D) -> bool:
var skill_id = get_skill_id_by_slot(slot_index)
if skill_id == "":
return false
var skill: SkillData = skills[skill_id]
# 检查条件
if skill_levels[skill_id] <= 0:
return false
if cooldowns[skill_id] > 0:
return false
# 消耗蓝量、进入冷却
cooldowns[skill_id] = skill.cooldown
execute_skill(skill, skill_levels[skill_id] - 1, target_pos, target_unit)
return true
func execute_skill(skill: SkillData, skill_level: int, target_pos: Vector3, target_unit: Node3D):
var dmg = skill.base_damage[min(skill_level, skill.base_damage.size() - 1)]
var dmg_type = "physical" if skill.damage_type == SkillData.DamageType.PHYSICAL else "magic"
if skill.is_projectile:
spawn_projectile(skill, dmg, target_pos, target_unit)
elif skill.radius > 0:
apply_area_damage(target_pos, skill.radius, dmg, dmg_type)
else:
if target_unit and target_unit.has_method("take_damage"):
target_unit.take_damage(dmg, dmg_type, hero)
func spawn_projectile(skill: SkillData, damage: float, target_pos: Vector3, target_unit: Node3D):
var projectile_scene = load("res://scenes/effects/projectile.tscn")
var projectile = projectile_scene.instantiate()
projectile.position = hero.global_position + Vector3.UP
get_tree().root.add_child(projectile)
if projectile.has_method("launch"):
projectile.launch(target_pos, skill.projectile_speed, damage,
"physical" if skill.damage_type == SkillData.DamageType.PHYSICAL else "magic",
hero, hero.team_id)
func apply_area_damage(center: Vector3, radius: float, damage: float, damage_type: String):
var space_state = hero.get_world_3d().direct_space_state
var shape = SphereShape3D.new()
shape.radius = radius
var query = PhysicsShapeQueryParameters3D.new()
query.shape = shape
query.transform = Transform3D(Basis.IDENTITY, center)
var results = space_state.intersect_shape(query)
for result in results:
var collider = result["collider"] as Node3D
if collider and collider.has_method("take_damage"):
collider.take_damage(damage, damage_type, hero)
func _process(delta):
for skill_id in cooldowns:
if cooldowns[skill_id] > 0:
cooldowns[skill_id] -= delta
if cooldowns[skill_id] < 0:
cooldowns[skill_id] = 0
func level_up_skill(slot_index: int) -> bool:
if skill_points <= 0:
return false
var skill_id = get_skill_id_by_slot(slot_index)
if skill_id == "":
return false
skill_levels[skill_id] += 1
skill_points -= 1
return true
func on_hero_level_up():
skill_points += 1
func get_skill_id_by_slot(slot: int) -> String:
return hero.data.skill_ids[slot] if slot < hero.data.skill_ids.size() else ""5.3 弹道系统
弹道是技能系统的重要部分——火球、箭矢、冲击波等飞行物都需要弹道系统:
C#
// Projectile.cs
using Godot;
public partial class Projectile : Area3D
{
private Vector3 _direction;
private float _speed;
private float _damage;
private string _damageType;
private Node3D _owner;
private int _ownerTeam;
private float _maxDistance = 50.0f;
private float _traveled = 0;
public void Launch(Vector3 target, float speed, float damage,
string damageType, Node3D owner, int ownerTeam)
{
_direction = (target - GlobalPosition).Normalized();
_speed = speed;
_damage = damage;
_damageType = damageType;
_owner = owner;
_ownerTeam = ownerTeam;
LookAt(target, Vector3.Up);
}
public override void _PhysicsProcess(double delta)
{
var movement = _direction * _speed * (float)delta;
Position += movement;
_traveled += movement.Length();
if (_traveled >= _maxDistance)
QueueFree(); // 超出范围自动消失
}
// 碰到敌方单位时造成伤害
public override void _Ready()
{
BodyEntered += OnBodyEntered;
}
private void OnBodyEntered(Node3D body)
{
if (body == _owner) return;
// 检查是否是敌方单位
if (body.IsInGroup("heroes") || body.IsInGroup("minions") || body.IsInGroup("towers"))
{
// 简化:通过 team_id 判断
if (body.Get("team_id").Variant() != _ownerTeam)
{
if (body.HasMethod("TakeDamage"))
body.Call("TakeDamage", _damage, _damageType, _owner);
QueueFree(); // 命中后消失
}
}
}
}GDScript
# projectile.gd
extends Area3D
var direction: Vector3
var speed: float
var damage: float
var damage_type: String
var owner_unit: Node3D
var owner_team: int
var max_distance: float = 50.0
var traveled: float = 0
func launch(target: Vector3, spd: float, dmg: float, dmg_type: String,
owner_node: Node3D, team: int):
direction = (target - global_position).normalized()
speed = spd
damage = dmg
damage_type = dmg_type
owner_unit = owner_node
owner_team = team
look_at(target, Vector3.UP)
func _ready():
body_entered.connect(_on_body_entered)
func _physics_process(delta):
var movement = direction * speed * delta
position += movement
traveled += movement.length()
if traveled >= max_distance:
queue_free()
func _on_body_entered(body: Node3D):
if body == owner_unit:
return
if body.is_in_group("heroes") or body.is_in_group("minions") or body.is_in_group("towers"):
if body.get("team_id") != owner_team:
if body.has_method("take_damage"):
body.take_damage(damage, damage_type, owner_unit)
queue_free()5.4 Buff 和 Debuff 系统
技能可以附加持续效果,比如加速、减速、眩晕等:
C#
// BuffData.cs
[GlobalClass]
public partial class BuffData : Resource
{
[Export] public string BuffId { get; set; } = "";
[Export] public string BuffName { get; set; } = "";
[Export] public float Duration { get; set; } = 3.0f;
[Export] public bool IsDebuff { get; set; } = false;
// 效果
[Export] public float SpeedMultiplier { get; set; } = 1.0f;
[Export] public float DamagePerSecond { get; set; } = 0;
[Export] public bool Stunned { get; set; } = false; // 眩晕
}
// BuffInstance.cs - 挂在英雄身上的运行时Buff
public partial class BuffInstance : Node
{
public BuffData Data { get; set; }
public float RemainingTime { get; private set; }
private Node3D _target;
public override void _Ready()
{
RemainingTime = Data.Duration;
_target = GetParent<Node3D>();
}
public override void _Process(double delta)
{
RemainingTime -= (float)delta;
// 持续伤害
if (Data.DamagePerSecond > 0)
{
_target.Call("TakeDamage", Data.DamagePerSecond * (float)delta, "magic", null);
}
if (RemainingTime <= 0)
{
RemoveBuff();
}
}
private void RemoveBuff()
{
// 移除效果(恢复速度等)
QueueFree();
}
}GDScript
# buff_data.gd
class_name BuffData
extends Resource
@export var buff_id: String = ""
@export var buff_name: String = ""
@export var duration: float = 3.0
@export var is_debuff: bool = false
# 效果
@export var speed_multiplier: float = 1.0
@export var damage_per_second: float = 0
@export var stunned: bool = false # 眩晕
# buff_instance.gd - 挂在英雄身上的运行时Buff
extends Node
var data: BuffData
var remaining_time: float
var target: Node3D
func _ready():
remaining_time = data.duration
target = get_parent()
func _process(delta):
remaining_time -= delta
# 持续伤害
if data.damage_per_second > 0:
target.take_damage(data.damage_per_second * delta, "magic", null)
if remaining_time <= 0:
remove_buff()
func remove_buff():
queue_free()5.5 技能冷却 UI
技能栏需要显示冷却遮罩,让玩家知道技能何时可用:
┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐
│ Q │ │ E │ │ R │ │ B/F │
│旋风斩│ │ 冲锋 │ │ 大招 │ │回城 │
│ │ │█████│ │ │ │ │
└─────┘ └─────┘ └─────┘ └─────┘
冷却中
(灰色遮罩+倒计时)章节导航
| 上一章 | 下一章 |
|---|---|
| ← 4. 英雄角色系统 | 6. 小兵与防御塔 → |
