How to filter buffed entities based on scene id in client side?


#1

Hi

I will explain my question on MobAnimationState of SiO2 bullet-char demo as an example.

In a networking game where we have multiple scenes, each client wants to listen for entities related to the scene it’s avatar is attached to.

I modified MobAnimationState based on my networked game, and I doubt if the way I am using to filter MobilityContainer and ActionContainer is right and efficient or is there a better way ?

Mobility and CharacterAction entities are buffed (parented) to mob entities and mob entities are buffed to a scene. I want to only get Mobility and CharacterAction entities which are happening in the scene that player is but there is no direct component on these entities to indicate to which scene they are belong. So to filter them I can see two ways :

1- Add a new SceneID component to each of those Mobility and CharacterAction entities then filter directly based on scene id. But then you need to update the SceneID component when character attaches to another scene.
2- Use a second filter, First find all mobs which are in the specified scene (where client avatar is attached) then filter Mobility and CharacterAction entities based on those mob ids. (I am using this approach in client side app states) .

I created a MobAnimationContainer which find all mobs in the specified scene

 private void onSceneChanged(SceneChangeEvent event) {
            if (isEnabled()) {
                mobAnimations.setFilter(Filters.fieldEquals(Buff.class, "target", event.getSceneId()));
            }
        }

then set a filter on MobilityContainer and ActionContainer.

private class MobAnimationContainer extends EntityContainer<MobAnimation> {

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

        public MobAnimationContainer(EntityData ed) {
            super(ed, Filters.fieldEquals(Buff.class, "target", EntityId.NULL_ID), Buff.class, BodyPosition.class);
        }

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

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

        @Override
        protected MobAnimation addObject(Entity entity) {
            filters.put(entity.getId(), Filters.and(Buff.class,
                    Filters.fieldEquals(Buff.class, "state", DefaultBuffStates.ENABLED),
                    Filters.fieldEquals(Buff.class, "target", entity.getId())));
            resetFilters();

            MobAnimation result = new MobAnimation(entity.getId());
            return result;
        }

        @Override
        protected void updateObject(MobAnimation mobAnimation, Entity entity) {

        }

        @Override
        protected void removeObject(MobAnimation t, Entity entity) {
            filters.remove(entity.getId());
            resetFilters();
        }

        public void resetFilters() {
            if (!filters.isEmpty()) {
                mobilities.setFilter(Filters.or(Buff.class, filters.values().toArray(new ComponentFilter[filters.size()])));
                actions.setFilter(Filters.or(Buff.class, filters.values().toArray(new ComponentFilter[filters.size()])));
            }
        }

    }

and this is how MobilityContainer and ActionContainer look like

  @SuppressWarnings("unchecked")
    private class MobilityContainer extends EntityContainer<BuffedComponent<Mobility>> {

        public MobilityContainer(EntityData ed) {
            super(ed, Filters.fieldEquals(Buff.class, "target", EntityId.NULL_ID), Buff.class, Mobility.class);
        }

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

        @Override
        protected BuffedComponent<Mobility> addObject(Entity e) {
            Buff p = e.get(Buff.class);
            Mobility m = e.get(Mobility.class);
            addMobility(p.getTarget(), m);
            return new BuffedComponent<>(p.getTarget(), m);
        }

        @Override
        protected void updateObject(BuffedComponent<Mobility> object, Entity e) {
        }

        @Override
        protected void removeObject(BuffedComponent<Mobility> object, Entity e) {
            removeMobility(object.parentId, object.value);
        }
    }

///

@SuppressWarnings("unchecked")
private class ActionContainer extends EntityContainer<BuffedComponent<CharacterAction>> {

    public ActionContainer(EntityData ed) {
        super(ed, Filters.fieldEquals(Buff.class, "target", EntityId.NULL_ID), Buff.class, CharacterAction.class);
    }

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

    @Override
    protected BuffedComponent<CharacterAction> addObject(Entity e) {
        Buff p = e.get(Buff.class);
        CharacterAction a = e.get(CharacterAction.class);
        addAction(p.getTarget(), a);
        return new BuffedComponent<>(p.getTarget(), a);
    }

    @Override
    protected void updateObject(BuffedComponent<CharacterAction> object, Entity e) {
    }

