DevBlog #3 : Asset Bundling
BACKGROUND
There are three things you need to know about me in relevance and in context for this topic:
- I am a self-taught programmer. I dived in the programming world without any formal education or knowledge in regards to game development and programming.
- I really love the HandMade Hero series. I have learned so much from it than from my years (currently still) in university.
- I hate Java. (Yes, this is out of context actually)
With those said, here is the event that transpired. I am currently developing my next big game, but of course I am still in the part of making my own personal engine/framework for it. I am now in the part of doing an asset management system and asynchronous asset loading system, it is working quite perfectly for my needs and I dare say that its API is really good. I planned on abstracting it a little more so that I can turn it into a library as a contribution to the löve community.
Then I happened to see a HandMade Hero video in YouTube’s home screen. After watching that, I watched the some of the vides in the Asset Pipeline playlist. It is really interesting. Bundling all of your assets, images and sounds, to a single binary file is a challenge first and a wonderful achievement second.
But since HandMade Hero is basically from scratch, I did a little bit of research. I have not found any article on how the whole thing works. I just know that I need to get to do the following:
- Make a new file
- Put a “magic number”
- Put some header details like tags and info
- Put the offset (pointer to the asset’s data) and the size of the data
- Put the data of the assets
Now, all of the steps above seems easy, but remember that we are dealing with binary and bytes. If you look at the code Casey is writing, there are a lot of things confusing and difficult to understand. But since I am overly excited and challenged to write my own “asset bundler” in Lua and Löve, nothing can stop me!
START OF THE QUEST
First of all, I need to learn how to deal with bytes in Lua. I have found these links as a guide using normal Lua io library and using Lua FFI to handle larger files.
Second, I need to learn how to parse the PNG. Extract the PNG header and its actual data. I have found this simple guide about it.
There is a problem with the second step, gladly it does because that made me ask over the löve discord server about some typo about the modulo operation with the guide.
So one of the veterans there @AuahDark (the author behind the asynchronous asset loading library I use) pointed out that I could just write to a file the loaded image data so that saves me from dealing with parsing the PNG all in all.
With a long talk over the support channel and with new knowledge about the love.data api, I am now able to make a simple (for now) asset bundler written in Lua with Löve.
I am going to share to you two methods on how to do it.
It is a good thing to familiarize yourself with the Lua and Löve API first.
- File
- ImageData
- FileData
- love.image.newImageData
- love.graphics.newImage
- love.filesystem.newFile
- love.filesystem.read
- love.data.pack
- love.data.unpack
- love.data.newDataView
- string.pack format specifier
Method I: ImageData -> Packed File
I.a WRITING
- Load an ImageData using
local imagedata = love.image.newImageData("path-to-image.png")
- Get all the necessary details like
format
,width
,height
, and so on. - Create a new File in write mode with
local file = love.filesystem.newFile("filename", "w")
- Pack data to be stored in the header:
local data_dimensions = love.data.pack("data", "<I4I4", imagedata:getDimensions()) local data_format = love.data.pack("data", "<s4", imagedata:getFormat()) --and more
(Disclaimer: I am not that much knowledgable about the Lua 5.3 string.pack format specifier) The "<I4I4"
means set to little endian
+ unsigned integer with 4 bytes
The "<s4"
means set to little endian
+ string length coded as an unsigned integer with 4 bytes
- Write the packed data to the file.
file:write(data_dimensions) file:write(data_format)
- Finally, write the ImageData itself to the file.
file:write(imagedata)
I.b READING
- Read the pack file and store it as a FileData with
local packfile = love.filesystem.newFileData("filename")
- Get the header strings we stored earlier (format and dimensions).
local width, height, index = love.data.unpack("<I4I4", packfile) local format, index2 = love.data.unpack("<s4", packfile, index)
- Create a Data view with
local view = love.data.newDataView(packfile, index2 - 1, packfile:getSize() - index2 + 1)
(To validate that we got the proper result, check with
print(view:getSize(), width * height * 4)
they should be the same value.)
- Create an ImageData with
local imagedata = love.image.newImageData(width, height, format, view)
- Create a Drawable Image with
local image = love.graphics.newImage(imagedata)
Now you can use this for displaying in the screen.
Let us now go with Method II.
Method II: Image File -> Packed File
II.a WRITING
- Read the image file (as a whole) and store it as a data with
local data = love.filesystem.read("data", "path-to-image.png")
- Create a File with write mode with
local file = love.filesystem.newFile("filename", "w")
- Pack the header tags you want.
local data_name = love.data.pack("data", "<s4", "Brandon B. Lim-it") local data_kind = love.data.pack("data", "<s4", "png") local data_path = love.data.pack("data", "<s4", "assets/images/foo.png")
- Write the packed data to the file.
file:write(data_name) file:write(data_kind) file:write(data_path)
(To validate, please open the “packfile” using a binary/hex viewer. For Linux users, you can use the
xxd -l 90 path-to-packfile
(90 is to only show the first few lines of the file). You should see the stringBrandon B. Lim-it
,png
,assets/images/foo.png
in it.) - Write the Image to the file with
file:write(data)
II.b READING
- Read the pack file and store it as a FileData with
local packfile = love.filesystem.newFileData("filename")
- Get the header strings we stored.
local str_author, index = love.data.unpack("<s4", packfile) local str_kind, index2 = love.data.unpack("<s4", packfile, index) local str_path, index3 = love.data.unpack("<s4", packfile, index2)
- Create a Data view with
local view = love.data.newDataView(packfile, index3 - 1, packfile:getSize() - index3 + 1)
- Create the ImageData from the Data view with
local imagedata = love.image.newImageData(view)
- Get the dimensions of the ImageData with
local width, height = imagedata:getDimensions()
- Create a Drawable image with
local image = love.graphics.newImage(imagedata)
Löve is really amazing, it has easily allow us to do complicated stuff with just few lines of code.
You may check the file saved in the save directory. Please see love.filesystem.getSaveDirectory for info on where to find it.
Comparison
The problem with Method I is that we have to store data like dimensions and format and so on which could be tedious.
The problem with Method II is that we store the whole data of the file which results in bigger file size.
(I will add more as soon as I find more)
Conclusion
Here is a snippet of using Method II (which I prefer). It includes some functions for ease.
--main.lua
local img
local function pack(file, format, str)
assert(file:type() == "File", "arg1 must be a File")
assert(type(format) == "string", "arg2 must be a string")
assert(type(str) == "string", "arg3 must be a string")
local data = love.data.pack("data", format, str)
local res, err = file:write(data)
if not res then
error(err)
end
end
local function pack_table(file, t)
assert(file:type() == "File", "arg1 must be a File")
assert(type(t) == "table", "arg2 must be a table")
local n = 0
for i = 1, #t do
local format = t[i][1]
local str = t[i][2]
pack(file, format, str)
n = n + 1
end
return n
end
local function get_headers(packdata, count, format)
assert(packdata:type() == "FileData", "arg1 must be a FileData")
assert(type(count) == "number", "arg2 must be a number")
assert(type(format) == "string", "arg3 must be a string")
local t = {}
local prev
for i = 1, count do
local str, index = love.data.unpack("<s4", packdata, prev)
t[i] = str
prev = index
end
return t, prev
end
local function do_pack()
local path = "avatar.png"
-- WRITING
local data = love.filesystem.read("data", path)
local filename = "pack2"
if love.filesystem.getInfo(filename) then
love.filesystem.remove(filename)
end
local file = love.filesystem.newFile(filename, "w")
-- Can pack individually
-- pack(file, "<s4", "brbl")
-- pack(file, "<s4", "png")
-- pack(file, "<s4", path)
-- Or use a table for ease?
local t = {
{ "<s4", "brbl" },
{ "<s4", "png" },
{ "<s4", path },
}
local n = pack_table(file, t)
file:write(data)
-- READING
local packdata = love.filesystem.newFileData(filename)
local headers, last = get_headers(packdata, n or 3, "<s4")
print("Headers: " .. table.concat(headers, ", "))
print("Last Index: " .. last)
local view = love.data.newDataView(packdata, last - 1, packdata:getSize() - last + 1)
local imgdata = love.image.newImageData(view)
local w, h = imgdata:getDimensions()
img = love.graphics.newImage(imgdata)
end
do_pack()
function love.draw()
love.graphics.draw(img, 32, 32)
end
-- Since this main.lua file is in a folder called "temp"
-- xxd -l 90 ~/.local/share/love/temp/pack2
-- test with "love ."
FUTURE
Right now this method works for a single file. Stay tuned as I test now with packing multiple PNG files to a single file.
Plans:
- As a separate library.
- As a program with GUI (drag-drop files, customize the header tags, etc)
If you have questions, follow me at Twitter and freely ask there.