Introduction

Entities are the elements that the game is made from. Things like buildings, items, players, decoration, enemies, bullets: All different types of entities.

An entity can have a visual representation or be invisible, the same is true for physics. Entities can interact, for example by bumping into each other or if the player picks up an item. Entities can also be used to create logical events by using the I/O event system, for example things like “If at least 2 players have entered the given area, open that door”. Entities can be created or removed from the game at any time and there is virtually no limit on the number.

Networking

The entity system is quite straight forward. If the server decides to create a new entity, it sends all the information about that entity to all the clients, this is called serialization. If the server decides to change a value of an entity, for example the position, it sends an update, telling the clients what changed. Everything the clients have to do is parse those messages and apply the new values or create the corresponding entity.

The methods and attributes of the EntityManager class are static so entities can be created from anywhere, for example the Network class of the client has to be able to create the entities with the information it has received.

Spawning

Spawning describes the act of inserting a new entity into the game. An entity may be created without being instantly added to the game. In this state, the entity does not think nor send updates. It allows the entity to be completely set up before “spawning” it or for example to prepare it for later use.

The actual spawn functions on the entity are quite simple:

    public void spawn(){
        EntityManager.spawn(this);
    }
 
    public void spawnReally(){
        ID = EntityManager.addEntity(this);
        Main.game.broadcast(String.format("eEnts:%s",serialize()));
    }

The spawn function tells the EntityManager that this entity wants to spawn. The EntityManager adds the entity to a list of to-be-spawned entities. After it thinked every entity, it calls the spawnReally function of the contained entities and clears the List.

In the spawnReally function, the entity is added to the global entity list which the EntityManager holds. From that time on it starts thinking and sending updates when values change. The entity also knows its unique ID from the addEntity method. IDs are unique to this single entity and even if the entity was removed can not be reused at the moment. Later there will be a possibility to reuse IDs after a certain time has passed to allow the game to never run out of IDs.

Finally, the clients need to be informed about the new entity. This is achieved by serializing the entity (see the Serialization section) and telling the game-class to broadcast the new entity to all clients. For further details about the network protocol read the Networking page.

Thinking

Every entity has a think-function that is called every frame. There it can do calculations, change its position and the like. To make this possible, the EntityManager (serverside and clientside), whose think-function is called by the main class every frame, has to “think” all the entities:

    public static synchronized void think(){
 
        //think all entities
        Iterator<Integer> li = entities.keySet().iterator();
        while (li.hasNext()){
            Entity e = (Entity) entities.get(li.next());
 
            e.think();  //call the think-function
        }
 
        //remove all entities according to the remove-list
        ListIterator li2 = toRemove.listIterator();
        while (li2.hasNext()){
            int e = (Integer) li2.next();
 
            entities.remove(e);
        }
        toRemove = new ArrayList();    //clear the remove-list
 
        //add all entities according to the add-list
        ListIterator li3 = toAdd.listIterator();
        while (li3.hasNext()){
            Entity e = (Entity) li3.next();
 
            e.spawnReally();
        }
        toAdd = new ArrayList();    //clear the add-list
 
    }

Since an entity can not modify the global list of entities while it is beeing looped through (for example in its think function), adding and removing entities has to be handled by add- and remove-lists. The spawn- and remove- functions add the respective command to a list, which is then processed after all entities have been thinked. This is only true for the Server, since the client should never add an entity by itself anyways.

Serialization

Serverside (serialize)

Basically the values of an entity are converted to strings and concatenated in a certain order. The base entity class starts the chain like this:

    public String serialize(){
 
        String serialized = String.format("%s",this.getClass().getName());
        serialized += String.format(",%d", this.ID);
        serialized += String.format(",%s", this.name);
        serialized += String.format(",%f", this.position.x);
        serialized += String.format(",%f", this.position.y);
 
 
        return serialized;
    }

