Affichage des articles dont le libellé est KISS. Afficher tous les articles
Affichage des articles dont le libellé est KISS. Afficher tous les articles

vendredi 1 janvier 2016

Pack textures and fonts at build time for your LibGDX project

When working with LibGDX and openGL ES we usually need to provide our application with various assets such as :
  • PNG for textures
  • Fonts (whether BitmapFont or FreeTypeFont) for texts
  • Sounds
  • 3D objects
  • etc...
When designing the application, we need to take care of :
  • Reducing the loading time of the assets by reducing the number of required assets
  • Reducing the rendering time of the required assets by packing assets together 
  • Reducing the integration time of the required assets by handling assets generation as part of the automated build process
A way of doing this is to pack all the required PNG for textures into a single TextureAtlas asset. LibGDX provides a TexturePacker utility for that. Some other third party utilities are usefull to generate BitmapFont that can be loaded by the LibGDX runtime.

As an experimented developper, we should expect regular changes among our application design during the development phase which means we should whether re-generate fonts and re-pack textures everytime or work with an application that is not efficient to load and to render (without packed assets and BitmapFont)... As a lazy developper, we might want all the assle of font and texture generation and packing to be handled automagically by the gradle build process.

Here is how we can achieve this...

The big picture

Okay, so let say we want to build an UI that renders a deck of cards using LibGDX :

Card Deck rendered with resources generated at build time

The UI is made of 52 sprites (one for each card) and a BitmapFont to render the label "LibGDX Card Deck"... Everything is laid out with a simple Table. What we want is to be able to change the finest detail with a little effort. For instance :
  • change the red tone used for hearts and diamonds
  • change the shape, the aspect ratio of the card
  • increase the size of the rank
  • decrease the size of the suite
  • change the position of the rank / suit inside the card
  • change the font used for the label

Project layout

When setting up a new LibGDX project you get something like this :

+ rootProject.projectDir
|--+ android
     |--+ assets
     |--+ build

     |--+ res
     |--+ src
     |--+ build.gradle
|--+ core
     |--+ src
     |--+ build.gradle
|--+ desktop
     |--+ src
     |--+ build.gradle
|--+ html

|--+ ios
|--+ resources
|--+ build.gradle

Where the ${rootProject.projectDir}/android/assets directory holds the assets required by the application (whatever the targeted platform : android, desktop, html or ios). So what we are going to do is to create a ${rootProject.projectDir}/resources directory containing the source files used to generate the required assets at build time. Intermediate temporary files will be generated into the ${rootProject.projectDir}/android/build directory.

Design of the cards

To design our card, we are going to use a SVG card pattern wich will define the overall aspect of the card : its shape and the position of the rank and the suit inside the shape. The SVG file will look something like this :


This SVG card pattern pattern use the custom SVGMASK file format defined by the AndroidSvgDrawable library. This allow us to generate all of the 52 combination of card rank x card suit without the need of 52 SVG files...

Then, we need 4 SVG files for suits (one for each card suit) and 13 SVG files for ranks (one for each card rank from 2 to A). Here are the sample files for Heart and King :

  

That's it, let see how we can now generate all of our 52 card textures at build time...

Generating the card textures

To generate the 52 textures, we will use the AndroidSvgDrawable plugin that now integrates better with LibGDX. First, we need to add the plugin dependency to our buildscript classpath in ${rootProject.projectDir}/build.gradle :

Then, we define a custom task in our ${rootProject.projectDir}/desktop/build.gradle file :

This will take the SVG card pattern file we put inside ${rootProject.projectDir}/resources/svg/mask and generate a PNG file for each SVG suit file x SVG rank file we put into ${rootProject.projectDir}/resources/svg/masked :

${rootProject.projectDir}/resources/svg/masked 

${rootProject.projectDir}/android/build/generated/assets

Generating the BitmapFont and the TextureAtlas


In order to generate the BitmapFont and the TextureAtlas at build time, we will rely on the API provided by LibGDX (we must use at least version 1.7.3-SNAPSHOT in order to be able to generate the BitmapFont as mentionned bellow). We should then add the necessary dependencies to our buildscript classpath in ${rootProject.projectDir}/build.gradle :

Most of the time, LibGDX APIs requires the LibGDX runtime to be loaded first, so we must add a specific task in our ${rootProject.projectDir}/desktop/build.gradle file that will load an headless LibGDX runtime for the build purpose :


Generating the BitmapFont

The BitmapFont will be generated from a TrueType font located in the ${rootProject.projectDir}/resources/font directory. A custom task in our ${rootProject.projectDir}/desktop/build.gradle file will generate the BitmapFont for us with the desired parameter :

A BitmapFont is made of 2 files :
  • a .fnt file that will be output in the ${rootProject.projectDir}/android/assets directory directly
  • a .png file that will be output in the ${rootProject.projectDir}/android/build/generated/assets directory because we want to merge it later with the others texture inside our final TextureAtlas...
