Ai rearchitecture for real btrees
All checks were successful
studiorailgun/Renderer/pipeline/head This commit looks good

This commit is contained in:
austin 2024-08-14 14:46:38 -04:00
parent d44fdc5673
commit bdec668fb8
18 changed files with 356 additions and 1173 deletions

View File

@ -564,6 +564,7 @@ Fix katana is frustum culled incorrectly
Fix rendering pipelines being broken when the katana is not drawn Fix rendering pipelines being broken when the katana is not drawn
Fix deleting entities on server not properly deleting the whole entity Fix deleting entities on server not properly deleting the whole entity
Windup and Cooldown animations on 2h block Windup and Cooldown animations on 2h block
AI rearchitecture to actually do behavior trees now that I know what they are
# TODO # TODO

View File

@ -155,6 +155,14 @@ public class ServerBlockTree implements BehaviorTree {
return this.state == BlockState.BLOCKING; return this.state == BlockState.BLOCKING;
} }
/**
* Checks if this tree is in any active state or not (ie is it playing an animation, blocking, etc)
* @return true if no animation is happening, false otherwise
*/
public boolean isIdle(){
return this.state == BlockState.NOT_BLOCKING;
}
/** /**
* Gets the block system data for this tree * Gets the block system data for this tree
* @return the data if it exists, otherwise null * @return the data if it exists, otherwise null

View File

@ -8,8 +8,8 @@ import com.google.gson.JsonElement;
import com.google.gson.JsonParseException; import com.google.gson.JsonParseException;
import electrosphere.logger.LoggerInterface; import electrosphere.logger.LoggerInterface;
import electrosphere.server.ai.creature.Attacker; import electrosphere.server.ai.trees.test.BlockerAITree;
import electrosphere.server.ai.creature.Blocker; import electrosphere.server.ai.trees.creature.AttackerAITree;
/** /**
* Deserializes ai tree data types * Deserializes ai tree data types
@ -20,9 +20,9 @@ public class AITreeDataSerializer implements JsonDeserializer<AITreeData> {
public AITreeData deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { public AITreeData deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
switch(json.getAsJsonObject().get("name").getAsString()){ switch(json.getAsJsonObject().get("name").getAsString()){
case Attacker.TREE_NAME: case AttackerAITree.TREE_NAME:
return context.deserialize(json, AttackerTreeData.class); return context.deserialize(json, AttackerTreeData.class);
case Blocker.TREE_NAME: case BlockerAITree.TREE_NAME:
return context.deserialize(json, BlockerTreeData.class); return context.deserialize(json, BlockerTreeData.class);
} }

View File

@ -3,7 +3,6 @@ package electrosphere.menu.debug;
import electrosphere.engine.Globals; import electrosphere.engine.Globals;
import electrosphere.renderer.ui.imgui.ImGuiWindow; import electrosphere.renderer.ui.imgui.ImGuiWindow;
import electrosphere.renderer.ui.imgui.ImGuiWindow.ImGuiWindowCallback; import electrosphere.renderer.ui.imgui.ImGuiWindow.ImGuiWindowCallback;
import electrosphere.server.ai.AI;
import imgui.ImGui; import imgui.ImGui;
/** /**
@ -38,14 +37,6 @@ public class ImGuiAI {
Globals.aiManager.setActive(!Globals.aiManager.isActive()); Globals.aiManager.setActive(!Globals.aiManager.isActive());
} }
if(ImGui.collapsingHeader("AI Statuses")){
for(AI ai : Globals.aiManager.getAIList()){
if(ai.getCurrentTree() != null){
ImGui.text("Entity ID " + ai.getParent().getId() + " - Active Tree: " + ai.getCurrentTree().getCurrentStateName() + " - Priority: " + ai.getCurrentTree().getCurrentStatePriority());
}
}
}
} }
}); });
aiWindow.setOpen(false); aiWindow.setOpen(false);

View File

@ -3,19 +3,13 @@ package electrosphere.server.ai;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import org.joml.Quaterniond;
import org.joml.Vector3d;
import electrosphere.entity.Entity; import electrosphere.entity.Entity;
import electrosphere.entity.EntityDataStrings; import electrosphere.entity.EntityDataStrings;
import electrosphere.entity.EntityUtils;
import electrosphere.entity.types.creature.CreatureUtils; import electrosphere.entity.types.creature.CreatureUtils;
import electrosphere.game.data.creature.type.ai.AITreeData; import electrosphere.game.data.creature.type.ai.AITreeData;
import electrosphere.game.data.creature.type.ai.AttackerTreeData;
import electrosphere.game.data.creature.type.ai.BlockerTreeData; import electrosphere.game.data.creature.type.ai.BlockerTreeData;
import electrosphere.logger.LoggerInterface; import electrosphere.logger.LoggerInterface;
import electrosphere.server.ai.creature.Attacker; import electrosphere.server.ai.trees.test.BlockerAITree;
import electrosphere.server.ai.creature.Blocker;
/** /**
* A collection of AITrees that are attached to a single entity. * A collection of AITrees that are attached to a single entity.
@ -34,14 +28,19 @@ public class AI {
Entity parent; Entity parent;
/** /**
* The list of trees for this ai in particular * The root node of the behavior tree
*/ */
List<AITree> trees = new LinkedList<AITree>(); AITreeNode rootNode;
/** /**
* The currently active tree * The blackboard for the tree
*/ */
AITree currentTree = null; Blackboard blackboard = new Blackboard();
/**
* Internal usage, used to track the nodes that have been evaluated and ensure we're not looping
*/
List<AITreeNode> evaluatedNodes = new LinkedList<AITreeNode>();
/** /**
* Constructs an AI from a list of trees that should be present on the ai * Constructs an AI from a list of trees that should be present on the ai
@ -54,11 +53,8 @@ public class AI {
//attach all trees //attach all trees
for(AITreeData aiData : treeData){ for(AITreeData aiData : treeData){
switch(aiData.getName()){ switch(aiData.getName()){
case Attacker.TREE_NAME: case BlockerAITree.TREE_NAME: {
rVal.trees.add(Attacker.construct(parent, (AttackerTreeData) aiData)); rVal.rootNode = BlockerAITree.create((BlockerTreeData) aiData);
break;
case Blocker.TREE_NAME: {
rVal.trees.add(Blocker.construct(parent, (BlockerTreeData) aiData));
} break; } break;
default: { default: {
LoggerInterface.loggerEngine.ERROR(new IllegalArgumentException("Trying to construct ai tree with undefined data type! " + aiData.getName())); LoggerInterface.loggerEngine.ERROR(new IllegalArgumentException("Trying to construct ai tree with undefined data type! " + aiData.getName()));
@ -80,47 +76,22 @@ public class AI {
* Simulates a single frame of this ai. Only runs simulation logic for the highest priority tree * Simulates a single frame of this ai. Only runs simulation logic for the highest priority tree
*/ */
protected void simulate(){ protected void simulate(){
if(this.shouldExecute()){
AITreeNode currentNode = this.rootNode;
evaluatedNodes.clear();
while(currentNode != null){
// //
//Check basic values of entity to make sure the ai trees aren't changing state when updating state and priority //loop checking
Vector3d currentPos = new Vector3d(EntityUtils.getPosition(parent)); if(evaluatedNodes.contains(currentNode)){
Quaterniond currentRot = new Quaterniond(EntityUtils.getRotation(parent)); LoggerInterface.loggerEngine.ERROR(new IllegalStateException("AI tree looped!"));
break;
//
//Update state and find the highest priority tree
for(AITree tree : trees){
tree.updateStateAndPriority();
if(currentTree == null){
currentTree = tree;
} else if(currentTree.getCurrentStatePriority() < tree.getCurrentStatePriority()){
currentTree = tree;
}
} }
// //
//Actual check to see if state has changed (throw error if it does) //evaluate node
Vector3d postUpdatePos = EntityUtils.getPosition(parent); evaluatedNodes.add(currentNode);
Quaterniond postUpdateRot = EntityUtils.getRotation(parent); currentNode = currentNode.evaluate(this.parent, this.blackboard);
if(postUpdatePos.distance(currentPos) > 0){
String message = "An AI tree changed entity state while it was updating its priority!\n" +
"This is an illegal operation!\n" +
"All entity updates should be in the simulate method!" +
"(The position changed)";
LoggerInterface.loggerEngine.ERROR(new IllegalStateException(message));
} }
if(postUpdateRot.dot(currentRot) < ERROR_CHECK_ROT_THRESHOLD){
String message = "An AI tree changed entity state while it was updating its priority!\n" +
"This is an illegal operation!\n" +
"All entity updates should be in the simulate method!" +
"(The rotation changed) " + postUpdateRot.dot(currentRot);
LoggerInterface.loggerEngine.ERROR(new IllegalStateException(message));
}
//
//Simulate the highest priority tree
if(currentTree != null && !CreatureUtils.hasControllerPlayerId(this.parent)){
currentTree.simulate();
} }
} }
@ -155,11 +126,19 @@ public class AI {
} }
/** /**
* Gets the current highest priority tree * Gets the root node of the behavior tree
* @return The highest priority tree * @return The root node
*/ */
public AITree getCurrentTree(){ public AITreeNode getRootNode(){
return this.currentTree; return this.rootNode;
}
/**
* Checks if the ai should simulate or not
* @return true if should simulate, false otherwise
*/
private boolean shouldExecute(){
return !CreatureUtils.hasControllerPlayerId(this.parent);
} }
} }

