libGDX for Coding Applets

 

libGDX is a Java-based game development framework for writing 2D and 3D games, which comes with excellent documentation, a great wiki, many fine tutorials (e.g. here, here, here, and here) and text books (such as Java Game Development with LibGDX by Lee Stemkoski, third-party libraries for additional features, and is capable of generating code for Windows, Max OS X, Linux, Android, iOS, and HTML5/WebGL.

The focus of this web page is libGDX's use of Google Web Toolkit (GWT) to compile Java to JavaScript so it can run in web browsers. I'll coded three examples: "Simple Painter", "Weather", and "Penguin3D".

I don't have the time to introduce libGDX here, but any of the tutorials linked to above do a fine job of that. I'll instead limit myself to pointing out which parts of libGDX can be utilized for building applets, and the program code itself is fully documented.



 

1. Simple Painter

The main purpose of "Simple Painter" is to demonstrate how to code applet-style controls, such as buttons, checkboxes, menu items, drop-down lists, dialogs, and input boxes, linking them to a drawing area. An important feature that's missing from this example is networked client-server communication, although that's possible of course, but I've decided to look at that topic in a separate example (that'll be added here later).

The painter GUI consists of three areas:

[Painter Stages]

The top-level application, called MyGdxGame, utilizes three libGDX stages and viewports – the stages are positioned to act as a controls sidebar, a menubar, and a drawing area.

The sidebar and menubar are built by the MyGdxGame class, but canvas drawing is handled by DrawCell, a subclass of libGDX's Actor.

DrawCell maintains a list of shape objects, representing lines, ovals, or rectangles, which are implemented as subclasses of a Shape abstract class. These relationships are shown in the class diagram:

[Painter UML Class Diagram]

