3. 帕鲁生物系统
帕鲁生物系统
帕鲁是这款游戏的灵魂。没有帕鲁,就只是一个普通的生存建造游戏。有了帕鲁,你的世界就"活"了——它们会四处游荡、会跟你打架、被抓后会帮你干活、甚至还会逃跑。这章我们就来设计一个完整的帕鲁生物系统。
本章你将学到
- 帕鲁的数据结构(属性、技能、性格、稀有度)
- 帕鲁 AI 行为树(让帕鲁"聪明"地行动)
- 属性与技能系统(火、水、草、雷互相克制)
- 帕鲁繁殖融合系统
- 稀有度系统(从普通到传说)
帕鲁数据结构
每只帕鲁就像一张"身份证"——上面记录了它是谁、有多强、擅长什么。我们用一个数据类来表示。
帕鲁数据定义
// PalData.cs
// 帕鲁数据结构——每只帕鲁的"身份证"
using Godot;
using System.Collections.Generic;
// 属性类型(火、水、草、雷、无)
public enum ElementType
{
None, // 无属性
Fire, // 火属性
Water, // 水属性
Grass, // 草属性
Thunder // 雷属性
}
// 稀有度(越后面越稀有)
public enum Rarity
{
Common, // 普通(灰色)
Rare, // 稀有(绿色)
Epic, // 史诗(紫色)
Legendary // 传说(金色)
}
// 帕鲁性格(影响属性加成)
public enum Personality
{
Brave, // 勇敢:攻击+20%,防御-10%
Calm, // 冷静:防御+20%,攻击-10%
Cheerful, // 开朗:速度+20%,攻击-10%
HardWorker, // 勤劳:工作效率+30%
Lazy // 懒惰:全属性-10%,但饥饿消耗减半
}
// 帕鲁的"身份证"
[GlobalClass]
public partial class PalData : Resource
{
// 基本信息
[Export] public string PalName = "帕鲁"; // 名字
[Export] public int PalId = 0; // 编号
[Export] public string Description = ""; // 描述
[Export] public ElementType Element = ElementType.None; // 属性
[Export] public Rarity RarityLevel = Rarity.Common; // 稀有度
[Export] public Personality Nature = Personality.Brave; // 性格
// 基础属性
[Export] public int BaseHp = 100; // 基础生命值
[Export] public int BaseAtk = 50; // 基础攻击力
[Export] public int BaseDef = 30; // 基础防御力
[Export] public int BaseSpd = 100; // 基础速度
[Export] public float WorkEfficiency = 1.0f; // 工作效率倍率
// 等级系统
[Export] public int Level = 1;
[Export] public int Exp = 0; // 当前经验值
// 技能列表
[Export] public Godot.Collections.Array<string> Skills = new();
// 获取经过性格加成后的实际攻击力
public int GetActualAtk()
{
float multiplier = Nature switch
{
Personality.Brave => 1.2f,
Personality.Lazy => 0.9f,
_ => 1.0f
};
return (int)(BaseAtk * multiplier);
}
// 获取经过性格加成后的实际防御力
public int GetActualDef()
{
float multiplier = Nature switch
{
Personality.Calm => 1.2f,
Personality.Lazy => 0.9f,
_ => 1.0f
};
return (int)(BaseDef * multiplier);
}
// 升级所需经验值
public int GetExpToNextLevel()
{
return Level * 100; // 简单公式:等级 × 100
}
}# pal_data.gd
# 帕鲁数据结构——每只帕鲁的"身份证"
class_name PalData
extends Resource
## 属性类型枚举
enum Element {
NONE, # 无属性
FIRE, # 火属性
WATER, # 水属性
GRASS, # 草属性
THUNDER, # 雷属性
}
## 稀有度枚举
enum Rarity {
COMMON, # 普通(灰色)
RARE, # 稀有(绿色)
EPIC, # 史诗(紫色)
LEGENDARY, # 传说(金色)
}
## 性格枚举
enum Personality {
BRAVE, # 勇敢:攻击+20%,防御-10%
CALM, # 冷静:防御+20%,攻击-10%
CHEERFUL, # 开朗:速度+20%,攻击-10%
HARD_WORKER,# 勤劳:工作效率+30%
LAZY, # 懒惰:全属性-10%,但饥饿消耗减半
}
## 基本信息
@export var pal_name: String = "帕鲁"
@export var pal_id: int = 0
@export var description: String = ""
@export var element: Element = Element.NONE
@export var rarity: Rarity = Rarity.COMMON
@export var nature: Personality = Personality.BRAVE
## 基础属性
@export var base_hp: int = 100
@export var base_atk: int = 50
@export var base_def: int = 30
@export var base_spd: int = 100
@export var work_efficiency: float = 1.0
## 等级系统
@export var level: int = 1
@export var exp: int = 0
## 技能列表
@export var skills: PackedStringArray = []
## 获取经过性格加成后的实际攻击力
func get_actual_atk() -> int:
var multiplier := 1.0
match nature:
Personality.BRAVE:
multiplier = 1.2
Personality.LAZY:
multiplier = 0.9
return int(base_atk * multiplier)
## 获取经过性格加成后的实际防御力
func get_actual_def() -> int:
var multiplier := 1.0
match nature:
Personality.CALM:
multiplier = 1.2
Personality.LAZY:
multiplier = 0.9
return int(base_def * multiplier)
## 升级所需经验值
func get_exp_to_next_level() -> int:
return level * 100 # 简单公式:等级 × 100数据说明
上面的代码定义了一只帕鲁拥有的所有信息:
- 基本信息:名字、编号、描述、属性、稀有度、性格
- 基础属性:生命值、攻击力、防御力、速度、工作效率
- 等级系统:当前等级和经验值
- 技能列表:帕鲁会哪些技能
其中性格系统很有趣——勇敢的帕鲁攻击高但防御低,勤劳的帕鲁干活快但打架弱。这让每只同种的帕鲁也不完全一样,增加了收集的乐趣。
帕鲁 AI 行为树
行为树是什么?你可以把它想象成一颗"决策树"——帕鲁每时每刻都在这棵树上做选择:我应该发呆?还是跟着玩家?还是去打架?
行为树的运行逻辑
行为树从上到下依次尝试:
- 先看要不要逃跑——如果血量低于 20%,什么都别想了,先跑
- 再看要不要打架——附近有敌人就去打
- 再看要不要干活——如果被分配了工作就去干
- 再看要不要跟着玩家——跟随模式下跟着玩家
- 以上都不需要——就自由活动(发呆、散步、吃东西)
帕鲁 AI 控制器
// PalAI.cs
// 帕鲁AI控制器——让帕鲁"聪明"地行动
using Godot;
public partial class PalAI : CharacterBody3D
{
// AI 状态
public enum AIState
{
Idle, // 空闲
Follow, // 跟随
Attack, // 攻击
Flee, // 逃跑
Work // 工作
}
[Export] public float MoveSpeed = 3.0f;
[Export] public float FleeSpeed = 6.0f;
[Export] public float AttackRange = 3.0f;
[Export] public float FollowDistance = 5.0f;
private AIState _currentState = AIState.Idle;
private Node3D _target; // 当前目标(敌人/玩家/工作点)
private PalData _palData; // 帕鲁数据
private float _gravity;
private NavigationAgent3D _navAgent;
private float _idleTimer;
private Vector3 _idleTarget;
public override void _Ready()
{
_gravity = (float)ProjectSettings.GetSetting("physics/3d/default_gravity");
_navAgent = GetNode<NavigationAgent3D>("NavigationAgent3D");
}
public override void _PhysicsProcess(double delta)
{
// 应用重力
if (!IsOnFloor())
{
Velocity = Velocity with { Y = Velocity.Y - _gravity * (float)delta };
}
// 根据状态执行不同行为
switch (_currentState)
{
case AIState.Idle:
ProcessIdle((float)delta);
break;
case AIState.Follow:
ProcessFollow();
break;
case AIState.Attack:
ProcessAttack();
break;
case AIState.Flee:
ProcessFlee();
break;
case AIState.Work:
ProcessWork();
break;
}
MoveAndSlide();
}
// 空闲状态:随机散步
private void ProcessIdle(float delta)
{
_idleTimer -= delta;
if (_idleTimer <= 0)
{
// 随机选一个附近的目标点
_idleTarget = GlobalPosition + new Vector3(
(float)GD.RandRange(-5, 5),
0,
(float)GD.RandRange(-5, 5)
);
_idleTimer = (float)GD.RandRange(2, 5); // 2~5秒后换方向
}
MoveTo(_idleTarget, MoveSpeed * 0.3f); // 慢慢走
}
// 跟随状态:跟着玩家
private void ProcessFollow()
{
if (_target == null) return;
float dist = GlobalPosition.DistanceTo(_target.GlobalPosition);
if (dist > FollowDistance)
{
MoveTo(_target.GlobalPosition, MoveSpeed);
}
}
// 攻击状态:追着敌人打
private void ProcessAttack()
{
if (_target == null)
{
_currentState = AIState.Idle;
return;
}
float dist = GlobalPosition.DistanceTo(_target.GlobalPosition);
if (dist <= AttackRange)
{
// 在攻击范围内,面向敌人
LookAt(_target.GlobalPosition);
// TODO: 播放攻击动画和技能效果
}
else
{
MoveTo(_target.GlobalPosition, MoveSpeed);
}
}
// 逃跑状态:远离威胁
private void ProcessFlee()
{
if (_target == null)
{
_currentState = AIState.Idle;
return;
}
// 计算远离敌人的方向
Vector3 fleeDir = (GlobalPosition - _target.GlobalPosition).Normalized();
Vector3 fleeTarget = GlobalPosition + fleeDir * 10f;
MoveTo(fleeTarget, FleeSpeed);
}
// 工作状态:前往工作点
private void ProcessWork()
{
if (_target == null) return;
float dist = GlobalPosition.DistanceTo(_target.GlobalPosition);
if (dist <= 2.0f)
{
// 到达工作点,执行工作
// TODO: 播放工作动画,产出资源
}
else
{
MoveTo(_target.GlobalPosition, MoveSpeed);
}
}
// 辅助方法:移向目标位置
private void MoveTo(Vector3 target, float speed)
{
Vector3 direction = (target - GlobalPosition).Normalized();
Velocity = new Vector3(direction.X * speed, Velocity.Y, direction.Z * speed);
// 面朝移动方向
if (direction != Vector3.Zero)
{
LookAt(GlobalPosition + direction);
}
}
// 外部调用:切换状态
public void SetState(AIState newState, Node3D target = null)
{
_currentState = newState;
_target = target;
}
}# pal_ai.gd
# 帕鲁AI控制器——让帕鲁"聪明"地行动
extends CharacterBody3D
## AI 状态枚举
enum AIState {
IDLE, # 空闲
FOLLOW, # 跟随
ATTACK, # 攻击
FLEE, # 逃跑
WORK, # 工作
}
@export var move_speed: float = 3.0
@export var flee_speed: float = 6.0
@export var attack_range: float = 3.0
@export var follow_distance: float = 5.0
var _current_state: AIState = AIState.IDLE
var _target: Node3D # 当前目标(敌人/玩家/工作点)
var _gravity: float
var _idle_timer: float = 0.0
var _idle_target: Vector3
func _ready() -> void:
_gravity = ProjectSettings.get_setting("physics/3d/default_gravity")
func _physics_process(delta: float) -> void:
# 应用重力
if not is_on_floor():
velocity.y -= _gravity * delta
# 根据状态执行不同行为
match _current_state:
AIState.IDLE:
_process_idle(delta)
AIState.FOLLOW:
_process_follow()
AIState.ATTACK:
_process_attack()
AIState.FLEE:
_process_flee()
AIState.WORK:
_process_work()
move_and_slide()
## 空闲状态:随机散步
func _process_idle(delta: float) -> void:
_idle_timer -= delta
if _idle_timer <= 0:
# 随机选一个附近的目标点
_idle_target = global_position + Vector3(
randf_range(-5, 5),
0,
randf_range(-5, 5)
)
_idle_timer = randf_range(2, 5) # 2~5秒后换方向
_move_to(_idle_target, move_speed * 0.3) # 慢慢走
## 跟随状态:跟着玩家
func _process_follow() -> void:
if _target == null:
return
var dist := global_position.distance_to(_target.global_position)
if dist > follow_distance:
_move_to(_target.global_position, move_speed)
## 攻击状态:追着敌人打
func _process_attack() -> void:
if _target == null:
_current_state = AIState.IDLE
return
var dist := global_position.distance_to(_target.global_position)
if dist <= attack_range:
# 在攻击范围内,面向敌人
look_at(_target.global_position)
# TODO: 播放攻击动画和技能效果
else:
_move_to(_target.global_position, move_speed)
## 逃跑状态:远离威胁
func _process_flee() -> void:
if _target == null:
_current_state = AIState.IDLE
return
# 计算远离敌人的方向
var flee_dir := (global_position - _target.global_position).normalized()
var flee_target := global_position + flee_dir * 10.0
_move_to(flee_target, flee_speed)
## 工作状态:前往工作点
func _process_work() -> void:
if _target == null:
return
var dist := global_position.distance_to(_target.global_position)
if dist <= 2.0:
# 到达工作点,执行工作
# TODO: 播放工作动画,产出资源
pass
else:
_move_to(_target.global_position, move_speed)
## 辅助方法:移向目标位置
func _move_to(target: Vector3, speed: float) -> void:
var direction := (target - global_position).normalized()
velocity = Vector3(direction.x * speed, velocity.y, direction.z * speed)
# 面朝移动方向
if direction != Vector3.ZERO:
look_at(global_position + direction)
## 外部调用:切换状态
func set_state(new_state: AIState, target: Node3D = null) -> void:
_current_state = new_state
_target = target属性与技能系统
属性克制关系
属性克制就像"石头剪刀布"——每种属性都有克制的对象和被克制的弱点。
克制倍率表
| 攻击属性 | 火 | 水 | 草 | 雷 | 无 |
|---|---|---|---|---|---|
| 火 | 1.0 | 0.5 | 1.5 | 1.0 | 1.0 |
| 水 | 1.5 | 1.0 | 0.5 | 0.5 | 1.0 |
| 草 | 0.5 | 1.5 | 1.0 | 1.0 | 1.0 |
| 雷 | 1.0 | 1.5 | 1.0 | 1.0 | 1.0 |
| 无 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 |
简单记法:
- 火烧草(1.5 倍伤害)
- 水灭火(1.5 倍伤害)
- 草吸水(1.5 倍伤害)
- 雷劈水(1.5 倍伤害)
技能系统
每个帕鲁最多拥有 4 个技能。技能有冷却时间,用完需要等一会儿才能再用。
| 技能名 | 属性 | 伤害 | 冷却 | 说明 |
|---|---|---|---|---|
| 火花 | 火 | 30 | 2秒 | 发射小火球 |
| 水枪 | 水 | 25 | 1.5秒 | 快速射出水柱 |
| 藤鞭 | 草 | 35 | 3秒 | 用藤蔓抽打 |
| 雷击 | 雷 | 40 | 5秒 | 召唤闪电 |
| 撞击 | 无 | 15 | 1秒 | 基础物理攻击 |
帕鲁繁殖融合系统
繁殖融合就是把两只帕鲁放在一起,生成一只新的帕鲁。新帕鲁可能继承父母的优点,也可能产生全新的变异。
繁殖规则
- 两只同种帕鲁 → 生出同种帕鲁(可能更高稀有度)
- 两只不同种帕鲁 → 可能生出新的混合品种
- 属性互补 → 火属性 + 水属性可能生出蒸气属性
- 稀有度 → 子代的稀有度 >= 父母中较低的那个
繁殖逻辑代码
// PalBreeding.cs
// 帕鲁繁殖系统
using Godot;
public partial class PalBreeding : Node
{
// 繁殖两只帕鲁,返回子代数据
public PalData Breed(PalData parentA, PalData parentB)
{
PalData child = new PalData();
// 名字:继承父亲的名字加" Jr."
child.PalName = parentA.PalName + " Jr.";
// 属性:50%概率继承父/母,10%概率变异
float roll = (float)GD.RandRange(0, 100) / 100f;
if (roll < 0.45f)
child.Element = parentA.Element;
else if (roll < 0.90f)
child.Element = parentB.Element;
else
child.Element = GetRandomElement(); // 变异
// 稀有度:取父母中较高的,或小概率提升
Rarity baseRarity = (Rarity)Mathf.Max((int)parentA.RarityLevel, (int)parentB.RarityLevel);
if (GD.RandRange(0, 100) < 10) // 10% 概率升一级
{
baseRarity = (Rarity)Mathf.Min((int)baseRarity + 1, (int)Rarity.Legendary);
}
child.RarityLevel = baseRarity;
// 属性值:取父母平均值 ± 随机波动
child.BaseHp = InheritStat(parentA.BaseHp, parentB.BaseHp);
child.BaseAtk = InheritStat(parentA.BaseAtk, parentB.BaseAtk);
child.BaseDef = InheritStat(parentA.BaseDef, parentB.BaseDef);
// 性格:随机继承父或母
child.Nature = GD.RandRange(0, 1) == 0 ? parentA.Nature : parentB.Nature;
// 等级从1开始
child.Level = 1;
return child;
}
// 属性继承:取平均值 ± 10% 随机波动
private int InheritStat(int statA, int statB)
{
float average = (statA + statB) / 2f;
float variation = average * (float)GD.RandRange(-10, 10) / 100f;
return (int)Mathf.Max(1, average + variation);
}
private ElementType GetRandomElement()
{
var elements = System.Enum.GetValues(typeof(ElementType));
return (ElementType)elements.GetValue((int)GD.RandRange(1, elements.Length - 1));
}
}# pal_breeding.gd
# 帕鲁繁殖系统
extends Node
## 繁殖两只帕鲁,返回子代数据
func breed(parent_a: PalData, parent_b: PalData) -> PalData:
var child := PalData.new()
# 名字:继承父亲的名字加" Jr."
child.pal_name = parent_a.pal_name + " Jr."
# 属性:50%概率继承父/母,10%概率变异
var roll := randf()
if roll < 0.45:
child.element = parent_a.element
elif roll < 0.90:
child.element = parent_b.element
else:
child.element = _get_random_element() # 变异
# 稀有度:取父母中较高的,或小概率提升
var base_rarity := maxi(parent_a.rarity, parent_b.rarity)
if randi_range(0, 100) < 10: # 10% 概率升一级
base_rarity = mini(base_rarity + 1, PalData.Rarity.LEGENDARY)
child.rarity = base_rarity
# 属性值:取父母平均值 ± 随机波动
child.base_hp = _inherit_stat(parent_a.base_hp, parent_b.base_hp)
child.base_atk = _inherit_stat(parent_a.base_atk, parent_b.base_atk)
child.base_def = _inherit_stat(parent_a.base_def, parent_b.base_def)
# 性格:随机继承父或母
child.nature = parent_a.nature if randi_range(0, 1) == 0 else parent_b.nature
# 等级从1开始
child.level = 1
return child
## 属性继承:取平均值 ± 10% 随机波动
func _inherit_stat(stat_a: int, stat_b: int) -> int:
var average := (stat_a + stat_b) / 2.0
var variation := average * randf_range(-0.1, 0.1)
return maxi(1, int(average + variation))
func _get_random_element() -> int:
var elements := [PalData.Element.FIRE, PalData.Element.WATER,
PalData.Element.GRASS, PalData.Element.THUNDER]
return elements[randi() % elements.size()]稀有度系统
稀有度决定了帕鲁出现的概率和基础属性的强弱。
| 稀有度 | 颜色 | 出现概率 | 属性倍率 | 说明 |
|---|---|---|---|---|
| 普通 | 灰色 | 60% | x1.0 | 大部分帕鲁都是普通的 |
| 稀有 | 绿色 | 25% | x1.3 | 偶尔能遇到 |
| 史诗 | 紫色 | 12% | x1.6 | 需要去特定区域才可能遇到 |
| 传说 | 金色 | 3% | x2.0 | 极其罕见,Boss 级别 |
稀有度生成逻辑
每次生成一只野生帕鲁时,用随机数决定它的稀有度:
- 60% 的概率生成普通帕鲁
- 25% 的概率生成稀有帕鲁
- 12% 的概率生成史诗帕鲁
- 3% 的概率生成传说帕鲁
传说帕鲁出现的 3% 概率意味着:平均每遇到 33 只帕鲁,才有 1 只是传说级别的。这个概率让玩家在遇到传说帕鲁时特别兴奋。
常见问题
Q:行为树和状态机有什么区别?
行为树像一个"层级菜单"——从上到下依次尝试每个选项,找到第一个满足条件的就执行。状态机像一个"流程图"——每个状态之间有明确的切换条件。行为树更适合复杂的 AI 决策(因为可以嵌套),状态机更适合简单的状态切换。帕鲁的 AI 行为比较复杂,所以用行为树。
Q:一只帕鲁最多有几个技能?
推荐 4 个。多了玩家记不住,少了战斗太单调。1 个基础攻击 + 1 个属性技能 + 1 个特殊技能 + 1 个被动技能,这样的组合既有策略性又不会太复杂。
Q:繁殖系统会不会导致玩家刷出超强帕鲁?
会的,所以需要加入限制:每次繁殖有冷却时间(比如 30 分钟游戏内时间),后代等级从 1 开始需要重新培养,最高属性有上限。这样即使繁殖出高属性帕鲁,也需要花时间培养。
Q:稀有度怎么影响游戏体验?
稀有度让玩家有"收集欲"。看到金色的传说帕鲁会特别想抓,即使仓库里已经有很多同种的普通帕鲁了。这就是"抽卡"的心理——你永远想要下一张。
下一步
帕鲁系统做好了,接下来实现 基地建造——给帕鲁一个可以工作的家。
