如何存储剧情类游戏数据并防止用户访问篡改(替代.txt文件)
嘿,作为刚入门的开发者,你能想到数据持久化+防篡改的需求真的超棒——这可是剧情类游戏保住可玩性的关键!别慌,我给你整理几个新手友好、落地简单的方案,一步步来搞定:
一、先搞定基础数据持久化(新手首选:嵌入式数据库)
别用明文TXT、JSON存数据,太容易被改了。首选嵌入式数据库,比如SQLite(几乎所有平台都支持,Unity、Android、iOS甚至Python都有现成的库),它把数据存在单个文件里,不是明文文本,而且操作起来也不难。
举个Unity里的简单例子(用SQLite-net库):
- 先安装SQLite-net(可以通过NuGet或者Unity Package Manager)
- 定义你的数据模型(用无意义字段名混淆):
public class PlayerStoryData { [PrimaryKey, AutoIncrement] public int ID { get; set; } public int SP { get; set; } // 对应剧情进度(代替直白的StoryProgress) public string UB { get; set; } // 对应已解锁分支ID列表(代替UnlockedBranches) public string CK { get; set; } // 校验码(后面防篡改用) }
- 存数据的代码:
// 初始化数据库连接 var db = new SQLiteConnection(Path.Combine(Application.persistentDataPath, "story_data.db")); // 创建表(如果不存在) db.CreateTable<PlayerStoryData>(); // 生成校验码 string checkCode = CalculateCheckCode(playerProgress.ToString(), unlockedBranches); // 插入或更新数据 var data = db.Table<PlayerStoryData>().FirstOrDefault(); if (data == null) { db.Insert(new PlayerStoryData { SP = playerProgress, UB = string.Join(",", unlockedBranches), CK = checkCode }); } else { data.SP = playerProgress; data.UB = string.Join(",", unlockedBranches); data.CK = checkCode; db.Update(data); }
- 读数据的代码:
var db = new SQLiteConnection(Path.Combine(Application.persistentDataPath, "story_data.db")); var data = db.Table<PlayerStoryData>().FirstOrDefault(); if (data != null) { // 先校验数据合法性 string calculatedCheck = CalculateCheckCode(data.SP.ToString(), data.UB); if (calculatedCheck == data.CK) { playerProgress = data.SP; unlockedBranches = data.UB.Split(',').Select(int.Parse).ToList(); } else { // 数据被篡改,重置或提示用户 ResetStoryData(); } }
二、简单防篡改手段(新手能快速上手)
上面提到的校验码是核心,再给你补充几个易落地的技巧:
1. 数据校验码(核心防护)
不用搞复杂加密,用哈希算法(比如MD5、SHA1,对付普通用户足够)。把关键数据拼接后生成哈希值,和数据一起存储。读取时重新计算哈希,对比不一致就说明数据被篡改。
比如刚才的CalculateCheckCode方法:
private string CalculateCheckCode(string progress, string branches) { // 加个自定义“盐”,让哈希更难破解 string rawString = progress + branches + "MyGameSecretSalt_123"; using (MD5 md5 = MD5.Create()) { byte[] inputBytes = System.Text.Encoding.ASCII.GetBytes(rawString); byte[] hashBytes = md5.ComputeHash(inputBytes); // 转成十六进制字符串 StringBuilder sb = new StringBuilder(); foreach (byte b in hashBytes) { sb.Append(b.ToString("X2")); } return sb.ToString(); } }
⚠️ 注意:这个“盐”别直接写死在代码里,可以拆成字符串拼接(比如"My" + "Game" + "Salt"),或者存在资源文件里,避免被反编译轻易拿到。
2. 混淆字段/表名
就像例子里用SP代替StoryProgress,别用直白的名称。就算用户找到数据库文件,也看不懂每个字段对应什么,增加篡改难度。
3. 简单文件加密(不用数据库的备选)
如果不想用数据库,比如用二进制文件存数据,可以把序列化后的对象用对称加密(比如AES)处理后再存。比如Python里的例子:
import pickle from Crypto.Cipher import AES from Crypto.Util.Padding import pad, unpad # 密钥要16/24/32位,这里用16位 key = b"MySecretKey_123456" cipher = AES.new(key, AES.MODE_CBC) # 序列化数据 data = {"sp": 5, "ub": [1,3]} serialized_data = pickle.dumps(data) # 加密并存储(要把初始化向量IV一起存) encrypted_data = cipher.encrypt(pad(serialized_data, AES.block_size)) with open("story_save.dat", "wb") as f: f.write(cipher.iv) f.write(encrypted_data) # 解密读取 with open("story_save.dat", "rb") as f: iv = f.read(16) encrypted_data = f.read() cipher = AES.new(key, AES.MODE_CBC, iv) decrypted_data = unpad(cipher.decrypt(encrypted_data), AES.block_size) saved_data = pickle.loads(decrypted_data)
三、针对剧情分支的额外防护
剧情类游戏最怕用户直接解锁全部分支,除了通用方法,还可以做这些:
- 只存关键选择,不存分支状态:比如不要存“分支A已解锁”,而是存“第2章选了帮助NPC”。游戏启动时,根据这些选择重新计算解锁分支。就算用户改了选择,也只能对应特定分支,没法直接刷全部分支。
- 隐藏分支触发逻辑:比如某个分支需要完成3个隐藏任务,别把“完成3个任务”存在本地,而是在游戏逻辑里实时计算,本地只存单个任务的完成状态,用户改单个任务也没用。
四、新手避坑提醒
- 绝对别用明文文本文件存数据!比如TXT、JSON,用户随便打开就能改,直接pass。
- 别把加密密钥硬编码在代码里!一定要做混淆处理,避免被反编译拿到。
- 测试时手动篡改数据文件,验证你的校验逻辑是否有效。
内容的提问来源于stack exchange,提问作者MrAszter




