MovableText(公告牌写字)

MovableText(公告牌写字)

在OGRE中,利用公告版是很简单的,但在公告版上写字的话就需要自己手动实现了,下面提供官方的一个类可供使用

使用方法:

MovableText * mTalk = new MovableText("talk",tempStr,"Game",10.0f);
mTalk->showOnTop(true);
mTalk->setTextAlignment(MovableText::H_CENTER, MovableText::V_ABOVE);
SceneNode* tempnode = mSceneMgr->createChildSceneNode("talk",Ogre::Vector3(0,70,0))->attachObject(mTalk);

上述方法中的new MovableText("talk",tempStr,"Game",10.0f);中这个"Game"肯定使用者回疑问,这是个字库,在OgreCore.zip中的Ogre.fontdef里添加

Game
{
type    truetype
source    game.ttf
size    32
resolution 55

code_points 19968-19968 20057-20057
}

上面的code_points是保存了UNICODE的10进制码,我是用的静态加载了常用的3500个字符与一些常用的符号,为了实现中文,使用了另找的中文字库

下载UNICODE的10进制码转换工具

http://www.cnblogs.com/gogoplayer/archive/2008/05/09/1189795.html

--------------MovableText.h------------------


#ifndef __MovableText_H__
#define __MovableText_H__

#include <Ogre.h>
#include <OgreFont.h>
#include <OgreFontManager.h>

namespace Ogre
{

class MovableText : public MovableObject, public Renderable
{
public:
   /// 水平对齐方式
   enum HorizontalAlignment
   {
    H_LEFT,   ///< 左对齐
    H_CENTER, ///< 居中
    H_RIGHT   ///< 右对齐
   };

   /// 垂直对齐方式
   enum VerticalAlignment
   {
    V_BELOW,///< 低端对齐
    V_ABOVE,///< 顶端对齐
    V_CENTER///< 居中
   };

protected:
   String     mFontName;///<
   String     mType;///<
   String     mName;///<
   DisplayString   mCaption;///< 
   HorizontalAlignment mHorizontalAlignment;///< 水平对齐方式
   VerticalAlignment mVerticalAlignment;///< 垂直对齐方式

   ColourValue    mColor;///<
   RenderOperation   mRenderOp;///<
   AxisAlignedBox   mAABB;///<
   LightList    mLList;///<

   Real     mCharHeight;///<
   Real     mSpaceWidth;///<

   bool     mNeedUpdate;///< 
   bool     mUpdateColors;///<
   bool     mOnTop;///<

   Real     mTimeUntilNextToggle;///<
   Real     mRadius;///< 包围半径
   Real     mAdditionalHeight;///<

   Camera*     mpCam;///< 摄像机指针
   RenderWindow*   mpWin;///< 渲染窗口指针
   FontPtr     mpFont;///< 字体指针
   MaterialPtr    mpMaterial;///< 
   MaterialPtr    mpBackgroundMaterial;///< 背景材质

   Vector3     mPositionOffset;///< 
   Vector3     mScaleOffset;///<

public:
   /// 构造函数
   /// @param[in] name 标识名
   /// @param[in] caption 字幕字符串
   /// @param[in] fontName 字体名
   /// @param[in] charHeight 字符高度
   /// @param[in] colour 字符颜色
   MovableText(const Ogre::String& name,
    const Ogre::DisplayString& caption,
    const Ogre::String& fontName = "BlueHighway",
    Ogre::Real charHeight = 1.0f,
    const Ogre::ColourValue& colour = Ogre::ColourValue::White);

   virtual ~MovableText(void);

   /// 设置字体名
   void    setFontName(const String &fontName);

   /// 设置显示字幕
   void    setCaption(const DisplayString &caption);

   /// 设置文字颜色
   void    setColor(const ColourValue &color);

   /// 设置文字高度
   void    setCharacterHeight(Real height);

   /// 设置间隔宽度
   void    setSpaceWidth(Real width);

   /// 设置文字对齐方式
   void    setTextAlignment(const HorizontalAlignment& horizontalAlignment,
    const VerticalAlignment& verticalAlignment);

   /// 设置
   void    setAdditionalHeight( Real height );

   /// 是否最前方显示
   void    showOnTop(bool show=true);

   /// 
   void setPositionOffset(const Ogre::Vector3& offset);

