cocos2d-xの座標操作まとめ(convertToNodeSpace、convertToWorldSpace他)

Pocket

convertto1

cocos2d-xでは、子ノードの座標は親ノードからの相対座標によって管理されています。

一方でタッチイベントは絶対座標によって管理されています。そこでcocos2d-xでは絶対座標を相対座標に、相対座標を絶対座標に変換できる関数がそれぞれ用意されているのですが、ときどき挙動が思ったのと違ったりすることがあるので、それぞれぼくなりにまとめてみました。

convertkankei

テキトーな画像で恐縮ですが。

親ノードと子ノードの関係

cocos2d-xの座標は、基本全て「親ノードからの相対座標」によって管理されます。
(若干違うかもしれないけど便宜的にそう思ってても問題ないっぽい)

まずCCSceneが一番の親に位置します。
CCSceneの座標開始位置は画面左下を(0,0)とした絶対座標と重なっています。そのため、CCScene直下に置いたものは(変なことをしない限り)絶対座標と同じ値で操作することができます。

※基本的にCCSceneの直下にはCCLayerを配置すると思うし、CCLayerも画面左下に座標位置が重なっているケースが殆どだと思うので、結果的にCCLayer直下のものも絶対座標で操作することができます。

たとえば上の画像を例にすると、ひとつのCCScene直下のCCLayerの中に矩形Aと矩形Bを入れたとします。
その時矩形Aの位置と矩形Bの位置は、それぞれ左下の(0,0)を基準とした絶対座標を返します。

補足:アンカーポイントに関して

上の図で点Aと点Bはそれぞれの矩形の中心に書かれていますが、この点の位置のことを「アンカーポイント(anchorPoint)」と言います。
cocos2d-xでは、全てのノードの座標はこのアンカーポイントによって管理されてます。

anchorpointこれはノードの左下を(0,0)とし、右上を(1,1)としたそのノードの基準点で、デフォルトは中心(0.5 , 0.5)に設定されています。setPositionで設定した位置はこの点の位置が設定されますし、getPositionではこの点の座標が返されます。

  • anchorPointはそのノードの基準点
  • オブジェクトの位置は、この基準点をもとに決定されます。
  • オブジェクトの回転、拡大や縮小もこの点を中心に行われます。

アンカーポイントの変更は簡単です。

//基準点を右上に設定する
CCPoint anchorPoint = ccp(1.0f,1.0f);
node->setAnchorPoint(anchorPoint);

補足2:CCLayerとCCSceneのアンカーポイントの挙動

上のように、anchorPointはデフォルトで(0.5,0.5)で指定されています。
それはCCLayerやCCSceneでも同様なのですが、実はこの2つに関しては、アンカーポイントをどう設定してもポジションは左下の位置が返されます

これは生成時に「ポジションに関してはアンカーポイント無視スール変数(m_bIgnoreAnchorPointForPosition)」がtrueに設定されているからで、それをfalseに設定することでCCSpriteと同じような挙動をさせることもやろうと思えばできます。

//CCLayerのポジションの挙動をCCSpriteと同じようにする
CCLayer* layer = CCLayer::create();
layer->ignoreAnchorPointForPosition(false);

以降、スプライトのアンカーポイントは(0.5 , 0.5)で中心を返し、CCLayerは左下を返すという前提で書いていきます。

cocos2d-xで相対座標を扱うケース

補足が長くなりましたが、要はCCLayerの直下にCCSpriteを入れてるだけの場合は、全て絶対座標と同じように扱うことができてそんなに何も気にしなくても作っていけます。

ただ以下のようなことをする場合は、親ノードからの相対座標で扱っていく必要が生じます。

  • CCSpriteの中にCCNodeを入れた場合
  • 親ノード(CCLayerあるいはCCScene)の位置をずらした場合

CCSpriteの中にCCSpriteを入れた場合

上にあるテキトーな図をまた例にし、矩形Aの中に矩形Bを入れた時のことを考えてみます。

その時、BはA’を(0,0)とした座標からの位置が返ってきますし、位置をセットする際もそうしないといけません。
うっかり絶対座標でBの位置を設定し、矩形AにaddChildしようものなら、Bをまるで迷子の子羊みたいな場所に寂しく佇ませてしまうという罪な光景を目にしなければならなくなるでしょう。

Bの座標位置 = (Bの絶対座標 – A’の絶対座標)

まあ、こんな座標の決め方は面倒だから絶対座標基準で考えず、常に親ノードの左下が基準になるんだと考えて設定していく方が作りやすいですよね。

