Precarga de Web Fonts para juegos HTML5

11 septiembre, 2016 21:26 por

Esta es una traducción del artículo original publicado en el blog de Mozilla Hacks.

En el desarrollo de juegos hay dos métodos de renderizado de texto: a través de bitmap fonts y de vector fonts. Los bitmap fonts son esencialmente una imagen sprite sheet que contiene todos los caracteres de una fuente dada. El sprite sheet utiliza un archivo de fuente normal (tradicionalmente .ttf). ¿Cómo se aplica al desarrollo de juegos en la Web y juegos HTML5?

Puedes utilizar bitmap fonts como de costumbre – son simplemente imágenes, después de todo, y la mayoría de los motores de juego HTML 5 y librerías los soportan correctamente. Para renderizar vector fonts, podemos confiar en cualquier fuente que sea accesible vía CSS: esto incluye tanto las fuentes del sistema ya presentes en el ordenador del jugador (como Arial o Times New Roman), o Web Fonts, que se pueden descargar sobre la marcha, si no están ya presentes en el sistema.

Sin embargo, no todos los motores de juego o frameworks incluyen mecanismos para cargar esas fuentes como activos regulares –como imágenes o archivos de audio – y confían en que ellas estén ya presentes. Esto puede conducir a peculiaridades en las que el juego intenta reproducir un texto con una fuente que no está cargada todavía… En cambio, el jugador no obtendrá ningún texto, o texto reproducido con una fuente alternativa o por defecto.

En este artículo vamos a explorar algunas técnicas para precargar Web Fonts en nuestros juegos, y describrir cómo integrarlas con un framework popular para juegos 2D: Phaser.

Cómo funciona la carga de Web Fonts

Hay dos maneras de cargar un Web Font: a través de CSS (utilizando @font-face) o a través de JavaScript (utilizando la API Font Loading). La solución CSS ha estado disponible desde hace algún tiempo; mientras que la API JavaScript no se ha adoptado ampliamente por los navegadores todavía. Si quieres lanzar un juego en estos días, recomendamos el método CSS por su portabilidad.

Declaración con @font-face

Esto es simplemente una declaración en tu código CSS que te permite establecer una familia de fuentes y apunta a los lugares en los que se pueden obtener. En este fragmento se declara una familia de fuentes llamada Amatica SC, y asume que tenemos un archivo TTF como un activo.

@font-face {
  font-family: 'Amatica SC';
  font-style: normal;
  font-weight: 400;
  src: local('Amatica SC'),
       local('AmaticaSC-Regular'),
       url(fonts/amaticasc-regular.ttf) format('truetype');
}

Nota: además de señalar a archivos específicos, también podemos apuntar a nombres de fuentes que pueden estar instaladas en el ordenador del usuario (en este caso, Amatica SC o Amatica SC-Regular).

Cargar de verdad

Es importante recordar que, ¡la declaración de una familia de fuentes via CSS no carga la fuente! La fuente es cargada únicamente cuando el navegador detecta por primera vez que va a ser utilizada.

Esto puede causar un fallo visual: o bien el texto se muestra con una fuente por defecto y después cambia a la Web Font (esto es conocido como FOUT o flash de texto sin estilo (Flash Of Unstyled Text)); o el texto no se muestra en absoluto y permanece invisible hasta que la fuente está disponible. En sitios web esto no suele ser un gran problema, pero en juegos (Canvas/WebGL) ¡el navegador no lo vuelve a renderizar automáticamente cuando la fuente está disponible! Por lo que si intentamos mostrar el texto y la fuente no está disponible, este es un gran problema.

Así que en tenemos que descargar la fuente antes de intentar utilizarla en nuestro juego…

Cómo forzar la descarga de Web Fonts

La API de carga de fuentes CSS

La API JavaScript fuerza la carga de una fuente. Actualmente esto solo funciona en Firefox, Chrome, y Opera (puedes comprobar para la información de soporte más actualizada en caniuse.com).

Ten en cuenta que cuando se utiliza FontFaceSet, todavía tienes que declarar tus fuentes en algún lugar – en este caso con @font-face en el CSS.

El cargador de Web Fonts de Typekit