   /// 
   Ogre::Vector3 getPositionOffset() const { return mPositionOffset; }

   /// 
   void setScaleOffset(const Ogre::Vector3& offset);

   /// 
   Ogre::Vector3 getScaleOffset() const { return mScaleOffset; }

   /// 获取字体名
   const   String&    getFontName() const {return mFontName;}

   /// 获取字幕字符串
   const   DisplayString& getCaption() const {return mCaption;}

   /// 获取字体颜色
   const   ColourValue& getColor() const {return mColor;}

   /// 获取字符高度
   Real    getCharacterHeight() const {return mCharHeight;}

   /// 获取间隔宽度
   Real    getSpaceWidth() const {return mSpaceWidth;}

   /// 
   Real    getAdditionalHeight() const {return mAdditionalHeight;}

   /// 获取是否在最前方显示
   bool    getShowOnTop() const {return mOnTop;}

   /// 获取包围盒
   AxisAlignedBox         GetAABB(void) { return mAABB; }

protected:
   // from MovableText, create the object
   void _setupGeometry();
   void _updateColors();

   /// 获取世界坐标系中的变换
   void getWorldTransforms(Matrix4 *xform) const;

   /// 获取包围半径
   Real getBoundingRadius(void) const {return mRadius;};

   /// 获取摄像机的视深
   Real getSquaredViewDepth(const Camera *cam) const {return 0;};

   /// 获取世界坐标系中的朝向
   /// @note 一直面朝摄像机
   const   Quaternion&    getWorldOrientation(void) const;

   /// 获取在世界坐标系中的坐标
   const   Vector3&    getWorldPosition(void) const;

   /// 获取包围盒
   const   AxisAlignedBox&   getBoundingBox(void) const {return mAABB;};

   /// 获取标识名
   const   String&     getName(void) const {return mName;};

   /// 获取类型名
   const   String&     getMovableType(void) const {static Ogre::String movType = "MovableText"; return movType;};

   void    _notifyCurrentCamera(Camera *cam);
   void    _updateRenderQueue(RenderQueue* queue);

   // from renderable
   void    getRenderOperation(RenderOperation &op);
   const   MaterialPtr& getMaterial(void) const {assert(!mpMaterial.isNull());return mpMaterial;};
   const   LightList&   getLights(void) const {return mLList;};
};

}

#endif

--------------MovableText.cpp------------------


#include "Ogre.h"
#include "OgreFontManager.h"
#include "MovableText.h"

using namespace Ogre;

#define POS_TEX_BINDING    0
#define COLOUR_BINDING     1
#define UNICODE_NEL 0x0085   ///< next line
#define UNICODE_CR 0x000D   ///< carriage return
#define UNICODE_LF 0x000A   ///< line feed
#define UNICODE_SPACE 0x0020 ///< space
#define UNICODE_ZERO 0x0030   ///< 0

MovableText::MovableText(const String &name,
       const DisplayString &caption,
       const String &fontName,
       Real charHeight,
       const ColourValue &color)
       : mpCam(NULL)
       , mpWin(NULL)
       , mpFont(NULL)
       , mName(name)
       , mCaption(caption)
       , mFontName(fontName)
       , mCharHeight(charHeight)
       , mColor(color)
       , mType("MovableText")
       , mTimeUntilNextToggle(0)
       , mSpaceWidth(0)
       , mUpdateColors(true)
       , mOnTop(false)
       , mHorizontalAlignment(H_LEFT)
       , mVerticalAlignment(V_BELOW)
       , mAdditionalHeight(0.0)
       , mPositionOffset(Vector3::ZERO)
       , mScaleOffset(Vector3::UNIT_SCALE)
{
if (name == StringUtil::BLANK )
   throw Exception(Exception::ERR_INVALIDPARAMS, "Trying to create MovableText without name", "MovableText::MovableText");

if (caption == StringUtil::BLANK )
   throw Exception(Exception::ERR_INVALIDPARAMS, "Trying to create MovableText without caption", "MovableText::MovableText");

mRenderOp.vertexData = NULL;
this->setFontName(mFontName);
this->_setupGeometry();
}

MovableText::~MovableText()
{
if (mRenderOp.vertexData)
   delete mRenderOp.vertexData;
}

