【英文论坛获奖教程】无限视差节点

无限视差节点

原作者:
Den

英文论坛原帖:

http://discuss.cocos2d-x.org/t/cocos3-0-tutorial-infinite-parallax-scrolling-with-primitive-shadows/14210

视差的含义是指对象的位置相对于背景的变化取决于观察者的位置。毕竟三维世界的特性被广泛的应用于在二维游戏创造出景深的效果。在Cocos2d-x中有一个视差节点ParallaxNode提供了根据他视差比例决定子节点卷动速度变快或变慢。

在图中你能看到每一层中在点1和点2之间的距离都是不一样的。这是一个对视差简单的示范。在cocos2d-x中对标准的视差节点有问题的是当物体跨过了屏幕的左边框后他们就消失了。视差节点并没有能够将移出左边可见区域的子节点立即转移到右边屏幕的功能。所以我们需要自己手动完成它。为了完成这一目标,我们需要扩充标准的ParallaxNode函数并创建我们自己的类”InfiniteParallaxNode”。

实现方法
为了实现我们的目的,我们需要创建“InfiniteParallaxNode.h” 和 "InfiniteParallaxNode.cpp"文件。
类定义在头文件里,像这样:

class InfiniteParallaxNode : public ParallaxNode
{
public:
    static InfiniteParallaxNode* create();
    void updatePosition();
};

```

“updatePosition”是用来管理元素变更位置的方法。
现在打开InfiniteParallaxNode.cpp文件。

“创建”方法看起来就跟定义在“CREATE_FUNC”中定义的宏:
InfiniteParallaxNode* InfiniteParallaxNode::create()
{
    // Create an instance of InfiniteParallaxNode
    InfiniteParallaxNode* node = new InfiniteParallaxNode();
    if(node) {
        // Add it to autorelease pool
        node->autorelease();
    } else {
        // Otherwise delete
        delete node;
        node = 0;
    }
    return node;
}

```


现在“updatePosition”的实现方法是:
void InfiniteParallaxNode::updatePosition()
{
    int safeOffset = -10;
    // Get visible size
    Size visibleSize = Director::getInstance()->getVisibleSize();
    // 1. For each child of an parallax node
    for(int i = 0; i < _children.size(); i++)
    {
        auto node = _children.at(i);
        // 2. We check whether it is out of the left side of the visible area
        if(convertToWorldSpace(node->getPosition()).x + node->getContentSize().width < safeOffset)
            // 3. Find PointObject that corresponds to current node
            for(int i = 0; i < _parallaxArray->num; i++)
            {
                auto po = (PointObject*)_parallaxArray->arr;
                // If yes increase its current offset on the value of visible width
                if(po->getChild() == node)
                    po->setOffset(po->getOffset() +
                                  Point(visibleSize.width + node->getContentSize().width,0));
            }
    }
}

```

*
在源文件的开头你需要添加“PointObject”类的下一个声明。
class PointObject : public Ref
{
public:
    inline void setRation(Point ratio) {_ratio = ratio;}
    inline void setOffset(Point offset) {_offset = offset;}
    inline void setChild(Node *var) {_child = var;}
    inline Point getOffset() const {return _offset;}
    inline Node* getChild() const {return _child;}
private:
    Point _ratio;
    Point _offset;
    Node* _child;
};

```


这样,我们就能够使用"PointObject"类了。


解释
为了理解我们在“updatePosition”里完成的动作actions,我们来看一眼“ParallaxNode”的结构。
它有两个数组的子节点。第一个是标准的节点Node*矢量。他存储了引用的子节点。同样这还有另一个数组“PointObjects”。每一个“PointObjects”数组包含一个星期参考a we“a”k reference弱关联?对应着第一个数组的相应节点。对我们至关重要的是“PointObject”包含了补偿值和配比。为每一个节点计算渲染过程的坐标如下所示:

pos.
x = -
pos.
x + 
pos.
x * ratio.
x + offset.
x;

pos.
y = -
pos.
y + 
pos.
y * ratio.
y + offset.
y;
当初始值“pos”是一个父节点的坐标是(视差节点parallax node)。

现在关于我们的教程。在第一行我们在第一个数组中遍历了子节点(有着“_children”这个名称的。)

在第二行我们检查了节点是否穿过了屏幕左边框。
  

根据上图所示,对应的条件判断语句是position.x + tree.width < 0。Position.x对应于convertToWorldSpace(node->getPosition()).x。然而,node->getContentSize().width对应于tree.width。

