To entity or not to entity?

Well the problem is that even the PhysicsSpace is already more than 1000 Lines of Code which I would have to partially adopt and also change everything (e.g. the CharacterControls) and then I am still missing the ThreadingType.Parallel which the AppState currently has. Maybe I am over-estimating this but for a first approach it feels massive.

Apart from that I am uncertain whether I don’t need the Controls anyway at a later stage (e.g. some purely visual things like a control influencing a particle emitter and all)

Again… something that’s really only needed because JME is integrating bullet with the scene graph. The game systems can already be run in a background loop, of which physics can be one of them. Just like in the sim-eth examples.

Yes, if you use CharacterControl then you will have to rewrite that in an ES way… but CharacterControl is the opposite of ES so it’s for the best, anyway.

PhysicsSpace is also working around not having an ES. It is essentially taken over 100% by a physics system using an ES approach.

That’s a very isolated thing, though. I find I hardly ever need controls and when I do it’s more of an effect (or animation). Just stuff that affects the visualization. Anything else breaks out of the ES.

So you are saying that I don’t actually need it really? Isn’t all that PhysicsSpace a bullet mechanism to allow multithreading (different physics spaces) and basically different world tiles (which I could do using the entity system, though).

Mhh I don’t know how far I will get without experience in physics systems and all, but yeah, maybe I’ll try that.

PhysicsSpace is a JME artifact, I guess. Looking at the code it seems its primary job is to help link Spatials to physics objects. The same thing that would be taken over be a physics system and the ES.

Here is how I am doing physic in server side :

Pretty mush similar to SimplePhysic system in sim-eth-es example.
Here I am having one PhysicSpace, you can have multiple physic spaces. (They can be run multi-threaded)

/**
 * Bullet physics system.
 *
 * @author Ali-RS <ali_codmw@yahoo.com>
 */
public class BulletPhysicsState extends AbstractGameSystem implements PhysicsSystem<PhysicsControlDriver> {

    static Logger log = LoggerFactory.getLogger(BulletPhysicsState.class);
    
    private PhysicsSpace physicSpace;

    private PhysicsDriverFactory driverFactory;
    
    private EntityData ed;
    private BulletPhysicsState.MobPhysicsBodyContainer mobBodies;
    private BulletPhysicsState.StaticPhysicsBodyContainer staticBodies;

    // Single threaded.... we'll have to take care when adding/removing
    // items.
    //private SafeArrayList<Body> mobBodies = new SafeArrayList<>(Body.class);
    private Map<EntityId, Body> index = new ConcurrentHashMap<>();
    private Map<EntityId, PhysicsControlDriver> driverIndex = new ConcurrentHashMap<>();

    // Still need these to manage physics listener notifications in a 
    // thread-consistent way   
    private ConcurrentLinkedQueue<Body> toAdd = new ConcurrentLinkedQueue<>();
    private ConcurrentLinkedQueue<Body> toRemove = new ConcurrentLinkedQueue<>();

    private SafeArrayList<PhysicsListener> listeners = new SafeArrayList<>(PhysicsListener.class);

    public BulletPhysicsState() {
    }

    protected void initialize() {
        log.info("initialize()");
        //The core of Bullet physic simulation.
        physicSpace = new PhysicsSpace();
        
        //Get PhysicsDriverFactory in case there is one registered
        //so it will auto create physic driver when new entity comes.
        driverFactory = getSystem(PhysicsDriverFactory.class);
        if(driverFactory != null){
            log.info("Found PhysicsDriverFactory.");
        }

        this.ed = getSystem(EntityData.class);
        if (ed == null) {
            throw new RuntimeException("Physics system requires an EntityData object.");
        }
        
    }

    protected void terminate() {
        physicSpace.destroy();
    }

    /**
     * Adds a listener that will be notified about physics related updates. This
     * is not a thread safe method call so must be called during setup or from
     * the physics/simulation thread.
     */
    @Override
    public void addPhysicsListener(PhysicsListener l) {
        listeners.add(l);
    }

    @Override
    public void removePhysicsListener(PhysicsListener l) {
        listeners.remove(l);
    }

    @Override
    public Body getBody(EntityId entityId) {
        return index.get(entityId);
    }

    @Override
    public void setControlDriver(EntityId entityId, PhysicsControlDriver driver) {
        synchronized (this) {
            driverIndex.put(entityId, driver);
            Body current = getBody(entityId);
            if (current != null) {
                current.driver = driver;
            }
        }
    }

