Now that I think about it some more, there may be a way.
Basic Idea
You say you are using a tile-based map, so you might consider implementing tile-based collision detection far away from the player. Consider the following diagram:

Dashed Lines:
- Green Screen Limits (Camera). Everything intersecting or inside this box is on-screen.
- Red Box2d Limits. Everything intersecting or inside this box is being updated by box2d.
- Blue Gameplay Limits. Everything inside this box is being updated.*
*I will refer to the space inside the blue box, but outside the red box as the tile-region.
Entities:
- Circle The player. The camera follows the player around (could be always centered on the player's position in the simplest case), but the red and green box always share the same center.
- Red Shapes Enemies. Constantly moving towards the player.
- Black Squares Obstacle Tiles.
Filled vs. Unfilled:
- Filled Shapes Box2d Active. These shapes contain active box2d bodies.
- Unfilled Shapes Box2d Inactive.
Updating
The key here is to update your enemies in 2 ways: when they are inside the red box, update them as you normally do.
When they are outside the red box (b2body is inactive), update their position discretely. That is, allow them to move to the next tile after a set amount of time (in accordance with the speed they are allowed to move as a box2d body). If you want info on how to do the path-finding, check out these two answers.
To do this you could maintain a list of tile-enemies, and a list of box2d enemies. Then you need to update the list of tile-enemies each turn.
If you are using a an ECS the implementation becomes much clearer: enemies in the tile region have a TileMove
component. Which looks something like this:
TileMove
- timeOnTile //The amount of time the enemy has spent on the current tile
- timeToMove //The amount of time required to spend on the current tile before moving again
- currentDestination //Probably the player
- currentTile //The tile the enemy occupies
The TileMoveSystem::update
it, or TileMove::update
code might look like this:
timeOnTile += dt
if (timeOnTile >= timeToMove){
//Decide where to move; probably includes some pathfinding and/or steering behaviours
oldTile->occupant = NULL //Tell the old tile it's no longer occupied
newTile->occupant = self //Tell the new tile it's now occupied
timeOnTile = 0.0;
}
Crossing the Tile Region - Box2d Boundary
You also need to be able to check if enemies are crossing the red box boundary and decide what to do with them.
Box2d to Tiled Region -
On the box2d side, it's pretty easy. I would reccomend making a sensor body that is updated with the camera's position and orientation. The dimensions of the sensor body are the same as the red box. Whenever the endContact
callback is triggered from the sensor, deactivate the b2body belonging to that entity, and create/restore a TileMove
component.
Tiled Region to Box2d -
Going the other way, you need to do your own area check to see if any of your tiled enemies have entered the red box. This is probably the trickiest bit. My gut feeling is that you should get the AABB of the red box, and query all the tiles (a rectangular sub-region of the global grid) to see if there is an enemy/obstacle that currently occupies each tile. For each entity found with a tileMove
component, do a narrow phase test to see if it's actually inside the red box.
Alternatively, just make the red box the AABB that's centered with the green box, like in the figure below

In this case there doesn't need to be a narrow phase; every occupied tile in the AABB region will activate its occupant. The drawback here is that there will need to be a few more active enemies than there would be otherwise.
In any case, once you have determined that an entity needs to move back into the box2d region you need to:
- create/restore the
PhysicsBody
component
- Set the transform of the box2d body to the current tile position.
- (re)activate the box2d body
- Remove the
MoveTile
component
Drawbacks
First, this means there is a whole other group of objects you have to maintain (the tile-entities). Also, you say you have ~150 bodies in the simulation. There's a lot of changes I've suggested, and the overhead cpu cost of determining when enemies/obstacles are inside/outside the box2d region (and transforming them appropriately) might not give the net performance gain you desire.
What I can say, is that, the more enemies you have in your game, the better this method will perform vs. the pure box2d implementation.
Finally, although I am somewhat uncertain as to the impact it will have on the overall performance, I am confident that this answers your core question: it will reduce the execution time of the box2d simulation.