Cocos2d-x 自定义UI组件开发(继承Widget/Node)深度指南
【摘要】 引言在游戏开发中,UI系统是用户体验的核心组成部分。Cocos2d-x虽然提供了丰富的内置UI组件,但实际项目中往往需要定制化的UI元素来满足特定需求。通过继承Widget或Node创建自定义UI组件,开发者可以实现高度灵活的界面系统。本文将深入探讨Cocos2d-x自定义UI组件开发的全过程,从基础概念到高级应用,提供完整的实现方案和最佳实践。技术背景UI组件继承体系graph TD ...
引言
技术背景
UI组件继承体系
graph TD
A[cocos2d::Node] --> B[cocos2d::ui::Widget]
B --> C[内置组件]
B --> D[自定义组件]
A --> E[自定义Node组件]
E --> F[复合组件]
E --> G[特殊效果组件]
继承Widget vs Node对比
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
组件生命周期
graph LR
A[创建] --> B[初始化]
B --> C[添加到场景]
C --> D[激活]
D --> E[交互]
E --> F[停用]
F --> G[移除]
G --> H[销毁]
应用使用场景
-
游戏HUD系统: -
自定义血条/能量条 -
技能冷却指示器 -
小地图组件
-
-
数据可视化: -
动态图表(柱状图、饼图) -
实时数据仪表盘 -
排行榜面板
-
-
交互控件: -
自定义转盘/选择器 -
拖拽排序列表 -
手势识别控件
-
-
动画效果: -
粒子效果控制器 -
过渡动画组件 -
变形动画控件
-
-
特殊功能: -
虚拟摇杆 -
手势密码锁 -
3D卡片翻转效果
-
不同场景下详细代码实现
场景1:继承Widget的自定义按钮(带特效)
// CustomButton.h
#ifndef __CUSTOM_BUTTON_H__
#define __CUSTOM_BUTTON_H__
#include "cocos2d.h"
#include "ui/CocosGUI.h"
USING_NS_CC;
using namespace ui;
class CustomButton : public Widget {
public:
static CustomButton* create(const std::string& normalImage,
const std::string& pressedImage,
const std::string& disabledImage = "");
virtual bool init(const std::string& normalImage,
const std::string& pressedImage,
const std::string& disabledImage);
void setClickCallback(const std::function<void()>& callback);
void setEnabled(bool enabled) override;
// 特效相关
void setPressEffectEnabled(bool enabled);
void setScaleEffect(float scaleFactor);
protected:
virtual bool onTouchBegan(Touch* touch, Event* event) override;
virtual void onTouchMoved(Touch* touch, Event* event) override;
virtual void onTouchEnded(Touch* touch, Event* event) override;
virtual void onTouchCancelled(Touch* touch, Event* event) override;
private:
void updateState();
void applyPressEffect();
void removePressEffect();
Sprite* _normalSprite;
Sprite* _pressedSprite;
Sprite* _disabledSprite;
std::function<void()> _clickCallback;
bool _pressEffectEnabled;
float _scaleEffectFactor;
bool _isPressed;
};
#endif // __CUSTOM_BUTTON_H__
// CustomButton.cpp
#include "CustomButton.h"
CustomButton* CustomButton::create(const std::string& normalImage,
const std::string& pressedImage,
const std::string& disabledImage) {
auto button = new (std::nothrow) CustomButton();
if (button && button->init(normalImage, pressedImage, disabledImage)) {
button->autorelease();
return button;
}
CC_SAFE_DELETE(button);
return nullptr;
}
bool CustomButton::init(const std::string& normalImage,
const std::string& pressedImage,
const std::string& disabledImage) {
if (!Widget::init()) {
return false;
}
// 创建精灵
_normalSprite = Sprite::create(normalImage);
_pressedSprite = Sprite::create(pressedImage);
if (!disabledImage.empty()) {
_disabledSprite = Sprite::create(disabledImage);
}
// 设置初始状态
_normalSprite->setVisible(true);
_pressedSprite->setVisible(false);
if (_disabledSprite) _disabledSprite->setVisible(false);
// 添加到自身
addChild(_normalSprite);
addChild(_pressedSprite);
if (_disabledSprite) addChild(_disabledSprite);
// 设置内容大小
setContentSize(_normalSprite->getContentSize());
// 启用触摸
setTouchEnabled(true);
setSwallowTouches(true);
// 初始化特效参数
_pressEffectEnabled = true;
_scaleEffectFactor = 0.95f;
_isPressed = false;
return true;
}
void CustomButton::setClickCallback(const std::function<void()>& callback) {
_clickCallback = callback;
}
void CustomButton::setEnabled(bool enabled) {
Widget::setEnabled(enabled);
updateState();
}
void CustomButton::setPressEffectEnabled(bool enabled) {
_pressEffectEnabled = enabled;
}
void CustomButton::setScaleEffect(float scaleFactor) {
_scaleEffectFactor = scaleFactor;
}
bool CustomButton::onTouchBegan(Touch* touch, Event* event) {
if (!isVisible() || !isEnabled()) {
return false;
}
Rect rect = getBoundingBox();
Vec2 localPoint = convertToNodeSpace(touch->getLocation());
if (rect.containsPoint(localPoint)) {
_isPressed = true;
updateState();
if (_pressEffectEnabled) {
applyPressEffect();
}
return true;
}
return false;
}
void CustomButton::onTouchMoved(Touch* touch, Event* event) {
if (!_isPressed) return;
Rect rect = getBoundingBox();
Vec2 localPoint = convertToNodeSpace(touch->getLocation());
bool inside = rect.containsPoint(localPoint);
if (inside != _isPressed) {
_isPressed = inside;
updateState();
if (_pressEffectEnabled) {
if (_isPressed) {
applyPressEffect();
} else {
removePressEffect();
}
}
}
}
void CustomButton::onTouchEnded(Touch* touch, Event* event) {
if (!_isPressed) return;
_isPressed = false;
updateState();
if (_pressEffectEnabled) {
removePressEffect();
}
Rect rect = getBoundingBox();
Vec2 localPoint = convertToNodeSpace(touch->getLocation());
if (rect.containsPoint(localPoint) && _clickCallback) {
_clickCallback();
}
}
void CustomButton::onTouchCancelled(Touch* touch, Event* event) {
_isPressed = false;
updateState();
if (_pressEffectEnabled) {
removePressEffect();
}
}
void CustomButton::updateState() {
if (!isEnabled()) {
_normalSprite->setVisible(false);
_pressedSprite->setVisible(false);
if (_disabledSprite) _disabledSprite->setVisible(true);
} else if (_isPressed) {
_normalSprite->setVisible(false);
_pressedSprite->setVisible(true);
if (_disabledSprite) _disabledSprite->setVisible(false);
} else {
_normalSprite->setVisible(true);
_pressedSprite->setVisible(false);
if (_disabledSprite) _disabledSprite->setVisible(false);
}
}
void CustomButton::applyPressEffect() {
if (_scaleEffectFactor != 1.0f) {
runAction(ScaleTo::create(0.1f, _scaleEffectFactor));
}
}
void CustomButton::removePressEffect() {
runAction(ScaleTo::create(0.1f, 1.0f));
}
场景2:继承Node的自定义进度条
// CustomProgressBar.h
#ifndef __CUSTOM_PROGRESS_BAR_H__
#define __CUSTOM_PROGRESS_BAR_H__
#include "cocos2d.h"
USING_NS_CC;
class CustomProgressBar : public Node {
public:
enum class Direction {
LEFT_TO_RIGHT,
RIGHT_TO_LEFT,
TOP_TO_BOTTOM,
BOTTOM_TO_TOP
};
static CustomProgressBar* create(const std::string& bgImage,
const std::string& fillImage);
virtual bool init(const std::string& bgImage,
const std::string& fillImage);
void setPercentage(float percentage);
float getPercentage() const { return _percentage; }
void setDirection(Direction direction);
Direction getDirection() const { return _direction; }
void setShowPercentageText(bool show);
void setTextColor(const Color3B& color);
void setTextFontSize(int size);
private:
void updateProgress();
void updateText();
Sprite* _bgSprite;
Sprite* _fillSprite;
Label* _percentLabel;
float _percentage;
Direction _direction;
bool _showText;
Color3B _textColor;
int _textSize;
};
#endif // __CUSTOM_PROGRESS_BAR_H__
// CustomProgressBar.cpp
#include "CustomProgressBar.h"
CustomProgressBar* CustomProgressBar::create(const std::string& bgImage,
const std::string& fillImage) {
auto bar = new (std::nothrow) CustomProgressBar();
if (bar && bar->init(bgImage, fillImage)) {
bar->autorelease();
return bar;
}
CC_SAFE_DELETE(bar);
return nullptr;
}
bool CustomProgressBar::init(const std::string& bgImage,
const std::string& fillImage) {
if (!Node::init()) {
return false;
}
// 创建背景精灵
_bgSprite = Sprite::create(bgImage);
addChild(_bgSprite);
setContentSize(_bgSprite->getContentSize());
// 创建填充精灵
_fillSprite = Sprite::create(fillImage);
_fillSprite->setAnchorPoint(Vec2(0, 0.5)); // 左对齐
_fillSprite->setPosition(Vec2(0, _bgSprite->getContentSize().height / 2));
addChild(_fillSprite);
// 创建百分比文本
_percentLabel = Label::createWithSystemFont("0%", "Arial", 20);
_percentLabel->setPosition(Vec2(_bgSprite->getContentSize().width / 2,
_bgSprite->getContentSize().height / 2));
addChild(_percentLabel);
// 初始化参数
_percentage = 0.0f;
_direction = Direction::LEFT_TO_RIGHT;
_showText = true;
_textColor = Color3B::WHITE;
_textSize = 20;
// 更新显示
updateProgress();
updateText();
return true;
}
void CustomProgressBar::setPercentage(float percentage) {
_percentage = MAX(0.0f, MIN(1.0f, percentage));
updateProgress();
updateText();
}
void CustomProgressBar::setDirection(Direction direction) {
_direction = direction;
updateProgress();
}
void CustomProgressBar::setShowPercentageText(bool show) {
_showText = show;
_percentLabel->setVisible(show);
}
void CustomProgressBar::setTextColor(const Color3B& color) {
_textColor = color;
_percentLabel->setTextColor(color);
}
void CustomProgressBar::setTextFontSize(int size) {
_textSize = size;
_percentLabel->setSystemFontSize(size);
}
void CustomProgressBar::updateProgress() {
Size bgSize = _bgSprite->getContentSize();
float fillWidth = bgSize.width * _percentage;
float fillHeight = bgSize.height * _percentage;
switch (_direction) {
case Direction::LEFT_TO_RIGHT:
_fillSprite->setScaleX(fillWidth / _fillSprite->getContentSize().width);
break;
case Direction::RIGHT_TO_LEFT:
_fillSprite->setScaleX(fillWidth / _fillSprite->getContentSize().width);
_fillSprite->setPositionX(bgSize.width - fillWidth);
break;
case Direction::TOP_TO_BOTTOM:
_fillSprite->setScaleY(fillHeight / _fillSprite->getContentSize().height);
_fillSprite->setAnchorPoint(Vec2(0.5, 1));
_fillSprite->setPositionY(bgSize.height);
break;
case Direction::BOTTOM_TO_TOP:
_fillSprite->setScaleY(fillHeight / _fillSprite->getContentSize().height);
_fillSprite->setAnchorPoint(Vec2(0.5, 0));
break;
}
}
void CustomProgressBar::updateText() {
if (_showText) {
int percent = static_cast<int>(_percentage * 100);
_percentLabel->setString(StringUtils::format("%d%%", percent));
}
}
场景3:复合组件(继承Widget的自定义面板)
// CustomPanel.h
#ifndef __CUSTOM_PANEL_H__
#define __CUSTOM_PANEL_H__
#include "cocos2d.h"
#include "ui/CocosGUI.h"
#include "CustomButton.h"
#include "CustomProgressBar.h"
USING_NS_CC;
using namespace ui;
class CustomPanel : public Widget {
public:
static CustomPanel* create(const std::string& backgroundImage);
virtual bool init(const std::string& backgroundImage);
void setTitle(const std::string& title);
void addCloseButton(const std::string& normalImage,
const std::string& pressedImage);
void addProgressBar(CustomProgressBar* progressBar);
void setContentSize(const Size& size) override;
void show();
void hide();
private:
void layoutChildren();
Sprite* _bgSprite;
Label* _titleLabel;
CustomButton* _closeButton;
Vector<CustomProgressBar*> _progressBars;
bool _isShowing;
};
#endif // __CUSTOM_PANEL_H__
// CustomPanel.cpp
#include "CustomPanel.h"
CustomPanel* CustomPanel::create(const std::string& backgroundImage) {
auto panel = new (std::nothrow) CustomPanel();
if (panel && panel->init(backgroundImage)) {
panel->autorelease();
return panel;
}
CC_SAFE_DELETE(panel);
return nullptr;
}
bool CustomPanel::init(const std::string& backgroundImage) {
if (!Widget::init()) {
return false;
}
// 创建背景
_bgSprite = Sprite::create(backgroundImage);
addChild(_bgSprite);
setContentSize(_bgSprite->getContentSize());
// 创建标题标签
_titleLabel = Label::createWithTTF("Panel Title", "fonts/arial.ttf", 24);
_titleLabel->setColor(Color3B::WHITE);
_titleLabel->setPosition(Vec2(getContentSize().width/2,
getContentSize().height - 30));
addChild(_titleLabel);
// 初始化其他成员
_closeButton = nullptr;
_isShowing = true;
return true;
}
void CustomPanel::setTitle(const std::string& title) {
_titleLabel->setString(title);
}
void CustomPanel::addCloseButton(const std::string& normalImage,
const std::string& pressedImage) {
if (_closeButton) {
removeChild(_closeButton);
}
_closeButton = CustomButton::create(normalImage, pressedImage);
_closeButton->setPosition(Vec2(getContentSize().width - 30,
getContentSize().height - 30));
_closeButton->setClickCallback([this]() {
hide();
});
addChild(_closeButton);
}
void CustomPanel::addProgressBar(CustomProgressBar* progressBar) {
_progressBars.pushBack(progressBar);
addChild(progressBar);
layoutChildren();
}
void CustomPanel::setContentSize(const Size& size) {
Widget::setContentSize(size);
_bgSprite->setContentSize(size);
layoutChildren();
}
void CustomPanel::show() {
if (!_isShowing) {
setVisible(true);
_isShowing = true;
runAction(ScaleTo::create(0.3f, 1.0f));
}
}
void CustomPanel::hide() {
if (_isShowing) {
_isShowing = false;
auto action = Sequence::create(
ScaleTo::create(0.3f, 0.8f),
CallFunc::create([this]() {
setVisible(false);
}),
nullptr
);
runAction(action);
}
}
void CustomPanel::layoutChildren() {
// 布局标题
_titleLabel->setPosition(Vec2(getContentSize().width/2,
getContentSize().height - 30));
// 布局关闭按钮
if (_closeButton) {
_closeButton->setPosition(Vec2(getContentSize().width - 30,
getContentSize().height - 30));
}
// 布局进度条
float yPos = getContentSize().height - 60;
for (auto bar : _progressBars) {
bar->setPosition(Vec2(getContentSize().width/2, yPos));
yPos -= bar->getContentSize().height + 10;
}
}
原理解释
自定义组件工作原理
-
继承体系: -
继承Widget获得UI组件特性(触摸、布局) -
继承Node获得基础节点功能(变换、渲染)
-
-
事件处理机制: -
重写触摸事件方法(onTouchBegan等) -
使用EventListener处理自定义事件 -
通过回调函数与外部交互
-
-
渲染流程: -
组件作为Node参与场景图渲染 -
子节点按添加顺序渲染 -
可通过visit方法自定义渲染
-
-
状态管理: -
维护组件内部状态(按下、禁用等) -
根据状态更新视觉表现 -
提供外部状态查询接口
-
关键技术点
-
触摸事件分发: -
使用EventListenerTouchOneByOne -
处理触摸点坐标转换 -
管理触摸事件吞噬
-
-
布局计算: -
重写doLayout方法 -
处理锚点和位置偏移 -
支持自适应布局
-
-
资源管理: -
使用SpriteFrameCache管理纹理 -
实现组件资源的动态加载 -
处理资源释放
-
-
动画集成: -
使用Action系统实现动画 -
支持属性动画 -
实现缓动效果
-
核心特性
-
高度可定制化: -
自由组合基础元素 -
自定义渲染效果 -
灵活的状态管理
-
-
事件驱动架构: -
内置触摸事件处理 -
自定义事件系统 -
回调函数机制
-
-
布局灵活性: -
支持绝对/相对布局 -
自动/手动布局模式 -
响应式设计
-
-
动画支持: -
内置动作系统 -
支持关键帧动画 -
平滑过渡效果
-
-
性能优化: -
脏矩形渲染 -
对象池重用 -
按需更新
-
原理流程图及解释
组件创建流程
graph TD
A[调用create工厂方法] --> B[new操作符创建实例]
B --> C[调用init初始化]
C --> D[创建子节点]
D --> E[设置初始状态]
E --> F[添加到场景]
F --> G[激活事件监听]
-
通过静态create方法创建实例 -
在构造函数中分配内存 -
调用init方法进行初始化 -
创建子节点并添加到组件 -
设置初始状态和属性 -
添加到场景图中 -
激活事件监听系统
事件处理流程
graph TD
A[触摸事件产生] --> B[事件分发器]
B --> C[组件节点]
C --> D{触摸点在区域内?}
D -->|是| E[调用onTouchBegan]
E --> F{返回true?}
F -->|是| G[标记为处理中]
G --> H[后续事件]
F -->|否| I[传递下层节点]
D -->|否| I
-
触摸事件由系统产生 -
事件分发器遍历场景图 -
到达组件节点时检查触摸点 -
在区域内调用onTouchBegan -
若返回true则处理后续事件 -
否则事件继续传递
环境准备
开发环境要求
-
操作系统:Windows 10/macOS 10.15+/Linux -
IDE:Visual Studio 2019+/Xcode 12+/CLion -
引擎版本:Cocos2d-x v3.17+ 或 v4.x -
编程语言:C++11+ -
依赖库: -
OpenGL ES 2.0+ -
OpenAL -
Freetype(字体渲染)
-
项目配置
-
创建Cocos2d-x项目: cocos new CustomUIProject -p com.yourcompany.customui -l cpp cd CustomUIProject -
添加自定义组件源文件: -
CustomButton.h/cpp -
CustomProgressBar.h/cpp -
CustomPanel.h/cpp
-
-
配置CMakeLists.txt: # 添加源文件 list(APPEND GAME_SOURCE Classes/CustomButton.cpp Classes/CustomProgressBar.cpp Classes/CustomPanel.cpp ) # 包含目录 include_directories( Classes ${COCOS2D_ROOT}/cocos ${COCOS2D_ROOT}/cocos/ui ) -
准备资源文件: -
按钮图片:button_normal.png, button_pressed.png -
进度条图片:progress_bg.png, progress_fill.png -
面板背景:panel_bg.png -
字体文件:arial.ttf
-
实际详细应用代码示例实现
主场景集成自定义组件
// HelloWorldScene.cpp
#include "HelloWorldScene.h"
#include "CustomButton.h"
#include "CustomProgressBar.h"
#include "CustomPanel.h"
USING_NS_CC;
Scene* HelloWorld::createScene() {
return HelloWorld::create();
}
bool HelloWorld::init() {
if (!Scene::init()) {
return false;
}
auto visibleSize = Director::getInstance()->getVisibleSize();
Vec2 origin = Director::getInstance()->getVisibleOrigin();
// 创建自定义按钮
auto startButton = CustomButton::create(
"button_normal.png",
"button_pressed.png",
"button_disabled.png"
);
startButton->setPosition(Vec2(visibleSize.width/2, visibleSize.height/2 + 100));
startButton->setClickCallback([]() {
log("Start button clicked!");
});
startButton->setScaleEffect(0.9f);
this->addChild(startButton);
// 创建自定义进度条
auto healthBar = CustomProgressBar::create(
"progress_bg.png",
"health_fill.png"
);
healthBar->setPosition(Vec2(visibleSize.width/2, visibleSize.height/2));
healthBar->setPercentage(0.75f);
healthBar->setDirection(CustomProgressBar::Direction::LEFT_TO_RIGHT);
healthBar->setShowPercentageText(true);
healthBar->setTextColor(Color3B::GREEN);
this->addChild(healthBar);
// 创建自定义面板
auto panel = CustomPanel::create("panel_bg.png");
panel->setPosition(Vec2(visibleSize.width/2, visibleSize.height/2 - 100));
panel->setTitle("Game Stats");
panel->addCloseButton("close_normal.png", "close_pressed.png");
panel->setScale(0.8f);
panel->setOpacity(0); // 初始透明
this->addChild(panel);
// 添加进度条到面板
auto manaBar = CustomProgressBar::create(
"progress_bg.png",
"mana_fill.png"
);
manaBar->setPercentage(0.5f);
manaBar->setDirection(CustomProgressBar::Direction::LEFT_TO_RIGHT);
panel->addProgressBar(manaBar);
auto expBar = CustomProgressBar::create(
"progress_bg.png",
"exp_fill.png"
);
expBar->setPercentage(0.3f);
expBar->setDirection(CustomProgressBar::Direction::LEFT_TO_RIGHT);
panel->addProgressBar(expBar);
// 显示面板按钮
auto showPanelBtn = CustomButton::create(
"button_normal.png",
"button_pressed.png"
);
showPanelBtn->setPosition(Vec2(visibleSize.width/2, visibleSize.height/2 - 180));
showPanelBtn->setClickCallback([panel]() {
panel->show();
});
this->addChild(showPanelBtn);
// 动画效果
panel->runAction(FadeIn::create(1.0f));
startButton->runAction(RepeatForever::create(
Sequence::create(
ScaleTo::create(0.5f, 1.05f),
ScaleTo::create(0.5f, 1.0f),
nullptr
)
));
return true;
}
高级自定义组件:虚拟摇杆
// VirtualJoystick.h
#ifndef __VIRTUAL_JOYSTICK_H__
#define __VIRTUAL_JOYSTICK_H__
#include "cocos2d.h"
#include "ui/CocosGUI.h"
USING_NS_CC;
using namespace ui;
class VirtualJoystick : public Widget {
public:
static VirtualJoystick* create(const std::string& bgImage,
const std::string& thumbImage);
virtual bool init(const std::string& bgImage,
const std::string& thumbImage);
Vec2 getDirection() const { return _direction; }
float getPower() const { return _power; }
bool isActive() const { return _isActive; }
void setDeadZone(float zone) { _deadZone = zone; }
void setCallback(const std::function<void(const Vec2&)>& callback);
protected:
virtual bool onTouchBegan(Touch* touch, Event* event) override;
virtual void onTouchMoved(Touch* touch, Event* event) override;
virtual void onTouchEnded(Touch* touch, Event* event) override;
virtual void onTouchCancelled(Touch* touch, Event* event) override;
private:
void updateThumbPosition(const Vec2& touchPos);
void resetThumb();
Sprite* _bgSprite;
Sprite* _thumbSprite;
Vec2 _direction;
float _power;
bool _isActive;
float _deadZone;
std::function<void(const Vec2&)> _callback;
};
#endif // __VIRTUAL_JOYSTICK_H__
// VirtualJoystick.cpp
#include "VirtualJoystick.h"
VirtualJoystick* VirtualJoystick::create(const std::string& bgImage,
const std::string& thumbImage) {
auto joystick = new (std::nothrow) VirtualJoystick();
if (joystick && joystick->init(bgImage, thumbImage)) {
joystick->autorelease();
return joystick;
}
CC_SAFE_DELETE(joystick);
return nullptr;
}
bool VirtualJoystick::init(const std::string& bgImage,
const std::string& thumbImage) {
if (!Widget::init()) {
return false;
}
// 创建背景精灵
_bgSprite = Sprite::create(bgImage);
addChild(_bgSprite);
setContentSize(_bgSprite->getContentSize());
// 创建拇指精灵
_thumbSprite = Sprite::create(thumbImage);
_thumbSprite->setPosition(Vec2(_bgSprite->getContentSize().width/2,
_bgSprite->getContentSize().height/2));
addChild(_thumbSprite);
// 初始化参数
_direction = Vec2::ZERO;
_power = 0.0f;
_isActive = false;
_deadZone = 0.2f;
_callback = nullptr;
// 启用触摸
setTouchEnabled(true);
setSwallowTouches(true);
return true;
}
void VirtualJoystick::setCallback(const std::function<void(const Vec2&)>& callback) {
_callback = callback;
}
bool VirtualJoystick::onTouchBegan(Touch* touch, Event* event) {
if (!isVisible() || !isEnabled()) {
return false;
}
Vec2 touchPos = convertToNodeSpace(touch->getLocation());
Rect bgRect = Rect(0, 0,
_bgSprite->getContentSize().width,
_bgSprite->getContentSize().height);
if (bgRect.containsPoint(touchPos)) {
_isActive = true;
updateThumbPosition(touchPos);
return true;
}
return false;
}
void VirtualJoystick::onTouchMoved(Touch* touch, Event* event) {
if (!_isActive) return;
Vec2 touchPos = convertToNodeSpace(touch->getLocation());
updateThumbPosition(touchPos);
}
void VirtualJoystick::onTouchEnded(Touch* touch, Event* event) {
if (!_isActive) return;
_isActive = false;
resetThumb();
}
void VirtualJoystick::onTouchCancelled(Touch* touch, Event* event) {
onTouchEnded(touch, event);
}
void VirtualJoystick::updateThumbPosition(const Vec2& touchPos) {
Vec2 center(_bgSprite->getContentSize().width/2,
_bgSprite->getContentSize().height/2);
Vec2 offset = touchPos - center;
// 限制在圆形范围内
float radius = _bgSprite->getContentSize().width/2;
float distance = offset.length();
if (distance > radius) {
offset = offset.normalize() * radius;
}
// 更新拇指位置
_thumbSprite->setPosition(center + offset);
// 计算方向和力度
_direction = offset / radius;
_power = distance / radius;
// 应用死区
if (_power < _deadZone) {
_direction = Vec2::ZERO;
_power = 0.0f;
}
// 触发回调
if (_callback) {
_callback(_direction);
}
}
void VirtualJoystick::resetThumb() {
Vec2 center(_bgSprite->getContentSize().width/2,
_bgSprite->getContentSize().height/2);
_thumbSprite->setPosition(center);
_direction = Vec2::ZERO;
_power = 0.0f;
if (_callback) {
_callback(_direction);
}
}
运行结果
主场景效果
+---------------------------+
| [开始按钮] |
| |
| [=========75%=======] | // 血条
| |
| +---------------------+ |
| | Game Stats | | // 面板
| | [========50%======] | | // 法力条
| | [====30%========] | | // 经验条
| | [关闭按钮] | |
| +---------------------+ |
| |
| [显示面板按钮] |
+---------------------------+
虚拟摇杆效果
+-----------------+
| O | // 背景
| ● | // 拇指
+-----------------+
组件交互效果
-
按钮点击时有缩放动画和状态变化 -
进度条实时显示数值百分比 -
面板可显示/隐藏,带有动画效果 -
虚拟摇杆拇指随触摸移动,计算方向和力度
测试步骤以及详细代码
单元测试框架
// TestCustomComponents.cpp
#include "gtest/gtest.h"
#include "CustomButton.h"
#include "CustomProgressBar.h"
#include "VirtualJoystick.h"
USING_NS_CC;
TEST(CustomButtonTest, Creation) {
auto button = CustomButton::create("btn_n.png", "btn_p.png");
EXPECT_NE(button, nullptr);
EXPECT_TRUE(button->getChildren().size() >= 2);
}
TEST(CustomButtonTest, ClickEvent) {
int clickCount = 0;
auto button = CustomButton::create("btn_n.png", "btn_p.png");
button->setClickCallback([&]() {
clickCount++;
});
// 模拟触摸事件
auto touch = Touch::create();
touch->setLocation(Vec2(100, 100));
button->setPosition(Vec2(100, 100));
button->setContentSize(Size(200, 50));
EventTouch event;
EXPECT_TRUE(button->onTouchBegan(touch, &event));
button->onTouchEnded(touch, &event);
EXPECT_EQ(clickCount, 1);
}
TEST(CustomProgressBarTest, Percentage) {
auto bar = CustomProgressBar::create("bg.png", "fill.png");
bar->setPercentage(0.5f);
EXPECT_FLOAT_EQ(bar->getPercentage(), 0.5f);
bar->setPercentage(1.5f); // 超过最大值
EXPECT_FLOAT_EQ(bar->getPercentage(), 1.0f);
bar->setPercentage(-0.5f); // 低于最小值
EXPECT_FLOAT_EQ(bar->getPercentage(), 0.0f);
}
TEST(VirtualJoystickTest, DirectionCalculation) {
auto joystick = VirtualJoystick::create("bg.png", "thumb.png");
joystick->setPosition(Vec2(100, 100));
// 模拟右下角触摸
auto touch = Touch::create();
touch->setLocation(Vec2(150, 50)); // 相对于摇杆中心(100,100)的偏移(50,-50)
EventTouch event;
joystick->onTouchBegan(touch, &event);
Vec2 dir = joystick->getDirection();
float power = joystick->getPower();
EXPECT_GT(dir.x, 0);
EXPECT_LT(dir.y, 0);
EXPECT_FLOAT_EQ(power, 0.7071f); // sqrt(0.5^2 + 0.5^2) ≈ 0.707
}
手动测试步骤
-
编译并运行程序 -
验证按钮状态变化: -
正常状态显示normal图片 -
按下状态显示pressed图片 -
禁用状态显示disabled图片
-
-
测试进度条: -
修改百分比值观察填充变化 -
切换不同方向模式 -
显示/隐藏百分比文本
-
-
测试面板: -
点击关闭按钮隐藏面板 -
点击显示面板按钮重新显示 -
验证面板内进度条布局
-
-
测试虚拟摇杆: -
触摸并拖动拇指 -
验证方向和力度计算 -
松开手指后拇指复位
-
部署场景
-
移动游戏: -
虚拟摇杆控制角色移动 -
技能按钮和血条显示 -
游戏设置面板
-
-
跨平台应用: -
桌面端配置工具 -
移动端控制界面 -
嵌入式设备控制面板
-
-
虚拟现实: -
VR菜单系统 -
3D空间中的交互控件 -
手势识别界面
-
-
数据可视化: -
实时监控仪表盘 -
可交互数据图表 -
动态报告生成器
-
-
教育应用: -
交互式教学工具 -
虚拟实验控制面板 -
学习进度跟踪器
-
疑难解答
问题1:触摸事件不响应
-
未启用触摸: setTouchEnabled(true) -
父节点吞噬了触摸事件 -
组件不可见或被禁用
// 确保启用触摸
button->setTouchEnabled(true);
// 设置触摸优先级
button->setTouchPriority(1);
// 检查可见性和启用状态
if (button->isVisible() && button->isEnabled()) {
// 处理触摸
}
// 调试触摸区域
auto debugDraw = DrawNode::create();
debugDraw->drawRect(Vec2::ZERO, button->getContentSize(), Color4F::RED);
button->addChild(debugDraw);
问题2:组件位置不正确
-
锚点设置不当 -
坐标系转换错误 -
父节点缩放影响
// 设置锚点为(0.5,0.5)表示中心点
button->setAnchorPoint(Vec2(0.5f, 0.5f));
// 使用绝对位置
button->setPosition(Vec2(visibleSize.width/2, visibleSize.height/2));
// 忽略父节点缩放
button->setIgnoreAnchorPointForPosition(false);
// 调试位置
log("Position: x=%f, y=%f", button->getPositionX(), button->getPositionY());
log("Anchor: x=%f, y=%f", button->getAnchorPoint().x, button->getAnchorPoint().y);
问题3:内存泄漏
-
未正确释放资源 -
循环引用 -
静态变量持有引用
// 在析构函数中释放资源
CustomButton::~CustomButton() {
CC_SAFE_RELEASE_NULL(_normalSprite);
CC_SAFE_RELEASE_NULL(_pressedSprite);
CC_SAFE_RELEASE_NULL(_disabledSprite);
}
// 使用autorelease
auto button = CustomButton::create(...);
button->autorelease();
// 避免循环引用
std::weak_ptr<CustomButton> weakButton = button;
// 定期检测内存
#if (CC_TARGET_PLATFORM == CC_PLATFORM_WIN32)
_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
#endif
未来展望
-
声明式UI: -
类似QML的声明式语法 -
可视化UI编辑器 -
数据与视图绑定
-
-
组件热更新: -
运行时替换组件 -
脚本化组件逻辑 -
动态资源加载
-
-
AI辅助设计: -
自动布局优化 -
智能配色方案 -
行为预测
-
-
跨平台统一: -
一套代码适配多平台 -
平台特性自动适配 -
原生控件桥接
-
-
增强现实集成: -
3D空间UI定位 -
手势识别控件 -
环境感知布局
-
技术趋势与挑战
趋势
-
组件化开发: -
微服务架构应用于UI -
独立可复用的组件库 -
组件市场生态
-
-
响应式设计: -
自动适应不同屏幕尺寸 -
动态布局调整 -
内容优先的自适应
-
-
动画驱动UI: -
基于物理的动画 -
微交互增强体验 -
状态过渡动画
-
-
可访问性: -
屏幕阅读器支持 -
键盘导航 -
色彩对比度检查
-
挑战
-
性能优化: -
复杂组件渲染性能 -
内存占用控制 -
电池消耗优化
-
-
跨平台一致性: -
不同平台渲染差异 -
输入方式适配 -
系统主题兼容
-
-
安全加固: -
恶意内容防护 -
用户数据安全 -
防篡改机制
-
-
开发效率: -
快速原型设计 -
可视化编辑工具 -
自动化测试
-
总结
-
开发方法: -
继承Widget创建交互控件 -
继承Node实现简单图形元素 -
组合模式构建复合组件
-
-
关键技术: -
触摸事件处理机制 -
组件状态管理 -
布局计算算法 -
动画系统集成
-
-
实现案例: -
自定义按钮(带特效) -
多方向进度条 -
复合面板组件 -
虚拟摇杆控制器
-
-
最佳实践: -
资源管理策略 -
内存泄漏预防 -
性能优化技巧 -
跨平台适配方法
-
-
未来方向: -
声明式UI设计 -
组件热更新 -
AI辅助开发 -
AR/VR集成
-
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)