下载 App

如何用TapTap 制造实现一套不丢档的云存档系统

精华修改于04/25379 浏览开发交流
TapTap 制造做的游戏以 WASM(WebAssembly)方式运行在 TapTap 平台上,本地存储依赖浏览器IndexedDB,清缓存或换设备就会丢失。所以云存档不是可选项,而是必须。
horizontal linehorizontal line
做 WASM 小游戏最怕的事:玩家换个设备,存档没了。
WASM 的本地存储(IndexedDB)既不可靠也不跨设备。所以我们必须把存档放到云端——但只用一条云端通道又不够稳,网络抖动就可能导致存档丢失。
这篇文章分享双通道云存档的完整实现:clientCloud(TapTap 平台)和 serverCloud(游戏服务器)独立运行、互为备份,加上本地缓存作为第三层保险。
核心指标:
正常流程:数据变动后 0 帧延迟写本地,2 秒内上传 serverCloud,15 秒内同步 clientCloud
异常恢复:任一通道故障,另一通道 + 本地缓存兜底,0 丢档
horizontal linehorizontal line

一、整体架构

┌─────────────┐
│   玩家操作    │
└──────┬──────┘
       │ MarkDirty()
       ▼
┌─────────────┐    同步写入     ┌──────────────┐
│  PlayerData  │──────────────→│  本地 File     │  ← 第一层: 即时持久化
│  (内存单例)   │               │  + 校验和      │
└──────┬──────┘               └──────────────┘
       │ 异步上传
       ▼
┌─────────────────────────────────────┐
│           CloudSync 调度器           │
├─────────────┬───────────────────────┤
│ serverCloud │     clientCloud       │
│  节流 2s    │      节流 15s         │  ← 第二层: 双云备份
│  + 排行榜    │      + 排行榜         │
└─────────────┴───────────────────────┘
为什么两条通道?
| 通道 | 优点 | 缺点 |
|------|------|------|
| clientCloud (TapTap) | 平台托管,零运维 | 依赖 TapTap SDK,部分环境不可用 |
| serverCloud (游戏服务器) | 完全可控,延迟低 | 服务器宕机时不可用 |
| 本地 File | 零延迟,离线可用 | WASM 环境不持久,换设备丢失 |
三者任意一个存活,存档就不会丢。
horizontal linehorizontal line

二、模块划分(6 个文件)

```
network/
  SaveEvents.lua        ← 常量: 云端 Key、事件名、排行榜 Key
  server_save.lua        ← 服务端: serverCloud 读写 + 旧格式迁移
systems/
  save.lua               ← 本地持久化: File + 校验和
  cloudsync.lua          ← 客户端调度器: 双通道上传/下载
  savesystem.lua         ← 门面: 协调加载/保存/失焦自动保存
  migration.lua           ← 版本迁移: v1→v2→v3 增量升级
  player_data/
    _internal.lua         ← 内存单例 + MarkDirty
    persistence.lua       ← 加载/保存/云同步协调
```
依赖关系(单向,无循环):
SaveSystem → PlayerData/Persistence → CloudSync → ClientNet (延迟require)
                                    → Save (本地)
                                    → Migration
CloudSync 对 ClientNet 使用 pcall(require, ...) 延迟加载,避免启动时循环依赖。
horizontal linehorizontal line

三、核心实现

3.1 常量统一管理(SaveEvents.lua)

客户端和服务端共用同一份 Key 定义,杜绝拼写不一致:
```lua
SaveEvents.CLOUDKEYS = {
    CHARACTER      = "characterv1",      -- 角色存档
    AUDIOSETTINGS = "audiosettingsv1", -- 音频设置(独立Key)
    LEGACY         = "gamesave",         -- 旧版单Key(仅迁移读取)
}
SaveEvents.LBKEYS = {
    LEVEL = "playerlevel",  WAVE  = "highestwave",
    TOWER = "towerfloor",   ARENA = "arenapoints",
    NAME  = "playername",   RESONANCE = "resonance_time",
}
```
设计决策:音频设置独立 Key,因为它修改频率低、体积小,不需要跟着角色存档一起上传。

3.2 本地持久化 + 校验和防损坏(save.lua)

