出自: http://blog.csdn.net/tonywjd/archive/2006/08/11/1052346.aspx
游戲主要用到了幾個(gè)引擎,物理引擎(PhysicsX SDK 2.3.2,即NovedeX的新版本),圖形渲染引擎(OGRE 1.2.0,包括人機(jī)界面的CEGUI部分),聲音引擎(Direct Sound),網(wǎng)絡(luò)引擎(RakNet,可惜由于時(shí)間等原因,加入失敗,大大降低游戲可玩性),建模用Maya和3dsMax。
模型的導(dǎo)出
OGRE有自己的3D模型格式(.mesh格式)。導(dǎo)出可以直接出OGRE官方網(wǎng)站下載導(dǎo)出插件。不過(guò)導(dǎo)出插件有好些Bug。對(duì)于建模方式的不同導(dǎo)出也不同,Maya的導(dǎo)出插件不能導(dǎo)出非封閉的曲面,而3dsMax可以,所以可以通過(guò)它導(dǎo)成.mesh。
另外,在物理引擎中要表現(xiàn)精確的碰撞,最好能得到模型的網(wǎng)格結(jié)構(gòu),即模型的點(diǎn)面信息,而不是用簡(jiǎn)單的規(guī)則形式來(lái)包裹。我們的游戲中地圖用的是前者,而車(chē)輛道具等用的是后者。
但PhysicsX并不支持.mesh。所以我們是通過(guò)導(dǎo)出插件導(dǎo)出的.mesh.xml格式文件中讀取點(diǎn)面信息(僅此),再寫(xiě)到自定義的二進(jìn)制文件格式中,為物理引擎使用。
OGRE與PhysicsX之間的耦合
PhysicsX和OGRE場(chǎng)景管理比較類(lèi)似,都有對(duì)應(yīng)的類(lèi)。一個(gè)可以移動(dòng)的物體,在OGRE中可以用SceneNode實(shí)現(xiàn),并加入到場(chǎng)景管理
中,在PhysicsX中則可以用NxActor對(duì)應(yīng)。NxActor中PhysicsX中的一個(gè)物理單位,它可以把一個(gè)物體用簡(jiǎn)單的幾個(gè)規(guī)則幾何形狀包
裹起來(lái),所以的重力、碰撞等物理作用都作用在NxActor中,并產(chǎn)生相應(yīng)的物理反應(yīng)。得到相應(yīng)的物理數(shù)據(jù)之后,再通過(guò)設(shè)置SceneNode相應(yīng)屬性,
就可以實(shí)現(xiàn)逼真的物理效果。
事實(shí)上,地圖場(chǎng)景也是作為普通的3D物體來(lái)實(shí)現(xiàn)。
而OGRE與PhysicsX之間的映射并不是對(duì)于每一個(gè)物體進(jìn)行映射封裝實(shí)現(xiàn)。而是把這兩部分封裝成獨(dú)立的兩個(gè)模塊,但這兩部分中所有的物體都是
一一對(duì)應(yīng)的。在主程序中,再把從PhysicsX部分計(jì)算出的結(jié)果,傳遞給OGRE部分,進(jìn)行繪制。這樣也為網(wǎng)絡(luò)的加入提供了好的嵌入點(diǎn)。因?yàn)橹灰?wù)端
進(jìn)行全部物理運(yùn)算,再把運(yùn)算的結(jié)果發(fā)給客戶(hù)端OGRE進(jìn)行渲染。
OGRE中的frameStated(const FrameEvent &evt)作為程序的主線(xiàn)程,在其中調(diào)用
m_NxScene->simulate(evt.timeSinceLastFrame); // m_NxScene, instance of (NxScene*)
m_NxScene->flushStream();
m_NxScene->fetchResults(NX_RIGID_BODY_FINISHED);
再把運(yùn)算的物理結(jié)果給OGRE繪制。
這樣OGRE與PhysicsX結(jié)合在一起,而兩者的內(nèi)部實(shí)現(xiàn)是互不影響的,可以獨(dú)立編程,只要處理好兩者物體的一一對(duì)應(yīng)關(guān)系。
地圖場(chǎng)景的實(shí)現(xiàn)
對(duì)于OGRE來(lái)說(shuō)有專(zhuān)門(mén)的室外地圖場(chǎng)景管理。但是由于其高度圖很難導(dǎo)入到PhysicsX中,除非通過(guò)建模時(shí)得到點(diǎn)面信息,但在具體操作中很難做到
高度圖與網(wǎng)格點(diǎn)面信息的一致。我們還試過(guò)在程序初始化時(shí)通過(guò)OGRE的Ray取得地圖上M*N個(gè)點(diǎn)的坐標(biāo),組成2*M*N個(gè)三角形面片,使用到物理引擎
中,近似的實(shí)現(xiàn)物理地圖。但這種實(shí)現(xiàn)所有的三角形在XZ平面上的投影都是一樣的,面片太少可能不精確,太多又會(huì)增加不必要的開(kāi)銷(xiāo),總的來(lái)說(shuō)不夠理想。
PhysicsX中也有專(zhuān)門(mén)用于處理地圖場(chǎng)景的NxActor,可以在創(chuàng)建NxActor前通過(guò)
terrainDesc.heightFieldVerticalAxis = NX_Y; // terrainDesc is a NxTriangleMeshDes
// Default: NX_NOT_HEIGHTFIELD
terrainDesc.heightFieldVerticalExtent = -1000.0f;
進(jìn)行設(shè)置。這樣可以大大的提高效率。這本來(lái)應(yīng)該是一個(gè)理想的做法。但由于我們建模時(shí)模型導(dǎo)出有些誤差的原因,會(huì)出現(xiàn)某些面片為垂直,物理碰撞的效果
在這些地方過(guò)于激烈,表現(xiàn)在屏幕上就是車(chē)會(huì)突然被撞得飛起來(lái)很高。我們?cè)嚵撕芫枚紱](méi)找到合適的模型導(dǎo)出方法避免這一現(xiàn)象。所以只有所地圖場(chǎng)景也作為一個(gè)普
通的NxActor進(jìn)行處理,這樣,雖然克服不了建模導(dǎo)出時(shí)的這個(gè)問(wèn)題,但也不易被玩家發(fā)覺(jué)。
這樣,地圖場(chǎng)景就有了實(shí)現(xiàn)的方法了:在OGRE中作為普通的.mesh對(duì)待,創(chuàng)建SceneNode和Entity;在PhysicsX中,作為普通的NxActor對(duì)待,只是用NxCooking進(jìn)行處理,具體沒(méi)去細(xì)究,可能是為了提高性能。
車(chē)輛的實(shí)現(xiàn)
車(chē)輛的實(shí)現(xiàn)是本游戲的重點(diǎn)。
當(dāng)然也分OGRE和PhysicsX兩部分實(shí)現(xiàn)。
OGRE部分還包括視角即(Camera),而其他諸如油量,道具之類(lèi)的與車(chē)輛本身無(wú)關(guān)的這里不做描述。一輛車(chē)的主要結(jié)構(gòu)如下:

