Cocos2d-x 3.x基础学习:视差节点ParallaxNode

2015年03月23日 11:35 0 点赞 0 评论 更新于 2025-11-21 18:14

视差现象概述

通常,当我们移动时,会观察到离我们越近的物体移动速度越快,而越远的物体移动速度越慢。例如,远处的山移动速度较慢,最远处的太阳几乎不动,这种现象被称为视差。在游戏中模拟视差效果,可以让玩家更真实地感受到游戏角色的移动。Cocos提供了ParallaxNode视差节点类,通过该类可以轻松创建视差层,并控制每一层的视差率、位置和层级。

Cocos2d-x 3.4版本视差节点ParallaxNode

【ParallaxNode】

ParallaxNode类的使用较为简单,只需将想要产生视差效果的节点添加为ParallaxNode的子节点,并设置视差率、位置和层级即可。

1、常用函数

ParallaxNode类中自定义的函数较少,主要是对父类Node的相关函数进行重写。需要注意的是,不要调用父类Node中相对应的重载函数,例如addChild(child, zOrder, tag)。核心函数如下:

  • create():用于创建视差节点。
  • addChild():用于添加子节点。
  • removeChild():用于移除子节点。

以下是ParallaxNode类的部分代码定义:

/**
* 模拟视差滚动的节点
* 子节点根据视差比率(parallaxRatio),做比父节点相对快/慢的移动。
*/
class CC_DLL ParallaxNode : public Node
{
public:
// 创建视差节点
static ParallaxNode* create();

// 添加子节点
// child : 子节点
// z : zOrder顺序
// ratio : Vec2(ratioX,ratioY) 两个方向上,相对ParallaxNode的移动比率
// offset : 相对ParallaxNode的偏移位置
void addChild(Node * child, int z, const Vec2& ratio, const Vec2& offset);

// 更新子节点位置信息
virtual void visit(Renderer *renderer, const Mat4 &parentTransform, uint32_t parentFlags) override;

// 移除子节点
// child 被删除的子节点。
// cleanup true 在这个节点上所有的动作和回调都会被删除, false 就不会删除。
virtual void removeChild(Node* child, bool cleanup) override;

// 移除所有子节点
// cleanup true 在这个节点上所有的动作和回调都会被删除, false 就不会删除。
virtual void removeAllChildrenWithCleanup(bool cleanup) override;
};

2、关于添加子节点:addChild()

ParallaxNode中添加子节点时,需使用重写的addChild()函数,而不能使用父类的addChild(Node* child, int z, int tag)等重载函数。该函数的实现如下:

// 添加子节点
// child : 子节点
// z : zOrder顺序
// ratio : Vec2(ratioX,ratioY) 两个方向上,相对ParallaxNode的移动比率
// offset : 相对ParallaxNode的偏移位置
void ParallaxNode::addChild(Node* child, int z, const Vec2& ratio, const Vec2& offset)
{
CCASSERT( child != nullptr, "Argument must be non-nil");
PointObject *obj = PointObject::create(ratio, offset);
obj->setChild(child);
ccArrayAppendObjectWithResize(_parallaxArray, (Ref*)obj);
Vec2 pos = this->absolutePosition();
// 子节点位置,是根据偏移位置offset,和视差率ratio的计算
pos.x = -pos.x + pos.x * ratio.x + offset.x;
pos.y = -pos.y + pos.y * ratio.y + offset.y;
child->setPosition(pos);
Node::addChild(child, z, child->getName());
}

可以看出,子节点的坐标位置(Position)由偏移位置(offset)和视差比率(ratio)决定。这也是为什么添加到ParallaxNode的子节点无法使用setPosition()手动设置位置的原因,这可能是使用ParallaxNode的一个弊端。

3、关于更新子节点位置:visit()

ParallaxNode的位置发生变化时,所有子节点的位置会根据ParallaxNode的位置、offset以及ratio进行计算并更新。位置变化后会调用visit()函数,其实现如下:

// 更新子节点位置信息
void ParallaxNode::visit(Renderer *renderer, const Mat4 &parentTransform, uint32_t parentFlags)
{
Vec2 pos = this->absolutePosition();
if( ! pos.equals(_lastPosition) )
{
// 计算所有孩子相对于ParallaxNode的位置
// 注意当我们移动ParallaxNode位置时,表现出来的其实是孩子位置的改变,这种变化是本类的核心设计。
for( int i=0; i < _parallaxArray->num; i++ )
{
PointObject *point = (PointObject*)_parallaxArray->arr[i];
// 例如ParallaxNode绝对位置为100,表现出来的是孩子位置为-100,ParallaxNode的移动我们不能感知,但孩子的位置却发生了变化。
// 简单点就是类似于一个摄像头场景的移动,摄像头未动,风景变了
// 如果ratio为(=1) ,则 position == offset
// 如果ratio为(0~1),则 position < offset,移动速度慢
// 如果ratio为(>1) ,则 position > offset,移动速度快
float x = -pos.x + pos.x * point->getRatio().x + point->getOffset().x;
float y = -pos.y + pos.y * point->getRatio().y + point->getOffset().y;
// 孩子的位置是通过上面两行计算出来的
// 因此手动设置其postion不会有任何作用
point->getChild()->setPosition(x,y);
}
_lastPosition = pos;
}
Node::visit(renderer, parentTransform, parentFlags);
}

4、子节点的视差效果

将子节点添加到ParallaxNode并设置偏移位置offset和视差比率ratio后,ParallaxNode的所有子节点会随着ParallaxNode的移动而移动。子节点移动后的位置根据ParallaxNode的位置、offset以及ratio计算得出。当子节点具有不同的视差比率ratio时,在移动过程中会出现移动速度不同的视差效果。

【代码实战】

0、图片素材

需准备HelloWorld.pngBall.pngSmile.png三张图片素材。

1、创建视差节点类,并添加子节点

创建三个子节点,并添加到ParallaxNode中,具体设置如下:

  • bg:锚点(0, 0),视差比率ratio(0.5, 0.5),偏移位置offset(0, 0)
  • ball:锚点(0.5, 0.5),视差比率ratio(1, 1),偏移位置offset(屏幕中心点坐标)
  • smile:锚点(0, 0),视差比率ratio(4, 4),偏移位置offset(0, 0)

同时,测试对精灵bg手动设置位置坐标setPosition()

//[1] 可视区域尺寸大小
Size vSize = Director::getInstance()->getVisibleSize();

//[2] 创建三个子节点
Sprite* bg = Sprite::create("HelloWorld.png");
bg->setAnchorPoint(Vec2(0, 0)); // 锚点(0, 0)
bg->setName("HelloWorld");
//Bug: 在ParallaxNode上的精灵,对其setPosition是无效的
// 具体看运行结果
bg->setPosition(Vec2(100, 100)); // 设置bg的坐标位置

Sprite* ball = Sprite::create("Ball.png");
ball->setAnchorPoint(Vec2(0.5, 0.5)); // 锚点(0.5, 0.5)
ball->setName("Ball");

Sprite* smile = Sprite::create("Smile.png");
smile->setAnchorPoint(Vec2(0, 0)); // 锚点(0, 0)
smile->setName("Smile");

//[3] 创建ParallaxNode,并添加子节点
// 创建视差节点类
ParallaxNode* parallaxNode = ParallaxNode::create();
this->addChild(parallaxNode, 0, "parallaxNode");

// 添加子节点
// addChild(node, zOrder, ratio, offset);
parallaxNode->addChild(bg, 0, Vec2(0.5, 0.5), Vec2(0, 0));
parallaxNode->addChild(ball, 1, Vec2(1, 1), Vec2(vSize.width/2, vSize.height/2));
parallaxNode->addChild(smile, 1, Vec2(4, 4), Vec2(0, 0));

