WECHAT

扫码添加微信

YoungFan 微信二维码
← 返回全部项目
手机游戏Unity3DXLuaLuaC#战斗系统母舰养成星域探索热更新AndroidiOS

深空7号

太空题材 2D 卡牌 RPG 手游。担任客户端主程,负责核心系统与工程链路的开发维护,客户端与服务端共享同一套 Lua 战斗核心。

年份2025
深空7号 项目封面

项目概览

《深空7号》是一款太空题材的 2D 卡牌 RPG 手游。玩家驾驶母舰在星域中推进航线,用挂机与关卡战斗积累资源,养成英雄与母舰部件,逐步解锁遗迹探险、迷宫、竞技场与公会 GVG 等玩法。项目面向国服与海外多个地区发行,覆盖中文(繁/简)、英文、印尼文等多语言与多渠道接入。

客户端基于 Unity 2022 + XLua + Addressables 构建,业务逻辑主要下沉到 Lua:Lua 代码约 2572 个文件、51 万余行,C# 侧保留框架、渲染、编辑器工具与 XLua 绑定层。服务端为 Go 微服务,按职能拆分为 center、logic、hall、fight、chat、login、recharge 等进程,进程间通过 gRPC 通信。配置表、协议与战斗核心在双端保持同源。

我的职责:客户端主程

我在项目中担任客户端主程,负责客户端技术方案与核心系统的开发维护,工作范围覆盖双端共享战斗、玩法接入、配置与工具链、热更新及版本构建。除具体模块实现外,也负责维护这些系统之间的集成与客户端整体可维护性。主要工作包括:

  • 负责双端共享的战斗系统:在 Lua 中实现并持续维护战斗核心,包括回合推进、技能与效果结算、目标选择、伤害与闪避统计、Buff 体系、能量与护盾机制;客户端与服务端加载同一份战斗代码
  • 负责战斗结果校验:客户端上报的战斗结果先做指纹校验,服务端再全量重算比对,拦截本地篡改
  • 负责配置表数据层:Excel 到双端配置的导出接入与维护,客户端数据表约 600 个文件、服务端 JSON 配置与协议相关配置
  • 维护星域相关的数据表与场景数据导出链路,以及母舰部件等玩法的数值表
  • 参与战斗表现与战斗相关业务系统,包括战斗流程驱动、结算统计、关卡与玩法数据接入
  • 维护战斗离线回归环境:战斗内核不依赖引擎,可脱离 Unity 直接运行,用于改动后的快速验证

战斗系统:一份逻辑、双端运行

我负责双端共享 Lua 战斗核心的开发与维护,涵盖回合推进、目标选择、技能与 Buff、伤害、能量及护盾结算。客户端与服务端使用同一套逻辑和配置,战斗内核不引用 Unity 类型;客户端根据逻辑产生的指令播放 Spine 动画、特效与飘字,服务端运行同一份代码进行权威重算。

模拟以离散逻辑帧驱动,技能触发、操作插入和状态更新都按逻辑帧组织,不受表现层帧率与播放速度影响。随机数采用基于 Wichmann–Hill 的三路组合线性同余生成器,由服务端下发的种子初始化,在同版本代码、配置和输入下保持可复现。数值采用万分比整数配置、统一缩放及结算取整来管理精度,借鉴定点数思路,但保留部分浮点中间计算,不将其描述为纯定点实现。

战斗回放保存初始数据、随机种子和带帧号的操作序列,通过重新推演还原过程。结果校验结合轻量指纹检查与关键战斗的服务端全量重算,发现不一致时输出差异日志。内核还可脱离 Unity 直接运行,用同一组战斗输入做离线回归,验证技能或数值改动并定位双端结果差异。

母舰系统

母舰是玩家的核心养成载体,也是整个游戏的“家”。母舰由十余种部件构成——舰桥、能量舱、引擎、护盾发生器、物质加工厂、造船厂、科研室、探测器、炮台、船坞等,每种部件承担一类功能:舰桥决定整体等级上限与开启条件,能量舱提供母舰能量(航行、功能开启、超载都要消耗),引擎影响星域航行,护盾发生器应对星际灾害,物质加工厂长期产出资源,造船厂按蓝图建造舰船,科研室负责技术研究,探测器负责派遣舰船探测星球与悬赏任务。部件本身有建造、升级、子模块与损坏修复四个状态维度,母舰还会遭遇星际风暴灾害,需要按防御任务要求派遣舰船或部件应战,防御结果受概率配置影响。

工程上,母舰系统采用“模块 + 命令 + 操作队列”三层结构。管理器中注册了十二个业务模块(母舰本体、操作队列、部件建造、部件升级、子模块、灾害控制、能量收集、探测器、物质加工厂、造船厂、科研室、部件修复),每个模块独立负责一块业务与自己的数据缓存、红点与事件;外部不直接调用模块方法,而是把操作投递到命令队列里串行执行,从而把调用方与执行方解耦,并让建造、升级、装配这类异步操作按序落地。所有需要排队的操作(部件建造、部件升级、舰船建造)统一由操作队列模块管理:按队列簇分组、每簇有若干队列、每队列同时只跑一个操作,客户端会为每个操作生成运行时唯一标识并与服务端下发的操作 ID 对齐,同时维护操作时长、结束时间、暂停后剩余时长与服务端时间校准,保证切后台、断线重连后进度仍然一致。