どういう間違いがおきてかCCSpriteの中にCCLayerを入れちゃった場合も、基本は親ノードの左下が基準って考えとくと位置決定はしやすいですね。
ただ上の補足にも書いたようにCCLayerは「左下のポイント」が基準点となるので、そこだけ注意が必要です。
(あるいはlayer->ignoreAnchorPointForPosition(false);とすれば中心を基準にできます)

convertToNodeSpace(絶対座標を相対座標に変換する)

さて、やっとタイトルに書いた関数まで行き着きました。
上に書いたように、CCSprite[A]の中にCCSprite[B]を入れる場合、Bの座標はA’からの相対位置を指定する必要があります。

ただタッチイベントは全て絶対座標で返ってきます。なので、行いたいアクションによっては絶対座標を相対座標に変換する必要があります。その際にはこのconvertToNodeSpaceを使うと便利です。

//$touch = CCTouch*
//spr = CCSprite*
const CCPoint pos = $touch->getLocation();
//そのスプライトの左下を基準にした相対座標に変換する
const CCPoint relativePos = spr->convertToNodeSpace(pos);

//上のと下のコードは結果が同じ
const CCPoint relativePos = spr->convertTouchToNodeSpace($touch);

ちなみに、似たようなのでconvertToNodeSpaceARという関数もありますが、上記は「左下を基準(0,0)とした相対座標」となるのに対して、これは「そのノードのアンカーポイントを基準とした相対座標」が返されるという差があります。

またconvertTouchToNodeSpace(convertTouchToNodeSpaceAR)というのもあり、タッチイベントに関してはそれを使う方がコードが1行で済む分楽ですね。

  • convertToNodeSpace(CCPoint);  そのノードの左下からの相対座標
  • convertToNodeSpaceAR(CCPoint);アンカーポイントからの相対座標
  • タッチ座標を取得するだけならconvertTouchToNodeSpace

convertToWorldSpace(親ノードからの相対座標を絶対座標に変換)

convertToNodeSpaceと対をなすのがこのconvertToWorldSpaceです。
使う際に注意しないといけないのが、親ノードで指定しないといけないということです。
(そうしないとよくわからない数値が返ってきます——理由は一番下に追記で記載しました)

//相対座標から絶対座標を得る
const CCPoint pos = spr->getPosition();
CCNode* parent = spr->getParent();
if(parent){
	const CCPoint absolutePoint = parent->convertToWorldSpace(pos);
}

ちなみに、CCSpriteをいくつも入れ子状態にしても、返ってくる数値は絶対座標みたいです。
それなら「親ノードから選択しなくても済むようにしてよ〜」って思わなくもありませんが、そんな文句は壁に向かって言っとけですよね。そうします。

また、これにもconvertToWorldSpaceARという関数が存在するのですが、これもなんだかよくわからない数値が返ってきます。

色々と調べてみたら、もしかしたら「parent::convertToWorldSpace(self::getPosition()) = self::convertToWorldSpaceAR(self::getPosition()) – self::getPosition()」かもしれないです。

//もしかしたら上のコードと下は結果が同じ?
const CCPoint pos = spr->getPosition();
const CCPoint absolutePoint = spr->convertToWorldSpace(pos) - pos;

