From 8462932abd3ee3d026d70f1414db6469da0d32d5 Mon Sep 17 00:00:00 2001 From: austin Date: Sun, 4 May 2025 13:58:08 -0400 Subject: [PATCH] block pathfinding work --- docs/src/progress/renderertodo.md | 4 + .../interact/ClientInteractionEngine.java | 9 +- .../client/ui/menu/debug/ImGuiAI.java | 38 ++- .../collision/PhysicsEntityUtils.java | 6 +- .../entity/ServerEntityUtils.java | 6 +- .../ai/trees/creature/melee/FellTree.java | 2 +- .../server/datacell/ServerWorldData.java | 14 ++ .../gridded/GriddedDataCellManager.java | 3 +- .../pathfinding/voxel/VoxelPathfinder.java | 226 +++++++++++++++++- 9 files changed, 280 insertions(+), 28 deletions(-) diff --git a/docs/src/progress/renderertodo.md b/docs/src/progress/renderertodo.md index d776c324..b264a3fd 100644 --- a/docs/src/progress/renderertodo.md +++ b/docs/src/progress/renderertodo.md @@ -1658,6 +1658,10 @@ Remove several usages of concurrent datastructures Fix block chunk data allocation explosion Fix pathfinding voxel hashing calculating +(05/04/2025) +Path to nearest valid voxel instead of a non-walkable voxel +Blocks factor into voxel pathfinding + diff --git a/src/main/java/electrosphere/client/interact/ClientInteractionEngine.java b/src/main/java/electrosphere/client/interact/ClientInteractionEngine.java index e616fdfb..c07d6658 100644 --- a/src/main/java/electrosphere/client/interact/ClientInteractionEngine.java +++ b/src/main/java/electrosphere/client/interact/ClientInteractionEngine.java @@ -175,14 +175,7 @@ public class ClientInteractionEngine { public static void destroyCollidableTemplate(Entity rVal){ lock.lock(); CollisionEngine interactionEngine = Globals.clientSceneWrapper.getInteractionEngine(); - Collidable collidable = (Collidable)rVal.getData(EntityDataStrings.INTERACTION_COLLIDABLE); - DBody body = (DBody)rVal.getData(EntityDataStrings.INTERACTION_BODY); - if(body != null){ - PhysicsUtils.destroyBody(interactionEngine, body); - if(collidable != null){ - interactionEngine.deregisterCollisionObject(body, collidable); - } - } + interactionEngine.destroyPhysics(rVal); lock.unlock(); } diff --git a/src/main/java/electrosphere/client/ui/menu/debug/ImGuiAI.java b/src/main/java/electrosphere/client/ui/menu/debug/ImGuiAI.java index 0d3d696e..0f35094e 100644 --- a/src/main/java/electrosphere/client/ui/menu/debug/ImGuiAI.java +++ b/src/main/java/electrosphere/client/ui/menu/debug/ImGuiAI.java @@ -18,6 +18,7 @@ import electrosphere.renderer.actor.ActorTextureMask; import electrosphere.renderer.ui.imgui.ImGuiWindow; import electrosphere.renderer.ui.imgui.ImGuiWindow.ImGuiWindowCallback; import electrosphere.server.ai.AI; +import electrosphere.server.ai.nodes.plan.PathfindingNode; import electrosphere.server.datacell.gridded.GriddedDataCellManager; import electrosphere.server.datacell.utils.EntityLookupUtils; import electrosphere.server.pathfinding.voxel.VoxelPathfinder; @@ -74,14 +75,6 @@ public class ImGuiAI { } - int serverIdForClientEntity = Globals.clientSceneWrapper.mapClientToServerId(Globals.playerEntity.getId()); - Entity serverPlayerEntity = EntityLookupUtils.getEntityById(serverIdForClientEntity); - AI playerAi = AI.getAI(serverPlayerEntity); - ImGui.text("AI applied to player entity: " + playerAi.isApplyToPlayer()); - if(ImGui.button("Toggle AI on player entity")){ - playerAi.setApplyToPlayer(!playerAi.isApplyToPlayer()); - } - if(ImGui.collapsingHeader("Statuses")){ for(AI ai : Globals.aiManager.getAIList()){ if(ImGui.collapsingHeader(ai.getParent().getId() + " - " + ai.getStatus())){ @@ -93,6 +86,23 @@ public class ImGuiAI { } if(ImGui.collapsingHeader("Debug Player AI")){ + int serverIdForClientEntity = Globals.clientSceneWrapper.mapClientToServerId(Globals.playerEntity.getId()); + Entity serverPlayerEntity = EntityLookupUtils.getEntityById(serverIdForClientEntity); + AI playerAi = AI.getAI(serverPlayerEntity); + Vector3d playerTargetPos = null; + if(PathfindingNode.hasPathfindingPoint(playerAi.getBlackboard())){ + playerTargetPos = PathfindingNode.getPathfindingPoint(playerAi.getBlackboard()); + } + + ImGui.text("AI applied to player entity: " + playerAi.isApplyToPlayer()); + if(playerTargetPos != null){ + ImGui.text("Target pos: " + playerTargetPos.x + "," + playerTargetPos.y + "," + playerTargetPos.z); + } else { + ImGui.text("Target pos: UNDEFINED"); + } + if(ImGui.button("Toggle AI on player entity")){ + playerAi.setApplyToPlayer(!playerAi.isApplyToPlayer()); + } if(ImGui.button("Reset iterations")){ numIterations = 1; } @@ -101,7 +111,11 @@ public class ImGuiAI { VoxelPathfinder voxelPathfinder = new VoxelPathfinder(); GriddedDataCellManager griddedDataCellManager = (GriddedDataCellManager)Globals.realmManager.getEntityRealm(serverPlayerEntity).getDataCellManager(); Vector3d playerPos = new Vector3d(EntityUtils.getPosition(serverPlayerEntity)); - List closedSet = voxelPathfinder.aStarStep(griddedDataCellManager, playerPos, new Vector3d(playerPos).add(10,0,0), 1000, numIterations); + Vector3d targetPos = new Vector3d(playerPos).add(10,0,0); + if(playerTargetPos != null){ + targetPos = playerTargetPos; + } + List closedSet = voxelPathfinder.aStarStep(griddedDataCellManager, playerPos, targetPos, 1000, numIterations); if(displayEntity.size() > 0){ for(Entity entity : displayEntity){ @@ -123,7 +137,11 @@ public class ImGuiAI { VoxelPathfinder voxelPathfinder = new VoxelPathfinder(); GriddedDataCellManager griddedDataCellManager = (GriddedDataCellManager)Globals.realmManager.getEntityRealm(serverPlayerEntity).getDataCellManager(); Vector3d playerPos = new Vector3d(EntityUtils.getPosition(serverPlayerEntity)); - List closedSet = voxelPathfinder.aStarStepOpen(griddedDataCellManager, playerPos, new Vector3d(playerPos).add(10,0,0), 1000, numIterations); + Vector3d targetPos = new Vector3d(playerPos).add(10,0,0); + if(PathfindingNode.hasPathfindingPoint(playerAi.getBlackboard())){ + targetPos = PathfindingNode.getPathfindingPoint(playerAi.getBlackboard()); + } + List closedSet = voxelPathfinder.aStarStepOpen(griddedDataCellManager, playerPos, targetPos, 1000, numIterations); if(displayEntity.size() > 0){ for(Entity entity : displayEntity){ diff --git a/src/main/java/electrosphere/collision/PhysicsEntityUtils.java b/src/main/java/electrosphere/collision/PhysicsEntityUtils.java index d6a91e00..553868e9 100644 --- a/src/main/java/electrosphere/collision/PhysicsEntityUtils.java +++ b/src/main/java/electrosphere/collision/PhysicsEntityUtils.java @@ -584,13 +584,13 @@ public class PhysicsEntityUtils { for(Entity parent : toReposition){ Vector3d parentPos = EntityUtils.getPosition(parent); if(ServerWorldData.convertRealToChunkSpace(parentPos.x) >= worldDat.getWorldSizeDiscrete()){ - parentPos.x = worldDat.convertWorldToReal(worldDat.getWorldSizeDiscrete()) - WORLD_MARGIN; + parentPos.x = ServerWorldData.convertWorldToReal(worldDat.getWorldSizeDiscrete()) - WORLD_MARGIN; } if(ServerWorldData.convertRealToChunkSpace(parentPos.y) >= worldDat.getWorldSizeDiscrete()){ - parentPos.y = worldDat.convertWorldToReal(worldDat.getWorldSizeDiscrete()) - WORLD_MARGIN; + parentPos.y = ServerWorldData.convertWorldToReal(worldDat.getWorldSizeDiscrete()) - WORLD_MARGIN; } if(ServerWorldData.convertRealToChunkSpace(parentPos.z) >= worldDat.getWorldSizeDiscrete()){ - parentPos.z = worldDat.convertWorldToReal(worldDat.getWorldSizeDiscrete()) - WORLD_MARGIN; + parentPos.z = ServerWorldData.convertWorldToReal(worldDat.getWorldSizeDiscrete()) - WORLD_MARGIN; } ServerEntityUtils.repositionEntity(parent,parentPos); } diff --git a/src/main/java/electrosphere/entity/ServerEntityUtils.java b/src/main/java/electrosphere/entity/ServerEntityUtils.java index d35a044d..3f6455af 100644 --- a/src/main/java/electrosphere/entity/ServerEntityUtils.java +++ b/src/main/java/electrosphere/entity/ServerEntityUtils.java @@ -73,9 +73,9 @@ public class ServerEntityUtils { position.x < 0 || position.y < 0 || position.z < 0 || - worldDat.convertRealToChunkSpace(position.x) >= worldDat.getWorldSizeDiscrete() || - worldDat.convertRealToChunkSpace(position.y) >= worldDat.getWorldSizeDiscrete() || - worldDat.convertRealToChunkSpace(position.z) >= worldDat.getWorldSizeDiscrete() + ServerWorldData.convertRealToChunkSpace(position.x) >= worldDat.getWorldSizeDiscrete() || + ServerWorldData.convertRealToChunkSpace(position.y) >= worldDat.getWorldSizeDiscrete() || + ServerWorldData.convertRealToChunkSpace(position.z) >= worldDat.getWorldSizeDiscrete() ){ throw new Error("Providing invalid location to reposition! " + position); } diff --git a/src/main/java/electrosphere/server/ai/trees/creature/melee/FellTree.java b/src/main/java/electrosphere/server/ai/trees/creature/melee/FellTree.java index b101657b..dbf95b54 100644 --- a/src/main/java/electrosphere/server/ai/trees/creature/melee/FellTree.java +++ b/src/main/java/electrosphere/server/ai/trees/creature/melee/FellTree.java @@ -32,7 +32,7 @@ public class FellTree { /** * Distance to start attacking at */ - static final float FELL_RANGE = 0.5f; + static final float FELL_RANGE = 0.7f; /** * Creates a fell tree diff --git a/src/main/java/electrosphere/server/datacell/ServerWorldData.java b/src/main/java/electrosphere/server/datacell/ServerWorldData.java index 2db7b087..f8677e34 100644 --- a/src/main/java/electrosphere/server/datacell/ServerWorldData.java +++ b/src/main/java/electrosphere/server/datacell/ServerWorldData.java @@ -285,6 +285,20 @@ public class ServerWorldData { public static double convertVoxelToRealSpace(int voxelPos, int worldPos){ return voxelPos + ServerWorldData.convertWorldToReal(worldPos); } + + /** + * Converts a chunk space vector to a real space vector + * @param voxelPos The voxel's position within the chunk + * @param worldPos The world pos of the chunk + * @return The real pos + */ + public static Vector3d convertVoxelToRealSpace(Vector3i voxelPos, Vector3i worldPos){ + return new Vector3d( + ServerWorldData.convertVoxelToRealSpace(voxelPos.x, worldPos.x), + ServerWorldData.convertVoxelToRealSpace(voxelPos.y, worldPos.y), + ServerWorldData.convertVoxelToRealSpace(voxelPos.z, worldPos.z) + ); + } public double getRelativeLocation(double real, int world){ return real - (world * dynamicInterpolationRatio); diff --git a/src/main/java/electrosphere/server/datacell/gridded/GriddedDataCellManager.java b/src/main/java/electrosphere/server/datacell/gridded/GriddedDataCellManager.java index 77c29020..67d4a86b 100644 --- a/src/main/java/electrosphere/server/datacell/gridded/GriddedDataCellManager.java +++ b/src/main/java/electrosphere/server/datacell/gridded/GriddedDataCellManager.java @@ -1096,7 +1096,8 @@ public class GriddedDataCellManager implements DataCellManager, VoxelCellManager if(trackingData == null){ throw new Error("Failed to find tracking data for " + start); } - List points = this.pathfinder.findPath(this, start, end, VoxelPathfinder.DEFAULT_MAX_COST); + Vector3d nearestValidGoal = this.pathfinder.scanNearestWalkable(this, end, VoxelPathfinder.DEFAULT_MAX_TARGET_SCAN_DIST); + List points = this.pathfinder.findPath(this, start, nearestValidGoal, VoxelPathfinder.DEFAULT_MAX_COST); return points; } diff --git a/src/main/java/electrosphere/server/pathfinding/voxel/VoxelPathfinder.java b/src/main/java/electrosphere/server/pathfinding/voxel/VoxelPathfinder.java index 5650644e..32f98ba3 100644 --- a/src/main/java/electrosphere/server/pathfinding/voxel/VoxelPathfinder.java +++ b/src/main/java/electrosphere/server/pathfinding/voxel/VoxelPathfinder.java @@ -11,6 +11,7 @@ import java.util.stream.Collectors; import org.joml.Vector3d; import org.joml.Vector3i; +import electrosphere.client.block.BlockChunkData; import electrosphere.server.datacell.ServerWorldData; import electrosphere.server.datacell.interfaces.VoxelCellManager; import electrosphere.server.physics.terrain.manager.ServerTerrainChunk; @@ -31,10 +32,15 @@ public class VoxelPathfinder { */ public static final int DEFAULT_MAX_COST = 12000; + /** + * Maximum distance to scan for a walkable position + */ + public static final double DEFAULT_MAX_TARGET_SCAN_DIST = 3; + /** * The heuristic lookup table */ - int[][][] heuristic = new int[3][3][3]; + private int[][][] heuristic = new int[3][3][3]; /** * Finds a path between two points given a voxel cell manager @@ -885,7 +891,223 @@ public class VoxelPathfinder { int aboveType = chunk.getType(voxelPos.x, aboveVoxel, voxelPos.z); float aboveWeight = chunk.getWeight(voxelPos.x, aboveVoxel, voxelPos.z); - return voxelType != ServerTerrainChunk.VOXEL_TYPE_AIR && voxelWeight > 0 && (aboveType == ServerTerrainChunk.VOXEL_TYPE_AIR || aboveWeight <= 0); + //checks + boolean standingOnGround = voxelType != ServerTerrainChunk.VOXEL_TYPE_AIR && voxelWeight > 0; + boolean aboveIsAir = (aboveType == ServerTerrainChunk.VOXEL_TYPE_AIR || aboveWeight <= 0); + + //check blocks as well + BlockChunkData blockChunkData = null; + + Vector3i blockPos = new Vector3i(); + Vector3i currChunk = new Vector3i(chunkPos); + Vector3i offsets = new Vector3i(); + int numBlocks = 0; + for(int x = 0; x < 4; x++){ + for(int y = 0; y < 4; y++){ + for(int z = 0; z < 4; z++){ + blockPos.set(voxelPos); + currChunk.set(chunkPos); + offsets.set( + x, + y + BlockChunkData.BLOCKS_PER_UNIT_DISTANCE, + z + ); + VoxelPathfinder.clampBlockOffsets(blockPos, currChunk, offsets); + blockChunkData = voxelCellManager.getBlocksAtPosition(currChunk); + if(blockChunkData.getType( + blockPos.x, + blockPos.y, + blockPos.z + ) != BlockChunkData.BLOCK_TYPE_EMPTY){ + numBlocks++; + } + } + } + } + boolean blocksChestHeight = numBlocks > 10; + + return standingOnGround && aboveIsAir && !blocksChestHeight; + } + + + /** + * Scans for the nearest walkable position to the specified target point + * @param voxelCellManager The voxel cell manager + * @param targetPoint The target point to scan from + * @param maxScanRadius The maximum radius to scan for walkable positions + * @return The closest point that is walkable, or null if no point is found + */ + public Vector3d scanNearestWalkable(VoxelCellManager voxelCellManager, Vector3d targetPoint, double maxScanRadius){ + int radius = 1; + int scanned = 0; + Vector3i originVoxel = ServerWorldData.convertRealToVoxelSpace(targetPoint); + Vector3i originChunk = ServerWorldData.convertRealToChunkSpace(targetPoint); + + //check if supplied point is valid + if(this.isWalkable(voxelCellManager, originChunk, originVoxel)){ + return targetPoint; + } + + Vector3i currVoxel = new Vector3i(); + Vector3i currChunk = new Vector3i(); + Vector3d realPos; + Vector3i offsets = new Vector3i(); + + while(true){ + scanned = 0; + for(int x = -radius; x <= radius; x++){ + for(int y = -radius; y <= radius; y++){ + currVoxel.set(originVoxel); + currChunk.set(originChunk); + offsets.set(-radius,x,y); + VoxelPathfinder.clampVoxelOffsets(currVoxel, currChunk, offsets); + realPos = ServerWorldData.convertVoxelToRealSpace(currVoxel.set(originVoxel).add(-radius,x,y), currChunk); + if(realPos.distance(targetPoint) < maxScanRadius){ + scanned++; + if(this.isWalkable(voxelCellManager, currChunk, currVoxel)){ + return realPos; + } + } + + currVoxel.set(originVoxel); + currChunk.set(originChunk); + offsets.set(radius,x,y); + VoxelPathfinder.clampVoxelOffsets(currVoxel, currChunk, offsets); + realPos = ServerWorldData.convertVoxelToRealSpace(currVoxel.set(originVoxel).add(-radius,x,y), currChunk); + if(realPos.distance(targetPoint) < maxScanRadius){ + scanned++; + if(this.isWalkable(voxelCellManager, currChunk, currVoxel)){ + return realPos; + } + } + + currVoxel.set(originVoxel); + currChunk.set(originChunk); + offsets.set(x,-radius,y); + VoxelPathfinder.clampVoxelOffsets(currVoxel, currChunk, offsets); + realPos = ServerWorldData.convertVoxelToRealSpace(currVoxel.set(originVoxel).add(-radius,x,y), currChunk); + if(realPos.distance(targetPoint) < maxScanRadius){ + scanned++; + if(this.isWalkable(voxelCellManager, currChunk, currVoxel)){ + return realPos; + } + } + + currVoxel.set(originVoxel); + currChunk.set(originChunk); + offsets.set(x,radius,y); + VoxelPathfinder.clampVoxelOffsets(currVoxel, currChunk, offsets); + realPos = ServerWorldData.convertVoxelToRealSpace(currVoxel.set(originVoxel).add(-radius,x,y), currChunk); + if(realPos.distance(targetPoint) < maxScanRadius){ + scanned++; + if(this.isWalkable(voxelCellManager, currChunk, currVoxel)){ + return realPos; + } + } + + currVoxel.set(originVoxel); + currChunk.set(originChunk); + offsets.set(x,y,-radius); + VoxelPathfinder.clampVoxelOffsets(currVoxel, currChunk, offsets); + realPos = ServerWorldData.convertVoxelToRealSpace(currVoxel.set(originVoxel).add(-radius,x,y), currChunk); + if(realPos.distance(targetPoint) < maxScanRadius){ + scanned++; + if(this.isWalkable(voxelCellManager, currChunk, currVoxel)){ + return realPos; + } + } + + currVoxel.set(originVoxel); + currChunk.set(originChunk); + offsets.set(x,y,radius); + VoxelPathfinder.clampVoxelOffsets(currVoxel, currChunk, offsets); + realPos = ServerWorldData.convertVoxelToRealSpace(currVoxel.set(originVoxel).add(-radius,x,y), currChunk); + if(realPos.distance(targetPoint) < maxScanRadius){ + scanned++; + if(this.isWalkable(voxelCellManager, currChunk, currVoxel)){ + return realPos; + } + } + } + } + if(scanned == 0){ + break; + } + radius++; + } + + return null; + } + + /** + * Clamps offsets to valid voxel positions + * @param voxelPos The existing voxel position + * @param chunkPos The existing chunk position + * @param offsets The offsets to apply + */ + private static void clampVoxelOffsets(Vector3i voxelPos, Vector3i chunkPos, Vector3i offsets){ + //calculate chunk offsets + voxelPos.x = (voxelPos.x + offsets.x); + voxelPos.y = (voxelPos.y + offsets.y); + voxelPos.z = (voxelPos.z + offsets.z); + if(voxelPos.x < 0){ + voxelPos.x = -1; + } else { + voxelPos.x = voxelPos.x / ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET; + } + if(voxelPos.y < 0){ + voxelPos.y = -1; + } else { + voxelPos.y = voxelPos.y / ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET; + } + if(voxelPos.z < 0){ + voxelPos.z = -1; + } else { + voxelPos.z = voxelPos.z / ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET; + } + //update world position + chunkPos.x = chunkPos.x + voxelPos.x; + chunkPos.y = chunkPos.y + voxelPos.y; + chunkPos.z = chunkPos.z + voxelPos.z; + voxelPos.x = (voxelPos.x + offsets.x + ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET) % ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET; + voxelPos.y = (voxelPos.y + offsets.y + ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET) % ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET; + voxelPos.z = (voxelPos.z + offsets.z + ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET) % ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET; + } + + + /** + * Clamps offsets to valid block positions + * @param blockPos The existing block position + * @param chunkPos The existing chunk position + * @param offsets The offsets to apply + */ + private static void clampBlockOffsets(Vector3i blockPos, Vector3i chunkPos, Vector3i offsets){ + //calculate chunk offsets + blockPos.x = (blockPos.x + offsets.x); + blockPos.y = (blockPos.y + offsets.y); + blockPos.z = (blockPos.z + offsets.z); + if(blockPos.x < 0){ + blockPos.x = -1; + } else { + blockPos.x = blockPos.x / BlockChunkData.CHUNK_DATA_WIDTH; + } + if(blockPos.y < 0){ + blockPos.y = -1; + } else { + blockPos.y = blockPos.y / BlockChunkData.CHUNK_DATA_WIDTH; + } + if(blockPos.z < 0){ + blockPos.z = -1; + } else { + blockPos.z = blockPos.z / BlockChunkData.CHUNK_DATA_WIDTH; + } + //update world position + chunkPos.x = chunkPos.x + blockPos.x; + chunkPos.y = chunkPos.y + blockPos.y; + chunkPos.z = chunkPos.z + blockPos.z; + blockPos.x = (blockPos.x + offsets.x + BlockChunkData.CHUNK_DATA_WIDTH) % BlockChunkData.CHUNK_DATA_WIDTH; + blockPos.y = (blockPos.y + offsets.y + BlockChunkData.CHUNK_DATA_WIDTH) % BlockChunkData.CHUNK_DATA_WIDTH; + blockPos.z = (blockPos.z + offsets.z + BlockChunkData.CHUNK_DATA_WIDTH) % BlockChunkData.CHUNK_DATA_WIDTH; } /**