Projectile system / Spatial Hashing

@pspeed

  1. Alright, i’ve tracked down the problem impacting my performance which were: not testing whether an object already is in the cell its vector is and tons of system.out.prints in a loop which would reduce the fps by like 500
  2. I am lost in the matter of checking collision. is the concept like; for each object loop through each collidable object in neighbouring cells?

EDIT: it seems like index.put(key,value) adds another key with a value assigned (even if the key the exact same as one of those already in the index).
How can i put 2 values under the same key which i thought the purpose of multimap was? I feel like im completely fkin lost in it

thanks for the patience…

Ad 2) Just to explain why im so confused; it doesnt seem like a good idea to put nested loop on something that is checked 1*projectileNumber times each loop update. But again i am very inexperienced and its the first time i’ve ever heard of such technique and probably its just me whose code is not very optimal

I actually don’t understand what you are saying here… but I also don’t know if you created the multimap as I suggested.

In basic concept,
multimap.put(“foo”, 123);
multimap.put(“foo”, 456);
Then multimap.get(“foo”) will return a collection with 123 and 456 in it.

It’s a nested loop that is only 1-2 iterations each, though… if your grid size is right.

Given the numbers you’ve supplied so far, I’m not convinced you need any of this though.

I actually don’t understand what you are saying here… but I also don’t know if you created the multimap as I suggested.

Yes,i did use the exact same multimap as you showed me here. This problem is fixed for now, at least.

It’s a nested loop that is only 1-2 iterations each, though… if your grid size is right.

What do you mean “right”? currently one cell is 5x5 and i thought that it would be the perfect balance between putting too many cells in use and checking collision in cell/s. What is the average grid size for implementations when there are lots of objects +/- true to real life size? (so average diameter of the object is around 1)

Given the numbers you’ve supplied so far, I’m not convinced you need any of this though.

yeah because im still struggling with basic functionalities.

But another problem emerged:

        for(GameObject value : index.values()){
        Vector3f vectorToMove = new Vector3f(value.getPosition().toVector3f()); // to be used later for spatial movement
        value.setPosition(new Vec3d(value.getPosition().x+0.01f,1.5d,value.getPosition().z+0.01f));
        updateObject(value);
     
        }

So i set up this loop to keep all GameObject related things in there, but it throws ConcurrentModificationException due to this piece of code:

public static void updateObject(GameObject gameObj){
        cell = worldGrid.getContainingCell(gameObj.getPosition());
        if(!index.get(cell).contains(gameObj)){
        previousCell = worldGrid.getContainingCell(gameObj.getPreviousCellVec());
        index.get(previousCell).remove(gameObj);
        index.put(cell, gameObj);
        gameObj.setPreviousCellVec(cell.getWorldOrigin().toVec3d());
        System.out.println("putting: " + gameObj + " to cell: " + cell);  // printing gameObj just to check if it really does work
        }
    } 

and to be exact, these two i believe:

index.get(previousCell).remove(gameObj);
        index.put(cell, gameObj);

because they change the lenght of the collection that is being iterated in the for each loop. I’ve looked up some ways to avoid this type of error but none seem to be applicable here (with my i believe poor knowledge)
Is there any way to update the object without getting this exception or do i have to rethink my approach?

Also, could you comment on the code i wrote (e.g whether updateObject method puts objects in and out of the cells “the proper way” or to put simply the most efficient and clean way).

For your ultimate game, what are the answers to those questions.

I’m not answering any more questions until mine are answered.

@pspeed Alright. So the target is around 1500, including 500 static ones, with an average radius smaller than grid size, different sizes, dont need checks between all of them (types of objects can collide with other types but not every type can collide with others of same type). Those are the answers. The problem of ConcurrentModificationException was solved and registering objects to their cells (even when moving) works perfectly with very satisfying performance (as for now). 2000 fast (say 1 cell every 2 iterations) moving objects are “updated” every iteration, and are put in their respecitve cells with a minimal fps cost, so i hope that if i implement collision detection the performance of the object update loop wont decrease more than 10fold. Also, sorry if i didnt quite get something you explained before english is not my native language
EDIT: current grid size is 3

Ideal grid size will be based on object size and how sparse they are, etc…

But one important rule: grid cell size should be large enough to be able to contain any single object… so an object will never be in more than 4 cells (in 2D) or 8 (in 3D).

