Project with 800 agents

I am doing a project that needs to have a lot of people (Agent Objects) in the screen and they need to move towards an objective.

For creating a new Agent Object I use:

bot1 = new Agent(x,y,z,assetManager, rootNode, bulletAppState, navMesh);

Where x,y,z are inicial position. assetManaget, rootNode, bulletAppState need to be inside the construtor, that way I can import model to world:

playerNode = (Node) a.loadModel(“Models/Agent.j3o”);
world.attachChild(playerNode); //load model to world

  1. I think this is not the best way to do it, because I will need to import model each time I create an object, but after trying to declare playerNode inside main and put it in Agent constructor, it only shows the first Agent model, every others are invisible. Is like I can’t have a different instance objects of the same variable

  2. This happens too with the NavMeshPathfinder variable, if I create a NavMesh in main and put it in the Agent construtor, and make a NavMeshFinder inside the Agent class, after use the .ComputePath() function in one Agent, all others agents will have the same path, dont know why.

  3. Lastly in terms of perfomance and frames, I think call the class method update, inside the SimpleUpdate() is not a good idea, right? What is the best I can do?

Edit:
Agent Code:

public class Agent extends SimpleApplication implements AnimEventListener{
//agent variables
double posX, posY, posZ;
double direction;
double vel;

//nodes for load models
Node world;
Node playerNode;

//animation variables
AnimControl controlAnim;
AnimChannel channelAnim;

//better control
BetterCharacterControl agentControl;
BulletAppState bullet;

//Pathfinder
NavMeshPathfinder navi;

Agent(double x, double y, double z, double d, AssetManager a, Node n, BulletAppState b, NavMesh navM)
{
    posX = x;
    posY = y;
    posZ = z;
    direction = d;
    vel = 0; //for now
    
    //load model
    playerNode = (Node) a.loadModel("Models/Agent.j3o");
    playerNode.move( (float) x, (float) y, (float) z);
    world = n;
    
    
    //load animations
    controlAnim = playerNode.getChild("Cube.001").getControl(AnimControl.class);
    controlAnim.addListener(this);
    channelAnim = controlAnim.createChannel();
    
    //maybe need to inicialize on stand
    channelAnim.setAnim("Walk"); //or "Stand"
    
    agentControl = new BetterCharacterControl(0.5f, 1.8f, 1); //radius , heiht, mass
    playerNode.addControl(agentControl);
    
    // set basic physical properties:
    //agentControl.setJumpForce(new Vector3f(0,5f,0));
    agentControl.setGravity(new Vector3f(0,1,0));
    agentControl.warp(new Vector3f((float)x, (float)y, (float)z)); // warp character into landscape at particular location
    // add to physics state
    bullet = b;
    bullet.getPhysicsSpace().add(agentControl);
    bullet.getPhysicsSpace().addAll(playerNode);

    world.attachChild(playerNode); //load model to world
    
    //pathfinder
    navi = new NavMeshPathfinder(navM);
    navi.setEntityRadius(0.5f);
    //agentControl.setCcdMotionThreshold(0.15f);
    navi.setPosition(new Vector3f((float)x, (float)y, (float)z));
    navi.computePath(new Vector3f(0f,0f,0f));
   

}

public void update(float tpf)
{
    //update position based on navmesh and others agents
    navi.setPosition(playerNode.getWorldTranslation());
    //System.out.println(navi.getDirectionToWaypoint());
    
    if(navi.getDistanceToWaypoint() < 0.25f) //TODO change maybe
    {
        if(!navi.isAtGoalWaypoint()) //if it is not at last waypoint
        {
            navi.goToNextWaypoint();
        }
        else
        {
            System.out.print("End");
        }
    }     
    agentControl.setWalkDirection(navi.getDirectionToWaypoint().mult(5)); //time per frame times 1m/s -> 3,6 km/h  
}

Main:

public void simpleInitApp() {

//load building plant
//Spatial plant = assetManager.loadModel("Models/Ed7/Ed7.j3o");
Node plant = (Node) assetManager.loadModel("Models/EdPlaceHolder/Ed.PlaceHolder.j3o");
    rootNode.attachChild(plant); 
    //plant.scale(4f,4f,4f);
    
    
//Load world physics
bulletAppState = new BulletAppState();
stateManager.attach(bulletAppState);

CollisionShape plantShape =
        CollisionShapeFactory.createMeshShape(plant);
landscape = new RigidBodyControl(plantShape, 0);
plant.addControl(landscape);

bulletAppState.getPhysicsSpace().add(landscape);

//Create pathfinder
Geometry navGeom = (Geometry) plant.getChild("Navmesh1"); //load navmesh inside plant object directory
Mesh mesh = navGeom.getMesh();
NavMesh navMesh = new NavMesh(mesh);

//create bots (testing for now)
bot1 = new Agent(20,30,75,1,assetManager, rootNode, bulletAppState, navMesh);
bot2 = new Agent(52,30,120,1,assetManager, rootNode, bulletAppState, navMesh);

}

@Override
public void simpleUpdate(float tpf) {
    bot1.update(tpf);
    bot2.update(tpf);
}

Thank you all!! :smile:

Showing more code would be helpful, but it sounds like you could have variables declared as static that shouldn’t be static, or your’e otherwise sharing variables between your agents instead of using a new instance for each agent.

This is how I manage up to 100 enemies at a time without any problems. Then you can put all the navigation and AI code inside the updateAgent().

