Improved Spidermonkey Class Registration

Since every class that will be send using spidermonkey has to be registered in the serializer at both the server and the client, plus you have to register it to the listeners to receive it, I made a 3 classes (1 to register classes, 1 listener class for the server, and 1 listener class for the client) to do this easily.



All you have to do is this to start the server/client:



[java]

Server server = Network.createServer( "app_name", 100, 9000, 9000 ); // name, version, tcp port, udp port

NetworkMessages.registerAll( server, new ServerListener() );

server.start();



Client client = Network.connectToServer( "app_name", 100, "localhost", 9000, 9000 ); // name, version, tcp port, udp port

NetworkMessages.registerAll( client, new ClientListener() );

client.start();

[/java]





The register class (NetworkMessages) is:

[java]

package shared._network;



import com.jme3.network.AbstractMessage;

import com.jme3.network.Client;

import com.jme3.network.ClientStateListener;

import com.jme3.network.ConnectionListener;

import com.jme3.network.MessageListener;

import com.jme3.network.Server;

import com.jme3.network.serializing.Serializer;





public class NetworkMessages

{

// register classes >>

// edit >>

private static void registerAll()

{

//register( LoginMessage.class );

//register( LoginResultMessage.class );



//register( NewCharacterMessage.class );

//register( RemoveCharacterMessage.class );

//register( MoveCharacterMessage.class );

//register( ChangeCharacterMovementKeysMessage.class );



//register( HelloMessage.class );

}

// edit <<





// dont edit >>

private static Server registerServer;

private static Client registerClient;

private static MessageListener registerListener;

private static void registerAll( Server server, Client client, MessageListener listener )

{

registerServer = server;

registerClient = client;

registerListener = listener;



registerAll();



if( registerServer != null )

if( registerListener instanceof ConnectionListener )

registerServer.addConnectionListener( (ConnectionListener) registerListener );

if( registerClient != null )

if( registerListener instanceof ClientStateListener )

registerClient.addClientStateListener( (ClientStateListener) registerListener );

}

public static void registerAll( Server server, MessageListener listener )

{

registerAll( server, null, listener );

}

public static void registerAll( Client client, MessageListener listener )

{

registerAll( null, client, listener );

}



@SuppressWarnings( "unchecked" )

private static void register( Class<? extends AbstractMessage> registerClass )

{

Serializer.registerClass( registerClass );



if( registerServer != null )

registerServer.addMessageListener( registerListener, registerClass );

if( registerClient != null )

registerClient.addMessageListener( registerListener, registerClass );

}

// dont edit <<

// register classes <<

}

[/java]





The server listener is:

[java]

package server._network;



import com.jme3.network.ConnectionListener;

import com.jme3.network.HostedConnection;

import com.jme3.network.Message;

import com.jme3.network.MessageListener;

import com.jme3.network.Server;

import java.util.logging.Logger;





public class ServerListener implements MessageListener<HostedConnection>, ConnectionListener

{

public void connectionAdded( final Server server, final HostedConnection client )

{

Logger.getLogger( "server" ).info( "Client #"+client.getId()+" connectedn" );

}



public void connectionRemoved( final Server server, final HostedConnection client )

{

Logger.getLogger( "server" ).info( "Client #"+client.getId()+" disconnectedn" );

}





public void messageReceived( final HostedConnection client, final Message message )

{

}

}

[/java]



The client listener is:

[java]

package client._network;



import com.jme3.network.Client;

import com.jme3.network.ClientStateListener;

import com.jme3.network.Message;

import com.jme3.network.MessageListener;





public class ClientListener implements MessageListener<Client>, ClientStateListener

{

public void clientConnected( final Client client )

{

}



public void clientDisconnected( final Client client, final DisconnectInfo info )

{

if( info != null ) // closed by server

{

System.out.println( "Disconnected: "+info.reason );

System.exit( 1 );

}

}





public void messageReceived( final Client client, final Message message )

{

}

}

[/java]



Advantages:

  • easy to setup
  • easy to update (when new message classes are added)



    Disadvantages:
  • to split receiver code you now have to put instanceof checks in the receive method (instead of registering the message classes to another receiver)
  • registering of the message classes isn’t thread save



    Lemme hear what you think of it.



    PS: you may change the name of NetworkMessages to something like NetworkMessagesRegisterer, but I’m planning to do more with it than just registering message classes.

