Feature Request for SiO2 EventBus : Support for filtered listeners

Hi
Before I open an issue for this on github page I thought to ask first if it is a good practice or not.

My propose is to let the listener to be able to set a filter based on a field value on event object. So instead of handling this if statement inside the listener itself the EventBus should handle this for the listener.
Similar to Filters in ZayES.

1 Like

I second this feature. I don’t see a reason for it to be an anti-pattern. It would clean up our code a bit.

Give me an example of a use-case. I’ve literally never even thought of needing such a thing so I need to wrap my brain around it.

Maybe to hide adding many events which are similar?
e.g. instead of an EnemyDeadEvent and an PlayerDeadEvent you just have a DeadEvent and filter isPlayer()

Yes, sorry I did not provide it in the first place. It was a bit hard to explain :wink:

The case is, in a multiplayer game which you have multiple parties where each Party(Team) has it’s own separate Scene (World).

A Party is a group of players who created a team and are playing the game in a Scene (World) that belongs to them.

Scene (World) is an entity with a Scene component and any object belongs to that scene is going buff to that Scene.

Each Scene has it’s own PhysicsSpace, NavSpace (NavMesh), Scenario (story of the scene contains multiple Quests (Task) )

You can have multiple parties each has it’s own Scene yet they are using one EntityData.

Now consider a Scenario for a Scene, where i have multiple Quests inside the Scenario.

A Quest (task) has a state,

possible states of a task:

    pending — the task did not met the unlocking requirement yet
    unlocked — the unlocking requirements are met, but it is not in progress yet, maybe because of the limit of possible tasks in progress
    in progress — the task was unlocked and shown to the player. It might be useful to have a sub-state, or a separate state for new task. This symbols that the task is technically in progress, but player did not read the objections yet. I did not give it a separate state because it is UI specific
    completed — task was in progress and player did everything necessary to complete it. Now it is time to collect the reward. If there is no reward the task might directly switch to the next state. There are also systems where quests autocomplete
    done — task was completed and user collected the reward
    canceled — this state is important if you want a quest to disappear. It’s particularly useful, if you introduce a new quests which player, who advanced beyond a certain threshold, shall not see

I have a QuestEvent fired whenever a Quest state changed. Now if I publish this event with EventBus all parties are going to receive that event in their Scenario , but I want only the relevant Scenario receive that event.
For this I need filtering based on scene id of Quest here.
I am not sure if that feature request is the proper answer to my need so again I need your help to settle it. :slightly_smiling_face:

Design question: what was the thinking behind the choice that the quest state is not a component in the ES? It seems like this would be a persistent part of the world state to me.

1 Like

Moved answer to relevant thread

My whole idea was to make it support multi-scene/multi-bus where you want to listen for events in a specific scene/bus. This might be partially against the idea of a singleton approach in EventBus implementation ?

@zissis may you give your use case also ?

My use case is not to far off of yours. I have upgraded ethereal to be able to run multiple scenes at once. Sector space scene, thousands of battle scenes, planetary orbit scenes, etc … players can easily switch from one scene to the next and only get objects from the scene they are watching. I would like to leverage the event bus on the server based on a scene filter. I am limiting my use of the event bus at the moment on the server side because of all the conditional statements all the listeners need to jump through. If we had filters then objects in a single scene could send asynchronous events to each other seamlessly.

1 Like

But you are really only moving the if statements, no? What events are you even sending? I would think you would only have one active ethereal space per connection at any given time? Or does every connection have all 1000 sim ethereal spaces?

In Ali_RS’s case, my argument would be that what you are sending events about is really part of the ES and that you should be handling them with systems. Else if you save the world (the ES data) then you lose all of your current quest state, no?

Yep, in my case I would loose them.

btw, @pspeed I do not know how sim etheral zone works internally but I want to know if it supports idea of multiple Parties (Teams) connect to server.

I am using one EntityData for all Parties and just using Filtesr to separate each scene.

In their own spaces? Or? Not sure what you mean exactly.

A lot of times you can fake multiple spaces just by putting everyone super far apart in some standard direction (y is common).

1 Like

All of them are in same etheral space. I only use a component filter to separate them in my systems. Every thing is based on your sim-eth-es example.

I think I need more information. Separating them how? Why do they need separation?

Typically, systems take inputs and produce outputs (in the most simplified form). Which systems care to only be dealing with a subset of space? And why?

