diff --git a/lib/gamnit/bmfont.nit b/lib/gamnit/bmfont.nit
new file mode 100644
index 0000000000..4abdaa54ee
--- /dev/null
+++ b/lib/gamnit/bmfont.nit
@@ -0,0 +1,422 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Parse Angel Code BMFont format and draw text
+#
+# The BMFont format supports packed textures, varying advance per character and
+# even kernings. It can be generated with a number of tools, inluding:
+# * BMFont, free software Windows app, http://www.angelcode.com/products/bmfont/
+# * Littera, a web app, http://kvazars.com/littera/
+#
+# Format reference: http://www.angelcode.com/products/bmfont/doc/file_format.html
+module bmfont
+
+private import dom
+
+import font
+
+# BMFont description, parsed with `Text::parse_bmfont` or loaded as a `BMFontAsset`
+#
+# This class flattens all the `info` and `common` data.
+class BMFont
+
+ # ---
+ # info part
+ #
+ # How the font was generated.
+
+ # Name of the source true type font
+ var face: Text
+
+ # Size of the source true type font
+ var size: Int
+
+ # Is the font bold?
+ var bold: Bool
+
+ # Is the font italic?
+ var italic: Bool
+
+ # Does the font uses the Unicode charset?
+ var unicode: Bool
+
+ # Padding for each character
+ #
+ # In the format `up,right,down,left`
+ var padding: String
+
+ # Spacing for each character
+ #
+ # In the format `horizontal,vertical`.
+ var spacing: String
+
+ # ---
+ # common part
+ #
+ # Information common to all characters
+
+ # Distance in pixels between each line of text
+ var line_height: Int
+
+ # Pixels from the top of the line to the base of the characters
+ var base: Int
+
+ # Width of the texture
+ var scale_w: Int
+
+ # Height of the texture
+ var scale_h: Int
+
+ # Textures
+ var pages = new Map[String, TextureAsset]
+
+ # Characters in the font
+ var chars = new Map[Char, BMFontChar]
+
+ # Distance between certain characters
+ var kernings = new HashMap2[Char, Char, Int]
+
+ redef fun to_s do return "<{class_name} {face} at {size} pt, "+
+ "{pages.length} pages, {chars.length} chars>"
+
+ # TODO
+ #
+ # # From info
+ # charset
+ # stretchH
+ # smooth
+ # aa
+ # outline
+ #
+ # # From common
+ # packed
+ # alphaChnl
+ # redChnl
+ # greenChnl
+ # blueChnl
+end
+
+# Description of a character in a `BMFont`
+class BMFontChar
+
+ # Subtexture left coordinate
+ var x: Int
+
+ # Subtexture top coordinate
+ var y: Int
+
+ # Subtexture width
+ var width: Int
+
+ # Subtexture height
+ var height: Int
+
+ # Drawing offset on X
+ var xoffset: Int
+
+ # Drawing offset on Y
+ var yoffset: Int
+
+ # Cursor advance after drawing this character
+ var xadvance: Int
+
+ # Full texture contaning this character and others
+ var page: TextureAsset
+
+ # TODO Channel where the image is found
+ #var chnl: Int
+
+ # Subtexture with this character image only
+ var subtexture: Texture = page.subtexture(x, y, width, height) is lazy
+end
+
+redef class Text
+
+ # Parse `self` as an XML BMFont description file
+ #
+ # Reports only basic XML format errors, other errors may be ignored or
+ # cause a crash.
+ #
+ # ~~~
+ # var desc = """
+ #
+ #
+ #
+ #
+ #
+ #
+ #
+ #
+ #
+ #
+ #
+ #
+ #
+ #
+ #
+ # """
+ #
+ # var fnt = desc.parse_bmfont("dir_in_assets").value
+ # assert fnt.to_s == ""
+ # assert fnt.line_height == 80
+ # assert fnt.kernings['A', 'C'] == -1
+ # assert fnt.chars['A'].page.path == "dir_in_assets/arial.png"
+ # ~~~
+ fun parse_bmfont(dir: String): MaybeError[BMFont, Error]
+ do
+ # Parse XML
+ var xml = to_xml
+ if xml isa XMLError then
+ var msg = "XML Parse Error: {xml.message}:{xml.location or else 0}"
+ return new MaybeError[BMFont, Error](maybe_error=new Error(msg))
+ end
+
+ # Basic sanity check
+ var roots = xml["font"]
+ if roots.is_empty then
+ var msg = "Error: the XML document doesn't declare the expected `font` root"
+ return new MaybeError[BMFont, Error](maybe_error=new Error(msg))
+ end
+
+ # Expect the rest of the document to be well formatted
+ var root = roots.first
+
+ var info = root["info"].first
+ assert info isa XMLAttrTag
+ var info_map = info.attributes_to_map
+
+ var common = root["common"].first
+ assert common isa XMLAttrTag
+ var common_map = common.attributes_to_map
+
+ var fnt = new BMFont(
+ info_map["face"],
+ info_map["size"].to_i,
+ info_map["bold"] == "1",
+ info_map["italic"] == "1",
+ info_map["unicode"] == "1",
+ info_map["padding"],
+ info_map["spacing"],
+ common_map["lineHeight"].to_i,
+ common_map["base"].to_i,
+ common_map["scaleW"].to_i,
+ common_map["scaleH"].to_i
+ )
+
+ # Pages / pixel data files
+ var xml_pages = root["pages"].first
+ for page in xml_pages["page"] do
+ if not page isa XMLAttrTag then continue
+
+ var attributes = page.attributes_to_map
+ var file = dir / attributes["file"]
+ fnt.pages[attributes["id"]] = new TextureAsset(file)
+ end
+
+ # Char description
+ for item in root["chars"].first["char"] do
+ if not item isa XMLAttrTag then continue
+
+ var attributes = item.attributes_to_map
+ var id = attributes["id"].to_i.code_point
+
+ var c = new BMFontChar(
+ attributes["x"].to_i, attributes["y"].to_i,
+ attributes["width"].to_i, attributes["height"].to_i,
+ attributes["xoffset"].to_i, attributes["yoffset"].to_i,
+ attributes["xadvance"].to_i,
+ fnt.pages[attributes["page"]])
+
+ fnt.chars[id] = c
+ end
+
+ # Kerning between two characters
+ var kernings = root["kernings"]
+ if kernings.not_empty then
+ for item in kernings.first["kerning"] do
+ if not item isa XMLAttrTag then continue
+
+ var attributes = item.attributes_to_map
+ var first = attributes["first"].to_i.code_point
+ var second = attributes["second"].to_i.code_point
+ var amount = attributes["amount"].to_i
+ fnt.kernings[first, second] = amount
+ end
+ end
+
+ return new MaybeError[BMFont, Error](fnt)
+ end
+end
+
+# BMFont from the assets folder
+#
+# ~~~
+# redef class App
+# var font = new BMFontAsset("arial.fnt")
+# var ui_text = new TextSprites(font, ui_camera.top_left)
+#
+# redef fun on_create
+# do
+# super
+#
+# font.load
+# assert font.error == null
+#
+# ui_text.text = "Hello world!"
+# end
+# end
+# ~~~
+class BMFontAsset
+ super Asset
+ super Font
+
+ # Font description
+ #
+ # Require: `error == null`
+ fun desc: BMFont
+ do
+ # Cached results
+ var cache = desc_cache
+ if cache != null then return cache
+ var error = error
+ assert error == null else print_error error
+
+ # Load on first access
+ load
+ error = self.error
+ assert error == null else print_error error
+
+ return desc_cache.as(not null)
+ end
+
+ private var desc_cache: nullable BMFont = null
+
+ # Error at loading
+ var error: nullable Error = null
+
+ # XML description in the assets folder
+ private var text_asset = new TextAsset(path) is lateinit
+
+ # Load font description and textures from the assets folder
+ #
+ # Sets `error` if an error occurred, otherwise
+ # the font description can be accessed via `desc`.
+ fun load
+ do
+ var text_asset = text_asset
+ text_asset.load
+ var error = text_asset.error
+ if error != null then
+ self.error = error
+ return
+ end
+
+ var desc = text_asset.to_s
+ var fnt_or_error = desc.parse_bmfont(path.dirname)
+ if fnt_or_error.is_error then
+ self.error = fnt_or_error.error
+ return
+ end
+
+ var fnt = fnt_or_error.value
+ self.desc_cache = fnt
+
+ # Load textures too
+ for page_name, texture in fnt.pages do
+ texture.load
+
+ # Move up any texture loading error.
+ # This algo keeps only the latest error,
+ # but this isn't a problem on single page fonts.
+ error = texture.error
+ if error != null then self.error = error
+ end
+ end
+
+ redef fun write_into(text_sprites, text)
+ do
+ var dx = 0.0
+ var dy = 0.0
+
+ var line_height = desc.line_height.to_f
+
+ var prev_char = null
+ for c in text do
+
+ var partial_line_mod = 0.4
+
+ # Special characters
+ if c == '\n' then
+ dy -= line_height.to_f
+ dx = 0.0 #advance/2.0
+ prev_char = null
+ continue
+ else if c == pld then
+ dy -= line_height * partial_line_mod.to_f
+ prev_char = null
+ continue
+ else if c == plu then
+ dy += line_height * partial_line_mod.to_f
+ prev_char = null
+ continue
+ else if c.is_whitespace then
+ var advance = if desc.chars.keys.has(' ') then
+ desc.chars[' '].xadvance.to_f
+ else if desc.chars.keys.has('f') then
+ desc.chars['f'].xadvance.to_f
+ else 16.0
+ dx += advance
+ prev_char = null
+ continue
+ end
+
+ # Replace or skip unknown characters
+ if not desc.chars.keys.has(c) then
+ var rc = replacement_char
+ if rc == null then continue
+ c = rc
+ end
+
+ var char_info = desc.chars[c]
+ var advance = char_info.xadvance.to_f
+
+ var kerning = 0.0
+ if prev_char != null then
+ kerning = (desc.kernings[prev_char, c] or else 0).to_f
+ end
+
+ var x = dx + char_info.width.to_f/2.0 + char_info.xoffset.to_f + kerning
+ var y = dy - char_info.height.to_f/2.0 - char_info.yoffset.to_f
+ var pos = text_sprites.anchor.offset(x, y, 0.0)
+ text_sprites.sprites.add new Sprite(char_info.subtexture, pos)
+
+ dx += advance + kerning
+ prev_char = c
+ end
+ end
+
+ # Character replacing other charactesr missing from the font
+ private var replacement_char: nullable Char is lazy do
+ for c in "�?".chars do
+ if desc.chars.keys.has(c) then return c
+ end
+ return null
+ end
+end