I think this complicates things, actually. Now you have some central class that you are forced to edit somehow. I personally have my message registration split across several different static classes that are specific to subsystems… and I recommend that.



Also, registering one listener for all messages is sort of bad design… but you don’t even have to do it. You can already register a single message listener that will be called for every message just by not specifying the class. I don’t necessarily recommend it as a general practice unless you already have some kind of automatic dispatch making the code cleaner… because otherwise it’s much messier to have a whole chain of instanceof checks. For a large number of messages it is more efficient to let the listener registry target a specific listener class.



Here is an interesting alternative that I use… chock full of magic. I’m considering adding it to core at some point. This is the version that I use in Mythruna and it can be simplified a little, I think.



[java]

import java.lang.reflect.;

import java.util.
;



import com.jme3.network.Client;

import com.jme3.network.HostedConnection;

import com.jme3.network.Message;

import com.jme3.network.MessageConnection;

import com.jme3.network.MessageListener;

import com.jme3.network.Server;



public abstract class AbstractMessageDelegator<S> implements MessageListener<S>{

private Class delegateType;

private Map<Class,Method> methods = new HashMap<Class, Method>();



protected AbstractMessageDelegator( Class delegateType, boolean automap ) {

this.delegateType = delegateType;

if( automap )

automap();

}



/**

  • Register this delegator for all classes mapped as messages.

    */

    public void register( Server server ) {

    for( Class type : methods.keySet() )

    server.addMessageListener( (MessageListener<? super HostedConnection>)this, type );

    }



    /**
  • Register this delegator for all classes mapped as messages.

    */

    public void register( Client client ) {

    for( Class type : methods.keySet() )

    client.addMessageListener( (MessageListener<? super Client>)this, type );

    }



    protected Method findDelegate( String name, Class parameterType ) {

    // We do an exhaustive search because it’s easier to

    // check for a variety of parameter types and it’s all

    // that Class would be doing in getMethod() anyway.

    for( Method m : delegateType.getDeclaredMethods() ) {

    //System.out.println( “Checking:” + m );

    if( !m.getName().equals(name) )

    continue;



    //System.out.println( “Checking parameters…” );

    Class<?>[] parms = m.getParameterTypes();

    if( parms.length != 2 )

    continue;



    if( !MessageConnection.class.isAssignableFrom( parms[0] ) )

    continue;



    if( !parms[1].isAssignableFrom( parameterType ) )

    continue;



    return m;

    }

    return null;

    }



    protected boolean allowName( String name ) {

    return true;

    }



    public AbstractMessageDelegator<S> automap() {

    for( Method m : delegateType.getDeclaredMethods() ) {

    //System.out.println( “Checking:” + m );

    if( !allowName(m.getName()) )

    continue;



    //System.out.println( “Checking parameters…” );

    Class<?>[] parms = m.getParameterTypes();

    if( parms.length != 2 )

    continue;



    if( !MessageConnection.class.isAssignableFrom( parms[0] ) )

    continue;



    // And if the second parameter is a message type then register it

    if( !Message.class.isAssignableFrom(parms[1]) )

    continue;



    methods.put( parms[1], m );

    }

    return this;

    }



    public AbstractMessageDelegator<S> map( String… methodNames ) {

    Set<String> names = new HashSet<String>( Arrays.asList(methodNames) );



    for( Method m : delegateType.getDeclaredMethods() ) {

    //System.out.println( “Checking:” + m );

    if( !names.contains(m.getName()) )

    continue;



    //System.out.println( “Checking parameters…” );

    Class<?>[] parms = m.getParameterTypes();

    if( parms.length != 2 )

    continue;



    if( !MessageConnection.class.isAssignableFrom( parms[0] ) )

    continue;



    // And if the second parameter is a message type then register it

    if( !Message.class.isAssignableFrom(parms[1]) )

    continue;



    methods.put( parms[1], m );

    }

    return this;

    }



    public AbstractMessageDelegator<S> map( Class messageType, String methodName ) {

    // Lookup the method

    Method m = findDelegate( methodName, messageType );

    if( m == null ) {

    throw new RuntimeException( “Method:” + methodName
  • " not found matching signature (MessageConnection, "
  • messageType.getName() + “)” );

    }