One connection … thousands of scenes. I can flip a user from scene to scene either through a client request to the server of from the server. I extended the server to filter out entities that are not in the player’s scene. It’s super optimized and allows you to use ethereal like the “rooms” pattern of commercial net servers. I would agree that quest data should be handled with ES. I have quest type stuff in my game and the data gets persisted and managed through the ES system. As for my upgrade request, here are some real use cases of events other systems react to asynchronously but need to filter out what “scene” the event came from to apply it to the correct scene of entities. All these use cases are currently there for the use of all the systems that manage the NPCs but auto responses to some or all will be added to the user’s side in the future as well.

Async events:
Fleet warping into sector
Fleet warped into sector
Fleet warping out of sector
Fleet battle started
Fleet battle ended

Those are just some examples to give you a sense of scope. I am not just building a game in a hard coded fashion but building a late bound infrastructure. I could have tightly coupled the appropriate systems to react to these events but by using an async event bus I am able to quickly bolt on new systems that listen to the appropriate events without touching the rest of the code. Basically I am using a MOM (Message Orientated Middle-ware) pattern on steroids for any systems that need to be aware of the events occurring in the environment. In the future I will be extending this framework to include correlation IDs in order to be able to provide asynchronous request/response patterns. This requested upgrade will as you said, move the if statements but that’s a big cleanup to the code base. If I have 10 systems listening to 10 events each currently that’s 100 if statements but if you handle filters in the bus then it’s just one.

Sony for the lengthy response but hopefully this outlines the use case clearly.

EDIT: I forgot to mention: the NPCs I am referring to are two types … scene bound and user bound … the events can also be listened to by the server side equivalent of traditional model controllers … not the typical model node tree but a custom one I wrote server side. For example, an AI controller for an NPC ship in a particular scene.

Take the BulletSystem as an example.

I took it and extended it to support multiple PhysicsSpace.

/*
 * $Id$
 * 
 * Copyright (c) 2018, Simsilica, LLC
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions 
 * are met:
 * 
 * 1. Redistributions of source code must retain the above copyright 
 *    notice, this list of conditions and the following disclaimer.
 * 
 * 2. Redistributions in binary form must reproduce the above copyright 
 *    notice, this list of conditions and the following disclaimer in 
 *    the documentation and/or other materials provided with the 
 *    distribution.
 * 
 * 3. Neither the name of the copyright holder nor the names of its 
 *    contributors may be used to endorse or promote products derived 
 *    from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 
 * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.otm.moona.physics.system;

import com.otm.moona.common.component.SpawnPosition;
import com.otm.moona.physics.EntityCollisionListener;
import java.util.*;
import java.util.concurrent.*;

import com.google.common.base.Function;

import org.slf4j.*;

import com.jme3.bullet.*;
import com.jme3.bullet.collision.*;
import com.jme3.bullet.PhysicsSpace.BroadphaseType;
import com.jme3.bullet.collision.shapes.CollisionShape;
import com.jme3.math.*;
import com.jme3.util.SafeArrayList;
import com.otm.moona.common.component.EntityBuff;
import com.otm.moona.common.component.PersistentEntityBuff;
import com.otm.moona.common.component.Scene;
import com.otm.moona.common.state.PrefabSystem;
import com.otm.moona.physics.*;
import com.otm.moona.physics.component.*;
import com.otm.moona.physics.component.shape.*;

import com.simsilica.es.*;
import com.simsilica.mathd.AaBBox;
import com.simsilica.sim.*;

/**
 * Adapts a bullet physics space to an SiO2 GameSystem using Zay-ES entities to
 * represent physical game objects.
 *
 * @author Paul Speed
 */
public class BulletSystem extends AbstractGameSystem implements PhysicsSystem {

    static Logger log = LoggerFactory.getLogger(OldBulletSystem.class);

    private Thread baseThread;

    //private PhysicsSpace pSpace;
    private CollisionDispatcher collisionDispatcher = new CollisionDispatcher();

    private EntityData ed;
    private CollisionShapes shapes;

    private BroadphaseType broadphaseType = BroadphaseType.DBVT;
    private Vector3f worldMin = new Vector3f(-10000f, -10000f, -10000f);
    private Vector3f worldMax = new Vector3f(10000f, 10000f, 10000f);
    private float speed = 1;

