【羽毛不会飞】【教程】奇形怪状-不规则按钮实现

需求介绍

  • 触摸虚线内的不规则区域,触发响应逻辑

问题分析

  • 一般游戏引擎提供给我们的按钮一般都是矩型或者其他的规则形状,无法完成上述需求
  • 透明区域,图片的 alpha 通道值为 0(), 非透明区域 alpha 值不为 0,根据这个特征实现不规则边缘检测,从而判断响应事件是否触发响应逻辑
  • 实时点击扫描运算量较高,在本项目场景中没有运行效率没有问题,但是在实时高频场景性能吃紧,为了做到一次封装万年使用,因此需要在初始化过程存储 alpha 信息备用。
  • 美术工作: 提供不通的不规则按钮,N个按钮拼接成图中的不规则按钮

原理拓展

  • 图片存储原理

一个 4x3 像素的图片,以 RGBA8888 色彩模式加载到内存中后,那么会以一大串连续的数据存储在内存中,例如:

序号 1、2、3、4、5 各自表示一个像素点的信息,每一个像素点中的四个值分别代表 R(红色)、G(绿色)、B、(蓝色)、A(透明度),每个值的范围为 0 ~ 255(图中以 16 进制表示即范围 00~FF),每个值可用 8 个 bit 进行表示。

实现方案

0、头文件:

#ifndef __irregularButton_h__
#define __irregularButton_h__

#include "header.h"

class irregularButton : public Button {
public:
    irregularButton();
    irregularButton(int alphaCheckValue, bool isOptimized);
    virtual ~irregularButton();

    virtual bool init(const std::string& normalImage, const std::string& selectedImage = "", const std::string& disableImage = "", Widget::TextureResType texType = Widget::TextureResType::LOCAL) override;
    virtual bool hitTest(const Vec2 &pt, const Camera* camera, Vec3 *p) const override;

    /*
        @param alphaCheckValue          if image's alphaValue larger than alphaCheckValue,the click will happen
        @param isOptimized              if use bit to store the alpha data, it will use smaller memory
    */
    static irregularButton* create(const std::string& normalImage, const std::string& selectedImage = "", const std::string& disableImage = "",int alphaCheckValue = 0, bool isOptimized = false, Widget::TextureResType texType = Widget::TextureResType::LOCAL);
    /*
      @param alphaCheckValue          will use "btn" normalImage,selectedImage and disableImage to init the  irregularButton,will not change the "btn"
      @param alphaCheckValue          if image's alphaValue larger than alphaCheckValue,the click will happen
      @param isOptimized              if use bit to store the alpha data, it will use smaller memory
  */
    static irregularButton* createWithButton(Button * btn, int alphaCheckValue = 0, bool isOptimized = false);
protected:
    bool getIsTransparentAtPoint(cocos2d::Vec2 point) const;//获取点击到的像素数据是否大于checkAlphaValue
    void loadNormalTransparentInfo(std::string normalImage);   //初始化按钮
private:

    int _alphaCheckValue;
    int normalImageWidth_;
    int normalImageHeight_;

    bool _isOptimized;
    int _alphaDataWidth;
    int _alphaDataHeight;
    unsigned char * _imageAlpha;
    unsigned int _alphaDataLength;
};


#endif

1、读取像素值并数据判断每个像素点的 alpha 值,采取 bool 类型进行存储。

  • 内存分析:

    首先 RGBA(32bit)我们只取了 Alpha(8bit)通道进行判断,判断结果按 bool 值进行存储,一个 bool 值类型在内存中占 8bit,因此此时内存为图形内存的 1/4。

  • 优化

    为了进一步减少内存,增加了另一种按位存储的方式。

    倘若我们结合位的或运算将每 8 个判断结果存入一个 char 的每一个 Bit 中,该 bit 为 1 则不透明,为 0 则透明,那么内存又为原来 1/8,总的算起来就是图形内存的 1/32 拉。

  • 内存示例

    一张 1024x1024 的图,内存占用大小为 1024x1024x(32bit/8)= 4194304byte = 4096kb = 4m,那么我们的存储数组内存大小为 4096/32 = 128kb,应该可以应付绝大多数的变态内存需求了。

  • 先扫描进行存储的目的是避免用户进行操作时再进行扫描,这样可能在平凡操作中用户会有卡顿感以及手感不舒服。

  • 此处实现不一定判断值为 0,可为用户指定临界值。

