Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to move objects from node to node? Libgdx Box2d A*Pathfinding

I can't succeed in making the enemies move from node to node.

I managed to set up the whole pathfinding, I'm getting a full path to the player and the position of the next node all the way to the end.

How do I move the box2d body from node to node?

Or even simpler - how do I move a box2d body to a certain position? I tried applying impulses, forces weren't able to do it.

Here is my simplified code. Both Monster and Player classes extend Sprite:

public class B2dSteeringEntity implements Steerable<Vector2>, Updateable {

public static final String TAG = B2dSteeringEntity.class.getName();

    Body body;
    boolean tagged;
    float maxLinearSpeed, maxLinearAcceleration;
    float maxAngularSpeed, maxAngularAcceleration;
    float boundingRadius;

SteeringBehavior<Vector2> behavior;
SteeringAcceleration<Vector2> steeringOutput;

public B2dSteeringEntity(Body body, float boundingRadius) {
    this.body = body;
    this.boundingRadius = boundingRadius;

    this.maxLinearSpeed = 250;
    this.maxLinearAcceleration = 200;
    this.maxAngularSpeed = 0;
    this.maxAngularAcceleration = 0;

    this.tagged = false;

    this.steeringOutput = new SteeringAcceleration<Vector2>(new Vector2());
}

@Override
public Vector2 getLinearVelocity() {
    return body.getLinearVelocity();
}

@Override
public float getAngularVelocity() {
    return body.getAngularVelocity();
}

@Override
public float getBoundingRadius() {
    return 0;
}

@Override
public boolean isTagged() {
    return tagged;
}

@Override
public void setTagged(boolean tagged) {
    this.tagged = tagged;
}

@Override
public float getZeroLinearSpeedThreshold() {
    return 0;
}

@Override
public void setZeroLinearSpeedThreshold(float value) {

}

@Override
public float getMaxLinearSpeed() {
    return maxLinearSpeed;
}

@Override
public void setMaxLinearSpeed(float maxLinearSpeed) {
    this.maxLinearSpeed = maxLinearSpeed;
}

@Override
public float getMaxLinearAcceleration() {
    return maxLinearAcceleration;
}

@Override
public void setMaxLinearAcceleration(float maxLinearAcceleration) {
    this.maxLinearAcceleration = maxLinearAcceleration;
}

@Override
public float getMaxAngularSpeed() {
    return maxAngularSpeed;
}

@Override
public void setMaxAngularSpeed(float maxAngularSpeed) {
    this.maxAngularSpeed = maxAngularSpeed;
}

@Override
public float getMaxAngularAcceleration() {
    return maxAngularAcceleration;
}

@Override
public void setMaxAngularAcceleration(float maxAngularAcceleration) {
    this.maxAngularAcceleration = maxAngularAcceleration;
}

@Override
public Vector2 getPosition() {
    return body.getPosition();
}

@Override
public float getOrientation() {
    return body.getAngle();
}

@Override
public void setOrientation(float orientation) {

}

@Override
public float vectorToAngle(Vector2 vector) {
    return SteeringUtils.vectorToAngle(vector);
}

@Override
public Vector2 angleToVector(Vector2 outVector, float angle) {
    return SteeringUtils.angleToVector(outVector, angle);
}

@Override
public Location<Vector2> newLocation() {
    return null;
}

public Body getBody() {
    return body;
}

public void setBehavior(SteeringBehavior<Vector2> behavior) {
    this.behavior = behavior;
}

public SteeringBehavior<Vector2> getBehavior() {
    return behavior;
}

private void applySteering(float deltaTime) {
    boolean anyAccelerations = false;

    if(!steeringOutput.linear.isZero()) {
        Vector2 force = steeringOutput.linear.scl(deltaTime);
        body.applyForceToCenter(force, true);
        anyAccelerations = true;
    }
    
    if(anyAccelerations) {
        Vector2 velocity = body.getLinearVelocity();
        float currentSpeedSquare = velocity.len2();
        if(currentSpeedSquare > maxLinearSpeed * maxLinearSpeed) {
            body.setLinearVelocity(velocity.scl(maxLinearSpeed / (float) Math.sqrt(currentSpeedSquare)));
        }
        
        if (body.getAngularVelocity() > maxAngularSpeed) {
            body.setAngularVelocity(maxAngularSpeed);
        }
    }
}

@Override
public void update(float deltaTime) {
    if (behavior != null) {
        behavior.calculateSteering(steeringOutput);
        applySteering(deltaTime);
    }
}

}