During the application's rendering stage (i.e. when MyGdxGame's render() is called), DrawCell cycles through its list of shapes redrawing each one onto the canvas by means of a libGDX ShapeRenderer (which plays the role of Java's Graphics2D).

Widgets

libGDX has good support for GUI widgets via its Scene2d.ui scene graph. The selection of widgets and layout controls aren't as numerous as Java's, but more than adequate for a typical applet. A complete list of widgets can be found here, and layouts are listed here (I only used Table).

The widgets in the sidebar affect the drawing on the canvas, but the menubar is purely for show. Two important GUI types missing from Scene2d.ui are menubars and input dialogs, so "Simple Painter" shows how they can be coded using libGDX's Dialog.

The 'Fancy' menu contains four menu items – two labels, a drop-down list, and a checkbox. The 'Input' label calls an input box that reads a string typed by the user, and displays it in a separate message box. The 'Help' menu invokes a text area dialog with a blue border.

[Menu Widgets]

The relevant code is in the buildFancyMenu() and buildHelpMenu() methods of MyGdxGame, and the sidebar is created by buildControls(). They are called from the application's create() method.

I deliberately dressed the widgets in a skin based on Java's metal styling, but one of the strengths of libGDX is the ease with which skinning can be changed, and the large number of skins available.

Compiling, Running, and Installing a GDX-applet

libGDX supports a simple programming lifecycle, starting with the creation of the project by calling the gdx-setup.jar tool.

I created code templates for the 'Desktop' and 'HTML' platforms, but focused on the 'Desktop' application initially. libGDX's desktop platform offers quick compilation times, based around the use of the Gradle build tool, with the command line call:

> gradlew desktop:run

As the program neared completion, I periodically tested the HTML version by calling:

> gradlew html:superdev

This took about 35 seconds to compile and invoke a local jetty-based HTTP server, which let me run the applet at http://localhost:8080. This version includes debug information that can be accessed through the browser's developer console, but I preferred to debug the desktop version.

> gradlew html:dist

produces a fully-functioning webpage that can be uploaded to a static web host. This took around 52 seconds, not including the time to move the files over to my departmental HTTP server. As a consequence, I only carried out this step occasionally; the applet at the top of this page is that code, with the libGDX generated webpage being accessed via an iframe.



 

2. A Weather Applet Using HTTP

The libGDX applet on the right demonstrates the use of HTTP to retrieve city weather data from https://www.weatherapi.com/. The code is fairly short, mostly located in a single WeatherApp class, and is documented. However, there are a few gotchas associated with network programming in libGDX that I'd like to highlight.

GET Requests and Responses

Before you start coding any HTTP application, you should become familiar with the website's API, and in particular how the GET or POST requests are encoded. The "Weather API" has good documentation (see here), and also a page where queries can be tried out.

A query to find the current weather for London, returning the data in JSON format, is encoded as the GET request:

http://api.weatherapi.com/v1/current.json?key=XXXX&q=London&aqi=no

The XXXX is the API key, which is obtained when you register with the site. This query is implemented as a HttpRequest object constructed at the start of lookupWeather() in WeatherApp.

The JSON response has the form:

  {
    "location": {
        "name": "London",
        "region": "City of London, Greater London",
        "country": "United Kingdom",
        "lat": 51.52,
        "lon": -0.11,
        "tz_id": "Europe/London",
        "localtime_epoch": 1674715349,
        "localtime": "2023-01-26 6:42"
    },
    "current": {
        "last_updated_epoch": 1674714600,
        "last_updated": "2023-01-26 06:30",
        "temp_c": 4.0,
        "temp_f": 39.2,
        "is_day": 0,
        "condition": {
            "text": "Overcast",
            "icon": "//cdn.weatherapi.com/weather/64x64/night/122.png",
            "code": 1009
        },
        "wind_mph": 5.6,
        "wind_kph": 9.0,
           :
           :  // many more fields
    }
  }

This string should be saved since it can be used to generate the classes used by libGDX's JsonBeans library.

The Weather API can also return a JSON error response if the data passed to it is incorrect. For example, if the query asks about a city called "dddddd", the response is:

  {
    "error": {
        "code": 1006,
        "message": "No matching location found."
    }
  }

This snippet can be used to generate the error classes used by JsonBeans.

By far the easiest way to generate these classes is with a JSON-to-Java tool; I utilized jsonschema2pojo, but there are several similar sites on the Web.

It's worth the time experimenting with the many options available at jsonschema2pojo to generate code that is as close a match for libGDX as possible. The options I used included: setting the source type to "json", setting the package name to "com.mygdx.game", opting for primitive types, including constructors, disabling annotations and additional properties, and not using word delimiters.

The outcome was four classes representing the JSON weather data: Weather, Location, Current, and Condition, and two classes for the error: WeatherError and Error. These are employed in the buildWeather() and buildError() methods in WeatherApp.

As with the first applet, I mostly developed and debugged the 'Desktop' version, and only occasionally tested the 'SuperDev' output. It's easy to forget that in order for the JSON-to-Java translation to work in GWT, it's necessary to add configuration code to WeatherApp.gwt.xml located in core/src. An extend-configuration-property line must be added for each class being used in the translation. For example, the line for the WeatherError class:

  < extend-configuration-property name="gdx.reflect.include" 
                        value="com.mygdx.game.WeatherError" />

The underlying mechanisms used by the JsonBeans library are serialization and reflection, and GWT imposes additional constraints on these that aren't present when using the 'Desktop' version of the application. The relevant libGDX documentation on these topics is Reading and writing JSON and Reflection.

Debugging Aids

I like to keep my debugging tools as simple as possible, which means a liberal sprinkling of calls to System.out.println(). This is fine when running the 'Desktop' version, but the 'HTML' version runs in a real-life browser with no command line to display messages. My solution is dprint() in WeatherApp:

  private void dprint(String s)
  // print debugging info to the desktop or to the browser console
  {
    if (isDebugging) {
      if (isWebGL())
        Gdx.app.log("WeatherApp", s);   
           // appears in browser web console
      else
        System.out.println(s);
    }
  }  // end of dprint()


  private boolean isWebGL()
  {  return (Gdx.app.getType() == Application.ApplicationType.WebGL); }

If the isDebugging global is set to true then the supplied string is either printed to the command line or to the browser's console log. The second choice requires a call to Gdx.app.setLogLevel(Application.LOG_INFO) at the start of create() in WeatherApp. The console can be accessed in Firefox and Chrome by typing CTRL-SHIFT-i. I send my debugging data to the 'Log' channel to distinguish it from 'Errors' and 'Warnings'.

CORS and Superdev

When I compiled the application using the html:superdev option, a CORS problem would intermittently arise.

CORS (Cross-origin resource sharing) is a server-side setting that prohibits a web page at a different location from downloading resources from that server. It's aimed specifically at AJAX requests, which are used by GWT to carry out its network communication. However, a website such as Weather API, with a public-facing API, will be set up to allow these kind of requests, so why the problem?

The first issue is identifying the problem, which is easiest to do by opening the Web browser console window, and checking the 'Errors' and 'Warnings' tabs. The error appears as a message something like:

Cross-Origin Request Blocked: 
The Same Origin Policy disallows reading the remote resource at
http://api.weatherapi.com/v1/current.json?key=xxxx&q=london&aqi=no. 
(Reason: CORS header 'Access-Control-Allow-Origin' missing). 
Status code: 200.

Note that the HTTP status code is 200 (meaning 'ok'), but the data stream is empty. As the wikipedia page on CORS explains, Access-Control-Allow-Origin is one of the headers that will be included in the server's response. Public-facing APIs will usually send "Access-Control-Allow-Origin: *". An examination of the response headers sent by the server shows that this is sent by the Weather API.

Unfortunately, I wasn't able to track down the problem, except that it didn't occur in the html:dist distribution code loaded onto my departmental server. This makes me think that it may be a problem with the Jetty server used by html:superdev. A simple, but rather unsatisfactory, fix is to download a CORS plugin for your browser (e.g. I installed CORS Everywhere for Firefox). For safety reason, you should only turn it on during tests of the html:superdev version.

Incidentally, the problem also seems to interact with the browser's caching of HTTP responses. For example, once a request for a particular city's weather has failed, it will continue to fail even after the browser and applet have been restarted and can return weather data for other cities.

Downloading Images

It's possible to download a PNG icon from the Weather API that represents the current weather. Its URL can be seen in the current.condition.icon field of the JSON weather response, and is accessible in Java with the following code:

  Condition cond = weather.getCurrent().getCondition();
  String urlStr = "http:" + cond.getIcon();

  HttpRequestBuilder requestBuilder = new HttpRequestBuilder();
  HttpRequest req = requestBuilder.newRequest().
          method(HttpMethods.GET).
          timeout(5000).   // ms
          url(urlStr).build();

The returned Net.HttpResponse object can be accessed in the 'Desktop' version like so:

  Net.HttpResponse resp = ....

  InputStream is = resp.getResultAsStream();
  byte[] bytes = StreamUtils.copyStreamToByteArray(is);
  StreamUtils.closeQuietly(is);
  iconPixmap = new Pixmap(bytes, 0, bytes.length); 

Unfortunately, the application's 'HTML' version runs up against restrictions imposed by GWT which stop the downloading and displaying of binary data. The compiler rejects the calls to Net.HttpResponse.getResultAsStream() and the Pixmap constructor.

It is possible to download the PNG data as a string, and convert it to a byte array:

  String pixStr = resp.getResultAsString();
  byte[] bytes = pixStr.getBytes("UTF-8");

but there appears to be no way around the restriction on the use of Pixmap.

The Space Key and Browsers

Many browsers (including Firefox and Chrome) interpret the pressing of the spacebar as a request to scroll down the webpage. This is obviously not desirable when the user wants to type a space into the applet's textfield (for example when looking up the weather in "San Francisco").

The HTML5 Backend and GWT Specifics documentation explains how to disable the browser actions associated with keys. For the spacebar, create() in WeatherApp calls Gdx.input.setCatchKey(Input.Keys.SPACE, true). However, this also means that a space character is no longer detected by the TextFieldListener attached to the textfield. It's necessary to have the top-level application implement InputProcessor so it can observe the space key press, and have its keyDown() method paste the space into the textfield. The code:

  // various globals
  private TextField cityTF;
  private String tfInput; 
  private boolean tfHasData = false;
  private boolean tfHasFocus = false;


  public boolean keyDown(int code) 
  {
    if ((code == Input.Keys.SPACE) && isWebGL() && tfHasFocus) {
      int pos = cityTF.getCursorPosition();
      cityTF.setText( insertStr(cityTF.getText(), " ", pos));
      cityTF.setCursorPosition(pos+1); 
      tfInput = cityTF.getText();
    }
    return false;
  }


  private String insertStr(String s, String lets, int pos) 
  {  return (s.substring(0, pos) + lets + s.substring(pos));  }

There's a few tricky aspects to this code. The first is that the space may be typed anywhere in the textfield, and so it's necessary to use the cursor's position to decide how the field should be updated. Also, the space should only be added to the text if the user is actually typing inside that field, which means monitoring the focus:

  cityTF.addListener(new FocusListener() {
    public void keyboardFocusChanged(FocusListener.FocusEvent event, 
                                     Actor actor, boolean isFocused) 
    {  tfHasFocus = isFocused; }
  });

A third point is that keyDown() is passed the key code, not a character, and rather counter-intuitively Input.Keys.toString() converts this to "SPACE" rather than " ". Finally, all of this extra work should only be carried out for the 'HTML' version of the app, so the code must check isWebGL().

The API Key

The Weather API requires an API key be included in every HTTP request. In WeatherApp, this is a plain string assigned to the API_KEY constant.

Of course, in the source code included here I've replaced the key with a string of "x"s, but the real string is used in the compiled JavaScript on this webpage. Fortunately, GWT's default is to obfuscate the JavaScript that it generates, and a quick scan of the code (using grep) shows that the API key isn't visible as plain text.

There are two provisos. One is that it's possible to turn off GWT's obfuscation (see HTML5 Backend and GWT Specifics), which would of course make the string easy to find. Another, is that it's a good idea to always delete libGDX's automatically generated debugging information before uploading your applet to a server. It's located in html/build/dist/WEB-INF/deploy/html/symbolMaps, and removing it will also reduce the size of your code by a few MBs.



 

3. A Penguin's Journey in 3D

This applet's features are mostly located in the Penguin3D class:

The penguin model was originally coded in the OBJ format, which was successfully loaded and displayed by the 'Desktop' version of the application, but the 'HTML' version failed to render the penguin's texture. For that reason I converted the model to libGDX's g3db format. I used a combination of the command line fbv-conv tool along with a GUI add-on called libgdx-fbxconv-gui which makes the conversion process very straightforward.

The penguin's movement and collision detection and prevention code is located in the Sprite3D class. It's closely based on BaseActor3D, a class written by Lee Stemkoski for Chapter 16 of his book Java Game Development with LibGDX", 2nd ed., Apress, 2018; his source code is available here.



 

4. Downloads and Other Links


Dr. Andrew Davison
E-mail: ad@coe.psu.ac.th
Back to the third-party libraries page