简介:一份基于C#的文字修仙游戏完整源码,面向正在学习C#、Unity游戏开发或准备毕业设计的开发者。资源包含75个文件,压缩包约2MB,涵盖C#脚本、配置文件、资源文件(.cs/.json/.resources)及可执行程序(.exe/.dll)等,其中核心脚本覆盖游戏主循环、场景切换、角色成长、回合制战斗和数据持久化等模块,便于对照源码理解整个游戏运行流程。目前已有650人学习下载。作者对源码进行了系统解析,并给出C#与Unity集成、事件驱动、文本剧情分支等关键知识点,学习者可借此理清文字修仙游戏的架构设计,掌握MonoBehaviour脚本编写、XML/JSON存档读写及UI界面管理方法,并在此基础上进行功能扩展与毕业设计二次开发。
1. 文字修仙游戏,从 C# 源码包开始的价值点
在资源站上看到“基于C#编写的文字修仙游戏源码.zip”这样的包,解压前很难判断里面是完整能跑的控制台工程,还是只写了一半的挂机 demo。这个题材作为 C# 练手素材很合适:不需要图形引擎,不用处理序列帧,所有玩法都能落到数据结构和状态切换上。对写过业务系统的熟手来说它也不幼稚——境界、突破、奇遇、战斗本质是数值计算、随机事件和状态机,正好用来验证泛型、LINQ、JSON 序列化这些日常能力。和 C++ 写同样玩法要先和内存管理较劲相比,C# 写这类纯逻辑游戏要省事很多。下文按最常见的工程形态展开,从主循环、数值模型、文本交互一路做到存档和源码二次扩展。
2. 先把骨架立住:游戏循环与境界状态机怎么设计
2.1 文字游戏为什么也要考虑“心跳”和输入循环
很多首次写文字修仙的人会把全部代码塞进一个while(true)加Console.ReadLine()里。这样做跑起来没问题,但一旦加入“闭关十秒自动获得修为”“受伤后每回合回复灵气”这类时间驱动逻辑,就需要在每条命令之间手动计算时间差,命令一多就乱了。
我一般会把游戏拆成两个层次:输入层读取命令、解析参数、分发给处理函数;心跳层在每次主循环迭代里推进世界时间。所谓心跳不一定是一秒一帧,更常见的做法是记录上次心跳的时刻,在每轮命令处理之后统一算一次时间差。这样“闭关两个月”和“等三个呼吸再触发毒发”都是同一套时间推进逻辑。
这里要注意一个 C# 写游戏的老问题:用Thread.Sleep卡主线程做延时,会在等待期间丢掉用户输入。所以主循环里不要用Sleep(1000)当心跳。下面给出的骨架用的是“空闲即检查”的方式,更接近真实游戏的事件循环。
2.2 用枚举和状态转移矩阵描述境界突破
境界系统最直观的做法是定义枚举:
public enum Realm { 炼气, 筑基, 金丹, 元婴, 化神 }但让业务代码到处比较枚举,逻辑会散落。更好的做法是把“从哪个境界突破到哪个境界、需要什么资源”抽成转移表:
public sealed record BreakthroughRule( Realm From, Realm To, int LevelRequired, int SpiritStoneCost);用数组或字典集中声明规则,判定突破时只查表,不散落 if。查表写法如下:
public bool TryBreakthrough(Player p) { var current = p.Realm; var rule = ruleMap.GetValueOrDefault(current); if (rule == null || p.Level < rule.LevelRequired) return false; if (!p.Wallet.TrySpend(rule.SpiritStoneCost)) return false; p.Realm = rule.To; return true; }境界规则集中在一个表里,便于调整数值,也方便后续加“天劫失败程度”这种分支状态。查表比多层 if 的优势,在修仙这种规则越来越多的题材里会越来越明显。
2.3 一个最小可运行的主循环骨架
主循环本身很短,但它决定后续所有子系统怎么挂进来:
while (true) { PrintStatus(player); // 渲染当前状态面板 Console.Write("> "); var raw = Console.ReadLine(); if (string.IsNullOrWhiteSpace(raw)) continue; var parts = raw.Trim().Split(' ', 2, StringSplitOptions.RemoveEmptyEntries); var cmd = parts[0].ToLowerInvariant(); var arg = parts.Length > 1 ? parts[1] : ""; if (commands.TryGetValue(cmd, out var handler)) { handler(player, arg); } else { Console.WriteLine($"未知指令:{cmd},输入 help 查看帮助"); } }commands 是一个Dictionary<string, Action<Player, string>>,把打坐、历练、突破、背包都注册成独立函数。每个玩法模块互不干扰,新增指令只增加一个注册项,还能直接生成 help 菜单。
| 指令 | 参数 | 作用 |
|---|---|---|
| 打坐 / dazuo | 分钟数 | 按修炼速度获得修为 |
| 突破 / break | 无 | 检查条件并尝试突破 |
| 背包 / bag | 无 | 查看丹药、灵石 |
| 存档 / save | 文件名 | 序列化玩家数据到 save 目录 |
这个骨架只到“能输入能反馈”的程度,但接入点已经留好:状态面板、命令分发、玩家数据传递。下一步把中间那层数据模型和数值公式补上。
3. 修炼和战斗不是一堆 if else:C# 数据模型与数值公式
3.1 用 record 和字典把属性表从逻辑里拆出来
常见错误是在 Player 类里写几十个自动属性:金灵根、木灵根、水灵根、火灵根、土灵根,每个再分十级,写出来既啰嗦又难遍历。更合理的做法是给 Player 挂一个字典:
public sealed class Player { public Realm Realm { get; set; } public int Level { get; set; } public Dictionary<string, int> Stats { get; } = new(); public int GetStat(string key) => Stats.TryGetValue(key, out var v) ? v : 0; public void AddStat(string key, int delta) { Stats.TryGetValue(key, out var old); Stats[key] = Math.Clamp(old + delta, 0, 100_000); } }Stats 的 key 用“体质”“悟性”“火灵根”这样的中文名,代码读起来和策划表一致。数值封顶直接用Math.Clamp,防止某个乘法 buff 把属性推成天文数字。用字典替代大量属性,另一个好处是战斗里“临时减防御一回合”这类状态也能落进 Stats,不用单独加字段。
3.2 修炼速度公式与突破判定怎么写
修炼速度不能只用一次乘法,否则高境界以后数值失衡。常见公式是:
每分钟修为增加 = 基础值 × 境界系数 × (悟性系数 + 灵根加成) × 状态倍率境界系数从 1.0 到 5.0 逐步放大,悟性系数接近 1 + 悟性 / 1000,灵根加成由五行相性决定。把公式写成一个静态方法,便于测试:
public static int GetCultivationGain(Player p, int minutes) { double realmK = RealmK.Value[p.Realm]; // 查表,境界越高系数越大 double talentK = 1.0 + p.GetStat("悟性") / 1000.0; double rootBonus = GetRootBonus(p); // 天灵根、杂灵根加权 double stateMult = p.GetStat("状态倍率") > 0 ? 1.5 : 1.0; double perMin = 10 * realmK * talentK * rootBonus * stateMult; return (int)Math.Floor(perMin * minutes); }接口测试时注意倍增尺度:如果每分钟产出数十万修为,玩家无法感知“从筑基到金丹的距离”;数值太小,打坐十几分钟没变化又显得挫败。常见手感是把初境到下一境界的挂机时间控制在 5 到 10 分钟,后期再拉开曲线。
| 境界 | 境界系数 | 升级所需修为 | 预计挂机时间 |
|---|---|---|---|
| 炼气 | 1.0 | 1000 | 5 分钟 |
| 筑基 | 2.0 | 8000 | 20 分钟 |
| 金丹 | 3.5 | 50000 | 80 分钟 |
| 元婴 | 5.0 | 300000 | 5 小时 |
参数表单独维护后,可以直接在表格工具里微调,再生成 C# 常量,避免在代码里反复改动魔法数字。
提示:数值常量尽量只增不改。旧存档里的玩家可能已经持有关键资源,一旦调整下一档的突破门槛,老玩家可能瞬间突破或反而被卡死,调试存档时容易排除不出原因。
3.3 回合制战斗与随机奇遇的平衡参数
文字游戏的战斗不需要坐标,只需要两个对象来回扣血。我习惯把玩家、NPC、妖兽统一成接口:
public interface IFighter { int MaxHp { get; } int Hp { get; set; } int Attack { get; } int Defense { get; } } public static class Combat { public static int CalcDamage(IFighter a, IFighter b, double critChance = 0.1) { int raw = Math.Max(1, a.Attack - b.Defense / 2); return Random.Shared.NextDouble() < critChance ? raw * 2 : raw; } }C# 的随机数有两个容易被忽略的点:第一,不要在每次攻击时 new 一个 Random,应该用Random.Shared;第二,随机事件触发要放进战斗回合之间统一判定,避免“暴击概率”和“奇遇概率”互相干扰。奇遇事件尽量用权重表:
public static T Pick<T>(IEnumerable<(T item, int weight)> options) { int total = options.Sum(x => x.weight); int roll = Random.Shared.Next(total); int acc = 0; foreach (var (item, weight) in options) { acc += weight; if (roll < acc) return item; } return default; }随机奇遇的权重建议把前 80% 结果设为“中性结果或小惩罚”。文字游戏乐趣来自挂机后数字涨跌带来的反馈,而不是抽卡式稀有奖励。参数保持“被惩罚后仍能靠修炼追回”的口径,避免玩家被一次随机秒杀清空进度。
4. 打字的体验也值得做:C# 指令解析、延迟输出与存档
4.1 用命令字典把指令解析做干净
前面主循环用Split(' ', 2)切出指令和参数,已经够用。很多人卡在“c#语言怎样截取字符串”这个问题上,是因为把数组切片和参数解析混在一起:先用 Substring 抠出命令,再用 Split 抠参数,一个动作切成两步,边界很难处理。Console.ReadLine 已经是字符串,用 Split 加StringSplitOptions.RemoveEmptyEntries直接处理即可。
文字修仙的中文指令通常会带别名,我习惯加一层映射表:
var aliases = new Dictionary<string, string> { ["dazuo"] = "打坐", ["打坐"] = "打坐", ["练功"] = "打坐" };统一入口ResolveCommand(raw)做小写归一、别名映射、参数解析三步。这样 help 菜单列出统一名,玩家用拼音、中文都能唤醒同一动作。字符串处理是文字游戏的输入面,做干净后,后面所有玩法接入都省心。
4.2 打字机特效的延时问题与 Stopwatch 方案
文字游戏常被要求有“逐字显示”的质感。控制台里最粗暴的写法是:
foreach (char ch in text) { Console.Write(ch); Thread.Sleep(30); }这段代码在内嵌等待命令的主循环里会导致动画期间丢输入,重文本场景还会拖慢整体节奏。“c# 延时 效率”这类问题下,我的建议是用 Stopwatch 控制输出节奏,不阻塞调用线程:
var sw = Stopwatch.StartNew(); int index = 0; const int charPerSecond = 35; while (index < text.Length) { int charsToShow = (int)(sw.Elapsed.TotalSeconds * charPerSecond); if (charsToShow > index) { Console.Write(text[index..charsToShow]); index = charsToShow; } // 玩家按任意键直接跳过动画 if (Console.KeyAvailable) { index = text.Length; Console.ReadKey(true); } } Console.WriteLine();Stopwatch 方案只在主循环的空隙里做增量渲染,不阻塞读取下一条命令。对挂机为主的文字修仙来说,这比Thread.Sleep更符合“长时间不操作也不卡”的预期。
4.3 JSON 存档让 zip 里的源码真正可玩
文字游戏的痛点常常是“修炼一夜,重启不想重头再来”。用 System.Text.Json 序列化玩家状态是最常见的方案:
public static void Save(Player p, string filePath) { var options = new JsonSerializerOptions { WriteIndented = true }; File.WriteAllText(filePath, JsonSerializer.Serialize(p, options)); } public static Player Load(string filePath) { if (!File.Exists(filePath)) return new Player(); var json = File.ReadAllText(filePath); return JsonSerializer.Deserialize<Player>(json) ?? new Player(); }这里容易被忽略的是序列化兼容性:Stats 字典在将来新增“灵根”字段时,旧存档不需要改;已存在存档缺少对应 key 时,GetStat用默认 0 兜底,不会崩。但如果给 Player 新增基础属性、改变枚举顺序、或者把枚举改名,老存档就会被默认值干扰。
| 变更类型 | 注意事项 |
|---|---|
| 字典值增删 | 旧存档无碍,写 GetStat 默认逻辑 |
| 枚举字段增删 | 老存档可能读到第 0 项,Load 后做合法性校验 |
| 枚举顺序调整 | 尽量不重排枚举顺序,否则旧存档映射错位 |
存档路径建议放 exe 同目录下的 save 文件夹,便于调试时直接删档重来,也符合小体量 C# 工程的自然形态。
5. 拿到源码包之后:工程拆解和扩展一个新境界
5.1 先看入口再找数据,十分钟读通一个 C# 游戏工程
拿到“基于C#编写的文字修仙游戏源码.zip”,先解压,再按顺序读:
- 找
.sln或.csproj,确认框架版本。.NET 6 以上可以直接用命令行跑,Framework 4.x 则要用 Visual Studio 2019 以上打开; - 打开包含
Main的入口文件,看主循环注册了哪些指令; - 顺着命令表找 Player、Enemy 这类数据实体,把境界枚举和数值表读一遍;
- 最后看战斗与突破的实现。
命令行或 PowerShell 下先构建,验证原包能跑:
unzip 基于C#编写的文字修仙游戏源码.zip cd 对应工程目录 dotnet build dotnet run如果原包只有一个 csproj 没有 sln,说明工程结构很简单,不必纠结组织方式,重点仍然是入口方法。
5.2 一个不破坏旧存档的扩展方法:新增“火灵根”属性
假设想在原工程里新增火灵根数值,并让它影响修炼速度。给 Player 增加字典 key 是安全的,因为旧存档缺少该 key 时会走 0 默认值。但要注意,如果把这个逻辑直接写进突破方法里,代码会迅速膨胀。
我会单独建一个 RootSystem 静态类,把“灵根属性到修炼倍率”的计算收敛在一个方法入口,再让修炼公式调用它:
public static class RootSystem { public static double GetFireRootBonus(Player p) { int fireRoot = p.GetStat("火灵根"); return 1.0 + fireRoot / 500.0; } }这样只影响修炼产出的倍率,不牵连背包和战斗。扩展新系统时,优先做加法而不是改旧逻辑,这是文字修仙这类小工程保持健康的关键。
5.3 用 60 次自动模拟验证数值平衡
最后给一个我常用的验证手段:写一段没有界面的模拟,让不同资质玩家反复修炼、突破,输出满级时间,不需要图形界面:
for (int trial = 0; trial < 60; trial++) { var p = new Player(); p.AddStat("悟性", 50); int loop = 0; while (p.Realm != Realm.化神 && loop++ < 3000) { p.AddStat("修为", GetCultivationGain(p, 1)); if (p.GetStat("修为") > BreakthroughRule.Target(p.Realm)) TryBreakthrough(p); } Console.WriteLine($"trial={trial}, realm={p.Realm}, loop={loop}"); }模拟的意义不是替代策划,而是快速暴露数值死角:某境界曲线过于陡峭时,loop 数会爆炸式增长。把参数表调平,再回到游戏里手动玩十分钟做体感校准。对文字修仙这类以数值体验为核心的游戏,先跑模拟再给玩家玩,比逐条试命令靠谱得多。改完参数重新dotnet build并跑一轮模拟,确认旧存档正常加载、新曲线符合预期,再把工程重新打成 zip 对外发布。
本文还有配套的精品资源,点击获取