6. 战斗与捕捉系统
2026/4/14大约 9 分钟
战斗与捕捉系统
战斗和捕捉是获取新帕鲁的主要方式。你遇到一只野生的帕鲁,先把它打残血,然后扔出帕鲁球去捕捉它。这个"打残血再捕捉"的机制就是这套系统的核心乐趣。
本章你将学到
- 即时战斗系统(帕鲁自动战斗 + 玩家辅助指令)
- 帕鲁球捕捉的抛物线物理
- 捕捉成功率的计算方式
- 属性克制的伤害倍率
- Boss 帕鲁的设计思路
战斗系统概览
战斗不是回合制的,而是即时战斗——你和帕鲁在 3D 世界里直接打架,所有动作都是实时发生的。
即时战斗系统
战斗是怎么进行的?
- 帕鲁自动战斗:你的帕鲁会自动攻击面前的敌人(用 AI 行为树的攻击模式)
- 玩家辅助指令:你可以按快捷键让帕鲁释放特定技能、切换帕鲁、或者扔帕鲁球
- 走位很重要:你可以控制角色移动来躲避敌人的技能
战斗控制器
C#
// CombatSystem.cs
// 战斗系统——处理伤害计算和战斗逻辑
using Godot;
public partial class CombatSystem : Node
{
// 属性克制倍率表(5x5)
private static readonly float[,] ElementChart = new float[,]
{
// None, Fire, Water, Grass, Thunder
/*None*/ { 1.0f, 1.0f, 1.0f, 1.0f, 1.0f },
/*Fire*/ { 1.0f, 1.0f, 0.5f, 1.5f, 1.0f },
/*Water*/ { 1.0f, 1.5f, 1.0f, 0.5f, 0.5f },
/*Grass*/ { 1.0f, 0.5f, 1.5f, 1.0f, 1.0f },
/*Thunder*/{ 1.0f, 1.0f, 1.5f, 1.0f, 1.0f },
};
// 计算实际伤害
public static float CalculateDamage(
int attackerAtk, int defenderDef,
ElementType attackElement, ElementType defenderElement,
float skillMultiplier = 1.0f)
{
// 基础伤害公式
float baseDamage = attackerAtk * (100f / (100f + defenderDef));
// 属性克制倍率
float elementMultiplier = ElementChart[(int)attackElement, (int)defenderElement];
// 最终伤害
float finalDamage = baseDamage * elementMultiplier * skillMultiplier;
// 随机波动 ±10%
float variance = (float)GD.RandRange(-0.1, 0.1);
finalDamage *= (1f + variance);
return Mathf.Max(1f, finalDamage); // 最少造成1点伤害
}
// 对目标造成伤害
public void DealDamage(Node3D target, float damage)
{
if (target.HasMethod("TakeDamage"))
{
target.Call("TakeDamage", damage);
}
}
}GDScript
# combat_system.gd
# 战斗系统——处理伤害计算和战斗逻辑
extends Node
# 属性克制倍率表
const ELEMENT_CHART := [
# NONE, FIRE, WATER, GRASS, THUNDER
[1.0, 1.0, 1.0, 1.0, 1.0], # NONE
[1.0, 1.0, 0.5, 1.5, 1.0], # FIRE
[1.0, 1.5, 1.0, 0.5, 0.5], # WATER
[1.0, 0.5, 1.5, 1.0, 1.0], # GRASS
[1.0, 1.0, 1.5, 1.0, 1.0], # THUNDER
]
## 计算实际伤害
static func calculate_damage(
attacker_atk: int,
defender_def: int,
attack_element: int,
defender_element: int,
skill_multiplier: float = 1.0
) -> float:
# 基础伤害公式
var base_damage := float(attacker_atk) * (100.0 / (100.0 + defender_def))
# 属性克制倍率
var element_multiplier: float = ELEMENT_CHART[attack_element][defender_element]
# 最终伤害
var final_damage := base_damage * element_multiplier * skill_multiplier
# 随机波动 ±10%
var variance := randf_range(-0.1, 0.1)
final_damage *= (1.0 + variance)
return maxf(1.0, final_damage) # 最少造成1点伤害
## 对目标造成伤害
func deal_damage(target: Node3D, damage: float) -> void:
if target.has_method("take_damage"):
target.call("take_damage", damage)帕鲁球捕捉系统
捕捉是游戏最有爽感的时刻——你扔出一个帕鲁球,看着它在空中划出一道弧线,命中目标帕鲁,然后帕鲁被吸进球里,球摇晃三下......成功还是失败?
捕捉判定流程
帕鲁球物理——抛物线
帕鲁球用 RigidBody3D 来实现抛物线飞行。就像你在现实中扔一个球——用力一扔,球就会沿着抛物线飞出去。
C#
// PalBall.cs
// 帕鲁球——捕捉帕鲁的道具
using Godot;
public partial class PalBall : RigidBody3D
{
[Export] public float ThrowForce = 20f; // 投掷力度
[Export] public float CaptureRadius = 2.0f; // 捕捉判定半径
private bool _hasHit = false;
private Node3D _targetPal;
public override void _Ready()
{
// 连接碰撞信号
BodyEntered += OnBodyEntered;
}
// 投掷帕鲁球(外部调用)
public void Throw(Vector3 direction, Vector3 startPos)
{
GlobalPosition = startPos;
// 给球一个初速度,让它沿抛物线飞行
ApplyCentralImpulse(direction * ThrowForce);
}
// 碰到物体时的处理
private void OnBodyEntered(Node body)
{
if (_hasHit) return;
_hasHit = true;
// 检查碰到的对象是不是帕鲁
if (body.HasMethod("TakeDamage")) // 帕鲁都有这个方法
{
_targetPal = (Node3D)body;
StartCapture();
}
else
{
// 没命中帕鲁,球落地
SleepTimer();
}
}
// 开始捕捉判定
private async void StartCapture()
{
// 停止球的物理运动
LinearVelocity = Vector3.Zero;
AngularVelocity = Vector3.Zero;
Freeze = true;
// 播放捕捉动画:帕鲁缩小被吸入球中
// TODO: 播放特效
// 计算捕捉成功率
float rate = CalculateCaptureRate(_targetPal);
// 三次摇晃判定
for (int i = 0; i < 3; i++)
{
// 等待1秒(摇晃动画)
await ToSignal(GetTree().CreateTimer(1.0), "timeout");
// 每次判定:随机数 < 成功率则通过
if (GD.RandRange(0, 100) > rate)
{
// 捕捉失败
CaptureFailed();
return;
}
// TODO: 播放球摇晃动画
}
// 三次都通过,捕捉成功
CaptureSuccess();
}
// 计算捕捉成功率
private float CalculateCaptureRate(Node3D pal)
{
// 获取帕鲁的血量百分比
float hpPercent = 1.0f; // 默认满血
if (pal.HasMethod("GetHpPercent"))
{
hpPercent = (float)pal.Call("GetHpPercent");
}
// 基础成功率 + 血量加成
// 血量越低,成功率越高
float baseRate = 30f; // 基础30%
float hpBonus = (1f - hpPercent) * 60f; // 血量越低加成越高,最多+60%
return Mathf.Min(95f, baseRate + hpBonus); // 最高95%
}
// 捕捉成功
private void CaptureSuccess()
{
GD.Print("捕捉成功!");
// TODO: 将帕鲁数据添加到玩家队伍
// TODO: 播放成功特效和音效
QueueFree();
}
// 捕捉失败
private void CaptureFailed()
{
GD.Print("捕捉失败!帕鲁逃出了帕鲁球");
// TODO: 播放失败特效
// TODO: 让帕鲁重新出现
QueueFree();
}
// 落地后自动消失
private async void SleepTimer()
{
await ToSignal(GetTree().CreateTimer(3.0), "timeout");
QueueFree();
}
}GDScript
# pal_ball.gd
# 帕鲁球——捕捉帕鲁的道具
extends RigidBody3D
## 投掷力度
@export var throw_force: float = 20.0
## 捕捉判定半径
@export var capture_radius: float = 2.0
var _has_hit: bool = false
var _target_pal: Node3D
func _ready() -> void:
# 连接碰撞信号
body_entered.connect(_on_body_entered)
## 投掷帕鲁球(外部调用)
func throw_ball(direction: Vector3, start_pos: Vector3) -> void:
global_position = start_pos
# 给球一个初速度,让它沿抛物线飞行
apply_central_impulse(direction * throw_force)
## 碰到物体时的处理
func _on_body_entered(body: Node) -> void:
if _has_hit:
return
_has_hit = true
# 检查碰到的对象是不是帕鲁
if body.has_method("take_damage"):
_target_pal = body as Node3D
_start_capture()
else:
# 没命中帕鲁,球落地
_sleep_timer()
## 开始捕捉判定
func _start_capture() -> void:
# 停止球的物理运动
linear_velocity = Vector3.ZERO
angular_velocity = Vector3.ZERO
freeze = true
# 播放捕捉动画:帕鲁缩小被吸入球中
# TODO: 播放特效
# 计算捕捉成功率
var rate := _calculate_capture_rate(_target_pal)
# 三次摇晃判定
for i in range(3):
# 等待1秒(摇晃动画)
await get_tree().create_timer(1.0).timeout
# 每次判定:随机数 < 成功率则通过
if randf_range(0, 100) > rate:
# 捕捉失败
_capture_failed()
return
# TODO: 播放球摇晃动画
# 三次都通过,捕捉成功
_capture_success()
## 计算捕捉成功率
func _calculate_capture_rate(pal: Node3D) -> float:
# 获取帕鲁的血量百分比
var hp_percent := 1.0 # 默认满血
if pal.has_method("get_hp_percent"):
hp_percent = pal.call("get_hp_percent")
# 基础成功率 + 血量加成
# 血量越低,成功率越高
var base_rate := 30.0 # 基础30%
var hp_bonus := (1.0 - hp_percent) * 60.0 # 血量越低加成越高,最多+60%
return minf(95.0, base_rate + hp_bonus) # 最高95%
## 捕捉成功
func _capture_success() -> void:
print("捕捉成功!")
# TODO: 将帕鲁数据添加到玩家队伍
# TODO: 播放成功特效和音效
queue_free()
## 捕捉失败
func _capture_failed() -> void:
print("捕捉失败!帕鲁逃出了帕鲁球")
# TODO: 播放失败特效
# TODO: 让帕鲁重新出现
queue_free()
## 落地后自动消失
func _sleep_timer() -> void:
await get_tree().create_timer(3.0).timeout
queue_free()捕捉成功率说明
成功率取决于目标帕鲁的剩余血量:
| 目标血量 | 捕捉成功率 | 说明 |
|---|---|---|
| 100%(满血) | 30% | 非常难抓 |
| 75% | 45% | 有点难 |
| 50% | 60% | 一半一半 |
| 25% | 75% | 比较容易 |
| 10% | 84% | 大概率成功 |
| 1%(丝血) | 89.4% | 几乎必中 |
最高成功率限制在 95%——即使丝血,也有 5% 的概率失败。这就是"最后一次摇晃帕鲁逃出来了"的刺激感。
Boss 帕鲁设计
Boss 帕鲁和普通帕鲁不一样——它们更大、更强、有多阶段战斗机制。
Boss 设计原则
- 血量是普通帕鲁的 5~10 倍
- 有多个战斗阶段(血量低于 50% 进入第二阶段,释放新技能)
- 有特殊机制(比如召唤小怪、场地技能、弱点暴露)
- 不能用帕鲁球捕捉(Boss 只能击败,击败后掉落稀有材料)
Boss 阶段设计举例
| Boss名 | 第一阶段(100%~50%HP) | 第二阶段(50%~0%HP) | 掉落物 |
|---|---|---|---|
| 烈焰龙 | 喷火、甩尾攻击 | 狂暴化:攻击速度翻倍,释放火焰风暴 | 龙鳞x5, 火焰精华x3 |
| 冰霜巨人 | 投掷冰块、踩踏 | 冻结地面:场地结冰,玩家打滑 | 巨人之心x1, 冰晶x10 |
| 雷电鸟 | 俯冲攻击、雷击 | 召唤雷暴:全屏随机落雷 | 雷鸟之羽x3, 雷电精华x5 |
常见问题
Q:帕鲁球扔不准怎么办?
可以在游戏中增加"瞄准辅助"——当帕鲁球接近目标时,轻微修正轨迹。这就像很多射击游戏里的"子弹磁吸"效果。调整 ThrowForce 和投掷角度也能影响命中率。
Q:战斗能不能让玩家直接参与(不只是指挥帕鲁)?
可以设计"玩家辅助攻击"——玩家可以用武器直接打帕鲁,但伤害远低于帕鲁的攻击。这样玩家有事可做(帮忙磨血),但战斗主力还是帕鲁。
Q:Boss 为什么不能捕捉?
Boss 的定位是"挑战关卡"而不是"收集目标"。如果 Boss 能捕捉,玩家拿到 Boss 级别的帕鲁后游戏就太简单了。打败 Boss 获得稀有材料来制作高级装备,是更好的奖励设计。
Q:多只帕鲁能不能同时战斗?
MVP 建议每次只上一只帕鲁。正式版可以设计"队伍战斗"——同时派 2~3 只帕鲁上场,但需要更复杂的 AI 协调和平衡性调整。
下一步
战斗和捕捉做好了,接下来实现 世界探索系统——给玩家一个值得探索的大世界。
