來公司四個半月了,從對用戶端遊戲編程的小白慢慢的也能寫一些東西了,當然了這裡最感謝的人就是九天了,對於九天其它的好我就不說了,就是感覺九天為了團隊,為了項目,他在很用心的做每一件事情。就如武俠小說裡的人物,有的練習功夫強身健體,有的為了取得江湖地位,有的為了報仇雪恨,當然有很少一部分當做興趣愛好研究,比如說周伯通,他就是個武癡,對什麼功夫都感興趣。可是這些人裡沒有一項符合九天的,我認為九天喜歡將事情做的比較完美已經成為一種習慣,無論是代碼方面還是說管理方面,處處為大家考慮,為公司考慮。到是他自己的事情考慮的很少,這裡我就不舉例子了,大家都懂得,:-D。要寫總結,就忍不住感謝下九天,來公司這麼長時間了,還沒感謝過他。
剛開始接觸用戶端遊戲,我只是本能的處於接受任務,完成任務。由於沒有認真仔細的去思考這些任務,犯了很多低級的錯誤。其實這就是一種不負責任的表現方式,只是把任務當做任務完成了,但並沒想這個東西是幹什麼用的,怎麼用?用術語說的話,要實現什麼功能,這個功能在什麼地方要用?這裡舉個我第一次接收的一個小任務的例子:實現一個函數,這個函數實現的功能是設定不同語言,來實現多語言載入。當時我也沒仔細想,就將這個函數寫成了一個全域函數,放在config檔案裡。config檔案是用來儲存工程設定還有一些通用的全域變數的東西,我寫在這裡,一個是設定不方便,在一個也不符合config檔案的性質。別看這是一個小小的函數,通過這個函數,程式裡實現了不同國家語言的分離,只通過修改材質的尾碼名就可以方便的切換不同國家的材質。
const string ConfigManager::GetResourceTextureName( const char* res_name ) {
std::string full_name(res_name);
switch (language_) {
case LAN_EN:
full_name += "_en.png";
break;
case LAN_CN:
full_name += "_cn.png";
break;
case LAN_KR:
full_name += "_kr.png";
break;
case LAN_JP:
full_name += "_jp.png";
break;
default:
CCAssert(0,"wrong language");
break;
}
return full_name;
}
故事代碼1
第二個比較典型的任務是金幣儀錶,如下:
圖片1
這個剛開始認為比較簡單的一個任務,就是說根據當前玩家的分數移動每個位元的數字材質,表現出一個數字。分析下需求大概有以下幾點要求:
1、玩家資料載入時算好每個素材的位置
2、監測到玩家資料有變化時材質根據數值變化,播放動作
3、數字變化動作就如滾輪效果,0——9,9——0實現平滑過度
4、數字不能同是播放所有位元的效果
剛開始做的時候美術給出的是一個橫版的材質,要做出豎版的效果,這可如何是好,經過和九天的討論得出通過裁剪出每個小的數字材質,然後去控制小的數字材質,這期間遇到了很多問題。剛開始對cocos2d-x動作那套不是太懂,基本就會用個MoveTo(),對ccnode那套也不是很熟,對這些概念都不是很理解。所以每次要表現數字變化時,都先產生一個材質數組,然後算出每個小的材質到目標位置的相對的距離。在動畫播放過程中,假如有新的動畫,則先將數字變化存放在數組裡,當然了一個是先進先出的模型。通過一周的折騰準系統是實現了,但是效果不是很好,尤其0——9,9——0的突變沒有處理好。並且還用的是單間模式,豈不知金幣儀錶只是ui層的一個功能模組,根本沒有必要用單間,所以這個也是對功能沒有想清楚。後來經過一周的折騰,發現這種方法太扭曲了,問題很多,經過和lyc,九天的商討改變了材質,由橫條改為豎條,就是這麼簡單的一改,問題一下簡單化了很多,首先我們都知道圓柱面展開後是一個矩形,這樣通過豎條的矩形就能很好的實現圓柱的功能,只要簡單的在矩形9後面再補個0,就可以了。因為9之後就是0,0之前就是9,為了讓動作的連貫性,動作播放完,立馬調用setposition將材質設定一個位置。就簡單的實現了滾動效果。功能實現了,效果還的慢慢調,遊戲不就是要個感覺嘛,通過對動作的時間調整,最後實現了這個功能。回想起來這中間又很多值得學習的地方:
1、在做任務的時候一定要想清楚功能,心裡基本要明白通過什麼方式去實現這個功能
2、將一些常用的算數歸納為數學公式,大大方便運算。
3、多和自己的同事交流,三人行必有我師,咱們這個團隊都是很樸實的技術青年,對每個人的問題都很熱心的。
4、只有通過多做,對這個東西熟悉了,理解了,才能做出如工藝品的東西
sky項目的加密解密程式還是比較有意思的,這個程式是通過一套叫tea的演算法,將程式的設定檔加密成為2進位檔案,在程式啟動的時候再進行解密。首先來看下密碼編譯演算法:
void TEA::encrypt(const ulong *in, ulong *out) {
ulong *k = (ulong*)_key;
register ulong y = in[0];
register ulong z = in[1];
register ulong a = k[0];
register ulong b = k[1];
register ulong c = k[2];
register ulong d = k[3];
register ulong delta = 0x9E3779B9; /* (sqrt(5)-1)/2*2^32 */
register int round = _round;
register ulong sum = 0;
while (round--) { /* basic cycle start */
sum += delta;
y += ((z << 4) + a) ^ (z + sum) ^ ((z >> 5) + b);
z += ((y << 4) + c) ^ (y + sum) ^ ((y >> 5) + d);
} /* end cycle */
out[0] = y;
out[1] = z;
}
故事代碼2
在這過程中首先的看懂這個加密函數,由於網上的例子是直接加密解密一個很短的字串,我就天真的認為密碼編譯演算法是傳入一個char*,然後通過加密傳出一個char*,這樣就加密好了。導致花了很多時間在尋找為什麼程式加密不對。再一個字串都是以‘/0’結尾的,由於對概念理解的不是很清楚,程式總是出現一些比較奇怪的問題。
const string TEA::SaveFile(const char* content)
{
int file_size = strlen(content);
byte *plain = new byte[file_size + 1];
CharToByte((char*)content, plain, file_size);
byte *new_plain = new byte[file_size*8 + 1];
int k = 0;
for (int i = 0; i < file_size; ++i)
{
encrypt(plain + i, new_plain + k);
k += 8;
}
char *out = new char[file_size*8 + 1];
ByteToChar(new_plain, out, file_size * 8);
string crypt_str(out, file_size * 8);
delete []plain;
delete []new_plain;
delete []out;
return crypt_str;
}
故事代碼3
最後通過做這個功能模組得到三點:
1、申請記憶體的時候,要多申請一位,用來儲存,結尾符號
2、讀取字串的時候都是以‘/0’結尾的,所以說對與2進位的資料流用strlen就不對了。
3、strlen函數其實就是遍曆數組每個字元,只到只到'/0'的時候返回遍曆次數。
4、要有勇氣和渴望多看看代碼,明白了遠離問題也就不難了。
這裡大概說說程式裡遇到一些問題,以及解決方案,增強自己的自備知識。
1、遊戲程式的運行機理
遊戲程式的main函數是一個while函數,只到玩家退出程式,while迴圈結束
int main()
{
while(1)
{
if(g_run)
{
Tick(ts);
DrawScene();
}
else
{
break;
}
}
return 0;
}
偽碼1
遊戲程式和應用軟體最大的區別就是,應用軟體一直在等待著使用者的輸入,來相應使用者的請求,而遊戲程式就不同了,隨著程式時間的配置,遊戲通過drawscene不斷的去渲染新的畫面,說的通俗點就是輸出一張張材質。材質本質上就是點的屬性集合。比如一個頂點顏色屬性顏色函數是通過color(uchar red, uchar green, uchar blue, uchar alpfa)。材質的像素就是來描述頂點數量的變數。像素越高表明材質越細膩。而遊戲畫面就是通過每秒播放材質的數量來描述的。這就是幀的概念,玩過flash的都懂得,如何通過設定沒幀材質來實現簡單的動畫。說這麼多就是要表達,遊戲程式就是通過在合適的時間輸出不同的材質。注意,我這裡說的是遊戲程式,而不是遊戲。因為遊戲是技術,美術,創意,音樂的集合體。就拿獵鳥裡寵物館裡一個彈出對話方塊來說,這個lyc實現的,說個真心話,這一點我非常佩服lyc,他做遊戲不是簡簡單單的去完成任務,而是通過一種審美的意識去看待每個模組的實現。雖說程式不能對整個遊戲的整體表現有太大的決定權,但在細節的處理上加上自己的理解和愛,局部還是會非常出彩的。
void PetInfoDialog::Show() {
this->setScale(0.0f);
CCScaleTo* scale = new CCScaleTo;
scale->initWithDuration(0.5f,1.0f);
CCEaseBackOut* ease = new CCEaseBackOut;
ease->initWithAction(scale);
scale->release();
runAction(ease);
ease->release();
}
void PetInfoDialog::Hide() {
CCScaleTo* scale = new CCScaleTo;
scale->initWithDuration(0.5f,0.0f);
CCEaseBackIn* ease = new CCEaseBackIn;
ease->initWithAction(scale);
scale->release();
CCCallFunc* call_func = new CCCallFunc;
if(!call_func->initWithTarget(this, callfunc_selector(PetInfoDialog::DestoryCallback) ) ) {
CCLog("call fun selector wrong");
return;
}
CCSequence* seq = new CCSequence;
seq->initOneTwo(ease, call_func);
ease->release();
call_func->release();
runAction(seq);
seq->release();
}
範例程式碼1
2、記憶體管理
學過c/c++的人都知道指標和記憶體管理是最難理解的,這裡我就不詳細說明記憶體管理的話題,只是說說在cocos2d-x裡如何管理好自己的記憶體。
cocos2d-x裡的精靈都繼承自CCobject類,先看看CCobjec自己申請和釋放記憶體的原理。
CCObject::CCObject(void)
{
static unsigned int uObjectCount = 0;
m_uID = ++uObjectCount;
// when the object is created, the refrence count of it is 1
m_uReference = 1;
m_bManaged = false;
}
void CCObject::release(void)
{
assert(m_uReference > 0);
--m_uReference;
if (m_uReference == 0)
{
delete this;
}
}
範例程式碼2
再來看看父類,我說的這個父類不是繼承關係的父類,而是說裝配層次的關係。父類調用addchild發生了什麼。
void CCNode::addChild(CCNode *child, int zOrder, int tag)
{
CCAssert( child != NULL, "Argument must be non-nil");
CCAssert( child->m_pParent == NULL, "child already added. It can't be added again");
if( ! m_pChildren )
{
this->childrenAlloc();
}
this->insertChild(child, zOrder);
child->m_nTag = tag;
child->setParent(this);
if( m_bIsRunning )
{
child->onEnter();
child->onEnterTransitionDidFinish();
}
}
void CCNode::insertChild(CCNode* child, int z)
{
unsigned int index = 0;
CCNode* a = (CCNode*) m_pChildren->lastObject();
if (!a || a->getZOrder() <= z)
{
m_pChildren->addObject(child);
}
else
{
CCObject* pObject;
CCARRAY_FOREACH(m_pChildren, pObject)
{
CCNode* pNode = (CCNode*) pObject;
if ( pNode && (pNode->m_nZOrder > z ))
{
m_pChildren->insertObject(child, index);
break;
}
index++;
}
}
child->setZOrder(z);
}
範例程式碼3
通過原始碼,得到結論如下:
1、精靈建立時,它的reference加1,relese的時候reference減1。
2、將精靈安裝到父類的時候,父類會調用addobject,這樣父類就會使精靈reference數加1,這就意為著精靈的生命週期又它的父類管理了。
3、如果不能一一對應的使reference的值加1和減1的話就會造成記憶體流失。
4、從程式裡看出,每個sprite只能添加到一個父類上。
3、狀態機器
void BackgroundAncientLayer::Update(cocos2d::ccTime ts)
{
if ((torch_num_ < kTorchCount) && (torch_time_ += ts) > 1.0f)
{
SetTorchGui();
torch_time_ = 0;
}
switch(state_)
{
case STATE_START:
SetState(STATE_CLOUD_OUT);
break;
case STATE_CLOUD_OUT:
#ifdef TARGET_IPHONE
PlayCloudAciton(ccp(1522.0f * kIphoneScaleFactor, 210.0f * kIphoneScaleFactor),
ccp(-420.0f * kIphoneScaleFactor, 850.0f * kIphoneScaleFactor), 80.0f, 15.0f);
#else
PlayCloudAciton(ccp(1707.0f, 0), ccp(-568.0f, 895.0f), 80.0f, 15.0f);
#endif
SetState(STATE_END);
case STATE_END:
break;
default:
break;
}
}
範例程式碼4
通過狀態來標記程式啟動並執行不同階段,有以下幾點好處:
1、思路比較清晰,在本例子中,很清晰的表明了要乾的事情,這個例子比較簡單,遇到複雜的狀態很大程度上可以降低程式的出錯率。
2、可以很容易的跳躍到不同的狀態上。
4、簡單的設計模式
摘自 武龍飛的空中樓閣