cocos2dx3.0 超級馬里奧開發筆記(一)——loadingbar、TableView和pageview的使用

學完cocos2dx課程的第一個項目選擇了超級瑪麗。可以說有自己的想法,簡單但是確實不簡單。

我花了一天把一份2.1版本的超級瑪麗源碼升級到了3.0,改改刪刪,參考那個源碼雖然好多不懂,但是馬虎升級成功,遊戲正常玩耍。

本着不爲把遊戲做出來而寫代碼的想法,羅列了一下這個遊戲可以使用到的知識點。數據持久化的三種方式、loading頁面、tmx地圖解析、cocosStudio場景、屏幕適配、關卡如何選擇、代碼結構的優化(各種類的抽象繼承),在基本功能出來後可以自己去設計變態關卡等。


兩天實現了loading界面、 主界面所有場景 和選關場景。

 效果如下gif:



Loading場景

進度條

超級瑪麗這種簡單的遊戲根本不需要預加載太多東西,或者說根本不需要loading這麼笨重的交互。沒辦法,熟悉一些方法,學習而已,別太較真。
場景的進度條使用ProgressTimer,如果我們使用cocosStudio創建的場景,就使用工具的那個。
設置一個進度條成員變量,然後:
	loadProgress = ProgressTimer::create(Sprite::create("image/loadingbar.png"));
	loadProgress->setBarChangeRate(Point(1, 0));//設置進程條的變化速率
	loadProgress->setType(ProgressTimer::Type::BAR);//設置進程條的類型
	loadProgress->setMidpoint(Point(0, 1));//設置進度的運動方向

	loadProgress->setPosition(visibleSize.width / 2, visibleSize.height / 2 );
	loadProgress->setPercentage(progressPercent);
	this->addChild(loadProgress);

要調整進度條,只需要在異步加載資源的回調函數中實現就可以了。
像loading場景也可以實現loading的動畫等其他效果,異步加載並不會對界面造成太多的卡頓效果,除非手機實在是太爛了。。

異步加載資源

一般緩存有三種,TextureCache、SpriteFrameCache、AnimationCache。
我們loading 的時候要根據需求預加載不同的資源。 然後在回調函數中處理進度條的進度。
超級瑪麗登陸loading我使用最簡單的TextureCache來操作。
在init裏面添加preloadResource函數,這個函數會直接執行完,不需等待任何結果。 然後資源會在後臺加載。