So a grid size of 3 is fine as long as no object had a radius bigger than 1.5

@pspeed Thank you very much. Im implementing the collision and its going good so far, used your idea to loop through all cells the sphere around GameObject intersects;

Vec3i min = grid.worldToCell(lowest location);
Vec3i max = grid.worldToCell(highest location);
for( int x = min.x; x <= max.x; x++ ) {
    for( int y = min.y; y <= max.y; y++ ) {
        for( int z = min.z; z <= max.z; z++ ) {
            GridCell cell = grid.getCell(x, y, z);
            index.put(cell, gameObject);
        }
    }
}

the one i implemented has “less than” rather than “less than or equal to”. Is there a game scenario my way doesnt work as “it should”? Because my way actually does save a lot of calculations.
Also, static objects such as trees, walls dont need to check for collision, right? They just need to be taken into account when mobile objects calculate their potential collisions, right?

Less than or equal to is necessary or you will miss half of the objects in each direction.

If min.x is 0 then that means there is part of the object’s radius in 0 and if max.x is 1 then it means that part of the object’s radius is in 1.

…so if you want to collide with everything you need to check x=0 and x=1… not just x=0.

Edit: and note: if your grid is sized properly then min and max will never be more than one apart in any direction.

Alright. Thank you very much, especially for the patience. Everything seems to be working, the objects detect each other but not themselves, and 300 objects colliding with each other at one time reduce the framerate to 300, which will get higher after i reduce calculations by adding ifs(), . Anyway, some minor tweaks and i’ll share my code here for the future

2 Likes

Here’s the code.
The grid and object management class:

package mygame;

import com.google.common.collect.Multimap;
import com.google.common.collect.MultimapBuilder;

import com.simsilica.mathd.Grid;
import com.simsilica.mathd.GridCell;
import com.simsilica.mathd.Vec3d;
import com.simsilica.mathd.Vec3i;

/**
 *
 * @author Makary
 */
public class SpatialHash {
    public static Multimap<GridCell, GameObject> index = MultimapBuilder.hashKeys().hashSetValues().build();
    public static Grid worldGrid;
    public static GridCell cell;
    public static GridCell previousCell;
    public static int gridSize =3;
    
    
    public static void Setup(){
        worldGrid = new Grid(gridSize);
    }
    
    public static void AddObject(GameObject gameObj){  // puts the object in a cell based on its position.
    cell = worldGrid.getContainingCell(gameObj.getPosition());  
    index.put(cell, gameObj);
    
    }
    
    public static void updateObject(GameObject gameObj){
        cell = worldGrid.getContainingCell(gameObj.getPosition());
        if(!index.get(cell).contains(gameObj)){
        previousCell = worldGrid.getContainingCell(gameObj.getPreviousCellVec());
        index.get(previousCell).remove(gameObj);
        index.put(cell, gameObj);
        gameObj.setPreviousCellVec(cell.getWorldOrigin().toVec3d());
        }
    }
    

    
    
    public static void checkForCollisionSmall(GameObject gameObj){ // this method checks for collision for small (with smaller radius than grid size) objects
Vec3i min = worldGrid.worldToCell(new Vec3d(gameObj.getPosition().x-gameObj.getRadius(),gameObj.getPosition().y-gameObj.getRadius(),gameObj.getPosition().z-gameObj.getRadius()));
Vec3i max = worldGrid.worldToCell(new Vec3d(gameObj.getPosition().x+gameObj.getRadius(),gameObj.getPosition().y+gameObj.getRadius(),gameObj.getPosition().z+gameObj.getRadius()));


for( int x = min.x; x <= max.x; x++ ) {  
    for( int y = min.x; y <= max.x; y++ ) {
        for( int z = min.x; z <= max.x; z++ ) {
            GridCell testedCell = worldGrid.getContainingCell(x, y, z);
            for(GameObject collisionObject : index.get(testedCell)){
                if(gameObj != collisionObject && gameObj.getPosition().toVector3f().distance(collisionObject.getPosition().toVector3f()) <= collisionObject.getRadius() &&gameObj.getPosition().toVector3f().distance(collisionObject.getPosition().toVector3f()) <= gameObj.getRadius()){
//                             what to do when the collision occurs?
                }
            }
        }
    }
}
    
    }

    

    
    
