[question answered, topic: java server+ gms client] server in GML that can host multiple games

M

maartenvt

Guest
Hi guys,

So I have been thinking about creating a small multiplayer game in GML, with a server which I can run on my pc and a client that players use to play the actual game (which are short 1v1 matches). The server would be an authoritative server, so it would run any game that is taking place and send updates to the clients, and the clients would check if their predictions match up with the authoritative results and conform to these if their predictions were different ( see http://www.gabrielgambetta.com/fpm1.html ).

Now I was thinking, if the server has to run multiple matches at the same time, how would I go about implementing this? It seems to me I could not use objects, any standard GML collision functions, the room editor etc. OR i would have to run these matches in the same room next to each other which would be really wonky...

Another option would be using data structures instead of objects, and my own collision functions, but this begs the question why I would still be using GML and not something like java? The only thing I would be using from GML are the drawing, surface and sound functions.

Maybe there is a way (an extension or DLL or something) to make the server start a new room without closing the 'main server room' for each seperate match? Or it would not even need to be displayed, as long as the server does the necessary calculations.

What are your thoughts on this issue? I would really like some input before starting to create something in GMS while GMS is not at all suited for the job.

Thanks in advance!

Maarten
 

FrostyCat

Redemption Seeker
And that's the thing: The architecture of GMS makes it infeasible to do centralized servers with it. Its singular global runtime makes it structurally unfit to handle multiple parallel sessions. And if you need to scale up later, the inability of GMS to run headless restricts your choice to home systems.

This is why people who know what they are doing with GMS-made online games use other languages to write the centralized server.
 

Tsa05

Member
Your server is a data hub--it should not be running the game in the conventional sense (like moving between rooms and stuff).
Instead, your players send status messages to the server that indicate what part of the game they are on, and what is happening, and the server relays that information to other players.

Have you written a multiplayer game before? If not, I suggest checking out the buffer functions (ou will need them), and also, try creating something a bit more local first, just to get the hang of it. With multiplayer gaming, you have to program rather differently! Traditionally, you can write code where one thing just follows another, but with multiplayer, you will frequently need to write it so that whenever one thing happens, the server is notified, then the game waits for an acknowledging response, then the next thing follows. You've got to get used to doing that first!

Try taking an existing game that you've done and converting it to a "server" game. Try to re-create a simple game in such a way that it doesn't act based upon user input-instead, user input is stored in a buffer and sent away with network functions, and then the replies from the server drive the action. If you can get this sort of thing working on your computer, then you are able to get a multiplayer game working!
 
M

maartenvt

Guest
Ah thanks for the replies guys!

@Tsa05 I'm familiar with the structure of a server client system, I made them before in different languages. I have also familiarized myself with the networking functions of GM:Studio (I made a basic client-server system with a request-response type communication, you can find it here: https://github.com/Maarten0110/GML-networking-test if you're interested). So the specifics of how to program server-client systems are not the issue here :)

@FrostyCat Do you have an example (maybe even open source) of people that have a GMS project with a server in a different language? What languages do they use? And why would they still use GMS on the client side and not something in the same language? I mean the only thing you are using Game Maker for then is the built in room system, the drawing functions, sounds etc, basically all the aesthetics. It seems to me this would only add another layer of work since both the client and server simulate the same game world (yes im aware that the server would not draw anything and calculate only the necessary interactions), unless you make a completely authoritative server. So this would mean you need to rewrite parts of your engine from the server to make client side predictions in GML, which would mean extra work. However, I am not experienced in making 2d graphics with different platforms than gamemaker, so I'm not sure how much time you gain by using game maker for the aesthetics..
 

Lewa

Member
I'm currently working on a game with multiplayer functionality and i don't write the server in GM (while the client is a GM game.)
There are multiple reasons why i choose to switch to another language for the server (in my case Java):

  • Object Oriented Programming
Game Maker isn't trully object oriented compared to other languages (like Java, C# or C++) You lack language specific features like access modifiers and object methods (which allow you to organize and write way cleaner code compared to GML where every variable of each instance is public and scripts which can be accessed from anywhere at any point in time.). Writing software which is heavily data driven (like servers) is way more enjoyable to write with languages which support that and it makes the code way more cleaner/encapsulated/easier to follow in the process.