```lua
-- 写入流程: JSON编码 → 计算校验 → 备份旧文件 → 写入新文件
function Save.Write(data)
    data.saveTime = os.time()
    local json = cjson.encode(data)
-- 备份旧存档
if fileSystem:FileExists(SAVE_FILE) then
    fileSystem:Copy(SAVE_FILE, SAVE_BACKUP)
end
-- 附加 FNV-1a 校验和
local checksum = ComputeChecksum(json)
local payload = json .. "\n--CHECKSUM:" .. checksum
local file = File(SAVE_FILE, FILE_WRITE)
file:WriteString(payload)
file:Close()
end
```
校验和算法:FNV-1a 变体 + XOR 混淆。不是加密级安全,但能检测文件意外损坏(写入中断、磁盘错误),不涉及服务端验证:
lua
local function ComputeChecksum(str)
    local hash = 2166136261  -- FNV offset basis
    for i = 1, #str do
        hash = hash ~ string.byte(str, i)
        hash = (hash * 16777619) & 0xFFFFFFFF
    end
    return string.format("%08x", (hash ~ 0x5A3C) & 0xFFFFFFFF)
end
读取时双重容错:主文件损坏 → 读备份文件;校验不过 → 打印警告但仍尝试使用(比完全丢档好)。

3.3 双通道上传调度(cloud_sync.lua 核心)

```lua
function CloudSync.Upload(data, callback)
    -- 前置检查: 角色未创建(playerName空)时不上传
    if not hasValidCharacter(data) then return end
local isForce = (callback ~= nil)
local now = os.time()
-- clientCloud 路径: 15s 节流
if isCloudAvailable() then
    if isForce or (now - lastCloudSaveTime_) >= 15 then
        clientCloud:BatchSet()
            :Set(CK.CHARACTER, charData)
            :Set(CK.AUDIO_SETTINGS, audioData)
            :SetInt(LB.LEVEL, level)   -- 顺带更新排行榜
            :SetInt(LB.WAVE, wave)
            -- ...更多排行榜字段
            :Save("保存进度", { ok=..., error=..., timeout=... })
    end
end
-- serverCloud 路径: 独立于 clientCloud, 2s 节流
CloudSync.UploadToServer(data, isForce)
end
```
关键设计:
1. 两条路径完全独立:clientCloud 超时不影响 serverCloud,反之亦然
2. 节流策略不同:serverCloud 2 秒(主通道,WASM 本地不持久),clientCloud 15 秒(平台限制)
3. 排行榜顺带写入:存档上传时 BatchSet 一并写入排行榜数据,省一次网络请求
4. 角色创建前不上传:防止空数据覆盖有效存档

3.4 serverCloud 上传与节流重试

```lua
function CloudSync.UploadToServer(data, force)
    if not hasValidCharacter(data) then return end
    local now = os.time()
-- 节流: 2s 内重复上传 → 暂存到 pendingUploadData_
if not force and (now - lastServerUploadTime_) < 2 then
    pendingUploadData_ = data
    return
end
-- 检查连接状态(延迟require避免循环依赖)
local ok, ClientNet = pcall(require, "network.client_main")
if not ok or not ClientNet.IsConnected() then
    pendingUploadData_ = data  -- 断线暂存, 等重连
    return
end
ClientNet.UploadSaveToServer(data)
lastServerUploadTime_ = now
pendingUploadData_ = nil
end
-- 每帧调用: 节流窗口过后自动重试
function CloudSync.FlushPending()
    if not pendingUploadData_ then return end
    if (os.time() - lastServerUploadTime) >= 2 then
        CloudSync.UploadToServer(pendingUploadData, true)
    end
end
```
被节流的数据不会丢失:暂存到 pendingUploadData_,由 FlushPending()(每帧调用)在窗口过后自动重试。

3.5 服务端接收与写入(server_save.lua)

服务端也有独立的节流,防止恶意客户端高频刷写:
```lua
function ServerSave.HandleUpload(eventData)
    local userId = info.userId
-- 服务端节流: 同一玩家 10s 内不重复写入
if (os.time() - (lastSaveTimes_[userId] or 0)) < 10 then
    -- 返回 Throttled ACK, 客户端不需重试
    connection:SendRemoteEvent(SAVE_ACK, true, { Success=true, Throttled=true })
    return
end
-- 解析 JSON → 写入 serverCloud (角色 + 音频 + 排行榜)
ServerSave.WriteToCloud(userId, saveData, audioData, function(ok, msg)
    lastSaveTimes_[userId] = os.time()
    -- 返回 ACK
    connection:SendRemoteEvent(SAVE_ACK, true, { Success=ok })
end)
end
```

3.6 启动时加载:三路竞速