表现层是场景化的:母舰本身是一个可交互场景,部件在场景中以对象形式存在(携带父部件与子部件关系、状态机、动画状态与特效状态),支持在场景里直接点击部件打开对应功能窗口。部件建造与装配有专门的表现控制器:光标在场景中移动,带最大/最小速度与加减速曲线,部件按装配进度逐段拼合;能量舱有独立的能量粒子收集表现,场景相机有远近切换与 FOV 缩放动画,UI 与场景各有一个状态机来协调“装配中 / 部件窗口打开 / 环绕浏览”等视图状态。

星域地图

星域地图是母舰航行的舞台,也是最主要的探索玩法。一个星系由若干星球与节点构成,节点类型包括矿点、事件点、星门、跳跃点、虫洞、宝箱与战斗残骸、世界 BOSS、可占领节点等。玩家以母舰停泊在某节点为起点,通过扫描逐步揭示周围节点——扫描会对比前后两次的节点集合,增量地抛出“新增节点”“节点更新”“宝箱残骸出现”等事件,已探索区域、区域解锁奖励与产出按星系分别记录。航行分三档:节点间飞行按距离与引擎等级耗时,快速传送用于已探索区域的短距跳跃,跨星系则走星门跃迁,需要提前准备。星球可以占领,占领方可在星球上部署舰船阵容、配置防护罩等级并留下占领线,形成轻量的 PVP 争夺;被击毁的舰队会在地图上留下残骸宝箱,成为其他玩家的收益来源。星图还承担派遣与产出:派遣舰队与舰船执行长线任务、矿点采集、星系产出结算,与母舰的能量、引擎、探测器等级相互咬合。

星图的数据侧按功能拆成若干管理器:地图核心负责节点数据、可见性、停泊状态、航线与跃迁、事件派发;占领、扫描、战斗、玩家进度各自独立成模块;节点对象化为一类一文件的表现对象(矿点、事件、星门、跳跃点、虫洞、世界 BOSS、玩家舰船),并带 LOD 处理。节点信息不整包下发,而是按需请求节点详情、配合定时刷新与事件驱动的增量更新,以控制长线运营中的消息量。

3D 星域场景

星图之外还有一套真实的 3D 星域场景,同一个空间里存在三个视图层级:星系总图(星系间的连线与入口)、星系区域(单个星系内的实体摆放)、母舰层(回到母舰内部视角)。层级之间用状态机切换,切换时保存并恢复相机位置与视图状态。Lua 侧的世界管理只提供访问场景对象的接口,不掺业务逻辑,业务分派到各层级与星域管理器;星域管理器负责实体实例化、LOD 显隐、实体信息面板(扫描、介绍、交互)、舰船沿贝塞尔曲线航行的导航系统,以及实体资源的异步加载与释放。C# 侧负责区域级流送:以流送源(位置 + 半径)为单位,带引用计数、加载超时与分帧推进,保证在星域里移动时不会一次性拉起整个星系。

星域编辑器与出表链路

星域的场景数据不是策划在 Excel 里填的,而是美术与策划在 Unity 里“摆”出来的,再由工具导出为配置。配套的编辑器工具包括:在场景视图中直接编辑星域的视图工具、实体增删窗口、实体与星系预制体生成窗口、星系区域文件与星系总图文件(含星系入口)的编辑工具,以及两个配置导出器。导出前会做校验,例如实体在星系内必须有唯一实例 ID、包围盒尺寸与触发半径必须合法;导出内容包含每个实体的配置 ID、局部坐标、包围盒半尺寸、触发半径与舰船站位,以及星系整体的包围盒范围,最终生成单个星系上万行的场景配置(最大的星系配置约 1.7 万行)。这套链路让星域可以按星系独立迭代,也避免了“场景里摆完还要人手抄一遍表”。

场景切换:抓屏 Pass 与过渡

母舰是一个可交互的 2D 场景(玩家在船体外壳上点部件进功能),星域是一套实时 3D 世界(星系总图、星系区域、母舰层三个视图),两者在玩法里频繁来回切。这两套场景的相机取景、光照与资源完全不同,直接切会有明显的画面跳变和加载空窗,靠 UI 挡也不自然。所以过渡做成“抓一帧旧画面 → 当遮罩盖在新场景上 → 淡出”,同时母舰场景里的面板要实时取“被模糊后的场景”当背景。

过渡抓屏由三部分配合。参数放在一个自定义的 URP Volume 组件上(是否抓屏、是否作为遮罩、遮罩透明度),场景与相机上就能调;一个自定义 RenderPass 只对星域主相机生效,挂在后处理之后。抓屏是“一次性”的:截到当前画面后立刻把标记复位,不会每帧重复抓。过渡时把抓到的旧画面作为遮罩贴在刚切过来的新画面上,再把遮罩透明度从 1 缓动到 0,视觉上就是旧场景逐渐化开、新场景在下面浮现。