View File

@ -1,40 +0,0 @@
package electrosphere.server.ai;
/**
* A behavior tree encapsulating some type of
*/
public interface AITree {
/**
* Simulates a single frame of logic for the ai.
* This is where the ai actually makes the entity do something
*/
public void simulate();
/**
* Gets the name of the tree
* @return The tree's name
*/
public String getTreeName();
/**
* Updates the state and priority of this tree.
* This should calculate the goal of this tree every frame.
* All state transitions should be in here.
*/
public void updateStateAndPriority();
/**
* Gets the name of the current state of this ai
* @return The name
*/
public String getCurrentStateName();
/**
* Gets the priority of the current state of this ai
* The priority increases with the value of the int. IE, a priority of 1 is of greater importance than a priority of 0.
* @return The priority
*/
public int getCurrentStatePriority();
}

View File

@ -0,0 +1,18 @@
package electrosphere.server.ai;
import electrosphere.entity.Entity;
/**
* A node in a behavior tree
*/
public interface AITreeNode {
/**
* Evaluates the node
* @param entity The entity to evaluate this node on
* @param blackboard The blackboard for the tree
* @return The next node to run, or null if no more nodes should be run
*/
public AITreeNode evaluate(Entity entity, Blackboard blackboard);
}

View File

@ -0,0 +1,57 @@
package electrosphere.server.ai;
import java.util.HashMap;
import java.util.Map;
/**
* Blackboard for storing data with the tree
*/
public class Blackboard {
/**
* The values in the blackboard
*/
Map<String,Object> values = new HashMap<String,Object>();
/**
* Puts a value into the blackboard
* @param key The key
* @param value The value
*/
public void put(String key, Object value){
values.put(key,value);
}
/**
* Gets a value from the blackboard
* @param key The key of the value
* @return The value if it exists, null otherwise
*/
public Object get(String key){
if(this.has(key)){
return values.get(key);
}
return null;
}
/**
* Checks if the blackboard contains a value at a given key
* @param key The key
* @return true if the blackboard has a value for that key, false otherwise
*/
public boolean has(String key){
return values.containsKey(key);
}
/**
* Deletes a value from the blackboard
* @param key The key to delete the value from
*/
public void delete(String key){
if(this.has(key)){
values.remove(key);
}
}
}