void LoadingScene::preloadResource()
{
	//這裏分開寫測試, 後期如果確定是一個場景中的直接使用plist加載
	std::string resouceMain = "image/mainscene/";
	float count = 20;//一共加載十七張
	everyAdd = 100 / count;
	Director::getInstance()->getTextureCache()->addImageAsync(resouceMain+"about_normal.png", CC_CALLBACK_1(LoadingScene::loadingCallback, this));
	Director::getInstance()->getTextureCache()->addImageAsync(resouceMain+"about_select.png", CC_CALLBACK_1(LoadingScene::loadingCallback, this));
	Director::getInstance()->getTextureCache()->addImageAsync(resouceMain+"backA.png", CC_CALLBACK_1(LoadingScene::loadingCallback, this));
	Director::getInstance()->getTextureCache()->addImageAsync(resouceMain+"backB.png", CC_CALLBACK_1(LoadingScene::loadingCallback, this));
	Director::getInstance()->getTextureCache()->addImageAsync(resouceMain+"background.png", CC_CALLBACK_1(LoadingScene::loadingCallback, this));
	Director::getInstance()->getTextureCache()->addImageAsync(resouceMain+"bg.png", CC_CALLBACK_1(LoadingScene::loadingCallback, this));
	Director::getInstance()->getTextureCache()->addImageAsync(resouceMain+"music_off.png", CC_CALLBACK_1(LoadingScene::loadingCallback, this));
	Director::getInstance()->getTextureCache()->addImageAsync(resouceMain+"music_on.png", CC_CALLBACK_1(LoadingScene::loadingCallback, this));
	Director::getInstance()->getTextureCache()->addImageAsync(resouceMain+"quitgame_normal.png", CC_CALLBACK_1(LoadingScene::loadingCallback, this));
	Director::getInstance()->getTextureCache()->addImageAsync(resouceMain+"quitgame_select.png", CC_CALLBACK_1(LoadingScene::loadingCallback, this));
	Director::getInstance()->getTextureCache()->addImageAsync(resouceMain+"Set_Music.png", CC_CALLBACK_1(LoadingScene::loadingCallback, this));
	Director::getInstance()->getTextureCache()->addImageAsync(resouceMain+"Setting_n.png", CC_CALLBACK_1(LoadingScene::loadingCallback, this));
	Director::getInstance()->getTextureCache()->addImageAsync(resouceMain+"setting_s.png", CC_CALLBACK_1(LoadingScene::loadingCallback, this));
	Director::getInstance()->getTextureCache()->addImageAsync(resouceMain+"startgame_normal.png", CC_CALLBACK_1(LoadingScene::loadingCallback, this));
	Director::getInstance()->getTextureCache()->addImageAsync(resouceMain+"startgame_select.png", CC_CALLBACK_1(LoadingScene::loadingCallback, this));
	Director::getInstance()->getTextureCache()->addImageAsync(resouceMain+"wxb.jpg", CC_CALLBACK_1(LoadingScene::loadingCallback, this));
	Director::getInstance()->getTextureCache()->addImageAsync(resouceMain+"zhy.jpg", CC_CALLBACK_1(LoadingScene::loadingCallback, this));
	Director::getInstance()->getTextureCache()->addImageAsync(resouceMain + "sound_effect_off.png", CC_CALLBACK_1(LoadingScene::loadingCallback, this));
	Director::getInstance()->getTextureCache()->addImageAsync(resouceMain + "sound_effect_on.png", CC_CALLBACK_1(LoadingScene::loadingCallback, this));
	Director::getInstance()->getTextureCache()->addImageAsync(resouceMain + "switchBg.png", CC_CALLBACK_1(LoadingScene::loadingCallback, this));

}

回調函數中這樣處理:
void LoadingScene::loadingCallback(Texture2D*)
{
	progessAdd();
}

void LoadingScene::progessAdd()
{
	progressPercent += everyAdd;

	if (100-progressPercent <everyAdd)
	{
		progressPercent = 100;
		auto scene = MainScene::createScene();
		Director::getInstance()->replaceScene(TransitionFade::create(1, scene));
	}
	percentLabel->setString(StringUtils::format("%d", int(progressPercent)));//更新percentLabel的值
	loadProgress->setPercentage(progressPercent);
}
在loading到100%的時候切換場景。
資源不是很多進度條沒有+1的前進,這裏也沒有任何技巧性的實現方法。 還望大家有好優化的時候告訴我,一起進步,萬分感謝~

補:好吧,我承認上面的loading邏輯其實是錯誤的!
正確的應該是每異步加載完一個資源纔可以加載後一個,同時最好一次加載一個,不然有可能造成內存陡升,導致應用直接被幹掉!
如下寫:
頭文件如下:
private:
	void update(float f);
	std::vector<std::string> reloadResources;
	int reloadNums = 0;
	int curReloadNum = 0;
	bool loading = true;

	void imageAsyncCallback( cocos2d::Texture2D*);

cpp如下:
bool Welcome::init()
{
    //////////////////////////////
    // 1. super init first
    if ( !Layer::init() )
    {
        return false;
    }
    
    Size visibleSize = Director::getInstance()->getVisibleSize();

    /////////////////////////////
    // 3. add your codes below...
    auto sprite = Sprite::create("welcome.png");

    // position the sprite on the center of the screen
    sprite->setPosition(Point(visibleSize.width/2, visibleSize.height/2));

    // add the sprite as a child to this layer
    this->addChild(sprite, 0);
    
	reloadResources = {"Themes/scene/mainscene.png","Themes/scene/mainscene_title.png"};
	reloadNums = reloadResources.size();
	this->scheduleUpdate();

    return true;
}

void Welcome::update(float f)
{
	if (loading)
	{
		loading = false;
		if (curReloadNum >= reloadNums)
		{
			this->unscheduleUpdate();
			auto scene = MainScene::createScene();
			Director::getInstance()->replaceScene(TransitionCrossFade::create(0.6, scene));
			return;
		}
		else
		{
			Director::getInstance()->getTextureCache()->addImageAsync(reloadResources[curReloadNum],
				CC_CALLBACK_1(Welcome::imageAsyncCallback, this));
		}
	}
	
}