void irregularButton::loadNormalTransparentInfo(std::string sName) {
    Image* normalImage = new Image();
    normalImage->initWithImageFile(sName);
    normalImageWidth_ = normalImage->getWidth();
    normalImageHeight_ = normalImage->getHeight();
    this->setContentSize(Size(normalImageWidth_, normalImageHeight_));
    if (_imageAlpha != nullptr) {
        delete[] _imageAlpha;
    }
    unsigned char* imageData = normalImage->getData();

    _alphaDataWidth = normalImageWidth_;//default
    _alphaDataHeight = normalImageHeight_;

    if (_isOptimized) {
        _alphaDataWidth = (normalImageWidth_ + 7) >> 3;//char == 8 bit,
        _alphaDataLength = _alphaDataWidth * normalImageHeight_ * sizeof(unsigned char);
        _imageAlpha = new unsigned char[_alphaDataLength];
        memset(_imageAlpha, 0, _alphaDataLength);
        for (int i = 0; i < normalImageHeight_; i++)
        {
            for (int j = 0; j < normalImageWidth_; j += 8)
            {
                int aj = j >> 3;
                for (int k = 0; k < 8; k++)
                {
                    if (j + k >= normalImageWidth_)
                    {
                        break;
                    }
                    unsigned char alpha = imageData[((i*normalImageWidth_ + j + k) << 2) + 3];
                    if (alpha > _alphaCheckValue)
                    {//set 1 in the corresponding bit position
                        int index = i * _alphaDataWidth + aj;
                        _imageAlpha[index] = _imageAlpha[index] | (1 << k);
                    }
                }
            }
        }
    }
    else {
        _alphaDataLength = normalImageWidth_ * normalImageHeight_ * sizeof(unsigned char);
        _imageAlpha = new unsigned char[_alphaDataLength];
        for (int i = 0; i < normalImageHeight_; i++)
        {
            for (int j = 0; j < normalImageWidth_; j++)
            {
                int index = i * normalImageWidth_ + j;
                _imageAlpha[index] = imageData[(index << 2) + 3];
            }
        }
    }
    delete normalImage;
}

2、拦截触摸,获得触摸位置。

我们通过继承 cocos2dx 中的 button 组件来封装我们的组件,通过阅读 Button 源代码,我们发现父类的虚函数 hitTest()会在触摸触发时拦截触摸,并触发我们注册的回调, 我们可以在该函数中调用判断 alpha 的函数。

bool irregularButton::hitTest(const Vec2 &pt, const Camera* camera, Vec3 *p) const {
    Vec2 localLocation = _buttonNormalRenderer->convertToNodeSpace(pt);
    Rect validTouchedRect;
    validTouchedRect.size = _buttonNormalRenderer->getContentSize();
    if (validTouchedRect.containsPoint(localLocation) && getIsTransparentAtPoint(localLocation))
    {
        //NotificationCenter::getInstance()->postNotification("NotifyIrregularBtn", (Ref*)m_iBtnID);
        return true;
    }
   return false;
}

3、判断触摸点是否大于指定的 alpha 值,这里要根据两种存储方式(是否按 bit 存储)分别采用不同的判断逻辑。

bool irregularButton::getIsTransparentAtPoint(cocos2d::Vec2 point) const {
    auto data = _imageAlpha;
    if (data == nullptr) {
        return true;
    }
    bool isTouchClaimed = false;
    auto locationInNode = point;//this->convertToNodeSpace(point);
    int x = (int)locationInNode.x;
    int y = (int)locationInNode.y;
    if (x >= 0 && x < normalImageWidth_ && y >= 0 && y < normalImageHeight_) {
        if (_isOptimized) {
            unsigned int i = (_alphaDataHeight - y - 1) * _alphaDataWidth + (x >> 3);
            unsigned char mask = 1 << (x & 0x07);
            if (i < _alphaDataLength && ((data[i] & mask) != 0))
            {
                isTouchClaimed = true;
            }
        }
        else
        {
            unsigned int i = (_alphaDataHeight - y - 1) * _alphaDataWidth + x;
            if (i < _alphaDataLength)
            {
                if ((unsigned int)data[i] > _alphaCheckValue)
                {
                    isTouchClaimed = true;
                }
            }
        }
    }
    return isTouchClaimed;
}
  • 注意在对象析构时释放存储判断结果的内存,否则会造成内存泄漏。

更多

.在编辑器上声明自定义数据数组

.包体优化指南

不规则3D地形行走

基于creator3.0的3D换装

快速实现3d抛物线绘制

奇形怪状-不规则按钮实现

今日技能你学废了吗?

更多精彩欢迎关注微信公众号?

8赞

需要预备处理,不知道速度怎么样

厉害了,这么好的文章竟然没有点赞,感谢分享,最近刚好在开发类似功能。

mark,备用

mark!!!

mark!

我记得好像不需要预处理吧,可以拿到imagedata,纹理信息本来就已经在内存里有一份了

这个做法很牛叉啊,以前的做法是:先将区域用若干个点大概描出来,然后数学计算点击的位置是否在区域内,手机上不用很精确的话勉强能满足需求。