Skip to content

Commit

Permalink
[GAME] Add Ai-System (#128)
Browse files Browse the repository at this point in the history
* add Ai-System with FightAI and IdleAi and some walk implementations

* rename interfaces

* stop using hero.getPositionComponent

* set Velcoity to 0,0 after movement

* add doc to AITools

* javadoc and some code opt

* rebase

* add dsl annoations

* remove test monster

* add aicomponent to dsl input

* Workaround for AIComponent type

* Decrease monster velocity

* spotless apply

Co-authored-by: mr <[email protected]>
  • Loading branch information
AMatutat and mr authored Jan 23, 2023
1 parent 3628881 commit 4bd9f74
Show file tree
Hide file tree
Showing 16 changed files with 384 additions and 6 deletions.
4 changes: 4 additions & 0 deletions dsl/src/runtime/GameEnvironment.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import ecs.components.AnimationComponent;
import ecs.components.PositionComponent;
import ecs.components.VelocityComponent;
import ecs.components.ai.AIComponent;
import ecs.entities.Entity;
import java.util.ArrayList;
import java.util.HashMap;
Expand Down Expand Up @@ -122,11 +123,14 @@ private static ArrayList<IType> buildBuiltInTypes() {
typeBuilder.createTypeFromClass(Scope.NULL, AnimationComponent.class);
var velocityComponentType =
typeBuilder.createTypeFromClass(Scope.NULL, VelocityComponent.class);
var aiComponentType = typeBuilder.createTypeFromClass(Scope.NULL, AIComponent.class);
types.add(questConfigType);
types.add(entityComponentType);
types.add(positionComponentType);
types.add(animationComponentType);
types.add(velocityComponentType);
types.add(aiComponentType);

return types;
}

Expand Down
55 changes: 55 additions & 0 deletions game/src/ecs/components/ai/AIComponent.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package ecs.components.ai;

import ecs.components.Component;
import ecs.components.ai.fight.IFightAI;
import ecs.components.ai.idle.IIdleAI;
import ecs.components.ai.idle.RadiusWalk;
import ecs.components.ai.transition.ITransition;
import ecs.components.ai.transition.RangeTransition;
import ecs.entities.Entity;
import semanticAnalysis.types.DSLContextMember;
import semanticAnalysis.types.DSLType;

/** AIComponent is a component that stores the idle and combat behavior of AI controlled entities */
@DSLType(name = "ai_component")
public class AIComponent extends Component {

public static String name = "AIComponent";
private /*@DSLTypeMember*/ IFightAI fightAI;
private /*@DSLTypeMember*/ IIdleAI idleAI;
private /*@DSLTypeMember*/ ITransition transition;

/**
* @param entity associated entity
* @param fightAI combat behavior
* @param idleAI idle behavior
* @param transition Determines when to fight
*/
public AIComponent(Entity entity, IFightAI fightAI, IIdleAI idleAI, ITransition transition) {
super(entity, name);
this.fightAI = fightAI;
this.idleAI = idleAI;
this.transition = transition;
}

/**
* @param entity associated entity
*/
public AIComponent(@DSLContextMember(name = "entity") Entity entity) {
super(entity, name);
System.out.println("DEBUG AI");
idleAI = new RadiusWalk(5);
transition = new RangeTransition(1.5f);
fightAI =
entity1 -> {
System.out.println("TIME TO FIGHT!");
// todo replace with melee skill
};
}

/** Excecute the ai behavior */
public void execute() {
if (transition.isInFightMode(entity)) fightAI.fight(entity);
else idleAI.idle(entity);
}
}
133 changes: 133 additions & 0 deletions game/src/ecs/components/ai/AITools.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package ecs.components.ai;

import com.badlogic.gdx.ai.pfa.GraphPath;
import ecs.components.PositionComponent;
import ecs.components.VelocityComponent;
import ecs.entities.Entity;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Random;
import level.elements.ILevel;
import level.elements.tile.Tile;
import level.tools.Coordinate;
import mydungeon.ECS;
import tools.Point;

public class AITools {
private static final Random random = new Random();

/**
* Finds the path to a random (accessible) tile in the given radius, starting from the position
* of the given entity.
*
* @param entity Entity whose position is the center point
* @param radius Search radius
* @return Path from the position of the entity to the randomly selected tile
*/
public static GraphPath<Tile> calculateNewPath(Entity entity, float radius) {
PositionComponent pc = (PositionComponent) entity.getComponent(PositionComponent.name);
VelocityComponent vc = (VelocityComponent) entity.getComponent(VelocityComponent.name);
if (pc != null && vc != null) {
ILevel level = ECS.currentLevel;
Point position = pc.getPosition();
List<Tile> tiles = new ArrayList<>();
for (float x = position.x - radius; x <= position.x + radius; x++) {
for (float y = position.y - radius; y <= position.y + radius; y++) {
tiles.add(level.getTileAt(new Point(x, y).toCoordinate()));
}
}
tiles.removeIf(Objects::isNull);
tiles.removeIf(tile -> !tile.isAccessible());
Coordinate newPosition = tiles.get(random.nextInt(tiles.size())).getCoordinate();
return level.findPath(
level.getTileAt(position.toCoordinate()), level.getTileAt(newPosition));
}
return null;
}

/**
* Finds the path from the position of one entity to the position of another entity.
*
* @param from Entity whose position is the start point
* @param to Entity whose position is the goal point
* @return Path
*/
public static GraphPath<Tile> calculateNewPath(Entity from, Entity to) {
PositionComponent myPositionComponent =
(PositionComponent) from.getComponent(PositionComponent.name);
PositionComponent heroPositionComponent =
(PositionComponent) to.getComponent(PositionComponent.name);
if (myPositionComponent != null && heroPositionComponent != null) {
ILevel level = ECS.currentLevel;
Coordinate myPosition = myPositionComponent.getPosition().toCoordinate();
Coordinate heroposition = heroPositionComponent.getPosition().toCoordinate();
return level.findPath(level.getTileAt(myPosition), level.getTileAt(heroposition));
}
return null;
}

/**
* Sets the velocity of the passed entity so that it takes the next necessary step to get to the
* end of the path.
*
* @param entity Entity moving on the path
* @param path Path on which the entity moves
*/
public static void move(Entity entity, GraphPath<Tile> path) {
PositionComponent pc = (PositionComponent) entity.getComponent(PositionComponent.name);
VelocityComponent vc = (VelocityComponent) entity.getComponent(VelocityComponent.name);
ILevel level = ECS.currentLevel;
Tile currentTile = level.getTileAt(pc.getPosition().toCoordinate());
int i = 0;
Tile nextTile = null;
do {
if (i >= path.getCount()) return;
if (path.get(i).equals(currentTile)) {
nextTile = path.get(i + 1);
}
i++;
} while (nextTile == null);

switch (currentTile.directionTo(nextTile)[0]) {
case N -> vc.setY(vc.getySpeed());
case S -> vc.setY(-vc.getySpeed());
case E -> vc.setX(vc.getxSpeed());
case W -> vc.setX(-vc.getxSpeed());
}
if (currentTile.directionTo(nextTile).length > 1)
switch (currentTile.directionTo(nextTile)[1]) {
case N -> vc.setY(vc.getySpeed());
case S -> vc.setY(-vc.getySpeed());
case E -> vc.setX(vc.getxSpeed());
case W -> vc.setX(-vc.getxSpeed());
}
}

/**
* Checks if the position of the player is within the given radius of the position of the given
* entity.
*
* @param entity Entity whose position specifies the center point
* @param range Reichweite die betrachtet werden soll
* @return Ob sich der Spieler in Reichweite befindet
*/
public static boolean playerInRange(Entity entity, float range) {
PositionComponent myPositionComponent =
(PositionComponent) entity.getComponent(PositionComponent.name);
if (ECS.hero != null) {
PositionComponent heroPositionComponent =
(PositionComponent) ECS.hero.getComponent(PositionComponent.name);
if (heroPositionComponent != null) {
Point myPosition = myPositionComponent.getPosition();
Point heroPosition = heroPositionComponent.getPosition();

float xDiff = myPosition.x - heroPosition.x;
float yDiff = myPosition.y - heroPosition.y;
float distance = (float) Math.sqrt(xDiff * xDiff + yDiff * yDiff);
return distance <= range;
}
}
return false;
}
}
13 changes: 13 additions & 0 deletions game/src/ecs/components/ai/fight/IFightAI.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package ecs.components.ai.fight;

import ecs.entities.Entity;

public interface IFightAI {

/**
* Implements the combat behavior of an AI controlled entity
*
* @param entity associated entity
*/
void fight(Entity entity);
}
48 changes: 48 additions & 0 deletions game/src/ecs/components/ai/fight/MeleeAI.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package ecs.components.ai.fight;

import com.badlogic.gdx.ai.pfa.GraphPath;
import ecs.components.ai.AITools;
import ecs.components.skill.Skill;
import ecs.entities.Entity;
import java.lang.reflect.InvocationTargetException;
import level.elements.tile.Tile;
import mydungeon.ECS;
import tools.Constants;

public class MeleeAI implements IFightAI {
private final float attackRange;
private final int delay = Constants.FRAME_RATE;
private int timeSinceLastUpdate = 0;
private final Skill fightSkill;
private GraphPath<Tile> path;

/**
* Attacks the player if he is within the given range. Otherwise, it will move towards the
* player.
*
* @param attackRange Range in which the attack skill should be executed
* @param fightSkill Skill to be used when an attack is performed
*/
public MeleeAI(float attackRange, Skill fightSkill) {
this.attackRange = attackRange;
this.fightSkill = fightSkill;
}

@Override
public void fight(Entity entity) {
if (AITools.playerInRange(entity, attackRange)) {
try {
fightSkill.execute(entity);
} catch (InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException(e);
}
} else {
if (timeSinceLastUpdate >= delay) {
path = AITools.calculateNewPath(entity, ECS.hero);
timeSinceLastUpdate = -1;
}
timeSinceLastUpdate++;
AITools.move(entity, path);
}
}
}
13 changes: 13 additions & 0 deletions game/src/ecs/components/ai/idle/IIdleAI.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package ecs.components.ai.idle;

import ecs.entities.Entity;

public interface IIdleAI {

/**
* Implements the idle behavior of an AI controlled entity
*
* @param entity associated entity
*/
void idle(Entity entity);
}
48 changes: 48 additions & 0 deletions game/src/ecs/components/ai/idle/RadiusWalk.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package ecs.components.ai.idle;

import com.badlogic.gdx.ai.pfa.GraphPath;
import ecs.components.PositionComponent;
import ecs.components.ai.AITools;
import ecs.entities.Entity;
import level.elements.ILevel;
import level.elements.tile.Tile;
import mydungeon.ECS;
import tools.Constants;

public class RadiusWalk implements IIdleAI {
private final float radius;
private GraphPath<Tile> path;
private final int breakTime = Constants.FRAME_RATE * 5;
private int currentBreak = 0;

/**
* Finds a point in the radius and then moves there. When the point has been reached, a new
* point in the radius is searched for from there.
*
* @param radius Radius in which a target point is to be searched for
*/
public RadiusWalk(float radius) {
this.radius = radius;
}

@Override
public void idle(Entity entity) {
if (path == null || pathFinished(entity)) {
if (currentBreak >= breakTime) {
currentBreak = 0;
path = AITools.calculateNewPath(entity, radius);
idle(entity);
}

currentBreak++;

} else AITools.move(entity, path);
}

private boolean pathFinished(Entity entity) {
PositionComponent pc = (PositionComponent) entity.getComponent(PositionComponent.name);
ILevel level = ECS.currentLevel;
return path.get(path.getCount() - 1)
.equals(level.getTileAt(pc.getPosition().toCoordinate()));
}
}
15 changes: 15 additions & 0 deletions game/src/ecs/components/ai/transition/ITransition.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package ecs.components.ai.transition;

import ecs.entities.Entity;

/** Determines when an ai switches between idle and fight */
public interface ITransition {

/**
* Function that determines whether an entity should be in combat mode
*
* @param entity associated entity
* @return if the entity should fight
*/
boolean isInFightMode(Entity entity);
}
23 changes: 23 additions & 0 deletions game/src/ecs/components/ai/transition/RangeTransition.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package ecs.components.ai.transition;

import ecs.components.ai.AITools;
import ecs.entities.Entity;

public class RangeTransition implements ITransition {

private final float range;

/**
* Switches to combat mode when the player is within range of the entity.
*
* @param range Range of the entity.
*/
public RangeTransition(float range) {
this.range = range;
}

@Override
public boolean isInFightMode(Entity entity) {
return AITools.playerInRange(entity, range);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package ecs.components;
package ecs.components.skill;

import graphic.Animation;
import java.lang.reflect.InvocationTargetException;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package ecs.components;
package ecs.components.skill;

import ecs.components.Component;
import ecs.entities.Entity;
import java.util.HashSet;
import java.util.Set;
Expand Down
Loading

0 comments on commit 4bd9f74

Please sign in to comment.