    methods.put( messageType, m );



    return this;

    }



    protected Method getMethod( Class c ) {

    Method m = methods.get©;

    if( m != null )

    return m;



    // Could do an exhaustive search but it would require

    // thread safety… and ordering, etc.

    return m;

    }



    protected abstract Object getSourceDelegate( S source );



    public void messageReceived( S source, Message msg ) {

    if( msg == null )

    return;



    Object delegate = getSourceDelegate(source);

    if( delegate == null ) {

    // Means ignore this message/source

    return;

    }



    Method m = getMethod(msg.getClass());

    if( m == null )

    throw new RuntimeException( “Method now found for class:” + msg.getClass() );



    try {

    m.invoke( delegate, source, msg );

    } catch( IllegalAccessException e ) {

    throw new RuntimeException( “Error executing:” + m, e );

    } catch( InvocationTargetException e ) {

    throw new RuntimeException( “Error executing:” + m, e.getCause() );

    }

    }

    }

    [/java]



    That’s a lot of code to absorb and some of it is a bit magic… but here is how one would use it:

    (this is for the handler on the server… thus HostedConnection based.)

    [java]

    public class MyMessageHandler extends AbstractMessageDelegator<HostedConnection> {

    public MyMessageHandler() {

    super( MyMessageHandler.class, true );

    }



    protected Object getSourceDelegate( HostedConnection source ) {

    // You can actually run the methods on a completely different object but we’ll stick to the simple case

    // By returning null you can also ignore a message if the connection is not

    // setup for it yet.

    return this;

    }



    protected void handleFoo( HostedConnection source, FooMessage msg ) {

    // Do some stuff with Foo messages

    }



    protected void handleBar( HostedConnection source, BarMessage msg ) {

    // Do some stuff with Bar messages

    }

    }

    [/java]



    To actually register it you could add it specifically for the messages it handles by:

    [java]

    new MyMessageHandler().register( server );

    [/java]



    I’m still honing the above which is why I haven’t committed it yet. I added functionality for delegating to different objects than the message handler itself and for manually mapping methods, etc. but I don’t use them much and I think ultimately that should be a separate class.



    Regarding registration with the serializer, that is always going to be a pain until I get around to rewriting the serializer. In the mean time, a shared set of static utility methods, always executed in the same order, is the best approach. You can further harden your classes by specifying an ID directly in the “@Serializable” annotation. This can insulate you from ordering issues, somewhat. Though remember to do similar with any serializable fields of the registered classes… since they will get autoregistered in a particular order, too.

Thought I’d pitch in my way of solving the same issue,

its a similar approach to yours.



Made a holder class on the common/shared project, where I listed all the message types and combined them into one big type array.

So whenever I make a new message, I just pitch it in there.



