4. 英雄角色系统
2026/4/15大约 5 分钟
4. 英雄角色系统:创建你的英雄
4.1 英雄数据结构
每个英雄有一套基础属性,用 Godot 的 Resource 来定义:
C#
// HeroData.cs
using Godot;
using System.Collections.Generic;
[GlobalClass]
public partial class HeroData : Resource
{
[Export] public string HeroName { get; set; } = "";
[Export] public string HeroClass { get; set; } = ""; // "warrior", "mage", "marksman"
[Export] public Texture2D Portrait { get; set; }
// 基础属性
[Export] public float MaxHp { get; set; } = 1000;
[Export] public float MaxMp { get; set; } = 500;
[Export] public float AttackDamage { get; set; } = 50;
[Export] public float AbilityPower { get; set; } = 0;
[Export] public float Armor { get; set; } = 30; // 物理防御
[Export] public float MagicResist { get; set; } = 30; // 法术防御
[Export] public float AttackSpeed { get; set; } = 0.8f; // 每秒攻击次数
[Export] public float MoveSpeed { get; set; } = 5.0f;
[Export] public float AttackRange { get; set; } = 2.0f; // 攻击距离
// 成长属性(每级增加多少)
[Export] public float HpPerLevel { get; set; } = 100;
[Export] public float AdPerLevel { get; set; } = 5;
[Export] public float ArmorPerLevel { get; set; } = 3;
// 技能ID列表
[Export] public string[] SkillIds { get; set; } = new string[4]; // 被动+3个技能
}GDScript
# hero_data.gd
class_name HeroData
extends Resource
@export var hero_name: String = ""
@export var hero_class: String = "" # "warrior", "mage", "marksman"
@export var portrait: Texture2D
# 基础属性
@export var max_hp: float = 1000
@export var max_mp: float = 500
@export var attack_damage: float = 50
@export var ability_power: float = 0
@export var armor: float = 30 # 物理防御
@export var magic_resist: float = 30 # 法术防御
@export var attack_speed: float = 0.8 # 每秒攻击次数
@export var move_speed: float = 5.0
@export var attack_range: float = 2.0 # 攻击距离
# 成长属性(每级增加多少)
@export var hp_per_level: float = 100
@export var ad_per_level: float = 5
@export var armor_per_level: float = 3
# 技能ID列表
@export var skill_ids: Array[String] = ["", "", "", ""] # 被动+3个技能4.2 英雄基类
所有英雄共享一套基础逻辑——移动、攻击、受伤、升级。我们用一个基类来统一管理:
C#
// HeroBase.cs
using Godot;
public partial class HeroBase : CharacterBody3D
{
// 英雄数据
protected HeroData _data;
protected int _level = 1;
protected float _currentHp;
protected float _currentMp;
protected float _exp = 0;
protected float _expToLevel = 100;
// 状态
public enum HeroState { Idle, Moving, Attacking, Casting, Dead }
protected HeroState _state = HeroState.Idle;
protected Node3D _target;
protected bool _isInBush = false;
protected int _teamId = 0; // 0=蓝方, 1=红方
// 组件引用
protected NavigationAgent3D _navAgent;
protected AnimationPlayer _animPlayer;
// 升级经验表
private static readonly float[] ExpTable = {
100, 200, 350, 550, 800, 1100, 1500, 2000, 2600, 3300,
4100, 5000, 6000, 7200, 8500
};
public override void _Ready()
{
_navAgent = GetNode<NavigationAgent3D>("NavigationAgent3D");
_animPlayer = GetNode<AnimationPlayer>("AnimationPlayer");
_currentHp = _data.MaxHp;
_currentMp = _data.MaxMp;
AddToGroup("heroes");
}
public override void _PhysicsProcess(double delta)
{
switch (_state)
{
case HeroState.Moving:
MoveAlongPath((float)delta);
break;
case HeroState.Attacking:
PerformAttack((float)delta);
break;
case HeroState.Dead:
// 死亡状态不做任何事
break;
}
}
protected void MoveAlongPath(float delta)
{
if (_navAgent.IsNavigationFinished())
{
_state = HeroState.Idle;
return;
}
var nextPos = _navAgent.GetNextPathPosition();
var direction = (nextPos - GlobalPosition).Normalized();
Velocity = direction * GetCurrentMoveSpeed();
LookAt(new Vector3(nextPos.X, GlobalPosition.Y, nextPos.Z), Vector3.Up);
MoveAndSlide();
}
// 计算当前移动速度(含装备加成)
protected virtual float GetCurrentMoveSpeed()
{
return _data.MoveSpeed; // 后续章节加入装备加成
}
// 受伤
public virtual void TakeDamage(float damage, string damageType, Node3D source)
{
if (_state == HeroState.Dead) return;
// 根据伤害类型计算实际伤害
float actualDamage = damageType == "physical"
? CalculatePhysicalDamage(damage)
: CalculateMagicDamage(damage);
_currentHp -= actualDamage;
if (_currentHp <= 0)
{
_currentHp = 0;
Die(source);
}
}
// 物理伤害计算(扣除护甲减伤)
protected float CalculatePhysicalDamage(float rawDamage)
{
float armor = _data.Armor + _data.ArmorPerLevel * (_level - 1);
float reduction = armor / (armor + 100); // 简化公式
return rawDamage * (1 - reduction);
}
// 法术伤害计算(扣除魔抗减伤)
protected float CalculateMagicDamage(float rawDamage)
{
float mr = _data.MagicResist;
float reduction = mr / (mr + 100);
return rawDamage * (1 - reduction);
}
// 死亡处理
protected virtual void Die(Node3D killer)
{
_state = HeroState.Dead;
// 给击杀者经验和金币
if (killer.IsInGroup("heroes"))
{
killer.Call("GainExp", 150 + _level * 30);
killer.Call("GainGold", 200 + _level * 20);
}
// 开始复活倒计时
float respawnTime = 5 + _level * 2; // 复活时间随等级增长
GetTree().CreateTimer(respawnTime).Timeout += () => Respawn();
}
// 复活
protected virtual void Respawn()
{
_state = HeroState.Idle;
_currentHp = GetMaxHp();
_currentMp = _data.MaxMp;
// 传送到基地
var spawnPos = _teamId == 0
? new Vector3(-45, 1, 0)
: new Vector3(45, 1, 0);
GlobalPosition = spawnPos;
}
// 获得经验
public void GainExp(float amount)
{
_exp += amount;
while (_exp >= _expToLevel && _level < 15)
{
_exp -= _expToLevel;
LevelUp();
}
}
// 升级
protected virtual void LevelUp()
{
_level++;
_expToLevel = _level < ExpTable.Length ? ExpTable[_level - 1] : _expToLevel * 1.2f;
// 升级提升属性
_currentHp = GetMaxHp(); // 升级回满血
_currentMp = _data.MaxMp;
}
// 获得金币
public void GainGold(float amount)
{
// 由 EconomySystem 管理,这里发送信号
EmitSignal(SignalName.GoldChanged, amount);
}
// 获取当前最大生命值(含等级加成)
public float GetMaxHp()
{
return _data.MaxHp + _data.HpPerLevel * (_level - 1);
}
// 移动到指定位置
public void MoveTo(Vector3 target)
{
_navAgent.TargetPosition = target;
_state = HeroState.Moving;
}
// 进入/离开草丛
public void SetInBush(bool inBush)
{
_isInBush = inBush;
}
[Signal]
public delegate void GoldChangedEventHandler(float amount);
}GDScript
# hero_base.gd
extends CharacterBody3D
# 英雄数据
var data: HeroData
var level: int = 1
var current_hp: float
var current_mp: float
var exp: float = 0
var exp_to_level: float = 100
# 状态
enum HeroState { IDLE, MOVING, ATTACKING, CASTING, DEAD }
var state: HeroState = HeroState.IDLE
var target: Node3D
var is_in_bush: bool = false
var team_id: int = 0 # 0=蓝方, 1=红方
# 组件引用
@onready var nav_agent: NavigationAgent3D = $NavigationAgent3D
@onready var anim_player: AnimationPlayer = $AnimationPlayer
# 升级经验表
var exp_table: Array[float] = [
100, 200, 350, 550, 800, 1100, 1500, 2000, 2600, 3300,
4100, 5000, 6000, 7200, 8500
]
signal gold_changed(amount: float)
func _ready():
current_hp = data.max_hp
current_mp = data.max_mp
add_to_group("heroes")
func _physics_process(delta):
match state:
HeroState.MOVING:
move_along_path(delta)
HeroState.ATTACKING:
perform_attack(delta)
HeroState.DEAD:
pass
func move_along_path(delta: float):
if nav_agent.is_navigation_finished():
state = HeroState.IDLE
return
var next_pos = nav_agent.get_next_path_position()
var direction = (next_pos - global_position).normalized()
velocity = direction * get_current_move_speed()
look_at(Vector3(next_pos.x, global_position.y, next_pos.z), Vector3.UP)
move_and_slide()
func get_current_move_speed() -> float:
return data.move_speed # 后续章节加入装备加成
func take_damage(damage: float, damage_type: String, source: Node3D):
if state == HeroState.DEAD:
return
var actual_damage = damage_type
if damage_type == "physical":
actual_damage = calculate_physical_damage(damage)
else:
actual_damage = calculate_magic_damage(damage)
current_hp -= actual_damage
if current_hp <= 0:
current_hp = 0
die(source)
func calculate_physical_damage(raw_damage: float) -> float:
var armor_val = data.armor + data.armor_per_level * (level - 1)
var reduction = armor_val / (armor_val + 100)
return raw_damage * (1 - reduction)
func calculate_magic_damage(raw_damage: float) -> float:
var mr = data.magic_resist
var reduction = mr / (mr + 100)
return raw_damage * (1 - reduction)
func die(killer: Node3D):
state = HeroState.DEAD
if killer.is_in_group("heroes"):
killer.gain_exp(150 + level * 30)
killer.gain_gold(200 + level * 20)
var respawn_time = 5 + level * 2
get_tree().create_timer(respawn_time).timeout.connect(respawn)
func respawn():
state = HeroState.IDLE
current_hp = get_max_hp()
current_mp = data.max_mp
var spawn_pos = Vector3(-45, 1, 0) if team_id == 0 else Vector3(45, 1, 0)
global_position = spawn_pos
func gain_exp(amount: float):
exp += amount
while exp >= exp_to_level and level < 15:
exp -= exp_to_level
level_up()
func level_up():
level += 1
exp_to_level = exp_table[level - 1] if level < exp_table.size() else exp_to_level * 1.2
current_hp = get_max_hp()
current_mp = data.max_mp
func gain_gold(amount: float):
emit_signal("gold_changed", amount)
func get_max_hp() -> float:
return data.max_hp + data.hp_per_level * (level - 1)
func move_to(target: Vector3):
nav_agent.target_position = target
state = HeroState.MOVING
func set_in_bush(in_bush: bool):
is_in_bush = in_bush4.3 英雄状态机
英雄在同一时间只能做一件事:移动、攻击、施法、死亡。用一个简单的状态机来管理:
┌───────┐ 点击移动 ┌─────────┐
│ Idle │ ─────────→│ Moving │
│ 待机 │ │ 移动中 │
└───┬───┘ └────┬────┘
↑ │
│ 目标死亡 │ 到达攻击范围
│ ↓
┌───┴───┐ ┌──────────┐
│ Dead │ ←──────── │ Attacking │
│ 死亡 │ 生命归零 │ 攻击中 │
└───────┘ └────┬─────┘
│ 按技能键
↓
┌──────────┐
│ Casting │
│ 施法中 │
└──────────┘4.4 血条显示
每个英雄头顶需要一个血条,让玩家直观看到生命值:
C#
// HealthBar3D.cs
using Godot;
public partial class HealthBar3D : SubViewportContainer
{
private ProgressBar _healthBar;
private ProgressBar _manaBar;
private Label _levelLabel;
private float _maxHp;
public override void _Ready()
{
// 在 SubViewport 中创建 UI 元素
var viewport = GetNode<SubViewport>("SubViewport");
var root = viewport.GetNode<Control>("Root");
_healthBar = root.GetNode<ProgressBar>("HealthBar");
_manaBar = root.GetNode<ProgressBar>("ManaBar");
_levelLabel = root.GetNode<Label>("LevelLabel");
}
public void UpdateHealth(float current, float max)
{
_maxHp = max;
_healthBar.MaxValue = max;
_healthBar.Value = current;
}
public void UpdateMana(float current, float max)
{
_manaBar.MaxValue = max;
_manaBar.Value = current;
}
public void UpdateLevel(int level)
{
_levelLabel.Text = $"Lv.{level}";
}
}GDScript
# health_bar_3d.gd
extends SubViewportContainer
var health_bar: ProgressBar
var mana_bar: ProgressBar
var level_label: Label
var max_hp: float
func _ready():
var viewport = $SubViewport
var root = viewport.get_node("Root")
health_bar = root.get_node("HealthBar")
mana_bar = root.get_node("ManaBar")
level_label = root.get_node("LevelLabel")
func update_health(current: float, max_val: float):
max_hp = max_val
health_bar.max_value = max_val
health_bar.value = current
func update_mana(current: float, max_val: float):
mana_bar.max_value = max_val
mana_bar.value = current
func update_level(lvl: int):
level_label.text = "Lv.%d" % lvl章节导航
| 上一章 | 下一章 |
|---|---|
| ← 3. 地图与分路系统 | 5. 战斗与技能系统 → |