    private PhysicsSpaceContainer pSpaces;
    private BodyContainer bodies;
    private GhostContainer ghosts;

    private EntitySet impulses;

    // Keeps track of just the bodies that are non-kinematic rigid bodies   
    private SafeArrayList<EntityPhysicsObject> mobs = new SafeArrayList<>(EntityPhysicsObject.class);

    // Keeps track of just the bodies that have control drivers
    private SafeArrayList<EntityRigidBody> driverBodies = new SafeArrayList<>(EntityRigidBody.class);

    private SafeArrayList<PhysicsObjectListener> objectListeners = new SafeArrayList<>(PhysicsObjectListener.class);

    private SafeArrayList<EntityCollisionListener> collisionListeners = new SafeArrayList<>(EntityCollisionListener.class);
    private CollisionFilter collisionFilter = new DefaultCollisionFilter();

    private ConcurrentLinkedQueue<ObjectSetup> pendingSetup = new ConcurrentLinkedQueue<>();

    private Function<PhysicsRayTestResult, EntityId> entityIdFunction = new EntityIdFunction();

    public BulletSystem() {
    }

    public PhysicsSpace getSpace(EntityId spaceId) {
        return pSpaces.getObject(spaceId);
    }

    /**
     * Initializes an EntityPhysicsObjects using the specified function. This is
     * useful for two reasons: 1) it can be called before the object actually
     * exists and will be called when the object shows up (beware leaks), 2) it
     * will always be called on the same thread that the physics simulation is
     * running on.
     */
    public void setupObject(EntityId objectId, Function<EntityPhysicsObject, ?> setup) {
        pendingSetup.add(new ObjectSetup(objectId, setup));
    }

    /**
     * Sets the ControlDriver for a physics object. This delegates to
     * setupObject() so will succeed even if the entity's rigid body hasn't been
     * created quite yet.
     */
    public void setControlDriver(EntityId objectId, final ControlDriver driver) {
        setupObject(objectId, new Function<EntityPhysicsObject, Void>() {
            @Override
            public Void apply(EntityPhysicsObject object) {
                EntityRigidBody body = (EntityRigidBody) object;
                body.setControlDriver(driver);
                if (driver == null) {
                    driverBodies.remove(body);
                } else if (!driverBodies.contains(body)) {
                    driverBodies.add(body);
                }
                return null;
            }
        });
    }

    /**
     * Adds a listener that will be notified about all physics objects changes.
     * These listeners are called several times per frame and should be
     * efficient and few. Note: this method is not thread safe and should only
     * be called by the simulation thread once the game manager has been
     * started.
     */
    public void addPhysicsObjectListener(PhysicsObjectListener l) {
        objectListeners.add(l);
    }

    public void removePhysicsObjectListener(PhysicsObjectListener l) {
        objectListeners.remove(l);
    }

    /**
     * Adds a collision listener that will be notified collisions between
     * entity-backed physical objects and other physics bodies (whether
     * entity-base dor not). Note: this method is not thread safe and should
     * only be called by the simulation thread once the game manager has been
     * started.
     */
    public void addEntityCollisionListener(EntityCollisionListener l) {
        collisionListeners.add(l);
    }

    public void removeEntityCollisionListener(EntityCollisionListener l) {
        collisionListeners.remove(l);
    }

    /**
     * Sets a filter that can cause collisions to be skipped before being passed
     * to the collision listeners.
     */
    public void setCollisionFilter(CollisionFilter collisionFilter) {
        this.collisionFilter = collisionFilter;
    }

    public CollisionFilter getCollisionFilter() {
        return collisionFilter;
    }

    /**
     * Sets the EntityData that this system will use to detect new physics
     * entities. Will be looked up in the GameSystemManager if not set. Note:
     * the EntityData must be set before the system has been initialized for it
     * to take effect. The method will throw an IllegalStateException if called
     * after initialization.
     */
    public void setEntityData(EntityData ed) {
        if (isInitialized()) {
            throw new IllegalStateException("System is already initialized");
        }
        this.ed = ed;
    }

    public EntityData getEntityData() {
        return ed;
    }

    /**
     * Sets the collision shapes registry that will be used to find collision
     * shapes for physics entities. This will be looked up in the
     * GameSystemManager if not set. Note: the CollisionShapes registry must be
     * set before the system has been initialized for it to take effect. The
     * method will throw an IllegalStateException if called after
     * initialization.
     */
    public void setCollisionShapes(CollisionShapes shapes) {
        if (isInitialized()) {
            throw new IllegalStateException("System is already initialized");
        }
        this.shapes = shapes;
    }