Every child class can add its own values to the chain by simply appending to the data from the superclass.

    @Override
    public String serialize(){
        String serialized = super.serialize();
        serialized += String.format(",%d", TextureManager.getID(this.texture));
        serialized += String.format(",%f", this.rotation);
 
        return serialized;
 
    }

The serialize method is called when the entity is spawned (see the spawning section).

Clientside (deserialize)

Analog to the serialization, the client just separates the values again and converts them back in the right order. On the base entity this looks like this:

    public void deserialize(ArrayList<String> args){
 
        this.ID = Integer.parseInt(args.get(0));
        this.name = args.get(1);
        this.position.setX(Float.parseFloat(args.get(2)));
        this.position.setY(Float.parseFloat(args.get(3)));
 
    }

And the child classes can retrieve their own values by just continuing to increase the index for the ArrayList after having the called the deserialize function of the super class.

    @Override
    public void deserialize(ArrayList<String> args){
        super.deserialize(args);
 
        this.texture = Integer.parseInt(args.get(4));
        this.rotation = Float.parseFloat(args@Override
    public void deserialize(ArrayList<String> args){
        super.deserialize(args);
 
        this.texture = Integer.parseInt(args.get(4));
        this.rotation = Float.parseFloat(args.get(5));
 
    }.get(5));
 
    }

The deserialize function of the entity is called by the deserialize function of the entity manager, which gets passed the full string containing all the comma seperated values the server submitted. The first value is always the name of the class.

    public static void deserialize(String s){
 
        String[] a = s.split(",");
        ArrayList args = new ArrayList();
        args.addAll(Arrays.asList(a));    //creating an ArrayList from the values
 
        Entity newEnt = null;
 
        try {
 
            newEnt = (Entity) Class.forName(a[0]).newInstance();   //creating a new object of the type that was specified by the first value
            args.remove(0);
            newEnt.deserialize(args);    //setup the entity with the values from the ArrayList
            newEnt.spawn();              //add the entity to the game
        } catch (InstantiationException ex) {
            Logger.getLogger(EntityManager.class.getName()).log(Level.SEVERE, null, ex);
        } catch (IllegalAccessException ex) {
            Logger.getLogger(EntityManager.class.getName()).log(Level.SEVERE, null, ex);
        } catch (ClassNotFoundException ex) {
            Logger.getLogger(EntityManager.class.getName()).log(Level.SEVERE, null, ex);
        }
 
 
    }

Updating

Updating is the process of sending the information about changed entity-attributes from the server to the client.

Serverside

Most set-methods on entities contain a call to addUpdate which adds the attribute-change to a list of changes to be sent to the client. If an attribute changed multiple times, addUpdate will just overwrite the existing value in the list so no duplicate updates will be sent.

    public void setPosX(float x){
        position.setX(x);
        addUpdate("x", Float.toString(x));
    }

After the EntityManager thinked all entities, it calls their update method and collects all updates to be done. Finally, it broadcasts the update to all clients.

    public static synchronized void update(){
        String update = "eUpd";     //update messages start with "eUpd"
 
        Iterator<Integer> li = entities.keySet().iterator();
 
        while (li.hasNext()){           //iterate over all entities
            Entity e = (Entity) entities.get(li.next());
            update += e.update();           //add their message to the update-packet
        }
 
        Main.game.broadcast(update);        //broadcast the update
    }

If there are any updates to be done, the entity just returns its ID, followed by a list of key-value pairs.

    public synchronized String update(){
 
        String retval = "";     //if there are no updates, just return an empty string
 
        if (update.size() > 0){
 
            retval = String.format(":%d", this.ID);     //start with the entity's ID
 
            Iterator li = update.keySet().iterator();
 
            while(li.hasNext()){            //iterate over all to be updated values
                String key = (String) li.next();
                String value = update.get(key);
                retval += String.format(",%s=%s", key, value);  //add it to the comma-separated list of key-value pairs
            }
            update = new HashMap();     //reset the list of to be updated values
        }    
        return retval;      //return our updates
    }