View File

@ -1,344 +0,0 @@
package electrosphere.server.ai.creature;
import java.util.Random;
import org.joml.Vector3d;
import electrosphere.engine.Globals;
import electrosphere.entity.Entity;
import electrosphere.entity.EntityTags;
import electrosphere.entity.EntityUtils;
import electrosphere.entity.state.attack.ServerAttackTree;
import electrosphere.entity.state.block.ServerBlockTree;
import electrosphere.entity.state.movement.groundmove.ClientGroundMovementTree;
import electrosphere.entity.state.movement.groundmove.ServerGroundMovementTree;
import electrosphere.entity.state.movement.groundmove.ClientGroundMovementTree.MovementRelativeFacing;
import electrosphere.entity.types.creature.CreatureUtils;
import electrosphere.game.data.creature.type.ai.AttackerTreeData;
import electrosphere.logger.LoggerInterface;
import electrosphere.server.ai.AITree;
import electrosphere.server.datacell.Realm;
import electrosphere.server.datacell.utils.DataCellSearchUtils;
/**
* Keeps track of targets and instructs the ai to attack
*/
public class Attacker implements AITree {
/**
* The name of the tree type
*/
public static final String TREE_NAME = "Attacker";
/**
* Different states available to the Attacker tree
*/
public enum AttackerState {
/**
* Not doing anything
*/
IDLE,
/**
* Trying to close the distance between itself and its target
*/
MOVING_INTO_ATTACKING_RANGE,
/**
* Moving around the target. If the target is a player, this is intended to give the player openings to attack during
*/
DANCING,
/**
* Actually going in for an attack
*/
ATTACKING,
}
/**
* The configuring tree data for this tree
*/
AttackerTreeData treeData;
/**
* The current state of this instance of this tree
*/
AttackerState state = AttackerState.IDLE;
/**
* The parent to this tree
*/
Entity parent;
/**
* The current target of this tree
*/
Entity target = null;
/**
* The number of frames the tree has been in its current state
*/
int currentStateFrameCount = 0;
/**
* The random used for rolls on behavior transitions
*/
Random random = new Random();
@Override
public String getTreeName(){
return TREE_NAME;
}
/**
* Constructor
* @param parent The parent to this tree
*/
private Attacker(Entity parent){
this.parent = parent;
}
/**
* Constructs an attacker tree
* @param parent The parent entity
* @param treeData The tree data
* @return The Attacker tree
*/
public static AITree construct(Entity parent, AttackerTreeData treeData) {
Attacker rVal = new Attacker(parent);
rVal.treeData = treeData;
return rVal;
}
@Override
public void simulate() {
switch(state){
case IDLE: {
ServerBlockTree serverBlockTree = ServerBlockTree.getServerBlockTree(parent);
if(serverBlockTree.isBlocking()){
serverBlockTree.stop();
}
} break;
case MOVING_INTO_ATTACKING_RANGE: {
//make sure we're blocking
ServerBlockTree serverBlockTree = ServerBlockTree.getServerBlockTree(parent);
if(!serverBlockTree.isBlocking()){
serverBlockTree.start();
}
//close distance
moveToTarget();
} break;
case DANCING: {
ServerBlockTree serverBlockTree = ServerBlockTree.getServerBlockTree(parent);
if(!serverBlockTree.isBlocking()){
serverBlockTree.start();
}
//TODO: move parallel
} break;
case ATTACKING: {
ServerBlockTree serverBlockTree = ServerBlockTree.getServerBlockTree(parent);
ServerAttackTree serverAttackTree = ServerAttackTree.getServerAttackTree(parent);
if(serverBlockTree.isBlocking()){
serverBlockTree.stop();
} else if(serverAttackTree.isAttacking()){
} else {
serverAttackTree.start();
}
} break;
}
}
@Override
public void updateStateAndPriority() {
switch(state){
case IDLE: {
if(target != null){
setState(AttackerState.MOVING_INTO_ATTACKING_RANGE);
} else {
searchForTarget();
}
} break;
case MOVING_INTO_ATTACKING_RANGE: {
if(inAttackRange()){
//should change to dancing or attacking
if(this.random.nextInt(2) > 0){
setState(AttackerState.ATTACKING);
} else {
setState(AttackerState.DANCING);
}
} else if(
inAggroRange() &&
!inAttackRange() &&
currentStateFrameCount > this.treeData.getStateChangeTimeout()
){
//should change to dancing or moving into attack range
if(this.random.nextInt(2) > 0){
setState(AttackerState.MOVING_INTO_ATTACKING_RANGE);
} else {
setState(AttackerState.DANCING);
}
} else if(!inAggroRange()) {
this.target = null;
setState(AttackerState.IDLE);
}
} break;
case DANCING: {
if(inAttackRange()){
//should change to dancing or attacking
if(this.random.nextInt(2) > 0){
setState(AttackerState.ATTACKING);
} else {
setState(AttackerState.DANCING);
}
} else if(
inAggroRange() &&
!inAttackRange() &&
currentStateFrameCount > this.treeData.getStateChangeTimeout()
){
//should change to dancing or moving into attack range
if(this.random.nextInt(2) > 0){
setState(AttackerState.MOVING_INTO_ATTACKING_RANGE);
} else {
setState(AttackerState.DANCING);
}
} else if(!inAggroRange()) {
this.target = null;
setState(AttackerState.IDLE);
}
} break;
case ATTACKING: {
if(inAttackRange()){
//should change to dancing or attacking
if(this.random.nextInt(2) > 0){
setState(AttackerState.ATTACKING);
} else {
setState(AttackerState.DANCING);
}
} else if(
inAggroRange() &&
!inAttackRange() &&
currentStateFrameCount > this.treeData.getStateChangeTimeout()
){
//should change to dancing or moving into attack range
if(this.random.nextInt(2) > 0){
setState(AttackerState.MOVING_INTO_ATTACKING_RANGE);
} else {
setState(AttackerState.DANCING);
}
} else if(!inAggroRange()) {
this.target = null;
setState(AttackerState.IDLE);
}
} break;
}
currentStateFrameCount++;
}
@Override
public String getCurrentStateName() {
switch(state){
case IDLE:
return "IDLE";
case MOVING_INTO_ATTACKING_RANGE:
return "MOVING INTO ATTACKING RANGE";
case DANCING:
return "DANCING";
case ATTACKING:
return "ATTACKING";
}
LoggerInterface.loggerEngine.ERROR(new IllegalStateException("Unaccounted for state name in ai tree"));
return "";
}
@Override
public int getCurrentStatePriority() {
switch(state){
case IDLE:
return 0;
case MOVING_INTO_ATTACKING_RANGE:
return 3;
case DANCING:
return 2;
case ATTACKING:
return 8;
}
LoggerInterface.loggerEngine.ERROR(new IllegalStateException("Unaccounted for state name in ai tree"));
return 0;
}
/**
* Sets the state of the tree
* @param state The state
*/
private void setState(AttackerState state){
this.state = state;
this.currentStateFrameCount = 0;
}
/**
* Searches for a valid target
*/
private void searchForTarget(){
Vector3d position = EntityUtils.getPosition(parent);
Realm realm = Globals.realmManager.getEntityRealm(parent);
for(Entity current : DataCellSearchUtils.getEntitiesWithTagAroundLocation(realm,position,EntityTags.LIFE_STATE)){
if(current != parent){
Vector3d potentialTargetPosition = EntityUtils.getPosition(current);
if(position.distance(potentialTargetPosition) < treeData.getAggroRange()){
target = current;
break;
}
}
}
}
/**
* Checks if the target is within the aggression range
*/
private boolean inAggroRange(){
boolean rVal = false;
Vector3d position = EntityUtils.getPosition(parent);
Vector3d targetPosition = EntityUtils.getPosition(target);
if(new Vector3d(position).distance(targetPosition) < treeData.getAggroRange()){
rVal = true;
}
return rVal;
}
/**
* Checks if the target is within range of an attack animation
* @return
*/
private boolean inAttackRange(){
boolean rVal = false;
Vector3d position = EntityUtils.getPosition(parent);
Vector3d targetPosition = EntityUtils.getPosition(target);
if(new Vector3d(position).distance(targetPosition) < treeData.getAttackRange()){
rVal = true;
}
return rVal;
}
/**
* Attempts to move towards the target
*/
private void moveToTarget(){
Vector3d targetPosition = EntityUtils.getPosition(target);
Vector3d characterPosition = EntityUtils.getPosition(parent);
Vector3d moveVector = new Vector3d(targetPosition).sub(characterPosition).normalize();
CreatureUtils.setFacingVector(parent, new Vector3d((float)moveVector.x,(float)moveVector.y,(float)moveVector.z));
ServerGroundMovementTree characterMoveTree = (ServerGroundMovementTree)CreatureUtils.serverGetEntityMovementTree(parent);
if(characterMoveTree.getState()==ClientGroundMovementTree.MovementTreeState.IDLE || characterMoveTree.getState()==ClientGroundMovementTree.MovementTreeState.SLOWDOWN){
characterMoveTree.start(MovementRelativeFacing.FORWARD);
}
}
}

