Cocos2d-x初探:英雄联盟皮肤选择菜单

2015年03月18日 11:50 0 点赞 0 评论 更新于 2025-11-21 14:22

时下最热门的游戏《英雄联盟》,我们可以使用Cocos2d-x来尝试实现它的皮肤选择菜单,这对于初学者来说是一个很好的练手项目。

最终效果图

这里会展示最终实现的英雄联盟皮肤选择菜单的效果图(原文未给出,可后续补充)。

设计说明

实现目标所需的动作

  • 移动(MoveTo):用于改变菜单项的位置。
  • 伸缩(ScaleTo):实现菜单项的缩放效果。
  • 倾斜(OrbitCamera):让菜单项产生倾斜的视觉效果。

实现目标所需的数学函数

使用函数 x/(x + a),其中 a 为常量,用于计算上述三个动作的值。

菜单大小

与原版Menu不同,该菜单大小不是全屏的,默认是屏幕的 2/3,可以通过 setContentSize() 函数进行自定义设置。

_index 变量

将所有的菜单项平铺构成一个长方形,_index 表示目前处于中间位置的菜单项的索引,具体可参考相关图示(原文未给出,可后续补充)。

显示方式

将菜单项距中心的距离 (i - _index) 作为函数变量 x,具体内容可查看 LOLMenu::updatePosition() 函数。

操作说明

滑动四分之一菜单宽的距离为一个单位的 _index,距离大于 0.6 小于 1.0 的部分进 1

使用方法

使用这个菜单只需了解两个函数:

  1. 构造函数LOLMenu::create()(由 CREATE_FUNC 创建)
  2. 添加菜单项函数void addMenuItem(cocos2d::MenuItem *item);

其他函数可查看代码实现。

菜单代码

LOLMenu.h

#ifndef __LOL__TE_MENU_H__
#define __LOL__TE_MENU_H__

#include "cocos2d.h"

/*
* 模仿英雄联盟的皮肤选择菜单
* 不同点在于,英雄联盟当皮肤过多时,部分皮肤会移出边缘,不显示
* 本实现会显示所有菜单项,向边缘移动会不断减缓
*/
class LOLMenu : public cocos2d::Layer {
public:
// 构造方法
CREATE_FUNC(LOLMenu);

// 添加菜单项
void addMenuItem(cocos2d::MenuItem *item);

// 更新位置
void updatePosition();

// 更新位置,有动画
void updatePositionWithAnimation();

// 位置矫正,修改位置,forward为移动方向,当超过1/3,进1
// true 为正向,false 为负向
void rectify(bool forward);

// 初始化
virtual bool init();

// 重置,显示索引设置为0
void reset();

private:
// 设置当前显示索引
void setIndex(int index);

// 获取当前显示菜单项的索引号
float getIndex();

// 返回被选中的item
cocos2d::MenuItem * getCurrentItem();

// 数学计算式width*index/(abs(index)+CALC_A) ,其中CALC_A为常数
float calcFunction(float index, float width);

private:
// 菜单菜单项的索引号
float _index;

// 上一次菜单项的索引号
float _lastIndex;

// 菜单项集合,_children顺序会变化,新建数组保存顺序
cocos2d::Vector<cocos2d::MenuItem*> _items;

// 监听函数
virtual bool onTouchBegan(cocos2d::Touch* touch, cocos2d::Event* event);
virtual void onTouchEnded(cocos2d::Touch* touch, cocos2d::Event* event);
virtual void onTouchMoved(cocos2d::Touch* touch, cocos2d::Event* event);

// 动画完结调用函数,这个主要是确定哪一个菜单项在前面
void actionEndCallBack(float dx);

// 当前被选择的item
cocos2d::MenuItem *_selectedItem;
};

#endif

LOLMenu.cpp

#include "LOLMenu.h"
#include <cmath>

#define PI acos(-1)

// 菜单的缩小比例 最小的比例是1-MENU_SCALE
#define MENU_SCALE 0.3

// 菜单的倾斜度 最多为45度
#define MENU_ASLOPE 60.0

// calcFunction(x) 为 x/(x+a),其中a为常数
#define CALC_A 1

// 动画运行时间
#define ANIMATION_DURATION 0.3f

// 菜单项的大小与屏幕的比例,当然可以通过setContentSize设置
#define CONTENT_SIZE_SCALE (2.0/3)

// 菜单项长度与菜单长度的比例 滑动一个菜单项长度,菜单项变化一个
#define ITEM_SIZE_SCALE (1.0/4)

/*
* 代码里面还有可以设置的参数,这里没有一一例出或给出函数
*/
USING_NS_CC;

bool LOLMenu::init() {
if (!Layer::init())
return false;

_index = 0;
_lastIndex = 0;
this->ignoreAnchorPointForPosition(false);
_selectedItem = nullptr;

auto size = Director::getInstance()->getWinSize();
this->setContentSize(size * CONTENT_SIZE_SCALE);
this->setAnchorPoint(Vec2(0.5f, 0.5f));

auto listener = EventListenerTouchOneByOne::create();
listener->onTouchBegan = CC_CALLBACK_2(LOLMenu::onTouchBegan, this);
listener->onTouchMoved = CC_CALLBACK_2(LOLMenu::onTouchMoved, this);
listener->onTouchEnded = CC_CALLBACK_2(LOLMenu::onTouchEnded, this);
getEventDispatcher()->addEventListenerWithSceneGraphPriority(listener, this);

return true;
}