void MovableText::setFontName(const String &fontName)
{
if((Ogre::MaterialManager::getSingletonPtr()->resourceExists(mName + "Material"))) 

   Ogre::MaterialManager::getSingleton().remove(mName + "Material"); 
}

if (mFontName != fontName || mpMaterial.isNull() || mpFont.isNull())
{
   mFontName = fontName;
   mpFont = FontManager::getSingleton().getByName(mFontName);
   if (mpFont.isNull())
    throw Exception(Exception::ERR_ITEM_NOT_FOUND, "Could not find font " + fontName, "MovableText::setFontName");

   mpFont->load();
   if (!mpMaterial.isNull())
   {
    MaterialManager::getSingletonPtr()->remove(mpMaterial->getName());
    mpMaterial.setNull();
   }

   mpMaterial = mpFont->getMaterial()->clone(mName + "Material");
   if (!mpMaterial->isLoaded())
    mpMaterial->load();

   mpMaterial->setDepthCheckEnabled(!mOnTop);
#if 1
   mpMaterial->setDepthBias(1.0,1.0);
#else
   mpMaterial->setDepthBias(1.0f);
#endif
   mpMaterial->setDepthWriteEnabled(mOnTop);
   mpMaterial->setLightingEnabled(false);
   mNeedUpdate = true;
}
}

void MovableText::setCaption(const DisplayString &caption)
{
if (caption != mCaption)
{
   mCaption = caption;
   mNeedUpdate = true;
}
}

void MovableText::setColor(const ColourValue &color)
{
if (color != mColor)
{
   mColor = color;
   mUpdateColors = true;
}
}

void MovableText::setCharacterHeight(Real height)
{
if (height != mCharHeight)
{
   mCharHeight = height;
   mSpaceWidth = 0;
   mNeedUpdate = true;
}
}

void MovableText::setSpaceWidth(Real width)
{
if (width != mSpaceWidth)
{
   mSpaceWidth = width;
   mNeedUpdate = true;
}
}

void MovableText::setTextAlignment(const HorizontalAlignment& horizontalAlignment,
           const VerticalAlignment& verticalAlignment)
{
if(mHorizontalAlignment != horizontalAlignment)
{
   mHorizontalAlignment = horizontalAlignment;
   mNeedUpdate = true;
}
if(mVerticalAlignment != verticalAlignment)
{
   mVerticalAlignment = verticalAlignment;
   mNeedUpdate = true;
}
}

void MovableText::setAdditionalHeight( Real height )
{
if( mAdditionalHeight != height )
{
   mAdditionalHeight = height;
   mNeedUpdate = true;
}
}

void MovableText::showOnTop(bool show)
{
if( mOnTop != show && !mpMaterial.isNull() )
{
   mOnTop = show;
#if 1
   mpMaterial->setDepthBias(1.0f, 1.0f);
#else
   mpMaterial->setDepthBias(1.0f);
#endif
   mpMaterial->setDepthCheckEnabled(!mOnTop);
   mpMaterial->setDepthWriteEnabled(mOnTop);
}
}