[java]public class NetworkConstants {



public static Class<?>[] AUTHENTICATION_MESSAGES = new Class[] { AuthenticationMessage.class, HandshakeMessage.class, ClientJoinMessage.class, ServerAcceptMessage.class, RegionJoinMessage.class };



public static Class<?>[] CACHE_TYPES = new Class[] { CachedMap.class, CachedRegion.class, Tile.class };

public static Class<?>[] CACHE_MESSAGES = new Class[] { CacheMessage.class, CachedInfoMessage.class, CachedRegionMessage.class, CachedMapMessage.class, CacheOkMessage.class };



public static Class<?>[] DEFINITIONS = new Class[] { Definition.class,

// Entities

EntityDefinition.class,

// World Objects

WorldObjectDefinition.class,

// Characters

CharacterDefinition.class, NPCDefinition.class, PlayerDefinition.class,

// Items

ItemDefinition.class, GearItemDefinition.class, WeaponItemDefinition.class, ArmorItemDefinition.class, ConsumableItemDefinition.class,

// Misc

ProjectileDefinition.class };



public static Class<?>[] ENTITY_MESSAGES = new Class[] { EntityMessage.class };



public static Class<?>[] WORLD_OBJECT_MESSAGES = new Class[] {

// Core

WorldObjectMessage.class, WorldObjectTCPMessage.class, WorldObjectUDPMessage.class,

// Init

WorldObjectDefinitionMessage.class, WorldObjectChunkMessage.class, WorldObjectRemoveMessage.class };



public static Class<?>[] PLAYER_MESSAGES = new Class[] {

// Core

PlayerMessage.class, PlayerTCPMessage.class, PlayerUDPMessage.class,

// Init

PlayerDefinitionMessage.class, PlayerChunkMessage.class, PlayerRemoveMessage.class, PlayerRequestMessage.class,

// Actions

PlayerInputMessage.class, PlayerMoveMessage.class, PlayerDamageMessage.class, PlayerExperienceMessage.class, PlayerDeathMessage.class };



public static Class<?>[] NPC_MESSAGES = new Class[] {

// Core

NPCMessage.class, NPCTCPMessage.class, NPCUDPMessage.class,

// Init

NPCDefinitionMessage.class, NPCChunkMessage.class, NPCRemoveMessage.class, NPCRequestMessage.class,

// Actions

NPCMoveMessage.class, NPCDamageMessage.class };



public static Class<?>[] ITEM_MESSAGES = new Class[] {

// Core

ItemMessage.class, ItemUDPMessage.class, ItemUDPMessage.class };



public static Class<?>[] PROJECTILE_MESSAGES = new Class[] {

// Core

ProjectileMessage.class };



public static Class<?>[] CHAT_MESSAGES = new Class[] { ChatMessage.class, ChatPlayerMessage.class, ChatPlayerPrivateMessage.class, ChatServerMessage.class, ChatServerPrivateMessage.class };

public static Class<?>[] INTERACTION_MESSAGES = new Class[] { InteractionMessage.class };

public static Class<?>[] SERIALIZED_CLASSES;



static {

int size = 0;

size += AUTHENTICATION_MESSAGES.length;

size += CACHE_TYPES.length;

size += CACHE_MESSAGES.length;



size += DEFINITIONS.length;

size += ENTITY_MESSAGES.length;

size += WORLD_OBJECT_MESSAGES.length;

size += PLAYER_MESSAGES.length;

size += NPC_MESSAGES.length;

size += ITEM_MESSAGES.length;

size += PROJECTILE_MESSAGES.length;



size += CHAT_MESSAGES.length;

size += INTERACTION_MESSAGES.length;



SERIALIZED_CLASSES = new Class[size];



int i = 0;

for (Class<?> c : AUTHENTICATION_MESSAGES) {

SERIALIZED_CLASSES[i++] = c;

}

for (Class<?> c : CACHE_TYPES) {

SERIALIZED_CLASSES[i++] = c;

}

for (Class<?> c : CACHE_MESSAGES) {

SERIALIZED_CLASSES[i++] = c;

}



for (Class<?> c : DEFINITIONS) {

SERIALIZED_CLASSES[i++] = c;

}

for (Class<?> c : ENTITY_MESSAGES) {

SERIALIZED_CLASSES[i++] = c;

}

for (Class<?> c : WORLD_OBJECT_MESSAGES) {

SERIALIZED_CLASSES[i++] = c;

}

for (Class<?> c : PLAYER_MESSAGES) {

SERIALIZED_CLASSES[i++] = c;

}

for (Class<?> c : NPC_MESSAGES) {

SERIALIZED_CLASSES[i++] = c;

}

for (Class<?> c : ITEM_MESSAGES) {

SERIALIZED_CLASSES[i++] = c;

}

for (Class<?> c : PROJECTILE_MESSAGES) {

SERIALIZED_CLASSES[i++] = c;

}



for (Class<?> c : CHAT_MESSAGES) {

SERIALIZED_CLASSES[i++] = c;

}

for (Class<?> c : INTERACTION_MESSAGES) {

SERIALIZED_CLASSES[i++] = c;

}

}

}[/java]



Then what I do on the server/client is pretty straight forward.



[java]Serializer.registerClasses(NetworkConstants.SERIALIZED_CLASSES);[/java]



Also if using a unified network handler you just do:



server.addMessageListener(snh, NetworkConstants.SERIALIZED_CLASSES);



Otherwise you can select whichever array you’d wish to track on that listener.

@pspeed said:
I added functionality for delegating to different objects than the message handler itself and for manually mapping methods, etc. but I don't use them much....