void LOLMenu::addMenuItem(cocos2d::MenuItem *item) {
item->setPosition(this->getContentSize() / 2);
this->addChild(item);
_items.pushBack(item);
reset();
// 如果希望开始没有移动效果,改成updatePosition函数即可
updatePositionWithAnimation();
}

void LOLMenu::updatePosition() {
auto menuSize = getContentSize();
for (int i = 0; i < _items.size(); i++) {
// 设置位置
float x = calcFunction(i - _index, menuSize.width / 2);
_items.at(i)->setPosition(Vec2(menuSize.width / 2 + x, menuSize.height / 2));

// 设置zOrder,即绘制顺序
_items.at(i)->setZOrder(-abs((i - _index) * 100));

// 设置伸缩比例
_items.at(i)->setScale(1.0 - abs(calcFunction(i - _index, MENU_SCALE)));

// 设置倾斜,Node没有setCamera函数,将OrbitCamera的运行时间设为0来达到效果
auto orbit1 = OrbitCamera::create(0, 1, 0, calcFunction(i - _lastIndex, MENU_ASLOPE), calcFunction(i - _lastIndex, MENU_ASLOPE) - calcFunction(i - _index, MENU_ASLOPE), 0, 0);
_items.at(i)->runAction(orbit1);
}
}

void LOLMenu::updatePositionWithAnimation() {
// 先停止所有可能存在的动作
for (int i = 0; i < _items.size(); i++)
_items.at(i)->stopAllActions();

auto menuSize = getContentSize();
for (int i = 0; i < _items.size(); i++) {
_items.at(i)->setZOrder(-abs((i - _index) * 100));
float x = calcFunction(i - _index, menuSize.width / 2);

auto moveTo = MoveTo::create(ANIMATION_DURATION, Vec2(menuSize.width / 2 + x, menuSize.height / 2));
_items.at(i)->runAction(moveTo);

auto scaleTo = ScaleTo::create(ANIMATION_DURATION, (1 - abs(calcFunction(i - _index, MENU_SCALE))));
_items.at(i)->runAction(scaleTo);

auto orbit1 = OrbitCamera::create(ANIMATION_DURATION, 1, 0, calcFunction(i - _lastIndex, MENU_ASLOPE), calcFunction(i - _index, MENU_ASLOPE) - calcFunction(i - _lastIndex, MENU_ASLOPE), 0, 0);
_items.at(i)->runAction(orbit1);
}

scheduleOnce(schedule_selector(LOLMenu::actionEndCallBack), ANIMATION_DURATION);
}

void LOLMenu::reset() {
_lastIndex = 0;
_index = 0;
}

void LOLMenu::setIndex(int index) {
_lastIndex = _index;
this->_index = index;
}

float LOLMenu::getIndex() {
return _index;
}

MenuItem * LOLMenu::getCurrentItem() {
if (_items.size() == 0)
return nullptr;
return _items.at(_index);
}

bool LOLMenu::onTouchBegan(Touch* touch, Event* event) {
// 先停止所有可能存在的动作
for (int i = 0; i < _items.size(); i++)
_items.at(i)->stopAllActions();

if (_selectedItem)
_selectedItem->unselected();

auto position = this->convertToNodeSpace(touch->getLocation());
auto size = this->getContentSize();
auto rect = Rect(0, 0, size.width, size.height);
if (rect.containsPoint(position)) {
return true;
}
return false;
}

void LOLMenu::onTouchEnded(Touch* touch, Event* event) {
auto size = getContentSize();
auto xDelta = touch->getLocation().x - touch->getStartLocation().x;
rectify(xDelta > 0);

if (abs(xDelta) < size.width * ITEM_SIZE_SCALE / 2) {
_selectedItem->activate();
}
updatePositionWithAnimation();
}

void LOLMenu::onTouchMoved(Touch* touch, Event* event) {
auto xDelta = touch->getDelta().x;
auto size = getContentSize();
_lastIndex = _index;
_index -= xDelta / (size.width * ITEM_SIZE_SCALE);
updatePosition();
}

void LOLMenu::rectify(bool forward) {
auto index = getIndex();
if (index < 0)
index = 0;
if (index > _items.size() - 1)
index = _items.size() - 1;

if (forward) {
index = (int)(index + 0.4);
} else {
index = (int)(index + 0.6);
}

setIndex((int)index);
}

void LOLMenu::actionEndCallBack(float dx) {
_selectedItem = getCurrentItem();
if (_selectedItem)
_selectedItem->selected();
}

float LOLMenu::calcFunction(float index, float width) {
return width * index / (abs(index) + CALC_A);
}

演示代码

LOLMenuDemo.h

#ifndef __LOLMenu_SCENE_H__
#define __LOLMenu_SCENE_H__

