Simple Game Server with Zay ES Part 2

The first part of this serie was about the game server itself. It was not much... but hey now we proceed and write our first simple client! For the client we reuse the VisualAppState from the invader game we wrote in the previous article series about entity system. That spares us a lot of work. And kind a proofe that the VisualAppState is something like a design pattern in ES. We can as well reuse the old EntityDataState.

So ok then let's begin.

Reuse it

Ok then let's take the visual state from the previous article serie and add it, as well the basic invader model. Place the model into the assets Models folder like usual. For convenience reason I added the visual system to this blog post as well, so you just can copy paste it.

package gameserver;

import com.jme3.app.Application;
import com.jme3.app.SimpleApplication;
import com.jme3.app.state.AbstractAppState;
import com.jme3.app.state.AppStateManager;
import com.jme3.light.DirectionalLight;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.scene.Spatial;
import com.simsilica.es.Entity;
import com.simsilica.es.EntityData;
import com.simsilica.es.EntityId;
import com.simsilica.es.EntitySet;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class VisualAppState extends AbstractAppState {

    private SimpleApplication app;
    private EntityData ed;
    private EntitySet entities;
    private final Map<EntityId, Spatial> models;
    private ModelFactory modelFactory;

    public VisualAppState() {
        this.models = new HashMap<>();
    }

    @Override
    public void initialize(AppStateManager stateManager, Application app) {
        super.initialize(stateManager, app);
        this.app = (SimpleApplication) app;

        ed = this.app.getStateManager().getState(EntityDataState.class).getEntityData();
        entities = ed.getEntities(Position.class, Model.class);

        app.getCamera().lookAt(Vector3f.UNIT_Z, Vector3f.UNIT_Y);
        app.getCamera().setLocation(new Vector3f(0, 0, 60));

        DirectionalLight light = new DirectionalLight();
        light.setDirection(new Vector3f(1, 1, -1));
        this.app.getRootNode().addLight(light);

        modelFactory = new ModelFactory(this.app.getAssetManager());
    }

    @Override
    public void cleanup() {
        entities.release();
        entities = null;
    }

    @Override
    public void update(float tpf) {
        if (entities.applyChanges()) {
            removeModels(entities.getRemovedEntities());
            addModels(entities.getAddedEntities());
            updateModels(entities.getChangedEntities());
        }
    }

    private void removeModels(Set<Entity> entities) {
        for (Entity e : entities) {
            Spatial s = models.remove(e.getId());
            s.removeFromParent();
        }
    }

    private void addModels(Set<Entity> entities) {
        for (Entity e : entities) {
            Spatial s = createVisual(e);
            models.put(e.getId(), s);
            updateModelSpatial(e, s);
            this.app.getRootNode().attachChild(s);
        }
    }

    private void updateModels(Set<Entity> entities) {
        for (Entity e : entities) {
            Spatial s = models.get(e.getId());
            updateModelSpatial(e, s);
        }
    }

    private void updateModelSpatial(Entity e, Spatial s) {
        Position p = e.get(Position.class);
        s.setLocalTranslation(p.getLocation());
                float angles[] = new float[3];
        angles[0] = p.getRotation().x;
        angles[1] = p.getRotation().y;
        angles[2] = p.getRotation().z;
        s.setLocalRotation(new Quaternion(angles));
    }

    private Spatial createVisual(Entity e) {
        Model model = e.get(Model.class);
        return modelFactory.create(model.getName());
    }
}

As well the old entity data app state ...

package gameserver;

import com.jme3.app.state.AbstractAppState;
import com.simsilica.es.EntityData;
import com.simsilica.es.base.DefaultEntityData;

public class EntityDataState extends AbstractAppState {
    private EntityData entityData;

    public EntityDataState() {
        this(new DefaultEntityData());
    }

    public EntityDataState( EntityData ed ) {
        this.entityData = ed;
    }

    public EntityData getEntityData() {
        return entityData;
    }

    @Override
    public void cleanup() {
        entityData.close();
        entityData = null; // cannot be reused

    }
}

I did not change any thing I just copy paste it to this article.

The beef

With that prepared we can start writting the code for our game client. It looks pretty much the same like the old Main.class we had in the invader example. The only difference is how we instantiate the EntityDataState, we handover a NetworkedEntityData object. This NetworkedEntityData sets up the connection with the game server and couples the entity data with the entity data from the game server.

package gameserver;

import com.jme3.app.SimpleApplication;
import com.jme3.renderer.RenderManager;

public class GameClient extends SimpleApplication {

    public static void main(String[] args) {
        GameClient app = new GameClient();
        app.start();
    }

    public GameClient() {
        super(new VisualAppState(), 
                new EntityDataState(new NetworkedEntityData("My Game Server", 1, "localhost", 9942).getEntityData()));
    }

    @Override
    public void simpleInitApp() {
    }

    @Override
    public void simpleUpdate(float tpf) {
    }

    @Override
    public void simpleRender(RenderManager rm) {
    }
}

As well we do add the VisualAppState.

So what exactly is this NetworkedEntityData object? Not a rocket since piece of code, but some new stuff init. So first we give it a name, version, inet address and a port for the connection setup. The version must be the same like the one from the server, else the connection will be refused with an error message.

package gameserver;

import com.jme3.network.Client;
import com.jme3.network.ClientStateListener;
import com.jme3.network.MessageConnection;
import com.jme3.network.Network;
import com.simsilica.es.EntityData;
import com.simsilica.es.client.EntityDataClientService;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import java.util.logging.Level;
import java.util.logging.Logger;

public class NetworkedEntityData {

    private EntityData ed;

    public NetworkedEntityData(String name, Integer version, String host, Integer port) {
        Client client;
        try {
            client = Network.connectToServer(name, version, host, port, port);
            client.getServices().addService(new EntityDataClientService(MessageConnection.CHANNEL_DEFAULT_RELIABLE));
            this.ed = client.getServices().getService(EntityDataClientService.class).getEntityData();
            final CountDownLatch startedSignal = new CountDownLatch(1);
            client.addClientStateListener(new ClientStateListener() {
                @Override
                public void clientConnected(Client c) {
                    startedSignal.countDown();
                }

                @Override
                public void clientDisconnected(Client c, ClientStateListener.DisconnectInfo info) {
                    System.out.println("Client disconnected.");
                }
            });
            client.start();

            // Wait for the client to start

            System.out.println("Waiting for connection setup.");
            startedSignal.await();
            System.out.println("Connected.");
            System.out.println("Press Ctrl-C to stop.");
        } catch (IOException ex) {
            Logger.getLogger(NetworkedEntityData.class.getName()).log(Level.SEVERE, null, ex);
        } catch (InterruptedException ex) {
            Logger.getLogger(NetworkedEntityData.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    public EntityData getEntityData() {
        return ed;
    }
}

In very short, it connects to the game server and hands us back an entity data handle we then can use. That's it basically. The System.out.println I added are not really necessary but adds some convenience.

So that's it basically. Start both the server and client in the SDK just right click it and choose "Run File". And that's it so far. I will add another article to make those invaders move around.

Comments

comments powered by Disqus