CocoStudio sample讲解——DemoBag背包系统

CocoStudio sample——DemoBag源代码地址
https://github.com/chukong/CocoStudioSamples/tree/master/DemoBag

一、了解目标
开始之前先看一下最终的效果,它由一个人物展示面板和三个背包组成,点击不同的按钮显示不同的背包:

二、通过层来管理

这些内容除了背包中的物件是程序动态添加的外其余都是在编辑器中设计好的。

在画布列表中我们呢可以看到,衣服背包、武器背包、宠物背包都是由独立的一个层(panel)管理。除此之外还有一个标题层(title_panel)和人物展示层(up_panel)。
在开始制作细节之前,第一步就是做好分层,将不同的功能区单独划分层。如下图,拉四个“层容器”,然后在属性区修改每个层的大小,将整体调整到合适的大小与位置。
如图:

三、分层添加控件

有了轮廓就可以开始细节内容,为了方便编辑可以先将每个层都分开。然后选择不同的控件往层里拖动。
类似标题栏,一共需要一个lable,一个背景图片,一个button。

 <img title = '4.png' src='http://cdn.cocimg.com/bbs/attachment/Fid_48/48_183396_eb11628a9620297.png' > <img title = '5.png' src='http://cdn.cocimg.com/bbs/attachment/Fid_48/48_183396_e2274adfbcc1fb2.png' >  

添加子控件的时候注意将拖动到对应的层之上,等待层容器边框变为金色高亮状态时松开。这样才能保证添加的新控件添加到层内而不是层外。每添加完成一个控件后直接在属性面板中设置控件的大小,并配置上纹理(将资源面板中的png图片拖到属性面板上的“特性”->“文件”栏里即可)。

下面的人物展示层(up_panel)同标题部分类似,除了图片外还有三个带文本按钮。

按钮上的文字不是另外添加的空间,而是按钮本身自带的属性,通过设置按钮文字可以增加图片的复用率。

按钮需要三种状态的贴图,分别是正常、按下、禁用。其中正常状态下是必须要有纹理的,另外两个状态则是可选的,如果为配置按下状态贴图,则程序中会以放大效果响应按下事件。
作为按钮除了要配置视觉上的内容外还要注意配置“交互”属性,否则是无法响应用户触摸事件的。

制作完成上边两个层后就剩下,下面的三个背包层了。对于这种十分类似的可以先创建一个,其余的采用复制的方式。

复制后的控件副本将会保留原控件的多数属性,仅仅更改控件的名称。所以新空间将会在原控件的位置显示,我们将复制后的控件拖动并摆放整齐。

四、整理项目

编辑完所有的控件后可以开始给控件修改名称以及ID。ID和名称两个属性是为了在程序中方便的找到该控件,如果需要在程序中使用该控件则一定保证该控件的id与名称属性在当前项目是唯一的,否则程序无法正确获取到指定的控件。
如果美术和程序是相互独立的话最好做一个文档来记录所有的控件及ID,方便程序工程师查找。

五、导出文件

编辑完成后,选择菜单—》文件—》导出项目,


使用默认导出设置即可。
导出成功后,打开这个UI工程所在的文件夹。其中\Export就是导出文件的目录,默认每一个画布导出后会独立占用一个文件夹。除了引用到的资源外还有一个导出的.json文件。该json配置文件记录了整个画布的所有信息,导出后请勿随意改动。

以上是编辑器层面的内容,下面是程序中的内容,如果您不关心程序实现可以不再继续浏览。

六、创建一个新的cocos2d-x项目

使用以下脚本创建一个新的cocos2d-x项目:

pythoncreate_project.py -project 项目名称 -package 包名 -language 语言