#include "cocos2d.h"

class LOLMenuDemo : public cocos2d::Layer {
public:
// there's no 'id' in cpp, so we recommend returning the class instance pointer
static cocos2d::Scene* createScene();

// Here's a difference. Method 'init' in cocos2d-x returns bool, instead of returning 'id' in cocos2d-iphone
virtual bool init();

// a selector callback
void menuCloseCallback(cocos2d::Ref* pSender);
void menuItem1Callback(cocos2d::Ref* pSender);
void menuItem2Callback(cocos2d::Ref* pSender);
void menuItem3Callback(cocos2d::Ref* pSender);
void menuItem4Callback(cocos2d::Ref* pSender);
void menuItem5Callback(cocos2d::Ref* pSender);
void hideAllSprite();

cocos2d::Sprite *sprite[5];

// implement the "static create()" method manually
CREATE_FUNC(LOLMenuDemo);
};

#endif // __HELLOWORLD_SCENE_H__

LOLMenuDemo.cpp

#include "LOLMenuDemo.h"
#include "LOLMenu.h"

USING_NS_CC;

Scene* LOLMenuDemo::createScene() {
// 'scene' is an autorelease object
auto scene = Scene::create();

// 'layer' is an autorelease object
auto layer = LOLMenuDemo::create();

// add layer as a child to scene
scene->addChild(layer);

// return the scene
return scene;
}

// on "init" you need to initialize your instance
bool LOLMenuDemo::init() {
//////////////////////////////
// 1. super init first
if (!Layer::init()) {
return false;
}

Size visibleSize = Director::getInstance()->getVisibleSize();
Vec2 origin = Director::getInstance()->getVisibleOrigin();

auto item1 = MenuItemImage::create("4_LOL_MENU/item1_0.png", "4_LOL_MENU/item1_0.png", CC_CALLBACK_1(LOLMenuDemo::menuItem1Callback, this));
auto item2 = MenuItemImage::create("4_LOL_MENU/item2_0.png", "4_LOL_MENU/item2_0.png", CC_CALLBACK_1(LOLMenuDemo::menuItem2Callback, this));
auto item3 = MenuItemImage::create("4_LOL_MENU/item3_0.png", "4_LOL_MENU/item3_0.png", CC_CALLBACK_1(LOLMenuDemo::menuItem3Callback, this));
auto item4 = MenuItemImage::create("4_LOL_MENU/item4_0.png", "4_LOL_MENU/item4_0.png", CC_CALLBACK_1(LOLMenuDemo::menuItem4Callback, this));
auto item5 = MenuItemImage::create("4_LOL_MENU/item5_0.png", "4_LOL_MENU/item5_0.png", CC_CALLBACK_1(LOLMenuDemo::menuItem5Callback, this));

LOLMenu *menu = LOLMenu::create();
menu->addMenuItem(item1);
menu->addMenuItem(item2);
menu->addMenuItem(item3);
menu->addMenuItem(item4);
menu->addMenuItem(item5);
menu->setPosition(visibleSize / 2);
this->addChild(menu, 2);

for (int i = 0; i < 5; i++) {
char str[100];
sprintf(str, "4_LOL_MENU/item%d.jpg", i + 1);
sprite[i] = Sprite::create(str);
sprite[i]->setAnchorPoint(Vec2(0.5f, 0.5f));
sprite[i]->setPosition(visibleSize / 2);
this->addChild(sprite[i]);
}

hideAllSprite();
return true;
}

void LOLMenuDemo::menuCloseCallback(Ref* pSender) {
#if (CC_TARGET_PLATFORM == CC_PLATFORM_WP8) || (CC_TARGET_PLATFORM == CC_PLATFORM_WINRT)
MessageBox("You pressed the close button. Windows Store Apps do not implement a close button.", "Alert");
return;
#endif

Director::getInstance()->end();

#if (CC_TARGET_PLATFORM == CC_PLATFORM_IOS)
exit(0);
#endif
}

void LOLMenuDemo::menuItem1Callback(cocos2d::Ref* pSender) {
hideAllSprite();
sprite[0]->setVisible(true);
}

void LOLMenuDemo::menuItem2Callback(cocos2d::Ref* pSender) {
hideAllSprite();
sprite[1]->setVisible(true);
}

void LOLMenuDemo::menuItem3Callback(cocos2d::Ref* pSender) {
hideAllSprite();
sprite[2]->setVisible(true);
}

void LOLMenuDemo::menuItem4Callback(cocos2d::Ref* pSender) {
hideAllSprite();
sprite[3]->setVisible(true);
}

void LOLMenuDemo::menuItem5Callback(cocos2d::Ref* pSender) {
hideAllSprite();
sprite[4]->setVisible(true);
}

void LOLMenuDemo::hideAllSprite() {
for (auto p : sprite) {
if (p->isVisible())
p->setVisible(false);
}
}

通过以上代码,我们可以使用Cocos2d-x实现一个类似英雄联盟皮肤选择菜单的效果。在实际开发中,你可以根据需求对代码进行进一步的优化和扩展。