pathfinding work + ai work
All checks were successful
studiorailgun/Renderer/pipeline/head This commit looks good

This commit is contained in:
austin 2025-05-04 16:00:32 -04:00
parent e59ab56e3f
commit 6339181aec
9 changed files with 262 additions and 46 deletions

View File

@ -1661,6 +1661,9 @@ Fix pathfinding voxel hashing calculating
(05/04/2025)
Path to nearest valid voxel instead of a non-walkable voxel
Blocks factor into voxel pathfinding
Debugging pathfinding code
New AI behaviors
- Will explore for resources if local ones aren't available
@ -1728,6 +1731,8 @@ Rearchitecting
- Main render is a ui element (that we can have multiple of)
- Shader injecting consts from the engine itself (ie max lights is dynamically injected, that way never have to worry about .glsl and .java not aligning)
- Cache busting for particle atlas cache
- Convert behavior tree nodes to use static evaluation methods instead of constructing objects
- This will make stepping through the logic for a tree SIGNIFICANTLY more legible with debugger
Code cleanup
- Rename "BehaviorTree" to be "Component" (what it actually is)

View File

@ -22,9 +22,14 @@ public class PathfindingNode implements AITreeNode {
/**
* The value used to check if the entity is close to a pathing point
* The value used to check if the entity is close to a pathing point horizontally
*/
public static final double CLOSENESS_CHECK_BOUND = 0.3f;
public static final double CLOSENESS_CHECK_BOUND_HORIZONTAL = 0.3f;
/**
* The value used to check if the entity is close to a pathing point vertically
*/
public static final double CLOSENESS_CHECK_BOUND_VERTICAL = 0.5f;
/**
* The blackboard key to lookup the target entity under
@ -43,7 +48,32 @@ public class PathfindingNode implements AITreeNode {
@Override
public AITreeNodeResult evaluate(Entity entity, Blackboard blackboard) {
public AITreeNodeResult evaluate(Entity entity, Blackboard blackboard){
//make sure that the solved pathfinding data is for the point we want
if(PathfindingNode.hasPathfindingData(blackboard)){
PathingProgressiveData pathingProgressiveData = PathfindingNode.getPathfindingData(blackboard);
Vector3d actualPoint = pathingProgressiveData.getPoints().get(pathingProgressiveData.getPoints().size() - 1);
Object targetRaw = blackboard.get(this.targetEntityKey);
Vector3d targetPos = null;
if(targetRaw == null){
throw new Error("Target undefined!");
}
if(targetRaw instanceof Vector3d){
targetPos = (Vector3d)targetRaw;
} else if(targetRaw instanceof Entity){
targetPos = EntityUtils.getPosition((Entity)targetRaw);
} else if(targetRaw instanceof Structure){
targetPos = ((Structure)targetRaw).getPos();
} else {
throw new Error("Unsupported target type " + targetRaw);
}
if(actualPoint.distance(targetPos) > CLOSENESS_CHECK_BOUND_HORIZONTAL){
PathfindingNode.clearPathfindingData(blackboard);
PathfindingNode.clearPathfindingPoint(blackboard);
}
}
//create a path if we don't already have one
if(!PathfindingNode.hasPathfindingData(blackboard)){
Object targetRaw = blackboard.get(this.targetEntityKey);
Vector3d targetPos = null;
@ -82,16 +112,41 @@ public class PathfindingNode implements AITreeNode {
if(pathingProgressiveData.getCurrentPoint() < pathingProgressiveData.getPoints().size()){
currentPathPos = pathingProgressiveData.getPoints().get(pathingProgressiveData.getCurrentPoint());
}
double dist = currentPathPos.distance(entityPos);
double vertDist = Math.abs(currentPathPos.y - entityPos.y);
double horizontalDist = Math.sqrt((currentPathPos.x - entityPos.x) * (currentPathPos.x - entityPos.x) + (currentPathPos.z - entityPos.z) * (currentPathPos.z - entityPos.z));
while(
currentPathPos != null &&
dist < CLOSENESS_CHECK_BOUND &&
vertDist < CLOSENESS_CHECK_BOUND_VERTICAL &&
horizontalDist < CLOSENESS_CHECK_BOUND_HORIZONTAL &&
pathingProgressiveData.getCurrentPoint() < pathingProgressiveData.getPoints().size() - 1
){
pathingProgressiveData.setCurrentPoint(pathingProgressiveData.getCurrentPoint() + 1);
currentPathPos = pathingProgressiveData.getPoints().get(pathingProgressiveData.getCurrentPoint());
dist = currentPathPos.distance(entityPos);
}
//if we're close enough to the final pathing point, always path to actual final point
if(
vertDist < CLOSENESS_CHECK_BOUND_VERTICAL &&
horizontalDist < CLOSENESS_CHECK_BOUND_HORIZONTAL &&
pathingProgressiveData.getCurrentPoint() == pathingProgressiveData.getPoints().size() - 1
){
Object targetRaw = blackboard.get(this.targetEntityKey);
Vector3d targetPos = null;
if(targetRaw == null){
throw new Error("Target undefined!");
}
if(targetRaw instanceof Vector3d){
targetPos = (Vector3d)targetRaw;
} else if(targetRaw instanceof Entity){
targetPos = EntityUtils.getPosition((Entity)targetRaw);
} else if(targetRaw instanceof Structure){
targetPos = ((Structure)targetRaw).getPos();
} else {
throw new Error("Unsupported target type " + targetRaw);
}
currentPathPos = targetPos;
}
PathfindingNode.setPathfindingPoint(blackboard, currentPathPos);
return AITreeNodeResult.SUCCESS;

View File

@ -0,0 +1,61 @@
package electrosphere.server.ai.nodes.plan;
import java.util.Random;
import org.joml.Vector3d;
import org.joml.Vector3i;
import electrosphere.engine.Globals;
import electrosphere.entity.Entity;
import electrosphere.entity.EntityUtils;
import electrosphere.server.ai.blackboard.Blackboard;
import electrosphere.server.ai.nodes.AITreeNode;
import electrosphere.server.datacell.Realm;
import electrosphere.server.datacell.ServerWorldData;
/**
* Generates a point to explore towards
*/
public class TargetExploreNode implements AITreeNode {
/**
* The key to store the point under
*/
String targetKey;
/**
* Distance to travel in whatever direction
*/
static final double OFFSET_DIST = 50;
/**
* constructor
* @param targetKey The key to store the point under
*/
public TargetExploreNode(String targetKey){
this.targetKey = targetKey;
}
@Override
public AITreeNodeResult evaluate(Entity entity, Blackboard blackboard){
Vector3d targetPos = null;
if(!blackboard.has(targetKey)){
Vector3d entPos = new Vector3d(EntityUtils.getPosition(entity));
Random rand = new Random();
Realm realm = Globals.realmManager.getEntityRealm(entity);
Vector3d offsetVec = new Vector3d(rand.nextDouble(),0,rand.nextDouble()).normalize().mul(OFFSET_DIST);
targetPos = entPos.add(offsetVec);
//solve for height via world data
Vector3i voxelPos = ServerWorldData.convertRealToVoxelSpace(targetPos);
Vector3i chunkPos = ServerWorldData.convertRealToChunkSpace(targetPos);
double height = realm.getServerWorldData().getServerTerrainManager().getElevation(chunkPos.x, chunkPos.z, voxelPos.x, voxelPos.z);
targetPos.y = height;
//store
blackboard.put(targetKey, targetPos);
}
return AITreeNodeResult.SUCCESS;
}
}

View File

@ -24,6 +24,36 @@ public class MoveToTree {
*/
public static final String TREE_NAME = "MoveTo";
/**
* Default distance to be within
*/
static final double DEFAULT_DIST = 0.5f;
/**
* Creates a move-to-target tree
* @param targetKey The key to lookup the target under
* @return The root node of the move-to-target tree
*/
public static AITreeNode create(String targetKey){
return new SelectorNode(
new SequenceNode(
//check if in range of target
new TargetRangeCheckNode(DEFAULT_DIST, targetKey),
new DataDeleteNode(BlackboardKeys.PATHFINDING_POINT),
new DataDeleteNode(BlackboardKeys.PATHFINDING_DATA),
//if in range, stop moving fowards and return SUCCESS
new SucceederNode(new MoveStopNode())
),
//not in range of target, keep moving towards it
new SequenceNode(
PathfindingNode.createPathEntity(targetKey),
new FaceTargetNode(BlackboardKeys.PATHFINDING_POINT),
new RunnerNode(new MoveStartNode(MovementRelativeFacing.FORWARD))
)
);
}
/**
* Creates a move-to-target tree
* @param dist The target distance to be within
@ -31,7 +61,7 @@ public class MoveToTree {
* @return The root node of the move-to-target tree
*/
public static AITreeNode create(double dist, String targetKey){
if(dist < PathfindingNode.CLOSENESS_CHECK_BOUND){
if(dist < PathfindingNode.CLOSENESS_CHECK_BOUND_HORIZONTAL){
throw new Error("Dist less than minimal amount! " + dist);
}
return new SelectorNode(

View File

@ -0,0 +1,37 @@
package electrosphere.server.ai.trees.creature.explore;
import electrosphere.server.ai.blackboard.BlackboardKeys;
import electrosphere.server.ai.nodes.AITreeNode;
import electrosphere.server.ai.nodes.meta.DataDeleteNode;
import electrosphere.server.ai.nodes.meta.collections.SequenceNode;
import electrosphere.server.ai.nodes.meta.debug.PublishStatusNode;
import electrosphere.server.ai.nodes.plan.TargetExploreNode;
import electrosphere.server.ai.trees.creature.MoveToTree;
/**
* A tree for exploring new chunks
*/
public class ExploreTree {
/**
* Name of the tree
*/
public static final String TREE_NAME = "ExploreTree";
/**
* Creates an explore tree
* @return The root node of the explore tree
*/
public static AITreeNode create(){
return new SequenceNode(
new PublishStatusNode("Explore"),
//resolve point to explore towards
new TargetExploreNode(BlackboardKeys.MOVE_TO_TARGET),
//move towards the point
MoveToTree.create(BlackboardKeys.MOVE_TO_TARGET),
//clear position after moving towards it
new DataDeleteNode(BlackboardKeys.MOVE_TO_TARGET)
);
}
}

View File

@ -16,6 +16,7 @@ import electrosphere.server.ai.nodes.meta.decorators.SucceederNode;
import electrosphere.server.ai.nodes.plan.SolveSourcingTreeNode;
import electrosphere.server.ai.nodes.plan.TargetEntityCategoryNode;
import electrosphere.server.ai.trees.creature.MoveToTree;
import electrosphere.server.ai.trees.creature.explore.ExploreTree;
/**
* A tree to acquire an item
@ -71,6 +72,11 @@ public class AcquireItemTree {
new TargetEntityCategoryNode(BlackboardKeys.HARVEST_TARGET_TYPE),
FellTree.create(BlackboardKeys.ENTITY_TARGET),
new RunnerNode(null)
),
new SequenceNode(
new PublishStatusNode("Explore new chunks for resources"),
//Failed to find sources of material in existing chunks, must move for new chunks
ExploreTree.create()
)
),
new SucceederNode(null)

View File

@ -1097,6 +1097,10 @@ public class GriddedDataCellManager implements DataCellManager, VoxelCellManager
throw new Error("Failed to find tracking data for " + start);
}
Vector3d nearestValidGoal = this.pathfinder.scanNearestWalkable(this, end, VoxelPathfinder.DEFAULT_MAX_TARGET_SCAN_DIST);
if(nearestValidGoal == null){
nearestValidGoal = this.pathfinder.scanNearestWalkable(this, end, VoxelPathfinder.DEFAULT_MAX_TARGET_SCAN_DIST);
throw new Error("Failed to resolve valid point near " + end.x + "," + end.y + "," + end.z);
}
List<Vector3d> points = this.pathfinder.findPath(this, start, nearestValidGoal, VoxelPathfinder.DEFAULT_MAX_COST);
return points;
}

View File

@ -35,7 +35,7 @@ public class VoxelPathfinder {
/**
* Maximum distance to scan for a walkable position
*/
public static final double DEFAULT_MAX_TARGET_SCAN_DIST = 3;
public static final double DEFAULT_MAX_TARGET_SCAN_DIST = 5;
/**
* The heuristic lookup table
@ -53,6 +53,9 @@ public class VoxelPathfinder {
public List<Vector3d> findPath(VoxelCellManager voxelCellManager, Vector3d startPoint, Vector3d endPoint, long maxCost){
List<Vector3d> rVal = null;
if(startPoint == null || endPoint == null){
throw new Error("Points undefined! " + startPoint + " " + endPoint);
}
if(startPoint.distance(endPoint) > MAX_DIST){
throw new Error("Distance is outside range provided! " + startPoint.distance(endPoint) + " vs " + MAX_DIST);
}
@ -905,7 +908,7 @@ public class VoxelPathfinder {
for(int x = 0; x < 4; x++){
for(int y = 0; y < 4; y++){
for(int z = 0; z < 4; z++){
blockPos.set(voxelPos);
blockPos.set(voxelPos).mul(BlockChunkData.BLOCKS_PER_UNIT_DISTANCE);
currChunk.set(chunkPos);
offsets.set(
x,
@ -953,6 +956,7 @@ public class VoxelPathfinder {
Vector3d realPos;
Vector3i offsets = new Vector3i();
while(true){
scanned = 0;
for(int x = -radius; x <= radius; x++){
@ -961,7 +965,7 @@ public class VoxelPathfinder {
currChunk.set(originChunk);
offsets.set(-radius,x,y);
VoxelPathfinder.clampVoxelOffsets(currVoxel, currChunk, offsets);
realPos = ServerWorldData.convertVoxelToRealSpace(currVoxel.set(originVoxel).add(-radius,x,y), currChunk);
realPos = ServerWorldData.convertVoxelToRealSpace(currVoxel, currChunk);
if(realPos.distance(targetPoint) < maxScanRadius){
scanned++;
if(this.isWalkable(voxelCellManager, currChunk, currVoxel)){
@ -973,7 +977,7 @@ public class VoxelPathfinder {
currChunk.set(originChunk);
offsets.set(radius,x,y);
VoxelPathfinder.clampVoxelOffsets(currVoxel, currChunk, offsets);
realPos = ServerWorldData.convertVoxelToRealSpace(currVoxel.set(originVoxel).add(-radius,x,y), currChunk);
realPos = ServerWorldData.convertVoxelToRealSpace(currVoxel, currChunk);
if(realPos.distance(targetPoint) < maxScanRadius){
scanned++;
if(this.isWalkable(voxelCellManager, currChunk, currVoxel)){
@ -985,7 +989,7 @@ public class VoxelPathfinder {
currChunk.set(originChunk);
offsets.set(x,-radius,y);
VoxelPathfinder.clampVoxelOffsets(currVoxel, currChunk, offsets);
realPos = ServerWorldData.convertVoxelToRealSpace(currVoxel.set(originVoxel).add(-radius,x,y), currChunk);
realPos = ServerWorldData.convertVoxelToRealSpace(currVoxel, currChunk);
if(realPos.distance(targetPoint) < maxScanRadius){
scanned++;
if(this.isWalkable(voxelCellManager, currChunk, currVoxel)){
@ -997,7 +1001,7 @@ public class VoxelPathfinder {
currChunk.set(originChunk);
offsets.set(x,radius,y);
VoxelPathfinder.clampVoxelOffsets(currVoxel, currChunk, offsets);
realPos = ServerWorldData.convertVoxelToRealSpace(currVoxel.set(originVoxel).add(-radius,x,y), currChunk);
realPos = ServerWorldData.convertVoxelToRealSpace(currVoxel, currChunk);
if(realPos.distance(targetPoint) < maxScanRadius){
scanned++;
if(this.isWalkable(voxelCellManager, currChunk, currVoxel)){
@ -1009,7 +1013,7 @@ public class VoxelPathfinder {
currChunk.set(originChunk);
offsets.set(x,y,-radius);
VoxelPathfinder.clampVoxelOffsets(currVoxel, currChunk, offsets);
realPos = ServerWorldData.convertVoxelToRealSpace(currVoxel.set(originVoxel).add(-radius,x,y), currChunk);
realPos = ServerWorldData.convertVoxelToRealSpace(currVoxel, currChunk);
if(realPos.distance(targetPoint) < maxScanRadius){
scanned++;
if(this.isWalkable(voxelCellManager, currChunk, currVoxel)){
@ -1021,7 +1025,7 @@ public class VoxelPathfinder {
currChunk.set(originChunk);
offsets.set(x,y,radius);
VoxelPathfinder.clampVoxelOffsets(currVoxel, currChunk, offsets);
realPos = ServerWorldData.convertVoxelToRealSpace(currVoxel.set(originVoxel).add(-radius,x,y), currChunk);
realPos = ServerWorldData.convertVoxelToRealSpace(currVoxel, currChunk);
if(realPos.distance(targetPoint) < maxScanRadius){
scanned++;
if(this.isWalkable(voxelCellManager, currChunk, currVoxel)){
@ -1047,31 +1051,45 @@ public class VoxelPathfinder {
*/
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;
int storageX = (voxelPos.x + offsets.x);
int storageY = (voxelPos.y + offsets.y);
int storageZ = (voxelPos.z + offsets.z);
if(storageX < 0){
storageX = -1;
} else {
voxelPos.x = voxelPos.x / ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET;
storageX = storageX / ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET;
}
if(voxelPos.y < 0){
voxelPos.y = -1;
if(storageY < 0){
storageY = -1;
} else {
voxelPos.y = voxelPos.y / ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET;
storageY = storageY / ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET;
}
if(voxelPos.z < 0){
voxelPos.z = -1;
if(storageZ < 0){
storageZ = -1;
} else {
voxelPos.z = voxelPos.z / ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET;
storageZ = storageZ / 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;
chunkPos.x = chunkPos.x + storageX;
chunkPos.y = chunkPos.y + storageY;
chunkPos.z = chunkPos.z + storageZ;
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;
if(
voxelPos.x < 0 || voxelPos.y < 0 || voxelPos.z < 0 ||
chunkPos.x < 0 || chunkPos.y < 0 || chunkPos.z < 0 ||
chunkPos.x > 65536 || chunkPos.y > 65536 || chunkPos.z > 65536
){
String message = "Failed to clamp \n" +
"voxelPos: " + voxelPos.x + "," + voxelPos.y + "," + voxelPos.z + "\n" +
"chunkPos: " + chunkPos.x + "," + chunkPos.y + "," + chunkPos.z + "\n" +
"offsets: " + offsets.x + "," + offsets.y + "," + offsets.z + "\n" +
"storage: " + storageX + "," + storageY + "," + storageZ + "\n" +
"";
throw new Error(message);
}
}
@ -1083,28 +1101,28 @@ public class VoxelPathfinder {
*/
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;
int storageX = (blockPos.x + offsets.x);
int storageY = (blockPos.y + offsets.y);
int storageZ = (blockPos.z + offsets.z);
if(storageX < 0){
storageX = -1;
} else {
blockPos.x = blockPos.x / BlockChunkData.CHUNK_DATA_WIDTH;
storageX = storageX / BlockChunkData.CHUNK_DATA_WIDTH;
}
if(blockPos.y < 0){
blockPos.y = -1;
if(storageY < 0){
storageY = -1;
} else {
blockPos.y = blockPos.y / BlockChunkData.CHUNK_DATA_WIDTH;
storageY = storageY / BlockChunkData.CHUNK_DATA_WIDTH;
}
if(blockPos.z < 0){
blockPos.z = -1;
if(storageZ < 0){
storageZ = -1;
} else {
blockPos.z = blockPos.z / BlockChunkData.CHUNK_DATA_WIDTH;
storageZ = storageZ / 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;
chunkPos.x = chunkPos.x + storageX;
chunkPos.y = chunkPos.y + storageY;
chunkPos.z = chunkPos.z + storageZ;
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;

View File

@ -44,7 +44,7 @@ public class HashUtils {
*/
public static long hashIVec(int x, int y, int z){
if (x < 0 || x > 65536 || y < 0 || y > 65536 || z < 0 || z > 65536) {
throw new IllegalArgumentException("Values must be in range [0, 65536]");
throw new IllegalArgumentException("Values must be in range [0, 65536] " + x + "," + y + "," + z);
}
return ((long) x) | ((long) y << SHIFT_Y) | ((long) z << SHIFT_Z);
}