一、概念
1.錨點Anchor
錨點由 anchorX 和 anchorY 兩個值表示,他們是通過節點尺寸計算錨點位置的乘數因子,范圍都是 0 ~ 1 之間。(0.5, 0.5) 表示錨點位于節點長度乘 0.5 和寬度乘 0.5 的地方,即節點的中心。
錨點屬性設為 (0, 0) 時,錨點位于節點本地坐標系的初始原點位置,也就是節點約束框的左下角。
2.坐標軸方向
x軸都是向右的,y軸是向上的。
3.坐標軸原點
這里比較有特色了。
先說世界坐標系。世界坐標系也叫做絕對坐標系,在 Cocos Creator 游戲開發中表示場景空間內的統一坐標體系,「世界」就用來表示我們的游戲場景。世界坐標系的原點在屏幕的左下角。
然后就是本地坐標系。本地坐標系也叫相對坐標系,是和節點相關聯的坐標系。每個節點都有獨立的坐標系,當節點移動或改變方向時,和該節點關聯的坐標系將隨之移動或改變方向。
所有子節點就會以 錨點所在位置 作為坐標系原點,注意這個行為和 cocos2d-x 引擎中的默認行為不同,是 Cocos Creator 坐標系的特色!
二、驗證
創建了一個ppp的scene,然后自動創建了一個Canvas,Canvas是處于世界坐標系當中的。
這時候再拖進去一張圖片,設置Anchor為0.5,0.5,坐標為0,0。因為這張圖也處于世界坐標系中,所以就會顯示在左下角。并且是圖片的中心點處于左下角,這也說明Anchor決定了一張圖片以哪個像素點做為自己的位置參考點。比如,把Anchor改為0,0。此時這張圖片的左下角會和世界坐標系的左下角對齊。
1.canvas
繼續來做驗證,Canvas寬高設置為960,640。Anchor設置為0.5,0.5。這表明,當我們設置它的位置時,實際上它的參考點是中心點,也就是480,320那個像素。Canvas本身處于世界坐標系,如果想讓它居中,那么Position應該設置多少呢?很明顯,相對于左下角的坐標原點,正是480,320。
2.canvas中的子節點
再拖進去一個圖片,設置Anchor為0.5,0.5。然后設置Position為0,0。此時,特色系統發揮作用了,canvas的坐標原點在其Anchor位置,所以新拖的圖片會被顯示在正中心。
這個時候,用肉眼確認一下這張新圖片,在世界坐標系中的坐標值,很顯然是480,320。然后掛個腳本組件,把這張圖片命名為pic1,使用API再驗證一下:
cc.log(this.pic1.convertToWorldSpaceAR(cc.v2(0,0)));
convertToWorldSpaceAR的作用是,將節點坐標系下的一個點轉換到世界空間坐標系。
如果此時,把pic1的Anchor改為0,0。在顯示上確實不一樣了,圖片會偏向右上方。但是convertToWorldSpaceAR打印的,仍然是480,320。如果此時把pic1的Position改為300,0。圖片會向右方移動,convertToWorldSpaceAR打印的,會變成780,320。
3.再嵌入一個子節點
把pic1里面,再嵌入一個小星星圖片叫star。當設置pic1的Anchor為0,0。設置star的Anchor為0,0;Postion為0,0時,發現star確實是在pic1的左下角。這驗證了,子節點以 錨點所在位置 作為坐標系原點。這時同樣可以驗證star在世界坐標系的位置:cc.log(this.pic1.getChildByName("star").convertToWorldSpaceAR(cc.v2(0,0)));
三、為什么搞成這樣子
參考位置系統對程序員來說極度不友好,摘抄一部分過來:
Q:原來寫一個需要居中的節點node.setPosition(parent.width/2,parent.height/2)
,現在需要寫的 node.setPosition(parent.width*(0.5-parent.anchorX),parent.height*(0.5-parent.anchorY))
。現在的很容易忘記寫現在的這種寫法 一旦父節點的錨點不在中心點,位置就不會居中。
比如有一組卡牌,有規律排列, 原來通過獲取坐標很容易得出是第幾張, 現在這是不容易的, 如果不考慮父節點錨點就會出錯. 而錨點除了運動外是常被忽略的, 如果后期不經意變動,那就會出問題.
A:居中的節點請讓美術直接在子節點上加 Widget,設置對齊到 horizontal center 或 vertical center 就可以了,不需要再寫代碼了(而且你的代碼只能對齊一次,Widget 可以在運行時保持對齊狀態)
忘記需要考慮錨點位置是改變規則以后的正常現象,請多一點耐心,轉換一下思路,相信很快就能看到新規則帶來的各種好處(尤其是編輯器里拼場景時)。
關于你說的通過位置獲取索引的問題,我理解你從以前經驗中產生的需求是「隨手設置一個可以讓父節點旋轉的點作為錨點,子節點排列不受錨點影響」,但現在整個工作流都重新設計過了,比如卡牌索引的問題,應該是動態創建的時候就保存好索引,這樣根本不用去算位置;而需要旋轉父節點時,只要知道子節點會圍繞父節點的錨點旋轉,再根據需要重新設置好錨點位置就可以了吧?
以編輯器為核心搭建 UI 的工作流程和以前用代碼寫 UI 的流程是完全不同的,所以需要修改一些系統來更好的匹配現在的工作流程。對于舊項目如何移植,我們之后會推出更多案例給大家參考。
我再總結一下錨點的作用。
錨點的作用無非兩個,標識美術關鍵元素和方便策劃布局。
標識美術關鍵點:
這種錨點是由美術在場景里設置的,就像我前面舉例的,十字準心和箭頭。
此時父節點旋轉,子物體必須繞著父物體錨點動 (現在滿足,原先滿足)。
父節點錨點位置如果變化,子物體應該自動跟著移動 (現在滿足,原先不滿足)。方便策劃布局:
如果是卡牌背景圖,按鈕,圖標這些 UI 元素,美術是無所謂錨點在哪的,一般都是策劃在編輯器里根據布局需要設置好的。
我們現在鼓勵的是策劃使用 widget 來做定位,策劃其實不再需要通過 anchor 來做布局了。
就算要用 anchor 布局,如果 anchor 需要變,一般是由于 UI 有了新的布局需求,本來就不是改一個 anchor 能解決的事情,更不會有策劃手賤去改一個本來設得好好的 anchor。
就算改錯了,策劃也不可能不預覽結果,有錯誤他自己會及時在編輯器下處理,不需要經過程序解決。
所以你帖子標題說的“位置系統對程序員來說極度不友好”應該是不成立的。
四、converToWorldSpaceAR和converToWorldSpace區別
參考 使用convertToNodeSpace()的一個問題
node.convertToWorldSpace(cc.p(0,0))
是將node的0,0(忽略錨點,也就是node的左下角)這個位置轉換為世界坐標,所以得到的位置應該是node的左下角相對于世界坐標系原點的偏移量。如果你要獲取node在世界坐標系的位置,應該使用Canvas.converToWorldSpaceAR(node),這樣才能獲取node在世界坐標系的position
關于這一點,看一下源碼,更清楚:
//CCNode.js:
* !#zh 將一個相對于節點左下角的坐標位置轉換到世界空間坐標系。
* 這個 API 的設計是為了和 cocos2d-x 中行為一致,
* 更多情況下你可能需要使用 convertToWorldSpaceAR
* @method convertToWorldSpace
* @param {Vec2} nodePoint
* @return {Vec2}
* @example
* var newVec2 = node.convertToWorldSpace(cc.v2(100, 100));
*/
convertToWorldSpace (nodePoint) {
this._updateWorldMatrix();
let out = new cc.Vec2(
nodePoint.x - this._anchorPoint.x * this._contentSize.width,
nodePoint.y - this._anchorPoint.y * this._contentSize.height
);
return vec2.transformMat4(out, out, this._worldMatrix);
},
* !#zh
* 將節點坐標系下的一個點轉換到世界空間坐標系。
* @method convertToWorldSpaceAR
* @param {Vec2} nodePoint
* @return {Vec2}
* @example
* var newVec2 = node.convertToWorldSpaceAR(cc.v2(100, 100));
*/
convertToWorldSpaceAR (nodePoint) {
this._updateWorldMatrix();
let out = new cc.Vec2();
return vec2.transformMat4(out, nodePoint, this._worldMatrix);
},
五、實例
參考快速上手:制作第一個游戲 摘星星,這里可以下載最終完成的項目
1.Game.js
//Game.js
onLoad: function () {
// 獲取地平面的 y 軸坐標
this.groundY = this.ground.y + this.ground.height/2;
...
spawnNewStar: function() {
var newStar = null;
// 使用給定的模板在場景中生成一個新節點
if (this.starPool.size() > 0) {
// this will be passed to Star's reuse method
newStar = this.starPool.get(this);
} else {
newStar = cc.instantiate(this.starPrefab);
}
// 將新增的節點添加到 Canvas 節點下面
this.node.addChild(newStar);
// 為星星設置一個隨機位置
newStar.setPosition(this.getNewStarPosition());
...
getNewStarPosition: function () {
// if there's no star, set a random x pos
if (!this.currentStar) {
this.currentStarX = (Math.random() - 0.5) * 2 * this.node.width/2;
}
var randX = 0;
// 根據地平面位置和主角跳躍高度,隨機得到一個星星的 y 坐標
var randY = this.groundY + Math.random() * this.player.jumpHeight + 50;
// 根據屏幕寬度和上一個星星的 x 坐標,隨機得到一個新生成星星 x 坐標
var maxX = this.node.width/2;
if (this.currentStarX >= 0) {
randX = -Math.random() * maxX;
} else {
randX = Math.random() * maxX;
}
this.currentStarX = randX;
// 返回星星坐標
return cc.v2(randX, randY);
},
這個Game.js腳本,是掛在Canvas上的。也就是說this.node.addChild添加星星時,實際上添加到了Canvas坐標系中。
地平面groundY計算時,也就是找地面圖片最頂端的坐標,因為上面說了,Anchor決定了一張圖片以哪個像素點做為自己的位置參考點,這里ground的Anchor設置了0.5,0.5。所以
this.groundY = this.ground.y + this.ground.height/2
randY就是地平面加上,跳躍高度最大值,中間的一個隨機值。后面還有個+50是為什么呢?可以測試一下,添加
randY = this.groundY;
,也就是Math.random()取0時,如果不加50,randY就是地平面的位置。此時因為添加的星星star的Anchor設置了0.5,0.5,將會出現星星有一半埋在地下線下面,顯然不合理。maxX 這里Canvas的坐標原點就在屏幕中心,所以randX是分正負的,也就是
var maxX = this.node.width/2;randX = -Math.random() * maxX;
當然,為了游戲更合理,randX總是會出現在上一次的位置坐標完全相反的半軸,這是通過currentStarX 這個變量來標記的。
2.Player.js
// called every frame
update: function (dt) {
// 根據當前加速度方向每幀更新速度
if (this.accLeft) {
this.xSpeed -= this.accel * dt;
} else if (this.accRight) {
this.xSpeed += this.accel * dt;
}
// 限制主角的速度不能超過最大值
if ( Math.abs(this.xSpeed) > this.maxMoveSpeed ) {
// if speed reach limit, use max speed with current direction
this.xSpeed = this.maxMoveSpeed * this.xSpeed / Math.abs(this.xSpeed);
}
// 根據當前速度更新主角的位置
this.node.x += this.xSpeed * dt;
// limit player position inside screen
if ( this.node.x > this.node.parent.width/2) {
this.node.x = this.node.parent.width/2;
this.xSpeed = 0;
} else if (this.node.x < -this.node.parent.width/2) {
this.node.x = -this.node.parent.width/2;
this.xSpeed = 0;
}
},
Player.js也是掛在Canvas上的。這里有邊緣檢測,如何判斷橫向移動出界:this.node.x > this.node.parent.width/2