(关于项目环境的配置请参考http://www.cocoachina.com/bbs/read.php?tid=161597)

将编辑器中导出的文件放置到新建项目的Resources文件夹内。

七、创建一个场景类

在cocos2d-x工程目录的Class文件夹下创建一个新的场景类(.h文件和.c++文件),取名为CocosGUIExamplesEquipScene,然后添加到VS工程内。
在AppDelegate.cpp文件里添加新建CocosGUIExamplesEquipScene类的类引用,并将boolAppDelegate::applicationDidFinishLaunching()方法尾部的场景创建方法替换为新的类:

// create a scene. it’s an autorelease object
//自动生成的HelloWorld场景。
// CCScene *pScene = HelloWorld::scene();//删除

//自定义的场景
CocosGUIExamplesEquipScene *pScene = newCocosGUIExamplesEquipScene();

pScene->autorelease();
// run

pDirector->runWithScene(pScene);

默认cocos需要的头文件应包含以下类容:

#ifndef__TestCpp__CocosGUIExamplesEquipScene__
#define__TestCpp__CocosGUIExamplesEquipScene__

#include"cocos2d.h"
#include"cocos-ext.h"

USING_NS_CC;
USING_NS_CC_EXT;
usingnamespace gui;

classCocosGUIExamplesEquipScene : public CCScene
{
public:
    CocosGUIExamplesEquipScene();
    ~CocosGUIExamplesEquipScene();
    
};

#endif /* defined(__TestCpp__CocosGUIExamplesEquipScene__)*/

不过我们需要扩展一些方法以及定义些变量来记录些控件信息:

#ifndef TestCpp__CocosGUIExamplesEquipScene
#define TestCpp__CocosGUIExamplesEquipScene

#include “cocos2d.h”
#include “cocos-ext.h”//引入cocos扩展库,使用cocostudio部分必须添加该引用

//通过宏变量记录5个容器层的tag
#define EQUIP_LAYOUT_TAG_ROOT 1000
#define EQUIP_LAYOUT_TAG_UP 1001
#define EQUIP_LAYOUT_TAG_CLOTHES 1002
#define EQUIP_LAYOUT_TAG_WEAPONS 1003
#define EQUIP_LAYOUT_TAG_PETS 1004
//通过宏变量记录三个按钮的tag
#define EQUIP_SWITCH_LAYOUT_BUTTON_TAG_CLOTHES 1005
#define EQUIP_SWITCH_LAYOUT_BUTTON_TAG_WEAPONS 1006
#define EQUIP_SWITCH_LAYOUT_BUTTON_TAG_PETS 1007

USING_NS_CC;
USING_NS_CC_EXT;
using namespace gui;//添加GUI命名空间,ui控件在cocos2d-x 2.2.2版本开始使用新的命名空间

class CocosGUIExamplesEquipScene : public CCScene
{
public:
CocosGUIExamplesEquipScene();
~CocosGUIExamplesEquipScene();

virtual void onEnter();
virtual void onExit();

// virtual void runThisTest();

protected:
// a selector callback
void menuCloseCallback(CCObject* pSender, TouchEventType type);//关闭按钮回调

// equip                                            original function name
void EquipInit();                                   // 初始化方法
void create();                                      // 统一入口,将调用下面三个创建方法
void createClothes();                               // 创建衣服面板
void createWeapons();                               // 穿件武器面板
void createPets();                                  // 创建宠物面板

void switchBtnCallBack(CCObject* pSender, TouchEventType type);          // 背包按钮回调函数

void touch(CCObject* pSender, TouchEventType type);                      // 触摸事件的统一回调函数    

void backOver(CCObject* pObject);                   // EquipBackOver

void close(CCObject* pObject);                      // 标题栏的关闭按钮

protected:
enum EQUIP_TYPE//当前选择的背包枚举
{
EQUIP_TYPE_NONE,
EQUIP_TYPE_CLOTHES,
EQUIP_TYPE_WEAPONS,
EQUIP_TYPE_PETS,
};

protected:
TouchGroup* m_pUILayer;//存放UI导出后的根视图

// equip
EQUIP_TYPE m_eEquipType;
//记录物品位置的数据字典
CCDictionary* m_dicBeUsedSlot;
CCDictionary* m_dicClothesSlot;
CCDictionary* m_dicWeaponsSlot;
CCDictionary* m_dicPetsSlot;
//背包物品集合
CCDictionary* m_dicClothes;
CCDictionary* m_dicWeapons;
CCDictionary* m_dicPets;
//三个背包层的zorder值
int container_1_Zorder;
int container_2_Zorder;
int container_3_Zorder;
//三个背包层的坐标
CCPoint container_1_Position;
CCPoint container_2_Position;
CCPoint container_3_Position;
//记录移动点
CCPoint movePoint;
CCPoint lastPoint;
//当前触摸的控件的坐标、位置信息,和所属的父级控件
CCPoint widgetLastWorldSpace;
CCPoint widgetLastNodeSpace;
Widget* lastWidgetParent;

};

#endif /* defined(TestCpp__CocosGUIExamplesEquipScene) */

八、实现

因为该实现较为复杂,我这里只简述下实现原理和关键步骤,具体实现请查看源码

背包是一个可以通过拖拽来给游戏中的任务更改道具的界面,从技术角度上来说就是可以将背包区的东西和物品栏的东西相互转移。
这里预先将所有的控件记录下来,然后在点击的时候将物品跟随触摸点移动,松开手指的时候查找当前触摸点是否是物品槽,并判断该物品槽内是否有物品,如果没有的话就将该物品放到该物品槽内。

i. 加载UI编辑器的内容

// equip root from json
    Layout* equipe_root =dynamic_cast<Layout*>(GUIReader::shareReader()->widgetFromJsonFile("cocosgui/gui_examples/equip_1/equip_1.json"));
    equipe_root->setTag(EQUIP_LAYOUT_TAG_ROOT);
  m_pUILayer->addWidget(equipe_root);       

ii. 通过程序添加些物品到背包中

此处用了三个函数分别创建了三个背包,分别是:
void createClothes();                               // 创建衣服面板
    void createWeapons();                               // 穿件武器面板
    void createPets();                                  // 创建宠物面板

iii. 给人物展示层(up_panel)添加人物
iv. 响应物品的拖拽事件


void CocosGUIExamplesEquipScene::touch(CCObject *pSender, TouchEventType type)
{
    switch (type)
    {
 //触摸开始事件,用来记录当前触摸的是那一个控件,以便知道点击的是那一个商品
        case TOUCH_EVENT_BEGAN:
        {
 //获取触摸的对象,并转化为widget
            Widget* widget = dynamic_cast<Widget*>(pSender);
            //获取控件的世界坐标并存放到widgetLastWorldSpace,(相对于屏幕的位置)
 CCPoint worldSpace = widget->convertToWorldSpace(CCPointZero);
            widgetLastWorldSpace = worldSpace;
 //存放当前控件的相对位置
            widgetLastNodeSpace = widget->getPosition();
            //存储当前触摸控件的父节点
            lastWidgetParent = static_cast<Widget*>(widget->getParent());
            widget->removeFromParentAndCleanup(false);
 
            m_pUILayer->addWidget(widget);
 
            widget->setPosition(widget->getTouchStartPos());
            movePoint = widget->getTouchStartPos();
        }
            break;
        //移动事件,更新最后触摸的所在点的位置
        case TOUCH_EVENT_MOVED:
        {
            Widget* widget = dynamic_cast<Widget*>(pSender);
            //更新触摸点的位置
            lastPoint = movePoint;
            movePoint = widget->getTouchMovePos();
            CCPoint offset = ccpSub(movePoint, lastPoint);
            CCPoint toPoint = ccpAdd(widget->getPosition(), offset);
            widget->setPosition(toPoint);
        }
            break;
         //触摸结束事件,在这里决定应该把物品放置在那一个背包或者物品格内。
        case TOUCH_EVENT_ENDED:
        {
 //记录松开点是否在人物展示窗口和背包窗口
            bool isInUsedSlot = false;
            bool isInEquipSlot = false;
 
            Widget* widget = dynamic_cast<Widget*>(pSender);
 
            CCDictElement* element = NULL;
 
            // 看拖拽的物品是否可以放到物品栏
            CCDICT_FOREACH(m_dicBeUsedSlot, element)
            { 
 //先遍历说有的子控件(即物品的格子),如果已经包含子控件则跳出循环
                Widget* usedSlot_wigt = dynamic_cast<Widget*>(element->getObject());
                if (usedSlot_wigt->getChildren()->count() > 0)
                {
                    continue;
                }
                //判断最终触摸点是否在当前物品格,是的话就将物品添加到这个物品格
                if (usedSlot_wigt->hitTest(widget->getPosition()))
                {
                    widget->removeFromParentAndCleanup(false);
                    widget->setPosition(CCPointZero);
                    usedSlot_wigt->addChild(widget);
 
                    isInUsedSlot = true;
                    break;
                }
            }
 
            // 看拖拽的物品是否可以放到背包
            CCDictionary* equipSlot_dic = NULL;
            CCDictionary* equip_dic = NULL;
 //判断当前选择的背包,并更新背包的数据
            switch (m_eEquipType)
            {
                case EQUIP_TYPE_CLOTHES:
                    equipSlot_dic = m_dicClothesSlot;
                    equip_dic = m_dicClothes;
                    break;
 
                case EQUIP_TYPE_WEAPONS:
                    equipSlot_dic = m_dicWeaponsSlot;
                    equip_dic = m_dicWeapons;
                    break;
 
                case EQUIP_TYPE_PETS:
                    equipSlot_dic = m_dicPetsSlot;
                    equip_dic = m_dicPets;
                    break;
 
                default:
                    break;
            }
            //循环查找所有的背包格,并判断是否可以放置当前的物品
            CCDICT_FOREACH(equipSlot_dic, element)
            {
                Widget* equipSlot = dynamic_cast<Widget*>(element->getObject());
                if (equipSlot->getChildren()->count() > 0)
                {
                    continue;
                }
 
                if (equipSlot->hitTest(widget->getPosition()))
                {
                    CCObject* obj = equip_dic->objectForKey(widget->getName());
                    if (obj)
                    {
                        widget->removeFromParentAndCleanup(false);
                        widget->setPosition(CCPointZero);
                        equipSlot->addChild(widget);
 
                        isInEquipSlot = true;
                    }
                    break;
                }
            }
            //如果最终松开的点不在这些面板内
            if (!isInUsedSlot && !isInEquipSlot)
            {
 //创建一个CCMoveTo的action动画,起点是跟谁触摸点物品的世界位置,终点物品被触摸前的位置
                CCPoint point = widgetLastWorldSpace;
                CCMoveTo* moveTo = CCMoveTo::create(1.0f, point);
                CCEaseExponentialOut* ease = CCEaseExponentialOut::create(moveTo);
                CCCallFuncO* calllFunc0 = CCCallFuncO::create(this, callfuncO_selector(CocosGUIExamplesEquipScene::backOver), widget);
                CCSequence* seq = CCSequence::create(ease, calllFunc0, NULL);
                widget->runAction(seq);
 
                // 将当前背包的所有物品设置为可点击
                CCDICT_FOREACH(equip_dic, element)
                {
                    Widget* widget = dynamic_cast<Widget*>(element->getObject());
                    widget->setTouchEnabled(false);
                }
 
                // 将up_panel层的所有子控件设置为不可点击
                Layout* equipe_root = dynamic_cast<Layout*>(m_pUILayer->getWidgetByTag(EQUIP_LAYOUT_TAG_ROOT));
                Layout* up_layout = dynamic_cast<Layout*>(equipe_root->getChildByName("up_panel"));
                CCObject* obj = NULL;
                up_layout->setTouchEnabled(false);
                CCARRAY_FOREACH(up_layout->getChildren(), obj)
                {
                    Widget* child = dynamic_cast<Widget*>(obj);
                    child->setTouchEnabled(false);
                }
            }
        }
            break;
 
        default:
            break;
    }
}

2赞

各位小伙伴们,sample讲解将会全部制作,目前仓库中还是2.2.0版本,我这里会尽快将新版本提交到github分支,如果需要直接看正在改的版本,可以到https://github.com/geron-cn/CocoStudioSamples查看。

以后的每一次版本发布都会及时更新该samples。感谢大家对cocostudio的支持

版主辛苦了:7::7::7:

这篇好,默默转给策划兼美工看 :14:

别默默转哈, 让策划,美术看完了, 来论坛顶一顶才是正道哈.

最喜欢版主大大的教程了!好物马克,美术觉得很有用!

这篇教程非常不错,谢谢!

好详细。感谢版主啊

可是代码能不能放百度网盘或其他网盘,因为托管那东东,我不会用。不知道怎么下载呢。

可以打包下载所有源代码.

楼上正解~~这是最新最快捷的方式。无需了解github。

:2: 很好教程。顶。顶。顶。顶。

正好遇到这个问题 感谢分享

我运行这个例子,界面怎么是这样的,没有适配好?

cocostuidios加载工程也不对,我使用的是1.4.0.0的,版本不对吗?

版本问题,1.2.0.1

在xp上也有问题,在wind7上就好了。。。

我和13楼 的情况一样 ,在游戏中显示错位,但在编辑器里面看到的没问题
打印出坐标和锚点坐标都是对的,但显示错位,是不是新版本有些函数实现改了

cocos2d版本 3.1.1
cocostudio版本 1.4.0.1

多谢版主

这个应该是分辨率适配的问题,换个小屏幕试一试。不同的分辨率可能会去读不同分辨率下的资源。

试过各种分辨率都不行。问题应该是这里
for (auto& obj : clothes_layout->getChildren())
{
Widget* slot = dynamic_cast<Widget*>(obj);
slot->setCascadeColorEnabled(false);
if (slot->getBoundingBox().containsPoint(jacket_iv->getPosition()))
{
jacket_iv->setPosition(Point::ZERO);
slot->addChild(jacket_iv);
break;
}
}
在判断物品控件和背包格子控件有重叠时,设置物品位置和背包格子位置重叠,并加入到其子节点中
这里调用后位置就错位了,打印这两个控件的位置和锚点都是正确的。只要注释这里 就能正确

想起个问题,你用的Cocos 2d-x版本是多少?3.0对锚点做了调整,所以会出现便宜的问题。

cocos2d版本 3.1.1
cocostudio版本 1.4.0.1