You can refer to the LibGDX freetype documentation to customize the task to generate the desired font appearence. You can also use a more complexe task to generate BitmapFont of different size, colors, ...

Generating the Atlas

As all the required PNG are now generated inside the ${rootProject.projectDir}/android/build/generated/assets directory, we are now ready to pack them all inside a single TextureAtlas. Once again, we use a custom task in our ${rootProject.projectDir}/desktop/build.gradle file that will do the job for us :

And here is the final skin.png TextureAtlas generated with a default size of 1024 x 1024 that is available in the ${rootProject.projectDir}/android/assets directory along with its skin.atlas descriptor :

skin.png
Once again you can refer to the LibGDX Texture packer documentation for more packing options and a finest control over the generated atlas. Note that LibGDX Texture packer support packing of NinePatch drawable that could be generated at build time with the AndroidSvgDrawable plugin.

Let's build it

In order to chain all of those tasks together, we bind the compileJava task from the java gradle plugin to our custom generateATLAS task :

Here we are, everything is ready, we can just sit and watch gradle work for us :

gradlew desktop:run

:core:compileJava
:core:processResources UP-TO-DATE
:core:classes
:core:jar
:desktop:initHeadlessLibGDX UP-TO-DATE
:desktop:generateFONT
:desktop:generatePNG
:desktop:generateATLAS
assets
Packing.........
Writing 1024x1024: android\assets\skin.png
:desktop:compileJava
:desktop:processResources UP-TO-DATE
:desktop:classes
:desktop:run

BUILD SUCCESSFUL

Total time: 11.758 secs

And if we build it once again, the Gradle magic happens, nothing is re-generated or re-packed because everything is UP-TO-DATE :

gradlew desktop:run

:core:compileJava UP-TO-DATE
:core:processResources UP-TO-DATE
:core:classes UP-TO-DATE
:core:jar UP-TO-DATE
:desktop:initHeadlessLibGDX UP-TO-DATE
:desktop:generateFONT UP-TO-DATE
:desktop:generatePNG UP-TO-DATE
:desktop:generateATLAS UP-TO-DATE
:desktop:compileJava UP-TO-DATE
:desktop:processResources UP-TO-DATE
:desktop:classes UP-TO-DATE
:desktop:run

BUILD SUCCESSFUL

Loading and rendering the assets

Loading the assets requires only a single atlas to be loaded by the application while rendering all the cards and the label will bind a single OpenGL ES texture :

Want to be more optimal ? We can't !

Changing a detail

One more thing ! It's now possible to change the card layout for all our cards by changing only one file. If I modify the SVG card pattern to have the suit in the center of the card and the rank in the upper left corner and the lower right corner as well :

When building the project once again :

gradlew desktop:run

:core:compileJava
:core:processResources UP-TO-DATE
:core:classes
:core:jar
:desktop:initHeadlessLibGDX UP-TO-DATE
:desktop:generateFONT UP-TO-DATE
:desktop:generatePNG
:desktop:generateATLAS
assets
Packing.........
Writing 1024x1024: android\assets\skin.png
:desktop:compileJava
:desktop:processResources UP-TO-DATE
:desktop:classes
:desktop:run

BUILD SUCCESSFUL

Total time: 11.758 secs

Then, the layout has been changed for all of the 52 cards :


Simple isn't it ?

You can find the whole LibGDX project in the sample directory of the AndroisSvgDrawable plugin GitHub project.

dimanche 29 janvier 2012

Jersey custom parameter, annotation and exception mapping

Jersey is the JSR-311 reference implementation for JAX-RS (Java API for RESTful Web Services). One of the drawback of this API is its lack of documentation when you want to go deeper into some complex or recurrent issues. Because we all like KISS code, let see how to keep Jersey simple, stupid!

Let's take the following example where we want to :
  1. Map Java Exception to specific HTTP response with localized messages
  2. Inject parameters of any type with custom validation
  3. Define a specialized annotation for specific injectable parameters
A first shot with no optimization will look like this :
@Path("/test")
@Produces("application/json")
public class TestEndpoint {
    
    private static final Logger log = Logger.getLogger(TestEndpoint.class.getName());
    
    private static final ResourceBundle resource = 
        ResourceBundle.getBundle("com.blogspot.avianey");
    
    @Inject TestService service;
    