然后再第三行我们找到“PointObject”,它对应于现有节点(那些已经从可视区域离开的节点)。
最后在第四行我们增大了补偿的量使得节点移动到屏幕的右边框。



简单的阴影节点
在这一个部分我们将要从我们的视差卷动节点创建一个对物体简单的阴影仿真。



实现方法
对于阴影仿真我们要创建类"ShadowLayer"。它在头文件的定义如下所示:
class ShadowLayer : public Layer
{
    static unsigned int SHADOW_TAG;


    Point _lightPosition;
    float _worldScale;


    Node* getShadowForNode(Node* node);
    float toDegrees(float radians);
    bool findIndex(int index, vector v);


protected:
    virtual float calculateSkew(Node* node);


public:
    static ShadowLayer* create(Node* parent, Point lightPosition, float worldScale, vector disableIndices);
    bool initWith(Node* parent, Point lightPosition, float worldScale, vector disableIndices);


    void update(Node* parent);
};

```

*
*
这里"SHADOW_TAG"是一个阴影节点的唯一识别符;"lightPosition"定义了光源在屏幕上的位置;"worldScale"是影响阴影长度的参数。update方法更新了阴影的外貌,当任意一个阴影节点的位置改变的时候都应该调用它;"disableIndices"是关于节点的向量标记,并且它不应该产生节点。
在"init"函数中我们创建了阴影。在我们的教程中,这个阴影是一个精灵sprite。目标节点修改后的拷贝。目标节点在这之后是能够倒映出阴影的父节点的一个精灵子节点*
bool ShadowLayer::initWith(Node *parent, Point lightPosition, float worldScale, vector disableIndices)
{
    if(!Layer::init())
        return false;


    _lightPosition = lightPosition;
    _worldScale = worldScale;


    // For each child of parent node we generate a shadow
    // and attach it to the node itself
    for(auto node : parent->getChildren())
    {
        auto shadow = getShadowForNode(node);
        node->addChild(shadow);


        if(disableIndices.size() > 0)
            // If nodes tag is within disable indices
            if(findIndex(node->getTag(), disableIndices))
                // Make its shadow invisible
                node->getChildByTag(SHADOW_TAG)->setVisible(false);
    }


    return true;
}

```
*
阴影制作过程包含以下几个步骤:
用目标节点的精灵框架创建精灵并将其初始化
根据目标节点时阴影节点集中于一点
复制目标节点的形变(包括比例和旋转)
根据世界比例world scale参数修改阴影节点
翻转阴影精灵
以正确的方式制作阴影(真实的阴影模样)
使得阴影精灵半透明并呈灰色
给阴影精灵分配唯一的阴影标签*
Node* ShadowLayer::getShadowForNode(Node *node)
{
    // Step 1
    auto shadow = Sprite::create();


    auto object = (Sprite*)node;
    shadow->setSpriteFrame(object->getSpriteFrame());


    // Step 2
    shadow->setAnchorPoint(Point(0.5,1.0)); // position it to the center of the target node
    shadow->setPosition(Point(shadow->getContentSize().width / 2, 0));


    // Step 3
    shadow->setRotation(object->getRotation());
    shadow->setScale(object->getScale());


    // Step 4
    shadow->setScaleY(_lightPosition.y / _worldScale);


    // Step 5
    shadow->runAction(FlipY::create(true));


    // Step 6
    shadow->setSkewX(calculateSkew(node));


    // Step 6
    shadow->setColor(Color3B(0, 0, 0));
    shadow->setOpacity(150);


    // Step 7
    shadow->setTag(SHADOW_TAG);


    return shadow;
}

```

*
歪斜Skew计算函数如下所示:
float ShadowLayer::calculateSkew(Node *node)
{
    Node* parent = node->getParent();
    float ED = _lightPosition.y - parent->getPosition().y;
    float EL = _lightPosition.x - (node->getParent()->getPosition().x + node->getPosition().x);
    float DLE = atan(ED / EL);
    float DB = node->getContentSize().height * node->getScaleY() +
            parnet->getContentSize().height * parent->getScaleY();
    float CB = tan(PI / 2 - DLE) * DB;
    float AB = node->getContentSize().height * node->getScaleY();
    float skew = 90.0 - toDegrees(atan(AB / CB));
    return skew;
}

```