//[4] 添加触摸事件
EventDispatcher* dispatcher = this->getEventDispatcher();
EventListenerTouchOneByOne* listener = EventListenerTouchOneByOne::create();
listener->onTouchBegan = CC_CALLBACK_2(HelloWorld::onTouchBegan, this);
listener->onTouchMoved = CC_CALLBACK_2(HelloWorld::onTouchMoved, this);
listener->onTouchEnded = CC_CALLBACK_2(HelloWorld::onTouchEnded, this);
dispatcher->addEventListenerWithSceneGraphPriority(listener, this);

2、实现触摸事件

  • 触摸开始:无操作。
  • 触摸移动ParallaxNode随之移动。
  • 触摸结束:输出每个节点的位置信息,查看差别。
    bool HelloWorld::onTouchBegan(Touch* pTouch, Event* pEvent)
    {
    return true;
    }
    

// 移动 ParallaxNode 节点 void HelloWorld::onTouchMoved(Touch pTouch, Event pEvent) { Vec2 delta = pTouch->getDelta(); // 移动 ParallaxNode 节点 // 所有子节点的位置,也会随之改变 ParallaxNode parallaxNode = (ParallaxNode)this->getChildByName("parallaxNode"); parallaxNode->setPosition(parallaxNode->getPosition() + delta); }

// 触摸结束后,输出每个节点的位置信息 void HelloWorld::onTouchEnded(Touch pTouch, Event pEvent) { ParallaxNode parallaxNode = (ParallaxNode)this->getChildByName("parallaxNode"); // 按名称获取子节点 Sprite bg = (Sprite)parallaxNode->getChildByName("HelloWorld"); Sprite ball = (Sprite)parallaxNode->getChildByName("Ball"); Sprite smile = (Sprite)parallaxNode->getChildByName("Smile"); // 输出坐标位置信息 CCLOG("parallax : %f %f", parallaxNode->getPositionX(), parallaxNode->getPositionY()); CCLOG("HelloWorld : %f %f", bg->getPositionX(), bg->getPositionY()); CCLOG("ball : %f %f", ball->getPositionX(), ball->getPositionY()); CCLOG("smile : %f %f", smile->getPositionX(), smile->getPositionY()); CCLOG("---------------------------------------"); }


<a id="heading-11-3-运行结果"></a>
### 3、运行结果
移动过程中,输出的数据结果如下:

parallax : 0.000000 0.000000 HelloWorld : 0.000000 0.000000 ball : 240.000000 160.000000

smile : 0.000000 0.000000

parallax : 101.836441 20.723732 HelloWorld : -50.918221 -10.361866 ball : 240.000000 160.000000

smile : 305.509338 62.171196

parallax : 19.378311 64.557907 HelloWorld : -9.689156 -32.278954 ball : 240.000000 160.000000

smile : 58.134933 193.673721

parallax : -1.778305 -81.284264 HelloWorld : 0.889153 40.642132 ball : 240.000000 160.000000

smile : -5.334915 -243.852783


<a id="heading-12-4-数据分析"></a>
### 4、数据分析
- **HelloWorld**:一开始设置`setPosition()`无效,因为其坐标只受`ratio`和`offset`影响。移动过程中坐标变为负数,是因为子节点坐标是相对`ParallaxNode`的偏移位置。`HelloWorld`的视差比率为0.5倍,当`parallaxNode`移动100个像素点时,`HelloWorld`只移动50个像素点,所以相对于`parallaxNode`的偏移位置为 -50。
- **ball**:移动过程中坐标位置未改变,因为其视差比率为1.0倍,`parallaxNode`移动多少,`ball`就移动多少,所以`ball`相对`parallaxNode`的位置仍是原来的偏移位置。
- **smile**:移动速度很快,视差比率为4.0倍。其坐标位置是`parallaxNode`的3倍,因为`parallaxNode`移动100个像素点时,`smile`移动400个像素点,所以相对于`parallaxNode`坐标的位置为400 - 100 = 300。