    @Override
    protected void removeObject(BuffedComponent<CharacterAction> object, Entity e) {
        removeAction(object.parentId, object.value);
    }
}

Is this the right way and efficient way to do it ?


#2

What do you mean by “scene”?

Why aren’t these different “scenes” just spaced in different parts of the world where SimEthereal, etc. will already filter them?


#3

I mean SimEthereal will filter only Mobs with BodyPosition, where as Mobility and CharacterAction are not physical entities and have no BodyPosition, they are just buffed to a mob entity.
How can SimEthereal help with filtering in ActionContainer and MobilityContainer ?


#4

Ah, I see what you mean.

You could create a component visibility filter similar to what I did for BodyPosition that would only let these components through to the client if they were pointing to one of the active entities.

/*
 * $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.simsilica.demo.server;

import java.util.*;

import org.slf4j.*;

import com.simsilica.es.*;
import com.simsilica.es.server.ComponentVisibility;
import com.simsilica.ethereal.NetworkStateListener;

import com.simsilica.demo.es.BodyPosition;

/**
 *  Limits the client's visibility of any entity containing a BodyPosition
 *  to just what the SimEthereal visibility says they can see.
 *
 *  @author    Paul Speed
 */
public class BodyVisibility implements ComponentVisibility {

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

    private NetworkStateListener netState;
    private EntityData ed;

    private Set<Long> lastActiveIds;

    private Map<EntityId, BodyPosition> lastValues = new HashMap<>(); 

    protected BodyVisibility( NetworkStateListener netState, Set<Long> lastActiveIds ) {
        this.netState = netState;
        this.lastActiveIds = lastActiveIds;
    } 
    
    public BodyVisibility( NetworkStateListener netState ) {
        this(netState, null);
    }

    @Override
    public Class<? extends EntityComponent> getComponentType() {
        return BodyPosition.class;
    }
    
    @Override
    public void initialize( EntityData ed ) {
        this.ed = ed;
    }

    @Override
    public <T extends EntityComponent> T getComponent( EntityId entityId, Class<T> type ) {
log.info("getComponent(" + entityId + ", " + type + ")");    
        //if( !netState.getActiveIds().contains(entityId) ) {
        //    return null;
        //}
        if( !lastValues.containsKey(entityId) ) {
            return null;
        }
        return ed.getComponent(entityId, type);
    }

    @Override
    public Set<EntityId> getEntityIds( ComponentFilter filter ) {
        if( log.isTraceEnabled() ) {
            log.trace("getEntityIds(" + filter + ")");
        }    
        if( filter != null ) {
            throw new UnsupportedOperationException("Filtering + body visibility not yet supported");
        }

        /*Set<Long> active = netState.getActiveIds();
        log.info("active:" + active);
 
        Set<EntityId> results = new HashSet<>();
        for( Long l : active ) {
            results.add(new EntityId(l));
        }
    
        return results;*/
        return lastValues.keySet();    
    }

    public boolean collectChanges( Queue<EntityChange> updates ) {
        Set<Long> active = netState.getActiveIds();
        boolean changed = false;
        if( log.isTraceEnabled() ) {
            log.trace("active:" + active);
            log.info("updates before:" + updates);
        }
 
        // Remove any BodyPosition updates that don't belong to the active
        // set
        for( Iterator<EntityChange> it = updates.iterator(); it.hasNext(); ) {
            EntityChange change = it.next();
            if( change.getComponentType() == BodyPosition.class 
                && !active.contains(change.getEntityId().getId()) ) {
                if( log.isTraceEnabled() ) {
                    log.trace("removing irrelevant change:" + change);
                }                
                it.remove();
            }
        }        
        
        // First process the removals
        for( Iterator<EntityId> it = lastValues.keySet().iterator(); it.hasNext(); ) {
            EntityId id = it.next();
            if( active.contains(id.getId()) ) {
                continue;
            }
            if( log.isTraceEnabled() ) {
                log.trace("removing:" + id);
            }
            updates.add(new EntityChange(id, BodyPosition.class));
            it.remove();
            changed = true;
        }
        
        // Now the adds
        for( Long l : active ) {
            EntityId id = new EntityId(l);
            if( lastValues.containsKey(id) ) {
                continue;
            }
            if( log.isTraceEnabled() ) {
                log.trace("adding:" + id);
            }
            BodyPosition pos = ed.getComponent(id, BodyPosition.class);
            lastValues.put(id, pos);
            updates.add(new EntityChange(id, pos)); 
            changed = true;
        }

if( changed ) {
    log.info("done collectChanges() " + active);
} 
        
        return changed;
    }
}