void MovableText::_setupGeometry()
{
if (mpFont.isNull())
{
   return;
}

if (mpMaterial.isNull())
{
   return;
}

size_t charlen = mCaption.size();

size_t vertexCount = charlen * 6;

if (mRenderOp.vertexData)
{
   // Removed this test as it causes problems when replacing a caption
   // of the same size: replacing "Hello" with "hello"
   // as well as when changing the text alignment
   //if (mRenderOp.vertexData->vertexCount != vertexCount)
   {
    delete mRenderOp.vertexData;
    mRenderOp.vertexData = NULL;
    mUpdateColors = true;
   }
}

if (!mRenderOp.vertexData)
   mRenderOp.vertexData = new VertexData();

mRenderOp.indexData = 0;
mRenderOp.vertexData->vertexStart = 0;
mRenderOp.vertexData->vertexCount = vertexCount;
mRenderOp.operationType = RenderOperation::OT_TRIANGLE_LIST; 
mRenderOp.useIndexes = false;

VertexDeclaration *decl = mRenderOp.vertexData->vertexDeclaration;
VertexBufferBinding   *bind = mRenderOp.vertexData->vertexBufferBinding;
size_t offset = 0;

// create/bind positions/tex.ccord. buffer
if (!decl->findElementBySemantic(VES_POSITION))
   decl->addElement(POS_TEX_BINDING, offset, VET_FLOAT3, VES_POSITION);

offset += VertexElement::getTypeSize(VET_FLOAT3);

if (!decl->findElementBySemantic(VES_TEXTURE_COORDINATES))
   decl->addElement(POS_TEX_BINDING, offset, Ogre::VET_FLOAT2, Ogre::VES_TEXTURE_COORDINATES, 0);

HardwareVertexBufferSharedPtr ptbuf = HardwareBufferManager::getSingleton().createVertexBuffer(decl->getVertexSize(POS_TEX_BINDING),
   mRenderOp.vertexData->vertexCount,
   HardwareBuffer::HBU_DYNAMIC_WRITE_ONLY);
bind->setBinding(POS_TEX_BINDING, ptbuf);

// Colours - store these in a separate buffer because they change less often
if (!decl->findElementBySemantic(VES_DIFFUSE))
   decl->addElement(COLOUR_BINDING, 0, VET_COLOUR, VES_DIFFUSE);

HardwareVertexBufferSharedPtr cbuf = HardwareBufferManager::getSingleton().createVertexBuffer(decl->getVertexSize(COLOUR_BINDING),
   mRenderOp.vertexData->vertexCount,
   HardwareBuffer::HBU_DYNAMIC_WRITE_ONLY);
bind->setBinding(COLOUR_BINDING, cbuf);

float *pPCBuff = static_cast<float*>(ptbuf->lock(HardwareBuffer::HBL_DISCARD));

float largestWidth = 0;
float left = 0 * 2.0 - 1.0;
float top = -((0 * 2.0) - 1.0);

// Derive space width from a capital A
if (mSpaceWidth == 0)
   mSpaceWidth = mpFont->getGlyphAspectRatio('A') * mCharHeight * 2.0;

// for calculation of AABB
Ogre::Vector3 min, max, currPos;
Ogre::Real maxSquaredRadius = 0;
bool first = true;

// Use iterator
DisplayString::iterator i, iend;
iend = mCaption.end();
bool newLine = true;
Real len = 0.0f;

Real verticalOffset = 0;
switch (mVerticalAlignment)
{
case MovableText::V_ABOVE:
   verticalOffset = mCharHeight;
   break;
case MovableText::V_CENTER:
   verticalOffset = 0.5*mCharHeight;
   break;
case MovableText::V_BELOW:
   verticalOffset = 0;
   break;
}
// Raise the first line of the caption
top += verticalOffset;
for (i = mCaption.begin(); i != iend; ++i)
{
   Font::CodePoint character = OGRE_DEREF_DISPLAYSTRING_ITERATOR(i);
   if (character == UNICODE_CR
    || character == UNICODE_NEL
    || character == UNICODE_LF) 
   {
    top += verticalOffset * 2.0f;
   }
}

for (i = mCaption.begin(); i != iend; ++i)
{
   Font::CodePoint character = OGRE_DEREF_DISPLAYSTRING_ITERATOR(i);

   if (newLine)
   {
    len = 0.0f;
    for (DisplayString::iterator j = i; j != iend; j++)
    {
     Font::CodePoint cr = OGRE_DEREF_DISPLAYSTRING_ITERATOR(j);

     if (cr == UNICODE_CR
      || cr == UNICODE_NEL
      || cr == UNICODE_LF) 
     {
      break;
     }
     else if (cr == UNICODE_SPACE) // space
     {
      len += mSpaceWidth;
     }
     else 
     {
      len += mpFont->getGlyphAspectRatio(character) * mCharHeight * 2.0f;
     }
    }

    newLine = false;
   }

   if (character == UNICODE_CR
    || character == UNICODE_NEL
    || character == UNICODE_LF) 
   {
    left = 0 * 2.0 - 1.0;
    top -= mCharHeight * 2.0;
    newLine = true;
    continue;
   }

   if (character == UNICODE_SPACE)
   {
    // Just leave a gap, no tris
    left += mSpaceWidth;
    // Also reduce tri count
    mRenderOp.vertexData->vertexCount -= 6;
    continue;
   }

   Real horiz_height = mpFont->getGlyphAspectRatio(*i);
   Real u1, u2, v1, v2; 
   Ogre::Font::UVRect utmp;
   utmp = mpFont->getGlyphTexCoords(character);
   u1 = utmp.left;
   u2 = utmp.right;
   v1 = utmp.top;
   v2 = utmp.bottom;

   // each vert is (x, y, z, u, v)
   //-------------------------------------------------------------------------------------
   // First tri
   //
   // Upper left
   if(mHorizontalAlignment == MovableText::H_LEFT)
    *pPCBuff++ = left;
   else
    *pPCBuff++ = left - (len / 2);
   *pPCBuff++ = top;
   *pPCBuff++ = -1.0;
   *pPCBuff++ = u1;
   *pPCBuff++ = v1;

   // Deal with bounds
   if(mHorizontalAlignment == MovableText::H_LEFT)
    currPos = Ogre::Vector3(left, top, -1.0);
   else
    currPos = Ogre::Vector3(left - (len / 2), top, -1.0);
   if (first)
   {
    min = max = currPos;
    maxSquaredRadius = currPos.squaredLength();
    first = false;
   }
   else
   {
    min.makeFloor(currPos);
    max.makeCeil(currPos);
    maxSquaredRadius = std::max(maxSquaredRadius, currPos.squaredLength());
   }

   top -= mCharHeight * 2.0;

   // Bottom left
   if(mHorizontalAlignment == MovableText::H_LEFT)
    *pPCBuff++ = left;
   else
    *pPCBuff++ = left - (len / 2);
   *pPCBuff++ = top;
   *pPCBuff++ = -1.0;
   *pPCBuff++ = u1;
   *pPCBuff++ = v2;

   // Deal with bounds
   if(mHorizontalAlignment == MovableText::H_LEFT)
    currPos = Ogre::Vector3(left, top, -1.0);
   else
    currPos = Ogre::Vector3(left - (len / 2), top, -1.0);
   min.makeFloor(currPos);
   max.makeCeil(currPos);
   maxSquaredRadius = std::max(maxSquaredRadius, currPos.squaredLength());

   top += mCharHeight * 2.0;
   left += horiz_height * mCharHeight * 2.0;

   // Top right
   if(mHorizontalAlignment == MovableText::H_LEFT)
    *pPCBuff++ = left;
   else
    *pPCBuff++ = left - (len / 2);
   *pPCBuff++ = top;
   *pPCBuff++ = -1.0;
   *pPCBuff++ = u2;
   *pPCBuff++ = v1;
   //-------------------------------------------------------------------------------------

   // Deal with bounds
   if(mHorizontalAlignment == MovableText::H_LEFT)
    currPos = Ogre::Vector3(left, top, -1.0);
   else
    currPos = Ogre::Vector3(left - (len / 2), top, -1.0);
   min.makeFloor(currPos);
   max.makeCeil(currPos);
   maxSquaredRadius = std::max(maxSquaredRadius, currPos.squaredLength());

   //-------------------------------------------------------------------------------------
   // Second tri
   //
   // Top right (again)
   if(mHorizontalAlignment == MovableText::H_LEFT)
    *pPCBuff++ = left;
   else
    *pPCBuff++ = left - (len / 2);
   *pPCBuff++ = top;
   *pPCBuff++ = -1.0;
   *pPCBuff++ = u2;
   *pPCBuff++ = v1;

   currPos = Ogre::Vector3(left, top, -1.0);
   min.makeFloor(currPos);
   max.makeCeil(currPos);
   maxSquaredRadius = std::max(maxSquaredRadius, currPos.squaredLength());

   top -= mCharHeight * 2.0;
   left -= horiz_height * mCharHeight * 2.0;

   // Bottom left (again)
   if(mHorizontalAlignment == MovableText::H_LEFT)
    *pPCBuff++ = left;
   else
    *pPCBuff++ = left - (len / 2);
   *pPCBuff++ = top;
   *pPCBuff++ = -1.0;
   *pPCBuff++ = u1;
   *pPCBuff++ = v2;

   currPos = Ogre::Vector3(left, top, -1.0);
   min.makeFloor(currPos);
   max.makeCeil(currPos);
   maxSquaredRadius = std::max(maxSquaredRadius, currPos.squaredLength());

   left += horiz_height * mCharHeight * 2.0;

   // Bottom right
   if(mHorizontalAlignment == MovableText::H_LEFT)
    *pPCBuff++ = left;
   else
    *pPCBuff++ = left - (len / 2);
   *pPCBuff++ = top;
   *pPCBuff++ = -1.0;
   *pPCBuff++ = u2;
   *pPCBuff++ = v2;
   //-------------------------------------------------------------------------------------

   currPos = Ogre::Vector3(left, top, -1.0);
   min.makeFloor(currPos);
   max.makeCeil(currPos);
   maxSquaredRadius = std::max(maxSquaredRadius, currPos.squaredLength());

   // Go back up with top
   top += mCharHeight * 2.0;

   float currentWidth = (left + 1)/2 - 0;
   if (currentWidth > largestWidth)
    largestWidth = currentWidth;
}

// Unlock vertex buffer
ptbuf->unlock();

min.makeFloor(max); //When only spaces are typed in min can exceed max, this rectifies this

// update AABB/Sphere radius
mAABB = Ogre::AxisAlignedBox(min, max);
mRadius = Ogre::Math::Sqrt(maxSquaredRadius);

if (mUpdateColors)
   this->_updateColors();

mNeedUpdate = false;
}