ただ、中身を見てみてもなぜそうなるのかよくわからないし、もしかしたら条件次第で違う結果が返ってくるかもですけど(´・ω・`)

※一番下の追記で自分自身だけで絶対座標を取得する方法を記載しました。

軽く検証してみる

どんな感じの値が入るのか、実際にコーディングして調べてみました。

convertToNodeSpace

CCSize visibleSize = CCDirector::sharedDirector()->getVisibleSize();
//後でこのポイントを変更してそれぞれずらす
CCPoint pos = ccp(0,0);

CCPoint center = visibleSize / 2;
CCSprite* rect1 = CCSprite::create("rect1.png");
rect1->setPosition(center);
this->addChild(rect1);
CCSize rect1Size = rect1->getContentSize();

CCSprite* rect2 = CCSprite::create("rect2.png");
rect2->setPosition(rect1Size / 2 + pos);

rect1->addChild(rect2);
CCSize rect2Size = rect2->getContentSize();

CCLayer* layer = CCLayer::create();
layer->ignoreAnchorPointForPosition(false);
layer->setAnchorPoint(ccp(0.5,0.5));
layer->setPosition(rect2Size / 2 + pos);
rect2->addChild(layer);

CCSprite* rect3 = CCSprite::create("point.png");
rect3->setPosition(layer->getContentSize()/ 2 + pos);
layer->addChild(rect3);

CCPoint rect1Point = rect1->getPosition();
CCPoint rect2Point = rect2->getPosition();
CCPoint layerPoint = layer->getPosition();
CCPoint rect3Point = rect3->getPosition();

CCLOG("---getContentSize---");
CCLOG("画面の大きさ:%f %f",visibleSize.width , visibleSize.height);
CCLOG("rect1.pngの大きさ:%f %f" , rect1->getContentSize().width , rect1->getContentSize().height);
CCLOG("rect2.pngの大きさ:%f %f" , rect2->getContentSize().width , rect2->getContentSize().height);
CCLOG("CCLayerの大きさ:%f %f" , layer->getContentSize().width , layer->getContentSize().height);
CCLOG("point.pngの大きさ:%f %f" , rect3->getContentSize().width , rect3->getContentSize().height);

CCLOG("---getPosition---");
CCLOG("rect1.pngの位置:%f %f" , rect1Point.x , rect1Point.y);
CCLOG("rect2.pngの位置:%f %f" , rect2Point.x , rect2Point.y);
CCLOG("CCLayerの位置 :%f %f" , layerPoint.x , layerPoint.y);
CCLOG("point.pngの位置:%f %f" , rect3Point.x , rect3Point.y);

CCPoint worldPoint1 = this->convertToWorldSpace(rect1Point);
CCPoint worldPoint2 = rect1->convertToWorldSpace(rect2Point);
CCPoint worldLayerPoint = rect2->convertToWorldSpace(layerPoint);
CCPoint worldPoint3 = layer->convertToWorldSpace(rect3Point);

CCLOG("---convertToWorldSpace---");
CCLOG("rect1.pngの位置:%f %f" , worldPoint1.x , worldPoint1.y);
CCLOG("rect2.pngの位置:%f %f" , worldPoint2.x , worldPoint2.y);
CCLOG("CCLayerの位置:%f %f" , worldLayerPoint.x , worldLayerPoint.y);
CCLOG("point.pngの位置:%f %f" , worldPoint3.x , worldPoint3.y);

worldPoint1 = this->convertToWorldSpaceAR(rect1Point);
worldPoint2 = rect1->convertToWorldSpaceAR(rect2Point);
worldLayerPoint = rect2->convertToWorldSpaceAR(layerPoint);
worldPoint3 = layer->convertToWorldSpaceAR(rect3Point);

CCLOG("---convertToWorldSpaceAR---");
CCLOG("rect1.pngの位置:%f %f" , worldPoint1.x , worldPoint1.y);
CCLOG("rect2.pngの位置:%f %f" , worldPoint2.x , worldPoint2.y);
CCLOG("CCLayerの位置:%f %f" , worldLayerPoint.x , worldLayerPoint.y);
CCLOG("point.pngの位置:%f %f" , worldPoint3.x , worldPoint3.y);

worldPoint1 = rect1->convertToWorldSpaceAR(rect1Point) - rect1Point;
worldPoint2 = rect2->convertToWorldSpaceAR(rect2Point) - rect2Point;
worldLayerPoint = layer->convertToWorldSpaceAR(layerPoint) - layerPoint;
worldPoint3 = rect3->convertToWorldSpaceAR(rect3Point) - rect3Point;

CCLOG("---convertToWorldSpaceAR(セルフバージョン)---");
CCLOG("rect1.pngの位置:%f %f" , worldPoint1.x , worldPoint1.y);
CCLOG("rect2.pngの位置:%f %f" , worldPoint2.x , worldPoint2.y);
CCLOG("CCLayerの位置:%f %f" , worldLayerPoint.x , worldLayerPoint.y);
CCLOG("point.pngの位置:%f %f" , worldPoint3.x , worldPoint3.y);

CCPoint nodePoint1 = rect1->convertToNodeSpace(center);
CCPoint nodePoint2 = rect2->convertToNodeSpace(center);
CCPoint nodeLayerPoint = layer->convertToNodeSpace(center);
CCPoint nodePoint3 = rect3->convertToNodeSpace(center);

CCLOG("---convertToNodeSpace---");
CCLOG("測定位置:%f %f",center.x , center.y);
CCLOG("rect1.pngの位置:%f %f" , nodePoint1.x , nodePoint1.y);
CCLOG("rect2.pngの位置:%f %f" , nodePoint2.x , nodePoint2.y);
CCLOG("CCLayerの位置:%f %f" , nodeLayerPoint.x , nodeLayerPoint.y);
CCLOG("point.pngの位置:%f %f" , nodePoint3.x , nodePoint3.y);

nodePoint1 = rect1->convertToNodeSpaceAR(center);
nodePoint2 = rect2->convertToNodeSpaceAR(center);
nodeLayerPoint = layer->convertToNodeSpaceAR(center);
nodePoint3 = rect3->convertToNodeSpaceAR(center);

CCLOG("---convertToNodeSpace---");
CCLOG("測定位置:%f %f",center.x , center.y);
CCLOG("rect1.pngの位置:%f %f" , nodePoint1.x , nodePoint1.y);
CCLOG("rect2.pngの位置:%f %f" , nodePoint2.x , nodePoint2.y);
CCLOG("CCLayerの位置:%f %f" , nodeLayerPoint.x , nodeLayerPoint.y);
CCLOG("point.pngの位置:%f %f" , nodePoint3.x , nodePoint3.y);

ちょっと色々とうんこコードですけど、全部を入れてみました。
rect1はCCLayer直下で、その後rect2,layer,rect3と入れ子にしています。
最初にあるposの値をずらすと、それぞれ位置を少しずらすことができます。

最初pos = ccp(0,0)で行った結果が以下の通りです。

convertto1

Cocos2d: —getContentSize—
Cocos2d: 画面の大きさ:480.000000 320.000000
Cocos2d: rect1.pngの大きさ:200.000000 200.000000
Cocos2d: rect2.pngの大きさ:100.000000 100.000000
Cocos2d: CCLayerの大きさ:480.000000 320.000000
Cocos2d: point.pngの大きさ:25.000000 25.000000
Cocos2d: —getPosition—
Cocos2d: rect1.pngの位置:240.000000 160.000000
Cocos2d: rect2.pngの位置:100.000000 100.000000
Cocos2d: CCLayerの位置 :50.000000 50.000000
Cocos2d: point.pngの位置:240.000000 160.000000
Cocos2d: —convertToWorldSpace—
Cocos2d: rect1.pngの位置:240.000000 160.000000
Cocos2d: rect2.pngの位置:240.000000 160.000000
Cocos2d: CCLayerの位置:240.000000 160.000000
Cocos2d: point.pngの位置:240.000000 160.000000
Cocos2d: —convertToWorldSpaceAR—
Cocos2d: rect1.pngの位置:480.000000 320.000000
Cocos2d: rect2.pngの位置:340.000000 260.000000
Cocos2d: CCLayerの位置:290.000000 210.000000
Cocos2d: point.pngの位置:480.000000 320.000000
Cocos2d: —convertToWorldSpaceAR(セルフバージョン)—
Cocos2d: rect1.pngの位置:240.000000 160.000000
Cocos2d: rect2.pngの位置:240.000000 160.000000
Cocos2d: CCLayerの位置:240.000000 160.000000
Cocos2d: point.pngの位置:240.000000 160.000000
Cocos2d: —convertToNodeSpace—
Cocos2d: 測定位置:240.000000 160.000000
Cocos2d: rect1.pngの位置:100.000000 100.000000
Cocos2d: rect2.pngの位置:50.000000 50.000000
Cocos2d: CCLayerの位置:240.000000 160.000000
Cocos2d: point.pngの位置:12.500000 12.500000
Cocos2d: —convertToNodeSpace—
Cocos2d: 測定位置:240.000000 160.000000
Cocos2d: rect1.pngの位置:0.000000 0.000000
Cocos2d: rect2.pngの位置:0.000000 0.000000
Cocos2d: CCLayerの位置:0.000000 0.000000
Cocos2d: point.pngの位置:0.000000 0.000000

全部中心にある為、最後のはきれいに0が並びました。

次に、pos=ccp(25,0)でやってみます。

convert2Cocos2d: —getContentSize—
(省略)

Cocos2d: —convertToWorldSpace—
Cocos2d: rect1.pngの位置:240.000000 160.000000
Cocos2d: rect2.pngの位置:265.000000 160.000000
Cocos2d: CCLayerの位置:290.000000 160.000000
Cocos2d: point.pngの位置:315.000000 160.000000
Cocos2d: —convertToWorldSpaceAR—
Cocos2d: rect1.pngの位置:480.000000 320.000000
Cocos2d: rect2.pngの位置:365.000000 260.000000
Cocos2d: CCLayerの位置:340.000000 210.000000
Cocos2d: point.pngの位置:555.000000 320.000000
Cocos2d: —convertToWorldSpaceAR(セルフバージョン)—
Cocos2d: rect1.pngの位置:240.000000 160.000000
Cocos2d: rect2.pngの位置:265.000000 160.000000
Cocos2d: CCLayerの位置:290.000000 160.000000
Cocos2d: point.pngの位置:315.000000 160.000000
Cocos2d: —convertToNodeSpace—
Cocos2d: 測定位置:240.000000 160.000000
Cocos2d: rect1.pngの位置:100.000000 100.000000
Cocos2d: rect2.pngの位置:25.000000 50.000000
Cocos2d: CCLayerの位置:190.000000 160.000000
Cocos2d: point.pngの位置:-62.500000 12.500000
Cocos2d: —convertToNodeSpace—
Cocos2d: 測定位置:240.000000 160.000000
Cocos2d: rect1.pngの位置:0.000000 0.000000
Cocos2d: rect2.pngの位置:-25.000000 0.000000
Cocos2d: CCLayerの位置:-50.000000 0.000000
Cocos2d: point.pngの位置:-75.000000 0.000000

ちょっとずつ移動してますね。当たり前だけど。

とりあえず、入れ子状態になっていても1回convertToNodeSpace(convertToWorldSpace)を実行すれば、絶対座標/相対座標に変換できて便利ですね。

まとめ

  • 子ノードは常に親ノードの左下からの相対座標で位置が定まってる
  • それぞれのノードはアンカーポイントが位置の基準点となっている
  • ただCCSceneとCCLayerはアンカーポイントに関わらず左下が基準点となる(変更可)
  • self::convertToNodeSpace—絶対座標をそのノードの相対座標に変換
  • parent::convertToWorldSpace─そのノードの相対座標を絶対座標に変換
  • 相対座標/絶対座標の変換はいくつも入れ子状態になってても正しく行われる
  • 追記:self::convertToWorldSpaceAR(CCPointZero)でも自身の絶対座標取得可

なんだか全然まとまってないですね(´・ω・`)