游戏启动时的数据加载流程:
Start()
  │
  ├─ 1. 同步读本地 File          ──→ localSaveTime
  │
  ├─ 2. 异步拉 clientCloud       ──→ cloudSaveTime      ← 设置 LoadPending 锁
  │
  └─ 3. 异步等 serverCloud 推送  ──→ serverSaveTime     ← 设置 ServerLoadPending 锁
  │
  ▼ 三路数据到齐(或超时)
  比较 saveTime → 取最新的那份
核心比较逻辑(persistence.lua):
```lua
function Persistence.Load(onComplete)
    -- 第一阶段: 同步加载本地
    local localSaved = Save.Read()
    local isNew = restoreFromSaveData(localSaved)
    local localSaveTime = (localSaved and localSaved.saveTime) or 0
-- 第二阶段: 异步拉云端
CloudSync.SetLoadPending(true)
CloudSync.Download(function(cloudResult, isError)
    CloudSync.SetLoadPending(false)
    if cloudResult then
        local useCloud = (cloudResult.saveTime > localSaveTime)
                      or (isNew and cloudHasData)
        if useCloud then
            -- 云端更新 → 覆盖本地
            restoreFromSaveData(cloudResult.character)
            Save.Write(CollectSaveData())  -- 回写本地
        else
            -- 本地更新 → 上传云端
            CloudSync.Upload(CollectSaveData())
        end
    end
    onComplete(isNew)
end)
end
```
超时保护:加载锁有 8-10 秒自动释放,防止云端永远不响应时卡死:
lua
function CloudSync.IsLoadPending()
    if cloudLoadPending_ and (os.time() - cloudLoadStartTime_) > 10 then
        cloudLoadPending_ = false  -- 超时自动释放
    end
    -- serverLoadPending_ 同理, 8 秒超时
    return cloudLoadPending_ or serverLoadPending_
end

3.7 服务端存档下发与冲突解决

玩家连接时,服务端主动推送存档:
lua
-- server_save.lua
function ServerSave.LoadAndSend(userId, connection)
    serverCloud:BatchGet(userId)
        :Key(CK.CHARACTER)
        :Key(CK.AUDIO_SETTINGS)
        :Key(CK.LEGACY)          -- 兼容旧格式
        :Fetch({
            ok = function(scores)
                -- 优先 character_v1, 回退 game_save
                local saveData = scores[CK.CHARACTER] or scores[CK.LEGACY]
                connection:SendRemoteEvent(SAVE_LOAD, true, {
                    SaveJson = cjson.encode(saveData),
                    SaveTime = saveData.saveTime,
                    Migrated = (scores[CK.CHARACTER] == nil),  -- 标记是否旧格式
                })
                -- 旧格式自动迁移到新 Key
                if not scores[CK.CHARACTER] and scores[CK.LEGACY] then
                    ServerSave._migrateToNewKeys(userId, saveData, audioData)
                end
            end,
        })
end
客户端收到后,在 HandleServerSave
中做冲突解决:
```lua
-- 使用服务端的条件:
--   1. 服务端 playerName 非空 (有效角色)
--   2. 服务端时间戳更新 OR 本地为空(WASM丢失)
local useServer = serverHasValidChar
    and ((serverSaveTime > localSaveTime) or localIsNew)
if useServer then
    PlayerData.RestoreFromServerSave(serverSaveData)
elseif localSaveTime > serverSaveTime then
    -- 本地更新 → 反向上传到服务端
    CloudSync.UploadToServer(collectSaveData(), true)
end
```
WASM 场景特殊处理:本地 IndexedDB 被清空时 localIsNew=true,只要服务端有有效数据就直接恢复,不比较时间戳。
horizontal linehorizontal line

四、版本迁移系统