最后是update函数
void ShadowLayer::update(Node* parent)
{
    int safeOffset = -10;
    // 1. For each target node
    for(auto node : parent->getChildren())
    {
        // 2. Get the shadow sprite
        auto shadow = node->getChildByTag(SHADOW_TAG);
        // 3. If the target node has left the screen visible area
        if(node->getParent()->getPosition().x + node->getPosition().x
                               + node->getContentSize().width < safeOffset)
            // Reset shadow skew
            shadow->setSkewX(0);
        else
            // Else recalculate skew
            shadow->setSkewX(calculateSkew(node));
    }
}

```



解释

这里的主要技巧就在于歪斜形变。这使得阴影精灵有着真实的外貌。见上图。
  

歪斜形变是通过角度u来定义的。所以我们的目标是为u找到合适的值。

我们能从下一个等式中找到需要的u值:
从我们计算得到的角度ACB得到u值:
CB我们能从下一个等式中找到u值:
DB是阴影和目标高度的和并且角度DCB和角度DLE相等。我们能从角度DLE找到需要的u值。
把所有的东西结合起来
===========================
创建一个无线视差节点:
vector disableShadowTags;


_backgroundElements = InfiniteParallaxNode::create();


unsigned int rocksQuantity = 7;
for(unsigned int i = 0; i < rocksQuantity; i++)
{
    // Create a sprite with rock texture
    auto rock = Sprite::create("rock.png");
    rock->setAnchorPoint(Point::ZERO);
    // Set scale factor as a random value from  interval
    rock->setScale(randomValueBetween(0.6, 0.75));
    rock->setTag(1000 + i);
    disableShadowTags.push_back(rock->getTag());
    _backgroundElements->addChild(rock,
                // Set random z-index from -10,-6]
                randomValueBetween(-10, -6),
                // Set ration (rocks moves slow)
                Point(0.5, 1),
                // Set position with random component
                Point((visibleSize.width / 5) * (i + 1) + randomValueBetween(0, 100),
                ground->getContentSize().height - 5));
}


unsigned int treesQuantity = 35;
for(unsigned int i = 0; i < treesQuantity; i++)
{
    auto tree = Sprite::create("tree.png");
    tree->setAnchorPoint(Point::ZERO);
    // Parameters for trees varies
    tree->setScale(randomValueBetween(0.5, 0.75));
    _backgroundElements->addChild(
                tree,
                randomValueBetween(-5, -1),
                Point(0.75, 1),
                Point(visibleSize.width / (treesQuantity - 5) * (i + 1) + randomValueBetween(25,50),
                          ground->getContentSize().height - 8));
}
addChild(_backgroundElements, 2);
现在创造阴影图层:
_shadows = ShadowLayer::create(
                _backgroundElements, sun->getPosition(), 425, disableShadowTags);
_shadows->retain();
还有移动视差卷动节点和更新阴影的"update"函数。
void HelloWorld::update(float delta)
{
    Point scrollDecrement = Point(5, 0);
    _backgroundElements->setPosition(_backgroundElements->getPosition() - scrollDecrement);
    _backgroundElements->updatePosition();
    _shadows->update(_backgroundElements);
}

```



结果

  


结论
上面提及的阴影仿真是非常简单并适应这种特殊情况。然而这能够使得外观更具吸引力。但你在更为复杂的环境下使用这个方法会看到更多的瑕疵,所以仅仅只把这个方法当成真实阴影视觉技术的替代或者一个你制作自己的实现方法的起点。*


另请参阅
为使阴影更加真实你能对他们进行模糊处理。

http://discuss.cocos2d-x.org/t/cocos3-0-tutorial-rendertexture-blur/13622

源代码在附件
http://discuss.cocos2d-x.org/uploads/default/6062/140524181717_IPSWS.zip (2412.5 KB)

http://discuss.cocos2d-x.org/uploads/default/6129/140526132133_IPSWS.zip (2464.4 KB)*
*

代码建议放进代码块里
前排支持下小塔~~

已改,谢谢!

收藏!:7:

感谢分享!!!!

相当不错 学习了:2::14:

小塔又复活了……:14::14:虽然我还没用上 3.X。不过看上去貌似很不错

以后一直会复活了 :2::2::2:

回 1楼(阿花君霸占路人) 的帖子

已改,谢谢!
??? 3.2 里已经修正了这个BUG?

:2: :2: :2: 顶起,学习了

顶,大力支持