 public void addToLiveMobs(Agent a) {
     liveMobs.add(a);
}

  @Override
 public void update(float tpf){
        for (int q = 0; q < liveMobs.size(); q++) {
            Agent ag = (Agent) liveMobs.get(q);
            ag.updateAgent(tpf);
        }
  }

Thank you for the reply, I just added the code in the question.
At the moment with two agents only, for tests, but will make a ArrayList later.

Extending SimpleApplication in your agent class may be causing the problem. Each time you create a new agent, your also creating a new SimpleApplication, the only class that should extend SimpleApplication is your main class.
You should be able to get this working correctly if you move all of the code pertaining to your agent into its own class rather than the class that extends SimpleApplication.

If you have a big class that isn’t your main class but it needs access to the update loop, then you can use the way I suggested with the ArrayList of agents that you sort through in the update loop (that’s my preferred way for many npcs), or you could also probably benefit from looking into how AppStates work if you haven’t already. Using App States is the correct way to extend the simple application like you were attempting
https://jmonkeyengine.github.io/wiki/jme3/advanced/application_states.html

1 Like

Removing the extends SimpleApplication didn solve the problem, the NavMeshPathfinder still got the same path for all agent objects :frowning: , can I somehow be sharing variables between agents?

I don’t use the NavMeshPathfinder for my project so I don’t know exactly how that should be setup to work, but it definitely seems like the agents are sharing some variable related to pathing.

Are you creating a seperate NavMesh for each agent since you changed your code? In the original code it looks like your’e also using the same navMesh object for both agents