View File

@ -1,127 +0,0 @@
package electrosphere.server.ai.creature;
import java.util.Random;
import org.joml.Quaterniond;
import org.joml.Vector3d;
import electrosphere.engine.Globals;
import electrosphere.entity.Entity;
import electrosphere.entity.EntityTags;
import electrosphere.entity.EntityUtils;
import electrosphere.entity.state.block.ServerBlockTree;
import electrosphere.entity.types.creature.CreatureUtils;
import electrosphere.game.data.creature.type.ai.BlockerTreeData;
import electrosphere.server.ai.AITree;
import electrosphere.server.datacell.Realm;
import electrosphere.server.datacell.utils.DataCellSearchUtils;
import electrosphere.util.math.MathUtils;
/**
* AI that just continuously blocks
*/
public class Blocker implements AITree {
/**
* The name of the tree type
*/
public static final String TREE_NAME = "Blocker";
/**
* The configuring tree data for this tree
*/
BlockerTreeData treeData;
/**
* The parent to this tree
*/
Entity parent;
/**
* The current target of this tree
*/
Entity target = null;
/**
* The number of frames the tree has been in its current state
*/
int currentStateFrameCount = 0;
/**
* The random used for rolls on behavior transitions
*/
Random random = new Random();
/**
* Private constructor
* @param parent The parent entity
*/
private Blocker(Entity parent){
this.parent = parent;
}
/**
* Constructs an attacker tree
* @param parent The parent entity
* @param treeData The tree data
* @return The Attacker tree
*/
public static AITree construct(Entity parent, BlockerTreeData treeData) {
Blocker rVal = new Blocker(parent);
rVal.treeData = treeData;
return rVal;
}
@Override
public void simulate() {
if(target == null){
searchForTarget();
}
if(ServerBlockTree.getServerBlockTree(parent) != null){
ServerBlockTree serverBlockTree = ServerBlockTree.getServerBlockTree(parent);
if(!serverBlockTree.isBlocking()){
serverBlockTree.start();
}
}
if(target != null){
Vector3d targetPos = EntityUtils.getPosition(target);
Quaterniond rotation = MathUtils.calculateRotationFromPointToPoint(EntityUtils.getPosition(parent), targetPos);
EntityUtils.getRotation(parent).set(rotation);
}
}
@Override
public String getTreeName() {
return TREE_NAME;
}
@Override
public void updateStateAndPriority() {
}
@Override
public String getCurrentStateName() {
return "BLCOKING";
}
@Override
public int getCurrentStatePriority() {
return 1;
}
/**
* Searches for a valid target
*/
private void searchForTarget(){
Vector3d position = EntityUtils.getPosition(parent);
Realm realm = Globals.realmManager.getEntityRealm(parent);
for(Entity current : DataCellSearchUtils.getEntitiesWithTagAroundLocation(realm,position,EntityTags.LIFE_STATE)){
if(current != parent && CreatureUtils.isCreature(current)){
target = current;
break;
}
}
}
}

