# 物理的使用指南 ## 使用准则与免责声明 ### 使用准则 - 仅限第三方用户生成内容(UGC)使用 - 只有创作者创建的自定义实体才能与物理刚体绑定,原版实体无法与PhysX创建的刚体绑定 - 物理身件可以与其他游戏物件(包括原生游戏物件)产生互动,但是原生游戏物件与其他原生游戏物件之间的互动方式及物理碰撞不会被改变 - 当两个碰撞同时发生时,核心游戏的物理引擎具有最高优先级 只有满足上述条件,您的模组才能符合要求进行上架 ### 免责声明 物理相关api与特性,可能会在将来调整并且不向前兼容。请随时做好调整的准备 但是,每次调整都会做出充分的预告和调整内容公告,方便你对您的模组进行最新内容的兼容。 ## 准备工作 由于我们的物理引擎是用的 NVIDIA PhysX,您可以先前往NVIDIA官网下载PhysX Visual Debugger,方便后续调试。 [https://developer.nvidia.com/physx-visual-debugger](https://developer.nvidia.com/physx-visual-debugger) ![img.png](./picture/physx/img_2.png) ## 全局配置 在使用物理引擎之前,您需要在模组的 behavior_packs 行为包中新增 physx_setting.json 全局配置文件 ![img.png](./picture/physx/img.png) 文件里面需要设置成 ```json { "enable": true } ``` 保存后,您的模组可以正常使用 PhysX 物理引擎了。 ## 开始创作 现在我会通过demo,来向你介绍physx能做的内容 ### 创建自定义刚体 使用物理内容的之前,您首先需要创建一个新的物理刚体,物理刚体**必须**绑定在`自定义实体`上,且创建刚体的方法只能在 `AddEntityServerEvent` 事件中使用 ```python # 创建一个自定义刚体 comp = serverApi.GetEngineCompFactory().CreatePhysx(entityId) comp.CreatePxActor() ``` 现在你已经为这个 entityId 的实体添加了一个物理刚体 > 注意: 刚体创建数量不得大于 8192, 虽然Physx限制是31767,但是我们需要预留一些刚体阈值给到其他实体和客户端内容 > 注意:必须要在 AddEntityServerEvent 事件中创建刚体,否则会报错 > 注意:实体必须绑定在 自定义实体上,不得绑定在原版实体上 ### 添加包围盒 目前这个刚体暂时没有任何功能和属性,为了让它能够正常工作,我们需要给刚体添加一些属性 首先需要添加包围盒,包围盒是刚体的基本属性,你只有添加了包围盒,才能让刚体与世界实体进行交互 添加包围盒的方法是 AddBoxGeometry ,参数方法如下 ```python # localTransform tuple(float,float,float) 盒子中心相对实体原点的偏移 # halfX float 表示盒子长度的一半 # halfY float 表示盒子高度的一半 # halfZ float 表示盒子宽度的一半 # staticFriction float 静摩擦系数 # dynamicFriction float 动摩擦系数,默认为e 注意:此摩擦力设置为0,并不代表刚体与原生游戏交互也是 0 摩擦力 # restitution float 弹性恢复系数,默认为0 # eventMask int PxEventMask枚举,用于监听碰撞事件,默认为PxRigidBodyFlag.Null,即不需要碰撞事件 # userData None或str 可记录自定义数据,长度不超过20,默认为None comp.AddBoxGeometry((0, 0.9, 0), 0.3, 0.9, 0.3, 0.05, 0.05, 0, PxEventMask.Server | PxEventMask.Found, None) ``` PxEventMask 枚举目前有六个 - PxEventMask.Server 服务端事件 - PxEventMask.Client 客户端事件 - PxEventMask.Lost 结束碰撞事件 - PxEventMask.Found 开始碰撞事件 - PxEventMask.Found_Detail 碰撞事件的详细信息 - PxEventMask.Null 该刚体不附加任何事件 此时你已经成功为你的刚体创建好了包围盒 当你启动游戏时,如果你此时提前打开了 PhysX Visual Debugger,那么刚体的包围盒就会显示出来了 ![img.png](./picture/physx/img_3.png) 红色代表世界的原生方块、绿色和黄色代表刚体的包围盒 ![img.png](./picture/physx/img_4.png) ### 添加约束和运动 如果你想给刚体添加一些约束,比如刚体的运动锁定某个轴体,您需要添加动态约束 ```python # 下面的方法是规定此刚体只会沿着 X Y Z 轴运动 comp.SetRigidDynamicLockFlags(PxRigidDynamicLockFlag.eLOCK_ANGULAR_X | PxRigidDynamicLockFlag.eLOCK_ANGULAR_Y | PxRigidDynamicLockFlag.eLOCK_ANGULAR_Z) ``` PxRigidDynamicLockFlag 枚举目前有六个 - PxRigidDynamicLockFlag.eLOCK_ANGULAR_X 锁定沿着 X 轴运动 - PxRigidDynamicLockFlag.eLOCK_ANGULAR_Y 锁定沿着 Y 轴运动 - PxRigidDynamicLockFlag.eLOCK_ANGULAR_Z 锁定沿着 Z 轴运动 - PxRigidDynamicLockFlag.eLOCK_LINEAR_X 锁定沿着 X 轴旋转 - PxRigidDynamicLockFlag.eLOCK_LINEAR_Y 锁定沿着 Y 轴旋转 - PxRigidDynamicLockFlag.eLOCK_LINEAR_Z 锁定沿着 Z 轴旋转 如果你希望您的刚体具有运动的属性,你需要将刚体设置为运动学刚体 那什么叫做有运动的属性? 比如你想要手动控制刚体的运动,那么你需要将刚体设置为运动学刚体 包括做成刚体风车、刚体传送带等行为 ```python # 目前PxRigdiBodyFlag 仅支持PxRigidBodyFlag.eKINEMATIC comp.SetRigidBodyFlag(PxRigidBodyFlag.eKINEMATIC, True) ``` ### AI控制 利用实体创建刚体,您需要对AI进行调整 否则在客户端会出现悬空的情况 ```python comp = serverApi.GetEngineCompFactory().CreateControlAi(entityId) comp.SetBlockControlAi(False) ``` ### 做一个简单的风车 对此我们可以通过上述内容做一个简单的风车,我们计划是配合方块调色板 + 物理引擎来做一个简单的风车 机械动力的风车是一个比较不错的参考对象 我们仿照机械动力的风车,来做一个中国版物理引擎的风车 首先根据上面介绍的内容,我们依次创建刚体,设置包围盒 但是原版Java版机械动力的风车,可以自由组合方块,所以我们需要一个刚体对应一个方块。目前规划是创建一个长度5格的刚体 可以做一个for循环 ```python for i in range(2): for j in range(-2,3): comp.AddBoxGeometry((j, 0.5 + i, 0), 0.5, 0.5, 0.5, 0.05, 0.05, 0, PxEventMask.Server | PxEventMask.Client | PxEventMask.Found | PxEventMask.Found_Detail | PxEventMask.Lost, '%s,%s,0' % (j, i)) ``` 此时就会创建一个总长度 5 格的刚体包围盒,并且每个刚体对应一个方块 因为这个刚体会有旋转的效果,所以我们需要给刚体添加一个运动属性 ```python # 目前PxRigdiBodyFlag 仅支持PxRigidBodyFlag.eKINEMATIC comp.SetRigidBodyFlag(PxRigidBodyFlag.eKINEMATIC, True) ``` 因为刚体旋转是通过传入一个四元数并对四元数调整来达到目标的 所以我们需要创建一个四元数组 ```python # 这里建议用一个成员变量来做存储,方便删除实体后对数据进行调整和删除 self.mSkeletons[entityId] = Quaternion(0, 0, 0, 1) ``` 因为旋转是一个连续的过程,所以我们需要重写Update方法,以达到每tick刷新我们的旋转数据 ```python # 这里我们对 self.mSkeletons 进行遍历,获取刚刚存储好的四元数,修改的同时传入刚体运动方法 for entityId, v in self.mSkeletons.iteritems(): # 旋转 q = v * Quaternion.AngleAxis(1, Vector3.Up()) # 修改变量值 self.mSkeletons[entityId] = q # 设置为运动学目标 comp = serverApi.GetEngineCompFactory().CreatePhysx(entityId) # 运动 comp.SetKinematicTarget(None, q.ToTuple()) ``` 此时刚体就会每tick旋转角度 但是目前做的内容仅仅只是对刚体进行了旋转,客户端没有表现,我们还需要用方块调色板对客户端进行调试 首先创建一个调色板 ```python dataDict = {'extra': {}, 'void': False, 'actor': {}, 'volume': (1, 5, 2), 'common': {('minecraft:grass_block', 0): [4, 9], ('minecraft:glass', 0): [1, 6], ('minecraft:quartz_block', 0): [3, 8], ('minecraft:white_wool', 0): [0, 5], ('minecraft:oak_planks', 0): [2, 7]}, 'eliminateAir': True} comp = compFactory.CreateBlock(levelId) palette = comp.GetBlankBlockPalette() palette.DeserializeBlockPalette(dataDict) blockGeometryComp = compFactory.CreateBlockGeometry(levelId) geometryName = blockGeometryComp.CombineBlockPaletteToGeometry(palette,"my_block_geometry") ``` 然后需要将调色板绑定在实体上,但是绑定刚体是欧拉角,也就是说我们还需要将四元数转成欧拉角 好在,ModSDK提供了这个简单的方法,物理引擎也有获取客户端刚体的四元数数据 ```python actorRenderComp = compFactory.CreateActorRender(entityId) comp = compFactory.CreatePhysx(entityId) q = Quaternion(comp.GetQuaternion()) # AddActorBlockGeometry 在 3.7 新增了一个参数, 会控制先旋转后偏移,还是先偏移后旋转。默认是False # 为了做风车,我们设置为 True,也就是先旋转再偏移 actorRenderComp.AddActorBlockGeometry(geometryName, (-2, 0, 0), q.EulerAngles().ToTuple(), True) self.mSkeletons.add(entityId) ``` 此时刚体的方块调色板就已经绑定在实体上了,但是这个我们查看 PhysX Visual Debugger 时,发现刚体变动了,但是调色板方块没有变化 所以我们也要对客户端表现进行调整 同样需要重写 Update 方法,每tick修改方块调色板的旋转角度 ```python def Update(self): for entityId in self.mSkeletons: comp = compFactory.CreatePhysx(entityId) q = comp.GetQuaternion() if q: q = Quaternion(q) actorRenderComp = compFactory.CreateActorRender(entityId) actorRenderComp.SetActorBlockGeometryRotation("my_block_geometry", q.EulerAngles().ToTuple()) ``` 当全部完成后,就可以做到风车围绕着一个自定义实体进行旋转,然后推动实体进行移动了 并且你也可以站在这个刚体上,但是站在刚体上,刚体并不会带着玩家一起移动,开发者们可以自行思考解决办法来处理这个问题! ![img_5.png](./picture/physx/img_5.png) ### 思考 虽然我们现在是在用 Update 的方式去动态的调整刚体旋转和外观旋转 但是进入游戏后会发现,用此方法来旋转会有一点的顿挫感,虽然影响不是很大,但如果你需要追求细致,可能还不算太完美 或许还可以通过 `GameRenderTickEvent` 来调整,但是这个方法笔者并没有使用过,所以需要开发者们自行思考研究