存档结构会随版本迭代变化。我们用类似数据库 migration 的方案:
```lua
-- migration.lua
local CURRENT_VERSION = 3
local migrations = {}
-- v2: 通用碎片 → 按品质分类碎片
migrations[2] = function(data)
    if data.universalFragments and data.universalFragments > 0 then
        data.qualityFragments = data.qualityFragments or { R=0, SR=0, SSR=0, UR=0 }
        local half = math.floor(data.universalFragments / 2)
        data.qualityFragments.R = data.qualityFragments.R + half
        data.qualityFragments.SR = data.qualityFragments.SR + (data.universalFragments - half)
        data.universalFragments = nil
    end
end
-- v3: 音频设置从 player.settings 独立到单独 Key
migrations[3] = function(data)
    if data.settings then
        AudioMgr.MigrateFromPlayerData(data)  -- 迁移到独立文件
        data.settings.musicEnabled = nil       -- 移除冗余字段
        data.settings.sfxEnabled = nil
    end
end
-- 顺序执行: fromVersion+1 到 CURRENTVERSION
function Migration.Run(data, fromVersion)
    for v = (fromVersion or 1) + 1, CURRENTVERSION do
        if migrations[v] then migrationsv end
    end
    return CURRENT_VERSION
end
```
加载时自动迁移:restoreFromSaveData
在恢复数据后立即调用 Migration.Run
:
lua
local function restoreFromSaveData(saved)
    I.data = DeepMerge(NewData(), saved.player) -- 合并默认值(新增字段自动填充)
    local toVer = Migration.Run(I.data, saved.migrationVersion or 1)
end
DeepMerge 的作用:新版本新增的字段在 NewData()
里有默认值,DeepMerge
把旧存档覆盖上去后,新字段自然保留默认值——不需要为每个新字段写迁移函数。
horizontal linehorizontal line

五、自动保存与失焦保护

5.1 脏标记 + 即时存档

```lua
-- _internal.lua
function M.MarkDirty()
    if not M.dirty then M.dirtyElapsed = 0 end
    M.dirty = true
end
-- persistence.lua (每帧 tick)
function Persistence.AutoSaveTick(dt)
    CloudSync.FlushPending()              -- 先处理被节流的上传
    if CloudSync.IsLoadPending() then return end  -- 加载中不存档
-- 周期性强制云端同步 (30s)
if (os.time() - lastForceCloudTime) >= 30 then
    Persistence.Save(true)
    return
end
-- 脏数据: AUTO_SAVE_DELAY=0, 下一帧立即存
if I.dirty then
    I.dirtyElapsed = I.dirtyElapsed + dt
    if I.dirtyElapsed >= 0 then  -- 实时保存
        Persistence.Save()
    end
end
end
```
AUTO_SAVE_DELAY = 0 意味着数据变动后下一帧就写本地。这在 WASM 环境是必要的——用户随时可能关闭标签页。

5.2 页面失焦强制保存

```lua
-- savesystem.lua
SubscribeToEvent("InputFocus", "HandleInputFocusSaveSystem")
function HandleInputFocus_SaveSystem(eventType, eventData)
    if not eventData["Focus"]:GetBool() then
        -- 失焦(切标签页/最小化) → 强制保存
        SaveSystem.Save(true)  -- force=true, 绕过节流
    end
end
```
horizontal linehorizontal line

六、踩过的坑

#1 WASM 的 os.clock() 不可靠

os.clock() 在 WASM 返回 CPU 时间而非墙钟时间,导致自动存档计时器永远不触发。解决:用Update 的 dt 累加替代 os.clock(),时间戳比较用 os.time()。

#2 空角色覆盖有效存档

玩家打开游戏但还没创角就触发了自动上传,空的 playerName="" 覆盖了服务端的有效存档。解决:上传前检查 hasValidCharacter(),playerName 为空直接跳过。

#3 clientCloud 延迟初始化

TapTap SDK 的 clientCloud 对象不是启动时立即可用,可能在几帧后才初始化完成。如果永久缓存"不可用"的结论,后续所有上传都会跳过。解决:只缓存 true,false 不缓存,每次重新检测。

#4 双锁加载防止提前进入游戏

联网模式下有两个异步加载源(clientCloud + serverCloud),如果只等一个就释放锁,另一个还没到时玩家可能已经进入创角页面。解决:两个独立加载锁,两个都释放后才允许正常存档。

#5 循环依赖

CloudSync 需要调用 ClientNet.UploadSaveToServer(),但 ClientNet 的初始化又依赖 CloudSync。解决:pcall(require, "network.client_main") 延迟加载,调用时才 require。

#6 节流丢数据

serverCloud 2 秒节流期间如果有新的存档请求,直接丢弃会导致最后几秒的改动丢失。解决:节流时暂存到 pendingUploadData_,FlushPending() 每帧检查,窗口过后自动重试。

#7 服务端也要节流

没有服务端节流时,一个频繁操作的客户端每 2 秒就往 serverCloud 写一次,50 个在线玩家 = 每秒 25 次写入。解决:服务端每用户 10 秒节流,返回 Throttled=true ACK,客户端不需重试。
horizontal linehorizontal line