View File

@ -1,123 +0,0 @@
package electrosphere.server.ai.creature;
import electrosphere.entity.Entity;
import electrosphere.entity.EntityUtils;
import electrosphere.entity.state.movement.groundmove.ClientGroundMovementTree;
import electrosphere.entity.state.movement.groundmove.ClientGroundMovementTree.MovementRelativeFacing;
import electrosphere.entity.types.creature.CreatureUtils;
import electrosphere.server.ai.AITree;
import java.util.Random;
import org.joml.Vector3d;
/**
* An ai whose goal is to wander sporadically
*/
public class MillAbout implements AITree {
Entity character;
boolean millAbout = true;
int millMoveTimer = 0;
int millMoveTimerTimeout = 100;
int MILL_MOVE_TIMEOUT_LOWER = 1500;
int MILL_MOVE_TIMEOUT_UPPER = 2000;
float millTargetMaxDist = 3;
boolean moveToTarget = false;
Vector3d moveTargetPosition;
private MillAbout(Entity character){
this.character = character;
}
@Override
public void simulate(){
Vector3d position = EntityUtils.getPosition(character);
//mill aboud
if(millAbout){
if(millMoveTimer >= millMoveTimerTimeout){
Random rand = new Random();
millMoveTimer = 0;
millMoveTimerTimeout = rand.nextInt(MILL_MOVE_TIMEOUT_UPPER - MILL_MOVE_TIMEOUT_LOWER) + MILL_MOVE_TIMEOUT_LOWER;
Vector3d moveVector = new Vector3d();
//search for a spot to mill to
// while(true){
moveVector.set(
rand.nextFloat() - 0.5,
rand.nextFloat() - 0.5,
rand.nextFloat() - 0.5
).normalize().mul(millTargetMaxDist);
moveVector.y = position.y; //Globals.commonWorldData.getElevationAtPoint(new Vector3d(position).add(moveVector));
//TODO: search in navmeshmanager to make sure navigable, otherwise generate new pos
// }
moveTargetPosition = new Vector3d(position).add(moveVector);
millAbout = false;
moveToTarget = true;
} else {
millMoveTimer++;
}
}
if(moveToTarget){
if(moveTargetPosition.distance(position) > 0.4){
Vector3d moveVector = new Vector3d(moveTargetPosition).sub(position).normalize();
CreatureUtils.setFacingVector(character, new Vector3d((float)moveVector.x,(float)moveVector.y,(float)moveVector.z));
ClientGroundMovementTree characterMoveTree = (ClientGroundMovementTree)CreatureUtils.serverGetEntityMovementTree(character);
if(characterMoveTree.getState()==ClientGroundMovementTree.MovementTreeState.IDLE || characterMoveTree.getState()==ClientGroundMovementTree.MovementTreeState.SLOWDOWN){
characterMoveTree.start(MovementRelativeFacing.FORWARD);
}
} else {
ClientGroundMovementTree characterMoveTree = (ClientGroundMovementTree)CreatureUtils.serverGetEntityMovementTree(character);
characterMoveTree.slowdown();
// System.out.println("Made it to destination");
moveToTarget = false;
millAbout = true;
}
}
// if(target != null){
// if(inAttackRange()){
// attack();
// } else {
// if(inAggroRange()){
// moveToTarget();
// } else {
// target = null;
// }
// }
// } else {
// searchForTarget();
// }
}
public static AITree construct(Entity parent) {
MillAbout rVal = new MillAbout(parent);
return rVal;
}
@Override
public String getTreeName() {
return "MillAbout";
}
@Override
public void updateStateAndPriority() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'updateStateAndPriority'");
}
@Override
public String getCurrentStateName() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'getCurrentStateName'");
}
@Override
public int getCurrentStatePriority() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'getCurrentStatePriority'");
}
}

View File

