简介:这份资源面向Java初学者与游戏开发入门者,提供一套回合制游戏核心逻辑的源码实现,帮助理解轮流行动机制下游戏流程与角色行为的组织方式。包内共0个文件,压缩包约1KB,以Java源码类文件为主,涵盖游戏管理器、士兵与Boss三类核心角色,分别承担流程调度、基础战斗单位与高难度敌人逻辑的职责。已有1015人学习下载,适合作为面向对象编程与设计模式的练手素材。读者可从中了解如何用管理器统一调度回合、决定当前行动者并调用其行为方法,如何为士兵定义生命值、攻击力、防御力与移动速度等属性及攻击、防御、移动等动作,以及如何让Boss扩展士兵能力并加入特殊技能、阶段变化或复杂AI逻辑。代码还体现了工厂模式创建角色、策略模式定义行为技能、状态模式处理战斗与非战斗状态等思路,并涉及封装、继承与多态等原则,便于对照学习如何构建可扩展的回合制游戏骨架。
1. 回合制游戏源码拆解:从战斗状态机到可运行工程的落地笔记
回合制游戏看起来简单——你一刀我一刀,轮流动手。但真去翻一份能跑的源码,你会发现它比很多即时制项目更考验状态管理:行动顺序、技能冷却、Buff 叠加、AI 决策、战斗结算,每一步都得有明确的阶段划分,否则一个回合没走完就乱套。我最近拆了一份回合制游戏的完整工程源码,它把战斗流程拆成了清晰的状态机,角色属性、技能配置、AI 行为都做了数据驱动,改数值不用动逻辑代码。这份资源适合两类人:想学游戏状态管理的开发者,以及想拿一套能跑的战斗框架做二次开发的人。下面按「能干什么 → 怎么跑 → 怎么改」的顺序,把这份源码拆开讲。
2. 工程结构与技术选型:为什么用状态机而不是协程
2.1 目录分层与模块职责
拿到源码先看目录,这份工程的顶层结构大致是这样:
TurnBasedGame/ ├── Assets/ │ ├── Scripts/ │ │ ├── Core/ # 战斗状态机、回合调度、事件总线 │ │ ├── Units/ # 角色基类、属性组件、Buff 容器 │ │ ├── Skills/ # 技能基类、效果实现、目标选择器 │ │ ├── AI/ # 行为树节点、决策评分 │ │ ├── UI/ # 战斗界面、行动条、伤害飘字 │ │ └── Data/ # ScriptableObject 配置、加载器 │ ├── Resources/ │ │ ├── Units/ # 角色配置资产 │ │ ├── Skills/ # 技能配置资产 │ │ └── AIDecisions/ # AI 决策权重配置 │ └── Scenes/ │ └── Battle.unity # 战斗主场景 ├── Packages/ └── ProjectSettings/Core 层是整场战斗的骨架,Units 和 Skills 是数据载体,AI 层只负责「选什么技能打谁」,UI 层订阅事件刷新表现。这种分层的好处是:战斗逻辑不依赖 MonoBehaviour 的生命周期,可以脱离场景单独跑单元测试。常见做法是把状态机做成纯 C# 类,Unity 只负责驱动Tick。
2.2 战斗状态机的阶段划分
回合制战斗的核心是「阶段推进」。这份源码把一场战斗拆成六个阶段,用枚举驱动:
public enum BattlePhase { BattleStart, // 战斗初始化,加载双方单位 RoundStart, // 回合开始,重置行动条、结算回合 Buff TurnStart, // 单个单位行动开始,结算该单位 Buff WaitingForInput, // 等待玩家输入或 AI 决策 ExecutingAction, // 执行技能,播放表现 TurnEnd, // 行动结束,检查死亡、触发被动 RoundEnd, // 回合结束,检查胜负 BattleEnd // 战斗结束,结算奖励 }状态机在BattleManager.Update里推进,每个阶段只做一件事。比如TurnStart阶段会先结算「中毒」「再生」这类持续效果,再判断单位是否被控制(眩晕跳过行动),最后才进入WaitingForInput。这样拆的好处是:任何一个环节出问题,看当前处于哪个 Phase 就能定位。
注意:状态机推进必须加超时保护。如果某个阶段因为异常没退出,战斗会卡死。常见做法是在
BattleManager里记录每个阶段的进入时间,超过阈值强制跳到下一阶段并打日志。
2.3 行动条与速度排序的实现
回合制里「谁先动手」由行动条决定。这份源码没有用简单的速度排序,而是用了「行动值累积」模型:
public class ActionGauge { public Unit Owner; public float CurrentValue; public float Speed => Owner.GetAttribute(AttributeType.Speed); // 每帧累积行动值,达到阈值即可行动 public bool Tick(float deltaTime) { CurrentValue += Speed * deltaTime; if (CurrentValue >= 1000f) { CurrentValue -= 1000f; return true; // 该单位可以行动 } return false; } }Speed越高,行动值攒得越快,出手频率越高。阈值 1000 是拍脑袋定的,实际项目里可以调。这种模型比「按速度排个序轮流动」更灵活,因为速度 Buff 可以实时影响出手顺序,不用重排整个队列。参数说明:deltaTime是帧间隔,CurrentValue是当前累积值,Speed从属性组件读取,支持装备和 Buff 加成。
2.4 事件总线解耦战斗逻辑与表现
战斗逻辑不该直接调 UI。这份源码用了一个轻量事件总线:
public static class EventBus { private static readonly Dictionary<Type, Delegate> handlers = new(); public static void Subscribe<T>(Action<T> callback) where T : IGameEvent { var type = typeof(T); handlers[type] = Delegate.Combine(handlers.GetValueOrDefault(type), callback); } public static void Publish<T>(T evt) where T : IGameEvent { if (handlers.TryGetValue(typeof(T), out var d)) (d as Action<T>)?.Invoke(evt); } }伤害计算完发一个DamageEvent,UI 层订阅后播飘字;单位死亡发UnitDeathEvent,AI 层订阅后重新评估目标。逻辑层和表现层互不引用,改 UI 不会碰坏战斗。常见坑是忘记取消订阅导致内存泄漏,我一般会在OnDestroy里统一反注册。
3. 核心模块拆解:技能系统与 AI 决策怎么写
3.1 技能基类与效果组合
技能系统是回合制游戏最容易被改烂的地方。这份源码把技能拆成「目标选择 + 效果列表」两部分:
[CreateAssetMenu(menuName = "Game/Skill")] public class SkillConfig : ScriptableObject { public string skillId; public string displayName; public int cost; // 消耗(怒气/法力) public TargetSelector selector; // 目标选择器 public List<SkillEffect> effects; // 效果列表,按顺序执行 public int cooldown; // 冷却回合数 } public abstract class SkillEffect : ScriptableObject { public abstract void Apply(Unit caster, Unit target, BattleContext ctx); }TargetSelector也是 ScriptableObject,常见实现有「单体敌方」「全体敌方」「血量最低友方」「随机敌人」。效果则包括DamageEffect、HealEffect、BuffEffect、SummonEffect。一个技能就是一组效果的组合,比如「火球术」= 单体敌方 + 伤害效果 + 灼烧 Buff。
这种设计的好处是加新技能不用写新类,策划在编辑器里拖几个效果资产就完事。我见过不少项目把技能写成硬编码的switch-case,加一个技能改一次代码,后期维护成本极高。
3.2 伤害计算公式与属性穿透
伤害计算放在DamageEffect.Apply里,公式大致是:
public override void Apply(Unit caster, Unit target, BattleContext ctx) { float atk = caster.GetAttribute(AttributeType.Attack); float def = target.GetAttribute(AttributeType.Defense); float penetration = caster.GetAttribute(AttributeType.Penetration); // 防御减免公式:防御越高减伤越多,但有下限 float effectiveDef = Mathf.Max(0, def - penetration); float reduction = effectiveDef / (effectiveDef + 200f); float damage = atk * (1f - reduction); // 暴击判定 float critRate = caster.GetAttribute(AttributeType.CritRate); bool isCrit = Random.value < critRate; if (isCrit) damage *= caster.GetAttribute(AttributeType.CritDamage); target.TakeDamage(damage, isCrit, caster); }200f是防御系数,调大则防御收益降低,调小则防御更值钱。penetration是穿透属性,先减防御再算减免。暴击率和暴击伤害从属性组件读,支持 Buff 动态修改。这段代码里Random.value是 Unity 的全局随机,如果要做战斗回放,得换成可播种的随机数生成器,否则同一场战斗重放结果不一致。
3.3 AI 决策:评分制比行为树更轻
这份源码的 AI 没有用行为树,而是用了「评分制」——给每个可选行动打分,选最高分执行:
public class AIDecisionMaker { public SkillConfig ChooseAction(Unit self, List<Unit> allies, List<Unit> enemies) { var options = new List<(SkillConfig skill, Unit target, float score)>(); foreach (var skill in self.GetAvailableSkills()) { foreach (var target in skill.selector.SelectTargets(self, allies, enemies)) { float score = Evaluate(skill, self, target, allies, enemies); options.Add((skill, target, score)); } } return options.OrderByDescending(o => o.score).First().skill; } private float Evaluate(SkillConfig skill, Unit self, Unit target, List<Unit> allies, List<Unit> enemies) { float score = 0f; // 治疗技能:友方血量越低分越高 if (skill.HasEffect<HealEffect>()) score += (1f - target.HpRatio) * 100f; // 伤害技能:目标血量越低分越高(收割) if (skill.HasEffect<DamageEffect>()) score += (1f - target.HpRatio) * 80f; // 击杀预判:如果预计伤害超过目标血量,大幅加分 if (skill.EstimatedDamage(self, target) >= target.CurrentHp) score += 200f; return score; } }评分权重都写在配置里,调 AI 性格不用改代码。比如「激进型」AI 把伤害权重调高,「保守型」把治疗权重调高。这种方案比行为树轻量得多,适合回合制这种决策空间有限的场景。
3.4 Buff 容器与叠加规则
Buff 管理是回合制的另一个重灾区。这份源码用BuffContainer统一管理:
public class BuffContainer { private List<BuffInstance> buffs = new(); public void AddBuff(BuffConfig config, Unit source) { var existing = buffs.FirstOrDefault(b => b.Config.buffId == config.buffId); if (existing != null) { switch (config.stackRule) { case StackRule.RefreshDuration: existing.RemainingTurns = config.duration; break; case StackRule.StackLayer: existing.Layers = Mathf.Min(existing.Layers + 1, config.maxLayers); break; case StackRule.Independent: buffs.Add(new BuffInstance(config, source)); break; } } else { buffs.Add(new BuffInstance(config, source)); } } public void TickTurn() { foreach (var buff in buffs) buff.OnTurnStart(); buffs.RemoveAll(b => b.IsExpired); } }StackRule决定同名 Buff 怎么处理:刷新持续时间、叠层、还是独立共存。TickTurn在每个回合开始时调用,结算持续伤害、减益倒计时。常见坑是 Buff 在单位死亡后还在结算,所以UnitDeathEvent里要清空 Buff 容器。
4. 跑起来:环境准备与首次运行排查
4.1 环境要求与打开工程
这份工程是 Unity 项目,常见做法是用 Unity 2021 LTS 或更高版本打开。步骤:
- 安装 Unity Hub,添加对应版本的编辑器
- 在 Hub 里「Add project from disk」,选中工程根目录
- 等待首次导入完成,Unity 会编译脚本、生成 Library 缓存
- 打开
Assets/Scenes/Battle.unity - 点 Play 按钮
如果打开后报编译错误,先看 Console 面板。常见原因是缺少依赖包,检查Packages/manifest.json里是否包含com.unity.textmeshpro和com.unity.inputsystem。这两个是 UI 和输入的基础依赖,缺了会编译不过。
4.2 战斗场景的对象结构
Battle.unity里的核心对象不多:
| 对象名 | 挂载脚本 | 职责 |
|---|---|---|
| BattleManager | BattleManager.cs | 驱动状态机、管理回合 |
| UnitSpawner | UnitSpawner.cs | 从配置生成双方单位 |
| BattleUI | BattleUI.cs | 行动条、技能按钮、血条 |
| EventSystem | Unity 自带 | 处理 UI 点击 |
BattleManager上有个battleConfig字段,指向一个BattleConfig资产,里面配了双方阵容。想换敌人,改这个资产就行,不用动场景。
4.3 首次运行常见报错与处理
第一次跑最容易遇到三类问题:
空引用异常,提示UnitSpawner找不到配置。原因是Resources/Units下的角色资产没有正确加载。检查UnitSpawner的unitIds列表,确保每个 id 在Resources/Units下都有同名资产。常见做法是用Resources.Load<UnitConfig>($"Units/{id}")加载,路径大小写敏感。
UI 按钮点了没反应。检查BattleUI是否订阅了WaitingForInputEvent,以及按钮的onClick是否绑定了BattleManager.OnSkillSelected。如果用的是新输入系统,还要确认EventSystem上挂的是InputSystemUIInputModule而不是旧的StandaloneInputModule。
战斗卡在某个阶段不动。在BattleManager里加一行日志,打印当前Phase。如果卡在ExecutingAction,多半是技能动画的AnimationEvent没触发回调。检查动画片段上是否有关键帧事件调用BattleManager.OnActionAnimationEnd。
提示:Unity 的
Resources文件夹加载方式在大型项目里不推荐,但这份工程规模小,用起来最省事。二次开发时如果资产变多,建议换成 Addressables。
5. 避坑与常见问题:那些让我重跑三遍的坑
5.1 行动条累积导致出手顺序错乱
现象:两个速度相同的单位,出手顺序每次不一样。
原因:ActionGauge.Tick里用deltaTime累积,帧率波动导致累积值有微小差异,排序时不稳定。
解决:在行动值相同时,用单位 id 做次级排序。或者在RoundStart时预计算一次出手顺序,回合内不再重排。我一般会在ActionGauge里加一个Priority字段,速度相同时按优先级排。
5.2 Buff 结算顺序影响伤害结果
现象:同一个技能,有时伤害高有时低。
原因:Buff 的OnTurnStart和OnTurnEnd执行顺序不确定,导致攻击加成 Buff 在伤害计算之后才生效。
解决:给 Buff 加Priority字段,TickTurn时按优先级排序执行。增益类 Buff 优先级设高,在伤害计算前生效;持续伤害类设低,在行动后结算。这份源码里BuffConfig已经有priority字段,但默认都是 0,需要手动配。
5.3 AI 在无可用技能时死循环
现象:战斗卡住,AI 单位不行动。
原因:AI 的所有技能都在冷却,GetAvailableSkills返回空列表,ChooseAction里First()抛异常。
解决:在ChooseAction开头判断,如果没有可用技能,返回一个「跳过回合」的默认行动。常见做法是加一个SkipTurnSkill,不消耗资源、无效果,只用来占位。
5.4 伤害飘字位置偏移
现象:飘字出现在屏幕角落而不是单位头顶。
原因:WorldToScreenPoint用的是主相机,但 UI 用的是 Overlay 模式,坐标系不一致。
解决:把伤害飘字的父节点设为 UI 根节点,用RectTransformUtility.WorldToScreenPoint转换坐标。或者把 UI 改成 Screen Space - Camera 模式,和主相机绑定。
5.5 战斗回放结果不一致
现象:同一场战斗,录制的操作回放后结果不同。
原因:用了Random.value做暴击判定,回放时随机种子不同。
解决:把随机数生成器封装成可播种的BattleRandom,战斗开始时用固定种子初始化。所有随机判定都走这个实例,回放时用相同种子就能复现。这份源码目前用的是全局随机,二次开发如果要加回放功能,这是必须改的地方。
6. 二次开发:从改数值到加新机制的实操路径
6.1 最快上手:改配置不碰代码
如果你只是想调数值、加角色、加技能,完全不用动 C# 代码。流程是:
- 在
Resources/Units右键 → Create → Game → UnitConfig,填 id、血量、攻击、防御、速度 - 在
Resources/Skills右键 → Create → Game → SkillConfig,选目标选择器,拖入效果资产 - 把技能 id 填到角色的
skillIds列表 - 在
BattleConfig里把新角色加进阵容
效果资产也是 ScriptableObject,DamageEffect填倍率,BuffEffect填 Buff 配置和层数。这套数据驱动的好处是策划能自己配,程序不用陪着加班。
6.2 加新技能效果:继承 SkillEffect
要加一个「吸血」效果,新建一个类:
[CreateAssetMenu(menuName = "Game/SkillEffects/LifeSteal")] public class LifeStealEffect : SkillEffect { public float stealRatio = 0.3f; // 吸血比例 public override void Apply(Unit caster, Unit target, BattleContext ctx) { // 从上下文取本次伤害值(由 DamageEffect 写入) float damageDealt = ctx.GetLastDamage(target); float healAmount = damageDealt * stealRatio; caster.Heal(healAmount); } }关键点是BattleContext要能拿到本次技能造成的伤害。常见做法是在DamageEffect.Apply里把伤害值写进ctx.lastDamage,后续效果按顺序读取。效果列表的执行顺序就是资产里拖拽的顺序,吸血效果必须放在伤害效果后面。
6.3 加新 AI 策略:扩展评分函数
想加一个「优先攻击防御最低的敌人」策略,改Evaluate方法:
// 在 Evaluate 里加一段 if (skill.HasEffect<DamageEffect>()) { float targetDef = target.GetAttribute(AttributeType.Defense); float lowestDef = enemies.Min(e => e.GetAttribute(AttributeType.Defense)); if (Mathf.Approximately(targetDef, lowestDef)) score += 50f; // 打防御最低的加分 }权重值建议做成配置字段而不是硬编码,这样调 AI 行为不用重新编译。我一般会在AIDecisionConfig里放一个Dictionary<string, float>,把「收割权重」「治疗权重」「集火权重」都做成可调参数。
6.4 验证改动是否生效
改完代码或配置后,跑一遍战斗,重点看三处:
- Console 有没有报错或警告
- 行动条顺序是否符合预期
- 伤害数字是否和手算一致
我习惯在DamageEffect里加一行Debug.Log,打印攻击、防御、减免、最终伤害,方便对账。上线前把这些日志用#if UNITY_EDITOR包起来,避免性能开销。
6.5 一个我踩过的坑
有次我加了个「反击」Buff,想让单位受击后自动反击。结果发现反击触发的伤害又触发了对方的反击,无限递归直接栈溢出。后来在BattleContext里加了个isCounterAttack标记,反击伤害不再触发反击,才解决。
从那以后我每次加「触发型」效果,都强制走一遍「会不会自己触发自己」的检查。回合制游戏的触发链很容易绕成环,加个标记位或者深度限制是最省事的后悔药。希望帮到你。
本文还有配套的精品资源,点击获取