返回

解决 LWJGL 2.9.1 物理模拟穿透问题 | 碰撞检测优化

java

解决 LWJGL 2.9.1 中的物理应用错误

在 LWJGL 2.9.1 中处理物理模拟时,经常出现的问题是玩家或摄像机意外地穿透游戏环境或跌入虚空。 这个问题通常源于物理计算和碰撞检测之间缺少准确的同步机制。 结合给出的示例代码,下文将剖析问题的潜在原因并给出相应解决方案。

问题分析

  1. 不准确的碰撞检测:

    • 提供的代码使用了一个基于每帧的碰撞检测机制,但没有考虑到潜在的穿透情况。 例如,如果物体在帧之间移动过快,有可能直接跳过障碍物,没有机会触发碰撞检测。
    • 代码中的CollisionHandler可能缺少足够精确的碰撞处理逻辑,尤其是在垂直方向。 重力、跳跃以及速度这些参数可能会累积误差,导致错误地判断与地面相交。
    • 对于CollisionHandler.isGroundBelowCamera()方法的具体实现细节并不清楚,这使得准确分析成为问题的一部分。 可能由于块的生成或者地形数据的加载出现同步的问题,无法有效检测到地板。
  2. 块加载同步问题:

    • 地块(chunks)是异步生成的,Chunk.generateChunkAtCameraPosition和随后的其他块生成都在独立线程中运行。 主游戏循环里的物理模拟(包括重力和碰撞检测)可能发生在异步生成的块网格加载和渲染之前。
    • 即使使用了 Chunk.isChunkAtCameraReady 标识,也不能确保所需块及其几何体在计算和模拟物理之前已经被加载。 在首次运行时候,它尤其存在问题。
  3. 简单的重力实现:

    • 重力的处理比较简化,依赖简单的速度累加。这在多数情况下足够了,但并没有处理诸如 “碰撞停止重力”和 “准确的垂直定位”等复杂的物理模拟的案例,这就可能使得玩家穿透地形的边缘区域,或者错误地“停留在空中”。

解决方案

方案一: 完善碰撞检测与处理

需要确保碰撞检测是可靠且考虑到物体大小。这里可以做几件事:

  • 连续碰撞检测: 使用诸如扫描之类的技巧来检测运动物体和障碍物之间在两个帧之间可能发生的碰撞。在每一次帧的移动计算前,检测到可能即将发生的碰撞,确保在玩家移动时准确地考虑与附近的几何体的相互作用。
    // CollisionHandler 类
     public static boolean checkCollision(Camera camera, List<ChunkMesh> chunks) {
           Vector3f futurePosition = new Vector3f(camera.getPosition());
    
           // Check forward direction
           float xOffsetForward =  camera.getDirection().x;
            float yOffsetForward = camera.getDirection().y;
             float zOffsetForward= camera.getDirection().z;
    
              float checkIncrement = 0.5f; 
    
             for(float t=0.0f;t<checkIncrement;t +=0.1f){
                 futurePosition.x =  camera.getPosition().x + (xOffsetForward * t);
                  futurePosition.y =   camera.getPosition().y +( yOffsetForward* t)  ;
                     futurePosition.z =   camera.getPosition().z + (zOffsetForward *t) ;
    
                        if(blockCollisionAt(futurePosition, chunks) ) {
    
                     return true; 
                   }
    
            }
    
           //If is moving up
            if(camera.velocityY >0.0f){
                if(blockCollisionAt(futurePosition.x , futurePosition.y  +camera.velocityY,  futurePosition.z, chunks)  ){
    
                   return true;
                 }
            }
    
           return  false;
         }
    
      private static boolean blockCollisionAt(Vector3f futurePosition,List<ChunkMesh> chunks ) {
    	  return blockCollisionAt( futurePosition.x,  futurePosition.y,  futurePosition.z, chunks);
      }
    
     private static boolean blockCollisionAt(float x , float y, float z,List<ChunkMesh> chunks) {
    
        	int blockX =  (int) x;
        	int blockY= (int) y;
        	int blockZ =(int)  z;
    
    
    	    if (blockX < 0 ||  blockY <0  ||  blockZ <0){
    		    return false; //Out of bounds
    	    }
    
    
            for(ChunkMesh  chunk : chunks) {
    
                if(chunk!= null && chunk.chunck != null) {
    
    	           	  //Local coordenates 
    	        	   Vector3f chunkOrigin = chunk.chunck.getOrigin();	   
    
    
    		            int localX= blockX - (int)chunkOrigin.x ;
    			          int localY =  blockY;
    				      int localZ= blockZ- (int)chunkOrigin.z;
    
    
    
    
    	          if (localX>=0 && localX <32  && localY>=0 &&  localZ >= 0 && localZ <32) {
    
    
    	                	if(chunk.chunck.getBlockAt(localX, localY,localZ) != null){
    
                                     return true; // Found collision
    
    		            }
                         }	      	
                 }
    
            }
    
        	return false;//No colision
        }
    
    
       public static void handleCollision(Camera camera, Vector3f oldPosition, List<ChunkMesh> chunks) {
    
        if (checkCollision(camera, chunks)) {
                camera.setPosition(oldPosition); 
            }
    
     }
    
    
    操作步骤:
  1. CollisionHandler类里, 更新checkCollisionblockCollisionAt的方法, 来执行提前检测,并考虑相交。
  2. 确保使用老的位置(oldPosition), 如果发生了碰撞,就使用该位置恢复camera
  3. Camera.move() 里应用CollisionHandler.handleCollision 的处理。
  • 准确的着陆检查:applyGravity 中确保相机没有直接穿过地面。 将位置强制到块之上。检查下方的方块位置是否存在, 并把摄像机强制上移,使其精确停留在地面。同时可以加入一个小的向上推动力(jumpStrength的一小部分),让着陆看起来平滑,像跳到地面一样。
 // Camera class