七、完整数据流时序

正常启动

客户端                          服务端
  │                               │
  ├─ Save.Read() 同步读本地        │
  ├─ clientCloud.BatchGet() ─────→│ (TapTap 平台)
  │                               │
  │    ←── SAVE_LOAD 推送 ────────┤ serverCloud.BatchGet(userId)
  │                               │
  ├─ 比较三个 saveTime            │
  ├─ 取最新的覆盖本地              │
  └─ 释放加载锁, 进入游戏          │

正常游戏中

玩家操作 → MarkDirty()
  │
  ├─ 下一帧: Save.Write() 写本地 (即时)
  ├─ 2s 后: UploadToServer() → 服务端写 serverCloud
  └─ 15s 后: clientCloud.BatchSet() → 写 clientCloud + 排行榜

异常恢复

| 场景 | 恢复路径 |
|------|---------|
| WASM 本地存储被清 | serverCloud 推送 → 恢复 |
| TapTap SDK 不可用 | 本地 + serverCloud 双保险 |
| 游戏服务器宕机 | 本地 + clientCloud 双保险 |
| 存档 JSON 损坏 | 备份文件 _backup.json 恢复 |
| 校验和不匹配 | 警告日志 + 仍尝试使用(比丢档好) |
horizontal linehorizontal line

八、可扩展方向

