拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Cocos Creator坐标系详解:从本地、世界到屏幕坐标的转换与实战

1. 从一次“诡异”的Bug说起为什么我的精灵跑偏了那天下午我正在调试一个简单的点击交互功能玩家点击屏幕任意位置一个角色精灵就会“闪现”到那个位置。逻辑看起来很简单监听触摸事件获取触摸点的坐标然后把这个坐标赋值给精灵的position属性。我信心满满地运行起来点击屏幕中央结果角色“嗖”地一下飞到了屏幕右上角差点飞出画布。我反复检查了触摸事件回调里的代码event.getLocation()获取坐标node.position pos赋值逻辑清晰毫无问题。问题出在哪这就是典型的坐标系认知混淆导致的“事故现场”。在 Cocos Creator 里一个简单的position属性背后其实牵扯到至少三套不同的坐标系系统节点本地坐标系、世界坐标系和屏幕坐标系。如果你没搞清楚它们之间的关系和转换规则就会像我一样遇到精灵位置飘忽不定、UI元素对不齐、点击检测失效等各种看似“诡异”的问题。很多新手开发者觉得坐标系抽象难懂其实只要抓住几个核心概念和它们之间的转换关系一切都会变得清晰。这篇文章我就结合自己踩过的坑和项目经验帮你彻底理清 Cocos Creator 中的坐标系让你在开发中不再为此迷糊。2. 坐标系家族三位核心成员与它们的职责要理解坐标系我们可以把它想象成描述一个物体位置的“语言系统”。在不同的语境坐标系下描述同一个位置的“说法”是完全不同的。在 Cocos Creator 的场景中主要有三位核心成员。2.1 节点本地坐标系以父节点为原点的“家庭地址”这是最基础、也最常用的一套坐标系。每个节点Node都有一个属于自己的本地坐标系。在这个坐标系里原点 (0, 0)永远位于该节点的锚点Anchor所在的位置。这是理解本地坐标的关键一个节点的position属性描述的就是该节点的锚点在其父节点的本地坐标系中的坐标。坐标轴X轴向右为正Y轴向上为正。这是 Cocos Creator 的默认设置也是2D游戏领域的常见约定。举个例子假设我们有一个名为Player的精灵节点它挂在一个名为GameLayer的节点下。如果Player的position是 (100, 50)它的锚点是默认的 (0.5, 0.5) 即中心点。那么它的含义是Player精灵的中心点位于其父节点GameLayer本地坐标系中的 (100, 50) 这个位置。注意节点的position属性永远是其锚点在父节点坐标系中的位置。改变锚点节点的视觉位置不会变但position值的含义变了。比如锚点从中心 (0.5,0.5) 改为左下角 (0,0)那么同样的position(100,50) 表示的就是节点的左下角在父坐标系中的位置节点整体会向右上方移动。2.2 世界坐标系绝对的“宇宙标准”如果说本地坐标系是“家庭地址”相对于父亲的家那么世界坐标系就是“GPS经纬度”相对于整个场景的绝对位置。它是整个场景树的根节点通常叫 Canvas 或一个顶层节点的本地坐标系被所有节点共享作为绝对参考。原点 (0, 0)位于场景根节点的锚点位置通常是 Canvas 的中心或左下角取决于 Canvas 的适配模式。作用当我们需要知道一个节点在屏幕上的绝对位置或者需要让两个没有直接父子关系的节点进行位置交互时就必须使用世界坐标。继续上面的例子Player节点的世界坐标描述的是Player的锚点在世界坐标系即根坐标系中的位置。这个值不能直接通过属性获取需要通过计算或 API 转换得到。2.3 屏幕坐标系与设备窗口直接挂钩的“像素地图”这是最贴近我们感知的坐标系它直接对应着设备屏幕的像素网格。原点 (0, 0)在 Cocos Creator 中默认位于屏幕的左下角。这一点非常重要且与一些其他图形库如某些原生UI系统原点在左上角不同。坐标轴X轴向右为正Y轴向上为正。单位像素px。它的范围取决于屏幕分辨率例如一个 1920x1080 的设备X轴范围是 0~1920Y轴范围是 0~1080。所有用户的输入事件如触摸Touch、鼠标点击Mouse其位置信息最初都是以屏幕坐标的形式提供的。这就是我开头那个Bug的根源我从触摸事件拿到的是屏幕坐标 (比如点击了屏幕中央 (960, 540))却直接把它赋值给了节点的position。而position期望的是父节点本地坐标两者数值和参考系天差地别节点自然就“飞”了。3. 坐标系间的“翻译官”核心转换API详解理解了三种坐标系的含义下一步就是掌握它们之间互相转换的方法。Cocos Creator 在cc命名空间下提供了非常完善的转换API它们就是你的“坐标翻译官”。3.1 本地坐标 世界坐标这是最频繁的转换之一。核心方法是节点实例上的convertToWorldSpaceAR和convertToNodeSpaceAR。我强烈推荐使用带ARAnchor Relative后缀的版本因为它们以节点的锚点为参考点更符合直觉。node.convertToWorldSpaceAR(localPos): 将相对于本节点锚点的本地坐标localPos转换到世界坐标系。// 假设 player 是一个节点锚点在中心 let localOffset cc.v2(50, 0); // 表示在player自身坐标系中其锚点右边50像素的位置 let worldPos player.convertToWorldSpaceAR(localOffset); // worldPos 现在是一个世界坐标系下的坐标表示player中心点右侧50像素的世界位置这个API常用于计算一个节点特定点如武器尖端的世界位置用于碰撞检测或特效生成。node.convertToNodeSpaceAR(worldPos): 将一个世界坐标worldPos转换到本节点坐标系中相对于本节点锚点。let worldPos cc.v2(200, 300); // 一个已知的世界坐标 let localPosInPlayer player.convertToNodeSpaceAR(worldPos); // localPosInPlayer 表示 worldPos 这个点在player节点坐标系中位于其锚点的什么方位 // 如果结果是 (10, -20)表示该点在player中心的右方10像素下方20像素处。这个API常用于判断一个世界空间中的点如另一个物体的位置相对于当前节点的方位。实操心得convertToWorldSpace和convertToNodeSpace不带AR是以节点本地坐标系原点即节点位置position所描述的点为参考进行转换的容易产生混淆。在绝大多数涉及节点自身偏移的场景下使用带AR的版本更不容易出错。记住AR版本参考锚点非AR版本参考本地坐标系原点。3.2 世界坐标 屏幕坐标由于屏幕坐标系与设备相关而世界坐标系是逻辑坐标它们之间的转换需要通过摄像机Camera来完成。在 Cocos Creator 中通常主摄像机负责这个映射。世界坐标转屏幕坐标camera.getWorldToScreenPoint(worldPos)let worldPos player.convertToWorldSpaceAR(cc.v2(0, 0)); // 获取player中心的世界坐标 let screenPos this.camera.getWorldToScreenPoint(worldPos); // screenPos 现在是一个Vec3其x和y分量就是对应的屏幕像素坐标。这个API常用于在屏幕上绘制跟随物体的UI提示如血条、名字需要知道物体在屏幕上的像素位置。屏幕坐标转世界坐标camera.getScreenToWorldPoint(screenPos)let screenPos cc.v2(touchX, touchY); // 触摸点的屏幕坐标 let worldPos this.camera.getScreenToWorldPoint(screenPos, new cc.Vec3()); // worldPos 现在是一个世界坐标系下的坐标表示这条从摄像机出发的射线在游戏世界Z0平面上的交点。这就是解决我开头那个Bug的关键触摸事件给的坐标是屏幕坐标必须通过摄像机转换成世界坐标才能被游戏世界中的节点理解。// 正确的点击移动代码 onTouchMove(event: cc.Event.EventTouch) { let screenPos event.getLocation(); // 获取屏幕坐标 let worldPos this.mainCamera.getScreenToWorldPoint(screenPos, new cc.Vec3()); // 通常我们还需要将世界坐标转换到目标节点的父节点坐标系 let localPosInParent this.player.parent.convertToNodeSpaceAR(worldPos); this.player.position localPosInParent; }注意事项getScreenToWorldPoint返回的是一个三维向量 (Vec3)。在2D游戏中我们通常只关心X和Y分量Z分量默认为0。这个方法本质上是计算一条从摄像机出发穿过屏幕指定像素点的射线与游戏世界中某个Z值平面默认是Z0的交点。如果你的游戏节点不在Z0的平面上需要做进一步处理。3.3 UI 节点坐标系一个特殊的本地坐标系对于挂在 Canvas 下的 UI 节点情况有些特殊。Canvas 通常采用适配模式如 FIT_WIDTH, FIT_HEIGHT它的尺寸和锚点会影响其子节点的坐标系。UI节点的position依然是其锚点在父节点可能是Canvas或其他UI节点本地坐标系中的位置。但这个坐标系的单位不是像素而是设计分辨率下的单位。例如你的设计分辨率是 1920x1080一个UI节点的 position 是 (960, 540)那么无论屏幕实际分辨率如何它都会位于其父节点坐标系的正中央。UI节点与屏幕坐标由于 Canvas 已经处理了屏幕适配将 UI 节点坐标转换到屏幕坐标通常更简单。你可以直接使用node.parent.convertToWorldSpaceAR(node.position)获取该节点锚点的世界坐标对于UI树这个世界坐标的原点通常在Canvas中心或左下角取决于设置然后再用摄像机转换到屏幕坐标。但对于点击检测Cocos Creator 的 UI 事件系统已经帮我们做好了转换我们通常直接用event.getLocation()获取的屏幕坐标与 UI 节点的包围盒进行比较即可。4. 锚点本地坐标系的“原点”与视觉控制的关键前面多次提到锚点Anchor它是理解本地坐标系的钥匙。锚点是一个归一化的值 (Vec2)范围是 [0, 1]它定义了节点本地坐标系原点在其自身纹理或尺寸范围内的相对位置。(0, 0)左下角(0.5, 0.5)中心默认值(1, 1)右上角锚点的核心影响决定position属性的参考点node.position描述的是节点锚点在父坐标系中的位置。改变锚点节点的position值虽然不变但节点的视觉位置会瞬间改变因为参考点变了。影响子节点的布局原点一个节点的本地坐标系原点就在其锚点上。所有子节点的position都是相对于这个原点来定位的。如果你把锚点从中心改到左下角那么所有子节点都会整体向右上方移动因为它们的“爸爸的家”的原点挪到了左下角。简化布局与动画定位想让一个按钮始终贴在屏幕左下角将它的锚点设为 (0, 0)position设为 (0, 0) 即可。无论屏幕如何缩放它都会牢牢钉在左下角。缩放与旋转所有变换缩放、旋转都是围绕锚点进行的。将锚点设为 (0.5, 0.5)缩放时节点会以其中心均匀放大缩小将锚点设为 (0, 0)缩放时节点会像从左下角“生长”或“收缩”一样。避坑技巧在编辑器中频繁调整节点位置时如果发现节点“飘走”首先检查是不是误点了锚点工具并移动了锚点。保持一个良好的习惯先确定锚点这个节点打算以哪个点作为定位和变换的参考再设置位置。对于需要精细对齐的UI使用锚点预设比手动调整position更可靠。5. 实战演练典型场景下的坐标系应用与问题排查理论说再多不如实际操练。下面我们通过几个典型场景串联起坐标系知识的应用。5.1 场景一实现物体跟随鼠标触摸移动这是开头Bug的正解。步骤分解获取输入坐标通过触摸/鼠标事件监听使用event.getLocation()获得屏幕坐标。转换到世界坐标使用主摄像机camera.getScreenToWorldPoint()将屏幕坐标转换为世界坐标。注意处理返回的 Vec3我们通常取其 x, y。转换到目标父节点坐标将世界坐标转换到移动目标节点其父节点的本地坐标系中。使用targetNode.parent.convertToNodeSpaceAR(worldPosVec2)。赋值将上一步得到的本地坐标赋值给目标节点的position。// 假设 this.node 是跟随鼠标的精灵this.camera 是主摄像机 onMouseMove(event: cc.Event.EventMouse) { // 1. 获取屏幕坐标 let screenPos cc.v2(event.getLocationX(), event.getLocationY()); // 2. 转换为世界坐标 (Vec3) let worldPosVec3 this.camera.getScreenToWorldPoint(screenPos, new cc.Vec3()); let worldPosVec2 cc.v2(worldPosVec3.x, worldPosVec3.y); // 3. 转换为父节点本地坐标 let localPosInParent this.node.parent.convertToNodeSpaceAR(worldPosVec2); // 4. 更新位置 this.node.position localPosInParent; }5.2 场景二在物体头顶生成一个世界UI如伤害数字我们希望伤害数字飘在怪物头顶但伤害数字本身是UI节点可能挂在Canvas下。获取怪物头顶的世界坐标先计算怪物头顶相对于其锚点的本地偏移如cc.v2(0, 怪物身高/2 10)然后使用monster.convertToWorldSpaceAR()转换为世界坐标。将世界坐标转换为UI坐标系伤害数字的父节点可能是Canvas或某个UI层。使用uiParentNode.convertToNodeSpaceAR(worldPos)将世界坐标转换到该UI父节点的本地坐标系。设置伤害数字位置将转换后的坐标赋值给伤害数字节点的position。// 生成伤害数字 spawnDamageNumber(damage: number, monster: cc.Node) { // 1. 计算怪物头顶世界坐标 let localOffset cc.v2(0, monster.height / 2 20); let worldPos monster.convertToWorldSpaceAR(localOffset); // 2. 创建伤害数字节点假设是预制体 let damageNode cc.instantiate(this.damagePrefab); damageNode.getComponent(cc.Label).string -${damage}; // 3. 将伤害数字添加到UI层this.uiLayer 是Canvas下的一个节点 this.uiLayer.addChild(damageNode); // 4. 将世界坐标转换到UI层的坐标系 let uiLocalPos this.uiLayer.convertToNodeSpaceAR(worldPos); damageNode.position uiLocalPos; // 5. 添加一个上浮动画... }5.3 场景三判断点击是否落在某个精灵上非UI节点Cocos Creator 的cc.Node有getBoundingBox()方法但它返回的是节点在其父坐标系下的轴对齐包围盒AABB。不能直接用屏幕坐标和它比较。获取触摸点的世界坐标同场景一camera.getScreenToWorldPoint(touchPos)。获取精灵的世界包围盒先通过node.getBoundingBox()获取本地包围盒这是一个矩形cc.Rect。但这个矩形的原点 (x, y) 是相对于节点本地坐标系原点的。我们需要的是这个矩形在世界坐标系中的表示。一个简单的方法是计算包围盒四个角的世界坐标。let localBox node.getBoundingBox(); // 计算包围盒四个角的本地坐标相对于节点原点 let bottomLeft cc.v2(localBox.x, localBox.y); let topRight cc.v2(localBox.x localBox.width, localBox.y localBox.height); // 将角点转换到世界坐标 let worldBottomLeft node.convertToWorldSpaceAR(bottomLeft); let worldTopRight node.convertToWorldSpaceAR(topRight); // 现在可以构造一个世界坐标系下的矩形 let worldBox cc.rect(worldBottomLeft.x, worldBottomLeft.y, worldTopRight.x - worldBottomLeft.x, worldTopRight.y - worldBottomLeft.y);判断包含使用worldBox.contains(worldTouchPos)判断触摸点世界坐标是否在矩形内。更优方案对于复杂形状或性能要求高的场景可以使用物理引擎如内置的Box2D的碰撞检测或者为精灵添加cc.PolygonCollider组件使用cc.Intersection.pointInPolygon方法进行精确判断。但对于简单的矩形/圆形按钮上述包围盒方法足够用。6. 常见问题排查清单与调试技巧当你遇到位置相关的问题时可以按以下清单逐一排查问题节点位置不对或者点击没反应。[ ]检查锚点确认节点的锚点设置是否符合你的预期。你是不是想以中心定位但锚点却在左下角[ ]检查父节点节点的position是相对于其直接父节点的。确认它的父节点是不是你想象的那一个。父节点的位置、缩放、旋转都会影响子节点。[ ]检查坐标系转换如果你在处理输入触摸/鼠标是否忘记了将屏幕坐标转换为世界坐标是否忘记了将世界坐标转换到正确的父节点本地坐标[ ]检查摄像机用于屏幕/世界坐标转换的摄像机引用是否正确是否是渲染目标场景的主摄像机[ ]使用调试绘制在update中临时绘制调试信息是终极法宝。update(dt) { // 1. 绘制节点的世界包围盒 let worldBox ...; // 计算方法见上文 this.drawRect(worldBox, cc.Color.RED); // 2. 绘制触摸点世界位置 if (this.lastTouchWorldPos) { this.drawDot(this.lastTouchWorldPos, cc.Color.GREEN); } }你可以写一个简单的调试脚本用cc.Graphics组件来画线、画矩形、画点直观地看到坐标系和边界。问题UI元素在不同分辨率下错位。[ ]检查Canvas适配模式Canvas组件的Fit Height、Fit Width等设置会影响根节点的实际尺寸和子节点的坐标系缩放。[ ]使用锚点布局而非硬编码位置对于需要适配的UI尽量使用锚点Anchor和边距Left, Right, Top, Bottom来定位而不是设置固定的position值。[ ]确认设计分辨率在Canvas上设置的设计分辨率是所有UI节点本地坐标的参考。确保你的UI布局基于此分辨率设计。问题旋转或缩放后子节点位置乱了。[ ]理解变换中心记住缩放和旋转是围绕节点的锚点进行的。如果子节点的布局依赖于父节点的尺寸父节点缩放后子节点的相对位置position虽然数值没变但视觉距离会同比变化。如果需要子节点保持与父节点某条边的固定距离应该使用锚点约束而不是固定position。坐标系是游戏开发空间思维的基石。初学时会觉得绕但一旦理解了“本地坐标相对于父节点锚点”、“世界坐标是绝对参考”、“屏幕坐标来自输入且需摄像机转换”这三层关系并通过convertToWorldSpaceAR、convertToNodeSpaceAR和摄像机的getScreenToWorldPoint这几个核心API进行串联所有位置问题都能迎刃而解。多写调试代码把看不见的坐标和边界用图形画出来是加深理解最快的方式。当你下次再遇到精灵“乱飞”或者点击“失灵”时希望你能淡定地打开这篇文章按照清单一步步锁定问题所在。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门