Subscribe to GameFromScratch on YouTube Support GameFromScratch on Patreon


11. March 2014

 

We are going to look at networking for your LibGDX application.  Networking in LibGDX is relatively primitive, supporting only socket communications.  In many cases though, that’s more than enough.  We are going to implement a very simple socket based chat application.  The code is heavily commented, so the discussion will actually be pretty sparse.  If I missed something please leave a comment and I will do my best to address it.

 

Alright, let’s jump right in with the code:

package com.gamefromscratch;

import com.badlogic.gdx.ApplicationListener;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Net.Protocol;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.net.ServerSocket;
import com.badlogic.gdx.net.ServerSocketHints;
import com.badlogic.gdx.net.Socket;
import com.badlogic.gdx.net.SocketHints;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.ui.Label;
import com.badlogic.gdx.scenes.scene2d.ui.Skin;
import com.badlogic.gdx.scenes.scene2d.ui.TextArea;
import com.badlogic.gdx.scenes.scene2d.ui.TextButton;
import com.badlogic.gdx.scenes.scene2d.ui.VerticalGroup;
import com.badlogic.gdx.scenes.scene2d.utils.ClickListener;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;

public class Networking implements ApplicationListener {
    private OrthographicCamera camera;
    private SpriteBatch batch;
    private Skin skin;
    private Stage stage;
    private Label labelDetails;
    private Label labelMessage;
    private TextButton button;
    private TextArea textIPAddress;
    private TextArea textMessage;
    
