games / lua
Tables : tableaux, dictionnaires et itération
Explication
La table : la seule structure de données de Lua, et c'est voulu
Lua fait un choix radical : il n'a qu'UN SEUL type de structure de données composite, la table. Pas de tableau séparé, pas de dictionnaire séparé, pas de liste séparée — tout est une table, qui peut se comporter comme un tableau indexé par des nombres, comme un dictionnaire indexé par des chaînes, ou comme les deux à la fois. Cette simplicité radicale est à la fois la grande force et la grande subtilité du langage.
Ce que vous allez apprendre
- Utiliser une table comme tableau indexé (à partir de 1) ou comme dictionnaire clé/valeur
- Ajouter, insérer et retirer des éléments avec
table.insert/table.remove - Comprendre pourquoi
#tn'est fiable que sur une séquence sans trou - Parcourir une table imbriquée (tables de tables) pour représenter des structures complexes
- Écrire une fonction de copie explicite pour éviter le piège de la copie par référence
Dans quel contexte ?
Un script d'inventaire de jeu doit stocker à la fois une liste ordonnée d'objets équipés (l'arme en main, en position 1) et un dictionnaire de statistiques du joueur (santé, mana, niveau). En Lua, ces deux besoins, qui semblent différents, se résolvent avec la même structure : la table. Savoir laquelle des deux "formes" utiliser, et surtout comment éviter que deux inventaires ne partagent accidentellement la même mémoire, est une compétence essentielle avant d'écrire le moindre système de jeu un peu sérieux.
| Usage de la table | Syntaxe typique | Parcours recommandé |
|---|---|---|
| Tableau (array) | {"a", "b", "c"} | ipairs |
| Dictionnaire | {cle = valeur} | pairs |
| Les deux mélangés | {1, 2, cle = valeur} | ipairs puis pairs séparément |
Piège fréquent
local copie = original ne copie jamais le contenu d'une table : les deux variables pointent vers la même donnée en mémoire. Modifier copie modifie aussi original. Il faut une fonction explicite (shallowCopy ou deepCopy, montrées plus bas) pour obtenir une vraie copie indépendante.
Le décalage d'index qui déroute tout le monde au début
Contrairement à la quasi-totalité des langages modernes (Python, JavaScript, C), les tables Lua utilisées comme tableau commencent leur indexation à 1, pas à 0. Ce choix, hérité des langages plus anciens comme Fortran, est une source d'erreurs classique chez les développeurs venant d'un autre langage — le réflexe à acquérir dès maintenant.
Pourquoi #t n'est pas toujours fiable
L'opérateur # donne la "longueur" d'une table, mais son comportement n'est bien défini QUE si la table ne contient pas de "trou" (un index manquant au milieu de la séquence). Dès qu'un trou apparaît, le résultat de #t devient imprévisible selon l'implémentation. C'est une limite à connaître pour éviter des bugs subtils dans du code qui semble pourtant correct au premier regard.
Le piège de la copie par référence
Comme dans beaucoup de langages, assigner une table à une nouvelle variable ne copie PAS son contenu : les deux variables pointent vers la même table en mémoire, et modifier l'une modifie l'autre. Il faut écrire soi-même une fonction de copie explicite (superficielle ou profonde selon le besoin) si l'on veut réellement dupliquer les données — un oubli fréquent qui cause des bugs difficiles à comprendre.
Commandes & code
Tables : tableaux, dictionnaires et itération
La table est LA structure de données unique de Lua : elle sert à la fois de tableau, de dictionnaire, d'objet, et même de module.
-- Table utilisée comme tableau (array), indices entiers commençant à 1 (PAS 0)
local inventory = {"potion", "epee", "bouclier"}
print(inventory[1]) -- "potion" (premier élément à l'index 1, pas 0)
print(#inventory) -- 3, l'opérateur "#" donne la longueur (pour un tableau sans trou)
inventory[4] = "arc" -- ajout en fin
table.insert(inventory, "casque") -- ajoute en fin également
table.insert(inventory, 1, "torche") -- insère en position 1, décale le reste
table.remove(inventory, 1) -- retire l'élément en position 1-- Table utilisée comme dictionnaire (map/hash), clés arbitraires
local player = {
name = "Alice",
level = 12,
["class"] = "Mage", -- syntaxe équivalente à "class = "Mage""
[1] = "special_first_slot", -- une table peut mélanger clés entières et clés string
}
print(player.name, player["level"], player.class)-- Tables imbriquées, très courantes pour représenter des structures complexes
local gameState = {
players = {
{ name = "Alice", hp = 100, position = { x = 10, y = 20 } },
{ name = "Bob", hp = 80, position = { x = 5, y = 15 } },
},
settings = { difficulty = "hard", maxPlayers = 4 },
}
print(gameState.players[1].name) -- "Alice"
print(gameState.players[2].position.x) -- 5
gameState.players[1].hp = gameState.players[1].hp - 20-- Piège classique : "#" (longueur) n'est fiable QUE sur une séquence sans trou (nil au milieu)
local broken = {1, 2, nil, 4}
print(#broken) -- résultat NON SPÉCIFIÉ par le standard (peut être 2 ou 4 selon l'implémentation)
-- Solution : maintenir une longueur explicite si des trous sont possibles, ou utiliser table.pack
local safe = table.pack(1, 2, nil, 4)
print(safe.n) -- 4, table.pack stocke explicitement le nombre d'arguments dans le champ "n"-- Copie de table : l'affectation directe copie la RÉFÉRENCE, pas le contenu (comme les objets en JS/Python)
local original = {hp = 100, mana = 50}
local reference = original -- reference pointe vers la MÊME table
reference.hp = 0
print(original.hp) -- 0 aussi ! ce n'était pas une copie
-- Copie superficielle (shallow copy) explicite
local function shallowCopy(t)
local copy = {}
for k, v in pairs(t) do
copy[k] = v
end
return copy
end
-- Copie profonde (deep copy) récursive, nécessaire pour des tables imbriquées
local function deepCopy(t)
if type(t) ~= "table" then return t end
local copy = {}
for k, v in pairs(t) do
copy[k] = deepCopy(v)
end
return copy
end-- Fonctions utilitaires courantes de la bibliothèque "table"
local scores = {42, 17, 99, 3, 56}
table.sort(scores) -- tri en place, croissant par défaut
table.sort(scores, function(a, b) return a > b end) -- tri décroissant avec fonction de comparaison
print(table.concat({"a", "b", "c"}, ", ")) -- "a, b, c"
print(table.concat(scores, "-")) -- "99-56-42-17-3"Résumé
- Les indices de tableau Lua commencent à 1, pas 0 ;
#tn'est fiable que sans trou dans la séquence. - Une table est copiée PAR RÉFÉRENCE : il faut une fonction explicite pour une copie superficielle ou profonde.
table.insert,table.remove,table.sort,table.concatcouvrent la majorité des manipulations courantes.ipairsparcourt la partie séquentielle dans l'ordre,pairsparcourt toutes les clés sans ordre garanti.
Exercices pratiques
Mission : sauver un inventaire de crafting corrompu
Objectif : Diagnostiquer un bug de partage de mémoire entre deux inventaires et une longueur imprévisible, puis corriger le script.
Contexte
Un script de crafting FiveM prépare un inventaire de secours à partir de l'inventaire principal du joueur :
local mainInventory = { "potion", "epee", "bouclier" }
local backupInventory = mainInventory
table.remove(backupInventory, 2) -- le joueur "vend" une copie de l'épée depuis le backup
print(#mainInventory) -- affiche 2, alors que le joueur n'a RIEN vendu de son inventaire principal !
mainInventory[5] = "torche"
mainInventory[4] = nil
print(#mainInventory) -- résultat imprévisible selon le testeurLe créateur ne comprend pas pourquoi vendre depuis backupInventory fait disparaître l'épée de mainInventory, ni pourquoi #mainInventory donne des résultats différents entre deux tests.