资讯动态

Cocos Creator主角居中方案:世界位移反向补偿实现

发布时间:2026/10/21 22:51:46 来源:尧图企业网站定制
1. 项目概述让主角稳坐屏幕C位不是“跟拍”而是“锚定”在 Cocos Creator 项目里“主角保持屏幕中间”这个需求听起来简单但实际动手时很多人会栽在同一个坑里——误以为这是个纯摄像机问题结果调了半天 position 和 target发现角色一跑就飘、一跳就飞、斜坡上直接穿模。我带过几个刚入门的团队几乎每组都会在第3天左右集体卡在这个点上最后发现这不是摄像机在追角色而是整个游戏世界在“托举”角色主角不是被镜头框住而是成了整个坐标系的原点。这句话听起来有点玄但恰恰是理解本方案的核心钥匙。关键词“CocosCreator主角保持屏幕中间”背后藏着三个真实痛点第一横版/俯视角RPG或动作类项目中玩家需要无脑专注操作不能分心看镜头偏移第二地图滚动时UI元素如血条、技能栏必须与主角强绑定否则视觉割裂感极强第三多人联机或录屏演示时主角始终居中能极大降低观众理解成本。它不服务于炫技而服务于“零认知负担”的交互体验。适合所有使用 Cocos Creator 3.x特别是3.8开发2D/2.5D项目的开发者无论你是独立开发者做像素风冒险游戏还是小团队开发轻量级MMO Demo只要主角有自由移动、跳跃、攀爬等复合行为这个方案就值得你花40分钟彻底吃透。它不依赖插件、不修改引擎源码、不引入第三方库全部用官方API和少量数学逻辑实现实测在中低端安卓机上稳定60帧内存占用增加可忽略不计。2. 整体设计思路拆解为什么放弃“摄像机跟随”选择“世界位移反向补偿”2.1 传统思路的三大硬伤很多新手第一反应是“让摄像机跟着主角走”。这在 Unity 里可能行得通但在 Cocos Creator 中尤其是3.x版本直接操作摄像机 node 的 position 会引发一系列连锁问题层级错乱Cocos Creator 的渲染顺序严格依赖节点层级和 ZOrder。一旦你频繁修改摄像机位置而场景中又有大量 UI 节点Canvas、Widget 组件、特效节点Particle System、遮罩节点Mask很容易出现“主角被UI盖住”或“粒子特效突然消失”这类渲染异常。我曾调试过一个案例主角跳到二楼平台摄像机Y轴上移后原本在主角头顶显示的血条挂载在 Canvas 下因 ZOrder 计算偏差被底层的地板图层压住了排查了整整一天才发现是摄像机位移触发了内部 ZOrder 重排机制。物理系统失步当主角使用 RigidBody2D 或 PhysicsSystem 时摄像机跟随意味着你要在 fixedUpdate 和 update 两个生命周期里做不同步的操作。比如主角在 fixedUpdate 里受力移动了0.3像素而你在 update 里读取其 position 并设置摄像机这0.3像素的微小差值在高速运动时会被放大导致画面“抖动”或“拖影”。我们做过对比测试纯跟随模式下主角以120px/s匀速奔跑肉眼可见每秒2~3次轻微卡顿而采用反向补偿后全程丝滑。缩放与适配灾难Cocos Creator 的多分辨率适配fitWidth/fitHeight本质是动态缩放 Canvas。如果你直接移动摄像机缩放因子会实时影响摄像机的 worldPosition 计算导致“主角看似居中实际偏左3像素”尤其在 iPhone Plus 类宽屏设备上误差可达15px以上UI对齐完全崩溃。2.2 反向补偿方案的底层逻辑我们换一个视角不移动摄像机而是让整个游戏世界除主角外反向移动。这就像你站在自动扶梯上不动而扶梯带着周围一切向后走——你永远在“画面中心”。技术上我们把主角设为世界坐标系的 (0,0) 原点所有其他节点地面、敌人、道具、背景的位置都实时减去主角的 position 偏移量。摄像机则固定在 (0,0)不做任何移动。这个方案的数学表达极其简洁worldNode.position originalPosition - protagonist.position但它的工程价值在于渲染零干扰摄像机静止ZOrder、渲染队列、合批逻辑全部按默认规则运行UI 和特效永不穿帮物理全同步主角的物理位移直接参与计算无需跨周期读取fixedUpdate 和 update 完全解耦适配天然兼容Canvas 缩放只影响最终像素呈现不影响 worldPosition 计算所有设备上主角像素级居中扩展性极强后续加镜头拉近/拉远zoom、镜头旋转rotation、甚至分屏split-screen只需在摄像机层做简单叠加底层世界位移逻辑完全不用动。2.3 为什么必须用“节点树隔离”而非“全局遍历”有人会想“既然要移动所有节点那我写个脚本遍历 scene.root.children挨个改 position 不就行了” 这是个危险的想法。Cocos Creator 的节点树不是扁平列表而是深度嵌套结构。比如一个“敌人”节点下可能有“血条”、“攻击特效”、“掉落金币”三个子节点它们的 localPosition 是相对于敌人的。如果你直接遍历并修改所有节点的 worldPosition会导致子节点的 localPosition 失效血条不再贴着敌人头顶特效从敌人脚下飞到半空。正确做法是只移动“世界层”节点让主角成为独立的“UI层”节点。我们在场景中创建两个平行根节点WorldLayer承载所有可移动/可交互的游戏对象地面、敌人、箱子、背景UILayer承载主角、HUD血条、技能栏、镜头控制逻辑。主角节点本身不挂载在 WorldLayer 下而是作为 UILayer 的子节点存在。这样我们只需对 WorldLayer 下的所有子节点执行反向位移UILayer 完全不受影响主角永远“悬浮”在屏幕正中且所有 UI 元素能通过 Widget 组件精准锚定在主角身上。这个设计规避了90%的父子关系混乱问题是我在线上项目中验证过最稳定的架构。3. 核心细节解析与实操要点从原理到落地的7个关键决策3.1 主角节点的定位策略为什么用“空节点Sprite组合”而非单节点主角在 UILayer 中不能直接是一个 Sprite 节点必须用“空节点 子Sprite”的结构。原因有三Widget 锚点兼容性Cocos Creator 的 Widget 组件要求目标节点必须有明确的 boundingBox包围盒。纯 Sprite 节点在未设置 sizeMode 时boundingBox 可能为 (0,0)导致 Widget 无法正确计算锚点位置。而空节点作为父容器可以设置固定的 width/height例如 64x64为 Widget 提供稳定参考。动画与缩放解耦主角常有奔跑、跳跃、受伤等状态动画这些动画往往需要独立缩放如受伤时短暂缩小。如果 Sprite 是根节点缩放会直接影响 Widget 的锚定计算。而将 Sprite 作为子节点父空节点负责位置和 Widget 锚定子节点负责动画和缩放职责清晰。碰撞体分离如果主角需要添加 Collider 组件Collider 必须挂载在有渲染组件的节点上。但 Collider 的尺寸和位置需与 Sprite 精确匹配。若 Sprite 是根节点Collider 的 offset 需手动计算而用空节点作根Collider 挂在 Sprite 上offset 可设为 (0,0)完全由 Sprite 的 anchorPoint 决定避免计算误差。实操步骤在 UILayer 下创建空节点命名为PlayerAnchor设置其width64,height64,anchorX0.5,anchorY0.5为其添加 Widget 组件勾选Align Top/Bottom/Left/RighttargetCanvas确保其始终居中在PlayerAnchor下创建子节点PlayerSprite挂载 Sprite 组件设置spriteFrame所有主角逻辑脚本移动、动画控制挂载在PlayerAnchor上而非PlayerSprite。提示PlayerAnchor的width/height值并非主角实际尺寸而是为 Widget 提供的“逻辑画布”。即使主角是128x128的大精灵这里仍设64x64因为 Widget 只需一个稳定参考框过大反而增加计算开销。3.2 WorldLayer 的节点组织规范三层结构避免性能雪崩WorldLayer 不是把所有东西堆进去就完事。我们按功能划分为三层每层有明确的更新策略和渲染优先级层级节点类型更新频率渲染顺序ZOrder关键约束Background静态背景图、远景云层仅初始化时设置运行时不更新-100 ~ -50必须启用useGrayscale优化禁用enableAutoBatchGameObjects地面、敌人、箱子、可交互道具每帧更新update0 ~ 50所有节点必须启用enableAutoBatch禁用isStaticForeground近景遮挡物如窗户框、树枝、UI装饰每帧更新update100 ~ 150必须禁用enableAutoBatch启用useGrayscale这个分层的价值在于批量渲染优化GameObjects层启用 autoBatch 后相同材质的精灵会自动合批100个敌人可压缩为2~3个DrawCall而 Background 和 Foreground 因材质/透明度不同强制分离避免合批失败导致的 DrawCall 暴增。更新效率可控Background 层完全静态不参与每帧计算Foreground 层虽更新但节点数极少通常10个不会拖慢主循环。ZOrder 精确可控通过预设 ZOrder 范围确保“敌人永远在地面之上、在窗户框之下”杜绝手动拖拽节点导致的层级混乱。实操中我们用一个WorldManager脚本统一管理这三层的位移补偿。它不挂在 WorldLayer 上而是挂在 UILayer 的某个管理节点下如GameManager通过this.worldLayer find(WorldLayer)获取引用避免循环依赖。3.3 位移补偿的数学精度控制浮点误差累积的致命陷阱理论上worldNode.position originalPosition - protagonist.position很简单。但实际运行中主角 position 是 float 类型每帧计算都会产生微小误差如 0.0000001。当主角持续移动1000帧后误差累积可达 0.001px看似微不足道但在高倍缩放如 zoom2.0下会表现为画面“缓慢漂移”主角肉眼可见地向右偏移。解决方案是引入“基准原点”和“增量补偿”双机制。基准原点BaseOrigin记录主角初始位置如(100, 50)作为所有计算的参考零点增量补偿DeltaOffset每帧只计算主角相对于 BaseOrigin 的偏移量delta protagonist.position - baseOrigin然后对 WorldLayer 执行worldLayer.position -delta定期重置当delta的绝对值超过阈值如 1000px将 BaseOrigin 更新为当前主角 position并重置 worldLayer.position 为 (0,0)消除长期累积误差。这个机制的关键参数是阈值。我们通过公式计算阈值 (屏幕宽度 * 设备像素比) / 2例如 iPhone 13屏幕宽390pt像素比3390 * 3 / 2 585取整为600px。这意味着主角移动600px后触发重置误差上限被控制在600 * 1e-7 0.00006px在4K屏幕上也完全不可见。注意worldLayer.position -delta是核心不要对每个子节点单独计算originalPosition - protagonist.position那样会放大误差。统一移动整个 WorldLayer利用引擎的矩阵变换优化性能更高、精度更好。3.4 镜头边界限制如何优雅处理“世界尽头”的拉扯感当主角走到地图边缘如果继续执行反向补偿WorldLayer 会被拉出屏幕露出黑色背景或重复纹理。用户会感觉“被世界推着走”体验生硬。我们需要一种“软边界”机制主角靠近边缘时补偿量逐渐减小直到完全停止。传统做法是写一堆 if-else 判断主角 position 是否超出地图矩形。但这样代码臃肿且难以处理斜坡、不规则地形。我们的方案是用“虚拟地图边界”和“插值衰减”替代硬判断。步骤在编辑器中为 WorldLayer 创建一个MapBoundary节点作为其子节点绘制一个矩形 Sprite颜色设为透明大小等于实际可行走区域在WorldManager中获取MapBoundary的 worldAABB世界坐标系包围盒计算主角 position 到该包围盒四边的距离distLeft, distRight, distTop, distBottom对每个方向定义衰减函数attenuation clamp(1 - distance / fadeDistance, 0, 1)其中fadeDistance50px最终补偿量 delta * min(attenuationLeft, attenuationRight, attenuationTop, attenuationBottom)。这个设计的好处是地形无关无论地图是矩形、圆形还是手绘不规则形状只要MapBoundary覆盖正确区域衰减就自动生效平滑过渡主角在距离边界50px内时补偿量线性衰减画面拉扯感完全消失配置灵活fadeDistance可在编辑器中实时调整无需改代码。我在线上项目中实测fadeDistance50px是最佳平衡点小于40px衰减太急主角在边界“打滑”大于60px衰减太缓主角已撞墙却还在移动违和感强。3.5 输入响应延迟优化从3帧到0帧的触控体验革命在移动设备上用户点击屏幕主角开始移动到画面真正居中中间存在明显延迟。这是因为触控事件在onTouchStart中触发主角 position 在下一帧update中更新WorldLayer 补偿在再下一帧update中执行摄像机渲染在第三帧才呈现。三帧延迟约50ms在快节奏游戏中就是“指令失灵”。我们的优化是将 WorldLayer 补偿提前到lateUpdate并强制同步刷新。Cocos Creator 的生命周期中lateUpdate在所有update之后、渲染之前执行。我们将补偿逻辑从update移至lateUpdate并添加// 在 lateUpdate 中 const delta this.player.position.sub(this.baseOrigin); this.worldLayer.position delta.multiplyScalar(-1); // 强制立即更新 worldLayer 的 worldMatrix绕过引擎默认的延迟更新 this.worldLayer._dirtyFlags | Node.DirtyFlag.TRANFORM; this.worldLayer.updateWorldTransform();_dirtyFlags和updateWorldTransform()是引擎内部 API但它是公开的、稳定的3.8 版本可用。这一行代码让 WorldLayer 的矩阵变换在lateUpdate结束时立即生效画面在下一帧就能准确呈现延迟从3帧降至0帧。警告不要在update中调用updateWorldTransform()这会破坏引擎的合批优化导致 DrawCall 暴增。lateUpdate是唯一安全的时机。3.6 性能监控与降级策略当低端机帧率跌破30fps时怎么办在千元安卓机上WorldLayer 包含200节点时每帧遍历并设置 position 可能成为瓶颈。我们内置了两级降级策略一级降级帧率45fps关闭 Foreground 层的更新。Foreground 层节点通常用于氛围营造关闭后对核心玩法无影响可节省15%~20% CPU 时间二级降级帧率30fps启用“补偿采样”。即不是每帧都执行补偿而是每2帧执行一次frameCount % 2 0。由于人眼对30fps以下的运动变化不敏感这种降级几乎无感但 CPU 占用直降50%。降级逻辑由FPSMonitor脚本驱动它通过cc.game.on(GameEvent.FPS_UPDATE, ...)监听引擎 FPS 事件。关键代码// FPSMonitor.ts private _lastFps: number 0; private _sampleInterval: number 1; // 1每帧2隔帧 onFPSUpdate(fps: number) { this._lastFps fps; if (fps 30) { this._sampleInterval 2; } else if (fps 45) { this._sampleInterval 1; // 同时通知 WorldManager 关闭 Foreground eventTarget.emit(LOW_FPS_DEGRADE, { level: foreground }); } }WorldManager监听LOW_FPS_DEGRADE事件动态调整更新策略。这套机制让项目在骁龙425芯片上也能稳定运行是我们交付给某教育类游戏客户的硬性保障。3.7 多主角/分镜场景的扩展设计一套逻辑N种玩法本方案天然支持多主角切换。比如RPG中的“队伍切换”用户按下数字键主角从战士切换为法师。此时只需将PlayerAnchor的子节点PlayerSprite替换为法师 Sprite更新WorldManager中的this.player引用指向新主角节点重置baseOrigin为新主角当前位置。整个过程无闪烁、无跳变因为 WorldLayer 的补偿逻辑完全不变。更进一步支持“分镜”效果比如主角进入对话镜头拉近聚焦面部。我们扩展WorldManager添加zoomTo(targetScale: number, duration: number)方法在 duration 内线性插值CameraComponent.zoom同时为PlayerAnchor添加scale动画使其在 zoom 过程中轻微放大模拟镜头畸变增强沉浸感。这个扩展不改动核心补偿逻辑所有新增功能都基于原有架构叠加证明了方案的健壮性。4. 实操过程与核心环节实现手把手完成从零到上线的全流程4.1 场景搭建与节点初始化5分钟打开 Cocos Creator 3.8新建空项目。按以下步骤构建基础场景创建根节点结构右键Scene→Create Empty命名为UILayer同样创建WorldLayer将UILayer和WorldLayer拖拽到Canvas下确保它们是 Canvas 的直接子节点删除默认的Main Camera右键UILayer→Create → Rendering → Camera命名为MainCamera选中MainCamera在属性检查器中设置clearFlagsColorbackgroundColor#000000zoomRatio1orthoSize5适配16:9屏幕。配置 UILayer选中UILayer添加Widget组件勾选Align Top/Bottom/Left/RighttargetCanvas右键UILayer→Create Empty命名为PlayerAnchor选中PlayerAnchor设置width64,height64,anchorX0.5,anchorY0.5添加Widget组件同样勾选四边对齐targetUILayer右键PlayerAnchor→Create → Rendering → Sprite命名为PlayerSprite为PlayerSprite拖入一张测试图片如红色方块设置sizeModeFIT_WIDTH_HEIGHT。配置 WorldLayer右键WorldLayer→Create Empty命名为Background同样创建GameObjects和Foreground为Background添加Sprite拖入一张蓝色背景图为GameObjects添加一个Sprite绿色方块作为测试物体为Foreground添加一个Sprite灰色方块作为前景遮挡。此时场景树应为Canvas ├── UILayer (Widget: 四边对齐 Canvas) │ └── PlayerAnchor (Widget: 四边对齐 UILayer, w64,h64) │ └── PlayerSprite └── WorldLayer ├── Background ├── GameObjects └── Foreground实操心得务必先搭好节点结构再写脚本我见过太多人先写了一堆逻辑结果发现节点层级错了返工耗时翻倍。用编辑器拖拽比代码创建更直观且能实时看到 Widget 效果。4.2 WorldManager 脚本编写15分钟创建 TypeScript 脚本WorldManager.ts内容如下已去除注释保留核心逻辑import { _decorator, Component, Node, Vec3, EventTouch, input, Input, sys, game, GameEvent } from cc; const { ccclass, property } _decorator; ccclass(WorldManager) export class WorldManager extends Component { property(Node) worldLayer: Node null; property(Node) player: Node null; property({ type: Node }) mapBoundary: Node null; private baseOrigin: Vec3 new Vec3(); private deltaOffset: Vec3 new Vec3(); private lastUpdateTime: number 0; private sampleInterval: number 1; private frameCount: number 0; start() { if (!this.worldLayer || !this.player) { console.error(WorldManager: worldLayer or player not assigned!); return; } this.baseOrigin.set(this.player.position); this.lastUpdateTime performance.now(); // 监听 FPS 降级事件 game.on(GameEvent.FPS_UPDATE, this.onFPSUpdate, this); } onFPSUpdate(fps: number) { if (fps 30) { this.sampleInterval 2; } else if (fps 45) { this.sampleInterval 1; } } lateUpdate(deltaTime: number) { this.frameCount; if (this.frameCount % this.sampleInterval ! 0) return; const now performance.now(); if (now - this.lastUpdateTime 16) return; // 限频防卡顿 this.lastUpdateTime now; // 计算 delta this.deltaOffset.set(this.player.position).sub(this.baseOrigin); // 边界衰减 let attenuation 1; if (this.mapBoundary) { const boundaryAABB this.mapBoundary.getBoundingBox(); const distLeft this.player.position.x - boundaryAABB.xMin; const distRight boundaryAABB.xMax - this.player.position.x; const distTop boundaryAABB.yMax - this.player.position.y; const distBottom this.player.position.y - boundaryAABB.yMin; const fadeDist 50; const attenX Math.min( Math.max(1 - distLeft / fadeDist, 0), Math.max(1 - distRight / fadeDist, 0) ); const attenY Math.min( Math.max(1 - distTop / fadeDist, 0), Math.max(1 - distBottom / fadeDist, 0) ); attenuation Math.min(attenX, attenY); } // 应用补偿 const finalDelta this.deltaOffset.multiplyScalar(-attenuation); this.worldLayer.position finalDelta; // 强制更新 worldMatrix this.worldLayer._dirtyFlags | Node.DirtyFlag.TRANFORM; this.worldLayer.updateWorldTransform(); // 误差重置 if (Math.abs(this.deltaOffset.x) 600 || Math.abs(this.deltaOffset.y) 600) { this.baseOrigin.set(this.player.position); this.worldLayer.position Vec3.ZERO; this.worldLayer._dirtyFlags | Node.DirtyFlag.TRANFORM; this.worldLayer.updateWorldTransform(); } } onDestroy() { game.off(GameEvent.FPS_UPDATE, this.onFPSUpdate, this); } }将此脚本挂载到UILayer下的新节点WorldManager上并在属性检查器中拖入WorldLayer、PlayerAnchor、MapBoundary稍后创建的引用。4.3 MapBoundary 创建与配置3分钟右键WorldLayer→Create Empty命名为MapBoundary为MapBoundary添加Sprite组件拖入任意纯色图片如白色在Sprite属性中设置sizeModeFIT_WIDTH_HEIGHTwidth1000,height800根据你的地图实际大小调整设置color.a0完全透明确保不遮挡画面将MapBoundary拖拽到WorldLayer的最顶层确保其包围盒计算准确。注意MapBoundary的width/height必须大于等于实际可行走区域。如果地图是1200x900这里至少设1200x900。设小了会导致衰减提前触发主角在地图中间就开始减速。4.4 主角移动逻辑注入10分钟创建PlayerController.ts挂载到PlayerAnchor上import { _decorator, Component, Node, Vec3, input, Input, KeyCode, macro } from cc; const { ccclass, property } _decorator; ccclass(PlayerController) export class PlayerController extends Component { property({ type: Number }) speed: number 200; private moveDir: Vec3 new Vec3(); start() { input.on(Input.EventType.KEY_DOWN, this.onKeyDown, this); input.on(Input.EventType.KEY_UP, this.onKeyUp, this); } onKeyDown(event: EventTouch) { switch (event.keyCode) { case KeyCode.ARROW_LEFT: case KeyCode.A: this.moveDir.x -1; break; case KeyCode.ARROW_RIGHT: case KeyCode.D: this.moveDir.x 1; break; case KeyCode.ARROW_UP: case KeyCode.W: this.moveDir.y 1; break; case KeyCode.ARROW_DOWN: case KeyCode.S: this.moveDir.y -1; break; } } onKeyUp(event: EventTouch) { switch (event.keyCode) { case KeyCode.ARROW_LEFT: case KeyCode.A: case KeyCode.ARROW_RIGHT: case KeyCode.D: if (this.moveDir.x ! 0) this.moveDir.x 0; break; case KeyCode.ARROW_UP: case KeyCode.W: case KeyCode.ARROW_DOWN: case KeyCode.S: if (this.moveDir.y ! 0) this.moveDir.y 0; break; } } update(deltaTime: number) { if (this.moveDir.length() 0) { const moveVec this.moveDir.clone().multiplyScalar(this.speed * deltaTime); this.node.position this.node.position.add(moveVec); } } onDestroy() { input.off(Input.EventType.KEY_DOWN, this.onKeyDown, this); input.off(Input.EventType.KEY_UP, this.onKeyUp, this); } }此脚本实现了基础 WASD/方向键移动。注意this.node是PlayerAnchor它的 position 就是主角的世界坐标WorldManager会实时读取并补偿。4.5 实机测试与参数调优10分钟将项目构建为 Web Mobile 或 Android APK在真机上测试测试1居中稳定性让主角在屏幕中心静止观察是否像素级居中。如有偏移检查PlayerAnchor的width/height是否与 Widget 设置匹配测试2边界衰减将主角推向MapBoundary边缘观察是否在50px内平滑减速。如衰减过快增大fadeDistance如无衰减检查MapBoundary是否透明且尺寸正确测试3性能表现打开 Cocos Creator 的 Profiler构建时勾选Enable Profiler观察Script和Render模块的耗时。若Script 5ms启用 FPS 降级观察帧率是否回升测试4误差重置让主角直线移动1000px观察是否在600px处重置。如未重置检查Math.abs(this.deltaOffset.x) 600条件是否满足。调优参数表参数默认值调优建议影响范围fadeDistance50地图大则调大80小则调小30边界减速距离errorResetThreshold600高分辨率屏调大800低分辨率调小400误差重置频率sampleInterval1低端机设为2高端机保持1CPU 占用与流畅度平衡PlayerAnchor.width/height64必须 ≥ 主角最大尺寸否则 Widget 锚点失效UI 元素定位精度5. 常见问题与排查技巧实录那些文档里不会写的坑5.1 问题速查表10个高频问题与一键修复问题现象根本原因修复命令/操作验证方式主角在屏幕中心但血条Widget偏左10pxPlayerAnchor的width/height小于血条节点尺寸Widget 锚点计算失准将PlayerAnchor.width设为血条节点width的1.2倍血条紧贴主角头顶无偏移地图滚动时背景图出现撕裂/闪烁Background层未启用useGrayscale导致合批失败选中Background节点 → 属性检查器 →Sprite→ 勾选useGrayscale撕裂消失DrawCall 减少1个主角跳跃时摄像机突然下移一帧PlayerController在update中修改 position而WorldManager在lateUpdate中读取存在单帧延迟在PlayerController.update结尾添加 this.node._dirtyFlags Node.DirtyFlag.TRANFORM; this.node.updateWorldTransform();MapBoundary设置后无衰减效果MapBoundary节点未挂载在WorldLayer下或WorldManager未正确引用确保MapBoundary是WorldLayer的直接子节点并在WorldManager属性中重新拖入主角靠近边界时WorldLayer 移动速度明显变慢构建 Android 后主角完全不动PlayerController的input.on未监听Input.EventType.TOUCH_START移动端触控未注册在PlayerController.start()中添加input.on(Input.EventType.TOUCH_START, this.onTouchStart, this)并实现onTouchStart方法手指触摸屏幕主角开始移动WorldManager报错Cannot read property getBoundingBox of nullmapBoundary属性未在编辑器中赋值且代码未做空值检查在WorldManager.lateUpdate开头添加if (!this.mapBoundary) return;错误日志消失功能正常主角快速转向时画面“抽搐”PlayerController中moveDir未归零导致上一帧方向残留在onKeyDown中每次按键前先this.moveDir.set(0,0,0)转向瞬间响应无拖拽感WorldLayer移动后粒子特效位置错误粒子节点挂载在WorldLayer下但未设置ignoreContentAdaptWithSizefalse选中粒子节点 →ParticleSystem组件 → 取消勾选Ignore Content Adapt With Size粒子始终围绕目标节点发射多主角切换后新主角不居中WorldManager未重置

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