1. 核心玩法设计
1. 伏魔记——核心玩法
本章你将掌握
通过本章学习,你将理解并掌握以下核心知识:
- RPG核心循环:理解"探索-任务-战斗-升级-剧情"的游戏循环
- 场景类型设计:掌握城镇、地下城、战斗三种场景的功能划分
- 角色属性系统:学会设计HP、MP、攻击、防御等核心属性及其数据结构
- 回合制战斗机制:理解速度决定先手、伤害计算公式等战斗原理
- 升级成长系统:掌握经验值累积、等级提升、属性增长的设计
- 游戏状态管理:使用状态机协调探索、对话、战斗、菜单等不同状态
- 数据结构设计:学会用结构体和类封装复杂的游戏数据
这些知识将为你构建一个完整的RPG游戏奠定坚实基础。
1.1 伏魔记是什么?
想象你穿越到了一个仙侠世界。你扮演一位初出茅庐的少年侠客,从一个宁静的小镇出发,走遍山川河流,和各路妖怪战斗,结交志同道合的伙伴,最终拯救天下苍生。
这就是伏魔记——一款致敬经典国产RPG(角色扮演游戏)的2D游戏,灵感来源于《仙剑奇侠传》《轩辕剑》等国民级作品。在这类游戏里,你不只是"操控一个角色",而是"扮演一个角色"——你会经历它的喜怒哀乐,做出影响故事走向的抉择。
为什么选择伏魔记作为进阶项目?
| 原因 | 说明 |
|---|---|
| 系统全面 | 涵盖对话系统、背包系统、战斗系统、任务系统等RPG核心模块 |
| 剧情驱动 | 学会用代码讲故事,把叙事和游戏机制结合 |
| 数据量大 | 锻炼管理复杂数据结构的能力(角色属性、物品、任务链等) |
| 成就感强 | 做出来之后,你拥有一个"可以和朋友分享的完整RPG" |
1.2 RPG的核心循环
每一个RPG都有一个核心循环(Core Loop)——玩家反复做的事情。伏魔记的核心循环可以概括为六个字:
┌─────────────────────────────────────────────────────────┐
│ │
│ 城镇探索 → 接任务 → 地下城冒险 → 战斗 → 升级 → 推进剧情 │
│ ↑ │ │
│ └────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘用大白话拆开来说:
- 城镇探索:玩家在安全的城镇里自由走动,和NPC(非玩家角色)聊天,去商店买卖东西
- 接任务:某个NPC告诉你"山上的妖怪最近越来越猖狂了",给你一个任务
- 地下城冒险:玩家离开城镇,走进危险的洞穴、森林或古墓
- 战斗:在地下城里遇到妖怪,进入回合制战斗——你打我一下,我打你一下,看谁的血先归零
- 升级:战斗胜利后获得经验值,攒够了就升级,变得更强
- 推进剧情:完成任务后,故事向前发展,解锁新的区域和更强大的敌人
然后回到城镇,接新任务,开始新一轮循环。
1.3 游戏世界的组成
一个RPG的世界由几种不同类型的"场景"组成,就像电影有不同的拍摄场景一样:
1.3.1 城镇场景
城镇是玩家的"大本营",在这里一切都是安全的——没有怪物会攻击你。
| 城镇功能 | 说明 | 对应系统 |
|---|---|---|
| 自由走动 | 用方向键控制角色在城镇里走来走去 | 移动系统 |
| NPC对话 | 走到NPC面前按确认键,和NPC聊天 | 对话系统 |
| 商店 | 和商人NPC对话,买卖装备和药材 | 背包系统 |
| 存档 | 找到特定NPC或道具,保存游戏进度 | 存档系统 |
| 任务接取 | 和有"感叹号"标记的NPC对话接任务 | 任务系统 |
1.3.2 地下城场景
地下城是危险区域,到处都是怪物。
| 地下城元素 | 说明 |
|---|---|
| 瓦片地图 | 用不同图案的方块拼出来的地图,有地面、墙壁、装饰等 |
| 遭遇战 | 在地图上走动时,随机或固定触发战斗 |
| 宝箱 | 散落在各处,打开可以获得金币、装备或关键道具 |
| 传送点 | 到达特定位置后,可以传送到其他区域 |
| Boss战 | 每个地下城的深处都有一个强大的Boss等着你 |
1.3.3 战斗场景
战斗在独立的场景中进行,和探索场景完全分开。
┌──────────────────────────────────┐
│ 战斗场景布局 │
│ │
│ [敌人A] [敌人B] [敌人C] │ ← 敌人区域(上方)
│ │
│ ─────────────────────────────── │
│ │
│ [角色A] [角色B] [角色C] │ ← 我方区域(下方)
│ │
│ ┌──────┬──────┬──────┬──────┐ │
│ │ 攻击 │ 技能 │ 物品 │ 防御 │ │ ← 行动菜单
│ └──────┴──────┴──────┴──────┘ │
│ │
│ HP: ████████░░ 80/100 │ ← 状态栏
└──────────────────────────────────┘1.4 角色属性系统
RPG中,每个角色(包括玩家和敌人)都有一组属性(Stats),决定了角色的强弱。你可以把属性理解为一个角色的"能力值档案":
| 属性 | 英文名 | 说明 | 生活比喻 |
|---|---|---|---|
| 生命值 | HP (Health Points) | 角色的血量,归零就倒了 | 你能挨多少下打 |
| 法力值 | MP (Magic Points) | 释放技能消耗的能量 | 你有多少"内力"可以施展 |
| 攻击力 | ATK (Attack) | 普通攻击造成的伤害 | 你拳头有多重 |
| 防御力 | DEF (Defense) | 减少受到的伤害 | 你穿了多少"护甲" |
| 速度 | SPD (Speed) | 决定行动先后顺序 | 谁先出手 |
| 经验值 | EXP (Experience) | 战斗胜利后获得,攒够就升级 | 你的"经验积累" |
| 等级 | LV (Level) | 角色的实力等级 | 你是"新手"还是"老手" |
属性的数据结构
在代码中,我们用一个结构体来存储角色属性:
using Godot;
/// <summary>
/// 角色属性——记录一个角色的所有能力值
/// </summary>
public struct CharacterStats
{
public string Name; // 角色名字
public int Level; // 等级
public int MaxHp; // 最大生命值
public int CurrentHp; // 当前生命值
public int MaxMp; // 最大法力值
public int CurrentMp; // 当前法力值
public int Attack; // 攻击力
public int Defense; // 防御力
public int Speed; // 速度
public int CurrentExp; // 当前经验值
public int ExpToNextLevel; // 升到下一级需要的经验值
/// <summary>
/// 创建一个新角色的默认属性
/// </summary>
public static CharacterStats CreateDefault(string name)
{
return new CharacterStats
{
Name = name,
Level = 1,
MaxHp = 100,
CurrentHp = 100,
MaxMp = 30,
CurrentMp = 30,
Attack = 15,
Defense = 8,
Speed = 10,
CurrentExp = 0,
ExpToNextLevel = 50
};
}
/// <summary>
/// 受到伤害(扣除HP,最少保留1点)
/// </summary>
public CharacterStats TakeDamage(int damage)
{
// 实际伤害 = 原始伤害 - 防御力(最低1点)
int actualDamage = Mathf.Max(damage - Defense, 1);
int newHp = Mathf.Max(CurrentHp - actualDamage, 0);
var result = this;
result.CurrentHp = newHp;
return result;
}
/// <summary>
/// 恢复HP
/// </summary>
public CharacterStats Heal(int amount)
{
int newHp = Mathf.Min(CurrentHp + amount, MaxHp);
var result = this;
result.CurrentHp = newHp;
return result;
}
/// <summary>
/// 检查角色是否还活着
/// </summary>
public bool IsAlive => CurrentHp > 0;
}## 角色属性——记录一个角色的所有能力值
class_name CharacterStats
var name: String ## 角色名字
var level: int ## 等级
var max_hp: int ## 最大生命值
var current_hp: int ## 当前生命值
var max_mp: int ## 最大法力值
var current_mp: int ## 当前法力值
var attack: int ## 攻击力
var defense: int ## 防御力
var speed: int ## 速度
var current_exp: int ## 当前经验值
var exp_to_next_level: int ## 升到下一级需要的经验值
## 创建一个新角色的默认属性
static func create_default(character_name: String) -> CharacterStats:
var stats = CharacterStats.new()
stats.name = character_name
stats.level = 1
stats.max_hp = 100
stats.current_hp = 100
stats.max_mp = 30
stats.current_mp = 30
stats.attack = 15
stats.defense = 8
stats.speed = 10
stats.current_exp = 0
stats.exp_to_next_level = 50
return stats
## 受到伤害(扣除HP,最少保留1点)
func take_damage(damage: int) -> CharacterStats:
# 实际伤害 = 原始伤害 - 防御力(最低1点)
var actual_damage: int = maxf(damage - defense, 1)
var new_hp: int = maxf(current_hp - actual_damage, 0)
var result = CharacterStats.new()
result.name = name
result.level = level
result.max_hp = max_hp
result.current_hp = new_hp
result.max_mp = max_mp
result.current_mp = current_mp
result.attack = attack
result.defense = defense
result.speed = speed
result.current_exp = current_exp
result.exp_to_next_level = exp_to_next_level
return result
## 恢复HP
func heal(amount: int) -> CharacterStats:
var new_hp: int = minf(current_hp + amount, max_hp)
var result = CharacterStats.new()
result.name = name
result.level = level
result.max_hp = max_hp
result.current_hp = new_hp
result.max_mp = max_mp
result.current_mp = current_mp
result.attack = attack
result.defense = defense
result.speed = speed
result.current_exp = current_exp
result.exp_to_next_level = exp_to_next_level
return result
## 检查角色是否还活着
func is_alive() -> bool:
return current_hp > 01.5 回合制战斗原理
伏魔记采用的是经典的回合制战斗。什么是回合制?简单说就是:你打我一下,我打你一下,轮流来。
这就像两个人打牌——你出一张,对方出一张,不能同时出手。谁先出手取决于谁的速度更快。
战斗流程
┌─────────────┐
│ 遭遇敌人 │
└──────┬──────┘
↓
┌─────────────┐
│ 比较速度 │ ──→ 谁速度快,谁先行动
│ 决定先手 │
└──────┬──────┘
↓
┌─────────────┐
│ 选择行动 │ ──→ 攻击 / 技能 / 物品 / 防御
└──────┬──────┘
↓
┌─────────────┐
│ 执行行动 │ ──→ 计算伤害,播放动画
│ 更新血量 │
└──────┬──────┘
↓
┌──────────────┐
│ 敌人全倒了吗? │ ── 是 → 战斗胜利,获得奖励
└──────┬───────┘
│ 否
↓
┌──────────────┐
│ 我方全倒了吗? │ ── 是 → 战斗失败
└──────┬───────┘
│ 否
↓
┌─────────────┐
│ 敌人行动 │ ──→ 敌人AI选择行动
└──────┬──────┘
↓
回到"选择行动"伤害计算公式
最简单的伤害计算公式:
实际伤害 = 攻击方.攻击力 - 防御方.防御力如果结果是负数或零,至少造成1点伤害(总不能打人还帮人回血吧)。
/// <summary>
/// 战斗计算器——处理伤害、治疗等数值计算
/// </summary>
public static class BattleCalculator
{
/// <summary>
/// 计算普通攻击的伤害
/// </summary>
public static int CalculateDamage(CharacterStats attacker, CharacterStats defender)
{
// 基础伤害 = 攻击力 - 防御力(最低1)
int baseDamage = Mathf.Max(attacker.Attack - defender.Defense, 1);
// 加一点随机波动(±10%),让战斗不那么死板
float randomFactor = GD.Randf() * 0.2f + 0.9f; // 0.9 ~ 1.1
int finalDamage = Mathf.RoundToInt(baseDamage * randomFactor);
return Mathf.Max(finalDamage, 1);
}
/// <summary>
/// 计算技能伤害
/// </summary>
public static int CalculateSkillDamage(
CharacterStats attacker,
CharacterStats defender,
int skillPower,
float multiplier = 1.5f)
{
// 技能伤害 = (技能威力 * 攻击力 - 防御力) * 倍率
int baseDamage = Mathf.Max(
(int)(skillPower * attacker.Attack * multiplier) - defender.Defense,
1
);
return baseDamage;
}
/// <summary>
/// 计算治疗后恢复的HP
/// </summary>
public static int CalculateHeal(int healPower, CharacterStats target)
{
int healAmount = healPower;
// 不能超过最大HP
int actualHeal = Mathf.Min(healAmount, target.MaxHp - target.CurrentHp);
return Mathf.Max(actualHeal, 0);
}
}## 战斗计算器——处理伤害、治疗等数值计算
class_name BattleCalculator
## 计算普通攻击的伤害
static func calculate_damage(attacker: CharacterStats, defender: CharacterStats) -> int:
# 基础伤害 = 攻击力 - 防御力(最低1)
var base_damage: int = maxf(attacker.attack - defender.defense, 1)
# 加一点随机波动(±10%),让战斗不那么死板
var random_factor: float = randf() * 0.2 + 0.9 # 0.9 ~ 1.1
var final_damage: int = roundi(base_damage * random_factor)
return maxf(final_damage, 1)
## 计算技能伤害
static func calculate_skill_damage(
attacker: CharacterStats,
defender: CharacterStats,
skill_power: int,
multiplier: float = 1.5
) -> int:
# 技能伤害 = (技能威力 * 攻击力 - 防御力) * 倍率
var base_damage: int = maxf(
int(skill_power * attacker.attack * multiplier) - defender.defense,
1
)
return base_damage
## 计算治疗后恢复的HP
static func calculate_heal(heal_power: int, target: CharacterStats) -> int:
var heal_amount: int = heal_power
# 不能超过最大HP
var actual_heal: int = minf(heal_amount, target.max_hp - target.current_hp)
return maxf(actual_heal, 0)1.6 升级系统
战斗胜利后,角色会获得经验值(EXP)。经验值攒够了,角色就会升级,各项属性都会提升——就像你在学校里努力学习,攒够了学分就能升级一样。
/// <summary>
/// 升级系统——处理经验值和等级提升
/// </summary>
public static class LevelSystem
{
/// <summary>
/// 升到下一级需要的经验值(递增公式)
/// </summary>
public static int GetExpRequired(int level)
{
// 每升一级需要的经验越来越多
// 公式:50 + (等级 - 1) * 30
return 50 + (level - 1) * 30;
}
/// <summary>
/// 给角色增加经验值,检查是否升级
/// 返回升了几级
/// </summary>
public static (CharacterStats newStats, int levelsGained) AddExperience(
CharacterStats stats, int expGained)
{
var current = stats;
int levelsGained = 0;
int remainingExp = expGained;
// 循环检查是否可以升级
while (remainingExp > 0 && current.IsAlive)
{
int expNeeded = current.ExpToNextLevel - current.CurrentExp;
if (remainingExp >= expNeeded)
{
// 经验够升级了
remainingExp -= expNeeded;
current = LevelUp(current);
levelsGained++;
}
else
{
// 经验不够,先存着
current.CurrentExp += remainingExp;
remainingExp = 0;
}
}
return (current, levelsGained);
}
/// <summary>
/// 执行一次升级,提升各项属性
/// </summary>
private static CharacterStats LevelUp(CharacterStats stats)
{
var newStats = stats;
newStats.Level += 1;
// 属性提升(每次升级固定增长)
newStats.MaxHp += 15; // 生命值+15
newStats.MaxMp += 5; // 法力值+5
newStats.Attack += 3; // 攻击力+3
newStats.Defense += 2; // 防御力+2
newStats.Speed += 1; // 速度+1
// 升级后恢复满状态
newStats.CurrentHp = newStats.MaxHp;
newStats.CurrentMp = newStats.MaxMp;
newStats.CurrentExp = 0;
newStats.ExpToNextLevel = GetExpRequired(newStats.Level);
return newStats;
}
}## 升级系统——处理经验值和等级提升
class_name LevelSystem
## 升到下一级需要的经验值(递增公式)
static func get_exp_required(level: int) -> int:
# 每升一级需要的经验越来越多
# 公式:50 + (等级 - 1) * 30
return 50 + (level - 1) * 30
## 给角色增加经验值,检查是否升级
## 返回一个数组:[新属性, 升了几级]
static func add_experience(stats: CharacterStats, exp_gained: int) -> Array:
var current = stats
var levels_gained: int = 0
var remaining_exp: int = exp_gained
# 循环检查是否可以升级
while remaining_exp > 0 and current.is_alive():
var exp_needed: int = current.exp_to_next_level - current.current_exp
if remaining_exp >= exp_needed:
# 经验够升级了
remaining_exp -= exp_needed
current = level_up(current)
levels_gained += 1
else:
# 经验不够,先存着
current.current_exp += remaining_exp
remaining_exp = 0
return [current, levels_gained]
## 执行一次升级,提升各项属性
static func level_up(stats: CharacterStats) -> CharacterStats:
var new_stats = CharacterStats.new()
new_stats.name = stats.name
new_stats.level = stats.level + 1
# 属性提升(每次升级固定增长)
new_stats.max_hp = stats.max_hp + 15 # 生命值+15
new_stats.max_mp = stats.max_mp + 5 # 法力值+5
new_stats.attack = stats.attack + 3 # 攻击力+3
new_stats.defense = stats.defense + 2 # 防御力+2
new_stats.speed = stats.speed + 1 # 速度+1
# 升级后恢复满状态
new_stats.current_hp = new_stats.max_hp
new_stats.current_mp = new_stats.max_mp
new_stats.current_exp = 0
new_stats.exp_to_next_level = get_exp_required(new_stats.level)
return new_stats1.7 游戏状态管理
RPG有多种不同的游戏状态——在城镇里走动、和NPC对话、在战斗中、看菜单。我们需要一个状态机来管理这些状态的切换。
┌──────────┐
│ 主菜单 │
└────┬─────┘
↓ 新游戏/继续
┌──────────┐ 按菜单键 ┌──────────┐
│ 城镇探索 │ ←────────────→ │ 游戏菜单 │
└────┬─────┘ └──────────┘
│ 遭遇敌人
↓
┌──────────┐
│ 回合战斗 │
└────┬─────┘
│ 战斗结束
↓
┌──────────┐
│ 战斗结算 │ ──→ 获得经验/金币 → 回到探索
└──────────┘/// <summary>
/// RPG游戏状态枚举
/// </summary>
public enum RpgGameState
{
MainMenu, // 主菜单
TownExplore, // 城镇/野外探索
Dialogue, // NPC对话中
Battle, // 回合制战斗中
BattleResult, // 战斗结算
GameMenu, // 游戏内菜单(背包/状态/存档)
Shop, // 商店
Cutscene // 过场剧情
}
/// <summary>
/// 游戏状态管理器
/// </summary>
public partial class RpgGameStateManager : Node
{
private RpgGameState _currentState = RpgGameState.MainMenu;
public RpgGameState CurrentState => _currentState;
/// <summary>
/// 切换到新状态
/// </summary>
public void ChangeState(RpgGameState newState)
{
if (_currentState == newState) return;
RpgGameState oldState = _currentState;
_currentState = newState;
GD.Print($"状态切换: {oldState} → {newState}");
// 根据新状态做对应的处理
switch (newState)
{
case RpgGameState.TownExplore:
GetTree().Paused = false;
break;
case RpgGameState.Battle:
GetTree().Paused = false;
break;
case RpgGameState.GameMenu:
GetTree().Paused = true;
break;
}
}
/// <summary>
/// 检查当前是否处于可交互状态
/// </summary>
public bool CanInteract()
{
return _currentState == RpgGameState.TownExplore;
}
}## RPG游戏状态枚举
enum RpgGameState {
MAIN_MENU, ## 主菜单
TOWN_EXPLORE, ## 城镇/野外探索
DIALOGUE, ## NPC对话中
BATTLE, ## 回合制战斗中
BATTLE_RESULT, ## 战斗结算
GAME_MENU, ## 游戏内菜单
SHOP, ## 商店
CUTSCENE ## 过场剧情
}
extends Node
var _current_state: RpgGameState = RpgGameState.MAIN_MENU
## 切换到新状态
func change_state(new_state: RpgGameState) -> void:
if _current_state == new_state:
return
var old_state: RpgGameState = _current_state
_current_state = new_state
print("状态切换: ", old_state, " → ", new_state)
# 根据新状态做对应的处理
match new_state:
RpgGameState.TOWN_EXPLORE:
get_tree().paused = false
RpgGameState.BATTLE:
get_tree().paused = false
RpgGameState.GAME_MENU:
get_tree().paused = true
## 检查当前是否处于可交互状态
func can_interact() -> bool:
return _current_state == RpgGameState.TOWN_EXPLORE游戏核心节点
点击节点可跳转到对应文档查看详细说明。
1.8 本章小结
在这一章中,我们从零分析了伏魔记这款RPG游戏的核心设计:
| 知识点 | 说明 |
|---|---|
| 核心循环 | 城镇探索→接任务→地下城冒险→战斗→升级→推进剧情 |
| 场景类型 | 城镇、地下城、战斗三种场景各司其职 |
| 角色属性 | HP、MP、攻击、防御、速度等,构成角色的"能力值档案" |
| 回合制战斗 | 你打我一下我打你一下,速度决定先手 |
| 伤害计算 | 攻击力减防御力,加随机波动 |
| 升级系统 | 攒经验→升级→属性提升→恢复满状态 |
| 状态管理 | 用状态机管理菜单、探索、战斗、对话等不同游戏阶段 |
下一章我们将正式搭建项目结构,创建场景骨架,为后续的系统开发打好地基。
