〖Array王锐大神力作〗osg与PhysX结合系列内容——角色运动控制
运动学角色体(CCT)
对于第一人称的运动类和射击类游戏来说,主角的控制和动画往往是整个系统效果表现的重点所在。我们能想到的角色控制需求主要包括:前进,后退,左转,右转,下蹲,跳跃,攀爬,举枪射击,以及在这些运动的过程中随时可以和地形或者其它静态碰撞体之间匹配。
角色控制方法,也就是Character Controller(CCT),可以遵循运动学(kinematic)的原理,直接使用位移的输入来进行控制;也可以遵循动力学(dynamic)原理,使用速度的输入来进行控制,或者使用外部力的输入来做控制。
从控制理论专业的角度来说,前者属于一阶控制系统(first order control),后者属于二阶或者三阶控制系统。至于这几类系统在输入输出,稳定性和时间响应上的区别,并不在本文的讨论范围之内。
不过,二阶或者更高阶系统往往会面临以下几个核心问题,这也是PhysX采用一阶系统(kinematic系统)的主要原因:
- 只能使用离散碰撞检测(discrete collision detection)的方式,因此当角色运动过快时,很容易穿透其它模型体表面,进入物体内部,这个问题被称作隧道效应(tunneling effect)。
- 物体在贴近墙角或者其它复杂的角落并继续向着墙前进时,会因为离散碰撞检测的原因发生剧烈的抖动,这对于用户的体验是非常差的。
- 采用速度(二阶)或者力(三阶)驱动的方式,无法精确地直接控制物体位置,因此也就无法预测角色实际所处的位置,给后续操作带来很多麻烦。
- 当角色站在一个斜坡上静止时,它是不应该滑动的,也就是说斜坡的摩擦力此时为无限大。而角色在同一斜坡上向上移动时,它的运动是不应该存在任何阻碍的,也就是说摩擦力此时为0。对于一个动力学系统来说,这是互相矛盾的,很难合理处理此时的需求。
- 当物体(角色)撞击到另一个物体时,它要如何回弹出去,这是通过物体的恢复系数决定的。对于角色控制来说,它是绝对不应该被任何物体或者地面反弹回去的。但是从模拟真实物理环境的角度考虑,再小的恢复系数都会存在一个回弹速度。对于速度或者力驱动的控制系统而言,这依然是矛盾的。
- 角色站立不动或者朝一个方向运动的时候,肯定不能存在额外的旋转。对于动力学系统来说,这种自由度的约束问题是很难妥善解决的。
传统的游戏和应用中并不一定要用物理引擎来驱动角色的控制系统;不过就算是自己去实现这类系统,也必然会遇到角色和地形/场景物体的交集检测问题。而这类问题的解决方案本身也是物理引擎的一部分,因此交给物理引擎去完成是非常合适的。
角色控制在PhysX中属于扩展功能,因此需要使用PxInitExtensions()进行初始化,并且在工程中链接外部依赖库PhysX3CharacterKinematic_x64.lib。
角色的创建和参数设置
创建可控制的角色之前,需要首先根据当前场景创建一个PxControllerManager对象,如果已有PxScene类型的对象_scene,则有:
- PxControllerManager* manager = PxCreateControllerManager(*_scene);
之后我们可以创建一个或者多个角色对象,但是需要预先设置这个角色的描述参数,包括它的碰撞体形状,初始位置,爬坡能力(可以爬上多少斜度的斜坡),跳跃高度等。角色的碰撞体形状只有两种选择,即Box(立方体)和Capsule(胶囊体)。后者更符合一个人体角色的构建需要,使用胶囊体类型的预定义参数来创建角色的过程如下所示:
- PxCapsuleControllerDesc desc;
- desc.radius = radius; // 胶囊体的半径值
- desc.height = height; // 胶囊体的高度值
- ……
- PxController* controller = manager->createController(desc);
如果后续需要释放这个角色对象,则执行controller的release()函数即可。
无论是使用PxCapsuleControllerDesc(胶囊体)还是PxBoxControllerDesc(立方体)来创建新的场景角色,它们都有一些共同的角色参数需要设置。这些参数决定了这个角色在场景中漫游时的各种特性,例如如何攀爬斜坡,如何上下楼梯,如何跳跃或者攀爬等等。下面列举一些常用的角色参数:
- PxCapsuleControllerDesc desc;
- desc.position = <角色的初始位置>;
- desc.upDirection = PxVec3(0.0f, 0.0f, 1.0f); // 注意OSG认为Z+是向上方向
- desc.density = 10.0f; // 角色的密度
- desc.slopeLimit = 0.707; // 角色爬坡的时候,能够攀登的最大角度cosine值
- desc.maxJumpHeight = 0.0f; // 设置角色跳跃的最大高度,因此他可能跃过不可见的障碍墙
- desc.invisibleWallHeight = 0.0f; // 如果设置了地形某些材质是无法行走的,那么这个地形周围
// 可以自动创建不可见的障碍墙,这里设置墙体的高度 - desc.stepOffset = 0.5f; // 设置角色能够直接迈过的台阶高度
- desc.material = <物理材质>;
角色的交互控制
创建了PxController对象之后,我们可以使用move()函数控制这个角色对象运动,它的基本用法如下:
- PxController* controller = …
- controller->move(offset, minDistance, elapsedTime, PxControllerFilters(), NULL);
这里的offset(类型为PxVec3)是角色运动的三维距离值,注意用户需要自己将重力向量传递进来,这样角色在脚下没有地面的时候会自然下落。例如我们期望向X方向运动0.1距离,则offset应当设置为
- offset = PxVec3(0.1f, 0.0f, 0.0f) + PxVec3(0.0f, 0.0f, -9.81f * scale);
- // 这里的scale是一个用户自己决定的变量,用来控制角色下落的距离
第二个参数minDistance用来决定角色停止运动的最小阈值,当角色实际运动的距离和offset小于这个值的时候,角色将认为自己已经到位并自动停止。一般情况下可以设置成一个较小的数值,例如0.001。
第三个参数elapsedTime设置了角色运动所需的时间。
后两个参数我们暂时采用了默认值。其中PxControllerFilters()是一个默认的回调,用户也可以使用自己的回调函数来控制角色和具体物体的碰撞检测逻辑;最后一个参数可以设置为从manager创建的一个PxObstacleContext对象,并通过它来定义一些障碍物对象,这些障碍物对于其它对象来说是不可见的。
角色控制还可能存在其它一些操作方式,典型的例如:走路/跑步,原地跳起/向前跳跃,下蹲,蹲下来行走,等等。一些动作游戏(例如《刺客信条》)可能会有更加复杂的控制手段。
走路,跑步,跳跃等动作,都可以通过move()函数来设置具体的速度和运动方向,遵循简单的经典物理公式即可。但是下蹲和下蹲行走可能需要实时改变角色的碰撞体形状,以便适配新的(临时)角色高度。
这里建议使用controller->resize()函数来实现下蹲时角色高度的修改,注意我们也可以使用controller->setHeight()。这两者的主要区别是前者的角色脚底可以自动适配到地面上,而后者会暂时悬空,导致下一次执行move()的时候会有一个下坠,如下图所示。
上下楼梯
场景中的角色控制经常会遇到一个难题:就是地面存在一个小的台阶时,会阻挡住角色的运动。如果场景地形比较复杂,那么很容易因此造成角色被卡死在某个角落,无法退出。并且上下楼梯台阶也是非常常见的场景角色控制需求。如果为了避免这种阻挡问题而要求物理场景中去掉所有的台阶,那么无论从感观角度和美术处理的角度都是不妥当的。
PhysX自动帮我们处理了这种情景,只要在创建PxController之前通过desc.stepOffset设置一个相对合理的台阶阈值,那么角色在被小于阈值的台阶阻挡时会自动越过并站立在新的台阶上。而大于阈值的台阶,系统会默认它本身已经是不可忽视的障碍物了,因此会阻挡角色的运动。
此外,如果使用了PxCapsuleControllerDesc,那么角色本身的碰撞体是胶囊体类型,它的头顶和脚面部分都可以理解为球面,因此角色可以自然地“滑”跃过一些小型的台阶或者障碍物。此时不设置desc.stepOffset也是可以的。
设置空气墙
当角色在场景中运动时,有可能会到达地形的边界,此时如果继续向前走的话,会导致角色一直向下掉落到“无限空间”里。这样的体验显然是不合适的。
如果在场景的边缘增加一些实体的高墙,那么当然可以避免角色掉落的问题,但是从视觉上不够美观。并且这些物理高墙不仅仅会阻挡角色,还会阻挡其它动态物体的运动。对于有些游戏逻辑而言,这会带来一些额外的问题。一个典型的场景是足球游戏中,球可以被踢出场外,但是球员肯定是被限制在场地中运动的,不能跑到观众席去。
此时,前文中提到的PxObstacleContext就可以发挥作用了。它可以用来构建“只作用于角色本身的不可见障碍物”,也就是一些动作游戏中常说的,可以阻挡玩家前进的“空气墙”。使用PxObstacleContext构建一堵空气墙的基本代码如下(这里假设PxControllerManager对象已经创建):
- physx::PxBoxObstacle airWall; // 创建一个宽高均为200的立方体,位置在(0, -100, 0)
- airWall.mPos = osgPhysics::toPxVec3d(osg::Vec3d(0.0, -100.0, 0.0));
- airWall.mHalfExtents = osgPhysics::toPxVec3(osg::Vec3(100.0f, 0.1f, 100.0f));
- physx::PxObstacleContext* context = manager->createObstacleContext();
- context->addObstacle(airWall); // 之后在move()函数中使用该context作为参数即可
构建测试场景并运行
osgPhysX中提供了一个character_controller例子,它演示了物理场景中角色运动的常见操作。启动该程序之后,系统将加载默认的lz.osg场景作为地形,并生成一个胶囊体在场景中心。
我们可以使用键盘上的方向键组合来控制角色沿着摄像机的方向前后左右运动。运动过程中物体会自动贴合地形和越过小型障碍物(台阶),或者被较高的障碍物阻挡。按键盘的“1”“2”可以切换自由漫游模式和角色过肩视角模式,以便更好地理解角色控制的意义。
当然这个例子还是略有不足的,我们并没有真的看到角色,只是一个胶囊体而已。这并不是物理引擎的问题,不过我们打算在下一章节好好解决一下这个重要的观感问题。
我们在osgPhysics中为了构建角色对象,首先要创建一个osgPhysics::CharacterController对象,并设置它使用胶囊体角色(通过createCapsule()方法)或者立方体角色(通过createBox()方法),同时使用ControllerData结构体来设置具体的角色参数。
这个CharacterController对象可以被设置到一个osgPhysics::UpdateCharacterCallback回调。而这个回调被设置到一个osg::MatrixTransform节点后,可以实时改变这个节点的空间位置,从而实现角色几何体的显示和动态位姿变换。
而角色的运动控制可以通过一个单独的osgGA::GUIEventHandler来完成,当用户使用键盘方向键输入时,程序中自动计算相机的前进方向并基于它来计算角色的前、后、左、右运动位移。这个位移值被传递到CharacterController对象中,并且在当前帧的数据更新时自动应用到底层的PhysX对象。
读者可以进一步自己实现一些更复杂的交互操作,例如跳起,快速奔跑等。如果角色归于静止,那么必须调用CharacterController的move()函数并且传入(0, 0, 0),否则角色将继续沿着上一次设置的位移值运动。
具体的示例代码请参看osgPhysX\\tests\\character_controller_test.cpp
暂无评论内容