    @POST
    @Path("{pathParam: \\d+}")
    public String testMethod(
            @PathParam("pathParam") Long id,
            @FormParam("date") String dateStr,
            @Context HttpServletRequest request) {

        // verifying that the user is logged in
        User user = (User) request.getSession().getAttribute("user");
        if (user == null) {
            throw new WebApplicationException(Status.UNAUTHORIZED);
        }
        
        // verifying the format of the sent date
        Date date = null;
        SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy");
        try {
            date = sdf.parse(dateStr);
        } catch (ParseException pe) {
            throw new WebApplicationException(Status.BAD_REQUEST);
        }
        
        try {
            // calling the business logic
            return service.testOperation(id, date, user);
        } catch (BusinessException boe) {
            log.log(Level.FINER, "Business problem while executing testMethod", boe);
            throw new WebApplicationException(
                    Response.status(Status.BAD_REQUEST)
                            .type(MediaType.TEXT_PLAIN)
                            .entity(resource.getString(boe.getMessage()))
                            .build());
        } catch (TechnicalException te) {
            log.log(Level.FINER, "Technical problem while executing testMethod", te);
            throw new WebApplicationException(
                    Response.status(Status.INTERNAL_SERVER_ERROR)
                            .type(MediaType.TEXT_PLAIN)
                            .entity(resource.getString("error.internal"))
                            .build());
        }
        
    }
    
}
It's easy to think about the number of duplicated lines of code we would have if such a solution is applied for all our exposed methods... Now it's time to go deeper into Jersey and JAX-RS.

Custom Exception mapping with Jersey

In a Three-Tier architecture, the Logic-Tier may throw an application specific BusinessException, the Data-Tier may throw a TechnicalException, and so on... As BusinessException are relative to business rule violations it might be interresting to alert the user with a comprehensive message in the Presentation-Tier. On the contrary, a synthetic message will be displayed to the user for TechnicalException that refers to problems that are not likely to happen.

Jersey makes it possible to bind Java Exception with specialized HTTP response. All we have to do is to register an ExceptionMapper for each Java Exception we want to handle in a generic manner.
@Provider
public class BusinessExceptionMapper implements ExceptionMapper<BusinessException> {

    private static final Logger log = Logger.getLogger(TestEndpoint.class.getName());
    
    private static final ResourceBundle resource = 
        ResourceBundle.getBundle("com.blogspot.avianey");

    @Override
    public Response toResponse(BusinessException e) {
        log.log(Level.FINER, "Business problem while executing testMethod", e);
        return Response.status(Status.BAD_REQUEST)
            .type(MediaType.TEXT_PLAIN)
            .entity(resource.getString(e.getMessage()))
            .build();
    }

}
@Provider
public class TechnicalExceptionMapper implements ExceptionMapper<TechnicalException> {

    private static final Logger log = Logger.getLogger(TestEndpoint.class.getName());
    
    private static final ResourceBundle resource = 
        ResourceBundle.getBundle("com.blogspot.avianey");

    @Override
    public Response toResponse(TechnicalException e) {
        log.log(Level.FINER, "Technical problem while executing testMethod", e);
        return Response.status(Status.INTERNAL_SERVER_ERROR)
            .type(MediaType.TEXT_PLAIN)
            .entity(resource.getString("error.internal"))
            .build();
    }

}
Just as service classes, @Provider classes should be placed in a package that is scanned by the Jersey Servlet at startup so they can be used at runtime.
<servlet>
    <servlet-name>Jersey</servlet-name>
    <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
    <init-param>
        <param-name>com.sun.jersey.config.property.packages</param-name>
        <param-value>pkg.provider; pkg.endpoint</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>
We no longer have to handle explicitly BusinessException and TechnicalException in our exposed method :
@Path("/test")
@Produces("application/json")
public class TestEndpoint {
    
    private static final Logger log = Logger.getLogger(TestEndpoint.class.getName());
    
    @Inject TestService service;
    
    @POST
    @Path("{pathParam: \\d+}")
    public String testMethod(
            @PathParam("pathParam") Long id,
            @FormParam("date") String dateStr,
            @Context HttpServletRequest request) {

        // verifying that the user is logged in
        User user = (User) request.getSession().getAttribute("user");
        if (user == null) {
            throw new WebApplicationException(Status.UNAUTHORIZED);
        }
        
        // verifying the format of the sent date
        Date date = null;
        SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy");
        try {
            date = sdf.parse(dateStr);
        } catch (ParseException pe) {
            throw new WebApplicationException(Status.BAD_REQUEST);
        }
        
        // calling the business logic
        // no need to catch exception here anymore
        return service.testOperation(id, date, user);
        
    }
    
}

Custom Jersey parameters

JAX-RS Param annotations like QueryParam, FormParam and PathParam can be apply to any Java Object that have a constructor with a single String argument. When calling a method with such an annotated parameter, Jersey instantiate a new object with the param value and pass it to the method.

We can easily use this feature to centralize the validation of date parameters accross all of our exposed methods :
/**
 * A DateParam to validate the format of date parameters received by Jersey
 */
public class DateParam {

    private Date date;
    