真正驱动这一切的是 Lua 侧的命令队列。切换视图层时按顺序投递:把相机对准母舰(或指定的星系入口)→ 抓屏 → 打开遮罩 → 在 0.3 秒内把遮罩淡到 0。抓屏命令本身还会额外等两帧再放行后续命令——否则截到的很可能已经是新场景的第一帧,过渡就露馅了。母舰层、星系总图层、星系区域层互相切换都走同一套编排,透明度的缓动也支持传时长逐帧插值,不需要缓动时直接设值并在降到 0 时关闭遮罩。

毛玻璃走的是另一个自定义 Pass,挂在母舰主场景所用的 Renderer 上(星域用的是另一套 Renderer,上面挂的是星云背景、程序化星空、相机过渡与多相机合并)。它在不透明物体之后、透明物体之前抓一次当前画面,先降到半分辨率,再做两组横向与纵向模糊——第二组的采样偏移翻倍,相当于把模糊半径再拉开一档;模糊核一次采样 7 个点,中心权重 0.4,两侧按 0.15 / 0.15 / 0.1 / 0.1 / 0.05 / 0.05 递减。结果以一张全局贴图输出,场景里的毛玻璃面板(ShaderGraph)直接引用这张贴图,于是面板后面看到的就是实时模糊后的场景,一次抓屏可以供多个面板共用。

工程上还有两处细节:两套 Renderer 的职责分开,星域那套不承担抓屏成本,母舰场景那套也不加载星空的程序化渲染;抓屏用的临时 RT 都在 Pass 的清理阶段释放,避免长期占用显存。这套代码在 Unity 升级时也做了适配,取相机颜色目标的位置按引擎版本分支(新版用 cameraColorTargetHandle,旧版用 cameraColorTarget)。

不发版修复:Lua 全链路热更新

启动流程本身就是一段 Lua 脚本(以文本资源内嵌在包内):SDK 初始化、版本检查、服务器地址下发、资源更新判定、Addressables 管线初始化、拉起正式 Lua 框架,全部由这段脚本串联。在此基础上做了三件事。

一是启动器的热补丁。版本检查接口除返回资源版本外,还能下发一段 Lua 补丁代码,客户端直接载入执行,用来修复启动链路自身的问题。像 SDK 回调时序、渠道地址下发、更新判定异常这类一旦出错就必须重新打包提审的问题,可以先靠下发补丁顶住。补丁执行包在保护调用中,失败只记录日志、不阻断启动;围绕启动前、启动器、启动后三个阶段各留了一个补丁挂点。

二是资源版本的三方裁决。客户端同时维护三个版本号:包内自带的首包版本、上次下载后落盘的缓存版本、CDN 上的最新版本,启动时取三者中的最大值作为本次资源版本并据此决定 catalog 地址。首包内置资源有独立清单,命中清单的包体走本地 StreamingAssets,其余走 CDN;远端 catalog 会被重写为带版本号的文件名,实现多版本共存与灰度切换。检测到玩家重装时会自动清空旧缓存。

三是打包脚本定制。Addressables 的打包流程被扩展出多个构建器:按规则重命名 Bundle、拷贝产物到 StreamingAssets、输出包体清单与日志、生成资源到 Bundle 的映射表,并自定义了同步取用的 Provider,让启动期少量资源在 Lua 侧可以立即拿到。

其他工程模块

框架与 UI。C# 侧框架负责模块发现与生命周期(反射式单例注册)、Lua 虚拟机与脚本加载、引擎事件到 Lua 的回调桥接;Lua 侧提供 UI 管理器(六级分层 + 栈式界面管理)、事件与响应式工具、对象池、定时器与场景管理等基础设施。界面配置由工具生成,控件回调自动绑定到 Lua。

网络层。协议使用 protobuf,Lua 侧统一注册协议号并派发到业务对象;实现了心跳检测、断线重连与重连后的消息补发(客户端未确认消息缓存 + 服务端补推),并对单条协议的处理耗时做监控与告警。C# 侧为减少 GC,采用由 C# 主动向 Lua 拉取发送缓冲的方式组织收发包。

配置表管线。策划在 Excel 中维护上百张配置表,通过统一工具一次生成服务端 Go 配置与常量文件、客户端 Lua 数据表与 JSON 中间产物,并附带配置自检脚本与多语言文本导出(文本按语言分表,界面中以语言 ID 引用);星域场景数据由编辑器直接导出,与 Excel 表共用同一条产线。

渠道与发行。SDK 层定义统一接口,各渠道以独立模块接入,覆盖广告、归因、社交登录、支付、推送、统计分析以及国内聚合渠道等二十余个模块;编辑器内可切换渠道与参数出包。服务器地址按平台、渠道、版本三级下发,支持多地区多环境并行。

表现与渲染。客户端以 Spine 和 2D 表现为主,3D 场景使用自研 URP 渲染扩展:程序化星空与星云背景(Volume 驱动的自定义 RenderPass,星域与母舰场景共用)、相机过渡、抓屏模糊与多相机合并渲染。