@ -1,152 +0,0 @@
package electrosphere.server.ai.creature;
import electrosphere.engine.Globals;
import electrosphere.entity.Entity;
import electrosphere.entity.EntityTags;
import electrosphere.entity.EntityUtils;
import electrosphere.entity.state.attack.ServerAttackTree;
import electrosphere.entity.state.movement.groundmove.ClientGroundMovementTree;
import electrosphere.entity.state.movement.groundmove.ClientGroundMovementTree.MovementRelativeFacing;
import electrosphere.entity.types.creature.CreatureUtils;
import electrosphere.server.ai.AITree;
import electrosphere.server.datacell.Realm;
import electrosphere.server.datacell.utils.DataCellSearchUtils;
import org.joml.Vector3d;
/**
* An ai whose goal is to find a weapon and try to kill nearby enemies
*/
public class MindlessAttacker implements AITree {
/**
* The parent entity to this ai tree
*/
Entity parent;
/**
* The current target of this tree
*/
Entity target;
//aggression ranges
float aggroRange = 10.0f;
float attackRange = 1.0f;
//cooldown controls
int attackCooldownMax = 250;
int attackCooldown = 0;
@Override
public void simulate(){
if(target != null){
if(inAttackRange()){
attack();
} else {
if(inAggroRange()){
moveToTarget();
} else {
target = null;
}
}
} else {
searchForTarget();
}
}
void attack(){
if(attackCooldown == 0){
attackCooldown = attackCooldownMax;
ServerAttackTree attackTree = CreatureUtils.serverGetAttackTree(parent);
attackTree.start();
} else {
attackCooldown--;
}
}
void moveToTarget(){
Vector3d targetPosition = EntityUtils.getPosition(target);
Vector3d characterPosition = EntityUtils.getPosition(parent);
Vector3d moveVector = new Vector3d(targetPosition).sub(characterPosition).normalize();
CreatureUtils.setFacingVector(parent, new Vector3d((float)moveVector.x,(float)moveVector.y,(float)moveVector.z));
ClientGroundMovementTree characterMoveTree = (ClientGroundMovementTree)CreatureUtils.serverGetEntityMovementTree(parent);
if(characterMoveTree.getState()==ClientGroundMovementTree.MovementTreeState.IDLE || characterMoveTree.getState()==ClientGroundMovementTree.MovementTreeState.SLOWDOWN){
characterMoveTree.start(MovementRelativeFacing.FORWARD);
}
}
boolean inAttackRange(){
boolean rVal = false;
Vector3d position = EntityUtils.getPosition(parent);
Vector3d targetPosition = EntityUtils.getPosition(target);
if(new Vector3d(position).distance(targetPosition) < attackRange){
rVal = true;
}
return rVal;
}
boolean inAggroRange(){
boolean rVal = false;
Vector3d position = EntityUtils.getPosition(parent);
Vector3d targetPosition = EntityUtils.getPosition(target);
if(new Vector3d(position).distance(targetPosition) < aggroRange){
rVal = true;
}
return rVal;
}
void searchForTarget(){
Vector3d position = EntityUtils.getPosition(parent);
Realm realm = Globals.realmManager.getEntityRealm(parent);
for(Entity current : DataCellSearchUtils.getEntitiesWithTagAroundLocation(realm,position,EntityTags.LIFE_STATE)){
if(current != parent){
Vector3d potentialTargetPosition = EntityUtils.getPosition(current);
if(position.distance(potentialTargetPosition) < aggroRange){
target = current;
break;
}
}
}
}
public static AITree construct(Entity parent) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'construct'");
}
@Override
public void updateStateAndPriority() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'updateStateAndPriority'");
}
@Override
public String getCurrentStateName() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'getCurrentStateName'");
}
@Override
public int getCurrentStatePriority() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'getCurrentStatePriority'");
}
@Override
public String getTreeName(){
return "MindlessAttacker";
}
}

View File

