
“游戏开发日志”这种标题看着像休闲记录真正做的时候并不会轻松。这次给项目新增“船”功能目标很直白让角色能走到湖边坐上一艘小船在湖面上划动同时在第一次成功划船时触发成就“湖上划水”。整个改动涉及场景、碰撞、动画、输入控制、成就系统和存档不是只往场景里放一艘船模型就能交工。这个功能更适合已经有一套第三人称角色控制和基础 UI 系统的项目如果你现在正在做类似的水上玩法可以参考这条开发日志里记录的顺序和踩坑点。先说明一下我所在的环境项目用 Unity 开发使用 URP 渲染管线角色控制和场景都是自研模块。下面提到的很多处理方式并不绑定 Unity换成其他引擎也是同一套思路。重点不是某个 API而是“船功能”落地的完整链路。1. 需求拆解船功能不是光加一艘船模型1.1 先定义“湖上划水”要解决什么在动代码之前我先把需求写在开发任务卡上。它不是“在湖面上放一条船”这么简单而是几个问题连在一起玩家如何发现船在什么位置登船角色上船后镜头、移动和碰撞怎么切换船怎么在水面上运动是自动漂移还是玩家手动划划船过程用什么判断是否达成成就成就触发后如何保存防止每次进游戏都重新弹一次。我把这些整理成一张最小可行清单。第一步只做单机场景里的单人船船不能开太快操作尽量接近“划船”而不是赛车。这样做的原因是如果直接把船做成载具的“油门刹车”玩家在湖面上会很容易飞出去观感和手感都会很奇怪。所以第一版船的控制方案我决定采用持续推力加惯性转弯的方式配合一小段左右对称的划桨动画而不是直接套用车的控制脚本。在功能设计阶段我还定了一个验收标准玩家从码头走上船按前进键开始划动镜头跟随船离开码头约 20 米后弹出“湖上划水”成就然后玩家可以正常下船。这个标准不用写得很复杂但可以帮助判断“功能是否完成了”。1.2 功能拆成哪几个模块拆完需求之后我把功能分成四个模块场景与碰撞水面区域、船体碰撞、可登陆码头。玩家登船与离船交互触发、角色父级切换、状态切换。船体运动前进转向、浮力、边界限制、动画同步。成就系统条件检测、触发校验、存档与 UI 反馈。这四个模块相互依赖。如果只做船体运动不处理登船角色就只能在岸边看着如果只做登船不处理成就触发功能虽然能玩但缺少一个明确的完成目标玩家也不会知道“哦这个玩法结束了”。我建议在接这种功能时别急着写代码先在一张纸上把玩家体验路径画出来看到船、靠近、提示交互、上船、划动、触发成就、下船。每一步都要有反馈这也是后面排查问题的依据。模块之间最好用事件解耦。比如船运动模块只负责移动不管成就成就系统监听“离岸距离”事件而不是直接修改船的移动参数。我第一次开发时图省事在船的 FixedUpdate 里直接调成就系统的方法结果后来加联机功能时发现所有逻辑混在一起根本拆不开。2. 场景与碰撞先把水面变成能“站住”的实体2.1 水面区域怎么划分很多新手会直接把水面做成一个透明平面然后让船在平面上飘。这样做在视觉上没问题但会有两个隐患第一角色走到水里时可能直接穿过水面第二船的位置和浮力计算没有一个相对稳定的基准面。我建议至少做两层结构一层是可见的水面渲染平面另一层是不可见的水体碰撞区域。碰撞区域可以是一个带有厚度的 Box Collider高度略低于水面这样船能漂浮在设定高度角色在浅水区也能正常站立。水的面积也要有边界。如果你用的是陆地地图湖边需要一圈碰撞体来挡角色否则角色会一直走到地图外。我用了一个最简单的方法在湖底铺设不参与渲染的碰撞网格湖岸边缘放上石头或围栏碰撞体再在水域四角各放一个可视标记方便测试时观察船是否超出范围。水面高度这个参数我建议单独用一个全局变量来存不要散落在材质和碰撞体里。因为后续调船体浮力、角色下水深度、镜头近剪裁面都需要知道“水面高度到底是多少”。我在项目里专门建了一个 WaterManager暴露一个GetWaterHeight()方法所有水面相关逻辑都从这里取数值。这样做的好处是如果以后做潮汐效果只需要改这一个地方。2.2 船的碰撞体与浮力模拟船体我先做了一个简化碰撞体分成两层船壳碰撞用于阻挡浮力点用于计算水面浮力。不要只给船一个大的 Box Collider因为船在水里会晃一个点很难模拟出前后左右摇晃的效果。常见的做法是在船体前后左右布置 4 个浮力采样点每个采样点检测当前深度产生向上的浮力让船恢复平衡。这里可以直接用伪代码表示foreach buoyancyPoint: depth waterSurfaceHeight - point.worldPosition.y if depth 0: force buoyancy * depth applyForce(point.position, up * force)我加了一行注释修改浮力系数时要同时调整船的吃水深度否则船会变成“水上摩托”。最初我把浮力系数调得过高船一进入水面就跳到半空镜头都跟不上。后面把系数降低再配合阻尼参数才让船呈现出“压着水面”的感觉。表格式整理一组初始参数方便调试时对照参数初始值作用调大后果浮力系数1.0每单位深度的浮力大小船容易弹跳吃水浅吃水深度0.3船体静止时下沉距离越大越像重船线性阻尼0.6船前进时水平阻力越大减速越快滑行越短转动阻尼0.8转向阻力越大转向越迟钝这些数值每个项目都不一样但调整顺序有规律。我一般先调吃水深度再调浮力系数最后调阻尼。如果一上来就调阻尼船会一直飘很难判断是浮力问题还是阻力问题。2.3 角色上船与下船角色上船这个交互我用了触发器加按键提示。在船旁边放一个可交互区域玩家进入范围后显示“按 E 上船”按下后角色切换到船上。这时候要处理几件事把角色作为船的子对象或者让角色的位移同步到船的位置关闭角色自带的碰撞体避免角色在船上和船体碰撞切换输入模式从“地面控制”改成“船上控制”。下船逻辑更麻烦。船在湖中央时不能下船否则角色会掉进水里。我限制只能在码头或者岸边浅水区域下船。实现时我在码头附近放了一个下船点玩家按键后先判断船的位置是否在下船点范围内再执行脱离。这样做的原因是如果你允许任意地方下船必须先处理角色落水和游泳状态否则第一版会留一堆 bug。上船时角色位置也很关键。如果角色直接移到船中心会有一瞬间“瞬移”的感觉如果移到船边又可能卡在碰撞体里。我用的方法是把角色放到“船员座位点”上这个点是一个空的子物体放在船体中央偏左的位置。上船时播放一个快速过渡动画把角色从站姿过渡到坐姿观感会自然很多。3. 划船手感和参数调整控制比画船难3.1 前进与转向拆开处理船的控制和角色行走最大的区别是角色可以急停转向船则需要惯性。我把输入分成两个轴前向推力和转向速度。前向推力决定船向前加速的快慢转向速度决定船转动方向。两个轴分开处理不容易出现“按前进却斜着走”的问题。第一版我先跑了最简单的直线测试按 W 船向前按 A/D 船转向。因为没有给船加刹车所以松开按键后船还会继续往前走一段。这一步很关键因为“划船”的感觉就来自这段滑行。滑行时间太长玩家觉得船失控滑行时间太短又像开车。我实际测试时把阻尼系数从 1.0 调到 0.3找出一个稍微滑行但不过头的区间。转向和前进合并的时候有个常见的“船尾甩动”问题船在高速前进时打方向船体会因为离心力滑向外侧。我处理的方法不是单纯提高转向阻尼而是在转向时对船体施加一个与当前速度方向相反的修正力。这个力不用太大只要能让船头转向时船尾跟着走而不是原地转。3.2 划船动画与位移同步成就名是“湖上划水”所以划船动画不能少。我在开发初期先做了一个简单的手臂摆动动画动作频率和船的推力绑定。这里要注意一个问题动画播放和船的移动并不是天然同步的。如果动画频率快船速慢玩家会觉得角色在空划如果动画频率慢船速快又像船自己是马达驱动。我用的办法是把划桨动画单独拆成一段可循环片段再用动画速度倍速来同步。前进力度大时动画播放速度也调快船停下来时动画播放速度降到 0。在测试时我会打开动画状态机和船体速度的调试输出确认数值吻合再继续调相机。这一步需要反复试。同一个动画在不同的船型上速度感不同快艇需要更急促的动画木筏则需要舒缓一些。我建议先做出“最小可接受”的同步效果再让美术同学去细化动作不要等到动画全部调好才开始验证移动手感。3.3 不同方向参数调整水面移动还有一个常见问题转向时船速会因为离心力变化。如果转向速度设置太高船会像陀螺一样原地转如果太低又调不过头。我在船上加了转向阻尼让转向速度随船速变化。低速时转向更灵活高速时转向更平稳。除此之外还必须限制船的移动范围。我用一个简单方法当船靠近湖边界时给船施加一个向湖中心方向的虚拟力同时让镜头不要穿帮。真实项目里边界也可以做成浮标或者水草区的碰撞体但从调试效率出发第一版先用距离判断最快。这里给出一个简单方向处理顺序船速度大于 0才允许转向转向值乘以一个与速度相关的系数速度越快单帧转角越小每帧用插值将当前船头方向转向目标方向而不是直接赋值边界检测单独做不依赖速度。这种处理方式能避免很多“手感太飘”的问题。如果你在测试时发现船总是左右摇摆可以先关闭转向输入只保留前进确认直线稳定性再逐步恢复转向。4. 成就系统接入达成条件、触发时机和防重复4.1 成就条件怎么设计“湖上划水”这个成就我暂时定义为“第一次成功划船离开码头”。这是一个很保守的条件适合验证整条链路。玩家只要上船向前划超过一定距离就触发成就。你不用在设计初期就搞“累计划船 1000 米”这种复杂数值因为后续要调平衡等核心玩法稳定了再改条件也不迟。实现上我在船的运动模块里加了一个“已离岸距离”的累计变量。当船与码头起点的距离超过 20 米就向成就系统发送一个事件。之所以用距离而不是时间是因为“第一次成功划船”这个描述更直观。如果你以后要多级成就比如“老船员”“码头之王”再把距离、次数、时间组合成多个判定条件即可。这里还要考虑“什么算离岸”。如果只是单纯比较船和码头的距离船平行于码头驶过也算离岸。我觉得更准确的是看“船与起点之间是否已经无法步行返回”或者“船头方向是否指向湖面”。但第一版我做了一个简化从码头起点到湖心方向拉一条参考线船在参考线投影上的距离超过阈值才判定。这样玩家即使沿岸边横着走也不会提前触发成就。4.2 日志与调试为什么成就没触发最容易出错的地方是事件触发时机。我第一次测试时发现船已经离岸很远成就还是没有弹出来。查了半天问题不是条件判断写错而是“船与起点距离”这个变量在船下锚的时候被反复归零了。因为我用的起点是世界坐标而船在下水的瞬间坐标会因为碰撞体调整发生一次微小跳变导致距离计算一直不稳定。解决办法是在玩家上船时记录一个固定的起点坐标之后只计算船当前位置和起点坐标的差值。不要在每次更新时重新读取“起点物体”的位置尤其不要用它作为距离基准。工程化的做法是将起点坐标作为局部变量保存而不是在成就检测函数里再次读取场景对象。调试时最好加日志。每帧输出“当前距离”“是否达到阈值”跑一遍就能看出问题。日志不用一直保留调试完可以放在 DEBUG 宏或开关里。我在项目里加了一个AchievementDebug开关默认关闭开启后可以在屏幕上看到实时的成就条件状态。这个开关对策划调整数值也很有用。4.3 存档和成就解锁反馈成就触发后必须保存解锁状态。我用了一个简单的玩家偏好文件保存成 JSON 或二进制。这个功能有一个坑存档必须放在“解锁成功”而不是“收到事件”的时候。如果你在成就条件满足的瞬间弹了个 UI但存档时失败玩家下次进游戏就会重复解锁体验会打折扣。真正稳妥的顺序是先写入存档再弹出 UI再播放声音或镜头特效。这样即使 UI 弹出时崩溃存档也已经写下了。另外还要去重每次启动游戏时检查成就状态如果已经解锁就不再触发“湖上划水”的提示。存档结构我用类似下面的方式{ achievements: [ { id: lake_paddling, unlocked: true, unlockTime: 2025-01-10T12:00:00 } ] }虽然字段很简单但它已经足够支撑“是否解锁”的查询。如果你要做成就进度条可以再增加progress和target字段。写文件时先写临时文件再替换正式存档可以降低切电或者强杀进程导致存档损坏的概率。5. 踩坑记录开发日志里最值得看的部分5.1 角色一上船就穿模我遇到的第一个坑是角色上船后直接穿进船体。原因很常见角色碰撞体没有关闭上船后角色仍然和船的碰撞体计算物理结果被挤进船壳里。解决办法是上船时把角色的刚体和碰撞体禁用下船时再启用并且把角色位置放到船的指定座位点。另外镜头也要切换成跟随船的第三人称视角避免镜头穿过船体。刚开始处理时我没有关闭角色刚体只是把角色移到座位点。结果船一加速角色就从座位上滑出去甚至被甩到水里。关闭刚体后角色的位移完全由船控制就没有这个现象了。不过要注意禁用刚体后角色身上的触发器交互也要一起关闭否则玩家在水面上还能按到岸边 NPC 的对话逻辑会乱。5.2 船在岸边抖动船在靠近岸边的浅水区抖动是浮力采样点太少导致的问题。岸边的水面高度和碰撞区域高度不完全一致船体四个采样点有的在水里有的在岸上浮力分布不均就会抖动。我调整了岸边碰撞体的深度把水面碰撞区域延伸到水下更深处让采样点不会因为地形起伏而突然失效。如果你也遇到类似抖动可以先观察是整船抖动还是局部抖动。整船抖动可能是浮力系数过高局部抖动则多半是采样点深度和地形碰撞体冲突。不要一上来就调浮力先关掉地形碰撞看是否恢复正常再决定改哪边。岸边抖动还有一个容易被忽视的来源水面 shader 的波浪高度。如果只做了视觉波浪没有把波浪高度同步到浮力计算中船物理上看起来是平的但视觉上水在动两者叠加后玩家会觉得船在抖。这种情况我建议把水面高度取一个平均值不要直接用顶点动画的最高值。5.3 成就重复触发或存档丢失成就重复触发大多数不是成就系统的问题而是状态数据没有正确持久化。我在异常测试里模拟了“解锁后立刻退出游戏”的情况发现如果 UI 弹出后马上强杀进程存档可能还没写进去。所以我把存档写入提前到事件触发后并加了“写入完成”的异步回调。如果使用异步要注意存档顺序不要有多个线程同时写同一个文件。如果你的成就界面在每次启动时都播放一遍解锁动画也要检查一下读取存档的时间点。我遇到过因为存档读取放在 UI 初始化之后导致启动瞬间成就列表是空的然后又被重新解锁一遍。正确顺序是先读取存档初始化成就状态再刷新 UI。5.4 性能水面网格和漂浮物水面如果用实时反射和动态网格对移动端或低配机器压力会比较大。我在测试时发现打开实时反射后帧率明显下降。后来改成预烘焙的反射探针或者简化水面 shader帧率稳定了很多。船的漂浮物、水花粒子也不要做得太狠粒子数量多的时候湖面上同时有 3 艘船就会卡顿。给粒子加一个最大数量用完后循环复用不要每艘船都实例化新的粒子系统。游戏开发里 60Hz 和 30Hz 的差别在水面上很敏感。如果水面更新逻辑放在 Update 里帧率波动时波浪速度也会波动导致船的动作一卡一卡。我建议把水面的物理采样和粒子更新放在固定时间步长里保证每帧数值稳定。帧率高低是设备差异不应该是水面表现波动的理由。性能这块适合用 Profiler 盯不能只看帧率面板。我在测试时发现水面 shader 的耗电并不高真正高的是每个浮力采样点引起的物理计算和特效粒子。如果采样点有 8 个每个点都要做相交检测性能压力会翻倍。可以先从 4 个点开始不够再加。6. 后续扩展与上线前检查6.1 从单功能到玩法扩展船功能跑通之后可以往几个方向扩展增加不同船型比如快艇、木筏、脚踏船增加湖泊区域的其他交互比如钓鱼、岸边 NPC 对话增加成就多级条件比如“累计划船时间达到 10 分钟”增加联机同步把船的移动状态通过服务器广播给其他玩家。每加一个方向都要回到核心模块检查。比如增加新船型浮力点位置和碰撞体要重新调增加联机还要处理船的插值和同步延迟。不要一次性铺开先把单机手感稳定再做扩展。还有一个容易忽略的扩展点船与角色状态机的整合。如果项目里已经有跑步、跳跃、游泳状态船状态必须能够正确处理切换。比如玩家持武器时能否上船上船后武器是否收回下船后角色是否回到之前的移动模式。这些不做的话功能会显得“很脆”。6.2 上线前检查清单我在发布这个功能之前会过一遍清单玩家是否能在码头正常上船、下船角色上船后镜头是否正确切换是否出现穿模船在行驶时是否稳定没有明显抖动划船动画的速度和船速是否匹配成就触发后是否只弹一次存档是否正确写入边界限制是否生效船不能冲到岸上或地图外低配设备上湖面区域帧率是否达标断线或中途退出后成就状态是否保持。这一步看起来像打勾其实比写代码更花时间。我在调试时经常忽略“玩家走到船的另一边时能不能看到交互提示”这种小问题最后还是要回到场景里手动测一遍。水面上玩法的调试和普通陆地上不一样。角色在地面走出问题可以看路径点船在水上出问题要先确认水面高度、浮力采样点、岸边地形有没有冲突。我建议保留一个调试快捷键用来显示所有浮力点位置和当前水面高度。这样后面改场景地形时能快速发现问题。如果你也在做类似的水上车船玩法我建议先按这个顺序把最小闭环跑通水面碰撞、登船、移动、成就触发、存档。功能不一定要一次做完整但每个环节都要有明确判断标准。踩过几次之后我发现很多问题不是工具能力不够而是前置条件和输入流程没有处理干净。