Note that this is just my opinion though. (I'm programming in other languages in my spare time / university besides GML)​
  • GPU requirement
As FrostyCat already said: you can't run a GM game without a rendering context. (You can of course rent a server which does have a GPU and installed DirectX, but those are way more expensive than a traditional server.)
  • Multithreading
This is a big one. GM is a singlethreaded application.
This means that you are very limited with the actual workload the server is able to do. (This includes processing incoming messages, sending messages to clients, doing collission detection/validation, etc...)
Servers have multicore CPUs for a reason.
In my case i split a lot of tasks to multiple threads which can run on seperate CPUs. As an example: One thread does handle broadcasting of TCP and UDP messages, one thread waits (and processes) incoming UDP messages, for each player a new thread is created which handles things like collission detection/validation, one thread could handle file/ressource loading (game levels, etc...) and so on.

This doesn't mean that you can't do that in GM. It's just that due to the lack of proper multithreading, scalability of the server falls short. (So if you want to do anything remotely CPU intensive you may run into some problems later down the line.)
As to the other points:
And why would they still use GMS on the client side and not something in the same language?
Can't speak for other devs. In my case i'm fammiliar with GM.

I mean the only thing you are using Game Maker for then is the built in room system, the drawing functions, sounds etc, basically all the aesthetics. It seems to me this would only add another layer of work since both the client and server simulate the same game world (yes im aware that the server would not draw anything and calculate only the necessary interactions), unless you make a completely authoritative server. So this would mean you need to rewrite parts of your engine from the server to make client side predictions in GML, which would mean extra work. However, I am not experienced in making 2d graphics with different platforms than gamemaker, so I'm not sure how much time you gain by using game maker for the aesthetics..
In my case it's true that i don't use a lot of the built-in functions (like rooms) of GM. But that's a whole different story.
You will of course have to implement some parts of the game on the server, but depending on the way your game works (and depending on the type of the game), you will probably be forced to write specific code for the server anyway.
(Besides the collission detection you will have to handle things differently especially if you want to spread your workload accross different cores by using multithreading.)
 
Last edited:
M

maartenvt

Guest
Thanks for your elaborate response! You raise some interesting points. I think I will write the server in java since that is the language I know best besides gml.

I am familiar with the standard java threads and sockets, so that should come in handy.

Maybe you could tell me some pitfalls you came accros that I should avoid, or just some tips when maker a gm client java server system?

Anyways thanks for the advice! I had hoped there was a way to make gm multithreaded but the other points you mentioned about gpu usage and suitability of objective languages for servers convinced me that gms is just a bad choice for a central server.
 

Roa

Member
The only way around some of this is to have different instances of servers running on different ports, but each run time will always be using 2 threads, one for logic and one for network and unless you painfully go through and set affinity for processing, its kinda hard to give them dedicated work space. But if you manage that far, you can have a master server who's only job is to tell which of the smaller servers you can run on. Its all pretty stupid and inefficient though.

better off making a c++ sever that can split threads as needed. Game maker is a total waste for dedicated wrapped up solutions.
 

Lewa

Member
Thanks for your elaborate response! You raise some interesting points. I think I will write the server in java since that is the language I know best besides gml.

I am familiar with the standard java threads and sockets, so that should come in handy.

Maybe you could tell me some pitfalls you came accros that I should avoid, or just some tips when maker a gm client java server system?

Anyways thanks for the advice! I had hoped there was a way to make gm multithreaded but the other points you mentioned about gpu usage and suitability of objective languages for servers convinced me that gms is just a bad choice for a central server.
Yes, there are some pitfalls.
First of, Game Maker and Java use (by default) different endianness (big- and little endian) for encoding datatypes with more than 1 byte (like 16 bit short, 32 bit Integer, etc...) so you will either have to convert between those endianesses manually or bypass this by using the appropiate classes on the serverside. (i do this mostly manually at the moment, although i believe there are Java classes which can handle that for you. Also have to look into that.)

Second, by connecting with a java server you have to manage some things manually which GM would automatically do for you.
In particular the whole packet fragmentation issue in TCP.
You see, TCP is a stream based protocoll. So if you send a "packet" via TCP from the server to the client (and the other way around), it may happen that this "packet" arrives in splitted packets at different points in time. If you are connecting with the default sockets to a GM based server you will not have to worry about it as GM stores additional header information inside of each packet and manages all that stuff behind the scenes for you. (it waits until all fragments of a packet are recieved and then fires the asynchronous networking event.)

If you use the raw sockets and connect to a custom server you will have to manage that stuff yourself. I ran into this problem back in the day and created a topic about it on the old gmc which explains this issue:
(i also posted my approach to this problem once i figured out what the issue was.)
http://gmc.yoyogames.com/index.php?showtopic=650252


There is also the GM:Studio <> Java server example from Debels on the old GMC which i also used as a reference for my server. (I have my own server code now, but back in the day this helped me to test the whole GM:Studio <> Java Server connection and see how it works.)
http://gmc.yoyogames.com/index.php?showtopic=586718
 
Last edited:
M

maartenvt

Guest
@Lewa I have used the java.nio.ByteBuffer class to convert between Endiannes, maybe it's worth checking out, although I'm wondering if this is an efficient way of dealing with endianness conversion(?):
Code:
package logic;

import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

public class GMLInputStream {

    private static ByteBuffer bb;
    private DataInputStream byteReader;
   
    public GMLInputStream(InputStream in) {
        bb = ByteBuffer.allocate(8);
        bb.order(ByteOrder.LITTLE_ENDIAN);
        byteReader = new DataInputStream(in);
    }
   
    private void prepare(int bytes) throws IOException {
        bb.clear();
        for (int i=0; i<bytes; i++)
            bb.put((byte)byteReader.readByte());
        bb.rewind();
    }
   
    public byte readS8() throws IOException {
        prepare(1);
        return bb.get();
    }
   
    public short readS16() throws IOException {
        prepare(2);
        return bb.getShort();
    }
   
    public int readS32() throws IOException {
        prepare(4);
        return bb.getInt();
    }
   
    public float readF32() throws IOException {
        prepare(4);
        return bb.getFloat();
    }
   
    public double readF64() throws IOException {
        prepare(8);
        return bb.getDouble();
    }
   
    public String readString() throws IOException {
        String result = "";
        byte b = byteReader.readByte();
        while (b != 0) {
            result += (char)b;
            b = byteReader.readByte();
        }
        return result;
    }
}

Anyway, thanks a lot for your awesome explanations! I think you might have saved me several headaches, because I had totally forgotten about the existence of endianness :p
 

Lewa

Member
@Lewa I have used the java.nio.ByteBuffer class to convert between Endiannes, maybe it's worth checking out, although I'm wondering if this is an efficient way of dealing with endianness conversion(?):
Code:
package logic;

import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

public class GMLInputStream {

    private static ByteBuffer bb;
    private DataInputStream byteReader;
  
    public GMLInputStream(InputStream in) {
        bb = ByteBuffer.allocate(8);
        bb.order(ByteOrder.LITTLE_ENDIAN);
        byteReader = new DataInputStream(in);
    }
  
    private void prepare(int bytes) throws IOException {
        bb.clear();
        for (int i=0; i<bytes; i++)
            bb.put((byte)byteReader.readByte());
        bb.rewind();
    }
  
    public byte readS8() throws IOException {
        prepare(1);
        return bb.get();
    }
  
    public short readS16() throws IOException {
        prepare(2);
        return bb.getShort();
    }
  
    public int readS32() throws IOException {
        prepare(4);
        return bb.getInt();
    }
  
    public float readF32() throws IOException {
        prepare(4);
        return bb.getFloat();
    }
  
    public double readF64() throws IOException {
        prepare(8);
        return bb.getDouble();
    }
  
    public String readString() throws IOException {
        String result = "";
        byte b = byteReader.readByte();
        while (b != 0) {
            result += (char)b;
            b = byteReader.readByte();
        }
        return result;
    }
}

Anyway, thanks a lot for your awesome explanations! I think you might have saved me several headaches, because I had totally forgotten about the existence of endianness :p
It's definitely a lot cleaner compared to what i have.
My code:
Code:
package server.packets;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

/**
* Game Makers endianess seems to be different to javas default endianess.
* This class provides functions for reading and writing of inputstreams, while converting
* between BIG_ENDIAN and LITTLE_ENDIAN to ensure a working data transport between the server and the game
* without changes in the information.
* 
* TODO: Look into possible optimizations. (Some of the functions allocate objects on the heap
* for the conversion process which could hinder performance.)
*
* @author Lewa
*
*/


public class StreamConverter {
   
    public static String buffer_read_string(DataInputStream in,int size) throws IOException{
            StringBuilder b = new StringBuilder();
           
            for(int i = 0;i<size;i++){
                b.append((char)(in.readByte() & 0xff));
            }
           
            return b.toString();
    }

   
    public static int buffer_read_u8(DataInputStream in) throws IOException{
            return (in.readByte() & 0xff);
    }

    public static byte buffer_read_u8_byte(DataInputStream in) throws IOException{
        return (byte) (in.readByte() & 0xff);
}

   
    public static int buffer_read_u16(DataInputStream in) throws IOException{
            byte[] b = new byte[]{buffer_read_u8_byte(in),buffer_read_u8_byte(in)};
            ByteBuffer buf = ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN);
            return buf.getShort()& 0xFFFF; 

    }
   
   
    public static int buffer_read_s16(DataInputStream in) throws IOException{
            byte[] b = new byte[]{in.readByte(),in.readByte()};
            ByteBuffer buf = ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN); 
            return buf.getShort();

    }
   
   
    /**
     * Skip the UDP header which the GM client included in each packet
     * (was nessecary due to a bug which is now fixed.)
     * @param input
     * @throws IOException
     */
    public static void skipHeaderUDP(DataInputStream input) throws IOException{
            //input.skipBytes(12);
    }
   
    public static int buffer_read_u32(DataInputStream in) throws IOException{
            byte[] b = new byte[]{buffer_read_u8_byte(in),buffer_read_u8_byte(in),buffer_read_u8_byte(in),buffer_read_u8_byte(in)};
            ByteBuffer buf = ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN); 
            return buf.getInt(); 

    }
   
   
    public static int buffer_read_s32(DataInputStream in) throws IOException{
            byte[] b = new byte[]{in.readByte(),in.readByte(),in.readByte(),in.readByte()};
            ByteBuffer buf = ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN); 
            return buf.getInt(); 


    }
   
    public static float buffer_read_f32(DataInputStream in) throws IOException{
        byte[] b;
            b = new byte[]{buffer_read_u8_byte(in),buffer_read_u8_byte(in),buffer_read_u8_byte(in),buffer_read_u8_byte(in)};
            ByteBuffer buf = ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN); 
            return buf.getFloat(); 
    }

    public static double buffer_read_f64(DataInputStream in) throws IOException{
        byte[] b;
            b = new byte[]{buffer_read_u8_byte(in),buffer_read_u8_byte(in),buffer_read_u8_byte(in),buffer_read_u8_byte(in),buffer_read_u8_byte(in),buffer_read_u8_byte(in),buffer_read_u8_byte(in),buffer_read_u8_byte(in)};
            ByteBuffer buf = ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN); 
            return buf.getDouble(); 

    }
   
   
    public static void buffer_write_u32(DataOutputStream out,int i) throws IOException{
        ByteBuffer b = ByteBuffer.allocate(4);
        b.order(ByteOrder.LITTLE_ENDIAN);
        b.putInt(i);
       

            out.write(b.array());
       
    }
   
    public static void buffer_write_f32(DataOutputStream out,float i) throws IOException{
        ByteBuffer b = ByteBuffer.allocate(4);
        b.order(ByteOrder.LITTLE_ENDIAN);
        b.putFloat(i);
        out.write(b.array());
       
    }
   
    public static void buffer_write_f64(DataOutputStream out,double i) throws IOException{
        ByteBuffer b = ByteBuffer.allocate(8);
        b.order(ByteOrder.LITTLE_ENDIAN);
        b.putDouble(i);
        out.write(b.array());
       
    }
   
   
    public static void buffer_write_u16(DataOutputStream out,int i) throws IOException{
        /*ByteBuffer b = ByteBuffer.allocate(2);
        b.order(ByteOrder.LITTLE_ENDIAN);
        b.putInt(i);
       

            out.write(b.array());*/       
       
        out.write((byte) i);
        out.write((byte) (i >> 8));
       
       
    }
   
    public static void buffer_write_s32(DataOutputStream out,int i) throws IOException{
        ByteBuffer b = ByteBuffer.allocate(4);
        b.order(ByteOrder.LITTLE_ENDIAN);
        b.putInt(i);
       

            out.write(b.array());
    }
   
    public static void buffer_write_u8(DataOutputStream out,int i) throws IOException{
        out.writeByte((byte) i);
    }

}

The biggest problem is the allocation of objects on the heap each time we want to convert from one endian to another.
Reducing that to simple binary arithmetic (like in my buffer_write_u16() method) could be a lot faster as there isn't any object allocation happening in the code.
 
T

ThunkGames

Guest
Hey,

While trying to search for something else (see link) on Google I came across this recent topic and decided to share this, which I found after quite a bit of searching. For many of the reasons covered here I wanted to use Java for my server (and I don't know c++) and deploy my server on a VPS which I pay a small price a month for. I believe signed 16 bit integers do not work but it is a VERY good starting point and modified various aspects (I am still learning about Java's NIO).

-David
 
Top