I looked at my code and the above is false. I use the separate delegator all the time on the server. It's useful when you actually want a player/client specific object handling the messages. You have to register a central message handle but can then delegate per HostedConnection.

[java]
public class PlayerHandler {
public void handleFoo( HostedConnection source, FooMessage msg ) {
...do player specific stuff with FooMessage
}
public void handleBar( HostedConnection source, BarMessage msg ) {
...do player specific stuff with BarMessage
}
}

public class PlayerMessageListener extends AbstractMessageDelegator<HostedConnect> {
public PlayerMessageListener() {
super( PlayerHandler.class, true );
}

protected Object getSourceDelegate( HostedConnection source ) {
return source.getAttribute( "playerHandle" );
}
}

new PlayerMessageListener().register( server );
[/java]

... then some other more general message listener that handles login and initial connection setup is responsible for sticking the PlayerHandler object on the HostedConnection as an attribute.

This really cleaned up my server code considerably.
@perfecticus said:
Thought I'd pitch in my way of solving the same issue,
its a similar approach to yours.

Made a holder class on the common/shared project, where I listed all the message types and combined them into one big type array.
So whenever I make a new message, I just pitch it in there.
[snipped really long class]


I'm trying to understand what the motivation is with all of the arrays? What does it get you over something simple like:

[java]
public class Messages {
public static void registerMessage() {
Serializer.registerClasses( UserStateMessage.class,
EntityStateMessage.class,
WarpPlayerMessage.class,
EntityListUpdateMessage.class,
...and so on...
);
}
}
[/java]

Thanks for the replies, these are quite useful.

It’s the first time I’m making a multiplayer game, so I still have to find out which networking designs work and which don’t, these replies will help me a lot with that.

1 Like
@pspeed said:
I'm trying to understand what the motivation is with all of the arrays? What does it get you over something simple like:

[java]
public class Messages {
public static void registerMessage() {
Serializer.registerClasses( UserStateMessage.class,
EntityStateMessage.class,
WarpPlayerMessage.class,
EntityListUpdateMessage.class,
...and so on...
);
}
}
[/java]


Well it was what first came into mind while building up the networking.

If I made it register all the messages in one hatch like that, I wouldn't
be able to easily separate into various message handlers.

[java]
server.addMessageListener(snhNPC, NetworkConstants.NPC_MESSAGES);
server.addMessageListener(snhPlayer, NetworkConstants.PLAYER_MESSAGES);
server.addMessageListener(snhItems, NetworkConstants.ITEM_MESSAGES);
[/java]

Just comes in handy at times, but I'm sure there are better ways to do it.
@perfecticus said:
[java]
server.addMessageListener(snhNPC, NetworkConstants.NPC_MESSAGES);
server.addMessageListener(snhPlayer, NetworkConstants.PLAYER_MESSAGES);
server.addMessageListener(snhItems, NetworkConstants.ITEM_MESSAGES);
[/java]

Just comes in handy at times, but I'm sure there are better ways to do it.


Ah, now I understand. I almost never do that (see above class) so it didn't occur to me.
@patrickvane1993 said:
Thanks for the replies, these are quite useful.
It's the first time I'm making a multiplayer game, so I still have to find out which networking designs work and which don't, these replies will help me a lot with that.


It can be tricky but it's also very rewarding. :)
@pspeed said:
It can be tricky but it's also very rewarding. :)


Exactly, more rewarding than creating PHP API's, lol.
Btw, does anyone know where I can get a job as a game programmer, because there aren't many game companies hiring for that, or are there?
@patrickvane1993 said:Btw, does anyone know where I can get a job as a game programmer, because there aren't many game companies hiring for that, or are there?

It's the most sought after position in game development, but the competition is still fierce. It comes down to where you live, i.e. if you're not located in Europe or North America, you're gonna have a hard time finding an established company.

Start a new thread about this if you'd like more pointers.
@erlend_sh said:
It's the most sought after position in game development, but the competition is still fierce. It comes down to where you live, i.e. if you're not located in Europe or North America, you're gonna have a hard time finding an established company.

Start a new thread about this if you'd like more pointers.


Will do, thanks.
(I live in the Netherlands btw).

Link to new thread: http://hub.jmonkeyengine.org/groups/general/forum/topic/job-as-game-programmer-1/