I register that one like:
hed.registerComponentVisibility(new BodyVisibility(ethereal.getStateListener(conn)));

During initialization of the game session on the server… ‘hed’ is the HostedEntityData for that connection.

            // Setup to start using SimEthereal synching
            EtherealHost ethereal = getService(EtherealHost.class); 
            ethereal.startHostingOnConnection(conn);
            ethereal.setConnectionObject(conn, avatarEntity.getId(), spawnLoc);                   
            EntityDataHostedService eds = getService(EntityDataHostedService.class);

            // Setup a filter for BodyPosition components to match what
            // SimEthereal says is visible for the client.
            HostedEntityData hed = eds.getHostedEntityData(conn);
            hed.registerComponentVisibility(new BodyVisibility(ethereal.getStateListener(conn))); 

#5

Cool, thanks so much for the help.
So the new component visibility filter in ZayES is the way to go. :wink:


#6

In general, you probably want to avoid having a LOT of these but it does make things a lot simpler when the client won’t even see the components that are attached to entities that it won’t be seeing anyway.


#7

@pspeed

should this be okay for MobilityVisibility

public class MobilityVisibility implements ComponentVisibility {

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

    private NetworkStateListener netState;
    private EntityData ed;

    private Set<Long> lastActiveIds;

    //private Map<EntityId, Mobility> lastValues = new HashMap<>(); 
    protected MobilityVisibility(NetworkStateListener netState, Set<Long> lastActiveIds) {
        this.netState = netState;
        this.lastActiveIds = lastActiveIds;
    }

    public MobilityVisibility(NetworkStateListener netState) {
        this(netState, null);
    }

    @Override
    public Class<? extends EntityComponent> getComponentType() {
        return Mobility.class;
    }

    @Override
    public void initialize(EntityData ed) {
        this.ed = ed;
    }

    @Override
    public <T extends EntityComponent> T getComponent(EntityId entityId, Class<T> type) {
        log.info("getComponent(" + entityId + ", " + type + ")");
        if (!netState.getActiveIds().contains(entityId.getId())) {
            return null;
        }
//        if( !lastValues.containsKey(entityId) ) {
//            return null;
//        }
        return ed.getComponent(entityId, type);
    }

    @Override
    public Set<EntityId> getEntityIds(ComponentFilter filter) {
        if (log.isTraceEnabled()) {
            log.trace("getEntityIds(" + filter + ")");
        }
        if (filter != null) {
            throw new UnsupportedOperationException("Filtering + visibility not yet supported");
        }

        Set<Long> active = netState.getActiveIds();
        log.info("active:" + active);

        Set<EntityId> results = new HashSet<>();
        for (Long l : active) {
            results.add(new EntityId(l));
        }

        return results;
        //return lastValues.keySet();    
    }

    public boolean collectChanges(Queue<EntityChange> updates) {
        Set<Long> active = netState.getActiveIds();
        boolean changed = false;
        if (log.isTraceEnabled()) {
            log.trace("active:" + active);
            log.info("updates before:" + updates);
        }

        // Remove any Mobility updates that don't belong to the active
        // set
        for (Iterator<EntityChange> it = updates.iterator(); it.hasNext();) {
            EntityChange change = it.next();
            if (change.getComponentType() == Mobility.class
                    && !active.contains(change.getEntityId().getId())) {
                if (log.isTraceEnabled()) {
                    log.trace("removing irrelevant change:" + change);
                }
                it.remove();
            }
        }
 
        return changed;
    }
}

and similar one for CharacterActionVisibility.

Not sure if I am understanding this part

// First process the removals
        for( Iterator<EntityId> it = lastValues.keySet().iterator(); it.hasNext(); ) {
            EntityId id = it.next();
            if( active.contains(id.getId()) ) {
                continue;
            }
            if( log.isTraceEnabled() ) {
                log.trace("removing:" + id);
            }
            updates.add(new EntityChange(id, Mobility.class));
            it.remove();
            changed = true;
        }
        
        // Now the adds
        for( Long l : active ) {
            EntityId id = new EntityId(l);
            if( lastValues.containsKey(id) ) {
                continue;
            }
            if( log.isTraceEnabled() ) {
                log.trace("adding:" + id);
            }
            Mobility pos = ed.getComponent(id, Mobility.class);
            lastValues.put(id, pos);
            updates.add(new EntityChange(id, pos)); 
            changed = true;
        } 