The following sketch is a simplified sequence diagraph that shows what happens when the server changes an attribute of an entity.

Clientside

The Network class passes the single update messages to the EntityManager class:

        else if(request.startsWith("eUpd")) {   //update entity attributes
 
            String[] ents = request.split(":");     //separate the messages for the single entities
 
            for(String s : ents){           //update every entity by passing the message to the EntityManager
                if(!s.equals("eUpd")){
                    EntityManager.update(s);
                }
            }
        }

The EntityManager then splits the message into single updates which then are split in to key and value and passed to the entity to let it handle the update.

public static void update(String s){        //update entity attributes, called from the Network class
 
        String[] spl = s.split(",");        //split the single key-value pairs (separated by commas)
 
        Entity ent = getEntity(Integer.parseInt(spl[0]));   //get the entity that should be updated
 
        for (int i=1;i<spl.length;i++){         //for every kay-value pair
            String[] kv = spl[i].split("=");        
            ent.update(kv[0], kv[1]);           //pass the key and the value to the entity's update-method
        }
    }

The Entity follows a simple scheme to parse the values: First it passes the pair to it's superclass (because to lower level attributes like for example position are likely to be updated more often) which returns if it could handle the update or not. If all superclasses failed to parse the key-value pair, the entity tries to handle it itself.

    @Override
    public boolean update(String key, String value){        //parse attribute-updates from the server
                                                        //returns whether we handled the message or not
        if(super.update(key,value)){        //can our superclasses handle this update?
                    //do nothing
        }
        else if(key.equals("texture"))      //update our texture
        {
            this.texture = Integer.parseInt(value);
        }
        else if(key.equals("rot"))          //update our rotation angle
        {
            this.rotation = Float.parseFloat(value);
        }
        else{
            return false;                   //neither we nor our superclass could handle this update,
        }                                   //so return false to let our child classes know.
 
        return true;    //Update handled.
    }

The following sketch is a simplified sequence diagraph that shows what happens when the client receives an “eUpd” message from the server.

Difficulties

One of the most obvious difficulties is creating the entity hierarchy. Which entity is superclass of what? It might seem simple, but it actually is not. At the moment, the entity hierarchy is not very complex. I could even have left out the EntityVisible or even the Entity itself, so the tree has the EntityPhysics on top. Keeping the changes from class to class small is important because there will be added more and more entities later, and there will certainly be some that just need to be visible but have no physics (decoration for example) or even not visual representation at all (logical entities or entities that mark a location). There are already some classes missing in between. The actual plan was to have an EntityHuman class in between the EntityPhysics and the EntityPlayer classes so NPCs (Non player controlled) can use it.

Another problem was the ID system. Entities must be uniquely idenfifyable across all clients and the server. No problem? Right. But what happens if we create lots of entities? We run out of IDs, so there must be a way to reuse IDs of removed entites. This problem is not yet solved but the basic idea is to insert a dummy entity that stores the time it replaced the removed entity and if enough time has passed (a few seconds should be more than enough) free up the ID again.

Implementing the serialization and the updates was no big deal, but I discovered one thing: String.format is language-dependant. On english machines, floating point values are represented using a ”.” character. On a german operating system, it is using a ”,”! This makes the String.format functions totally useless for this use case. String.format is only for pretty-printing output that should be displayed to the user and should never be used for storing values to files or sending them over the network. A possible workaround would be setting the language for the java environment, though.

The most hated enemy during the development of the entity system was the so called “ConcurrentModificationException”. It, for example, prevented entities from adding or removin entities inside their tick and draw functions because at that time the EntityManager is looping over the entity list. There must be no changes to a list while it has active iterators. You can see the solution to this problem in the Spawning section above.

 
 documentation/entitysystem.txt · Last modified: 2011/05/10 08:45 by armageddon
 
Except where otherwise noted, content on this wiki is licensed under the following license:CC Attribution-Noncommercial-Share Alike 3.0 Unported
Recent changes RSS feed Driven by DokuWiki