@ -1,315 +0,0 @@
package electrosphere.server.ai.creature;
import electrosphere.engine.Globals;
import electrosphere.entity.Entity;
import electrosphere.entity.EntityTags;
import electrosphere.entity.EntityUtils;
import electrosphere.entity.state.attack.ServerAttackTree;
import electrosphere.entity.state.equip.ClientEquipState;
import electrosphere.entity.state.movement.groundmove.ClientGroundMovementTree;
import electrosphere.entity.state.movement.groundmove.ClientGroundMovementTree.MovementRelativeFacing;
import electrosphere.entity.types.creature.CreatureUtils;
import electrosphere.entity.types.item.ItemUtils;
import electrosphere.server.ai.AITree;
import electrosphere.server.datacell.Realm;
import electrosphere.server.datacell.utils.DataCellSearchUtils;
import electrosphere.util.math.MathUtils;
import org.joml.Quaterniond;
import org.joml.Vector3d;
/**
* An ai whose goal is to try to kill enemies; however, if it has no access to a weapon it will try to run away
*/
public class OpportunisticAttacker implements AITree {
/**
* The parent of this ai tree
*/
Entity parent;
/**
* The target for this ai
*/
Entity target;
float aggroRange = 10.0f;
float attackRange = 1.0f;
float weaponSeekRange = 10.0f;
float pickupRange = 2.0f;
int attackCooldownMax = 250;
int attackCooldown = 0;
enum OpportunisticAttackerGoal {
ATTACK,
SEEK_WEAPON,
FLEE,
IDLE,
}
OpportunisticAttackerGoal goal;
@Override
public String getTreeName(){
return "OpportunisticAttacker";
}
OpportunisticAttacker(Entity character){
this.parent = character;
}
public static void attachToCreature(Entity creature){
OpportunisticAttacker ai = new OpportunisticAttacker(creature);
ai.goal = OpportunisticAttackerGoal.IDLE;
}
@Override
public void simulate(){
switch(goal){
case ATTACK:
if(target != null){
if(inAttackRange()){
if(hasWeapon()){
faceTarget();
attack();
} else {
if(weaponInRange()){
setGoal(OpportunisticAttackerGoal.SEEK_WEAPON);
} else {
setGoal(OpportunisticAttackerGoal.IDLE);
}
}
} else {
if(inAggroRange()){
if(hasWeapon()){
faceTarget();
moveToTarget();
} else {
if(weaponInRange()){
setGoal(OpportunisticAttackerGoal.SEEK_WEAPON);
} else {
setGoal(OpportunisticAttackerGoal.FLEE);
}
}
} else {
target = null;
setGoal(OpportunisticAttackerGoal.IDLE);
}
}
} else {
setGoal(OpportunisticAttackerGoal.IDLE);
}
break;
case SEEK_WEAPON:
if(target != null){
if(weaponInRange()){
if(weaponInPickupRange()){
pickupWeapon();
searchForTarget();
if(target != null){
setGoal(OpportunisticAttackerGoal.ATTACK);
}
} else {
moveToTarget();
}
} else {
setGoal(OpportunisticAttackerGoal.FLEE);
}
} else {
if(inAggroRange()){
if(hasWeapon()){
moveToTarget();
} else {
if(!weaponInRange()){
setGoal(OpportunisticAttackerGoal.FLEE);
}
}
} else {
target = null;
setGoal(OpportunisticAttackerGoal.IDLE);
}
}
break;
case FLEE:
if(target != null){
if(weaponInRange()){
setGoal(OpportunisticAttackerGoal.SEEK_WEAPON);
} else {
if(inAggroRange()){
//find direction opposite target and move there
} else {
target = null;
setGoal(OpportunisticAttackerGoal.IDLE);
}
}
} else {
setGoal(OpportunisticAttackerGoal.IDLE);
}
break;
case IDLE:
searchForTarget();
break;
}
}
void attack(){
if(attackCooldown == 0){
attackCooldown = attackCooldownMax;
ServerAttackTree attackTree = CreatureUtils.serverGetAttackTree(parent);
attackTree.start();
} else {
attackCooldown--;
}
}
void moveToTarget(){
Vector3d targetPosition = EntityUtils.getPosition(target);
Vector3d characterPosition = EntityUtils.getPosition(parent);
Vector3d moveVector = new Vector3d(targetPosition).sub(characterPosition).normalize();
CreatureUtils.setFacingVector(parent, new Vector3d((float)moveVector.x,(float)moveVector.y,(float)moveVector.z));
ClientGroundMovementTree characterMoveTree = (ClientGroundMovementTree)CreatureUtils.serverGetEntityMovementTree(parent);
if(characterMoveTree.getState()==ClientGroundMovementTree.MovementTreeState.IDLE || characterMoveTree.getState()==ClientGroundMovementTree.MovementTreeState.SLOWDOWN){
characterMoveTree.start(MovementRelativeFacing.FORWARD);
}
}
boolean inAttackRange(){
boolean rVal = false;
Vector3d position = EntityUtils.getPosition(parent);
Vector3d targetPosition = EntityUtils.getPosition(target);
if(new Vector3d(position).distance(targetPosition) < attackRange){
rVal = true;
}
return rVal;
}
boolean inAggroRange(){
boolean rVal = false;
Vector3d position = EntityUtils.getPosition(parent);
Vector3d targetPosition = EntityUtils.getPosition(target);
if(new Vector3d(position).distance(targetPosition) < aggroRange){
rVal = true;
}
return rVal;
}
void searchForTarget(){
Vector3d position = EntityUtils.getPosition(parent);
Realm realm = Globals.realmManager.getEntityRealm(parent);
for(Entity current : DataCellSearchUtils.getEntitiesWithTagAroundLocation(realm,position,EntityTags.LIFE_STATE)){
if(current != parent){
Vector3d potentialTargetPosition = EntityUtils.getPosition(current);
if(position.distance(potentialTargetPosition) < aggroRange){
target = current;
setGoal(OpportunisticAttackerGoal.ATTACK);
break;
}
}
}
}
boolean hasWeapon(){
boolean rVal = false;
if(ClientEquipState.hasEquipState(parent)){
//TODO:fix hasWeapon
ClientEquipState equipState = ClientEquipState.getEquipState(parent);
// if(equipState.hasEquipPrimary()){
// rVal = true;
// }
}
return rVal;
}
boolean weaponInRange(){
boolean rVal = false;
Vector3d position = EntityUtils.getPosition(parent);
Realm realm = Globals.realmManager.getEntityRealm(parent);
for(Entity current : DataCellSearchUtils.getEntitiesWithTagAroundLocation(realm,position,EntityTags.ITEM)){
if(current != parent && ItemUtils.isItem(current) && ItemUtils.isWeapon(current)){
Vector3d potentialTargetPosition = EntityUtils.getPosition(current);
if(position.distance(potentialTargetPosition) < weaponSeekRange){
target = current;
return true;
}
}
}
return rVal;
}
boolean weaponInPickupRange(){
Vector3d position = EntityUtils.getPosition(parent);
if(target != null){
Vector3d potentialTargetPosition = EntityUtils.getPosition(target);
if(position.distance(potentialTargetPosition) < pickupRange){
return true;
}
}
return false;
}
void pickupWeapon(){
if(ClientEquipState.hasEquipState(parent)){
ClientEquipState equipState = ClientEquipState.getEquipState(parent);
// if(!equipState.hasEquipPrimary()){
// equipState.attemptEquip(target);
// }
}
}
void faceTarget(){
Vector3d position = EntityUtils.getPosition(parent);
Vector3d targetPosition = EntityUtils.getPosition(target);
Vector3d movementVector = new Vector3d(targetPosition).sub(position).normalize();
Quaterniond movementQuaternion = new Quaterniond().rotationTo(MathUtils.getOriginVector(), new Vector3d(movementVector.x,0,movementVector.z)).normalize();
CreatureUtils.setFacingVector(parent, movementVector);
EntityUtils.getRotation(parent).set(movementQuaternion);
}
void setGoal(OpportunisticAttackerGoal goal){
this.goal = goal;
switch(goal){
case IDLE:
// System.out.println("Set goal idle");
break;
case FLEE:
// System.out.println("Set goal flee");
break;
case SEEK_WEAPON:
// System.out.println("Set goal seek weapon");
break;
case ATTACK:
// System.out.println("Set goal attack");
break;
}
}
public static AITree construct(Entity parent) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'construct'");
}
@Override
public void updateStateAndPriority() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'updateStateAndPriority'");
}
@Override
public String getCurrentStateName() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'getCurrentStateName'");
}
@Override
public int getCurrentStatePriority() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'getCurrentStatePriority'");
}
}

View File

@ -0,0 +1,37 @@
package electrosphere.server.ai.nodes.combat;
import electrosphere.entity.Entity;
import electrosphere.entity.state.block.ServerBlockTree;
import electrosphere.server.ai.AITreeNode;
import electrosphere.server.ai.Blackboard;
/**
* Attempts to block
*/
public class BlockNode implements AITreeNode {
/**
* The next node to execute
*/
AITreeNode next = null;
/**
* Constructor
* @param next (Optional) The next node to execute
*/
public BlockNode(AITreeNode next){
this.next = next;
}
@Override
public AITreeNode evaluate(Entity entity, Blackboard blackboard) {
if(ServerBlockTree.getServerBlockTree(entity) != null){
ServerBlockTree serverBlockTree = ServerBlockTree.getServerBlockTree(entity);
if(serverBlockTree.isIdle()){
serverBlockTree.start();
}
}
return next;
}
}

