这篇文章主要是通过一步一步实现一个功能完善的跑马灯公告来展示ClippingNode的用法并且最终深入ClippingNode的源码,了解其实现原理。
首先,先介绍一下ClippingNode,ClippingNode也叫裁剪节点,能将一些内容通过使用模板裁剪出来显示在界面上,可以实现一些很炫酷的效果。来看看今天要实现的效果
1、ClippingNode类分析
先来看看ClippingNode的声明文件 看看其中的public方法
class CC_DLL ClippingNode : public Node
{
public:
static ClippingNode* create();
static ClippingNode* create(Node *stencil);
Node* getStencil() const;
void setStencil(Node *stencil);
virtual bool hasContent() const;
GLfloat getAlphaThreshold() const;
void setAlphaThreshold(GLfloat alphaThreshold);
bool isInverted() const;
void setInverted(bool inverted);
};
首先是create,这个方法是用于创建一个ClippingNode,这个就不多做赘述了,第二个create是创建一个带遮罩模板的裁剪节点。
接下来的getStencil和setStencil分别是获取和设置一个遮罩模板,裁剪物体方法就是通过这个遮罩模板的,遮罩模板只要是基于Node的对象都可以(非常重要)。
接下来的hasContent返回其是否有需要绘制的内容,如果没有绘制的内容则返回false,有则返回true。
getAlphaThreshold和setAlphaThreshold分别是获取和设置一个像素的透明度值,取值范围从0-1,其中0表示都不绘制,1表示都绘制。0.5表示透明度在0.5以上的都绘制,这个函数涉及到opengl的Alpha测试的相关概念,Alpha测试的作用通过一句话解释就是:所有像素的透明度值低于某个阀值的统统抛弃,不绘制到屏幕上。
最后的isInverted和setInverted分别表示绘制的内容是模板内的还是模板外的,其效果如下:
2、简易跑马灯实现
上节简单介绍了一下ClippingNode的函数,这节就通过实现一个简易的跑马灯功能来直观的了解。首先介绍一下制作跑马灯的思路。
首先我们需要将跑马灯中的一部分超出的字裁剪掉,不让他显示在界面上。这就需要用到ClippingNode,现在先来做第一步。实现的代码如下:
//设置模板
auto stencil = Sprite::create();
//设置显示区域大小
stencil->setTextureRect(Rect(0, 0, 50, 30));
//设置跑马灯文字
auto text = Label::createWithSystemFont("-1dasdasdasd efadaewfevgds dfhrthrbgrg1-", "", 24);
//设置锚点
text->setAnchorPoint(Vec2::ANCHOR_MIDDLE_LEFT);
//创建裁剪节点
auto clippingNode = ClippingNode::create(stencil);
//设置节点位置
clippingNode->setPosition(Vec2(700, 400));
//显示模板内的内容
clippingNode->setInverted(false);
//添加显示内容
clippingNode->addChild(text, 2);
//加入到UI树
addChild(clippingNode);
上述的每一句代码都有注释,就不再多解释了,这一步实现出来的效果如下图,但是跑马灯还不能动起来,待会我们就将跑马灯动起来。
现在我们就设计一个Action将跑马灯动起来,跑马灯一般需要先将文字左移,移动到文字看不见的时候再将文字移除或者隐藏,代码如下(为了简便,就直接设置隐藏了):
auto sequ = Sequence::create(MoveBy::create(5.0f, Vec2(-text->getContentSize().width, 0)), Hide::create(), nullptr);
text->runAction(sequ);
现在跑马灯的样子就如同开篇展示的那样了,可是这样还不能直接使用,因为这只是一串代码,还需要对其进行一定的封装,然后提供一个非常简便的方法给别的类调用。
3、封装
现在我们从便捷性的角度考虑如何将跑马灯功能封装成一个函数供其他类调用。首先提取出函数的参数,分别是:显示区域,跑马灯文字,字体字号,跑马灯位置,跑马灯的父节点。下面是初步封装好的一套跑马灯函数的声明:
void showMarquee(Node* parent, const std::string& text, const std::string& font, float fontSize, const Rect& showRect, const Vec2& position);
看参数是不是有些略多,每次调用这个函数是不是非常的不方便,那么我们现在来看看究竟有那些参数是必须要传入的吧。每次调用跑马灯显示的文字都会改变,其他的参数在一个游戏中是不会改变的。那么就有必要做一个类来保证使用方法的便捷性了。
首先,我们简单的构建一下一个跑马灯类,如下
#include "cocos2d.h"
USING_NS_CC;
class Marquee : public Node
{
public:
CREATE_FUNC(Marquee);
bool init();
void show(const std::string& text);
public:
const std::string& getFont() const { return _font; }
void setFont(std::string& font) { _font = font; }
float getFontSize() const { return _fontSize; }
void setFontSize(float fontSize) { _fontSize = fontSize; }
public:
const Rect& getShowRect() const { return _showRect; }
void setShowRect(Rect& showRect) { _showRect = showRect; }
protected:
Marquee() :
_font(""),
_fontSize(24),
_showRect(Rect(0,0,200,30))
{};
~Marquee() {};
private:
std::string _font;
float _fontSize;
Rect _showRect;
};
然后是最重要的init方法和show方法的实现
bool Marquee::init()
{
//设置模板
auto stencil = Sprite::create();
//设置显示区域大小
stencil->setTextureRect(_showRect);
//设置跑马灯文字
_label = Label::createWithSystemFont("", _font, _fontSize);
//设置锚点
_label->setAnchorPoint(Vec2::ANCHOR_MIDDLE_LEFT);
_label->setAlignment(TextHAlignment::LEFT);
//创建裁剪节点
auto clippingNode = ClippingNode::create(stencil);
//显示模板内的内容
clippingNode->setInverted(false);
//添加显示内容
clippingNode->addChild(_label);
//加入到UI树
addChild(clippingNode);
return true;
}
void Marquee::show(const std::string& text)
{
_label->setString(text);
_label->setPosition(Vec2(0, _label->getContentSize().height / 2));
auto sequ = Sequence::create(MoveBy::create(5.0f, Vec2(-_label->getContentSize().width, 0)), Hide::create(), nullptr);
_label->runAction(sequ);
}
这样就可以通过以下的调用方法来调用跑马灯了
<span style="white-space:pre"> </span>Marquee* m = Marquee::create();
m->show("----hhahahah veeeeee-----");
m->setPosition(Vec2(700, 300));
this->addChild(m);
4、完善
看上去,此前的步骤我们已经完成了一个跑马灯的功能,实际上这个类距离真正能使用还差那么一点点,因为传入跑马灯的消息的传入时机是不确定的,可能这一条消息还没有播放完成下一条就要开始播放了。这样就需要实现一个播放等待队列,将需要播放的消息加入播放队列,然后跑马灯自动判断是否需要显示。下面是改进后的类声明文件以及实现文件。
.h:
#include "cocos2d.h"
USING_NS_CC;
class Marquee : public Node
{
public:
enum class State
{
idle,
playing,
};
public:
CREATE_FUNC(Marquee);
bool init();
void addMessage(const std::string& text);
public:
const std::string& getFont() const { return _font; }
void setFont(std::string& font) { _font = font; }
float getFontSize() const { return _fontSize; }
void setFontSize(float fontSize) { _fontSize = fontSize; }
public:
const Rect& getShowRect() const { return _showRect; }
void setShowRect(Rect& showRect) { _showRect = showRect; }
public:
const State& getState() const { return _state; }
protected:
Marquee() :
_font(""),
_fontSize(24),
_showRect(Rect(0,0,200,30)),
_state(State::idle)
{};
~Marquee() {};
void show(const std::string& text);
private:
State _state;
private:
std::string _font;
float _fontSize;
Rect _showRect;
private:
Label * _label;
private:
std::queue<std::string> _texts;
};
.cpp:
#include <Marquee.h>
bool Marquee::init()
{
//设置模板
auto stencil = Sprite::create();
//设置显示区域大小
stencil->setTextureRect(_showRect);
//设置跑马灯文字
_label = Label::createWithSystemFont("", _font, _fontSize);
//设置锚点
_label->setAnchorPoint(Vec2::ANCHOR_MIDDLE_LEFT);
_label->setAlignment(TextHAlignment::LEFT);
//创建裁剪节点
auto clippingNode = ClippingNode::create(stencil);
//显示模板内的内容
clippingNode->setInverted(false);
//添加显示内容
clippingNode->addChild(_label);
//加入到UI树
addChild(clippingNode);
stencil->setColor(Color3B::BLACK);
addChild(stencil, -1);
return true;
}
void Marquee::show(const std::string& text)
{
_state = State::playing;
_label->setString(text);
_label->setPosition(Vec2(0, 0));
auto sequ = Sequence::create(
Show::create(),
MoveBy::create(5.0f, Vec2(-(_label->getContentSize().width + _showRect.size.width / 2), 0)),
Hide::create() , DelayTime::create(1.0f),
CCCallFunc::create([&]()
{
if (_texts.size() == 0)
{
_state = State::idle;
}
else
{
show(_texts.front());
_texts.pop();
}
}), nullptr);
_label->runAction(sequ);
}
void Marquee::addMessage(const std::string& text)
{
if (text.empty())
{
return;
}
if (_state == State::idle)
{
show(text);
}
else
{
_texts.push(text);
}
}
此处将show方法隐藏,并且提供了addMessage方法,内部实现了一个有限状态机,根据状态来显示剩余的消息,其使用方法与此前相似:
m = Marquee::create();
m->addMessage("----hhahahah veeeeee-----");
m->setPosition(Vec2(700, 300));
this->addChild(m);