1. 存档加密:当前只有校验和,可升级为 AES 加密存储
2. 多槽位存档:当前单角色,扩展 Key 命名为 character_v1_slot2
3. 存档差异同步:只上传变化的字段,减少带宽(当前是全量覆盖)
4. 存档回滚:serverCloud 保留最近 N 个版本,支持客服回档
5. 离线队列:断网时攒一批操作,重连后批量上传(当前只暂存最后一份)
我也不敢说完全不丢档,我有款游戏使用的正是这套存档系统目前为止还没丢过档。仅供参考,大佬们有任何补充或批量建议请评论区留言。
猜你想搜
taptap 制造本地持久化
别再烧积分了!从每次 2000 到每次 50 的血泪经验截图
别再烧积分了!从每次 2000 到每次 50 的血泪经验
适用场景:所有在用 TapTap Code 开发游戏的小白 难度:★☆☆☆☆ 背景最近群里总有人"炫耀"一次操作花了几千积分,说实话这不是值得高兴的事——这是在告诉所有人你的开发方式有根本性问题。我也走过这条弯路,所以来说说怎么改。 积分消耗的本质是什么积分 = AI 处理的 token 数量 = 你发给 AI 的内容 + AI 回给你的内容所以烧积分的本质只有两种: 你让 AI 做了大量无效/重
精华
82 赞
23 回复
如何提升你游戏中的UI美术水平截图
如何提升你游戏中的UI美术水平
一、请为你的游戏挑选一款合适的字体 很多开发者没有注意到其实UI界面中最重要的元素是字体,一款合适的字体能给整个游戏的界面感官带来蜕变。 请看案例 那么如何挑选适合自己的字体呢? 我总结出了一些小经验可供参考: 黑体:通用 宋体:古风、武侠、修仙 楷体:古风、武侠、修仙 圆体:卡通、动漫、Q版 科技:科幻、未来、电子 顺手分享一个免费可商用字体网站: 当然 如果你不知道自己的游戏适合什么风格
精华
126 赞
44 回复
2D 等轴测视角:完整理论截图
2D 等轴测视角:完整理论
2D 等轴测视角:完整理论 一、视角的本质 什么是等轴测 人站在高处斜着往下看地面,地面上的方格子不再是正方形,而变成了菱形。这就是等轴测的视觉本质。 俯视(正上方看下去): 等轴测(斜上方看下去): ┌──┬──┬──┐ ◇ ├──┼──┼──┤ ◇ ◇ ├──┼──┼──┤ ◇ ◇ ◇ └──┴──┴──┘ ◇ ◇ ◇ 正方形网格经过旋转 + 压缩,变成了菱形网格。 为什么是 2:1
6 赞
浅记《阿方的旅程》开发历程截图
浅记《阿方的旅程》开发历程
预计阅读时长10分钟,欢迎您先品鉴游戏,再回来看这篇“流水账” 这是我第一次实际接触“用自然语言制作游戏”,也是我第一次接触游戏开发工作,所以一切都是从零开始,跌跌撞撞,懵懵懂懂。[表情_豹哭] 我是通过获得的本次开发资格,6月22日晚,在微信公众号收到了测试资格的消息,最开始公布主题“跳跃”的时候,脑子里最先想到的就是几款非常经典的游戏「马里奥」「Flappy Bird」「Doodle Jump
10 赞
1 回复
开发日志(day2)坚持去做就是好的开始截图
开发日志(day2)坚持去做就是好的开始
今天主要是修复了一下昨天遇到的很多问题,闪帧;装备的等级没有随怪物等级参考;再次进入地下城,怪物状态竟然显示(已倒下);不会自动更换目标,导致会反复鞭尸;等很多的问题,不过通过昨天的经验,有效的向ai提出问题并且发送截图发给ai调试,也是把这些问题成功排除,甚至还有多余的free额度来生成新的内容。 新增的内容有返回城镇;返回主界面;中途退出地下城保留进入前的状态;完成地下城的面板结算;当然最有感
1 赞
游戏自荐《少女终焉契约》截图
游戏自荐《少女终焉契约》
之前玩放置类型的rpg游戏的时候我就在想,现在游戏的操作有的太过于繁琐了,有没有一款休闲一点的,想上线就上线,离线的时候角色能自动行动的游戏呢?感谢TapTap制造,我按照我的想法开发出了现在这款游戏。 一、故事背景和UI 游戏的故事背景是传统的异世界穿越故事,玩家被女神召唤到异世界,随后组建工会,招募美少女,打怪升级探索世界。所以我选择了更加二次元萌系画风的UI风格,主页为各个主要的功能,点开来
8 赞
3 回复
【Tap make 塔拉拉之立项干货】萌新看过来!截图
【Tap make 塔拉拉之立项干货】萌新看过来!
题外话:本人也是tap make 使用者之一。塔啦啦忠实信徒、天哥忠实拥护者、八年游戏策划经验的低级策划。但是!我已经做出来一款5000+热度、且稳定可更新的框架、并且还在持续产出更多游戏内容的橘猫! 这几天正好在开新坑。我是3月初加入的tapmake大家庭。两周时间于3月25日将这个作品上架。当时的全凭感觉摸索。如今,自己也是一个有自己项目的小菜鸡了。今天,就来分享一下,如何高效的制作一款高大上
投票精华
69 赞
24 回复
永恒虚空持续开发日记1——关于游戏中的新名词:场频截图
永恒虚空持续开发日记1——关于游戏中的新名词:场频
写在前边:首先感谢TapTap制造版主大大@汪崽 对于作品潜力的肯定,给与了我积分计划二的权限[表情_不好意思]。这边也在加紧开发改进各种功能中,期望尽快给大家带来成熟的版本。[表情_猫咪举手] 大家好,我是永恒虚空的制作人黑猫不详,大家叫我黑猫就好。从今天开始我将以后置开发日记的形式给大家分享我在TapTap制造中制作的游戏永恒虚空的开发过程,新人希望大家多多关照! 今天先给大家介绍我作品中最重
投票
3 赞
00:54
等距场景编辑器——自行编辑2D瓦片场景地图截图
等距场景编辑器——自行编辑2D瓦片场景地图
一个可嵌入任何 UrhoX 项目的完整等距瓦片地图编辑器,适用于 RPG、策略、模拟经营等类型游戏的关卡设计。 SKILL审核已通过,各位创作者可以根据自己的需要对工具进行调整修改 声明:该工具仅作为场景编辑以及序列化,交互等功能需要用户自行在项目中定义 后续会根据实际情况补充rule tile相关逻辑,实现tile map编辑自由 目录 1. 界面总览 2. 六种编辑工具 3. 双视图模式 4.
精华
21 赞
11 回复
01:29
官方阅,申请积分计划2,希望官方继续支持我完成这款游戏,截图
官方阅,申请积分计划2,希望官方继续支持我完成这款游戏,
视频内容只是一部分, 本游戏耗时3个月,目前完成进度百分之80, 是生存游戏,进入修仙界,走剧情找到宗门加入, 这时游戏才是真正开始, 提升实力境界后就可以变强,获取功法法术法宝, @视频内是游戏里挑战妖族入侵一种主流玩法,游戏就是围绕着这个来进行, 还有很多地方可以战斗,获得资源修炼变强,比如去藏书阁学习功法, 去法宝楼抽法宝,每一个宗门数十个法宝抽取 去修仙界探索,获得各自的资源,可能遇见妖兽
1 赞
3 回复