    public CollisionShapes getCollisionShapes() {
        return shapes;
    }

    @Override
    protected void initialize() {
        if (ed == null) {
            ed = getSystem(EntityData.class, true);
        }
        if (shapes == null) {
            shapes = getSystem(CollisionShapes.class, true);
        }

        baseThread = Thread.currentThread();

        //pSpace = new PhysicsSpace(worldMin, worldMax, broadphaseType);
        //pSpace.addCollisionListener(collisionDispatcher);
        pSpaces = new PhysicsSpaceContainer(ed);
        bodies = new BodyContainer(ed);
        ghosts = new GhostContainer(ed);

        getSystem(PrefabSystem.class, true).register(ShapeInfo.class, Mass.class,
                BoxShape.class,
                SphereShape.class,
                CylinderShape.class,
                CapsuleShape.class);

    }

    @Override
    protected void terminate() {
        //pSpace.destroy();
    }

    @Override
    public void start() {
        super.start();
        pSpaces.start();
        bodies.start();
        ghosts.start();

        impulses = ed.getEntities(EntityBuff.class, Impulse.class);
    }

    @Override
    public void update(SimTime time) {

        if (baseThread != Thread.currentThread()) {
            throw new IllegalStateException("The bullet system must be updated from the same thread it was initialized."
                    + " initialized from:" + baseThread + " updated from:" + Thread.currentThread());
        }

        super.update(time);

        startFrame(time);

        pSpaces.update();
        bodies.update();
        ghosts.update();

        // Run setup after we have the latest bodies and ghosts
        // We'll run it every time because when there are no pending setup
        // objects, it's essentially free... and when there are we don't 
        // know if the entity is ready or not yet.
        runPendingSetup();

        impulses.applyChanges();
        if (!impulses.isEmpty()) {
            // We don't really care if the set changed or not, we will
            // always iterate over all items until they are removed.
            // The applyImpulses() method will clear the current impulse
            // if the body exists for the entity.
            applyImpulses(impulses);
        }

        float t = (float) (time.getTpf() * speed);
        if (t != 0) {

            for (EntityRigidBody b : driverBodies.getArray()) {
                b.getControlDriver().update(time, b);
            }

            //pSpace.update(t);
            //pSpace.distributeEvents();
            for (PhysicsSpace pSpace : pSpaces.getArray()) {
                pSpace.update(t);
                pSpace.distributeEvents();
            }

            for (EntityPhysicsObject o : mobs.getArray()) {

                if (o instanceof EntityGhostObject) {
                    // The only reason its in the mobs array is because
                    // is has a parent
                    EntityGhostObject g = (EntityGhostObject) o;
                    EntityRigidBody parent = g.getParent();
                    if (parent == null) {
                        // May not have resolved yet
                        g.setParent(parent = bodies.getObject(g.getParentId()));
                    }
                    g.updateToParent();
                }

                objectUpdated(o);

                if (o instanceof EntityRigidBody) {
                    ((EntityRigidBody) o).updateLastVelocity();
                }
            }

            // Distribute updates for bodies that have drivers but are not
            // normal mobs.
            for (EntityRigidBody b : driverBodies.getArray()) {
                if (b.getMass() == 0) {
                    objectUpdated(b);
                }
            }
        }

        endFrame();
    }

    @Override
    public void stop() {
        pSpaces.filters.clear();
        impulses.release();
        pSpaces.stop();
        ghosts.stop();
        bodies.stop();
        super.stop();
    }

    @Override
    public List<EntityId> rayCast(Vector3f from, Vector3f to) {
        //List<PhysicsRayTestResult> rayTest = pSpace.rayTest(from, to);
        return null;//Lists.transform(rayTest, entityIdFunction);
    }

    protected void runPendingSetup() {
        ObjectSetup setup = null;
        while ((setup = pendingSetup.poll()) != null) {
            if (!setup.execute()) {
                // Add it back to the queue
                pendingSetup.add(setup);

                // If there is a lot of delay, this is horribly inefficient and it
                // would be better to peek and then remove... we hope that in general
                // the object exists already.
            }
        }
    }

