Zay-ES game settings and sessions

Instead of necro’ing, I thought I’d link to a relevant post instead:

Two questions:

  1. Game settings (e.g. how fast does the player run, what gravitational forces are in play)
    • Q: would there ever be a reason to put them onto a ‘settings’ entity ?
  2. In my game, I have a hierarchy of the game so to speak: 1 Server > n Arena(s)
    • Q: A player is at any time in an arena and interacts with only the players in that arena and abides by the arena specific game settings in that arena (with an arena specific map)
      Is a component ‘arena’ enough to act as ‘game session’ (thinking of the linked post by @pspeed )- what other stuff would there be? Arena could then be the 'filtering on the client view and on the server physics side (where I imagine a physical world per arena would be enough) - can’t think of other backend systems that would need to filter based on the (but maybe just because my game is simple and Physics is so central to any logic in the game)

I guess, if multiple states are observing said entity, it saves you from implementing an Observer pattern on the settings.

Edit: But I guess it’s a lot of different components (one for each setting), unless it’s bulked into fewer components, negates the component design

The more I think about it, the harder it is to find a good reason not to make everything components, and divide the settings into multiple layers:

  • Server layer (a factor of every setting that is server wide) - The server could be a watchedentity
  • Arena layer (a factor of every setting that is arena wide) - The arenas could be entities
  • An entity layer (factor of every setting that is entity specific)

Then when figuring out what a setting is equal to, you’d sum up the three.

How do you guys handle these kinds of settings?

I can’t speak to how ECS users handle this because I don’t use an ECS. However, I think that modeling servers/arenas as entities sounds odd. I wouldn’t think of them as entities, but as configurable environments. I handle server settings through a configuration system that pulls configuration keys/values out of a Java properties file. I personally would handle arenas outside of the ECS and configure them in a similar fashion, running a separate ECS for each arena (the arenas are totally separate, right?).

Yes, totally seperate.

So I would need a seperate ECS and Physics Space for each arena. But every setting that is in the arena is affecting the players… I think…otherwise it wouldn’t need to be there.

I haven’t looked into java properties file, I’m just using ini-files at the moment, to read settings in.

It’s kind of up to you where you want to put the separation and how much filtering you want to do on what goes to the client… but yeah, an ECS per arena (a game session per arena, and an ECS per game session) makes sense.

Why does the client need to know this information? The game logic for these should be handled on the server. If it’s just informational for the user then an argument could be made that you just have a general info (text) object that includes it or something.

…but I wanted to make sure you weren’t using the actual speed, etc. on the client because that’s incorrect.

But for an visual or auditive output it might be helpful. For example when you want to play footstep sounds which play faster when the speed is higher. But this could probably be made somehow different too. :slight_smile:

Yeah, by detecting the actual speed and playing the sound as appropriate. Then you don’t have to worry about components being synched to possibly faster position network packets, etc.

How would you detect the actual speed? Sorry if this sounds newby but I think I am missing something here :smile:

old position new postion and time elapsed.

Ah okay, well this just seems a little to “complicated” in my eyes. But maybe I am wrong…

float speed = newVec.subtract(oldVec).length()?

…not that complicated.

But if you want to sync visuals to sounds then you should calculate the sounds to go with the visuals. That way you avoid footsteps starting after the player has moved and continuing when they stop… or footsteps while they are floating in the air, etc.

I mean, I really doubt the server is sending every animation frame to the client so if the player is walking there must already be some client-side determination.

It‘s not. I try to sync as little as posible to the client (and still have too many things). And saying that I know what unnecessary sync I will remove tomorrow :smile:

Alright. Thank you very much for your insight, @pspeed and @ia97lies! :slight_smile:

Thanks for the input everyone.

The Game Session per Arena and ECS på Game Session is a bit over my level (modifying Sim-eth-es to handle this). If anyone has done this I’d love some pointers, otherwise I will leave that idea for now and work on other stuff :slight_smile:

My original idea was to add ‘Arena’ as a component and account for that component both on the server side and client side.

That could work also. “Best” is relative if you aren’t ready to take apart the existing code.

You will want to pick a dimension to separate your arenas in space. Give them a wide margin. You may also want to insert some low-level filtering on the server for each client at some point… but you start to get to the point where an EntityData per arena is just as easy to implement.

Note: Sim-ethereal doesn’t care about Zay-ES. They are separate. So modifying the demo code to use a different EntityData database per arena but the same space in Sim-ethereal shouldn’t be too difficult when you want to look into that.