View File

@ -0,0 +1,60 @@
package electrosphere.server.ai.nodes.combat;
import org.joml.Vector3d;
import electrosphere.engine.Globals;
import electrosphere.entity.Entity;
import electrosphere.entity.EntityTags;
import electrosphere.entity.EntityUtils;
import electrosphere.game.data.creature.type.ai.AttackerTreeData;
import electrosphere.server.ai.AITreeNode;
import electrosphere.server.ai.Blackboard;
import electrosphere.server.datacell.Realm;
import electrosphere.server.datacell.utils.DataCellSearchUtils;
/**
* BTree node to seek targets for melee attacks
*/
public class MeleeTargetingNode implements AITreeNode {
/**
* The next node to execute
*/
AITreeNode next = null;
/**
* The attacker tree data
*/
AttackerTreeData treeData;
/**
* Constructor
* @param next (Optional) The next node to execute
*/
public MeleeTargetingNode(AITreeNode next){
this.next = next;
}
@Override
public AITreeNode evaluate(Entity entity, Blackboard blackboard) {
throw new UnsupportedOperationException("Unimplemented!");
}
/**
* Searches for a valid target
*/
private void searchForTarget(Entity parent){
Vector3d position = EntityUtils.getPosition(parent);
Realm realm = Globals.realmManager.getEntityRealm(parent);
for(Entity current : DataCellSearchUtils.getEntitiesWithTagAroundLocation(realm,position,EntityTags.LIFE_STATE)){
if(current != parent){
Vector3d potentialTargetPosition = EntityUtils.getPosition(current);
if(position.distance(potentialTargetPosition) < treeData.getAggroRange()){
// target = current;
break;
}
}
}
}
}

View File

@ -0,0 +1,84 @@
package electrosphere.server.ai.nodes.movement;
import org.joml.Vector3d;
import electrosphere.entity.Entity;
import electrosphere.entity.EntityUtils;
import electrosphere.entity.state.movement.groundmove.ClientGroundMovementTree;
import electrosphere.entity.state.movement.groundmove.ClientGroundMovementTree.MovementRelativeFacing;
import electrosphere.entity.state.movement.groundmove.ServerGroundMovementTree;
import electrosphere.entity.types.creature.CreatureUtils;
import electrosphere.game.data.creature.type.ai.AttackerTreeData;
import electrosphere.server.ai.AITreeNode;
import electrosphere.server.ai.Blackboard;
/**
* Moves the ai into optimial positioning for melee combat
*/
public class MeleeMovement implements AITreeNode {
/**
* The next node to execute
*/
AITreeNode next = null;
/**
* The attacker tree data
*/
AttackerTreeData treeData;
/**
* Constructor
* @param next (Optional) The next node to execute
*/
public MeleeMovement(AITreeNode next){
this.next = next;
}
@Override
public AITreeNode evaluate(Entity entity, Blackboard blackboard) {
throw new UnsupportedOperationException("Unimplemented!");
}
/**
* Checks if the target is within the aggression range
*/
private boolean inAggroRange(Entity parent, Entity target){
boolean rVal = false;
Vector3d position = EntityUtils.getPosition(parent);
Vector3d targetPosition = EntityUtils.getPosition(target);
if(new Vector3d(position).distance(targetPosition) < treeData.getAggroRange()){
rVal = true;
}
return rVal;
}
/**
* Checks if the target is within range of an attack animation
* @return
*/
private boolean inAttackRange(Entity parent, Entity target){
boolean rVal = false;
Vector3d position = EntityUtils.getPosition(parent);
Vector3d targetPosition = EntityUtils.getPosition(target);
if(new Vector3d(position).distance(targetPosition) < treeData.getAttackRange()){
rVal = true;
}
return rVal;
}
/**
* Attempts to move towards the target
*/
private void moveToTarget(Entity parent, Entity target){
Vector3d targetPosition = EntityUtils.getPosition(target);
Vector3d characterPosition = EntityUtils.getPosition(parent);
Vector3d moveVector = new Vector3d(targetPosition).sub(characterPosition).normalize();
CreatureUtils.setFacingVector(parent, new Vector3d((float)moveVector.x,(float)moveVector.y,(float)moveVector.z));
ServerGroundMovementTree characterMoveTree = (ServerGroundMovementTree)CreatureUtils.serverGetEntityMovementTree(parent);
if(characterMoveTree.getState()==ClientGroundMovementTree.MovementTreeState.IDLE || characterMoveTree.getState()==ClientGroundMovementTree.MovementTreeState.SLOWDOWN){
characterMoveTree.start(MovementRelativeFacing.FORWARD);
}
}
}

View File

@ -0,0 +1,24 @@
package electrosphere.server.ai.trees.creature;
import electrosphere.server.ai.AITreeNode;
import electrosphere.server.ai.nodes.combat.BlockNode;
/**
* Figures out how if it should attack and how it can, then executes that subtree
*/
public class AttackerAITree {
/**
* Name of the tree
*/
public static final String TREE_NAME = "Attacker";
/**
* Creates an attacker ai tree
* @return The root node of the tree
*/
public static AITreeNode create(){
return new BlockNode(null);
}
}

View File

@ -0,0 +1,25 @@
package electrosphere.server.ai.trees.test;
import electrosphere.game.data.creature.type.ai.BlockerTreeData;
import electrosphere.server.ai.AITreeNode;
import electrosphere.server.ai.nodes.combat.BlockNode;
/**
* Creates a blocker test tree
*/
public class BlockerAITree {
/**
* Name of the tree
*/
public static final String TREE_NAME = "Blocker";
/**
* Creates a blocker tree
* @return The root node of the tree
*/
public static AITreeNode create(BlockerTreeData data){
return new BlockNode(null);
}
}