    // Pick a resolution that is 16:9 but not unreadibly small
    public final static float VIRTUAL_SCREEN_HEIGHT = 960;
    public final static float VIRTUAL_SCREEN_WIDTH = 540;
    
    
    @Override
    public void create() {        
        camera = new OrthographicCamera(Gdx.graphics.getWidth(),Gdx.graphics.getHeight());
        batch = new SpriteBatch();
        
        // Load our UI skin from file.  Once again, I used the files included in the tests.
        // Make sure default.fnt, default.png, uiskin.[atlas/json/png] are all added to your assets
        skin = new Skin(Gdx.files.internal("data/uiskin.json"));
        stage = new Stage();
        // Wire the stage to receive input, as we are using Scene2d in this example
        Gdx.input.setInputProcessor(stage);

        
        // The following code loops through the available network interfaces
        // Keep in mind, there can be multiple interfaces per device, for example
        // one per NIC, one per active wireless and the loopback
        // In this case we only care about IPv4 address ( x.x.x.x format )
        List<String> addresses = new ArrayList<String>();
        try {
            Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
            for(NetworkInterface ni : Collections.list(interfaces)){
                for(InetAddress address : Collections.list(ni.getInetAddresses()))
                {
                    if(address instanceof Inet4Address){
                        addresses.add(address.getHostAddress());
                    }
                }
            }
        } catch (SocketException e) {
            e.printStackTrace();
        }
        
        // Print the contents of our array to a string.  Yeah, should have used StringBuilder
        String ipAddress = new String("");
        for(String str:addresses)
        {
            ipAddress = ipAddress + str + "\n";
        }
        
        // Now setupt our scene UI
        
        // Vertical group groups contents vertically.  I suppose that was probably pretty obvious
        VerticalGroup vg = new VerticalGroup().space(3).pad(5).fill();//.space(2).pad(5).fill();//.space(3).reverse().fill();
        // Set the bounds of the group to the entire virtual display
        vg.setBounds(0, 0, VIRTUAL_SCREEN_WIDTH, VIRTUAL_SCREEN_HEIGHT);
        
        // Create our controls
        labelDetails = new Label(ipAddress,skin);
        labelMessage = new Label("Hello world",skin);
        button = new TextButton("Send message",skin);
        textIPAddress = new TextArea("",skin);
        textMessage = new TextArea("",skin);

        // Add them to scene
        vg.addActor(labelDetails);
        vg.addActor(labelMessage);
        vg.addActor(textIPAddress);
        vg.addActor(textMessage);
        vg.addActor(button);
        
        // Add scene to stage
        stage.addActor(vg);
        
        // Setup a viewport to map screen to a 480x640 virtual resolution
        // As otherwise this is way too tiny on my 1080p android phone.
        stage.setViewport(VIRTUAL_SCREEN_WIDTH, VIRTUAL_SCREEN_HEIGHT,false);
        stage.getCamera().position.set(VIRTUAL_SCREEN_WIDTH/2,VIRTUAL_SCREEN_HEIGHT/2,0);
        
        // Now we create a thread that will listen for incoming socket connections
        new Thread(new Runnable(){

            @Override
            public void run() {
                ServerSocketHints serverSocketHint = new ServerSocketHints();
                // 0 means no timeout.  Probably not the greatest idea in production!
                serverSocketHint.acceptTimeout = 0;
                
                // Create the socket server using TCP protocol and listening on 9021
                // Only one app can listen to a port at a time, keep in mind many ports are reserved
                // especially in the lower numbers ( like 21, 80, etc )
                ServerSocket serverSocket = Gdx.net.newServerSocket(Protocol.TCP, 9021, serverSocketHint);
                
                // Loop forever
                while(true){
                    // Create a socket
                    Socket socket = serverSocket.accept(null);
                    
                    // Read data from the socket into a BufferedReader
                    BufferedReader buffer = new BufferedReader(new InputStreamReader(socket.getInputStream())); 
                    
                    try {
                        // Read to the next newline (\n) and display that text on labelMessage
                        labelMessage.setText(buffer.readLine());    
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start(); // And, start the thread running
        
        // Wire up a click listener to our button
        button.addListener(new ClickListener(){
            @Override 
            public void clicked(InputEvent event, float x, float y){
                
                // When the button is clicked, get the message text or create a default string value
                String textToSend = new String();
                if(textMessage.getText().length() == 0)
                    textToSend = "Doesn't say much but likes clicking buttons\n";
                else
                    textToSend = textMessage.getText() + ("\n"); // Brute for a newline so readline gets a line
                
                SocketHints socketHints = new SocketHints();
                // Socket will time our in 4 seconds
                socketHints.connectTimeout = 4000;
                //create the socket and connect to the server entered in the text box ( x.x.x.x format ) on port 9021
                Socket socket = Gdx.net.newClientSocket(Protocol.TCP, textIPAddress.getText(), 9021, socketHints);
                try {
                    // write our entered message to the stream
                    socket.getOutputStream().write(textToSend.getBytes());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
    }

    @Override
    public void dispose() {
        batch.dispose();
    }

    @Override
    public void render() {        
        Gdx.gl.glClearColor(0.5f, 0.5f, 0.5f, 1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
        batch.setProjectionMatrix(camera.combined);
        batch.begin();
        stage.draw();
        batch.end();
    }

    @Override
    public void resize(int width, int height) {
    }

    @Override
    public void pause() {
    }

    @Override
    public void resume() {
    }
}

 

And when you run the code you should see:

image

 

The top two values are the IP v4 addresses of the machine you are running on.  You need to enter these in the other copy of the application you want to chat with.  This address uniquely identifies your machine on the internet, while we specified the port (9021) using code.  Think of a port like a mailbox at an apartment building.  The street address of the building is comparable to the IP address, while PORT is akin to the apartment number.  Port values go from 1 to 65,536, although several are reserved.  Most of the reserved ports are > 100, so when picking a port for your application look up to see if that address is a dedicated port ( such as 80 for HTTP communication, or 21 for FTP ), or simply pick a random high value.  Your machine can have multiple addresses, one per network adapter and possibly more, especially if you run virtualization software like VMWare.  Then there is the value 127.0.0.1, this is a special address known as a loop back adapter, and address that points back at itself.

 

Where it currently says “Hello World”, this is where any incoming messages will be displayed.

 

In the first textbox, you enter the IP address of machine you want to send a message to.  In this example, you can actually send a message to yourself, simply enter your own IP address.  Finally the second textbox is the text of the message to send.  Of course you send the message by pressing the Send Message button.

 

One thing you may notice from the above example is I set it to run in the oddball resolution of 540x960.  Why did I do this?  For a couple reasons.  First, the native resolution of the phone I tested on ( HTC One ) is 1920x1080 and the default Scene2D Skin/Font are wayyyyyyyy too small at that resolution.  Of course I could have created a a new skin that was more appropriate but, well, I'm lazy.  The second reason is that resolution is a decent size and is the same aspect ratio as 1080p ( 16:9 ), so it scales well, both up and down, when displayed on a 16:9 screen.  On an iPad it’s going to look like absolute garbage.  Perhaps in the future I will do a post specifically about handling multiple device resolutions.

 

There is one more thing to be aware of, this example currently will not work on iOS.  The Scene2D TextField widget currently doesn’t bring up the iOS on screen controller.  There is a fix on the LibGDX contributions forum.  It’s an iOS specific hack/workaround, so I wouldn’t expect it to be merged into the main trunk.  Finally, LibGDX built in networking is fairly simple, limited to just socket programming.  For more robust networking support check out kryonet, it supported in LibGDX and is reported to work across all LibGDX platforms except HTML5.  Which isn’t really surprising, as the author Nathan Sweet is also a LibGDX contributor.

Programming , ,

blog comments powered by Disqus

Month List

Popular Comments