should that be included also ?
If not, then should I just return false in collectChanges ?


#8

Those last loops are to cause events to be sent to the clients when things go out of and into scope.

We’re using the active IDs set from the SimEthereal layer and that’s going to change independent of entities. These loops are detecting entities that are no longer visible and making sure to send a remove component event… and they’re detecting new entities that we hadn’t seen before and sending a component update event.

…else the client side entity sets will never change.

For components that change a lot like action, it’s not so critical. But for long-lived components like BodyPosition and Mobility, you want to make sure those sets update themselves as the active object moves in and out of view.


#9

Ah, I see. Thanks for clarification.


#10

@pspeed there is something that I cant figure out

In my MobilityVisibility class in collectChanges() :

// Remove any Mobility updates that don't belong to the active
        // set
        for (Iterator<EntityChange> it = updates.iterator(); it.hasNext();) {
            EntityChange change = it.next();
            if (change.getComponentType() == Mobility.class
                    && !active.contains(change.getEntityId().getId())) {
                if (log.isTraceEnabled()) {
                    log.trace("removing irrelevant change:" + change);
                }
                it.remove();
            }
        }

this line is off course wrong

 if (change.getComponentType() == Mobility.class
                    && !active.contains(change.getEntityId().getId()))

I want to check against the target (parent) id not Mobility entity id.

I mean this should be the correct way :

