macro pathfinding implementation

This commit is contained in:
austin 2025-05-30 22:09:10 -04:00
parent 57de3d5d81
commit 414707f6d5
25 changed files with 210 additions and 20 deletions

View File

@ -2086,6 +2086,7 @@ Skybox reflects time of day
Standardize data sourcing in MacroTemporalData
Macro pathfinding scaffolding
Macro pathfinding work
Actual macro pathfinding implementation

View File

@ -15,13 +15,18 @@ public class SequenceNode implements CollectionNode {
/**
* The child nodes of the sequence
*/
List<AITreeNode> children;
private List<AITreeNode> children;
/**
* The name associated with this node
*/
private String name;
/**
* Constructor
* @param children All the children of the sequence
*/
public SequenceNode(List<AITreeNode> children){
public SequenceNode(String name, List<AITreeNode> children){
if(children == null){
throw new IllegalArgumentException("Trying to create sequence node with no children!");
}
@ -29,13 +34,14 @@ public class SequenceNode implements CollectionNode {
throw new IllegalArgumentException("Trying to create sequence node with no children!");
}
this.children = children;
this.name = name;
}
/**
* Constructor
* @param children All the children of the sequence
*/
public SequenceNode(AITreeNode ... children){
public SequenceNode(String name, AITreeNode ... children){
if(children == null){
throw new IllegalArgumentException("Trying to create sequence node with no children!");
}
@ -43,14 +49,19 @@ public class SequenceNode implements CollectionNode {
throw new IllegalArgumentException("Trying to create sequence node with no children!");
}
this.children = Arrays.asList(children);
this.name = name;
}
@Override
public AITreeNodeResult evaluate(Entity entity, Blackboard blackboard) {
for(AITreeNode child : children){
AITreeNodeResult result = child.evaluate(entity, blackboard);
if(result != AITreeNodeResult.SUCCESS){
return result;
try {
AITreeNodeResult result = child.evaluate(entity, blackboard);
if(result != AITreeNodeResult.SUCCESS){
return result;
}
} catch(Throwable e){
throw new Error(this.name, e);
}
}
return AITreeNodeResult.SUCCESS;

View File

@ -10,6 +10,7 @@ import electrosphere.server.ai.blackboard.Blackboard;
import electrosphere.server.ai.nodes.AITreeNode;
import electrosphere.server.datacell.Realm;
import electrosphere.server.macro.MacroData;
import electrosphere.server.macro.region.MacroRegion;
import electrosphere.server.macro.spatial.MacroAreaObject;
import electrosphere.server.macro.spatial.path.MacroPathNode;
import electrosphere.server.macro.structure.VirtualStructure;
@ -40,8 +41,8 @@ public class MacroPathfindingNode implements AITreeNode {
*
* @param targetEntityKey
*/
public static PathfindingNode createPathEntity(String targetEntityKey){
PathfindingNode rVal = new PathfindingNode();
public static MacroPathfindingNode createPathEntity(String targetEntityKey){
MacroPathfindingNode rVal = new MacroPathfindingNode();
rVal.targetEntityKey = targetEntityKey;
return rVal;
}
@ -58,8 +59,8 @@ public class MacroPathfindingNode implements AITreeNode {
if(targetRaw == null){
throw new Error("Target undefined!");
}
if(targetRaw instanceof MacroPathNode macroNode){
targetPos = macroNode.getPosition();
if(targetRaw instanceof MacroRegion macroRegion){
targetPos = macroRegion.getPos();
} else {
throw new Error("Unsupported target type " + targetRaw);
}

View File

@ -25,6 +25,7 @@ public class StandardCharacterTree {
*/
public static AITreeNode create(StandardCharacterTreeData data){
return new SequenceNode(
"StandardCharacter",
new PublishStatusNode("StandardCharacter"),
//check that dependencies exist
new SelectorNode(

View File

@ -27,6 +27,7 @@ public class CharacterGoalTree {
*/
public static AITreeNode create(){
return new SequenceNode(
"CharacterGoalTree",
//check if we have goals
MacroCharacterGoalNode.createAny(),
//select based on the type of goals the character has
@ -34,6 +35,7 @@ public class CharacterGoalTree {
//character's goal is to leave sim range
new SequenceNode(
"CharacterGoalTree",
MacroCharacterGoalNode.create(CharacterGoalType.LEAVE_SIM_RANGE),
new PublishStatusNode("Leaving simulation range"),
new FaceTargetNode(BlackboardKeys.POINT_TARGET),
@ -42,6 +44,7 @@ public class CharacterGoalTree {
//character's goal is to build a structure
new SequenceNode(
"CharacterGoalTree",
MacroCharacterGoalNode.create(CharacterGoalType.BUILD_STRUCTURE),
new PublishStatusNode("Construct a shelter"),
new BeginStructureNode(),
@ -50,6 +53,7 @@ public class CharacterGoalTree {
//character's goal is to acquire an item
new SequenceNode(
"CharacterGoalTree",
MacroCharacterGoalNode.create(CharacterGoalType.ACQUIRE_ITEM),
new PublishStatusNode("Acquire building material"),
//try to find building materials
@ -58,6 +62,7 @@ public class CharacterGoalTree {
//character's goal is to move to a macro virtual structure
new SequenceNode(
"CharacterGoalTree",
MacroCharacterGoalNode.create(CharacterGoalType.MOVE_TO_MACRO_STRUCT),
new PublishStatusNode("Move to macro structure"),
MacroMoveToTree.create(BlackboardKeys.MACRO_TARGET)

View File

@ -21,6 +21,7 @@ public class AttackerAITree {
*/
public static AITreeNode create(AttackerTreeData attackerTreeData){
return new SequenceNode(
"AttackerAITree",
MeleeAITree.create(attackerTreeData)
);
}

View File

@ -12,6 +12,7 @@ import electrosphere.server.ai.nodes.meta.collections.SelectorNode;
import electrosphere.server.ai.nodes.meta.collections.SequenceNode;
import electrosphere.server.ai.nodes.meta.decorators.RunnerNode;
import electrosphere.server.ai.nodes.meta.decorators.SucceederNode;
import electrosphere.server.ai.nodes.plan.MacroPathfindingNode;
import electrosphere.server.ai.nodes.plan.PathfindingNode;
/**
@ -50,8 +51,9 @@ public class MacroMoveToTree {
}
return new SelectorNode(
new SequenceNode(
"MacroMoveToTree",
//check if in range of target
new TargetRangeCheckNode(dist, targetKey),
new TargetRangeCheckNode(dist, targetKey),
new DataDeleteNode(BlackboardKeys.PATHFINDING_POINT),
new DataDeleteNode(BlackboardKeys.PATHFINDING_DATA),
//if in range, stop moving fowards and return SUCCESS
@ -60,7 +62,8 @@ public class MacroMoveToTree {
//not in range of target, keep moving towards it
new SequenceNode(
PathfindingNode.createPathEntity(targetKey),
"MacroMoveToTree",
MacroPathfindingNode.createPathEntity(targetKey),
new FaceTargetNode(BlackboardKeys.PATHFINDING_POINT),
new RunnerNode(new MoveStartNode(MovementRelativeFacing.FORWARD))
)

View File

@ -50,6 +50,7 @@ public class MoveToTree {
}
return new SelectorNode(
new SequenceNode(
"MoveToTree",
//check if in range of target
new TargetRangeCheckNode(dist, targetKey),
new DataDeleteNode(BlackboardKeys.PATHFINDING_POINT),
@ -60,6 +61,7 @@ public class MoveToTree {
//not in range of target, keep moving towards it
new SequenceNode(
"MoveToTree",
PathfindingNode.createPathEntity(targetKey),
new FaceTargetNode(BlackboardKeys.PATHFINDING_POINT),
new RunnerNode(new MoveStartNode(MovementRelativeFacing.FORWARD))

View File

@ -24,6 +24,7 @@ public class ExploreTree {
*/
public static AITreeNode create(){
return new SequenceNode(
"Explore",
new PublishStatusNode("Explore"),
//resolve point to explore towards
new TargetExploreNode(BlackboardKeys.MOVE_TO_TARGET),

View File

@ -25,6 +25,7 @@ public class EquipToolbarTree {
*/
public static AITreeNode create(String targetKey){
return new SequenceNode(
"EquipToolbar",
new PublishStatusNode("Equip an item"),
//check that we have this type of item
new DataTransferNode(targetKey, BlackboardKeys.INVENTORY_CHECK_TYPE),

View File

@ -44,6 +44,7 @@ public class MeleeAITree {
public static AITreeNode create(AttackerTreeData attackerTreeData){
return new SequenceNode(
"MeleeAITree",
//preconditions here
new HasWeaponNode(),
new MeleeTargetingNode(attackerTreeData.getAggroRange()),
@ -53,6 +54,7 @@ public class MeleeAITree {
//in attack range
new SequenceNode(
"MeleeAITree",
//check prior to performing action
new MeleeRangeCheckNode(attackerTreeData,MeleeRangeCheckType.ATTACK),
@ -66,18 +68,21 @@ public class MeleeAITree {
new RandomizerNode(
//wait
new SequenceNode(
"MeleeAITree",
new PublishStatusNode("Waiting"),
new FaceTargetNode(BlackboardKeys.ENTITY_TARGET),
new TimerNode(new SucceederNode(null), 600)
),
//wait
new SequenceNode(
"MeleeAITree",
new PublishStatusNode("Waiting"),
new FaceTargetNode(BlackboardKeys.ENTITY_TARGET),
new TimerNode(new SucceederNode(null), 300)
),
//attack
new SequenceNode(
"MeleeAITree",
new PublishStatusNode("Attacking"),
new FaceTargetNode(BlackboardKeys.ENTITY_TARGET),
new AttackStartNode(),
@ -88,6 +93,7 @@ public class MeleeAITree {
//in aggro range
new SequenceNode(
"MeleeAITree",
//check prior to performing action
new MeleeRangeCheckNode(attackerTreeData,MeleeRangeCheckType.AGGRO),
@ -96,6 +102,7 @@ public class MeleeAITree {
//wait
new SequenceNode(
"MeleeAITree",
new PublishStatusNode("Waiting"),
new FaceTargetNode(BlackboardKeys.ENTITY_TARGET),
new TimerNode(new SucceederNode(null), 1200)
@ -103,6 +110,7 @@ public class MeleeAITree {
//strafe to the right
new SequenceNode(
"MeleeAITree",
new PublishStatusNode("Strafing right"),
new WalkStartNode(),
new InverterNode(new OnFailureNode(
@ -117,6 +125,7 @@ public class MeleeAITree {
//strafe to the left
new SequenceNode(
"MeleeAITree",
new PublishStatusNode("Strafing left"),
new WalkStartNode(),
new InverterNode(new OnFailureNode(
@ -132,6 +141,7 @@ public class MeleeAITree {
//approach target
//move towards target and attack
new SequenceNode(
"MeleeAITree",
new PublishStatusNode("Move into attack range"),
new FaceTargetNode(BlackboardKeys.ENTITY_TARGET),
new SucceederNode(new MoveStartNode(MovementRelativeFacing.FORWARD)),

View File

@ -35,11 +35,13 @@ public class AcquireItemTree {
*/
public static AITreeNode create(String blackboardKey){
return new SequenceNode(
"AcquireItemTree",
new PublishStatusNode("Acquire an item"),
//solve how we're going to get this top level item
new SolveSourcingTreeNode(blackboardKey),
new SelectorNode(
new SequenceNode(
"AcquireItemTree",
new PublishStatusNode("Pick up an item"),
//check if we should be sourcing this item by picking it up
new SourcingTypeNode(SourcingType.PICKUP, BlackboardKeys.ITEM_TARGET_CATEGORY),
@ -50,6 +52,7 @@ public class AcquireItemTree {
new RunnerNode(null)
),
new SequenceNode(
"AcquireItemTree",
new PublishStatusNode("Craft an item"),
//check if we should be sourcing this from a recipe
new SourcingTypeNode(SourcingType.RECIPE, blackboardKey),
@ -57,6 +60,7 @@ public class AcquireItemTree {
new RunnerNode(null)
),
new SequenceNode(
"AcquireItemTree",
new PublishStatusNode("Harvest an item"),
//check if we should be sourcing this from harvesting foliage
new SourcingTypeNode(SourcingType.HARVEST, blackboardKey),
@ -66,6 +70,7 @@ public class AcquireItemTree {
new RunnerNode(null)
),
new SequenceNode(
"AcquireItemTree",
new PublishStatusNode("Fell a tree"),
//check if we should be sourcing this from felling a tree
new SourcingTypeNode(SourcingType.TREE, blackboardKey),
@ -74,6 +79,7 @@ public class AcquireItemTree {
new RunnerNode(null)
),
new SequenceNode(
"AcquireItemTree",
new PublishStatusNode("Explore new chunks for resources"),
//Failed to find sources of material in existing chunks, must move for new chunks
ExploreTree.create()

View File

@ -42,6 +42,7 @@ public class FellTree {
public static AITreeNode create(String targetKey){
return new SequenceNode(
"FellTree",
//preconditions here
new DataStorageNode(BlackboardKeys.INVENTORY_CHECK_TYPE, ItemIdStrings.ITEM_STONE_AXE),
EquipToolbarTree.create(BlackboardKeys.INVENTORY_CHECK_TYPE),
@ -51,6 +52,7 @@ public class FellTree {
//in attack range
new SequenceNode(
"FellTree",
//check if in range of target
new TargetRangeCheckNode(FellTree.FELL_RANGE, targetKey),
//stop walking now that we're in range
@ -60,6 +62,7 @@ public class FellTree {
//attack
new SequenceNode(
"FellTree",
new PublishStatusNode("Attacking"),
new FaceTargetNode(BlackboardKeys.ENTITY_TARGET),
new AttackStartNode(),

View File

@ -23,8 +23,10 @@ public class MaslowTree {
*/
public static AITreeNode create(){
return new SequenceNode(
"MaslowTree",
//check that dependencies exist
new SequenceNode(
"MaslowTree",
new PublishStatusNode("Checking dependencies for maslow tree.."),
new MacroDataExists(),
new IsCharacterNode()

View File

@ -21,6 +21,7 @@ public class CombatTree {
*/
public static AITreeNode create(){
return new SequenceNode(
"CombatTree",
new PublishStatusNode("Engaged in mortal combat"),
new SucceederNode(null)
);

View File

@ -21,6 +21,7 @@ public class FleeTree {
*/
public static AITreeNode create(){
return new SequenceNode(
"FleeTree",
new PublishStatusNode("Flee!"),
new SucceederNode(null)
);

View File

@ -21,6 +21,7 @@ public class MaslowSafetyTree {
*/
public static AITreeNode create(){
return new SequenceNode(
"MaslowSafetyTree",
new PublishStatusNode("Evaluate safety"),
new SelectorNode(
FleeTree.create(),

View File

@ -23,6 +23,7 @@ public class ConstructShelterTree {
*/
public static AITreeNode create(){
return new SequenceNode(
"ConstructShelter",
new PublishStatusNode("Construct a shelter"),
new BeginStructureNode(),
BuildStructureTree.create(),

View File

@ -23,22 +23,27 @@ public class ShelterTree {
*/
public static AITreeNode create(){
return new SequenceNode(
"ShelterTree",
new PublishStatusNode("Evaluate shelter"),
//make sure that this entity actually cares about shelter
new SequenceNode(
"ShelterTree",
//if this is a character
new IsCharacterNode()
),
//now that we know the entity cares about shelter, check if they have shelter
new SequenceNode(
"ShelterTree",
//if has shelter..
new SelectorNode(
new SequenceNode(
"ShelterTree",
new HasShelter()
//already has shelter
//TODO: check environment (ie time of day) to see if we should return to shelter
),
new SequenceNode(
"ShelterTree",
//does not have shelter
ConstructShelterTree.create()
)

View File

@ -19,6 +19,7 @@ public class ForageTree {
*/
public static AITreeNode create(){
return new SequenceNode(
"ForageTree"
);
}

View File

@ -32,18 +32,22 @@ public class BuildStructureTree {
*/
public static AITreeNode create(){
return new SequenceNode(
"BuildStructureTree",
new PublishStatusNode("Construct a structure"),
//figure out current task
new SelectorNode(
//make sure we know what material we need to build with currently
new SequenceNode(
"BuildStructureTree",
new SolveBuildMaterialNode(),
new PublishStatusNode("Trying to place block in structure"),
//if has building materials
new SequenceNode(
"BuildStructureTree",
new InventoryContainsNode(BlackboardKeys.BUILDING_MATERIAL_CURRENT),
//if we're within range to place the material
new SequenceNode(
"BuildStructureTree",
//not in range, move to within range
MoveToTree.create(CollisionEngine.DEFAULT_INTERACT_DISTANCE, BlackboardKeys.STRUCTURE_TARGET),
//equip the type of block to place
@ -55,6 +59,7 @@ public class BuildStructureTree {
),
//does not have building materials
new SequenceNode(
"BuildStructureTree",
new InverterNode(new InventoryContainsNode(BlackboardKeys.BUILDING_MATERIAL_CURRENT)),
new RunnerNode(new PublishStatusNode("Waiting on macro character to set goal to find materials to build with"))
),

View File

@ -24,6 +24,7 @@ public class BlockerAITree {
*/
public static AITreeNode create(BlockerTreeData data){
return new SequenceNode(
"BlockerAITree",
new BlockStartNode(),
new MeleeTargetingNode(5.0f),
new FaceTargetNode(BlackboardKeys.ENTITY_TARGET)

View File

@ -200,6 +200,14 @@ public class MacroPathNode {
return this.neighborNodes.stream().map((Long neighborId) -> cache.getNodeById(neighborId)).collect(Collectors.toList());
}
/**
* Gets the list of neighbor ids
* @return The list of neighbor ids
*/
public List<Long> getNeighborIds(){
return this.neighborNodes;
}
/**
* Adds a neighbor to this node
* @param neighbor The neighbor

View File

@ -1,6 +1,10 @@
package electrosphere.server.service;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.concurrent.ExecutorService;
import org.joml.Vector3d;
@ -11,6 +15,7 @@ import electrosphere.engine.signal.SignalServiceImpl;
import electrosphere.engine.threads.ThreadCounts;
import electrosphere.server.datacell.Realm;
import electrosphere.server.macro.MacroData;
import electrosphere.server.macro.spatial.path.MacroPathCache;
import electrosphere.server.macro.spatial.path.MacroPathNode;
import electrosphere.server.pathfinding.recast.PathingProgressiveData;
@ -75,7 +80,120 @@ public class MacroPathingService extends SignalServiceImpl {
* @return The path
*/
private List<Vector3d> findPath(MacroData macroData, MacroPathNode start, MacroPathNode end){
throw new Error("Not implemented yet!");
List<Vector3d> rVal = null;
//tracks whether we've found the goal or not
boolean foundGoal = false;
int countConsidered = 0;
MacroPathCache pathCache = macroData.getPathCache();
//create sets
PriorityQueue<PathfinderNode> openSet = new PriorityQueue<PathfinderNode>();
Map<Long,PathfinderNode> openSetLookup = new HashMap<Long,PathfinderNode>();
Map<Long,PathfinderNode> closetSet = new HashMap<Long,PathfinderNode>();
//add start node
PathfinderNode node = new PathfinderNode(start, 0, start.getId());
openSet.add(node);
openSetLookup.put(node.graphNode.getId(),node);
//search
while(openSet.size() > 0 && !foundGoal){
//pull from open set
PathfinderNode currentNode = openSet.poll();
long currentCost = currentNode.cost;
openSetLookup.remove(currentNode.graphNode.getId());
closetSet.put(currentNode.graphNode.getId(), currentNode);
countConsidered++;
//iterate along neighbors
for(Long neighborId : currentNode.graphNode.getNeighborIds()){
//goal check
if(end.getId() == neighborId){
foundGoal = true;
break;
}
//add-to-set check
if(!closetSet.containsKey(neighborId) && !openSetLookup.containsKey(neighborId)){
MacroPathNode neighborGraphNode = pathCache.getNodeById(neighborId);
long newCost = currentCost + neighborGraphNode.getCost();
PathfinderNode newNode = new PathfinderNode(
neighborGraphNode,
newCost, currentNode.graphNode.getId()
);
openSet.add(newNode);
openSetLookup.put(neighborGraphNode.getId(), newNode);
}
}
//if found goal
if(foundGoal){
//reverse up the chain from here
rVal = new LinkedList<Vector3d>();
rVal.add(end.getPosition());
while(currentNode.prevNode != currentNode.graphNode.getId()){
rVal.add(0,currentNode.getPosition());
currentNode = closetSet.get(currentNode.prevNode);
}
rVal.add(0,start.getPosition());
break;
}
//error check
if(openSet.size() < 1){
throw new Error("Open set ran out of nodes! " + countConsidered);
}
}
if(!foundGoal){
throw new Error("Failed to find goal " + countConsidered);
}
return rVal;
}
/**
* A node to use during searching
*/
protected static class PathfinderNode implements Comparable<PathfinderNode> {
/**
* The corresponding graph node
*/
MacroPathNode graphNode;
/**
* Cost to get to this node
*/
long cost = 0;
/**
* The previous node
*/
long prevNode = 0;
public PathfinderNode(
MacroPathNode graphNode,
long cost, long prevNode
){
this.graphNode = graphNode;
this.cost = cost;
this.prevNode = prevNode;
}
@Override
public int compareTo(PathfinderNode o) {
return (int)(this.cost - o.cost);
}
/**
* Gets the position of the node
* @return The position of the node
*/
public Vector3d getPosition(){
return graphNode.getPosition();
}
}
}

View File

@ -42,13 +42,13 @@ public class CharaSimulation {
return;
}
//send a character on a walk
// if(chara.getId() == 3){
// Town hometown = CharacterUtils.getHometown(realm.getMacroData(), chara);
// if(hometown != null){
// MacroRegion target = hometown.getFarmPlots(realm.getMacroData()).get(0);
// CharacterGoal.setCharacterGoal(chara, new CharacterGoal(CharacterGoalType.MOVE_TO_MACRO_STRUCT, target));
// }
// }
if(chara.getId() == 3){
Town hometown = CharacterUtils.getHometown(realm.getMacroData(), chara);
if(hometown != null){
MacroRegion target = hometown.getFarmPlots(realm.getMacroData()).get(0);
CharacterGoal.setCharacterGoal(chara, new CharacterGoal(CharacterGoalType.MOVE_TO_MACRO_STRUCT, target));
}
}
}
/**