    @Override
    public PhysicsControlDriver getControlDriver(EntityId entityId) {
        return driverIndex.get(entityId);
    }

    private void fireBodyListListeners() {
        if (!toAdd.isEmpty()) {
            Body body = null;
            while ((body = toAdd.poll()) != null) {
                //bodies.add(body);
                for (PhysicsListener l : listeners.getArray()) {
                    l.addBody(body);
                }
            }
        }
        if (!toRemove.isEmpty()) {
            Body body = null;
            while ((body = toRemove.poll()) != null) {
                //bodies.remove(body);
                for (PhysicsListener l : listeners.getArray()) {
                    l.removeBody(body);
                }
            }
        }
    }

    @Override
    public void start() {
        physicSpace.setGravity(new Vector3f(0, -5f, 0));
        mobBodies = new BulletPhysicsState.MobPhysicsBodyContainer(ed);
        mobBodies.start();

        staticBodies = new BulletPhysicsState.StaticPhysicsBodyContainer(ed);
        staticBodies.start();
    }

    @Override
    public void stop() {
        mobBodies.stop();
        mobBodies = null;
        staticBodies.stop();
        staticBodies = null;
    }

    @Override
    public void update(SimTime time) {
        physicSpace.distributeEvents();
        physicSpace.update((float) time.getTpf());

        for (PhysicsListener l : listeners.getArray()) {
            l.beginFrame(time);
        }

        // Update the entity list       
        mobBodies.update();
        staticBodies.update();

        // Fire off any pending add/remove events 
        fireBodyListListeners();

        double tpf = time.getTpf();

        // Apply control driver changes
        for (Body b : mobBodies.getArray()) {
            if (b.driver != null) {
                b.driver.update(tpf, b);
            }
        }

        // Integrate
        for( Body b : mobBodies.getArray() ) {
            b.integrate(tpf);
        }
        // Publish the results
        for (PhysicsListener l : listeners.getArray()) {
            for (Body b : mobBodies.getArray()) {
                l.updateBody(b);
            }
        }

        for (PhysicsListener l : listeners.getArray()) {
            l.endFrame(time);
        }
    }

    protected Body createMobBody(EntityId entityId, double invMass, double radius, boolean create) {
        Body result = index.get(entityId);
        if (result == null && create) {
            synchronized (this) {
                result = index.get(entityId);
                if (result != null) {
                    return result;
                }
                result = new Body(entityId);
                result.radius = radius;
                result.invMass = invMass;

                // Hookup the driver if it has one waiting
                PhysicsControlDriver driver = driverIndex.get(entityId);
                //else ask driver factory(if there is one already registered) to create it
                if(driver == null && driverFactory != null){
                    driver = driverFactory.createDriver(ed, entityId);
                    setControlDriver(entityId, driver);
                }
                result.driver = driver;

                // Set it up to be managed by physics
                toAdd.add(result);
                index.put(entityId, result);
            }
        }
        return result;
    }

    protected boolean removeMobBody(EntityId entityId) {
        Body result = index.remove(entityId);
        if (result != null) {
            toRemove.add(result);
            PhysicsControlDriver driver = ((PhysicsControlDriver) result.driver);
            driver.removeFromPhysicSpace(physicSpace);
            driverIndex.remove(entityId, driver);
            return true;
        }
        return false;
    }

    protected Body createStaticBody(EntityId entityId, double invMass, boolean create) {
        Body result = index.get(entityId);
        if (result == null && create) {
            synchronized (this) {
                result = index.get(entityId);
                if (result != null) {
                    return result;
                }
                result = new Body(entityId);
                result.invMass = invMass;

                // Hookup the driver if it has one waiting
                PhysicsControlDriver driver = driverIndex.get(entityId);
                //else ask driver factory(if there is one already registered) to create it
                if(driver == null && driverFactory != null){
                    driver = driverFactory.createDriver(ed, entityId);
                    setControlDriver(entityId, driver);
                }
                result.driver = driver;
                
                index.put(entityId, result);
            }
        }
        return result;
    }

    protected boolean removeStaticBody(EntityId entityId) {
        Body result = index.remove(entityId);
        if (result != null) {
            PhysicsControlDriver driver = ((PhysicsControlDriver) result.driver);
            driver.removeFromPhysicSpace(physicSpace);
            driverIndex.remove(entityId, driver);
            return true;
        }
        return false;
    }

    /**
     * Maps the appropriate entities to physics mob bodies.
     */
    private class MobPhysicsBodyContainer extends EntityContainer<Body> {

