ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Unity与C#的苏绣文化虚拟展馆交互漫游系统开发实践

基于Unity与C#的苏绣文化虚拟展馆交互漫游系统开发实践 1. 项目概述与整体设计思路1.1 为什么选“虚拟展馆”来呈现苏绣文化这几年我一直在做文化遗产数字化方向的项目接触过不少博物馆、非遗工坊的数字化需求。做苏绣这个题材最开始其实是甲方抛过来的一个命题——能不能把针法、纹样、历史脉络这些静态资料变成用户可以自己“走进去”看的东西我想了很久最终敲定了虚拟展馆这个形态而不是做一个简单的Web 3D展厅或者视频导览原因有几点。第一苏绣本身有很强的“过程性”和“空间性”。一幅成品挂在那里普通人很难看出劈丝、运针、配色这些门道但如果把展厅做成可以漫游的空间引导用户从“远观作品”走到“近看针法”再走到“动手体验”区域学习路径就自然形成了。第二交互漫游系统能给用户提供自主性——想先看双面绣就看双面绣想直接进历史长廊也可以这种自由度是视频和图文没法给的。第三从技术角度看Unity 3D对中小型虚拟展馆的支持非常成熟C#的脚本体系也足够应对交互逻辑不需要像虚幻那样上很高的硬件门槛。这个项目适合谁来参考如果你是做文化遗产数字化、虚拟展厅、文博教育类应用的开发者或者想用Unity C#做一套带完整交互的漫游系统这篇文章应该能帮到你。我尽量把关键的思路、踩过的坑和可直接抄作业的代码结构都讲清楚。1.2 需求拆解一个“能逛、能点、能学”的系统长什么样在动手写第一行代码之前我把需求拆成了几个层次。最底层是“空间层”用户要能在展厅里自由行走视角要舒服不能穿墙、不能掉出地图。第二层是“展示层”展品要有足够的视觉细节苏绣的丝线光泽、绣布纹理、装裱质感都要能体现出来至少不能让人一眼看出是粗糙的贴图。第三层是“交互层”点击展品要弹出介绍面板可以看文字、看细节放大图甚至播放一小段针法演示动画。第四层是“导览层”系统要提供路线引导用户不会迷路菜单要能随时切换展厅区域。这四层对应到Unity里就是场景搭建、材质与灯光系统、射线检测与UI交互、导航与菜单管理。后文会一层一层展开。另外我特意把“展品信息管理”和“场景对象”做了解耦。展品的名称、年代、工艺特点、图片、音频讲解词全部放在一个独立的数据文件里场景里只留一个空物体挂脚本。这样后期想替换展品、增减内容不需要动场景改数据文件就行。这一点对苏绣这类展品信息经常需要专家校对的项目尤其重要——我后面会再细说这个结构的优势。2. 技术选型与开发环境搭建2.1 Unity版本与渲染管线URP是性价比最高的选择展馆类项目对画面要求说高不高说低也不低。如果用内置渲染管线Built-in Render Pipeline做出来的效果有点“复古”灯光表现力不够。如果用高清晰度渲染管线HDRP画质确实好但对硬件要求偏高而且如果目标用户里有配置一般的电脑或者Mac跑起来会吃力。折中下来我选了URPUniversal Render Pipeline也就是通用渲染管线。URP在Unity 2021以后已经非常稳定它支持基于物理的渲染PBR可以让苏绣的丝线材质产生真实的光泽变化同时性能开销控制得非常好。我项目里用的是Unity 2021.3 LTS这个版本长期支持稳定性有保障。如果你用的是Intel版Mac安装时注意选Intel对应的编辑器版本别下成Apple Silicon版。另外URP项目创建之后记得在Graphics设置里确认管线资产已经指派否则材质会显示成紫色这个坑我见过不少人踩过。小提示如果你已经用内置管线搭了一半场景也可以后期升级到URPUnity提供自动转换工具但贴图和材质可能需要手动调一遍所以我建议新项目直接选URP别走弯路。2.2 C#版本与开发环境LTS版本自带的就好用C#在Unity里的使用不需要额外安装SDK编辑器自带的Mono和IL2CPP编译环境已经够用。我的项目脚本用的C#语法以8.0为主没有用太新的特性因为要考虑可维护性和后期同事接手时的上手成本。开发环境我用的Rider也有人用Visual Studio看个人习惯。这里有个实质差异Unity 2021.3 LTS对Visual Studio 2022的集成已经很顺畅断点调试、自动补全都没有问题而Rider的优势在于对Unity特定API的补全和重构更智能。选哪个都可以关键是C#项目的程序集定义Assembly Definition要规划好。我在这个项目里把脚本分成了三个程序集Core数据模型与工具类、Interaction交互逻辑、UI界面控制。这样分开的好处是编译速度快、依赖关系清晰。比如UI程序集引用Core和Interaction但Core不依赖任何Unity组件相关的类纯C#逻辑后期写单元测试也方便。2.3 关键依赖与插件少而精别被插件绑架虚拟展馆类项目能用的插件很多但我尽量少引第三方依赖。最终只用了一个比较重要的插件——一个轻量级的JSON解析库用来读取展品配置数据。实际上Unity自带的JsonUtility也能用只不过它对List支持不友好需要包一层包装类。如果你能接受这个写法JsonUtility完全可以胜任少一个插件少一份兼容性风险。其余交互功能全部用Unity自带组件完成射线检测用Physics.RaycastUI用Unity UIuGUI场景加载用异步场景加载接口。这套方案最大的优势就是“稳”——不需要依赖第三方SDK的更新节奏打包时也不容易出现插件冲突。3. 虚拟展馆场景搭建与美术表现3.1 展厅布局规划动线设计是第一优先级做虚拟展馆最忌讳的是把展品随便往场景里一摆用户进来像逛迷宫。我参考了线下博物馆的动线设计原则把整个展馆分成了五个区域入口大厅、历史长廊、工艺展示厅、名家作品厅、互动体验区。入口大厅承担引导职能放一个总览导览图用户可以在这里选择直接去哪个区域也可以选择“智能导览模式”跟着指引路线走。历史长廊用时间轴结构从宋代苏绣起源到近现代发展墙面上的图文展板配合灯光引导。工艺展示厅重点展示绣针、绷架、丝线等工具和材料以及劈丝、运针等核心技法的演示动画。名家作品厅则是整个展馆的重点双面绣、乱针绣等代表作会以高精度模型配合特殊灯光呈现。互动体验区做了一个简化版的“数字绣绷”小游戏用户可以用鼠标模拟穿针引线体验基本针法。展厅面积在Unity单位下大概是50米乘40米这个体量对性能压力不大也给用户留足了游览空间不会产生压迫感。每个区域之间用走廊连接走廊两侧挂装饰画和文字引言让切换区域的过程也保持信息密度。我当时画动线草图的时候原则很简单每个区域只有一条主动线不要出现“回字形”路线展品排布按照“重要程度”递增把最核心的作品放在区域最深处这样可以保障用户在到达核心展品前已经积累了情绪和背景知识。3.2 苏绣材质与灯光PBR材质参数的手动调试经验苏绣作品的材质表现是整个项目视觉上最关键的环节。丝线有独特的光泽——它不是镜面反射也不是纯漫反射而是沿绣线方向的高光反射。在PBR材质模型里我把绣品材质的Smoothness设置在0.6到0.75之间Metallic设置为0丝线不是金属然后用细节法线贴图模拟“经纬纹理”。这里必须说一个细节苏绣的丝线是有方向性的不同针法的丝线走向完全不同。如果整个作品只用一个法线贴图远看没问题近看就会觉得“假”。我的解决方案是一个绣品模型拆成多个子网格每个子网格根据实际针法方向单独给法线贴图然后通过Shader的UV坐标旋转来控制丝线方向。灯光方面我用了三盏主光一盏主方向光角度大约45度用来模拟展厅顶部照明一盏暖色调的点光源放在每件重点展品的斜上方强度控制在1.2到1.5之间色温偏暖凸显绣品的色彩饱和度还有一盏冷色调的补光从反方向打过来避免阴影区域死黑。URP的Lighting设置里我开启了软阴影Soft Shadows阴影质量选了High。这个设置对画面观感提升非常明显而且URP的性能开销在可控范围内。切记不要把整个场景都用点光源堆满展馆大场景下动态光源数量控制在8个以内其余区域依赖烘焙光照贴图。3.3 展品数据管理用JSON配置文件驱动场景内容前面提到我把展品信息和场景对象解耦这里具体展开讲讲怎么做的。我在Core程序集里定义了一个ExhibitData类包含以下字段展品ID、名称、作者、年代、材质工艺、详细介绍文本、展品图片路径、音频讲解路径、是否支持放大查看、是否支持针法演示。所有展品数据存放在一个exhibits.json文件里放在StreamingAssets目录下这样发布后还能替换文件不需要重新打包。场景里我为每个展品位置放一个空物体挂上ExhibitItem组件这个组件只负责持有展品ID和对应的交互范围Collider。运行时ExhibitManager读取JSON文件把数据填充到字典里然后根据ID找到场景里对应的ExhibitItem把数据绑定上去。这样做的好处是什么如果苏绣专家后期要修改某幅作品的介绍文字或者新增一幅作品只需要改JSON文件加一张图片完全不碰Unity场景和C#代码。对于文化遗产类项目来说内容校审是常态这种数据驱动的架构能省下大量时间。我还做了一层缓存机制图片和音频首次加载后缓存在内存里避免用户来回走动时重复加载造成卡顿。加载过程使用协程或异步方法不会阻塞主线程。这个方案在我最终测试中表现良好即使在低配电脑上切换展品详情也不需要等待超过0.5秒。4. 交互漫游系统的核心设计与操作实现4.1 第一人称漫游控制手感比功能重要虚拟展馆的第一人称控制核心诉求是“舒服”而不是“真实”。我不建议直接用Unity的CharacterController搭配物理引擎做严格模拟因为展厅空间有限碰撞体的边缘处理不当会把用户卡住。我的实现方案是使用CharacterController组件但做了一些定制化调整。移动速度设为每秒3.5米这个速度比游戏里的步行稍快比跑步慢适合逛展的节奏。鼠标灵敏度横向为2.0纵向为1.5并且把纵向视角限制在-30度到60度之间——限制俯仰角是为了防止用户把头低到地板或者仰到天花板影响游览体验。有一点非常重要展馆场景里一定要给用户一个“回到主路”的机制。我在地面用透明的导航线实际上是Decal Projector投出来的箭头贴图做引导按下Tab键可以切换是否显示。同时在每个区域入口放置一个小的传送点Icon用户点击后可以选择传送到其他区域入口。这个传送功能用C#写的话就是一个简单的坐标赋值但实际体验提升巨大。移动实现方面千万不要在Update里直接修改Transform的position应该用CharacterController.Move方法传入速度乘以deltaTime。这样可以规避碰撞检测出错的问题。另外防抖动的小技巧在LateUpdate里处理相机位置让相机始终跟随控制器但让相机的旋转在自己控制这样角色走到墙边时头部不会穿模。4.2 展品点击交互射线检测与细节放大展品点击是系统最核心的交互方式。我的实现思路是重写一个专用的交互管理器在Update里检测屏幕中心的射线或者鼠标点击位置是否碰撞到了IInteractable接口的对象。C#接口设计如下定义IInteractable接口包含void OnInteract()方法和string GetInteractionHint()方法。场景中所有可以被交互的对象——展品、传送点、讲解员NPC、互动装置——都实现这个接口但每个对象的交互逻辑可以完全不同。展品的OnInteract逻辑如下首先在屏幕中心生成一个准星图标默认隐藏当射线落到可交互物体上时显示用户点击后交互管理器会调用OnInteract展品对象把自身的ExhibitData传给UI管理器UI管理器弹出详情面板。详情面板分三块左侧是大图展示区支持鼠标滚轮缩放和拖拽查看细节中间是文字信息包括作者、年代、工艺说明底部是音频讲解的播放控制条。这里有个用户容易忽略的点展品大图不能用UI的Image直接缩放到很大否则图片会模糊。正确的做法是准备比显示尺寸至少大两倍的源图然后在UI里用RawImage配合RectTransform控制显示区域这样放大后依然清晰。缩放查看功能我用了一个很轻量的实现方案在详情面板里放一个ScrollRect里面承载一张尺寸远超可视区域的RawImage用户通过拖拽和滚轮改变RawImage的localScale。这个过程本质上是C#对RectTransform的操作代码量不大但很实用。4.3 多区域切换与导览系统从A点到B点的优雅实现多区域导览听起来简单但如果直接做场景切换会面临加载黑屏、状态丢失、audio中断等问题。我的方案是“单场景多区域”也就是整个展馆只有一个Unity场景区域之间的切换本质上是坐标传送。每个区域是一个独立的空物体子节点包含各自的灯光、Collider和展品。区域切换时系统做三件事暂停玩家控制、渐隐屏幕用一个全屏Canvas遮罩做透明度动画、将玩家控制器传送到目标区域入口、渐显屏幕、恢复控制。这套流程用协程实现非常清晰整个过程约1.2秒不会让用户有晕眩感。智能导览模式则基于一个简单的路径点系统。每个区域设定2到3个关键看点形成一个有序列表。用户开启智能导览后系统会在每个看点之间生成一条由多个路径点组成的导航路径地面上的箭头标识引导用户走向下一个看点到达后自动弹出该展品的详情面板停留约30秒后继续引导到下一个。这套功能的底层其实是一个状态机Idle等待、Navigating走向目标、Viewing正在观赏、Finished全部完成。状态机用C#枚举和switch语句实现逻辑非常清晰方便后续扩展新的导览路线比用行为树插件更轻量。4.4 UI界面与交互反馈信息层级要克制展馆项目的UI设计原则是“克制”。文化遗产类项目不需要花哨的界面特效但信息层级必须清楚。我最终设计的UI分为三层顶层是常驻菜单包括返回大厅、区域切换、导览开关、音量控制中层是展品详情面板只在一个展品被点击时出现底层是场景内交互提示比如准星旁的“点击查看”文字提示。字体选择我特意规避了Unity默认的Arial改用思源宋体和思源黑体的组合。展品名称、展板引言用宋体系统菜单和操作提示用黑体。这个细节对文化感的营造效果很明显用户虽然不一定能说出差异但整体气质完全不同。UI动画我大量使用了Unity的DoTween插件但考虑到插件依赖的问题后来把透明度和位移动画改写成了Unity自带的Animator实现用AnimationClip控制CanvasGroup的alpha和RectTransform的anchoredPosition。功能上完全等价但少了一个插件依赖后期升级Unity版本时少了一分风险。5. 关键C#脚本实现与逻辑解析5.1 数据层JSON序列化与反序列化的细节问题数据层是整个系统的地基。展品数据需要从JSON反序列化为C#对象这里有几个细节需要特别注意。第一Unity的JsonUtility不支持Dictionary的序列化因此展品数据列表直接用数组包裹。我的数据结构是三层的ExhibitConfig包含一个ExhibitData[] exhibits数组数组里每个元素包含所有展品字段。反序列化的代码非常简洁string json File.ReadAllText(Application.streamingAssetsPath /exhibits.json); ExhibitConfig config JsonUtility.FromJsonExhibitConfig(json);第二多语言支持要考虑字段命名。如果以后要做英文版或日文版JSON字段名最好从一开始就保持英文显示文本放在单独的翻译文件里。我这个项目暂时只有中文但字段名仍然全部是英文目的就是预留国际化空间。第三图片路径的引用不要硬编码绝对路径统一用相对于StreamingAssets的相对路径然后在运行时拼接。由于不同平台下路径格式不同Windows是盘符开头Mac是根目录开头移动端在沙盒内拼接时用Path.Combine而不是字符串加号可以避免很多隐性问题。我实际写代码时还加了一个数据校验逻辑加载完JSON后立即校验每个展品是否有非空的名称和介绍文本以及图片文件是否存在校验失败时在Console窗口输出警告而不是运行时才发现图片加载不出来。这对后期内容维护非常有用。5.2 交互层基于接口的多态设计模式交互层我用了一个比较经典的面向对象设计——接口多态。核心代码在Unity的C#环境里实现起来非常顺手而且可扩展性极强。假设你要新增一个“扫码查看AR效果”的展品只需要新建一个类实现IInteractable接口挂到对应物体上交互管理器不需要改动任何代码新功能就能自动被识别。这其实就是C#面向对象设计里“开闭原则”的应用——对扩展开放对修改关闭。我再举一个具体的实现片段方便你理解整个交互链路的数据流public interface IInteractable { void OnInteract(); string GetInteractionHint(); }某个展品对象的实现可能是public class ExhibitItem : MonoBehaviour, IInteractable { public string exhibitId; private ExhibitData data; public void OnInteract() { UIManager.Instance.ShowExhibitDetail(data); AudioManager.Instance.PlayNarration(data.narrationClip); } public string GetInteractionHint() { return data null ? : ${data.name}点击查看详情; } }在实际运行过程中交互管理器用一个Update方法持续做射线检测将检测结果与IInteractable接口做匹配命中后把return的hint文本交给UI层显示。这段代码我用到了C#的GetComponent泛型方法但注意每次调用GetComponent是有性能开销的如果场景里可交互物体很多需要在初始化时缓存一次别在Update里反复取组件。这是一个很容易忽略的性能问题尤其在展品数量较多的场景里稍不留意就会把帧率拖下来。5.3 表现层音频播放、动画控制与数据绑定表现层包括声音、动画和视图更新。声音方面我用AudioSource池的方式管理多个解说音频避免频繁创建和销毁AudioSource。展品被点击播放解说时如果有其他解说正在播放系统会先停止它再播放新的避免声音重叠。动画方面针法演示动画我用的是Unity的Animator状态机配合一个简单的枚举控制。例如“劈丝”演示动画的播放本质上是把动画状态机切换到对应状态同时在模型上激活一条丝线分裂的特效。代码逻辑如下当用户在工艺展示厅点击“劈丝”演示按钮时InteractionManager调用演示控制器DemonstrationController.Play(split_silk)控制器负责切换动画状态、播放配音、并在动画结束事件里恢复到待机状态。这里有个Unity开发的经典坑动画事件Animation Event不要挂在有代码逻辑的GameObject上否则代码重构时事件回调很容易丢失。我的做法是专门用一个空的子物体挂事件接受脚本父物体只负责渲染和动画状态职责分离后整个系统稳很多。UI数据绑定方面我并没有使用额外的MVVM框架而是手动写了一个简单的观察者模式。ExhibitDetailPanel注册一个静态事件OnExhibitDataChanged数据更新时触发。这种方式比引入重型框架更适合这种中小型项目——代码量少逻辑一目了然而且不需要额外学习成本。5.4 性能优化与内存管理C#层面的三个关键点虚拟展馆项目最怕的就是漫游过程中掉帧或者内存暴涨。针对这个问题我从C#层面做了三个关键优化。第一UI元素全部放入Canvas的合适层级并且把不需要交互的界面设置为Raycast Target false。这样做的原因是uGUI的raycast检测会有CPU开销界面元素一多每次点击检测都会遍历所有可点击物体取消不需要的raycast目标能明显降低开销。第二展品材质的共享与实例化。Unity里如果一个材质被多个物体使用修改其中一个物料的材质属性会影响所有物体。但对于绣品这种每件都需要独立光泽参数的必须使用MaterialPropertyBlock而不是直接实例化材质。MaterialPropertyBlock可以在不创建材质实例的情况下修改单个物体的渲染属性大幅减少材质实例数量降低Draw Call和内存占用。第三C#代码的垃圾回收压力控制。展品详情面板频繁打开关闭如果每次打开都new一个列表来承载UI元素会不断产生零碎垃圾导致GC垃圾回收产生卡顿。我的做法是在面板初始化时就把所有用到的控件引用缓存到字段里后续只更新字段指向的对象内容不重新创建UI元素。另外频繁调用的字符串操作尽量使用StringBuilder拼接避免大量字符串常量在堆上反复分配。6. 常见问题与排查技巧实录6.1 打包发布Mac/Windows平台差异与IL2CPP踩坑这个项目我先后打过Windows和Mac两个平台的包踩过的坑非常典型。如果你也是用Intel版Mac做开发打包时注意Build Settings里Architecture要选x86_64别选Universal否则打出来的包在某些机器上会有兼容性问题。另外一个非常容易忽视的是IL2CPP与Mono的差异。开发测试时默认用Mono运行正常一旦切换到IL2CPP打包代码中如果有反射调用或者AOT提前编译不支持的用法就会在运行时直接崩溃。我的做法是尽早用IL2CPP打一个开发包测试而不是全部开发完再测。如果某些第三方库不兼容IL2CPP提前发现可以留足替换方案的时间。最后一个坑是StreamingAssets路径在Mac平台下的特例。Mac打包后的StreamingAssets是在.app包内部路径不能直接写文件只能读取。如果你在编辑器里可以写、打包后不能写多半是这个原因。我在项目里所有的展品数据都只读不写所以影响不大。6.2 运行时卡顿Draw Call与内存泄漏的排查思路如果你做完场景后一跑就发现帧率很低别急着优化代码先看看Draw Call和SetPass Call。打开Window Rendering Frame Debugger可以清晰地看到每一帧的渲染状态。展馆里面用了大量绣品模型每个模型的纹理贴图如果都是2048x2048几件展品没问题几十件展品就会把显存挤爆。我的方案是主要展品用2048背景展品只用1024并且理论上可以用纹理图集把同一区域的地板、墙面合并成一张大图。但图集合并要小心——如果后期要单独调整某个材质的颜色或UV偏移就会牵一发动全身。所以图集只用在静态环境的贴图上动态展品不用。内存泄漏常见的来源是事件没有注销。比如UI面板订阅了OnExhibitDataChanged事件但面板关闭时没有取消订阅那么面板对象虽然被隐藏了却仍然在事件列表里导致内存无法回收。我在每个UI面板的OnDisable方法里都写了取消订阅的代码这是Unity开发中非常基础但极其重要的习惯。6.3 交互失灵射线检测命中但按钮无响应的排错流程如果你遇到“准星已经变色但点击展品没反应”的情况按这个顺序排查首先确认点击时射线确实击中了展品Collider且Collider没有设置成Trigger如果设置成了Trigger物理射线默认不会检测到要用Physics.Raycast的重载让射线也能检测Trigger其次确认是否有其他UI元素挡住了点击事件uGUI的事件系统会拦截物理射线点击常见的问题是详情面板关闭后残留一个透明全屏Image它的Raycast Target还没关导致所有点击都被它吃掉最后再检查展品对象是否实现了接口以及交互管理器的执行事件是否被其他逻辑阻塞。我实际项目中遇到过好几次类似问题最终的根源都是某个隐藏的UI元素Raycast Target没关干净。排查方法是在Event System的Graphic Raycaster组件上打开调试信息或者干脆在交互管理器里写一行Debug.Log打印点击命中的物体名称看到底是谁“接住”了这次点击。6.4 数据加载失败JSON路径与编码问题的快速定位如果你发现展品面板弹出来一片空白大概率是数据没有正确加载进来。优先检查三处JSON文件有没有放在StreamingAssets目录下并设置为Copy to Output Directory路径拼接是否正确尤其是在Mac平台下要用Application.streamingAssetsPath动态获取JSON文件保存格式是不是UTF-8。第三个问题最隐蔽因为Windows下的记事本默认保存的是带BOM的UTF-8如果某个编辑器把文件存成了GB2312或者带特殊字符Unity的JsonUtility解析时不会直接报错但字符串里会出现乱码。我的建议是所有JSON文件统一用VS Code或记事本另存为UTF-8无BOM格式并且开发初期就在加载后打一条日志输出第一个展品的名称确认数据解析正常后再继续其他工作。7. 从项目复盘到扩展方向做到这里整个系统已经具备了一个文化类虚拟展馆该有的核心能力空间漫游、展品展示、交互点击、导览引导、数据驱动内容更新。整体开发周期大约六周其中场景搭建和材质调试占了一半时间交互逻辑反而没有花太多精力——这正说明前期架构设计非常重要把数据层和表现层解耦之后代码层面的开发效率会大幅提升。复盘的时候我觉得最有价值的决定有三个一是选URP作为渲染管线为后续的画面升级留足空间二是用数据驱动的方式管理展品内容让非程序员也能参与内容维护三是在交互设计上坚持“克制”原则不炫技把每一处交互都落在“让用户更理解苏绣”这个目标上。最后分享一个实用的小技巧如果你想让虚拟展馆的展示效果再上一个档次可以考虑在展品周围加一个“隐藏区块”——当玩家距离展品小于一定距离时自动切换到预设的“近景特写”虚拟相机视角播放一小段丝线质感的特写动画。这个功能背后的代码其实就是C#的简单距离判断和虚拟相机切换但给用户的沉浸感提升是肉眼可见的。后续如果你想把这个系统扩展到线下大屏展示或者接入AR眼镜做增强现实导览底层的交互接口和数据架构都可以直接复用只需要替换表现层和交互方式。这也是我推荐大家做一个“可生长的系统”的原因——不是为了一次性交付而是让这个作品能随着技术更新持续进化。
返回列表