    protected void applyImpulses(Set<Entity> impulses) {
        for (Entity e : impulses) {
            EntityBuff impulse = e.get(EntityBuff.class);
            EntityRigidBody body = bodies.getObject(impulse.getTarget());
            if (body == null) {
                // Skipping... we may not have created it yet.
                // Note: we need to be careful not to leak objects by
                //       partially destroying them such that the impulse
                //       entities still exist but the physics entities don't.
                log.warn("Missing body for:" + e.getId());
                continue;
            }

            // Apply the impulse
            Impulse imp = e.get(Impulse.class);
            if (imp.getLinearVelocity() != null) {
                //body.getObject().setLinearVelocity(imp.getLinearVelocity());
                body.getObject().applyImpulse(imp.getLinearVelocity(), new Vector3f());
            }
            if (imp.getAngularVelocity() != null) {
                //body.getObject().setAngularVelocity(imp.getAngularVelocity());
                body.getObject().applyTorqueImpulse(imp.getAngularVelocity());
            }

            // Remove the impulse component so the entity drops out of this
            // set
            ed.removeComponent(e.getId(), Impulse.class);
        }
    }

    private void startFrame(SimTime time) {
        for (PhysicsObjectListener l : objectListeners.getArray()) {
            l.startFrame(time);
        }
    }

    private void endFrame() {
        for (PhysicsObjectListener l : objectListeners.getArray()) {
            l.endFrame();
        }
    }

    private void objectAdded(EntityPhysicsObject o) {
        for (PhysicsObjectListener l : objectListeners.getArray()) {
            l.added(o);
        }
    }

    private void objectUpdated(EntityPhysicsObject o) {
        for (PhysicsObjectListener l : objectListeners.getArray()) {
            l.updated(o);
        }
    }

    private void objectRemoved(EntityPhysicsObject o) {
        for (PhysicsObjectListener l : objectListeners.getArray()) {
            l.removed(o);
        }
    }

    private EntityPhysicsObject toEntityPhysicsObject(Object o) {
        if (!(o instanceof EntityPhysicsObject)) {
            return null;
        }
        return (EntityPhysicsObject) o;
    }

    private class PhysicsSpaceContainer extends EntityContainer<PhysicsSpace> {

        Map<EntityId, ComponentFilter<PersistentEntityBuff>> filters = new HashMap<>();

        public PhysicsSpaceContainer(EntityData ed) {
            super(ed, Filters.fieldEquals(Scene.class, "enabled", true), Scene.class);
        }

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

        @Override
        protected PhysicsSpace addObject(Entity e) {
            PhysicsSpace pSpace = new PhysicsSpace(worldMin, worldMax, broadphaseType);
            pSpace.addCollisionListener(collisionDispatcher);
            filters.put(e.getId(), Filters.fieldEquals(PersistentEntityBuff.class, "target", e.getId()));
            resetFilters();
            return pSpace;
        }

        @Override
        protected void updateObject(PhysicsSpace object, Entity e) {

        }

        @Override
        protected void removeObject(PhysicsSpace object, Entity e) {
            filters.remove(e.getId());
            resetFilters();
            object.destroy();
        }

        private void resetFilters() {
            if (!filters.isEmpty()) {
                bodies.setFilter(Filters.or(PersistentEntityBuff.class, filters.values().toArray(new ComponentFilter[filters.size()])));
                ghosts.setFilter(Filters.or(PersistentEntityBuff.class, filters.values().toArray(new ComponentFilter[filters.size()])));
            }
        }
    }

    private class BodyContainer extends EntityContainer<EntityRigidBody> {

        public BodyContainer(EntityData ed) {
            super(ed, Filters.fieldEquals(PersistentEntityBuff.class, "target", EntityId.NULL_ID), PersistentEntityBuff.class, SpawnPosition.class, ShapeInfo.class, Mass.class);
        }

        @Override
        public EntityRigidBody[] getArray() {
            return super.getArray();
        }

        @Override
        protected void setFilter(ComponentFilter filter) {
            super.setFilter(filter);
        }