追記:親ノードを使わずに自分だけでconvertToWorldSpaceにて絶対座標を取得する

書き終えた後に気づいた(´・ω・`)
そもそもなぜconvertToWorldSpaceは親ノードで指定しないといけないのかって言うと、引数で渡してる位置が親ノード基準&親ノード視点だからなんですね。なんか日本語がわかりづらくて変ですけど。
ただ、だから自分自身に自分のポジションを指定してconvertToWorldSpaceをすると変な値が返ってくるのは当然だったわけです。自分基準じゃないんだから。

なら、自分のアンカーポイントの座標を自分基準で取得したのを引数に入れてあげたら、自分自身だけを使って絶対座標を取得することもできますね。

たとえば、上のコードの「convertToWorldSpace」の部分を以下のように変更します。

//(省略)
CCPoint worldPoint1 = rect1->convertToWorldSpace(rect1->getAnchorPointInPoints());
CCPoint worldPoint2 = rect2->convertToWorldSpace(rect2->getAnchorPointInPoints());
CCPoint worldLayerPoint = layer->convertToWorldSpace(layer->getAnchorPointInPoints());
CCPoint worldPoint3 = rect3->convertToWorldSpace(rect3->getAnchorPointInPoints());
//(省略)

【実行結果】
pos = ccp(0,0)の場合
Cocos2d: —convertToWorldSpace—
Cocos2d: rect1.pngの位置:240.000000 160.000000
Cocos2d: rect2.pngの位置:240.000000 160.000000
Cocos2d: CCLayerの位置:240.000000 160.000000
Cocos2d: point.pngの位置:240.000000 160.000000

pos = ccp(25,0)の場合
Cocos2d: —convertToWorldSpace—
Cocos2d: rect1.pngの位置:240.000000 160.000000
Cocos2d: rect2.pngの位置:265.000000 160.000000
Cocos2d: CCLayerの位置:290.000000 160.000000
Cocos2d: point.pngの位置:315.000000 160.000000

おかげさまで、上と同じ結果になりました。

ちなみにこのgetAnchorPointInPoints()というのは、自分自身の左下を基準としたアンカーポイントの位置を返す関数なのですが、実はconvertToWorldSpaceARの内部で加えられているものなんです。

ということは、以下のコードで絶対座標が取得できるということになります。

spr->convertToWorldSpaceAR(CCPointZero);

はい、とっても簡単になりましたね。ますますブログはまとまりがなくなりましたが、コード自体はとてもまとまりました。

cocos2d-xの座標操作まとめ(convertToNodeSpace、convertToWorldSpace他)” への1件のコメント

  1. ピンバック: #ProgMemo #cocos2dx cocos2dxの座 | TodayKabu

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です