I think it should be simply to create a custome EntityDataHostedService

which keeps multiple EntityData (each per arena) and adds corresponding EntityData to each connection.

Please correct me if I am wrong ?

btw, @asser_fahrenholz are you creating a MOBA game ?

I’m porting an old old game. Simplified, it’s something like:

  • 2d (top-down) space-shooter
  • 8 different classes of space ships
  • 1 server usually has a theme
  • multiple arenas per server, arenas defines what the ‘game’ is
  • possible game types: capture the flag, soccer, racing, deathmatch etc.
  • attacking uses your own health

A video of it here:

The old game has many shortcomings though:

  • it’s peer-2-peer, not client-server
  • addons has to be done as bots that emulate clients
  • no-one knows who owns it
  • source code not available
2 Likes

This is my first attempt:

package example.net.server;

/*
 * $Id$
 * 
 * Copyright (c) 2015, 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.
 */
import com.jme3.network.HostedConnection;
import com.jme3.network.Server;
import com.jme3.network.service.AbstractHostedService;
import com.jme3.network.service.HostedServiceManager;
import com.simsilica.es.EntityData;
import com.simsilica.es.ObservableEntityData;
import com.simsilica.es.base.DefaultEntityData;
import com.simsilica.es.net.EntitySerializers;
import com.simsilica.es.server.EntityHostSettings;
import com.simsilica.es.server.HostedEntityData;
import com.simsilica.es.server.SessionDataDelegator;
import java.util.HashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A HostedService that manages access to an EntityData instance for client
 * connections. When added to the service manager, this will handle the
 * networking necessary to facilitate client-server EntityData access. A
 * reciprocal EntityClientService should be added to the Client's
 * ClientServiceManager to complete the setup.
 *
 * <p>
 * It is up to the game server to periodically call sendUpdates() to flush any
 * pending EntitySet changes to clients.</p>
 *
 * @author Paul Speed
 */
