SDL農場遊戲開發 6.FarmUILayer顯示介面
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");
}
好了,本節結束。