Découverte du format HDF5 en python

Posted on lun. 08 juillet 2019 in hobby

La map d'un jeu open-world, ça représente quoi ?

Ceux qui me connaissent savent que je code sur mon temps libre. Et il y a quelques temps, je me suis mis en tête de coder pour le fun un petit jeu open-world sur le thème du monde sous-marin. Très vite, la problématique du stockage de la carte de ce jeu s'est posée. Comment stocker ça correctement.

En gros, voilà comment ça se présente : on est dans un jeu sous-marin, donc je dois pouvoir stocker la profondeur (on utilisera un integer pour faire simple) de n'importe quel endroit sur la carte. En partant sur une granularité de 1m², si je veux une carte de 5km*5km, ça donne 5000*5000=25 millions de points à stocker1.

25 millions d'integer, ça prend grosso-modo 50Mo. Sans structure et en utilisant des short int de 2 octets. Cool !

En RAM, ça passe large, même sur un raspberry pi ! Plus qu'à stocker ça sur disque.

Place au code

Flat

Pour le dev, on va commencer par stocker ça dans des fichiers "flat", tout simple.

Admettons que j'ai une fonction depth(x,y) qui pour chaque coordonnée (x,y) me génère une profondeur.

Génération de ce fichier map flat :

data = ''
for y in range(0, int(GAME_HEIGHT)):
    for x in range(0, int(GAME_WIDTH)):
        data += '{} '.format(int(depth(x,y)))
    data += '\n'
with open(output_file, "w") as f:
    f.write(data)

Facile !

Et ça donne un fichier d'un peu moins de 100Mo. Et 12Mo gzippé. Par contre, ça veut dire que pour l'utiliser, il faut éventuelle le dézipper et écrire toute la mécanique d'import du fichier en RAM, et de requêtage.

Ca reste clairement jouable !

Sqlite

Et si on avait envie d'utiliser un format de BDD parce qu'on aime ça ? genre du sqlite, ça donnerait quoi ? En plus, c'est fourni en standard dans python.

import sqlite3
conn = sqlite3.connect(output_file+'.tmp')
cur = conn.cursor()
cur.execute('CREATE TABLE map (x SMALLINT, y SMALLINT, h SMALLINT);')
for y in range(0, int(GAME_HEIGHT)):
    for x in range(0, int(GAME_WIDTH)):
        c = int(depth(x,y))
        cur.execute("INSERT INTO map VALUES (?,?,?)", (x, y, c))

conn.commit()
cur.execute('VACUUM;')
conn.close()

Bon, par contre, ça nous donne un fichier d'un peu plus de 400Mo, pour les mêmes données que précédemment. Plutôt embêtant. Et même inenvisageable.

HDF5

Devant les résultats obtenus pour Sqlite, je me suis mis en tête de chercher un autre format de fichier à-la sqlite, mais qui donnerait des fichiers plus petits. Et j'ai fini par trouvé le HDF5. En python, ça s'utilise avec la lib h5py ou pytables. Je suis parti avec le dernier.

import tables
db_struct = {
    'x': tables.Int16Col(),
    'y': tables.Int16Col(),
    'h': tables.Int16Col()
}

h5file = tables.open_file(output_file, mode="w", title='Map')
filters = tables.Filters(complevel=9)       # petit truc ici : je demande la compression au max avec les algo de commpresison standards par defaut de hdf5
group = h5file.create_group('/', 'group', 'Group')
table = h5file.create_table(group, 'map', db_struct, filters=filters)

heights = table.row

for y in range(0, int(GAME_HEIGHT)):
    for x in range(0, int(GAME_WIDTH)):
        heights['x'] = x
        heights['y'] = y
        heights['h'] = int(depth(x,y))
        heights.append()

table.flush()
table.flush()
h5file.close()

Là, c'est tout de suite mieux ! On a directement un fichier de 12Mo, et qui s'utilise peu ou prou comme un fichier Sqlite :

h5file = tables.open_file(self.m5p, mode='r')
rows = self.h5file.root.group.map.row.table
depth = rows.read_where('((x=={}) & (y=={}))'.format(x,y))['h'][0]
h5file.close()

Banco pour HDF5, alors !


  1. 5km*5km, c'est juste le début, j'espère bien en générer des plus grande, mais là, je bloque sur d'autres choses :-)