FarmUILayer主要用于显示,它的功能大致如下:
- 商店、背包按钮。
- 作物的收获、铲除按钮。
- 作物的具体信息,如多少小时后成熟。
- 农场信息,如金币数目、等级和经验。
其界面大致如下:
商店按钮、仓库按钮和存档按钮。
农场信息,包括金币数目、等级和经验。
作物信息和作物操作按钮。
尽管FarmUILayer中包含了农场游戏中的大部分控件,但是它并不包括这些按钮的功能具体实现,而是委托给了上层,即FarmScene。
1.UI的制作
SDL_Engine的UI部分是我根据《cocos2dx 游戏开发之旅》中的一个章节和参考cocos2dx中的源码实现的,并没有像Cocos或者类似的方便的界面编辑工具,只能乖乖地手写。如果使用cocos2dx的话,可以去官网查看是否有对应的界面编辑工具。
无论是SDL_Engine,还是cocos2dx(这里指的是c++版,像之后推出的js和lua版都是在c++版的基础上做了脚本化的封装),如果想要给某个控件添加回调函数的话都需要在程序中获取到该控件,之后再手动绑定回调函数。不得不说,这就是编译型语言的弊端。
本次共使用了两个UI文件,其名称分别是farm_layer.xml和farm_crop_info.xml。
前一个文件用于显示商店、仓库和存档按钮以及农场信息;后一个则是显示作物信息。而作物的操作按钮则是在程序中动态生成的。
在制作好UI文件后,就需要在程序中加载该文件并添加,若需要交互则还需要获取对应的控件。
2.FarmUILayer的构成
接下来分析一下FarmUILayer类的构成。
首先,FarmUILayer并不负责按钮操作的具体实现,例如:它并不关心是如何存档的,或者是在点击了铲除按钮后做了什么操作。因此需要创建一个委托,由实现了这个委托接口的类来负责按钮的具体操作;其次,FarmUILayer中还有着农场的等级、金币数目和经验,故还会有公有函数负责刷新显示;最后则是作物信息和作物的操作按钮,它们都是只会在点击了作物后才会出现,在点击了空地后会隐藏,因此还会有公有函数负责显示/隐藏作物信息和操作按钮。
3.FarmUILayer的实现
FarmUILayer.h
class Crop;
class FarmUILayerDelegate
{
public:
FarmUILayerDelegate(){}
virtual ~FarmUILayerDelegate(){}
virtual void harvestCrop(Crop* crop) = 0;
virtual void shovelCrop(Crop* crop) = 0;
virtual void fightCrop(Crop* crop) = 0;
//打开仓库
virtual void showWarehouse() = 0;
//打开商店
virtual void showShop() = 0;
virtual void saveData() = 0;
};
FarmUILayerDelegate即为委托接口类,它的函数从上自下依次是:收获作物、铲除作物、战斗(扩展接口)、打开仓库、打开商店和存档。
class FarmUILayer : public Layer
{
private:
ui::Button* m_pWarehouseBtn;
ui::Button* m_pShopBtn;
//农场背景板
Node* m_pTagSprite;
Menu* m_pMenu;
//收获按钮
MenuItemSprite* m_pHarvestItem;
//铲子按钮
MenuItemSprite* m_pShovelItem;
//战斗按钮
MenuItemSprite* m_pFightItem;
//委托者
FarmUILayerDelegate* m_pDelegate;
//当前操作的作物
Crop* m_pOperatingCrop;
//面板对应作物
Crop* m_pInfoCrop;
//作物经过秒数 主要用于刷新显示面板
int m_nCropSecond;
//作物状态面板
Node* m_pCropInfoPanel;
FarmUILayer中的一部分属性都是从UI文件中获取到的,如m_pWarehouseBtn、m_pShopBtn等;接下来是作物的操作按钮,即Menu和对应的三个MenuItemSprite,这几个在cocos2dx也是有着对应的类的。
Menu和Widget派生类的最大不同就是:
每个Widget控件内部都会有一个事件监听器;而Menu和MenuItem(本身或派生类)中只有Menu有一个事件监听器,这也就是为什么MenuItem的派生类需要加在Menu中的原因。
FarmUILayerDelegate* m_pDelegate在本例子中指向的一直是FarmScene,关于委托不太明白的可以百度设计模式。
一般来说,要操作的作物和显示作物信息的作物应该是相同的,即当点击了某个作物时,那么应该既显示作物信息,又显示出操作按钮,这里之所以分为两个指针也是为了扩展性考虑。
public:
FarmUILayer();
~FarmUILayer();
CREATE_FUNC(FarmUILayer);
bool init();
void update(float dt);
void setDelegate(FarmUILayerDelegate* pDelegate) { m_pDelegate = pDelegate; }
/**
* 展示操作按钮 会根据crop的状态生成不同的按钮
* @param crop 要操作的作物
*/
void showOperationBtns(Crop* crop);
/**
* 隐藏操作按钮
*/
void hideOperationBtns();
/**
* 更新显示的金币
* @param goldNum 金币数目
*/
void updateShowingGold(int goldNum);
/**
* 更新显示的等级
* @param lv 等级
*/
void updateShowingLv(int lv);
/**
* 更新显示的经验
* @param exp 当前经验
* @param maxExp 当前等级的最大经验
*/
void updateShowingExp(int exp, int maxExp);
FarmUILayer类的公有函数,和前面分析的基本一致。
private:
MenuItemSprite* initializeOperationBtn(const string& text);
//操作按钮回调函数
void clickOperationBtnCallback(Object* sender);
//存档回调
void clickSaveBtnCallback(Object* sender);
//显示作物状态
void showCropInfo(Crop* crop);
//隐藏作物状态
void hideCropInfo();
//仓库按钮回调函数
void warehouseBtnCallback(Object* sender);
//商店按钮回调函数
void shopBtnCallback(Object* sender);
//更新作物状态的时间
void updateCropInfo();
//显示 or 隐藏操作按钮
void setVisibleOfOperationBtns(bool visible);
目前是把显示/隐藏作物信息的两个函数设置为私有,其主要会在show/hideOperationBtn中调用。
接着就是FarmUILayer.cpp的编写了。
bool FarmUILayer::init()
{
auto manager = ui::UIWidgetManager::getInstance();
//加载仓库、商店、背包等控件
auto node = manager->createWidgetsWithXml("scene/farm_layer.xml");
m_pWarehouseBtn = node->getChildByName<ui::Button*>("warehouse_btn");
m_pWarehouseBtn->addClickEventListener(
SDL_CALLBACK_1(FarmUILayer::warehouseBtnCallback, this));
m_pShopBtn = node->getChildByName<ui::Button*>("shop_btn");
m_pShopBtn->addClickEventListener(SDL_CALLBACK_1(FarmUILayer::shopBtnCallback, this));
//背景板
m_pTagSprite = node->getChildByName("tag_bg");
this->addChild(node);
//保存按钮添加函数回调
auto saveBtn = node->getChildByName<ui::Button*>("save_btn");
saveBtn->addClickEventListener(SDL_CALLBACK_1(FarmUILayer::clickSaveBtnCallback, this));
//创建按钮
m_pHarvestItem = this->initializeOperationBtn(STATIC_DATA_STRING("harvest_text"));
m_pShovelItem = this->initializeOperationBtn(STATIC_DATA_STRING("shovel_text"));
m_pFightItem = this->initializeOperationBtn(STATIC_DATA_STRING("fight_text"));
m_pMenu = Menu::create(m_pHarvestItem, m_pShovelItem, m_pFightItem, nullptr);
this->addChild(m_pMenu);
//设置不可用
m_pMenu->setEnabled(false);
m_pHarvestItem->setVisible(false);
m_pShovelItem->setVisible(false);
m_pFightItem->setVisible(false);
m_pHarvestItem->setSwallowed(true);
m_pShovelItem->setSwallowed(true);
m_pFightItem->setSwallowed(true);
//作物信息节点
m_pCropInfoPanel = manager->createWidgetsWithXml("scene/farm_crop_info.xml");
this->addChild(m_pCropInfoPanel);
m_pCropInfoPanel->setVisible(false);
return true;
}
init函数中加载了外部UI文件后获取到相应的控件后,又设置了回调函数;生成了三个MenuItemSprite后添加到同一个Menu后先隐藏起来,供之后使用。
void FarmUILayer::update(float dt)
{
//面板作物不存在或已经枯萎或时间未到,不需刷新
if (m_pInfoCrop == nullptr || m_pInfoCrop->isWitherred()
|| m_pInfoCrop->getSecond() == m_nCropSecond)
{
return;
}
this->updateCropInfo();
}
update函数中会判断是否是否应该刷新作物信息面板,如果需要刷新,则刷新时间和进度条。
void FarmUILayer::showOperationBtns(Crop* crop)
{
//已经显示
if (m_pOperatingCrop == crop)
return;
SDL_SAFE_RETAIN(crop);
SDL_SAFE_RELEASE(m_pOperatingCrop);
m_pOperatingCrop = crop;
this->setVisibleOfOperationBtns(true);
//显示作物信息
this->showCropInfo(crop);
}
void FarmUILayer::hideOperationBtns()
{
if (m_pOperatingCrop == nullptr)
return;
this->setVisibleOfOperationBtns(false);
//隐藏作物信息
this->hideCropInfo();
SDL_SAFE_RELEASE_NULL(m_pOperatingCrop);
}
显示/隐藏操作按钮会根据作物的状态不同而出现不同的按钮。比如作物成熟时会显示“收获”和“铲除”。按钮出现或者隐藏时会有一个动作,因为它们的主要实现大致相同,因此都交给了私有函数setVisibleOfOperationBtns()。
oid FarmUILayer::updateShowingGold(int goldNum)
{
auto goldLabel = m_pTagSprite->getChildByName<LabelAtlas*>("gold");
goldLabel->setString(StringUtils::toString(goldNum));
}
void FarmUILayer::updateShowingLv(int lv)
{
auto lvLabel = m_pTagSprite->getChildByName<LabelAtlas*>("level");
lvLabel->setString(StringUtils::toString(lv));
}
void FarmUILayer::updateShowingExp(int exp, int maxExp)
{
//经验控件
auto expLabel = m_pTagSprite->getChildByName<LabelAtlas*>("exp");
auto progress = m_pTagSprite->getChildByName<ProgressTimer*>("exp_progress");
string text = StringUtils::format("%d/%d", exp, maxExp);
expLabel->setString(text);
int percentage = int((float)exp / maxExp * 100);
progress->setPercentage(percentage);
}
金币数目等控件的更新则比较简单,根据传入的参数来调用对应控件更新显示。
MenuItemSprite* FarmUILayer::initializeOperationBtn(const string& text)
{
//创建新按钮
LabelBMFont* label = LabelBMFont::create(text, "fonts/1.fnt");
auto size = label->getContentSize();
size.width *= 1.5f;
size.height *= 1.5f;
label->setPosition(size.width / 2, size.height / 2);
label->setAnchorPoint(Point(0.5f,0.5f));
Scale9Sprite* normalSprite = Scale9Sprite::create(Sprite::createWithSpriteFrameName("bt6_1.png"), Rect(5, 5, 10, 10));
normalSprite->setPreferredSize(size);
Scale9Sprite* selectedSprite = Scale9Sprite::create(Sprite::createWithSpriteFrameName("bt6_2.png"), Rect(5, 5, 10, 10));
selectedSprite->setPreferredSize(size);
MenuItemSprite* item = MenuItemSprite::create(normalSprite, selectedSprite);
item->addChild(label, 6);
item->setName(text);
item->setCallback(SDL_CALLBACK_1(FarmUILayer::clickOperationBtnCallback, this));
return item;
}
这个函数根据传入的参数生成对应的MenuItem并返回。首先根据文本创建一个Label,之后根据这个label的尺寸创建一个稍微大一些的Scale9Sprite,最后创建MenuItemSprite并返回。
void FarmUILayer::clickOperationBtnCallback(Object* sender)
{
if (m_pHarvestItem == sender)
m_pDelegate->harvestCrop(m_pOperatingCrop);
else if (m_pShovelItem == sender)
m_pDelegate->shovelCrop(m_pOperatingCrop);
else if (m_pFightItem == sender)
m_pDelegate->fightCrop(m_pOperatingCrop);
}
收获、铲除、战斗按钮的回调函数是同一个回调函数,因此需要判断一下传递的sender是哪个指针。
void FarmUILayer::showCropInfo(Crop* crop)
{
//设置面板显示的作物
SDL_SAFE_RETAIN(crop);
m_pInfoCrop = crop;
m_nCropSecond = m_pInfoCrop->getSecond();
int id = crop->getCropID();
//获取作物名称
auto pCropSt = StaticData::getInstance()->getCropStructByID(id);
string name = pCropSt->name;
//获取作物季数
int harvestCount = crop->getHarvestCount();
int totalHarvest = pCropSt->harvestCount;
auto nameLabel = m_pCropInfoPanel->getChildByName<LabelBMFont*>("name_label");
auto fruitSprite = m_pCropInfoPanel->getChildByName<Sprite*>("fruit_sprite");
auto timePorgress = m_pCropInfoPanel->getChildByName<ProgressTimer*>("time_progress");
auto ripeLabel = m_pCropInfoPanel->getChildByName<LabelBMFont*>("ripe_label");
string nameText;
bool bVisible = false;
Size size = m_pCropInfoPanel->getContentSize();
//判断作物是否枯萎,是则只显示名称控件
if (crop->isWitherred())
{
auto name_format = STATIC_DATA_STRING("crop_name_witherred_format").c_str();
bVisible = false;
nameText = StringUtils::format(name_format, name.c_str());
}
else
{
auto name_format = STATIC_DATA_STRING("crop_name_format");
auto time_format = STATIC_DATA_STRING("crop_time_format");
bVisible = true;
nameText = StringUtils::format(name_format.c_str(), name.c_str(), harvestCount, totalHarvest);
}
//显示状态面板
m_pCropInfoPanel->setVisible(true);
auto pos = crop->getPosition();
pos.y -= crop->getContentSize().height * crop->getAnchorPoint().y + size.height / 2;
m_pCropInfoPanel->setPosition(pos);
//设置显示名称
nameLabel->setString(nameText);
//确定果实精灵的位置
auto fruit_format = STATIC_DATA_STRING("fruit_filename_format");
auto fruitName = StringUtils::format(fruit_format.c_str(), id);
fruitSprite->setSpriteFrame(fruitName);
pos = nameLabel->getPosition();
auto nameSize = nameLabel->getContentSize();
auto fruitSize = fruitSprite->getContentSize();
pos.x = pos.x - nameSize.width * 0.5f - fruitSize.width * 0.5f;
pos.y = timePorgress->getPositionY() - timePorgress->getContentSize().height / 2 - fruitSize.height * 0.5f;
fruitSprite->setPosition(pos);
timePorgress->setVisible(bVisible);
ripeLabel->setVisible(bVisible);
if (bVisible)
{
this->updateCropInfo();
}
}
void FarmUILayer::hideCropInfo()
{
SDL_SAFE_RELEASE_NULL(m_pInfoCrop);
m_pCropInfoPanel->setVisible(false);
}
showCropInfo函数的功能就是更新作物面板的各个控件,比如作物名称、当前季数等。它需要传递一个作物指针,之后保存这个指针,然后根据这个指针更新作物信息面板中的各种控件。
void FarmUILayer::warehouseBtnCallback(Object* sender)
{
m_pDelegate->showWarehouse();
}
void FarmUILayer::shopBtnCallback(Object* sender)
{
m_pDelegate->showShop();
}
这两个回调函数分别调用了委托者的showWarehouse()和showShop()函数。
void FarmUILayer::updateCropInfo()
{
//获取作物当前时间和总时间
time_t startTime = m_pInfoCrop->getStartTime();
int totalHour = m_pInfoCrop->getGrowingHour(-1);
time_t endTime = startTime + totalHour * 3600;
time_t curTime = time(nullptr);
auto timePorgress = m_pCropInfoPanel->getChildByName<ProgressTimer*>("time_progress");
auto ripeLabel = m_pCropInfoPanel->getChildByName<LabelBMFont*>("ripe_label");
string timeText;
auto time_format = STATIC_DATA_STRING("crop_time_format");
//判别时间
if (curTime >= endTime)
{
timeText = STATIC_DATA_STRING("ripe_text");
}
else
{
auto deltaTime = endTime - curTime;
int hour = deltaTime / 3600;
int minute = (deltaTime - hour * 3600) / 60;
int second = deltaTime - hour * 3600 - minute * 60;
//刷新时间,使得可以一秒刷新一次
m_nCropSecond = second;
timeText = StringUtils::format(time_format.c_str(), hour, minute, second);
}
//prohress内部容错,当前并未控制取值范围
float percentage = (curTime - startTime) / (totalHour * 36.f);
timePorgress->setPercentage(percentage);
ripeLabel->setString(timeText);
}
updateCropInfo()主要用于更新作物面板信息中的时间和进度条。
void FarmUILayer::setVisibleOfOperationBtns(bool visible)
{
if (m_pOperatingCrop == nullptr)
{
LOG("error:m_pOperatingCrop == nullptr\n");
return;
}
auto pos = m_pOperatingCrop->getPosition();
auto size = m_pHarvestItem->getContentSize();
//位置偏移
vector<float> deltas;
deltas.push_back( size.width);
deltas.push_back(-size.width);
float scale = visible ? 1.5f : 1.f;
//菜单按钮
vector<MenuItemSprite*> items;
if (m_pOperatingCrop->isRipe())
items.push_back(m_pHarvestItem);
items.push_back(m_pShovelItem);
m_pMenu->setEnabled(visible);
for (size_t i = 0;i < items.size(); i++)
{
auto item = items[i];
//结束位置
auto toPos = Point(pos.x + scale * deltas[i], pos.y);
ActionInterval* action = nullptr;
auto move = MoveTo::create(0.1f, toPos);
if (visible)
{
item->setPosition(pos.x + deltas[i], pos.y);
action = move;
item->setVisible(true);
}
else
{
auto hide = Hide::create();
action = Sequence::createWithTwoActions(move, hide);
}
action->setTag(1);
item->setEnabled(visible);
item->stopActionByTag(1);
item->runAction(action);
}
}
操作按钮在出现时有一个向外移动的动作,而在隐藏时有一个向内平移的动作。
4.FarmScene的修改
首先,需要在FarmScene创建一个FarmUILayer的实例,之后FarmScene需要继承FarmUILayerDelegate并实现相应的函数。
FarmScene.h
#include "FarmUILayer.h"
//...
class FarmScene : public Scene
, public FarmUILayerDelegate
public://委托
virtual void harvestCrop(Crop* crop);
virtual void shovelCrop(Crop* crop);
virtual void fightCrop(Crop* crop);
virtual void showWarehouse();
virtual void showShop();
virtual void saveData();
//...
private:
Value getValueOfKey(const string& key);
private:
SoilLayer* m_pSoilLayer;
CropLayer* m_pCropLayer;
FarmUILayer* m_pFarmUILayer;
之后需要实现以上几个函数。
FarmScene.cpp
①. 实例化FarmUILayer
Value FarmScene::getValueOfKey(const string& key)
{
auto dynamicData = DynamicData::getInstance();
Value* p = dynamicData->getValueOfKey(key);
Value value;
//不存在对应的键,自行设置
if (p == nullptr)
{
if (key == FARM_LEVEL_KEY)
value = Value(1);
else if (key == FARM_EXP_KEY)
value = Value(0);
else if (key == GOLD_KEY)
value = Value(0);
//设置值
dynamicData->setValueOfKey(key, value);
}
else
{
value = *p;
}
return value;
}
DynamicData中的getValueOfKey()会在键对应的值不存在时返回空指针,为避免这种情况,FarmScene对农场游戏的几个属性做了一个特殊处理。
bool FarmScene::init()
{
//...
//ui层
m_pFarmUILayer = FarmUILayer::create();
m_pFarmUILayer->setDelegate(this);
this->addChild(m_pFarmUILayer);
//初始化土壤和作物
this->initializeSoilsAndCrops();
//更新数据显示
int gold = this->getValueOfKey(GOLD_KEY).asInt();
int lv = this->getValueOfKey(FARM_LEVEL_KEY).asInt();
int exp = this->getValueOfKey(FARM_EXP_KEY).asInt();
int maxExp = DynamicData::getInstance()->getFarmExpByLv(lv);
m_pFarmUILayer->updateShowingGold(gold);
m_pFarmUILayer->updateShowingLv(lv);
m_pFarmUILayer->updateShowingExp(exp, maxExp);
//...
}
init函数中实例化了FarmUILayer,之后更新金币、等级和经验的显示。
此时若在FarmScene.cpp中添加了FarmUILayerDelegate类中的函数的话,应该能看到以下场景:
②.处理点击事件
接着更新FarmScene::handleTouchEvent。
bool FarmScene::handleTouchEvent(Touch* touch, SDL_Event* event)
{
auto location = touch->getLocation();
//是否点击了土地
auto soil = m_pSoilLayer->getClickingSoil(location);
//点到了“空地”
if (soil == nullptr)
{
m_pFarmUILayer->hideOperationBtns();
return true;
}
//获取土壤对应的作物
auto crop = soil->getCrop();
//未种植作物
if (crop == nullptr)
{
}
else//存在作物,显示操作按钮
{
m_pFarmUILayer->showOperationBtns(crop);
}
return false;
}
在这个函数里负责获取触碰点对应的土壤,若点击了“空地”,则尝试隐藏操作按钮和作物信息;若点击了土壤且土壤上还种植着作物,则显示显示作物信息以及操作菜单。此时应能看到以下场景:
目前虽然已经可以显示作物信息和操作按钮了,但是还是无法点击“收获”按钮和“铲除”,那是因为我们还没有实现对应的函数(-_-!!!多好的一句废话)。
③.作物的收获
void FarmScene::harvestCrop(Crop* crop)
{
auto dynamicData = DynamicData::getInstance();
//隐藏操作按钮
m_pFarmUILayer->hideOperationBtns();
//果实个数
int number = crop->harvest();
int id = crop->getCropID();
//获取果实经验
auto pCropSt = StaticData::getInstance()->getCropStructByID(id);
//获取经验值和等级
Value curExp = this->getValueOfKey(FARM_EXP_KEY);
Value lv = this->getValueOfKey(FARM_LEVEL_KEY);
int allExp = dynamicData->getFarmExpByLv(lv.asInt());
curExp = pCropSt->exp + curExp.asInt();
//是否升级
if (curExp.asInt() >= allExp)
{
curExp = curExp.asInt() - allExp;
lv = lv.asInt() + 1;
allExp = dynamicData->getFarmExpByLv(lv.asInt());
//更新控件
m_pFarmUILayer->updateShowingLv(lv.asInt());
//等级写入
dynamicData->setValueOfKey(FARM_LEVEL_KEY, lv);
}
m_pFarmUILayer->updateShowingExp(curExp.asInt(), allExp);
//果实写入
dynamicData->addGood(GoodType::Fruit, StringUtils::toString(id), number);
//经验写入
dynamicData->setValueOfKey(FARM_EXP_KEY, curExp);
//作物季数写入
dynamicData->updateCrop(crop);
}
当点击了收获按钮后,harvestCrop()就会被调用,先隐藏操作按钮,之后获取果实个数、经验和该等级的最大经验;然后判断是否升级;最后更新数据。
编译运行后,目前已经能实现收获了,界面如下:
可以看到,香蕉有两季,因此收获后成为了倒数第二个阶段。
④.作物的铲除
void FarmScene::shovelCrop(Crop* crop)
{
//隐藏操作按钮
m_pFarmUILayer->hideOperationBtns();
SDL_SAFE_RETAIN(crop);
m_pCropLayer->removeCrop(crop);
DynamicData::getInstance()->shovelCrop(crop);
//设置土壤
auto soil = crop->getSoil();
soil->setCrop(nullptr);
crop->setSoil(nullptr);
SDL_SAFE_RELEASE(crop);
}
铲除作物相对则比较简单了,移除作物后更新DynamicData即可。界面如下:
⑤.数据保存
数据的保存相对比较简单:
void FarmScene::saveData()
{
DynamicData::getInstance()->save();
printf("save data success\n");
}
好了,本节结束。