最近在用OGRE+NXOGRE(一个结合AGEIA PHYSX 和OGRE的物理引擎)做赛车游戏,网上查不到什么中文资料,于是转载这篇,希望对自己和他人有所帮助.
游戏主要用到了几个引擎,物理引擎(PhysicsX SDK 2.3.2,即NovedeX的新版本),图形渲染引擎(OGRE 1.2.0,包括人机界面的CEGUI部分),声音引擎(Direct Sound),网络引擎(RakNet,可惜由于时间等原因,加入失败,大大降低游戏可玩性),建模用Maya和3dsMax。
模型的导出
OGRE有自己的3D模型格式(.mesh格式)。导出可以直接出OGRE官方网站下载导出插件。不过导出插件有好些Bug。对于建模方式的不同导出也不同,Maya的导出插件不能导出非封闭的曲面,而3dsMax可以,所以可以通过它导成.mesh。
另外,在物理引擎中要表现精确的碰撞,最好能得到模型的网格结构,即模型的点面信息,而不是用简单的规则形式来包裹。我们的游戏中地图用的是前者,而车辆道具等用的是后者。
但PhysicsX并不支持.mesh。所以我们是通过导出插件导出的.mesh.xml格式文件中读取点面信息(仅此),再写到自定义的二进制文件格式中,为物理引擎使用。
OGRE与PhysicsX之间的耦合
PhysicsX和OGRE场景管理比较类似,都有对应的类。一个可以移动的物体,在OGRE中可以用SceneNode实现,并加入到场景管理中,在PhysicsX中则可以用NxActor对应。NxActor中PhysicsX中的一个物理单位,它可以把一个物体用简单的几个规则几何形状包裹起来,所以的重力、碰撞等物理作用都作用在NxActor中,并产生相应的物理反应。得到相应的物理数据之后,再通过设置SceneNode相应属性,就可以实现逼真的物理效果。
事实上,地图场景也是作为普通的3D物体来实现。
而OGRE与PhysicsX之间的映射并不是对于每一个物体进行映射封装实现。而是把这两部分封装成独立的两个模块,但这两部分中所有的物体都是一一对应的。在主程序中,再把从PhysicsX部分计算出的结果,传递给OGRE部分,进行绘制。这样也为网络的加入提供了好的嵌入点。因为只要服务端进行全部物理运算,再把运算的结果发给客户端OGRE进行渲染。
OGRE中的frameStated(const FrameEvent &evt)作为程序的主线程,在其中调用
m_NxScene->simulate(evt.timeSinceLastFrame); // m_NxScene, instance of (NxScene*)
m_NxScene->flushStream();
m_NxScene->fetchResults(NX_RIGID_BODY_FINISHED);
再把运算的物理结果给OGRE绘制。
这样OGRE与PhysicsX结合在一起,而两者的内部实现是互不影响的,可以独立编程,只要处理好两者物体的一一对应关系。
地图场景的实现
对于OGRE来说有专门的室外地图场景管理。但是由于其高度图很难导入到PhysicsX中,除非通过建模时得到点面信息,但在具体操作中很难做到高度图与网格点面信息的一致。我们还试过在程序初始化时通过OGRE的Ray取得地图上M*N个点的坐标,组成2*M*N个三角形面片,使用到物理引擎中,近似的实现物理地图。但这种实现所有的三角形在XZ平面上的投影都是一样的,面片太少可能不精确,太多又会增加不必要的开销,总的来说不够理想。
PhysicsX中也有专门用于处理地图场景的NxActor,可以在创建NxActor前通过
terrainDesc.heightFieldVerticalAxis = NX_Y; // terrainDesc is a NxTriangleMeshDes
// Default: NX_NOT_HEIGHTFIELD
terrainDesc.heightFieldVerticalExtent = -1000.0f;
进行设置。这样可以大大的提高效率。这本来应该是一个理想的做法。但由于我们建模时模型导出有些误差的原因,会出现某些面片为垂直,物理碰撞的效果在这些地方过于激烈,表现在屏幕上就是车会突然被撞得飞起来很高。我们试了很久都没找到合适的模型导出方法避免这一现象。所以只有所地图场景也作为一个普通的NxActor进行处理,这样,虽然克服不了建模导出时的这个问题,但也不易被玩家发觉。
这样,地图场景就有了实现的方法了:在OGRE中作为普通的.mesh对待,创建SceneNode和Entity;在PhysicsX中,作为普通的NxActor对待,只是用NxCooking进行处理,具体没去细究,可能是为了提高性能。
车辆的实现
车辆的实现是本游戏的重点。
当然也分OGRE和PhysicsX两部分实现。
OGRE部分还包括视角即(Camera),而其他诸如油量,道具之类的与车辆本身无关的这里不做描述。一辆车的主要结构如下:
对于一辆车的建模,这里还是比较粗糙,只是分为车轮和车体,把车轮分离出来主要是实现轮子的转向效果。
由于本游戏的初衷是实现多人对战的网络游戏,只是到最后没能实现网络,改为单机。但所以的封装都是为多人网络游戏准备的。
所有车会由一个类进行统一管理,而这个类用了Singleton设计模式。在其上有另一个封装OGRE场景和车辆的总类。
PhysicsX部分,车体由十几简单的面片进行包裹,而车轮是关键。
车辆通过构造轮子的层次结构实现,车由车体和四个车轮组成,实现运动主要由车轮控制。
所有车辆可控的运动(比如碰撞为不可控)都由车轮带动。这也符合实现的物理。
车轮的实现主要是通过NxWheelShape实现。下面是部分对NxWheelShape的分析。
NxWheelShape属性:
radius:Range: (0,inf) 车轮半径
suspensionTravel:Range: [0,inf),suspension的作用距离
virtual void setLongitudalTireForceFunction (NxTireFunctionDesc tireFunc)=0
设定动力对正向的加速度等的影响
virtual void setLateralTireForceFunction (NxTireFunctionDesc tireFunc)
设定动力对侧向的加速度等的影响,可以实现侧滑之类的
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时,直接设定速度,这种模式不再受motorTorque和brakeTorque等影响
brakeTorque:Range: [0,inf)
刹车力矩
inverseWheelMass:设定动力对加速度等的影响,越大作用产生的效果越强
motorTorque:Range: (-inf,inf)
动力矩,使车前进
steerAngle:Range: (-PI,PI) 车轮的偏向角,以弧度表示
virtual void NxWheeleShape::setSuspension(NxSpringDesc spring) [pure virtual]
与其他物体的联接有关
上面这部分是以前写的总结,虽然不完整,现在也不想再细下去了,到此。
不过有一点要提的是,虽然车轮的摩擦力,弹性系数什么的(restitution,staticFriction, dynamicFriction)可以设置,就像其他基本形状一样,但似乎没什么作用,就跟全为0一般,我试了很久,都是这样,不知什么原因。这样要使车表现出一定的阻力效果,可以通过把刹车力设为一定值实现。实现上可控地去影响轮子运动速度的只有动力和刹车力(setMotorTorque, setBrakeTorque),而影响车轮角度的是(setSteerAngle),另外当然也可以直接设置轮子的前进速度和转动的角速度, NxWheelShape提供了这种接口。不过为了逼真性,最好不要调用这两个接口,因为自己实现物理效果可能有一堆公式转化,而只有在车辆初始化或使用什么道具的时候调用。
要实现侧滑之类的效果,只要调用setLateralTireForceFunction即可。
另外,用现成的NxWheelShape还是有缺点的,就是很难调手感,可能也较难实现复杂的效果。我调了很久,最后只能将就了。
PhysicsX不需视角处理。
PhysicsX类之间的结构与OGRE部分基本上一模一样。
赛道圈数的判断实现
不知道像极品飞车之类的游戏赛道圈数是怎么实现的,应该与我们的不同,因为它可以每时每刻的判断车辆是不是在往回走。我们想过可能的方法,比如赛道内某一点,判断其到车位置的矢量和速度矢量的夹角。没试过。
我们用的是另一种只能判断当前圈数的办法,对实现这点来说很准确,无论倒开,车体一半穿过起跑线再返回什么的都能准确判断。这利用了PhysicsX的Trigger。
在赛道起跑线位置放置两个相隔很近的很薄但很高且与起跑线等长的长方体NxActor。该NxActor的形状属性为:
BoxDesc.shapeFlags |= NX_TRIGGER_ENABLE; // NxBoxShapeDesc BoxDesc;
这样任何NxActor碰到了这个物理都会触发一个onTrigger过程。这里只要定义一个类的对象(如mItemTrigger)为接受触发事件,而这个类继承NxUserTriggerReport,并实现
virtual void onTrigger(NxShape& triggerShape, NxShape& otherShape, NxTriggerFlag status)方法。再
mScene->setUserTriggerReport(mItemTrigger); // NxScene* mScene;
就可能实现。
每次触发都会调用一次onTrigger,在onTrigger中可以判断是刚进入该物体还是离开。实际上对于车在两个长方体NxActor中的位置,总共有七种状态(从哪一头进入到该位置,虽然结果位置一样,但算为不同状态,这样可排除车辆一半进起跑线又返回引起的计数错误)。具体状态如下图:
这样状态间的转换即为:
由于只能检测到是哪个NxActor,而车辆对应的类(这里NxVehicle)是自己封装的,里面包含一个NxActor,所以得到哪个 NxVehicle可以充分利用NxActor中的一个public成员void* userData的作用,让它指向它所在的NxVehicle对象(this)即可。
CEGUI的实现
引用Lzx一段:
“利用OGRE引擎和CEGUI进行了基本的场景和GUI布局进行了编写。
CEGUI读取2DGUI布局的配置文件,这个文件是用xml来编写的。这样可以将程序编写和UI的设计相对分开,使得程序设计和UI美工设计可以更好的分开。读取后在3D场景中进行绘制。”
这个实际与游戏过程部分是独立的。由于时间紧迫,我们并没进行很好的封装实现,只在一味在写在类(class CuteCarFrameListener : public ExampleFrameListener, public MouseMotionListener, public MouseListener)中。
这部分具体不是我做的,也没去细究。而且是在网络加入失败后做的,所以并没考虑网络方面的接口,这里便不详述了。
另外,小地图和车速表的实现也是这部分内容。
小地图实现先贴一张赛道图,在从物理引擎得到车辆的位置,算出在地图上的相当位置(百分比),映射显示到小地图上即可。
车速表则更简单,读出速度,画个图就可。
玩家视角Camera的实现
在车辆实现中已有描述,这里只是一种实现方案。
只创建一个Camera对象,注意它不能被多个SceneNode来attach,所以转换视角时,要先detatch。这个当时探索了好些时间,不过都是Ogre基本知识,做起来还是简单的,不说罢。
提一下,在实际车辆行驰中,人的视线跟随通常会比车辆转弯的动作稍晚一点点,这里就加了一个缓冲,也就是每次看到的东西都是30帧之前的场景,以实现更好的效果。这个通过一个链表(数组)就很容易实现。
另外车辆左右摆动时,人的视角是不会跟着左右摆的,虽然现实中可能会一些,但在游戏中会给人过于晃动的感觉,这里就把左右摆动去掉了,也就是Camera向上向量始终与y轴平时,这通过一些数学运算便可,说实话,我是凑出来了,花好长时间。
声音的实现
也不是我做的,同学Lzx做的,引用一段:
“音频模块支持3D和非3D的播放模式。考虑到播放时多次读音频文件会提高音频模块的CPU占用率而影响游戏进行,所以针对一个音频文件,每次把音频文件全部读取到缓存中而不是多次读取,每次读取一部分。这是个用内存换CPU效率的选择。”
声音的加入,对原先的程序基本无影响。因为使用的DirectSound和上面这些基本算独立,Lzx封装得也相当不错。声音播放时自动会开线程,对游戏其它模块无任何影响。
有关网络引擎加入失败
原计划网络引擎采用RakNet,也封装了一些东西,也提供了不错的接口。但由于当初游戏的整体设计不充分,游戏框架设计成一个Ogre作为主线程,而网络部分需要单独开辟一个线程来进行数据传输和处理,如果并在一个线程里面,会使线程阻塞,使数据传送相当缓慢,甚至不成功,导致游戏的无法继续. 由于时间的关系,框架更新时间不足,只好放弃这块,做成单机版。
由于网络引擎需要额外的线程和缓冲,而OGRE并不提供这种功能。最好的办法是创建一个主线程,而把OGRE中循环作为一个子线程来处理。但这样做,原先我们封装的系统是不能够支持,大量改变代码,时间已不够,所以无奈只好放弃。
在网络上花的精力太少,一开始不够重视也是个原因啊。而事实上我们对于网络上的编程毫无经验。
贴些游戏截图,美工还蛮PP
后话
调试就用OGRE提供的Log好了,不能一步步进行调试。还有一个好办法就是生成.map文件,再根据windows中出错提示框中通常提示运行到哪个虚拟地址出错,在.map文件中找出代码的具体出错行。这实际上对于已经给玩家使用的程序根据玩家获得的出错信息找Bug很有帮助