    public DateParam(String dateStr) {
        SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy");
        try {
            this.date = sdf.parse(dateStr);
        } catch (ParseException pe) {
            throw new WebApplicationException(Status.BAD_REQUEST);
        }
    }
    
    public Date value() {
        return this.date;
    }
    
}
Once again, our original code gained in maintainability :
@Path("/test")
@Produces("application/json")
public class TestEndpoint {
    
    private static final Logger log = Logger.getLogger(TestEndpoint.class.getName());
    
    @Inject TestService service;
    
    @POST
    @Path("{pathParam: \\d+}")
    public String testMethod(
            @PathParam("pathParam") Long id,
            @FormParam("date") DateParam date,
            @Context HttpServletRequest request) {

        // verifying that the user is logged in
        User user = (User) request.getSession().getAttribute("user");
        if (user == null) {
            throw new WebApplicationException(Status.UNAUTHORIZED);
        }
        
        // calling the business logic
        return service.testOperation(id, date.value(), user);
        
    }
    
}

Contextual object injection

Now we will see how it possible to directly inject the logged User into our exposed method. There is two diferent approaches doing this :
  1. Use the @Context annotation with a custom provider to inject the desired Object
  2. Create a custom annotation and its Injectable and associated InjectableProvider
In the first approach, we define a new Injectable implementation for the User Type and associate it with the @Context annotation.
@Provider
public class LoggedUserProvider extends AbstractHttpContextInjectable<User>
        implements InjectableProvider<Context, Type> {

    private final HttpServletRequest r;
    
    public LoggedUserProvider(@Context HttpServletRequest r) {
        this.r = r;
    }

    /**
     * From interface InjectableProvider
     */
    @Override
    public Injectable<user> getInjectable(ComponentContext ic, Context a, Type c) {
        if (c.equals(User.class)) {
            return this;
        }
        return null;
    }

    /**
     * From interface InjectableProvider
     * A new Injectable is instanciated per request
     */
    @Override
    public ComponentScope getScope() {
        return ComponentScope.PerRequest;
    }

    /**
     * From interface Injectable
     * Get the logged User associated with the request
     * Or throw an Unauthorized HTTP status code
     */
    @Override
    public User getValue(HttpContext c) {
        final User user = Contestr.getSessionUser(r);
        if (user == null) {
            throw new WebApplicationException(Response.Status.UNAUTHORIZED);
        }
        return user;
    }
}
The exposed method consist now of one line of code only !
@Path("/test")
@Produces("application/json")
public class TestEndpoint {
    
    @Inject TestService service;
    
    @POST
    @Path("{pathParam: \\d+}")
    public String testMethod(
            @PathParam("pathParam") Long id,
            @FormParam("date") DateParam date,
            @Context User user) {
        // calling the business logic
        return service.testOperation(id, date.value(), user);
    }
    
}
In the second approach, we need to define a new annotation :
@Target({ ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface LoggedUser {}
Just as seen before, we need to associate the @LoggedUser with a new Injectable for the User type :
@Provider
public class LoggedUserProvider  
    implements Injectable<User>, InjectableProvider<LoggedUser, Type> {

    private final HttpServletRequest r;
    
    public LoggedUserProvider(@Context HttpServletRequest r) {
        this.r = r;
    }

    @Override
    public Injectable<user> getInjectable(ComponentContext cc, LoggedUser a, Type c) {
        if (c.equals(User.class)) {
            return this;
        }
        return null;
    }

    /**
     * From interface InjectableProvider
     * A new Injectable is instanciated per request
     */
    @Override
    public ComponentScope getScope() {
        return ComponentScope.PerRequest;
    }

    /**
     * From interface Injectable
     * Get the logged User associated with the request
     * Or throw an Unauthorized HTTP status code
     */
    @Override
    public User getValue() {
        final User user = (User) r.getSession().getAttribute("user");
        if (user == null) {
            throw new WebApplicationException(Response.Status.UNAUTHORIZED);
        }
        return user;
    }
    
}
This solution is much more flexible than the previous one as we can build more than one annotation for the same Type :
  • @LoggedUser : retrieve the logged in User and throw an HTTP 401 unauthorized status code if no logged in User is associated with the current request.
  • @AdminLoggedUser : retrieve the logged in User and throw an HTTP 401 unauthorized status code if no logged in User is associated with the current request or if the User is not an administrator.
@Path("/test")
@Produces("application/json")
public class TestEndpoint {
    
    @Inject TestService service;
    
    @POST
    @Path("{pathParam: \\d+}")
    public String testMethod(
            @PathParam("pathParam") Long id,
            @FormParam("date") DateParam date,
            @LoggedUser User user) {
        // calling the business logic
        return service.testOperation(id, date.value(), user);
    }
    
}
In a next post, I will cover the integration of Jersey with Guice or any other JSR-330 compliant IOC framework.
Fork me on GitHub