        public MobPhysicsBodyContainer(EntityData ed) {
            super(ed, OrFilter.create(PhysicsType.class,
                    Filters.fieldEquals(PhysicsType.class, "type", PhysicsType.DYNAMIC),
                    Filters.fieldEquals(PhysicsType.class, "type", PhysicsType.KINEMATIC)),
                    SphereShape.class,
                    PhysicsDriverInfo.class,
                    Position.class, 
                    MassProperties.class, 
                    PhysicsType.class
            );
        }

        @Override
        protected Body[] getArray() {
            return super.getArray();
        }

        @Override
        protected Body addObject(Entity e) {
            log.info("Add Mob physic body");
            MassProperties mass = e.get(MassProperties.class);
            SphereShape shape = e.get(SphereShape.class);
      
            // Right now only works for CoG-centered shapes                   
            Body newBody = createMobBody(e.getId(), mass.getInverseMass(), shape.getRadius(), true);

            Position pos = e.get(Position.class);
            newBody.setPosition(pos.getLocation());
            newBody.setRotation(pos.getFacing());
            if(newBody.driver != null){
                PhysicsControlDriver driver = ((PhysicsControlDriver) newBody.driver);
                driver.updatePhysicalProperties(ed, e.getId());
                driver.addToPhysicSpace(physicSpace);
            }
            
            return newBody;
        }

        @Override
        protected void updateObject(Body object, Entity e) {
            // We don't support live-updating mass or shape right now
        }

        @Override
        protected void removeObject(Body object, Entity e) {
            removeMobBody(e.getId());
        }

    }

    /**
     * Maps the appropriate entities to physics static bodies.
     */
    private class StaticPhysicsBodyContainer extends EntityContainer<Body> {

        public StaticPhysicsBodyContainer(EntityData ed) {

            super(ed, 
                    Filters.fieldEquals(PhysicsType.class, "type", PhysicsType.STATIC),
                    PhysicsDriverInfo.class,
                    Position.class, 
                    MassProperties.class, 
                    PhysicsType.class                  
            );

        }

        @Override
        protected Body[] getArray() {
            return super.getArray();
        }

        @Override
        protected Body addObject(Entity e) {
            log.info("Add static physic body");
            MassProperties mass = e.get(MassProperties.class);
            //SphereShape shape = e.get(SphereShape.class);

            // Right now only works for CoG-centered shapes                   
            Body newBody = createStaticBody(e.getId(), mass.getInverseMass(), true);

            Position pos = e.get(Position.class);
            newBody.setPosition(pos.getLocation());
            newBody.setRotation(pos.getFacing());

            if(newBody.driver != null){
                PhysicsControlDriver driver = ((PhysicsControlDriver) newBody.driver);
                driver.updatePhysicalProperties(ed, e.getId());
                driver.addToPhysicSpace(physicSpace);
            }
         
            return newBody;
        }

        @Override
        protected void updateObject(Body object, Entity e) {
            // We do support live-updating mass or shape .
            ((PhysicsControlDriver) object.driver).updatePhysicalProperties(ed, e.getId());
        }

        @Override
        protected void removeObject(Body object, Entity e) {
            removeStaticBody(e.getId());
        }

    }
}

I added a PhysicsControlDriver over ControlDriver in sim-eth-es to support physic controls.

package com.overthemoon.core.physics.bullet;

import com.jme3.bullet.PhysicsSpace;
import com.overthemoon.core.api.physics.ControlDriver;
import com.simsilica.es.EntityData;
import com.simsilica.es.EntityId;

/**
 *
 * @author Ali-RS <ali_codmw@yahoo.com>
 */
public interface PhysicsControlDriver extends ControlDriver {
    /**
     * Add your physic objects to physicSpace provided by this method.
     * @param physicsSpace 
     */
    public void addToPhysicSpace(PhysicsSpace physicsSpace);
    
    /**
     * Remove physic objects from physicSpace.
     * @param physicsSpace 
     */
    public void removeFromPhysicSpace(PhysicsSpace physicsSpace);
    
    /**
     * Update physics state with your physic components.
     * Note, this method is supposed to work with EntitySystem 
     * structure using Zay-ES.
     * @param entity 
     */
    public void updatePhysicalProperties (EntityData ed, EntityId entityId);
}

and as an example here is BetterCharacterDriver

/**
 * Basic driver for controlling characters
 *
 * @author Ali-RS <ali_codmw@yahoo.com>
 */
public class BetterCharacterDriver implements PhysicsControlDriver {

    private PhysicsCharacterControl control; // extends BetterCharacterControl

