You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何存储剧情类游戏数据并防止用户访问篡改(替代.txt文件)

嘿,作为刚入门的开发者,你能想到数据持久化+防篡改的需求真的超棒——这可是剧情类游戏保住可玩性的关键!别慌,我给你整理几个新手友好、落地简单的方案,一步步来搞定:

一、先搞定基础数据持久化(新手首选:嵌入式数据库)

别用明文TXT、JSON存数据,太容易被改了。首选嵌入式数据库,比如SQLite(几乎所有平台都支持,Unity、Android、iOS甚至Python都有现成的库),它把数据存在单个文件里,不是明文文本,而且操作起来也不难。

举个Unity里的简单例子(用SQLite-net库):

  1. 先安装SQLite-net(可以通过NuGet或者Unity Package Manager)
  2. 定义你的数据模型(用无意义字段名混淆):
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; } // 校验码(后面防篡改用)
}
  1. 存数据的代码:
// 初始化数据库连接
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);
}
  1. 读数据的代码:
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

火山引擎 最新活动