    public static void createObject(Vec3d spawnpoint,float radius,boolean type, Vec3d spawnpointCopy){
    GameObject newObject = new GameObject();  // object is an unit or a projectile
    newObject.setPosition(spawnpoint);
    newObject.setRadius(radius);
    newObject.setType(type);
    newObject.setPreviousCellVec(spawnpointCopy);
    AddObject(newObject);
    
    }
}

The GameObject class:

package mygame;

import com.simsilica.mathd.Vec3d;

/**
 *
 * @author Makary
 */
public class GameObject {
    
        public  float radius;
    public  Vec3d position;
    public boolean type; // true = projectile, false = hittable (unit, wall ,tree, whatever)
    Vec3d previousCellVec; // helps with removing objects from its cell, neccesary to avoid ConcurrentModificationError
    
        public float getRadius() {
        return radius;
    }

    public void setRadius(float radius) {
        this.radius = radius;
    }

    public Vec3d getPosition() {
        return position;
    }

    public void setPosition(Vec3d position) {
        this.position = position;
    }
    public void setType(boolean type){
    this.type = type;
    }
    
    public boolean getType(){
    return type;
    }
    
    public void setPreviousCellVec(Vec3d previousCell){
    this.previousCellVec = previousCell;
    }
    public Vec3d getPreviousCellVec(){  
    return previousCellVec;
    }
}

Example of how to update all objects in the world grid (put it in the SimpleUpdate method): (i implemented it in another class therefore there is SpatialHash. before every variable)

ArrayList<GameObject> arrayList = new ArrayList<>();
   arrayList.addAll(SpatialHash.index.values());
    for (int i=0; i < arrayList.size(); i++){
        SpatialHash.checkForCollisionSmall(arrayList.get(i));     
        Vector3f vectorToMove = new Vector3f(arrayList.get(i).getPosition().toVector3f()); // to later be used for spatial movement
        arrayList.get(i).setPosition(new Vec3d(arrayList.get(i).getPosition().x+0.0f,1.5d,arrayList.get(i).getPosition().z+0.0f));
        SpatialHash.updateObject(arrayList.get(i));
        
            }
        }

if you want to target performance rather than realism (eg. projectiles fired upward wont turn around and fall to the ground) you can replace

 public boolean type;

with something like:

 public byte type;  // example: 1= tree,2= projectile fired upwards, 3= projectile fired horizontally 

and make it so the GameObjects of types 2 and 3 cannot possibly collide, and then you can cut out the “y” loop in the loop:

for( int x = min.x; x <= max.x; x++ ) {  
    for( int y = min.y; y <= max.y; y++ ) {
        for( int z = min.z; z <= max.z; z++ ) {
            GridCell testedCell = worldGrid.getContainingCell(x, y, z);
            for(GameObject collisionObject : index.get(testedCell)){
                if(gameObj != collisionObject && gameObj.getPosition().toVector3f().distance(collisionObject.getPosition().toVector3f()) <= collisionObject.getRadius() &&gameObj.getPosition().toVector3f().distance(collisionObject.getPosition().toVector3f()) <= gameObj.getRadius()){
//                             what to do when the collision occurs?
                }
            }
        }
    }
}

EDIT: when checking for collision, try to put the fastest if() conditions first, otherwise you may end up with a lot of unnecessary calculations in nested loops which are in an updtae method which can really slow down the performance, so be wise when editing collision checking methods.

@pspeed having all the things done, one technical question; should i store unit hp/range etc in GameObject class, or in a set of ArrayList/multimap?

Best if as ECS Component data.

but if you dont use ECS, then in some GameObject related class.(that you can anyway have in arraylist/maps)

@oxplay2 Why shouldnt i make it a part of GameObject class and just add it to the constructor? Would it impact the performance somehow?

Only philosophical reasons.

Many of us are followers of the “ways of ECS” and would do it differently is all. But if you have never used an entity-component-system approach, it has a fairly steep (but relatively short) learning curve. It definitely requires you to think about problems in a totally different way.

…it does pay dividends down the road, though.

Using the java Iterator class will avoid the concurrent modification exception.

Iterator (Java Platform SE 8 ).

I tried that, but it’d have to put the whole objectUpdate method into that loop, which i wanted to avoid for readability reasons. Thank you all, the problem was solved.

1 Like