    private volatile Vector3f walkDirection = new Vector3f();
    private volatile Vector3f viewDirection = new Vector3f();
    private Quaternion tempRot = new Quaternion();
    private Vector3f tempPos = new Vector3f();

    public BetterCharacterDriver(PhysicsCharacterControl control) {
        this.control = control;

    }

    @Override
    public void addToPhysicSpace(PhysicsSpace physicsSpace) {
        if (control != null && physicsSpace != null) {
            control.setPhysicsSpace(physicsSpace);
            control.setEnabled(true);
        } else {
            throw new NullPointerException(" BetterCharacterControl not instantiated !");
        }
    }

    @Override
    public void removeFromPhysicSpace(PhysicsSpace physicsSpace) {
        if (control != null && physicsSpace != null) {
            physicsSpace.remove(control);
            control.setEnabled(false);
        }
    }

    @Override
    public void update(double tpf, Body body) {
        // Grab local versions of the player settings in case another
        // thread sets them while we are calculating.
        Vector3f walkDir = walkDirection;
        Vector3f viewDir = viewDirection;

        control.setViewDirection(viewDir);

        control.setWalkDirection(walkDir);

        // Setup the current world rotation of the body 
        tempRot.lookAt(control.getViewDirection(), Vector3f.UNIT_Y);
        body.orientation.set(tempRot.getX(), tempRot.getY(), tempRot.getZ(), tempRot.getW());

        // Apply the accelerated velocity oriented into world space       
        tempPos = control.getPhysicsRigidBody().getPhysicsLocation();
        body.pos.set(tempPos.x, tempPos.y, tempPos.z);

        Vector3f tempVel = control.getVelocity();
        body.velocity.set(tempVel.x, tempVel.y, tempVel.z);

    }

    @Override
    public void updatePhysicalProperties(EntityData ed, EntityId entityId) {
        Position pos = ed.getComponent(entityId, Position.class);
        control.warp(pos.getLocation().toVector3f());
        control.getPhysicsRigidBody().setDamping(0.2f, 0.2f);
        control.getPhysicsRigidBody().setLinearSleepingThreshold(0.1f);
    }

    public void applyMovementState(Vector3f walkDirection, Vector3f viewDirection) {
        this.walkDirection = walkDirection;
        this.viewDirection = viewDirection;
    }

    public PhysicsCharacterControl getControl() {
        return control;
    }

}

I share the driver with SteerSystem to apply steer force.

One problem I have is I do not know how should i integrate collision shapes which are controlled by animation bones, to server side.

In answering another post:

I did a little digging and discovered that JME’s PhysicsSpace is really the wrapper for native bullet (or in the case of JBullet the bullet engine). So it’s hard to avoid using it.

@Ali_RS you might consider reposting your response over there and/or continuing that conversation. You have way more experience with bullet and Zay-ES than I do. It seems like you may have taken the final option in my suggestion on that other thread. I’d be interested to know more about your Body object. Are you effectively keeping a physics-only set of spatials?

I think it would be nice to open up PhysicsSpace a bit more. Perhaps at least add one or two more “else” branches to the add(Object)/remove(Object) methods.

I don’t know whether I discovered a Bug or if this is again due to my wrong design :smiley:
Since RigidBodyControl does not support getSpatial(), my Object is a Tuple<Spatial, RigidBodyControl> which leads to an Exception in EntityContainer’s Constructor:

public class RigidBodyContainer extends EntityContainer<Tuple<Spatial, RigidBodyControl>>

with the following Exception:

java.lang.ClassCastException: sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl cannot be cast to java.lang.Class
        at com.simsilica.es.EntityContainer.<init>(EntityContainer.java:96)
        at com.simsilica.es.EntityContainer.<init>(EntityContainer.java:83)
        at de.darkigames.TheSlaughter.EntitySystem.Util.RigidBodyContainer.<init>(RigidBodyContainer.java:26)

But maybe we could have a community EntitySystem <-> Bullet Wrapper but I don’t feel knowledged enough with either Bullet or Zay-ES

Yeah, I guess my code doesn’t support nested parameterized types yet. A shame.

You probably should just create your own inner class that extends Tuple or something I guess. At this moment, I’m not sure what it would take to handle this case and I won’t be able to look at it until this weekend at the earliest.

Edit: you can file an SiO2 issue if you like to track it for the future.

I have reported that now however don’t hurry on that one, the workaround is really fine :slight_smile:

Me looking for it too. :slight_smile:
I need nested parameterized type in a few of my containers.