Este es un cargador de código abierto desarrollado por Typekit y Google – puedes verlo en el repositorio de Web Font Loader en Github. Éste puede cargar fuentes alojadas localmente, como también fuentes de repositorios populares como Typekit, Google Fonts, etc.

En el siguiente fragmento cargaremos Amatica SC directamente de Google Fonts y especificaremos una función de devolución de llamada – para renderizar texto en un canvas 2D – que será invocado cuando las fuentes estén cargadas y listas para su uso:

Librería FontFace Observer

FontFace Observer es otro cargador de código abierto que no contiene código para repositorios de fuentes comunes. Si tienes tus propias fuentes alojadas, esto podría ser una opción mejor que la de Typekit puesto que es un tamaño de archivo más ligero.

Esta librería utiliza una interfaz Promise – pero no te preocupes, hay una versión con un polyfill si necesitas el soporte para navegadores viejos. Aquí de nuevo necesitas declarar tus fuentes vía CSS, para que la librería sepa donde ir a obtenerlas:

Integrando la carga de fuentes en Phaser

Ahora que hemos visto como cargar Web Fonts en HTML5, vamos a hablar sobre cómo integrar estas fuentes con un motor de juego. El proceso será diferente de un motor o framework a otro. He tomado Phaser como ejemplo puesto que está muy extendida su utilización para el desarrollo de juegos 2D. Puedes echar un vistazo a algunos ejemplos aquí:

Y, por supuesto, está el repositorio Github con el código fuente completo, donde puedes mirar detenidamente lo que he construido.

Así funciona Phaser: el juego está dividido en estados del juego, cada uno de los cuales ejecuta una secuencia de fases. Las fases más importantes son init, preload, create, render, y update. La fase de precarga (preload) es donde debemos cargar los activos del juego como imágenes, sonidos, etc., pero desafortunadamente, el cargador de Phaser no proporciona un método para la precarga de fuentes.

Hay varias maneras de evitar o solucionar este problema:

Retrasar la representación de la fuente

Podemos utilizar la API Font Loading o una librería para forzar una descarga de la fuente en la fase de precarga. Sin embargo, esto crea un problema. El cargador de Phaser no nos permite indicar cuando toda la carga ha finalizado. Esto significa que no podemos pausar la carga y evitar que la fase de precarga termine hasta que podemos cambiar a create – aquí es donde querríamos establecer nuestro mundo del juego.

Un primer acercamiento sería retrasar la renderización del texto hasta que se cargue la fuente . Después de todo, tenemos una devolución de la llamada disponible en una promesa, ¿cierto?