對(duì)于一輛車(chē)的建模,這里還是比較粗糙,只是分為車(chē)輪和車(chē)體,把車(chē)輪分離出來(lái)主要是實(shí)現(xiàn)輪子的轉(zhuǎn)向效果。
由于本游戲的初衷是實(shí)現(xiàn)多人對(duì)戰(zhàn)的網(wǎng)絡(luò)游戲,只是到最后沒(méi)能實(shí)現(xiàn)網(wǎng)絡(luò),改為單機(jī)。但所以的封裝都是為多人網(wǎng)絡(luò)游戲準(zhǔn)備的。
所有車(chē)會(huì)由一個(gè)類(lèi)進(jìn)行統(tǒng)一管理,而這個(gè)類(lèi)用了Singleton設(shè)計(jì)模式。在其上有另一個(gè)封裝OGRE場(chǎng)景和車(chē)輛的總類(lèi)。
PhysicsX部分,車(chē)體由十幾簡(jiǎn)單的面片進(jìn)行包裹,而車(chē)輪是關(guān)鍵。
車(chē)輛通過(guò)構(gòu)造輪子的層次結(jié)構(gòu)實(shí)現(xiàn),車(chē)由車(chē)體和四個(gè)車(chē)輪組成,實(shí)現(xiàn)運(yùn)動(dòng)主要由車(chē)輪控制。
所有車(chē)輛可控的運(yùn)動(dòng)(比如碰撞為不可控)都由車(chē)輪帶動(dòng)。這也符合實(shí)現(xiàn)的物理。
車(chē)輪的實(shí)現(xiàn)主要是通過(guò)NxWheelShape實(shí)現(xiàn)。下面是部分對(duì)NxWheelShape的分析。
NxWheelShape屬性:
radius:Range: (0,inf) 車(chē)輪半徑
suspensionTravel:Range: [0,inf),suspension的作用距離
virtual void setLongitudalTireForceFunction (NxTireFunctionDesc tireFunc)=0
設(shè)定動(dòng)力對(duì)正向的加速度等的影響
virtual void setLateralTireForceFunction (NxTireFunctionDesc tireFunc)
設(shè)定動(dòng)力對(duì)側(cè)向的加速度等的影響,可以實(shí)現(xiàn)側(cè)滑之類(lèi)的
axleSpeed:Range: (-inf,inf)
NOTE: NX_WF_AXLE_SPEED_OVERRIDE flag must be raised for this to have effect
An overridden axle speed of course renders the axle motor and brake torques ineffective
用setAxleSpeed時(shí),直接設(shè)定速度,這種模式不再受motorTorque和brakeTorque等影響
brakeTorque:Range: [0,inf)
剎車(chē)力矩
inverseWheelMass:設(shè)定動(dòng)力對(duì)加速度等的影響,越大作用產(chǎn)生的效果越強(qiáng)
motorTorque:Range: (-inf,inf)
動(dòng)力矩,使車(chē)前進(jìn)
steerAngle:Range: (-PI,PI) 車(chē)輪的偏向角,以弧度表示
virtual void NxWheeleShape::setSuspension(NxSpringDesc spring) [pure virtual]
與其他物體的聯(lián)接有關(guān)
上面這部分是以前寫(xiě)的總結(jié),雖然不完整,現(xiàn)在也不想再細(xì)下去了,到此。
不過(guò)有一點(diǎn)要提的是,雖然車(chē)輪的摩擦力,彈性系數(shù)什么的
(restitution,staticFriction,dynamicFriction)可以設(shè)置,就像其他基本形狀一樣,但似乎沒(méi)什么作用,就跟全
為0一般,我試了很久,都是這樣,不知什么原因。這樣要使車(chē)表現(xiàn)出一定的阻力效果,可以通過(guò)把剎車(chē)力設(shè)為一定值實(shí)現(xiàn)。實(shí)現(xiàn)上可控地去影響輪子運(yùn)動(dòng)速度的只
有動(dòng)力和剎車(chē)力(setMotorTorque,setBrakeTorque),而影響車(chē)輪角度的是(setSteerAngle),另外當(dāng)然也可以直
接設(shè)置輪子的前進(jìn)速度和轉(zhuǎn)動(dòng)的角速度,NxWheelShape提供了這種接口。不過(guò)為了逼真性,最好不要調(diào)用這兩個(gè)接口,因?yàn)樽约簩?shí)現(xiàn)物理效果可能有一
堆公式轉(zhuǎn)化,而只有在車(chē)輛初始化或使用什么道具的時(shí)候調(diào)用。
要實(shí)現(xiàn)側(cè)滑之類(lèi)的效果,只要調(diào)用setLateralTireForceFunction即可。
另外,用現(xiàn)成的NxWheelShape還是有缺點(diǎn)的,就是很難調(diào)手感,可能也較難實(shí)現(xiàn)復(fù)雜的效果。我調(diào)了很久,最后只能將就了。
PhysicsX不需視角處理。
PhysicsX類(lèi)之間的結(jié)構(gòu)與OGRE部分基本上一模一樣。
賽道圈數(shù)的判斷實(shí)現(xiàn)
不知道像極品飛車(chē)之類(lèi)的游戲賽道圈數(shù)是怎么實(shí)現(xiàn)的,應(yīng)該與我們的不同,因?yàn)樗梢悦繒r(shí)每刻的判斷車(chē)輛是不是在往回走。我們想過(guò)可能的方法,比如賽道內(nèi)某一點(diǎn),判斷其到車(chē)位置的矢量和速度矢量的夾角。沒(méi)試過(guò)。
我們用的是另一種只能判斷當(dāng)前圈數(shù)的辦法,對(duì)實(shí)現(xiàn)這點(diǎn)來(lái)說(shuō)很準(zhǔn)確,無(wú)論倒開(kāi),車(chē)體一半穿過(guò)起跑線(xiàn)再返回什么的都能準(zhǔn)確判斷。這利用了PhysicsX的Trigger。
在賽道起跑線(xiàn)位置放置兩個(gè)相隔很近的很薄但很高且與起跑線(xiàn)等長(zhǎng)的長(zhǎng)方體NxActor。該NxActor的形狀屬性為:
BoxDesc.shapeFlags |= NX_TRIGGER_ENABLE; // NxBoxShapeDesc BoxDesc;
這樣任何NxActor碰到了這個(gè)物理都會(huì)觸發(fā)一個(gè)onTrigger過(guò)程。這里只要定義一個(gè)類(lèi)的對(duì)象(如mItemTrigger)為接受觸發(fā)事件,而這個(gè)類(lèi)繼承NxUserTriggerReport,并實(shí)現(xiàn)
virtual void onTrigger(NxShape& triggerShape, NxShape& otherShape, NxTriggerFlag status)方法。再
mScene->setUserTriggerReport(mItemTrigger); // NxScene* mScene;
就可能實(shí)現(xiàn)。
每次觸發(fā)都會(huì)調(diào)用一次onTrigger,在onTrigger中可以判斷是剛進(jìn)入該物體還是離開(kāi)。實(shí)際上對(duì)于車(chē)在兩個(gè)長(zhǎng)方體NxActor中的位
置,總共有七種狀態(tài)(從哪一頭進(jìn)入到該位置,雖然結(jié)果位置一樣,但算為不同狀態(tài),這樣可排除車(chē)輛一半進(jìn)起跑線(xiàn)又返回引起的計(jì)數(shù)錯(cuò)誤)。具體狀態(tài)如下圖:

這樣狀態(tài)間的轉(zhuǎn)換即為:

由于只能檢測(cè)到是哪個(gè)NxActor,而車(chē)輛對(duì)應(yīng)的類(lèi)(這里NxVehicle)是自己封裝的,里面包含一個(gè)NxActor,所以得到哪個(gè)
NxVehicle可以充分利用NxActor中的一個(gè)public成員void*
userData的作用,讓它指向它所在的NxVehicle對(duì)象(this)即可。
CEGUI的實(shí)現(xiàn)
引用Lzx一段:
“利用OGRE引擎和CEGUI進(jìn)行了基本的場(chǎng)景和GUI布局進(jìn)行了編寫(xiě)。
CEGUI讀取2DGUI布局的配置文件,這個(gè)文件是用xml來(lái)編寫(xiě)的。這樣可以將程序編寫(xiě)和UI的設(shè)計(jì)相對(duì)分開(kāi),使得程序設(shè)計(jì)和UI美工設(shè)計(jì)可以更好的分開(kāi)。讀取后在3D場(chǎng)景中進(jìn)行繪制。”
這個(gè)實(shí)際與游戲過(guò)程部分是獨(dú)立的。由于時(shí)間緊迫,我們并沒(méi)進(jìn)行很好的封裝實(shí)現(xiàn),只在一味在寫(xiě)在類(lèi)(class
CuteCarFrameListener : public ExampleFrameListener, public
MouseMotionListener, public MouseListener)中。
這部分具體不是我做的,也沒(méi)去細(xì)究。而且是在網(wǎng)絡(luò)加入失敗后做的,所以并沒(méi)考慮網(wǎng)絡(luò)方面的接口,這里便不詳述了。
另外,小地圖和車(chē)速表的實(shí)現(xiàn)也是這部分內(nèi)容。
小地圖實(shí)現(xiàn)先貼一張賽道圖,在從物理引擎得到車(chē)輛的位置,算出在地圖上的相當(dāng)位置(百分比),映射顯示到小地圖上即可。
車(chē)速表則更簡(jiǎn)單,讀出速度,畫(huà)個(gè)圖就可。
玩家視角Camera的實(shí)現(xiàn)
在車(chē)輛實(shí)現(xiàn)中已有描述,這里只是一種實(shí)現(xiàn)方案。
只創(chuàng)建一個(gè)Camera對(duì)象,注意它不能被多個(gè)SceneNode來(lái)attach,所以轉(zhuǎn)換視角時(shí),要先detatch。這個(gè)當(dāng)時(shí)探索了好些時(shí)間,不過(guò)都是Ogre基本知識(shí),做起來(lái)還是簡(jiǎn)單的,不說(shuō)罷。
提一下,在實(shí)際車(chē)輛行馳中,人的視線(xiàn)跟隨通常會(huì)比車(chē)輛轉(zhuǎn)彎的動(dòng)作稍晚一點(diǎn)點(diǎn),這里就加了一個(gè)緩沖,也就是每次看到的東西都是30幀之前的場(chǎng)景,以實(shí)現(xiàn)更好的效果。這個(gè)通過(guò)一個(gè)鏈表(數(shù)組)就很容易實(shí)現(xiàn)。
另外車(chē)輛左右擺動(dòng)時(shí),人的視角是不會(huì)跟著左右擺的,雖然現(xiàn)實(shí)中可能會(huì)一些,但在游戲中會(huì)給人過(guò)于晃動(dòng)的感覺(jué),這里就把左右擺動(dòng)去掉了,也就是Camera向上向量始終與y軸平時(shí),這通過(guò)一些數(shù)學(xué)運(yùn)算便可,說(shuō)實(shí)話(huà),我是湊出來(lái)了,花好長(zhǎng)時(shí)間。
聲音的實(shí)現(xiàn)
也不是我做的,同學(xué)Lzx做的,引用一段:
“音頻模塊支持3D和非3D的播放模式。考慮到播放時(shí)多次讀音頻文件會(huì)提高音頻模塊的CPU占用率而影響游戲進(jìn)行,所以針對(duì)一個(gè)音頻文件,每次把音頻文件全部讀取到緩存中而不是多次讀取,每次讀取一部分。這是個(gè)用內(nèi)存換CPU效率的選擇。”
聲音的加入,對(duì)原先的程序基本無(wú)影響。因?yàn)槭褂玫腄irectSound和上面這些基本算獨(dú)立,Lzx封裝得也相當(dāng)不錯(cuò)。聲音播放時(shí)自動(dòng)會(huì)開(kāi)線(xiàn)程,對(duì)游戲其它模塊無(wú)任何影響。
有關(guān)網(wǎng)絡(luò)引擎加入失敗
原計(jì)劃網(wǎng)絡(luò)引擎采用RakNet,也封裝了一些東西,也提供了不錯(cuò)的接口。但由于當(dāng)初游戲的整體設(shè)計(jì)不充分,游戲框架設(shè)計(jì)成一個(gè)Ogre作為主線(xiàn)
程,而網(wǎng)絡(luò)部分需要單獨(dú)開(kāi)辟一個(gè)線(xiàn)程來(lái)進(jìn)行數(shù)據(jù)傳輸和處理,如果并在一個(gè)線(xiàn)程里面,會(huì)使線(xiàn)程阻塞,使數(shù)據(jù)傳送相當(dāng)緩慢,甚至不成功,導(dǎo)致游戲的無(wú)法繼續(xù).
由于時(shí)間的關(guān)系,框架更新時(shí)間不足,只好放棄這塊,做成單機(jī)版。
由于網(wǎng)絡(luò)引擎需要額外的線(xiàn)程和緩沖,而OGRE并不提供這種功能。最好的辦法是創(chuàng)建一個(gè)主線(xiàn)程,而把OGRE中循環(huán)作為一個(gè)子線(xiàn)程來(lái)處理。但這樣做,原先我們封裝的系統(tǒng)是不能夠支持,大量改變代碼,時(shí)間已不夠,所以無(wú)奈只好放棄。
在網(wǎng)絡(luò)上花的精力太少,一開(kāi)始不夠重視也是個(gè)原因啊。而事實(shí)上我們對(duì)于網(wǎng)絡(luò)上的編程毫無(wú)經(jīng)驗(yàn)。
貼些游戲截圖,美工還蠻PP








后話(huà)
調(diào)試就用OGRE提供的Log好了,不能一步步進(jìn)行調(diào)試。還有一個(gè)好辦法就是生成.map文件,再根據(jù)windows中出錯(cuò)提示框中通常提示運(yùn)行到
哪個(gè)虛擬地址出錯(cuò),在.map文件中找出代碼的具體出錯(cuò)行。這實(shí)際上對(duì)于已經(jīng)給玩家使用的程序根據(jù)玩家獲得的出錯(cuò)信息找Bug很有幫助