 bot1 = new Agent(20,30,75,1,assetManager, rootNode, bulletAppState, navMesh);
 bot2 = new Agent(52,30,120,1,assetManager, rootNode, bulletAppState, navMesh);

you’d instead want to do this.

bot1 = new Agent(20,30,75,1,assetManager, rootNode, bulletAppState, new NavMesh(bot_1_mesh));
bot2 = new Agent(52,30,120,1,assetManager, rootNode, bulletAppState, new NavMesh(bot_2_mesh));

It’s okay for all your agents to share the same reference to variables like the assetManager or the bulletAppState and rootNode since all the agent’s are apart of the same physics space, but variables related to their navigation, or things like health and NPC stats should be unique to each agent

1 Like

Thank you very much, it worked.

Now I have a list of agents, want to have 800 agents, but the FPS droped to 3. At first I thought that calling 800 agents updates and calculate the distance to waypoint was the main cause, but then I only call the updates every 200 frames, and the FPS stayed at 3. So the main problem I think is the BetterCharacter walking thing, it needs to update all the agents every frame. Any suggestion?

Im working with detour-crowd and it updates 20-25 agents per frame vs updating 800 in one frame if I read your statement right.

Maybe try spreading the load out more? Just a guess.

Also, be sure to set the option on the bullet app state to run it in a separate thread.

@mitm
I am not loading 20-25 agents per frame, 800 every time.
How can I spread the load? BetterCharacterControl update the agent position automatic after I use the setWalkDirection method.

@Robbi_Blechdose
You are suggesting to change the bullet app state thread type to PARALLEL instead of SEQUENTIAL? I just tried it and the FPS didnt change.

From my experience with large enemy counts, changing the thread type didnt make much of a difference. If you have a lot of enemies, you’ll probably benefit from using or making a much lighter weight physics system, or attempting @mitm’s suggestionand see if that works. I think a combination of spreading out the load and using a lighter physics system would be the best solution if your looking to go close to the thousands in NPC count.

Hopefully someone will chime in and correct me if my solution could be improved, but the problem of loading hundreds of agents was the biggest dilemma with my game at first, and I solved it by creating a lightweight physics control for each Agent that only uses a single ray and a radius variable to check for overlapping NPCs. The results don’t look much different from the better character control, but a singificant difference is that you can walk through small enemies with my physics control.

I set my enemy count up to 400 to to test it out, and I’m getting 40 fps, so I’d imagine if you spread the load out over multiple frames then you could go even higher. I also use about 4-5 better character controls in my game. One for each player, and then certain large enemies that don’t spawn in large amounts also use a better character control

2 Likes

Nice awser!
So, there is any code out there allready done? if not, to do that I just use this methods (https://jmonkeyengine.github.io/wiki/jme3/advanced/physics.html) right?

Im quite certain that you’ll never get to your goal using the A* method. Iv watched a ton of this game including the devlogs and hes quite vocal on his achievements.

3 Likes

I’m not sure of any other code out there for lighter weight physics. My codes still sloppy and dependent on my project so I don’t think all of it would be of benefit to share, but if you’d like I could post the code I’m using to determine the NPC’s position based on a ray, mass, and radius when I have a chance.

I also didn’t think to mention how I’m doing my path finding. I’m using my own implementation of the Dijkstra method that uses a small set of pre-mapped pathing nodes and caches paths between two nodes. There’s definitely a lot of imperfections in the way I’m doing it, but for my game perfect pathing isn’t as crucial, so long as the enemy eventually finds the player some way or another.

if you could share the code, just for getting the idea would be very nice. By the way, I making a building evacuation simulator.

You may find the gravity and mass could use adjusting. I actually lied when I referred to it as a physics “control”, all of the code is still in my Agent class and runs if the character control is null.

 public Agent(Spatial s, GameState gs, JumpableBetterCharacterControl bcc, int type) {
        spatial = s;
        if (bcc != null) {
            charControl = bcc;
        } else if (charControl == null) {
            // anymore init for custom physics should go here
        }
}


 public void update(float tpf){
     if (charControl != null) {
    //update for agents that do use a chracter control
       }
       else if(charControl == null){
            Vector3f eyeSpot = getGroundLoc();

            //fixes a glitch where enemies that are too short sink through the ground
            if (height < 9) {
                eyeSpot.setY(eyeSpot.getY() + 9);
            } else {
                eyeSpot.setY(eyeSpot.getY() + height);
            }
            /// end fix

            Ray r = new Ray(eyeSpot, new Vector3f(0f, -1f, 0f));
            Vector3f intersectionPoint = gameState.getCollisionWithout(r, "ambient");
            if (intersectionPoint != null) {
                float originalDist = (intersectionPoint.subtract(eyeSpot).length());
                float dist;
                // \/ if the ground node isn't within .02f of the ground...
                if (originalDist > (height + .02f) || originalDist < (height - .02f)) {
                    //ajust height of tiny npcs accordingly
                    if (height >= 9f) {
                        originalDist = height - originalDist;
                    } else {
                        originalDist = 9f - originalDist;
                    }
                    
                    dist = originalDist;
                    
                    if(dist < -.2f){ //move down
                        fallTimeFactor += (tpf * 225);
                        dist = -tpf * gravity * (6.75f + fallTimeFactor);
                        
                        if(dist < originalDist){
                            dist = originalDist;
                        }
                    }
                    else if(dist > .25f){//move up slopes
                        
                        dist = tpf * gravity * 48;
                         if(dist > originalDist){
                            dist = originalDist;
                        }
                    }
                    //sudden y changes of higher than 4.5 are considered "flawed jumps" and are ignored to esily prevent jumpy physics (IMPORTANT: needs tested at high tpf / low fps to ensure enemies dont fall through world
                    else if(dist > 4.5f){
                        dist = 0;
                    }
                    
                    if(dist >= -.05f){ //sets the npc as being on the ground
                        if(fallTimeFactor != 0){
                            fallTimeFactor = 0;
                        }
                    }
                        groundNode.move(0, dist, 0);
                    if(dist > -.2f){
                        setInAir(false);
                    }
                    else{
                        setInAir(true);
                    
                }
                }
            } else{
                groundNode.move(0, (-tpf) * gravity * 20, 0);
                setInAir(true);
        }

            //rotate
         
            viewDir.setY(0);
            //set the view dir for NPCs
            if (agentType == 1 &&! stunned) {
                rotateQuat.lookAt(viewDir, upVar);
                Quaternion currentQuat = groundNode.getWorldRotation();
                rotateQuat.slerp(currentQuat, 1-(tpf*(8.3f)));
                groundNode.setLocalRotation(rotateQuat);
            }

            //physics push calulation
            pushDir.set(0, 0, 0);
            ArrayList others = gameState.getLiveMobs();
            for (int g = 0; g < others.size(); g++) {
                Agent a = (Agent) others.get(g);
                if (!a.equals(this) && a.getSpatial() != null) {
                    Vector3f dir = getGroundLoc().subtract(a.getGroundLoc());
                    float dist = dir.length();
                    if (dist < radius + .3f && dist != 0) {
                        pushDir.addLocal(dir.mult(4.f / (dist * dist)));
                    }
                }
            }

            pushDir.multLocal(900 / mass);
            pushDir.setY(0);
            //move along now
            walkDir.addLocal(pushDir);
            walkDir.setY(0);
            //ensures no action-moves are happening - these moves will affect movement in their own way
            if (inAction <= 0 && !snared && !stunned) {
                groundNode.move(walkDir.mult(tpf));
            } else if (inAction > 0 && !snared) {
                groundNode.move(pushDir.mult(tpf));
            }
        }
    }