[SOLVED] Zay-es Net remove entities too far from client

Hi there,
I’ve read in the jme forum but I still don’t get it how to remove entities on client side that are too far away from the players position.

My use case:
I’m using zay-es Net for my multiplayer game. There is a server and multiple clients. Clients can move in a huge world and their view distance is fairly high. Because of the high view distance, I need to remove all entities that are more than e.g. 100m away from the client to increase rendering performance and reduce network traffic. More than that I don’t want to send unnecessary information to the clients to prevent cheating.

Every entity has a position component and I’ve read there are component filters. But I don’t get how to use it or how to remove entities for specific clients on the basis of the distance between client and entity.

Also I’ve found this thread regarding reducing the network traffic using filters, but I don’t know if it is fixed now.

You can filter based on position but you probably want to include something like a grid cell ID in your Position component to make that easier.

Or you can use something like SimEthereal which can take care of a lot of the issues for you: GitHub - Simsilica/SimEthereal: A high performance library for real-time networked object synching.

…and is WAY more efficient than using zay-es-net alone. Example of using Zay-ES and SimEthereal here: Examples/sim-eth-es at master · Simsilica/Examples · GitHub

Combining the two, it’s possible to have SimEthereal tell Zay-ES-net which entities to ignore. For each HostedEntityData that is created you can register a component visibility filter.

HostedEntityData hed = eds.getHostedEntityData(conn);
hed.registerComponentVisibility(new YourComponentVisibility(ethereal.getStateListener(conn))); 

A ComponentVisibility implementation can filter a specific type of component by entity ID… or in the case above, the active ID set list for that SimEthereal client.

I guess I don’t have any public examples of this yet. But this is a quick cut-paste of my own filter I use in one of my MOSS demos:

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;
    }


}

Edit: actually I think it filters all entities not in the active set for that client… as long as that have a BodyComponent (in that example) on the server.

1 Like

Thank you for this super fast response. I will look into this :slight_smile:

It works! Thanks that was very useful :slight_smile:

1 Like

Sorry but one question remains: What if I want to hide multiple components by the ComponentVisibility? Do I need to create a new component just to tag the visibility and add it to all systems which requires this visibility feature? Because I no longer want to send information such as position, health, animations trigger and some others when the entity is “out of sight”.

How is the client selecting entities that are “out of sight” if not including the position somehow?

The server calculates if an entity is out of sight from a client because he knows the position of the client and the entity. Both use the position component. But the client still receive other component updates such as for the health component.

Only if the client is asking for “all entities with a health component” and leaving position out. Why is the client asking for “every entity in the world with a health component”? That seems like a weird design.

So my HealthSystem has to use this:
entities = ed.getEntities(HealthComponent.class, PositionComponent.class);
instead of this?
entities = ed.getEntities(HealthComponent.class);

Why is the health system running on all of the clients instead of once on the server?

Ah now I get it sry :smiley:
The HealthSystem is indeed running on server side only. But there are other systems. It just was an example that other systems can still receive updates.

So you mean it is bad design when other components except the PositionComponent requires the visibility feature?

Well, my question for anything on the client that is not using the position component is “Why is the client asking for everything in the world instead of just the stuff nearby?”

What does the client need to know that isn’t specifically related to the player or specifically related to position? It’s a sign that the client is trying to be too smart.

Hm okay that make sense. I have to think about this :slight_smile: Thank you

Yeah, I would need specific examples to answer further… ones that aren’t really already on the server in the end. :slight_smile:

Good morning :smiley:
Okay I slept about it one night. Let’s say we want to see health bars above NPC heads and to trigger animations.

For this I would create 2 components and systems that will run on client side because the client should show the bars and play the animations. Further more this should only be the case for all nearby entities. That means I would use the visibility feature that use the position component.

Now we are in the case that the server still send way too much information to the client. The clients knows about the health points of all entities. The animations cannot be played when entities are too far away from the client because the spatials were detached. But the animation triggers are still sent over the network.

What should I do now? Do I have to add the position component to the healthbar and animation system? Is this still cheat safe?

Where are you going to put the health bars?

I have a model system which use the position component and the model type component. The model type contains the name of the model (j3o file). The model system then loads the model, attaches the spatials to the scene graph and update the spatial translations by using the position component.

The animation and the health bar system get the spatials from the model system (hashmap with public getter). If the spatial was found, the bar will be attached / the animation is triggered. If the entities are out of sight, then the spatial cannot be found. But the client still got unecessary information from the server through the component updates.

But it makes no sense to request health components for things that don’t have position… because they are also guaranteed to never have a model loaded.

In fact, it’s an implementation detail that you even attach them to your models at all.

After all, you could have implemented the health bars 100% independent of the models.

Ok so this means what should I do is to also add the position component to the healthbar system? :see_no_evil:

To me, it makes no sense to request “all entities with health components” if you are going to display them… because by definition you will need a position to display them.

So, yes, I’m not sure how many different ways that I can say the same thing.

This makes sense:
getEntities(Health.class, Position.class)

This doesn’t:
getEntities(Health.class)
…because then you will potentially get entities that never will have displayed health bars.

Edit: also it’s more the ES way to do this… and potentially display the health bars on their own even if they don’t have a model.