void Welcome::imageAsyncCallback(Texture2D*)
{
	curReloadNum++;
	loading = true;
}

這樣就做到了每次只加載一個資源,每一幀來操作一次,可以實時的進行垃圾回收。

遊戲主場景

這裏就直接略過,看源碼就知道了。 只是單純的背景和兩個Menu。

選擇關卡

本文的重點在於關卡的選擇。超級瑪麗一共八關,可以選擇對應的關卡進行遊戲。
這裏我們實現類似於保衛蘿蔔關卡切換的功能,左右拖動進行選關,點擊進行遊戲。
這裏有三個方法可以實現:
第一種:
新建一個Layer,橫向添加8個精靈。實現觸摸事件,左右觸摸拖動實現Layer的移動。觸摸結束後,根據拖動的位置自己調整層的position(使用MoveTo動畫)。
這種方法的機制簡單,但是寫起來複雜。
第二種:
我使用了TableView。剛開始沒有理解TableView的用法,嘗試了一下失敗了。
TableView可以創建水平的或者垂直的一系列小單元,可以設置這些單元的間隔。  想了一下,特別適合做排行榜,不知道scroll做排行榜效果怎麼樣。
TableView的弊端在於拖動結束的時候會根據觸摸慣性來決定停止在哪個單元,不符合我們的預期。
使用TabelView報錯解析:
引入庫libExtensions
在類中添加頭文件 #include "cocos-ext.h"
cocos2dx 無法打開包括文件: “extensions/ExtensionMacros.h”: No such file or directory (..\Classes\SelectLevel
是以爲附加包含目錄沒有引入$(EngineRoot)(添加方法參考我的博客:點擊打開鏈接
使用方法:
在init添加,千萬不要丟掉代理方法setDelegate,不然會發現無響應:
	tableView = TableView::create(this, Size(spWidth, spHeight));
	tableView->setDirection(ScrollView::Direction::HORIZONTAL);
	tableView->setPosition(Point((winSize.width - spWidth)/2, (winSize.height-spHeight)/2));
	tableView->setDelegate(this);
	this->addChild(tableView);
	tableView->reloadData();
必須要實現的六個回調函數,其中兩個是scroll的在.h文件中聲明{}就好。
void SelectLevel::tableCellTouched(TableView* table, TableViewCell* cell)
{
	CCLOG("cell touched at index: %ld", cell->getIdx());
}

Size SelectLevel::tableCellSizeForIndex(TableView *table, ssize_t idx)
{
	return Director::getInstance()->getVisibleSize();
}

TableViewCell* SelectLevel::tableCellAtIndex(TableView *table, ssize_t idx)
{
	auto string = String::createWithFormat("%ld", idx+1);
	TableViewCell *cell = table->dequeueCell();
	if (!cell) {
		cell = new TableViewCell();
		cell->autorelease();
		auto sprite = Sprite::create(StringUtils::format("image/level/select%d.jpg", idx));
		sprite->setAnchorPoint(Point::ZERO);
		sprite->setPosition(Point(0, 0));
		cell->addChild(sprite);

		auto label = Label::createWithSystemFont(string->getCString(), "Helvetica", 20.0);
		label->setPosition(Point::ZERO);
		label->setAnchorPoint(Point::ZERO);
		label->setTag(123);
		cell->addChild(label);
	}
	else
	{
		auto label = (Label*)cell->getChildByTag(123);
		label->setString(string->getCString());
	}


	return cell;
}

ssize_t SelectLevel::numberOfCellsInTableView(TableView *table)
{
	return Global::getInstance()->getTotalLevels();
}



第三種:
PageView。
在官方的Demo都有例子,在extensions->最下面->GUI Edito裏面,我看到的第一眼就知道這個效果就是我想要的(至少差不多,我們可以改)。
根據Demo實現起來也很簡單。但是有一個要注意的地方:
pageView->addEventListenerPageView(this, pagevieweventselector(SelectLevel::pageViewEvent));
回調函數pageViewEvent的聲明一定要使用:
void SelectLevel::pageViewEvent(Ref *pSender, PageViewEventType type)
{

}
千萬不要使用:
void SelectLevel::pageViewEvent(Ref *pSender)
{

}
後面這種寫法編譯不報錯,但是運行的時候報錯崩潰。

PageList的實現如下:
    pageView = PageView::create();
	pageView->setSize(Size(winSize.width, winSize.height));
	pageView->setPosition(Point(0,0));

	for (int i = 1; i < Global::getInstance()->getTotalLevels(); i++)
	{
		Layout* layout = Layout::create();
		layout->setSize(Size(winSize.width, winSize.height));

		ImageView* imageView = ImageView::create(StringUtils::format("image/level/select%d.jpg", i));
		imageView->setScale9Enabled(true);
		imageView->setSize(Size(spWidth, spHeight));
		imageView->setPosition(Point(layout->getSize().width / 2.0f, layout->getSize().height / 2.0f));
		layout->addChild(imageView);

		Text* label = Text::create(StringUtils::format("page %d", i), "fonts/Marker Felt.ttf", 30);
		label->setColor(Color3B(192, 192, 192));
		label->setPosition(Point(layout->getSize().width / 2.0f, layout->getSize().height / 2.0f));
		layout->addChild(label);

		pageView->addPage(layout);
	}
	pageView->addEventListenerPageView(this, pagevieweventselector(SelectLevel::pageViewEvent));

	this->addChild(pageView);

這樣我們就實現了拖動選關了,下面就該實現單擊開始遊戲了。
這裏又出現了問題,PageView的回調函數在每次觸摸end的時候都會回調,所以我們不能根據它的回調函數來做場景切換。
我考慮在當前層添加Touch事件來判斷開始遊戲,結果發現PageView把觸摸回調事件攔截了。
沒辦法,我就在PageView上面添加了一個透明層,在這個層中來處理觸摸時間,實現開始遊戲。
(PS:朋友把PageView裏面的Menu改成button是可以的,可以拖動和點擊)
首先添加透明層:
	auto layerr = Layer::create();

	layerr->setContentSize(Size(spWidth, spHeight));
	layerr->setPosition(size.width/2, size.height/2);
	
	layerr->setZOrder(111);
	this->addChild(layerr);
	auto listenTouch = EventListenerTouchOneByOne::create();
	listenTouch->onTouchBegan = CC_CALLBACK_2(SelectLevel::onTouchBegan, this);
	//listenTouch->onTouchMoved = CC_CALLBACK_2(SelectLevel::onTouchMoved, this);
	listenTouch->onTouchEnded = CC_CALLBACK_2(SelectLevel::onTouchEnded, this);
	//listenTouch->onTouchCancelled = CC_CALLBACK_2(SelectLevel::onTouchCancelled, this);
	Director::getInstance()->getEventDispatcher()->addEventListenerWithSceneGraphPriority(listenTouch, layerr);
其實是做好觸摸回調:
要判斷好什麼時候才切換場景。觸摸PageView外面區域不切換,移動PageView超過5像素不切換場景。
bool SelectLevel::onTouchBegan(Touch* touch, Event* event)
{
	clickBeginPoint = touch->getLocation();
	return true;
}

void SelectLevel::onTouchEnded(Touch* touch, Event* event)
{
	int dragDistance = abs(touch->getLocation().x - clickBeginPoint.x);
	//如果單擊超過5像素 切換場景
	if (dragDistance < 5 )
	{
		Rect layerRect = Rect((winSize.width - spWidth) / 2, (winSize.height - spHeight) / 2, spWidth, spHeight);
		if (layerRect.containsPoint(touch->getLocation()))
		{
			//要切換的關
			int level = pageView->getCurPageIndex()+1;
		
		}
	}
}

到這裏PageView選擇並開始對應關卡的遊戲就搞定了。


明天進行TMX地圖的解析了,有需要源碼的可以加發郵件給我 [email protected]或者加QQ179261480。
最後完成代碼後會對整個代碼進行共享。


發佈了63 篇原創文章 · 獲贊 56 · 訪問量 25萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章