一般化(Generalization)是很重要的一個步驟
卻也是常常被人忽略的一個細節
簡單來說,就是把特殊用途的程式碼
轉變成為一般用途的程式碼
舉個例子來說,我現在有20種怪物的AI要寫
直覺上我們會建立20個Class來分別實現
但是事實上,真的需要那麼多嗎?
仔細分析一下怪物的種類與特性
地上爬的,天上飛的,水裡游的,就分成三大類
攻擊模式,體型大小等等都可以是分類的依據
kgsprogrammer 發表在 痞客邦 留言(1) 人氣(75)
進入這一個章節的最後一部份
一口氣就介紹三小節State,Composite和Command
不過不用太擔心,我想這篇文章會很短
因為...我懶得打那麼多的字~
前兩篇文章不斷提到觀念,也是這個章節的中心思想
當你遇到複雜的巢狀判斷式,或是冗長的判斷式時
請毫不猶豫的簡化它!
所以延續前面的主題,另外有三個Pattern可以幫助你簡化
也就是State,Composite和Command
講到State,就不能不提到目前的AI State machine架構
沒錯,它就是典型的State Pattern
AI利用FSMActor來操縱State的轉移
來達成不同階段AI的行為
再來是Composite
看完書中的範例,我腦中只想到一個架構:AI Parser
其實就是XML Parser
利用Composite的架構來解析複雜的語法標籤
真是再合適不過了
至於Command,我並不是很熟這個架構
但是我直覺性的聯想到Debug Tool裡面的Command Mode
因為Debug Tool是會無限延伸的
定死了架構到後面一定會很悽慘
Command在這時候就是很好的選擇
把各種不同類型指令放到底下的Class去解析,去處理
只訂定共通的操縱介面
如此一來Debug Tool的Parser就會很乾淨,也容易管理和擴充
以上,就是Simplification最後一個部分~
kgsprogrammer 發表在 痞客邦 留言(3) 人氣(64)
這次來比較兩個有趣的東西:Strategy和Decorator
同樣是巢狀邏輯判斷式的寫法
改法不止限於上次提到的Composed
Strategy和Decorator也是不錯的選擇
艾倫史密斯在互動元件那邊做了一個示範
詳細內容我不贅述了,有興趣可以去找找看
總之是在處理主角吸上互動元件後
怎麼對主角施予重力和建立Joint
讓主角不會掉下來,並且可以做有限制的移動
第一個碰到的問題是
當主角踩在互動元件上面的那一瞬間
要怎麼去判斷他踩到的是什麼呢?
想當然爾,碰撞會有碰撞訊息
於是以下的Code就會出現:
switch (pMsg->m_uiType)
{
case MST_TRIGGER_COLLISION:
{
ksMsgActor *pObj = pMsg->m_pTouchAI;
if ( NiIsKindOf(ksObjA, pObj) || NiIsKindOf(ksObjB, pObj) )
// 建立第一種Joint
else if ( NiIsKindOf(ksObjC, pObj) || NiIsKindOf(ksObjD, pObj) )
// 建立第二種Joint
else if ( NiIsKindOf(ksObjE, pObj) )
// 建立第三種Joint
else if ( NiIsKindOf(ksObjF, pObj) )
// 建立第四種Joint
break;
}
}
很好,決定了Joint的種類
接下來主角開始移動,但是磁力物件千百種
有的直有的彎,怎麼知道主角該不該轉彎呢?
主角吸在物件上面,所以會有Trigger的訊息
於是下列的程式碼出現:
switch (pMsg->m_uiType)
{
case MST_TRIGGER_ON_STAY:
{
ksMsgActor *pObj = pMsg->m_pTouchAI;
if ( NiIsKindOf(ksObjA, pObj) || NiIsKindOf(ksObjB, pObj) )
// 走在直線型的物件上
else if ( NiIsKindOf(ksObjC, pObj) || NiIsKindOf(ksObjD, pObj) )
// 走在圓弧形的物件上
break;
}
}
如果你以為艾倫史密斯是這種寫法,那你就錯了
要知道,他可是史密斯
寫法當然要不一樣,所以他採取的是Strategy
一種抽象到不行的寫法,害我每次Trace Code都很痛苦...
不過這真的是物件抽象的極致
搞到最後他也不知道是碰到哪一個種類的物件~XD
使用Strategy,可以大大減少主角這邊判斷的機制
我只需要呼叫統一的介面,就可以輕鬆吸上磁力物件
就像這樣:
ksTerrainDestKit* pKit = (ksTerrainDestKit*)pAI->GetKit(TERRAIN_DEST_KIT);
pKit->CreateJoint();
這裡的CreateJoint裡面包著就是Strategy function
同理,主角移動時也用類似的方式來取得資料:
pKit->AddForce();
剩下的由物件自己去處,而ksTerrainDestKit就是Strategy委託者
那麼Decorator何時會使用呢?
對我來說,如果Strategy是橫向的封裝
那Decorator就是縱向的封裝
Strategy會依據種類來決定使用哪一種function
但是Decorator是一層一層的
封裝多少層,就會經過多少個function
兩個Pattern詳細比較在豬屎排程寫的比較多
但是真正要使用的時刻,怎麼判斷用哪一個
還真的是靠個人修行!
講了那麼多,其實兩種Pattern的比較只講了一點點...
好吧,我承認自己修行不夠,沒法寫出精闢的解析
所以就讓我繼續下一章節吧!XD
kgsprogrammer 發表在 痞客邦 留言(1) 人氣(44)
結束了創建(Creation)的部分,接下來則是簡化(Simplification)
我想又臭又長的程式碼是沒有人可以接受的
kgsprogrammer 發表在 痞客邦 留言(2) 人氣(46)
最後一個是為了確保Factory唯一性所做的機制
也就是DesignPatterns裡面的Singleton
相信很多人都有使用過的經驗
但是書中提到的卻是使用過度的Singletonitis(指沈迷於Singleton)
裡面講了很多不需要使用Singleton的情形
它最大的問題就是製造了全域的變數
而且還是一個工廠(Factory)的大小!
並不是說不要使用Singleton
而是使用它之前一定要慎重的思考其必要性
到最後得到一個結論:
『當你可以設計出一種避免使用Singleton的方法時
就不要使用它』
再回到AI的建立
假設今天要建立物理的Shape
而且一隻AI可能需要多個Shape
可能會有這樣的設計:
class ShapeFactory
{
public:
static ShapeFactory * GetInstance()
{
static ShapeFactory *ms_pFactory = NULL;
if (ms_pFactory == NULL)
ms_pFactory = new ShapeFactory();
return ms_pFactory;
};
NxShape *CreateShape(iType, vExtend, kTransform)
{
switch (iType)
{
case NxBoxShape:
return ksPhysX::Get()->CreateBoxShape(vExtend, kTransform);
case NxSphereShape:
return ksPhysX::Get()->CreateSphereShape(
vExtend, kTransform);
case NxCapsultShape:
return ksPhysX::Get()->CreateCapsultShape(
vExtend, kTransform);
}
};
private:
ShapeFactory();
~ShapeFactory();
};
class PhysXKit
{
public:
NxShape *CreateShape(iType, vExtend, kTransform);
{
return ShapeFactory::GetInstance()->CreateShape(
iType, vExtend, kTransform);
}
};
嗯,我需要建立很多不同種類的Shape
所以我建立了一個Shpae Factory來處理這件事情
而且還是一個Singleton的Factory
但是有這個必要嗎?
事實上這個Factory只會被PhysXKit使用
卻被建立為全域的Singleton Instance
理論上它應該被合併到PhysXKit裡面,如下:
class PhysXKit
{
public:
NxShape *CreateShape(iType, vExtend, kTransform)
{
switch (iType)
{
case NxBoxShape:
return ksPhysX::Get()->CreateBoxShape(vExtend, kTransform);
case NxSphereShape:
return ksPhysX::Get()->CreateSphereShape(
vExtend, kTransform);
case NxCapsultShape:
return ksPhysX::Get()->CreateCapsultShape(
vExtend, kTransform);
}
};
};
這就是書上提到的Inline Singleton(Singleton內置化)
如果再看詳細一點,會發現ksPhysX似乎也有同樣的問題
如此一直改下去,就能夠把不該存在的Singleton消除了
kgsprogrammer 發表在 痞客邦 留言(1) 人氣(51)
延續上一次的主題,再來看看Factory內部的建立流程
一個AI需要載入Model,需要建立FSM,需要建立Physics...
所以流程大概會有:
// Load model and create callback function
m_pAM = NiActorManager::Create( sFileName );
m_pkAnimCallback = NiNew AnimCallbackObject;
m_pAM->SetCallbackObject(m_pkAnimCallback);
// create state machine and some state node
m_pFSM = NiNew ksFSM( iAgentType );
ksBaseStateNode *pIdleState = NiNew ksBaseStateNode(
IdleStateID );
m_pFSM->AddState(pIdleState);
ksBaseStateNode *pMoveState = NiNew ksBaseStateNode(
MoveStateID );
m_pFSM->AddState(pMoveState);
// create NxShape and NxActor for physcis
NxShapeDesc *pShapeDesc = ksPhysX::Get()->CreateBoxShape(
shapeflag, extension, transform);
m_pPhysXActor = ksPhysX::Get()->GetPhysXScene()->createActor(
pShapeDesc);
嗯,看完頭也昏了,所以這也不是個好的建立方式
這時候Builder就出現了
這也是一種對於建構子的封裝方式
因為使用者建立AI的時候,通常不想知道太詳細的建立方式
而且寫太多雜七雜八很容易搞混或是漏寫
既然我們知道可以分成三大部分,就可以把建立的步驟拆解
每一個步驟都封裝成為function,只留下簡單的interface
首先把Builder建立出來:
class ksAIBuilder : public NiRefObject
{
public:
ksAIBuilder();
~ksAIBuilder();
// Load model and create callback function
NiActorManager *CreateActorManager();
// create state machine and some state node
ksFSM *CreateFSM();
// create NxShape and NxActor for physcis
NxActor *CreatePhysics();
};
建立完之後再來就是使用了,於是AI的建立流程就變成:
ksAIBuilder *pBuilder = NiNew ksAIBuilder();
m_pAM = pBuilder->CreateActorManager();
m_pFSM = pBuilder->CreateFSM();
m_pPhysXAcotr = pBuilder->CreatePhysics();
這樣一來要建立AI就簡單多了,變化性又高
當我想要一個沒有物理的AI,只要不呼叫CreatePhysics就可以了
既直覺又不容易出錯
kgsprogrammer 發表在 痞客邦 留言(1) 人氣(37)
首先要道歉一下,我看書的速度真的太慢了...
總之目前進入了第一大單元 - 創建(Creation)
顧名思義的它就是要教你怎麼用好的方式去建立一個Class
由於裡面內容牽扯到大量豬屎排程(Design Patterns)
不熟的人還請搭配書本服用
在我們建立一系列有繼承關係的Class,例如State Node
會很直覺的想要建立一個Factory來統一建立的介面
這樣做除了可以統一Client端呼叫建立的介面之外
維護起來也會比較簡單(不用到處東找西找)
但是怎麼建立那個Factory卻是個大學問
首先是Factory的介面
一般來說,以建立AI的例子來講,如下:
ksAI *pAI = AIFactory->CreateAI();
假如今天想要建立不同種類的怪物AI呢?
ksAI *pAI = AIFactory->CreateAI( iAIType );
複雜一點,建立AI的時候必須給他一些初始值:
ksAI *pAI = AIFactory->CreateAI( iAIType, iInitHP );
好像不夠,產生器需要生產的種類跟速度,有些怪物一開始要隱形
有些怪物需要Ragdoll Model:
ksAI *pAI = AIFactory->CreateAI( iAIType, iInitHP,
iChildType, fGeneraeSpeed, bVisiable,
sRagdollModelName...... );
是不是開始覺得複雜起來?因為初始值的種類太多了
當然你可以把初始值的資料包裝成Class或是Structure
但是總覺得包裝起來很麻煩,還要多建立一個物件,是否有其他方式呢?
書上提供一種Creation Method的方法
簡單來說就是對建構子(Constructor)作封裝的動作
例如我們確定了怪物的大分類,可以把介面再細分一下:
ksAI *pNormalAI = AIFactory->CreateNormalAI( iAIType,
iInitHP, bVisiable);
ksAI *pGeneratorAI = AIFactory->CreateGeneratorAI( iAIType,
iInitHP, iChildType, fGeneraeSpeed);
ksAI *pRagdollAI = AIFactory->CreateRagdollAI( iAIType, iInitHP,
sRagdollModelName);
這樣一來一方面你可以很清楚知道你建立了哪個種類的AI
另一方面還可以簡化和控制傳入的參數數量
並且把子類別的建構子也封裝了起來
kgsprogrammer 發表在 痞客邦 留言(1) 人氣(63)
最近在書上看到的,關於設計過頭與設計不足的一些現象,給大家做參考
設計過頭:
學完了DesignPattern就一昧的想要使用
結果原本短短幾行或一個Function就可以解決的問題
卻使用了複雜的架構(當然是從DesignPattern學來的)來完成,白白多寫了幾十行,甚至幾百行的Code
設計不足:
最常見的就是設計完的架構裡面,含有雜亂的資料結構(或甚至沒有!)
每一個Class之間的變數或function都可以互相使用,沒有切割好溝通的Interface
造成取得各種資料的管道千奇百怪,沒有任何安全性
變數及function名稱本身無意義,且會讓人誤解!
Function內容冗長且缺乏邏輯,執行好幾個不相干的內容,且有大量重複的程式碼
最重要的是,程式碼難以閱讀,難以理解!
撰寫程式碼一定要注意的事情:
@移除重複的程式碼:將可重複利用的部分蒐集起來,寫成function
@簡化程式碼:一開始寫的時候一定會有雜亂的演算法或步驟
但是只要仔細再看過一次,通常都可以找到更精簡的方式來撰寫
@讓程式碼的目的更明確:看不懂在做什麼的程式碼最令人頭痛
除了Debug會讓人摸不著頭緒外,想要修改的人也會看的一頭霧水;
另外變數與function的命名也很重要,之所以會有匈牙利命名法這類的東西
就是為了讓人有更加習慣且統一的規則,增加程式碼的可讀性與理解
@不斷提醒自己要時常去做以上三件事情:程式寫久了難免會有忽略或是寫壞掉的地方
定期去做檢查,或甚至是Refactory可以讓整個架構爛掉之前做些補救
kgsprogrammer 發表在 痞客邦 留言(0) 人氣(48)