public class MultiDataHostedService extends AbstractHostedService
    implements EntityHostSettings {

public static final String ATTRIBUTE_ARENA = "arena";
public static final String ATTRIBUTE_DEFAULT_ARENA = "1";

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

private int channel;
private HashMap<String, ObservableEntityData> arenaToEntityDataMap;
private HashMap<HostedConnection, String> connToArenaMap;
//private final ObservableEntityData ed;
private boolean autoHost = true;
private int maxEntityBatchSize = 20;
private int maxChangeBatchSize = 20;

private SessionDataDelegator delegator;

/**
 * Creates a new EntityDataHostedService for the specified EntityData that
 * will communicate over the specified channel. Autohosting is set to true
 * by default.
 */
public MultiDataHostedService(int channel, ObservableEntityData ed) {
    this(channel, ed, true);
}

/**
 * Creates a new EntityDataHostedService for the specified EntityData that
 * will communicate over the specified channel and will automatically host
 * for new connections depending on the specified autoHost value.
 */
public MultiDataHostedService(int channel, ObservableEntityData ed, boolean autoHost) {

    this.arenaToEntityDataMap = new HashMap<>();
    this.connToArenaMap = new HashMap<>();
    this.channel = channel;
    this.arenaToEntityDataMap.put(ATTRIBUTE_DEFAULT_ARENA, ed);
    this.autoHost = autoHost;

    // Make sure the relevant serializers are registered
    EntitySerializers.initialize();
}

public EntityData getEntityData(String arena) {
    return arenaToEntityDataMap.get(arena);
}

@Override
public int getChannel() {
    return channel;
}

/**
 * Must be called by the game server to send pending updates to the relevant
 * clients.
 */
public void sendUpdates() {

    for (HostedConnection conn : getServer().getConnections()) {
        HostedEntityData hed = conn.getAttribute(HostedEntityData.ATTRIBUTE_NAME);
        if (hed == null) {
            continue;
        }
        hed.sendUpdates();
    }
}

/**
 * Sets up the specified connection for hosting remote entity data commands.
 * By default this is performed automatically in addConnection() and is
 * controlled by the setAutoHost() property.
 */
public void startHostingOnConnection(HostedConnection hc, String arena) {
    log.debug("startHostingOnConnection:" + hc);
    hc.setAttribute(ATTRIBUTE_ARENA, arena);

    if (!arenaToEntityDataMap.containsKey(arena)) {
        DefaultEntityData ed = new DefaultEntityData();
        arenaToEntityDataMap.put(arena, ed);
        hc.setAttribute(HostedEntityData.ATTRIBUTE_NAME, new HostedEntityData(this, hc, ed));
    } else {
        hc.setAttribute(HostedEntityData.ATTRIBUTE_NAME, new HostedEntityData(this, hc, (ObservableEntityData) this.getEntityData(arena)));
    }

    connToArenaMap.put(hc, arena);
}

/**
 * Terminates the specified connection for hosting remote entity data
 * commands. By default this is performed automatically in
 * removeConnection().
 */
public void stopHostingOnConnection(HostedConnection hc) {
    String arena = hc.getAttribute(ATTRIBUTE_ARENA);
    HostedEntityData hed = hc.getAttribute(HostedEntityData.ATTRIBUTE_NAME);
    if (hed == null) {
        return;
    }
    log.debug("stopHostingOnConnection:" + hc);
    hc.setAttribute(HostedEntityData.ATTRIBUTE_NAME, null);
    hc.setAttribute(arena, null);

    hed.close();

    connToArenaMap.remove(hc);

    if (this.isArenaEmpty(arena)) {
        arenaToEntityDataMap.remove(arena);
    }

}

/**
 * Sets the maximum number of entities that will be sent back in a single
 * batched results message. The optimal number largely depends on the
 * relative size of an average entity as the app retrieves it. Small
 * components and small numbers of components per entity means larger
 * batches are optimal. Defaults to 20.
 */
public void setMaxEntityBatchSize(int i) {
    this.maxEntityBatchSize = i;
}

@Override
public int getMaxEntityBatchSize() {
    return maxEntityBatchSize;
}

/**
 * Sets the maximum number of EntityChanges that will be sent back in a
 * single batched results message. The optimal number largely depends on the
 * average size of components. Defaults to 20.
 */
public void setMaxChangeBatchSize(int i) {
    this.maxChangeBatchSize = i;
}

@Override
public int getMaxChangeBatchSize() {
    return maxChangeBatchSize;
}

/**
 * Set to true to have new connections automatically 'hosted' by this entity
 * service. In other words, any newly added connections will automatically
 * have startHostingOnConnection() called. Set this to false if the
 * application requires further client setup before letting them access
 * entities. In that case, the game server will have to call
 * startHostingOnConnection() manually. Defaults to true.
 */
public void setAutoHost(boolean b) {
    this.autoHost = b;
}

public boolean getAutoHost() {
    return autoHost;
}

@Override
protected void onInitialize(HostedServiceManager services) {

    // A general listener for forwarding the ES messages
    // to the client-specific handler
    this.delegator = new SessionDataDelegator(HostedEntityData.class,
            HostedEntityData.ATTRIBUTE_NAME,
            true);
    getServer().addMessageListener(delegator, delegator.getMessageTypes());
}

@Override
public void start() {
}

@Override
public void stop() {
    for (HostedConnection conn : getServer().getConnections()) {
        stopHostingOnConnection(conn);
    }
}

@Override
public void terminate(HostedServiceManager hsm) {
    getServer().removeMessageListener(delegator, delegator.getMessageTypes());
}

@Override
public void connectionAdded(Server server, HostedConnection hc) {
    log.debug("Connection added:" + hc);
    if (autoHost) {
        startHostingOnConnection(hc, ATTRIBUTE_DEFAULT_ARENA);
    }
}

@Override
public void connectionRemoved(Server server, HostedConnection hc) {
    log.debug("Connection removed:" + hc);
    stopHostingOnConnection(hc);
}

public void goToArena(HostedConnection hc, String arena) {
    stopHostingOnConnection(hc);
    startHostingOnConnection(hc, arena);
}

public boolean isArenaEmpty(String arena) {
    for (HostedConnection conn : getServer().getConnections()) {
        if (conn.getAttribute(ATTRIBUTE_ARENA) == arena) {
            return false;
        }
    }
    return true;
}
}

I’ll update here how it goes.

1 Like

That quickly became overwhelming trying to implement. The design isn’t completely thought through (mine, not sim-eth’s) - I’ll leave it be for now and focus on adding value to the game.

One thing is, how would I keep track of player data if every arena is completely seperated. There would need to be a centralized EntityData for the Server and then a number of EntityData for each playable session, and a link between them if needed (like player experience or points or currency or what not).