public Skeleton(B2WorldCreator creator, GameScreen screen, float x, float y, State state, HeroKnight player) {
    super(creator, screen, x, y, state, player);
    controller = screen.getController();
    setBounds(x, y, 150 / Constants.PPM, 150 / Constants.PPM);
    enemyAgentComponent = new EnemyAgentComponent(b2body, b2dSteeringEntity, controller, player);

}

And here is the EnemyAgentComponent class:

public class EnemyAgentComponent implements Component, Telegraph, Updateable, Pather<Node> {

public static final String TAG = EnemyAgentComponent.class.getName();
public StateMachine<EnemyAgentComponent, EnemyState> stateMachine;

public boolean isInProximity() {
    return inProximity;
}

public boolean inProximity = false;
public boolean lowHP = false;
public boolean isShot = false;
private boolean isRequested = false;
private boolean requestingMovement;

private Steering wayPoint;
private Body body;
private Controller controller;

private HeroKnight player;
private Node startNode, endNode, previousStartNode, previousEndNode;
private Vector2 goal;
private PathfindingTarget newGoal;
private float impulseMag;

private boolean nodeReached;
private boolean pathGenerated;
private Arrive<Vector2> arriveSB;
private Steering waypoint;
private B2dSteeringEntity steering;
private IndexedAStarPathFinder<Node> pathFinder;
private GraphPathImp resultPath = new GraphPathImp();
private float pathfindingTimer;
private boolean firstPathGenerated;

public boolean isTouchingPlayer() {
    return touchingPlayer;
}

public void setTouchingPlayer(boolean touchingPlayer) {
    this.touchingPlayer = touchingPlayer;
}

private boolean touchingPlayer;

public EnemyAgentComponent(Body body, B2dSteeringEntity steering, Controller controller, HeroKnight player) {
    construct(body, steering, controller, player);
}

public void construct(Body body, B2dSteeringEntity steering, Controller controller, HeroKnight player) {
    this.steering = steering;
    this.controller = controller;
    this.body = body;
    this.player = player;
    stateMachine = new DefaultStateMachine<>(this, EnemyState.SEEKING);
    MessageManager.getInstance().addListener(this, Messages.PLAYER_IN_SIGHT);
    MessageManager.getInstance().addListener(this, Messages.PLAYER_ATTACKED_ENEMY);
    MessageManager.getInstance().addListener(this, Messages.LOW_HP);
    MessageManager.getInstance().addListener(this, Messages.PLAYER_OUT_OF_SIGHT);
    MessageManager.getInstance().addListener(this, Messages.TOUCHING_PLAYER);

    pathFinder = new IndexedAStarPathFinder<Node>(LevelManager.groundGraph, false);
    requestingMovement = false;
    goal = null;
    nodeReached = false;
    pathGenerated = false;
    firstPathGenerated = false;
    pathfindingTimer = 0;
    touchingPlayer = false;

}

public Vector2 getGoal() {
    return goal;
}

public boolean isRequestingMovement() {
    return requestingMovement;
}

public void setRequestingMovement(boolean requestingMovement) {
    this.requestingMovement = requestingMovement;
}

public boolean isPathGenerated() {
    return pathGenerated;
}

public GraphPathImp getResultPath() {
    return resultPath;
}

public void generatePath() {
    previousEndNode = endNode;
    previousStartNode = startNode;
    resultPath.clear();
    pathFinder.searchNodePath(startNode, endNode, new HeuristicImp(), resultPath);
    newGoal = new PathfindingTarget(resultPath.get(0).getNodePosition());
    pathGenerated = true;
}

public void update(float deltaTime) {
    startNode = LevelManager.groundGraph.getNodeByXY(body.getPosition().x, body.getPosition().y);
    endNode = LevelManager.groundGraph.getNodeByXY(player.b2body.getPosition().x, player.b2body.getPosition().y);
    //If player gets in certain range of the enemy and is not touching the enemy, enemy's path is being generated
    if (inProximity) {
        if (!firstPathGenerated && !touchingPlayer)
            if ((pathfindingTimer == 0)) {
                generatePath();
                previousStartNode = startNode;
                previousEndNode = endNode;
                firstPathGenerated = true;
            }

        //If a path was already created, a new path is being requested only if player's position changes
        if (firstPathGenerated && (previousEndNode != endNode) && pathfindingTimer == 0 && !touchingPlayer) {
            generatePath();
        }
        if (firstPathGenerated)
            pathfindingTimer += deltaTime;
        //Paths are generated every 2 seconds
        if (pathfindingTimer >= 2) {
            pathfindingTimer = 0;
        }
        //If enemy touches the player pathfinding ends
        if (touchingPlayer) {
            pathfindingTimer = 0;
            pathGenerated = false;
            resultPath.clear();
            body.setLinearVelocity(0, 0);
        }
    }
    //The arrive behaviour is set, newGoal being the position of next node
    if (steering.getLinearVelocity().x == 0 && steering.getLinearVelocity().y == 0 && newGoal != null) {
            steering.setBehavior(new Arrive<Vector2>(steering, newGoal));
    }

    steering.update(deltaTime);
    //Updating the next node position based on the enemy reaching a node
    if (pathGenerated)
        if (Math.abs(body.getPosition().x - newGoal.getPosition().x) <= 0.1f && Math.abs(body.getPosition().y - newGoal.getPosition().y) <= 51 / 2f / Constants.PPM)
        {
            updatePath();
    }
}


public void updatePath() {
    //Setting the next target position
    if (resultPath.getCount() > 0) {
        resultPath.removeIndex(0);
        if (!touchingPlayer && resultPath.getCount() > 0) {
            newGoal.setPosition(resultPath.get(0).getNodePosition());
            requestingMovement = true;
            nodeReached = false;
        }
    }
}


@Override
public boolean handleMessage(Telegram telegram) {
    if (telegram.message == Messages.PLAYER_IN_SIGHT) {
        inProximity = true;

        return true;
    }
    if (telegram.message == Messages.PLAYER_OUT_OF_SIGHT) {
        inProximity = false;
        firstPathGenerated = false;
        pathGenerated = false;
        pathfindingTimer = 0;
        resultPath.clear();
    }
    if (telegram.message == Messages.LOW_HP) {
        lowHP = true;

        return true;
    }
    if (telegram.message == Messages.PLAYER_ATTACKED_ENEMY) {
        isShot = true;

        return true;
    }
    if (telegram.message == Messages.TOUCHING_PLAYER) {
        touchingPlayer = true;
        return true;
    }

    return false;
}


@Override
public void acceptPath(PathFinderRequest<Node> request) {
    if (request.pathFound) {
        resultPath = (GraphPathImp) request.resultPath;
    }
}

}

