godot-genre-action-rpg
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseGenre: Action RPG
游戏类型:Action RPG
Expert blueprint for action RPGs emphasizing real-time combat, character builds, loot, and progression.
这是一份专注于实时战斗、角色构筑、战利品与成长系统的Action RPG专业开发蓝图。
NEVER Do
绝对不要做的事
- NEVER make stats invisible to players — Hidden stats feel like RNG. Show damage numbers, crit chance %, armor values clearly.
- NEVER use linear damage scaling — makes early/late game boring. Use exponential:
damage = level * 10.damage = base * pow(1.15, level) - NEVER forget diminishing returns on defense — Armor as prevents invincibility stacking.
damage_reduction = armor / (armor + 100) - NEVER make loot drops feel samey — Differentiate rarities with visual effects (Epic = purple glow), sound cues, and meaningful stat differences.
- NEVER skip hit recovery/stagger — Attacks without hitstun feel weightless. Add 0.2-0.5s stagger on hit for impact feedback.
- 绝对不要对玩家隐藏属性——隐藏的属性会让玩家觉得全是随机概率(RNG)。要清晰显示伤害数值、暴击率百分比、护甲值等信息。
- 绝对不要使用线性伤害成长——这种公式会让前期和后期游戏都变得乏味。应该使用指数成长公式:
damage = level * 10。damage = base * pow(1.15, level) - 绝对不要忽略防御属性的边际效益递减——采用这样的护甲公式可以防止玩家堆叠护甲到无敌状态。
damage_reduction = armor / (armor + 100) - 绝对不要让战利品掉落千篇一律——通过视觉效果(如史诗品质=紫色光晕)、音效提示以及有意义的属性差异来区分不同稀有度的物品。
- 绝对不要跳过受击硬直/ stagger效果——没有受击硬直的攻击会显得毫无重量感。攻击命中时添加0.2-0.5秒的硬直来提供打击反馈。
Available Scripts
可用脚本
MANDATORY: Read the appropriate script before implementing the corresponding pattern.
强制要求:在实现对应模式前,请先阅读相应的脚本。
damage_label_manager.gd
damage_label_manager.gd
Pooled floating damage numbers with vertical stacking logic. Pre-warms pool, handles critical hit scaling, and auto-fades via tweens.
采用对象池的浮动伤害数字系统,支持垂直堆叠逻辑。预初始化对象池,处理暴击伤害缩放,并通过补间动画自动淡出。
telegraphed_enemy.gd
telegraphed_enemy.gd
AoE telegraph pattern for enemy attacks. Wind-up animation with visual cues gives players dodge window, then executes damage zone.
敌人攻击的AOE预警模式。通过前摇动画和视觉提示给玩家留出闪避窗口,随后触发伤害区域。
Core Loop
核心循环
Combat → Loot → Level Up → Build Power → Challenge Harder Content → Repeat战斗 → 战利品 → 升级 → 提升战力 → 挑战更高难度内容 → 重复Skill Chain
技能链
godot-project-foundationsgodot-characterbody-2dgodot-combat-systemgodot-rpg-statsgodot-inventory-systemgodot-ability-systemgodot-quest-systemgodot-economy-systemgodot-save-load-systemsgodot-project-foundationsgodot-characterbody-2dgodot-combat-systemgodot-rpg-statsgodot-inventory-systemgodot-ability-systemgodot-quest-systemgodot-economy-systemgodot-save-load-systemsCombat System
战斗系统
Real-Time Combat with Stats
基于属性的实时战斗
gdscript
class_name CombatController
extends Node
signal damage_dealt(target: Node, amount: int, type: String)
signal enemy_killed(enemy: Node, xp_reward: int)
func calculate_damage(attacker: RPGStats, defender: RPGStats, base_damage: int) -> Dictionary:
# Physical damage formula
var attack_power := attacker.get_stat("strength") * 2 + base_damage
var defense := defender.get_stat("armor")
# Damage reduction formula (diminishing returns)
var reduction := defense / (defense + 100.0)
var final_damage := int(attack_power * (1.0 - reduction))
# Critical hit check
var crit_chance := attacker.get_stat("crit_chance") / 100.0
var is_crit := randf() < crit_chance
if is_crit:
final_damage = int(final_damage * attacker.get_stat("crit_damage") / 100.0)
return {
"damage": max(1, final_damage),
"is_crit": is_crit,
"damage_type": "physical"
}
func apply_damage(target: Node, damage_result: Dictionary) -> void:
if target.has_method("take_damage"):
target.take_damage(damage_result["damage"], damage_result["is_crit"])
damage_dealt.emit(target, damage_result["damage"], damage_result["damage_type"])gdscript
class_name CombatController
extends Node
signal damage_dealt(target: Node, amount: int, type: String)
signal enemy_killed(enemy: Node, xp_reward: int)
func calculate_damage(attacker: RPGStats, defender: RPGStats, base_damage: int) -> Dictionary:
# Physical damage formula
var attack_power := attacker.get_stat("strength") * 2 + base_damage
var defense := defender.get_stat("armor")
# Damage reduction formula (diminishing returns)
var reduction := defense / (defense + 100.0)
var final_damage := int(attack_power * (1.0 - reduction))
# Critical hit check
var crit_chance := attacker.get_stat("crit_chance") / 100.0
var is_crit := randf() < crit_chance
if is_crit:
final_damage = int(final_damage * attacker.get_stat("crit_damage") / 100.0)
return {
"damage": max(1, final_damage),
"is_crit": is_crit,
"damage_type": "physical"
}
func apply_damage(target: Node, damage_result: Dictionary) -> void:
if target.has_method("take_damage"):
target.take_damage(damage_result["damage"], damage_result["is_crit"])
damage_dealt.emit(target, damage_result["damage"], damage_result["damage_type"])Hitbox/Hurtbox Combat
Hitbox/Hurtbox战斗系统
gdscript
class_name Hitbox
extends Area2D
@export var damage: int = 10
@export var knockback_force: float = 200.0
@export var attack_owner: Node
var has_hit: Array[Node] = [] # Prevent multi-hit per swing
func _ready() -> void:
monitoring = false # Enable only during attack frames
func enable() -> void:
has_hit.clear()
monitoring = true
func disable() -> void:
monitoring = false
func _on_area_entered(area: Area2D) -> void:
if area is Hurtbox:
var target := area.owner_entity
if target != attack_owner and target not in has_hit:
has_hit.append(target)
var result := CombatController.calculate_damage(
attack_owner.stats, target.stats, damage
)
CombatController.apply_damage(target, result)
apply_knockback(target)
func apply_knockback(target: Node) -> void:
var direction := (target.global_position - attack_owner.global_position).normalized()
if target.has_method("apply_knockback"):
target.apply_knockback(direction * knockback_force)gdscript
class_name Hitbox
extends Area2D
@export var damage: int = 10
@export var knockback_force: float = 200.0
@export var attack_owner: Node
var has_hit: Array[Node] = [] # Prevent multi-hit per swing
func _ready() -> void:
monitoring = false # Enable only during attack frames
func enable() -> void:
has_hit.clear()
monitoring = true
func disable() -> void:
monitoring = false
func _on_area_entered(area: Area2D) -> void:
if area is Hurtbox:
var target := area.owner_entity
if target != attack_owner and target not in has_hit:
has_hit.append(target)
var result := CombatController.calculate_damage(
attack_owner.stats, target.stats, damage
)
CombatController.apply_damage(target, result)
apply_knockback(target)
func apply_knockback(target: Node) -> void:
var direction := (target.global_position - attack_owner.global_position).normalized()
if target.has_method("apply_knockback"):
target.apply_knockback(direction * knockback_force)RPG Stats System
RPG属性系统
Attribute-Based Stats
基于基础属性的属性系统
gdscript
class_name RPGStats
extends Resource
signal stat_changed(stat_name: String, new_value: float)
signal level_up(new_level: int)gdscript
class_name RPGStats
extends Resource
signal stat_changed(stat_name: String, new_value: float)
signal level_up(new_level: int)Base attributes (increased on level up)
Base attributes (increased on level up)
@export var strength: int = 10
@export var dexterity: int = 10
@export var intelligence: int = 10
@export var vitality: int = 10
@export var strength: int = 10
@export var dexterity: int = 10
@export var intelligence: int = 10
@export var vitality: int = 10
Derived stats (calculated from attributes)
Derived stats (calculated from attributes)
var derived_stats: Dictionary = {}
var derived_stats: Dictionary = {}
Modifiers from equipment, buffs, etc.
Modifiers from equipment, buffs, etc.
var flat_modifiers: Dictionary = {} # +50 health
var percent_modifiers: Dictionary = {} # +10% damage
var level: int = 1
var experience: int = 0
var skill_points: int = 0
func _init() -> void:
recalculate_stats()
func recalculate_stats() -> void:
derived_stats = {
# Health: Vitality-based
"max_health": vitality * 10 + 100,
"health_regen": vitality * 0.5,
# Mana: Intelligence-based
"max_mana": intelligence * 8 + 50,
"mana_regen": intelligence * 0.3,
# Physical: Strength + Dexterity
"physical_damage": strength * 2,
"armor": strength + vitality,
# Critical: Dexterity-based
"crit_chance": 5.0 + dexterity * 0.2,
"crit_damage": 150.0 + dexterity * 0.5,
# Speed: Dexterity-based
"attack_speed": 1.0 + dexterity * 0.01,
"move_speed": 100.0 + dexterity * 2
}
# Apply modifiers
for stat_name in derived_stats:
var base := derived_stats[stat_name]
var flat := flat_modifiers.get(stat_name, 0.0)
var percent := percent_modifiers.get(stat_name, 0.0)
derived_stats[stat_name] = (base + flat) * (1.0 + percent / 100.0)func get_stat(stat_name: String) -> float:
if stat_name in derived_stats:
return derived_stats[stat_name]
return get(stat_name)
func add_experience(amount: int) -> void:
experience += amount
while experience >= get_xp_for_next_level():
experience -= get_xp_for_next_level()
level += 1
skill_points += 5
level_up.emit(level)
func get_xp_for_next_level() -> int:
# Exponential scaling
return int(100 * pow(1.5, level - 1))
---var flat_modifiers: Dictionary = {} # +50 health
var percent_modifiers: Dictionary = {} # +10% damage
var level: int = 1
var experience: int = 0
var skill_points: int = 0
func _init() -> void:
recalculate_stats()
func recalculate_stats() -> void:
derived_stats = {
# Health: Vitality-based
"max_health": vitality * 10 + 100,
"health_regen": vitality * 0.5,
# Mana: Intelligence-based
"max_mana": intelligence * 8 + 50,
"mana_regen": intelligence * 0.3,
# Physical: Strength + Dexterity
"physical_damage": strength * 2,
"armor": strength + vitality,
# Critical: Dexterity-based
"crit_chance": 5.0 + dexterity * 0.2,
"crit_damage": 150.0 + dexterity * 0.5,
# Speed: Dexterity-based
"attack_speed": 1.0 + dexterity * 0.01,
"move_speed": 100.0 + dexterity * 2
}
# Apply modifiers
for stat_name in derived_stats:
var base := derived_stats[stat_name]
var flat := flat_modifiers.get(stat_name, 0.0)
var percent := percent_modifiers.get(stat_name, 0.0)
derived_stats[stat_name] = (base + flat) * (1.0 + percent / 100.0)func get_stat(stat_name: String) -> float:
if stat_name in derived_stats:
return derived_stats[stat_name]
return get(stat_name)
func add_experience(amount: int) -> void:
experience += amount
while experience >= get_xp_for_next_level():
experience -= get_xp_for_next_level()
level += 1
skill_points += 5
level_up.emit(level)
func get_xp_for_next_level() -> int:
# Exponential scaling
return int(100 * pow(1.5, level - 1))
---Loot System
战利品系统
Item Generation
物品生成
gdscript
class_name LootGenerator
extends Node
enum Rarity { COMMON, UNCOMMON, RARE, EPIC, LEGENDARY }
const RARITY_WEIGHTS := {
Rarity.COMMON: 60,
Rarity.UNCOMMON: 25,
Rarity.RARE: 10,
Rarity.EPIC: 4,
Rarity.LEGENDARY: 1
}
const RARITY_AFFIX_COUNT := {
Rarity.COMMON: 0,
Rarity.UNCOMMON: 1,
Rarity.RARE: 2,
Rarity.EPIC: 3,
Rarity.LEGENDARY: 4
}
@export var affix_pool: Array[ItemAffix]
@export var base_items: Array[ItemBase]
func generate_item(item_level: int, magic_find: float = 0.0) -> Item:
var rarity := roll_rarity(magic_find)
var base := base_items.pick_random()
var item := Item.new()
item.base = base
item.rarity = rarity
item.item_level = item_level
# Roll affixes based on rarity
var affix_count := RARITY_AFFIX_COUNT[rarity]
var available_affixes := affix_pool.duplicate()
for i in affix_count:
if available_affixes.is_empty():
break
var affix := available_affixes.pick_random()
available_affixes.erase(affix)
item.affixes.append(generate_affix_roll(affix, item_level))
return item
func roll_rarity(magic_find: float) -> Rarity:
var weights := RARITY_WEIGHTS.duplicate()
# Magic find increases rare+ drops
weights[Rarity.RARE] *= (1.0 + magic_find / 100.0)
weights[Rarity.EPIC] *= (1.0 + magic_find / 100.0)
weights[Rarity.LEGENDARY] *= (1.0 + magic_find / 100.0)
var total := 0.0
for w in weights.values():
total += w
var roll := randf() * total
for rarity in weights:
roll -= weights[rarity]
if roll <= 0:
return rarity
return Rarity.COMMON
func generate_affix_roll(affix: ItemAffix, item_level: int) -> Dictionary:
# Scale affix values with item level
var min_roll := affix.min_value * (1.0 + item_level * 0.1)
var max_roll := affix.max_value * (1.0 + item_level * 0.1)
return {
"affix": affix,
"value": randf_range(min_roll, max_roll)
}gdscript
class_name LootGenerator
extends Node
enum Rarity { COMMON, UNCOMMON, RARE, EPIC, LEGENDARY }
const RARITY_WEIGHTS := {
Rarity.COMMON: 60,
Rarity.UNCOMMON: 25,
Rarity.RARE: 10,
Rarity.EPIC: 4,
Rarity.LEGENDARY: 1
}
const RARITY_AFFIX_COUNT := {
Rarity.COMMON: 0,
Rarity.UNCOMMON: 1,
Rarity.RARE: 2,
Rarity.EPIC: 3,
Rarity.LEGENDARY: 4
}
@export var affix_pool: Array[ItemAffix]
@export var base_items: Array[ItemBase]
func generate_item(item_level: int, magic_find: float = 0.0) -> Item:
var rarity := roll_rarity(magic_find)
var base := base_items.pick_random()
var item := Item.new()
item.base = base
item.rarity = rarity
item.item_level = item_level
# Roll affixes based on rarity
var affix_count := RARITY_AFFIX_COUNT[rarity]
var available_affixes := affix_pool.duplicate()
for i in affix_count:
if available_affixes.is_empty():
break
var affix := available_affixes.pick_random()
available_affixes.erase(affix)
item.affixes.append(generate_affix_roll(affix, item_level))
return item
func roll_rarity(magic_find: float) -> Rarity:
var weights := RARITY_WEIGHTS.duplicate()
# Magic find increases rare+ drops
weights[Rarity.RARE] *= (1.0 + magic_find / 100.0)
weights[Rarity.EPIC] *= (1.0 + magic_find / 100.0)
weights[Rarity.LEGENDARY] *= (1.0 + magic_find / 100.0)
var total := 0.0
for w in weights.values():
total += w
var roll := randf() * total
for rarity in weights:
roll -= weights[rarity]
if roll <= 0:
return rarity
return Rarity.COMMON
func generate_affix_roll(affix: ItemAffix, item_level: int) -> Dictionary:
# Scale affix values with item level
var min_roll := affix.min_value * (1.0 + item_level * 0.1)
var max_roll := affix.max_value * (1.0 + item_level * 0.1)
return {
"affix": affix,
"value": randf_range(min_roll, max_roll)
}Equipment System
装备系统
gdscript
class_name Equipment
extends Node
signal equipment_changed(slot: String, item: Item)
enum Slot { HEAD, CHEST, HANDS, LEGS, FEET, WEAPON, OFFHAND, RING1, RING2, AMULET }
var equipped: Dictionary = {} # Slot -> Item
func equip(item: Item) -> Item:
var slot: Slot = item.base.slot
var previous: Item = equipped.get(slot)
# Unequip old item
if previous:
remove_item_stats(previous)
# Equip new item
equipped[slot] = item
apply_item_stats(item)
equipment_changed.emit(Slot.keys()[slot], item)
return previous # Return to inventory
func apply_item_stats(item: Item) -> void:
var stats := owner.stats as RPGStats
# Base stats
for stat_name in item.base.base_stats:
stats.flat_modifiers[stat_name] = stats.flat_modifiers.get(stat_name, 0) + item.base.base_stats[stat_name]
# Affix stats
for affix_data in item.affixes:
var affix := affix_data["affix"] as ItemAffix
var value := affix_data["value"]
if affix.is_percent:
stats.percent_modifiers[affix.stat] = stats.percent_modifiers.get(affix.stat, 0) + value
else:
stats.flat_modifiers[affix.stat] = stats.flat_modifiers.get(affix.stat, 0) + value
stats.recalculate_stats()gdscript
class_name Equipment
extends Node
signal equipment_changed(slot: String, item: Item)
enum Slot { HEAD, CHEST, HANDS, LEGS, FEET, WEAPON, OFFHAND, RING1, RING2, AMULET }
var equipped: Dictionary = {} # Slot -> Item
func equip(item: Item) -> Item:
var slot: Slot = item.base.slot
var previous: Item = equipped.get(slot)
# Unequip old item
if previous:
remove_item_stats(previous)
# Equip new item
equipped[slot] = item
apply_item_stats(item)
equipment_changed.emit(Slot.keys()[slot], item)
return previous # Return to inventory
func apply_item_stats(item: Item) -> void:
var stats := owner.stats as RPGStats
# Base stats
for stat_name in item.base.base_stats:
stats.flat_modifiers[stat_name] = stats.flat_modifiers.get(stat_name, 0) + item.base.base_stats[stat_name]
# Affix stats
for affix_data in item.affixes:
var affix := affix_data["affix"] as ItemAffix
var value := affix_data["value"]
if affix.is_percent:
stats.percent_modifiers[affix.stat] = stats.percent_modifiers.get(affix.stat, 0) + value
else:
stats.flat_modifiers[affix.stat] = stats.flat_modifiers.get(affix.stat, 0) + value
stats.recalculate_stats()Ability System
技能系统
Skill Trees and Unlocks
技能树与解锁机制
gdscript
class_name SkillTree
extends Resource
@export var skills: Array[Skill]
@export var connections: Dictionary # skill_id -> Array[prerequisite_ids]
func can_unlock(skill_id: String, unlocked_skills: Array[String]) -> bool:
if skill_id in unlocked_skills:
return false # Already unlocked
var prereqs: Array = connections.get(skill_id, [])
for prereq in prereqs:
if prereq not in unlocked_skills:
return false
return true
func unlock_skill(skill_id: String, player: Node) -> bool:
var skill := get_skill(skill_id)
if not skill or player.stats.skill_points < skill.cost:
return false
player.stats.skill_points -= skill.cost
player.unlocked_skills.append(skill_id)
player.ability_manager.add_ability(skill.ability)
return truegdscript
class_name SkillTree
extends Resource
@export var skills: Array[Skill]
@export var connections: Dictionary # skill_id -> Array[prerequisite_ids]
func can_unlock(skill_id: String, unlocked_skills: Array[String]) -> bool:
if skill_id in unlocked_skills:
return false # Already unlocked
var prereqs: Array = connections.get(skill_id, [])
for prereq in prereqs:
if prereq not in unlocked_skills:
return false
return true
func unlock_skill(skill_id: String, player: Node) -> bool:
var skill := get_skill(skill_id)
if not skill or player.stats.skill_points < skill.cost:
return false
player.stats.skill_points -= skill.cost
player.unlocked_skills.append(skill_id)
player.ability_manager.add_ability(skill.ability)
return trueActive Abilities
主动技能
gdscript
class_name ActiveAbility
extends Resource
@export var name: String
@export var cooldown: float = 5.0
@export var mana_cost: int = 20
@export var damage_multiplier: float = 2.0
@export var aoe_radius: float = 0.0
@export var effect_scene: PackedScene
var current_cooldown: float = 0.0
func can_use(caster: Node) -> bool:
return current_cooldown <= 0 and caster.stats.current_mana >= mana_cost
func use(caster: Node, target_position: Vector2) -> void:
if not can_use(caster):
return
caster.stats.current_mana -= mana_cost
current_cooldown = cooldown
var effect := effect_scene.instantiate()
effect.global_position = target_position
effect.damage = int(caster.stats.get_stat("physical_damage") * damage_multiplier)
effect.caster = caster
caster.get_tree().current_scene.add_child(effect)
func update_cooldown(delta: float) -> void:
current_cooldown = max(0, current_cooldown - delta)gdscript
class_name ActiveAbility
extends Resource
@export var name: String
@export var cooldown: float = 5.0
@export var mana_cost: int = 20
@export var damage_multiplier: float = 2.0
@export var aoe_radius: float = 0.0
@export var effect_scene: PackedScene
var current_cooldown: float = 0.0
func can_use(caster: Node) -> bool:
return current_cooldown <= 0 and caster.stats.current_mana >= mana_cost
func use(caster: Node, target_position: Vector2) -> void:
if not can_use(caster):
return
caster.stats.current_mana -= mana_cost
current_cooldown = cooldown
var effect := effect_scene.instantiate()
effect.global_position = target_position
effect.damage = int(caster.stats.get_stat("physical_damage") * damage_multiplier)
effect.caster = caster
caster.get_tree().current_scene.add_child(effect)
func update_cooldown(delta: float) -> void:
current_cooldown = max(0, current_cooldown - delta)Enemy Design
敌人设计
Scaling Difficulty
难度缩放
gdscript
class_name EnemySpawner
extends Node
@export var base_enemy_scene: PackedScene
@export var area_level: int = 1
func spawn_enemy(position: Vector2) -> Node:
var enemy := base_enemy_scene.instantiate()
enemy.global_position = position
# Scale stats with area level
var stats := enemy.stats as RPGStats
var level_mult := 1.0 + (area_level - 1) * 0.15
stats.vitality = int(stats.vitality * level_mult)
stats.strength = int(stats.strength * level_mult)
stats.recalculate_stats()
# Scale rewards
enemy.xp_reward = int(enemy.xp_reward * level_mult)
enemy.loot_table.item_level = area_level
add_child(enemy)
return enemygdscript
class_name EnemySpawner
extends Node
@export var base_enemy_scene: PackedScene
@export var area_level: int = 1
func spawn_enemy(position: Vector2) -> Node:
var enemy := base_enemy_scene.instantiate()
enemy.global_position = position
# Scale stats with area level
var stats := enemy.stats as RPGStats
var level_mult := 1.0 + (area_level - 1) * 0.15
stats.vitality = int(stats.vitality * level_mult)
stats.strength = int(stats.strength * level_mult)
stats.recalculate_stats()
# Scale rewards
enemy.xp_reward = int(enemy.xp_reward * level_mult)
enemy.loot_table.item_level = area_level
add_child(enemy)
return enemyCommon Pitfalls
常见误区
| Pitfall | Solution |
|---|---|
| Stats feel meaningless | Ensure each point noticeably affects gameplay |
| Loot feels same | Dramatic visual and mechanical differences between rarities |
| Combat too simple | Combo systems, positioning matters, enemy variety |
| Progression walls | Multiple viable paths, catch-up mechanics |
| Inventory management tedium | Auto-sort, quick-sell, stash tabs |
| 误区 | 解决方案 |
|---|---|
| 属性显得毫无意义 | 确保每一点属性都能显著影响游戏体验 |
| 战利品千篇一律 | 不同稀有度的物品在视觉和机制上要有显著差异 |
| 战斗过于简单 | 加入连招系统、强调站位、增加敌人种类 |
| 成长瓶颈 | 提供多种可行的成长路径,加入追赶机制 |
| 背包管理繁琐 | 加入自动排序、快速出售、储物分页等功能 |
Architecture Overview
架构概述
AutoLoads:
├── PlayerStats (godot-rpg-stats)
├── InventoryManager (godot-inventory-system)
├── QuestManager (godot-quest-system)
├── LootGenerator (godot-economy-system)
└── GameManager (godot-scene-management)
Player:
├── CharacterBody2D/3D
├── RPGStats
├── Equipment
├── AbilityManager
├── Hitbox/Hurtbox
└── InputHandler
Enemies:
├── AI Controller (state machine)
├── RPGStats (scaled)
├── HealthComponent
├── LootTable
└── Hitbox/HurtboxAutoLoads:
├── PlayerStats (godot-rpg-stats)
├── InventoryManager (godot-inventory-system)
├── QuestManager (godot-quest-system)
├── LootGenerator (godot-economy-system)
└── GameManager (godot-scene-management)
Player:
├── CharacterBody2D/3D
├── RPGStats
├── Equipment
├── AbilityManager
├── Hitbox/Hurtbox
└── InputHandler
Enemies:
├── AI Controller (state machine)
├── RPGStats (scaled)
├── HealthComponent
├── LootTable
└── Hitbox/HurtboxGodot-Specific Tips
Godot专属技巧
- Resources for items: Use for items - easily serializable for save/load
Resource - Object pooling: Pool damage numbers, projectiles, item pickups
- Animation callbacks: Use AnimationPlayer method tracks to enable/disable hitboxes
- Stat recalculation: Only recalculate on equip/level, not every frame
- 用Resource实现物品系统:使用来定义物品,便于序列化以实现存档/读档功能
Resource - 对象池技术:对伤害数字、投射物、物品拾取物等使用对象池
- 动画回调:利用AnimationPlayer的方法轨道来启用/禁用hitbox
- 属性重计算:只在装备更换或升级时重计算属性,不要每帧都计算
Example Games for Reference
参考游戏示例
- Diablo / Path of Exile - Loot-focused ARPG
- Elden Ring / Dark Souls - Combat-focused action RPG
- Hades - Roguelike ARPG hybrid
- Grim Dawn - Deep character builds
- Diablo / Path of Exile - 以战利品为核心的ARPG
- Elden Ring / Dark Souls - 以战斗为核心的动作RPG
- Hades - 肉鸽类ARPG混合游戏
- Grim Dawn - 拥有深度角色构筑的ARPG
Reference
参考资料
- Master Skill: godot-master
- 核心技能:godot-master