        @Override
        protected EntityRigidBody addObject(Entity e) {
            PersistentEntityBuff sceneBuff = e.get(PersistentEntityBuff.class);
            Mass mass = e.get(Mass.class);
            AaBBox bounds = new AaBBox();
            CollisionShape shape = shapes.getShape(e.getId(), e.get(ShapeInfo.class), bounds);
            EntityRigidBody result = new EntityRigidBody(sceneBuff.getTarget(), e.getId(), shape, mass, bounds);

            // Update the physics location from the SpawnPosition
            SpawnPosition pos = e.get(SpawnPosition.class);
            result.setPhysicsLocation(pos.getLocation());
            result.setPhysicsRotation(pos.getOrientation());

            if (log.isTraceEnabled()) {
                log.trace("pSpace.adding:" + result);
            }

            pSpaces.getObject(sceneBuff.getTarget()).add(result);

            objectAdded(result);
            if (mass.getMass() > 0) {
                mobs.add(result);
            } else {
                // We will also need to send the update
                //objectUpdated(result);
            }

            return result;
        }

        @Override
        protected void updateObject(EntityRigidBody object, Entity e) {
            PersistentEntityBuff sceneBuff = e.get(PersistentEntityBuff.class);
            if (!object.getSpaceId().equals(sceneBuff.getTarget())) {
                pSpaces.getObject(object.getSpaceId()).remove(object);
                pSpaces.getObject(sceneBuff.getTarget()).add(object);
                object.setSpaceId(sceneBuff.getTarget());
                if (object.getControlDriver() != null) {
                    object.getControlDriver().initialize(object);
                }
            }

            Mass mass = e.get(Mass.class);
            if (mass.getMass() == 0) {
                // See if it's the spawn position that has moved
                SpawnPosition pos = e.get(SpawnPosition.class);
                if (log.isTraceEnabled()) {
                    log.trace("Moving " + object + "  to:" + pos);
                }
                object.setPhysicsLocation(pos.getLocation());
                object.setPhysicsRotation(pos.getOrientation());
                objectUpdated(object);
            }
        }

        @Override
        protected void removeObject(EntityRigidBody object, Entity e) {
            if (log.isTraceEnabled()) {
                log.trace("pSpace.removing:" + object);
            }

            PhysicsSpace pSpace;
            // in case physics space is not already removed 
            if ((pSpace = pSpaces.getObject(object.getSpaceId())) != null) {
                pSpace.remove(object);
            }

            // Could be optimized to check if it's a mob first
            mobs.remove(object);

            if (object.getControlDriver() != null) {
                driverBodies.remove(object);
            }

            objectRemoved(object);
        }
    }

    private class GhostContainer extends EntityContainer<EntityGhostObject> {

        public GhostContainer(EntityData ed) {
            super(ed, Filters.fieldEquals(PersistentEntityBuff.class, "target", EntityId.NULL_ID), PersistentEntityBuff.class, SpawnPosition.class, ShapeInfo.class, Ghost.class);
        }

        @Override
        public EntityGhostObject[] getArray() {
            return super.getArray();
        }

        @Override
        protected void setFilter(ComponentFilter filter) {
            super.setFilter(filter);
        }

        @Override
        protected EntityGhostObject addObject(Entity e) {
            PersistentEntityBuff sceneBuff = e.get(PersistentEntityBuff.class);
            Ghost ghost = e.get(Ghost.class);
            AaBBox bounds = new AaBBox();
            CollisionShape shape = shapes.getShape(e.getId(), e.get(ShapeInfo.class), bounds);
            EntityGhostObject result = new EntityGhostObject(sceneBuff.getTarget(), e.getId(), shape, ghost.getCollisionMask(), bounds);

            SpawnPosition pos = e.get(SpawnPosition.class);
            if (ghost.getParentEntity() != null) {
                // See if the parent body is already created
                EntityRigidBody parent = bodies.getObject(ghost.getParentEntity());

                // Either way, setup the rest of the stuff for the parent
                result.setParent(ghost.getParentEntity(), parent, pos);
            } else {
                // Update the physics location from the SpawnPosition
                result.setPhysicsLocation(pos.getLocation());
                result.setPhysicsRotation(pos.getOrientation());
            }

            pSpaces.getObject(sceneBuff.getTarget()).add(result);

            objectAdded(result);
            if (ghost.getParentEntity() != null) {
                // Then we update it like a mob
                mobs.add(result);
            } else {
                // We will also need to send the update since no more
                // will be coming.
                //objectUpdated(result);
            }

            return result;
        }