Skeleton class is being updated and drawn using skeleton.update(deltaTime) and skeleton.draw(deltaTime).

EDIT2: Now tried using linear impulses, movement is very clunky:

if (newGoal != null) {
        Vector2 direction = new Vector2(newGoal.getPosition().x - body.getPosition().x, newGoal.getPosition().y - body.getPosition().y).nor();
        float speed = Constants.SKELETON_SPEED;
        Vector2 velocity = new Vector2(speed * direction.x, speed * direction.y);
        body.applyLinearImpulse(velocity.x * body.getMass(), velocity.y * body.getMass(), body.getWorldCenter().x, body.getWorldCenter().y, true);
    }
if (pathGenerated)
        if (Math.abs(body.getPosition().x - newGoal.getPosition().x) <= 0.1f && Math.abs(body.getPosition().y - newGoal.getPosition().y) <= 51 / 2f / Constants.PPM)
        {
            Gdx.app.debug(TAG, "Node reached!");
            updatePath();
    }
like image 866
Deso2121 Avatar asked Jul 02 '20 12:07

Deso2121


1 Answers

Moving an entity is not directly related to AI. The AI just finds a way, the entity follows it.

With box2d, you have to apply forces or impulses into the right directions, make sure your box2d world's step() method is being called and update your entites rendering coordinates (e.g. the sprite) to match the box2d's body coordinates. Then call your AI from the new coordinates in the next loop again, determine in which direction you have to move now and apply your forces again.

So the loop looks like

  1. Find a way via AI
  2. Find direction to the next node from your position
  3. Apply forces/impulses
  4. box2d world step
  5. Adapt the entities rendering coordinates to match the box2d body position
  6. Repeat

Giving you a more detailed answer requires more knowledge about how you render your entities and what your loop looks like. Please share some example code.

Edit: After you added some code I find it still unclear what you actual problem is. Can you move your entity but fail to stop it? Does it move at all? Do you call world.step()? The only obvious issue I could find after a quick look is that you scale applyForceToCenter with deltaTime which is not needed as world.step already takes a timeStep as a parameter.

like image 171
Sebastian Avatar answered Oct 23 '22 11:10

Sebastian