public void applyGravity() {
          if (!onGround) {
              velocityY += gravity; // Apply gravity
               if (velocityY < 0) {
                    if(CollisionHandler.checkCollision(this, MainGameLoop.chunks) ){
                             position.y = (float) Math.ceil(position.y);
                             onGround= true;
                            velocityY=0;
                            doubleJumpAvailable = true; 
                    } else {
                              position.y += velocityY;  // Actualize y when no colision
                    }
                   
                }else {
                 position.y += velocityY; 
                }
                
              
          }
      }

   //  in Camera class, move method, you need update onGround variable and others things on isCollided detection
操作步骤:
1. 修改`applyGravity`的方法,当相机向下运动(`velocityY < 0`)的时候,就尝试做一个提前碰撞的检测。
2. 在确认着陆的时候强制相机的`y`坐标到一个有效的位置,把`onGround`的值设为真,重置速度,并把二次跳跃激活。

方案二: 同步地块加载

可以尝试使区块加载的同步。这减少了“缺失块”导致的碰撞问题。 确保只有加载好的地块参与碰撞检测。

  • 加入加载队列:CopyOnWriteArrayList<ChunkMesh> toLoadChunks代替原始的chunks静态成员。 这将会保留待渲染的块, 直到全部完成渲染的块集合。 这可以在主循环里处理, 使渲染和碰撞更可预测。
    // MainGameLoop
        public static List<ChunkMesh> toLoadChunks = new CopyOnWriteArrayList<>(); //Chunks pending to load and render
    public static  List<Entity> entities = new ArrayList<>();//Rendered chunks.
    
    • 把所有的MainGameLoop.chunks替换成MainGameLoop.toLoadChunks
     // Chunk class.
        MainGameLoop.toLoadChunks.add(mesh);
    
    //   CollisionHandler class
          for(ChunkMesh  chunk : chunks) { //change chunks ->  toLoadChunks
    
         //In Camera move() method
          boolean collided = CollisionHandler.checkCollision(this, MainGameLoop.toLoadChunks);
    
          //In Camera Class ApplyGravity
          if(CollisionHandler.checkCollision(this, MainGameLoop.toLoadChunks)){// Change from chunks  to   toLoadChunks 
    
     //MainGameLoop , index loading
    
           if (index < toLoadChunks.size()) {
    
              }
    
    
     for (int i = 0; i < entities.size(); i++) {
    
          }
    
    
操作步骤:

1.  将所有涉及 chunk 的 list (chunks) 替换为 `toLoadChunks`。 比如`CollisionHandler`以及其他的class2.  在 `MainGameLoop`的主循环里处理这些元素, 就像当前例子展示的那样。 在把新的`Entity`加入列表前, 我们等待他们加载完成, 这将确保碰撞发生在地块完成渲染之后。
  
3. 更新 主循环内的 entity渲染循环以处理 `entities`列表而不是chunks列表, 使得渲染同步, 也能进一步确保, 所有需要的方块在它们进行渲染时均加载完成。

####  方案三: 更精准的垂直方向运动处理

尝试一个比较精准的速度,以及加速度方式处理垂直方向运动:

   * **更准确的 `applyGravity` :** 
        通过重构  `applyGravity`,我们可以计算在给定的速度下的着陆位置,并在相机撞击地面时停止下落, 以及调整相机位置在地面之上的方案, 不会让相机有太大的下落速度以至撞入地形。
```java
   // Camera class

       public void applyGravity() {

           if (!onGround) {
               velocityY += gravity; // Apply gravity
                  float targetY =   position.y + velocityY;
           
            if(targetY <=0.0f){

                      position.y = (float) Math.ceil(position.y);

                       velocityY=0;
                       onGround= true;
                      doubleJumpAvailable = true;
              
               }  else{
                           // Verify Collision for current Y with CollisionHandler, for smooth gravity
                        if (CollisionHandler.checkCollisionY(this,MainGameLoop.toLoadChunks ,targetY)) {
                            //Adjust the position so that the camera remains exactly on the top surface.
                                position.y = (float) Math.ceil(position.y);

                                onGround = true;   //The camera now rest in the block
                                 velocityY = 0;
                                  doubleJumpAvailable= true;
                            }   else{ //No Collision, Keep going down!
                             position.y  = targetY;   
                              }  
                            
                   }  
               }

       }

    // CollisionHandler

      public static boolean checkCollisionY(Camera camera, List<ChunkMesh> chunks,float targetY) {

            
        if (targetY  <= 0) return true;
		      
		    return  blockCollisionAt( camera.getPosition().x, targetY,  camera.getPosition().z,chunks );


       }

操作步骤:
1. 更新相机中的applyGravity, 把velocityY的变化提前计算, 用这个目标targetY位置去使用 checkCollisionY, 对比判断一下位置上是否存在地块
2. 添加一个新的方法到CollisionHandler, 用于仅垂直高度碰撞。

这些方案可能都需要组合使用。 需要关注每一部分以得到比较鲁棒的运动和碰撞效果,尤其确保异步的区块加载对主循环来说没有副作用。通过应用这些修改,就能有效解决摄像头穿透地块并跌入虚空的问题,从而产生更为可靠且顺畅的游戏体验。

进一步的考虑:

  • 考虑使用一个游戏循环中的一个固定时间步骤进行物理模拟,而不是依赖于不定的帧率,以此确保计算的物理结果的一致性。
  • 持续地在你的开发中对测试各种案例进行测试, 并针对这些案例调整各个部分的碰撞参数, 找到游戏世界中的平衡点。