for (Iterator<EntityChange> it = updates.iterator(); it.hasNext();) {
            EntityChange change = it.next();
            Buff buff;
            if (change.getComponentType() == Buff.class && (buff = (Buff) change.getComponent()).getType() == BuffTypes.MOBILITY
                    && !active.contains(buff.getTarget().getId())) { 

then should I use a BuffVisibility instead of a MobilityVisibility ?


#11

I just upgraded the network layer to not even transmit any updates for object that are not in the Scene. The Scene ID is baked into a modified version of @pspeed’s network library and into my Mob app state.


#12

Yes, that’s wrong… you aren’t trying to filter Mobility entities… you are trying to filter out mobility components POINTING to those entities. So you have to get the Mobility object and check its target entity Id.

I don’t understand what all of the Buff stuff is, so I can’t comment. Not sure why that’s needed as it feels like polymorphic bad-ju-ju tying all of your entity systems together unnecessarily. But I could be reading it wrong.

Edit: forgot that Mobility does not contain its own target.


#13

It’s even easier then because you only need to filter Parent and everything else should be fine:


#14

@pspeed can you please take a look at this implementation. Should this be fine ?

public class ParentVisibility implements ComponentVisibility {

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

    private NetworkStateListener netState;
    private EntityData ed;

    private Set<Long> lastActiveIds;

    private Map<EntityId, Parent> lastValues = new HashMap<>();

    protected ParentVisibility(NetworkStateListener netState, Set<Long> lastActiveIds) {
        this.netState = netState;
        this.lastActiveIds = lastActiveIds;
    }

    public ParentVisibility(NetworkStateListener netState) {
        this(netState, null);
    }

    @Override
    public Class<? extends EntityComponent> getComponentType() {
        return Parent.class;
    }

    @Override
    public void initialize(EntityData ed) {
        this.ed = ed;
    }

    @Override
    public <T extends EntityComponent> T getComponent(EntityId entityId, Class<T> type) {
        log.info("getComponent(" + entityId + ", " + type + ")");
        //if( !netState.getActiveIds().contains(entityId) ) {
        //    return null;
        //}
        if (!lastValues.containsKey(entityId)) {
            return null;
        }
        return ed.getComponent(entityId, type);
    }

    @Override
    public Set<EntityId> getEntityIds(ComponentFilter filter) {
        if (log.isTraceEnabled()) {
            log.trace("getEntityIds(" + filter + ")");
        }
        if (filter != null) {
            throw new UnsupportedOperationException("Filtering + body visibility not yet supported");
        }

        /*Set<Long> active = netState.getActiveIds();
        log.info("active:" + active);
 
        Set<EntityId> results = new HashSet<>();
        for( Long l : active ) {
            results.add(new EntityId(l));
        }
    
        return results;*/
        return lastValues.keySet();
    }

    public boolean collectChanges(Queue<EntityChange> updates) {
        Set<Long> active = netState.getActiveIds();
        boolean changed = false;
//        if( log.isTraceEnabled() ) {
//            log.trace("active:" + active);
//            log.info("updates before:" + updates);
//        }

        // Remove any Parent updates that don't belong to the active
        // set
        for (Iterator<EntityChange> it = updates.iterator(); it.hasNext();) {
            EntityChange change = it.next();
            if (change.getComponentType() == Parent.class
                    && !active.contains(((Parent) change.getComponent()).getParentId().getId())) {
                if (log.isTraceEnabled()) {
                    log.trace("removing irrelevant change:" + change);
                }
                it.remove();
            }
        }

        // First process the removals
        for (Iterator<Map.Entry<EntityId, Parent>> it = lastValues.entrySet().iterator(); it.hasNext();) {
            Map.Entry<EntityId, Parent> entry = it.next();
            if (active.contains(entry.getValue().getParentId().getId())) {
                continue;
            }
            if (log.isTraceEnabled()) {
                log.trace("removing:" + entry.getKey());
            }
            updates.add(new EntityChange(entry.getKey(), Parent.class));
            it.remove();
            changed = true;
        }

        // Now the adds
        for (Long l : active) {
            EntityId id = new EntityId(l);
            if (lastValues.containsValue( new Parent(id))) {
                continue;
            }
            if (log.isTraceEnabled()) {
                log.trace("adding:" + id);
            }
            
            Set<EntityId> findEntities = ed.findEntities(Filters.fieldEquals(Parent.class, "parentId", id), Parent.class);
            findEntities.forEach(child -> {
                Parent parent = lastValues.put(child, new Parent(id));
                updates.add(new EntityChange(child, parent));
            });

            changed = true;
        }

        if (changed) {
            log.info("done collectChanges() " + active);
        }

        return changed;
    }
}

#15

This is returning all of the active Ids and not just the ones with Parent components.

I got away with this for BodyPosition because all active IDs will always have a BodyPosition.

That part is a little unfortunate… but I guess there is no way around it.

I do wonder what advantage the closure form of this loop has over a standard for( : ) loop. It seems to me like there would always be some overhead to create the closure and it’s actually MORE code than the standard loop.

            Set<EntityId> findEntities = ed.findEntities(Filters.fieldEquals(Parent.class, "parentId", id), Parent.class);
            findEntities.forEach(child -> {
                Parent parent = lastValues.put(child, new Parent(id));
                updates.add(new EntityChange(child, parent));
            });

Versus:

            for( EntityId child : ed.findEntities(Filters.fieldEquals(Parent.class, "parentId", id), Parent.class) ) {
                Parent parent = lastValues.put(child, new Parent(id));
                updates.add(new EntityChange(child, parent));
            };

Is this just an idiomatic habit or is there some additional advantage to the first form?

My impression was the closure forms were better when there was additional processing on the stream… and then creating the Closure instance is worthwhile.


#16

Well, there is one way around it and that’s to have some “ParentSystem” that’s only job is to keep track of the parent IDs… then these visibility filters could do an intersection of the sets.

Probably not worth the trouble, though. It would eliminate duplicate effort across the one-per-client HostedEntityData instances. So maybe something to keep in mind.


#17

I read/watched in a few tutorials/benchmarks that were saying first form would always be faster because it is done internally by JVM.
I will do more search on it to get sure about it.


#18

That would be odd… the byte code for the second form can’t get much simpler.


#19

My understanding is that a for loop is faster than a stream or foreach lambda function. But this is not always true, it depends on the use case. This is a good read if you get time, it talks about it a bit. But there is a lot of information out there.
https://www.beyondjava.net/performance-java-8-lambdas


#20

In a tutorial I watched previously I got tricked by the guy in video who suggested to use foreach by saying it is more performant because it does an internal iteration whereas enhanced for loop is an external iteration (I really did not understand what did he mean).
I put the video here. Watch from time 3:53

After more research on this it seems as Paul said it is more usable for working with streams and specially when doing parallel processing.