void MovableText::_updateColors(void)
{
assert(!mpFont.isNull());
assert(!mpMaterial.isNull());

if (mpFont.isNull())
{
   return;
}

if (mpMaterial.isNull())
{
   return;
}

// Convert to system-specific
RGBA color;
Root::getSingleton().convertColourValue(mColor, &color);
HardwareVertexBufferSharedPtr vbuf = mRenderOp.vertexData->vertexBufferBinding->getBuffer(COLOUR_BINDING);
RGBA *pDest = static_cast<RGBA*>(vbuf->lock(HardwareBuffer::HBL_DISCARD));
for (uint i = 0; i < mRenderOp.vertexData->vertexCount; ++i)
   *pDest++ = color;
vbuf->unlock();
mUpdateColors = false;
}

const Quaternion& MovableText::getWorldOrientation(void) const
{
assert(mpCam);
return const_cast<Quaternion&>(mpCam->getDerivedOrientation());
}

const Vector3& MovableText::getWorldPosition(void) const
{
assert(mParentNode);
return mParentNode->_getDerivedPosition();
}

void MovableText::getWorldTransforms(Matrix4 *xform) const 
{
if (this->isVisible() && mpCam)
{
   Matrix3 rot3x3, scale3x3 = Matrix3::IDENTITY;

   // store rotation in a matrix
   mpCam->getDerivedOrientation().ToRotationMatrix(rot3x3);

   // parent node position
   Vector3 ppos = mParentNode->_getDerivedPosition() + Vector3::UNIT_Y*mAdditionalHeight;
   Quaternion pori = mParentNode->_getDerivedOrientation();
   Vector3 alignmentOffset = Vector3::ZERO;

   //ppos += pori * (mPositionOffset + alignmentOffset * 0.01);

   // apply scale
   scale3x3[0][0] = mParentNode->_getDerivedScale().x / 2;
   scale3x3[1][1] = mParentNode->_getDerivedScale().y / 2;
   scale3x3[2][2] = mParentNode->_getDerivedScale().z / 2;

   // apply all transforms to xform       
   *xform = (rot3x3 * scale3x3);
   xform->setTrans(ppos);
}
}

void MovableText::getRenderOperation(RenderOperation &op)
{
if (this->isVisible())
{
   if (mNeedUpdate)
    this->_setupGeometry();
   if (mUpdateColors)
    this->_updateColors();
   op = mRenderOp;
}
}

void MovableText::_notifyCurrentCamera(Camera *cam)
{
mpCam = cam;
}

void MovableText::_updateRenderQueue(RenderQueue* queue)
{
if (this->isVisible())
{
   if (mNeedUpdate)
    this->_setupGeometry();
   if (mUpdateColors)
    this->_updateColors();

   queue->addRenderable(this, mRenderQueueID, OGRE_RENDERABLE_DEFAULT_PRIORITY);
}
}

void Ogre::MovableText::setPositionOffset( const Ogre::Vector3& offset )
{
mPositionOffset = offset;
}

void Ogre::MovableText::setScaleOffset( const Ogre::Vector3& offset )
{
mScaleOffset = offset;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值