        @Override
        protected void updateObject(EntityGhostObject object, Entity e) {
            // Could allow offset adjustment through spawn position changes

            PersistentEntityBuff sceneBuff = e.get(PersistentEntityBuff.class);
            if (!object.getSpaceId().equals(sceneBuff.getTarget())) {
                pSpaces.getObject(object.getSpaceId()).remove(object);
                pSpaces.getObject(sceneBuff.getTarget()).add(object);
                object.setSpaceId(sceneBuff.getTarget());
            }
        }

        @Override
        protected void removeObject(EntityGhostObject object, Entity e) {

            PhysicsSpace pSpace;
            // in case physics space is not already removed 
            if ((pSpace = pSpaces.getObject(object.getSpaceId())) != null) {
                pSpace.remove(object);
            }

            // Could be optimized to check if it's a mob first
            mobs.remove(object);

            objectRemoved(object);
        }
    }

    private class ObjectSetup {

        EntityId objectId;
        Function<EntityPhysicsObject, ?> function;
        int tries = 0;

        public ObjectSetup(EntityId objectId, Function<EntityPhysicsObject, ?> function) {
            this.objectId = objectId;
            this.function = function;
        }

        public boolean execute() {
            EntityRigidBody body = bodies.getObject(objectId);
            if (body != null) {
                function.apply(body);
                return true;
            }
            // Don't know what this would be used for but it's easy to support
            EntityGhostObject ghost = ghosts.getObject(objectId);
            if (ghost != null) {
                function.apply(ghost);
                return true;
            }

            tries++;
            if (tries > 100) {
                log.warn("Object setup for:" + objectId + " exceeded 100 tries waiting for object.  Aborting setup.");
                return true;
            }
            return false;
        }
    }

    private class CollisionDispatcher implements PhysicsCollisionListener {

        public void collision(PhysicsCollisionEvent event) {
            EntityPhysicsObject a = toEntityPhysicsObject(event.getObjectA());
            EntityPhysicsObject b = toEntityPhysicsObject(event.getObjectB());
            if (a == null && b == null) {
                // Nothing to deliver
                return;
            }

            if (collisionFilter != null && collisionFilter.filterCollision(a, b, event)) {
                return;
            }

            /*log.info("A:" + event.getObjectA() 
                    + " B:" + event.getObjectB()
                    + "\n    type:" + event.getType() 
                    + " A wp:" + event.getPositionWorldOnA()
                    + " B wp:" + event.getPositionWorldOnB()
                    + "\n    B wn:" + event.getNormalWorldOnB()
                    + " dist:" + event.getDistance1());*/
            for (EntityCollisionListener l : collisionListeners) {
                l.collision(a, b, event);
            }

            // Now deliver it to any control drivers if needed
            if (a.getControlDriver() != null) {
                a.getControlDriver().addCollision(b, event);
            }
            if (b.getControlDriver() != null) {
                b.getControlDriver().addCollision(a, event);
            }
        }
    }

    private static class EntityIdFunction implements Function<PhysicsRayTestResult, EntityId> {

        public EntityId apply(PhysicsRayTestResult result) {
            if (result == null) {
                return null;
            }
            Object entityId = result.getCollisionObject().getUserObject();
            return entityId != null ? (EntityId) entityId : null;
        }
    }

}

There for each active Scene I create a Bullet PhysicSpace and add entity bodies to their corresponding physic space based on the scene id of each body I get from PersistentEntityBuff component.
I mean in my systems I have no problem and it works well I am talking about SimEtheral internal for example will this SharedObjectSpace which I do not know what it does or this ZoneManager which acts based on body position information, will they perform correctly ?

@Ali_RS

I extended ethereal to have a built in multi scene / room feature within it. Basically each user’s connection only sends updates for entities that are in the user’s scene and there is a mechanism to flip the user from scene to scene either from a request from the client or directly from the server. Works like the traditional “rooms” feature in commercial net servers

1 Like

I do not have enough knowledge to do these stuff on my own and I have stuck to what SimEtheral is providing.

Basically each user’s connection only sends updates for entities that are in the user’s scene

I use Filters from client side to do this and server has no control over it i suppose !?:wink:

In my case I have thousands of scenes running on the server. If I filtered on the client I would be flooding the connection with tons of objects that the client would not even display. So I filter on the server’s side of the client connection so only the objects that belong to the scene that the client is in get transmitted.

Fortunately in my game, each Party(Team) has just one active scene at a time which is limited (x = 1000m, z = 1000m). After the party win current level, the scene dispose and a new level starts which means a new Scene with new story.