function preload() {
  // cargar otros recursos
  // ...

  let font = new FontFaceObserver('Amatica SC');
  font.load().then(function () {
    game.add.text(0, 0, 'Lorem ipsum', {
      font: '12px Amatica SC',
      fill: '#fff'
    });
  }
}

Hay un problema con este método: ¿qué ocurre si la devolución de la llamada es invocada antes de que la fase de preload haya finalizado? Nuestro objeto Phaser.Text sería eliminado en el momento que cambiamos a create.

Lo que podemos hacer es proteger la creación del texto bajo dos banderas: una que indique que la fuente ha sido cargada, y una segunda que indique que la fase de creación ha comenzado:

var fontLoaded = false;
var gameCreated = false;

function createText() {
  if (!fontLoaded || !gameCreated) return;
  game.add.text(0, 0, 'Lorem ipsum', {
      font: '12px Amatica SC',
      fill: '#fff'
  });
}

function preload() {
  let font = new FontFaceObserver('Amatica SC');
  font.load().then(function () {
    fontLoaded = true;
    createText();
  });
}

function create() {
  gameCreated = true;
  createText();
}

La principal desventaja de este método es que ignoramos por completo el cargador de Phaser. Dado que este no pone las fuentes en la cola como un activo, el juego comenzará y las fuentes no estarán allí; esto probablemente causará un fallo o efecto de parpadeo. Otro problema es que la pantalla o barra de “Carga” ignorará las fuentes, se visualizará como si el 100% hubiera sido cargado, y cambiará al juego incluso aunque nuestros activos de fuentes todavía no hayan sido cargados.

Utilizando un cargador personalizado

¿Y si pudiéramos modificar el cargador de Phaser añadiéndole lo que queramos? ¡Podemos! Podemos extender Phaser.Loader y añadir un método al prototipo que enlistará un activo – ¡un Web Font! El problema es que necesitamos modificar un método interno (destinando a uso privado) en Phaser.Loader, loadFile, para poder decirle al cargador de Phaser cómo cargar la fuente , y cuando la carga ha finalizado.

// Creamos nuestro propia clase cargadora que extiende Phaser.Loader.
// Este nuevo cargado tendrá soporte para web fonts
function CustomLoader(game) {
    Phaser.Loader.call(this, game);
}

CustomLoader.prototype = Object.create(Phaser.Loader.prototype);
CustomLoader.prototype.constructor = CustomLoader;

// nuevo método para cargar web fonts
// esto sigue la estructura de todos los métodos para carga de recursos
CustomLoader.prototype.webfont = function (key, fontName, overwrite) {
    if (typeof overwrite === 'undefined') { overwrite = false; }

    // aquí fontName se almacena en la propiedad `url` de file
    // luego de ser agregado al a lista de files
    this.addToFileList('webfont', key, fontName);
    return this;
};

CustomLoader.prototype.loadFile = function (file) {
    Phaser.Loader.prototype.loadFile.call(this, file);

    // debemos invocar asyncComplete cuando el archivo ha sido cargado
    if (file.type === 'webfont') {
        var _this = this;
        // nota: file.url contiene el nombre de la fuente
        var font = new FontFaceObserver(file.url);
        font.load(null, 10000).then(function () {
            _this.asyncComplete(file);
        }, function ()  {
            _this.asyncComplete(file, 'Error loading font ' + file.url);
        });
    }
};

Una vez que el código está listo, necesitamos crear una instancia de la clase e intercambiarla en game.load. Este intercambio debe tener lugar tan pronto como sea posible: en la fase init del primer estado del juego ejecutado.

function init() {
    // intercambiar Phaser.Loader por el nuestro
    game.load = new CustomLoader(game);
}

function preload() {
    // ahora podemos cargar nuestra fuente como un recurso normal
    game.load.webfont('fancy', 'Amatica SC');
}

La ventaja de este método es la integración real con el cargador, por lo que si tenemos una barra de carga, ésta no finalizará hasta que la fuente haya sido totalmente descargada (o falle por un timeout). La desventaja, por supuesto, es que estamos modificando un método interno de Phaser, así que no tenemos garantía de que nuestro código seguirá funcionando en futuras versiones del framework.

Una solución tonta…

Un método que he utilizado en competencias de juego es no comenzar el juego del todo hasta saber que la fuente está preparada. Dado que la mayoría de los navegadores no renderizan un texto hasta que Web Font ha sido cargado, lo que hago es crear una pantalla con un botón de Reproducción que utiliza el Web Font… De este modo, sé que el botón será visible una vez que se haya cargado la fuente, por lo que es seguro comenzar el juego.

La desventaja obvia es que no comenzamos a cargar activos hasta que el jugador pulsa este botón… pero funciona y es muy fácil de implementar. Aquí hay una captura de pantalla de ejemplo de una de esas pantallas de presentación, creada con elementos regulares HTML5 DOM y animaciones CSS:

Captura de pantalla - juego

Y aquí lo tienes, ¡renderización de Web Fonts en juegos HTML5! En el futuro, una vez que la API Font Loading esté más desarrollada, motores de juego HTML5 y frameworks comenzarán a integrarla en su código, y esperemos que no tengamos que hacer esto nosotros mismos o buscar una solución útil.

Hasta entonces, ¡feliz codificación!

The following two tabs change content below.

Compartir artículo:

Start the discussion at foro.mozilla-hispano.org

  • ¡Participa!

    Firefox Friends »
    Agrega botones de Firefox en tu sitio web y comparte tu amor por Mozilla Firefox.
    Ayuda a otros usuarios en Twitter.
    Colabora con la comunidad »
    En Mozilla lo importante son las personas. Descubre cómo puedes colaborar.

    Boletín Firefox

    Suscríbete al boletín de novedades de Firefox.

  • Descargas

    Descarga los programas de Mozilla.

    Lo más visto

    cc-by-sa