Necrown - Devlog #06 [Engine Dev] : Réécriture de fonctions et restauration d'objets
Splix Il y a 12 mois Premium Pro - Adhésion à vie0

Salut les gamecodeurs!!!

Dans le dernier devlog, nous avions créé un système orienté objet, censé nous permettre de standardiser la création de nos objets. Ainsi, tous les objets que nous créerons auront, via leur classe, une variable isObject, une variable type et une table prototype. Nous allons utiliser ces particularités aujourd’hui… Tout d’abord, nous allons créer un nouveau fichier: stdLib.lua, dans lequel nous allons ajouter ou remanier des fonctions standard du lua. Je ne vais pas tout détailler, mais on y importe le module utf8 de lua, on migre ici la fonction split que nous avions créé dans dataStorage et on l’ajoute au module string du lua, et on ajoute également tout un tas de fonctions utiles à différents modules (math, string, table…)

Ce qui va nous intéresser ici c’est d’utiliser les particularités de nos objets:

if not game.inDev then
  print = function(...) end
end

local oldType = type

function type(p_val)
  local old = oldType(p_val)
  if old == "table" and p_val.isObject then
    return p_val.type
  else
    return old
  end
end

function hasType(p_val, p_type)
  local old = oldType(p_val)
  if old == "table" and p_val.isObject then
    for _, v in pairs(p_val.prototype) do
      if v == p_type then return true end
    end
    return false
  else
    return (old == p_type)
  end
end

function isObject(p_val)
  if oldType(p_val) ~= "table" then return false end
  return (p_val.isObject == true)
end

Pour commencer, vous remarquerez que lorsque l’on n’est pas en version de développement, on réécrit print pour en faire une fonction vide (De toute manière la console ne s’ouvrira pas chez les joueurs, il n’y a donc pas besoin pour cette fonction d’effectuer un quelconque traitement). Ensuite, on enregistre en local la fonction type pour pouvoir continuer à l’utiliser ici, avant de la réécrire. Maintenant, quand on appellera la fonction type en lui envoyant un objet, elle nous renverra son type enregistré par nos soins, et non « table ». La fonction hasType va nous dire si l’objet qu’on lui envoie possède un certain type, et tout ça grâce à notre table prototype qui enregistre toute la chaîne d’héritage. La fonction isObject nous permet de nous assurer que l’élément qu’on lui envoie est bien une table avant de vérifier qu’elle possède la variable isObject.

Vous constatez maintenant l’intérêt d’avoir standardisé la création de nos objets: Cela nous permet d’ajouter tout un tas de fonctionnalités à notre code, et ce n’est que le début. Nous allons retourner dans le fichier dataStorage et modifier la fonction fileIO.save:

local function saveObject(p_obj)
  if p_obj.onSave then p_obj:onSave() end

  p_obj.address = nil
  p_obj.typeObj = p_obj.type
end

local function preSave(p_table)
  if isObject(p_table) then saveObject(p_table) end
  
  for _, v in pairs(p_table) do
    if hasType(v, "table") then
      if isObject(v) then saveObject(v) end
      preSave(v)
    end
  end
end

function fileIO.save(p_path, p_table)
  if string.find(p_path, "/") ~= nil then createDir(p_path) end
  preSave(p_table)
  return love.filesystem.write(p_path, serial.pack(p_table, nil, game.inDev and 0 or 1))
end

Lorsque l’on sauvegarde une table, il faut vérifier si celle-ci est un objet, ou si elle contient des objets, car dans ce cas on va devoir effectuer quelques action avant d’enregistrer les données dans un fichier. En effet, lorsque l’on sauvegarde un objet, cela ne sert à rien d’enregistrer la variable hex, qui enregistre l’adresse mémoire de notre objet, puisque cette adresse sera différente lorsqu’on le chargera depuis le fichier de sauvegarde. Par contre, on aura besoin du type de l’objet pour le recréer, et type est une variable qui provient de sa métatable, elle ne sera donc pas sauvegardée, voilà pourquoi il faut qu’on l’enregistre dans une variable typeObj. On va aussi appeler la fonction onSave de nos objet lorsqu’elle existe, afin de régler au besoin certains détails avant la sauvegarde. Toutes ces actions sont réalisées par la fonction saveObject, elle même appelée par la fonction récursive preSave, qui se chargera de vérifier tout le contenu des tables pour préparer les objets à la sauvegarde. Voyons maintenant pour le chargement des objets:

function fileIO.restore(p_table, p_obj)
  local obj = nil
  if p_table.typeObj ~= nil then
    obj = (classes[p_table.typeObj]):new()
  else
    obj = p_obj or {}
  end
  
  for k, v in pairs(p_table) do
    if k == "typeObj" then goto continue end
    
    if type(v) == "table" then
      obj[k] = fileIO.restore(v, obj[k])
    else
      obj[k] = v
    end
    
    ::continue::
  end
  
  if obj.onRestore then obj:onRestore() end
  
  return obj
end

Nous pourrons envoyer à la fonction restore les données chargées par la fonction load, et elle nous permettra de charger tous les objets de façon récursive. Cette fonction va récupérer le type de l’objet via typeObj et créer un nouvel objet de ce type, avant de réattribuer les valeurs de ses variables. Il faut toutefois noter que l’on envoie pas de paramètres lors de la recréation de l’objet. Dans la plupart des cas cela ne devrait pas poser de problèmes, mais il faudra se souvenir de ce détail quand nous réaliserons nos classes. Une fois notre objet recréé, on appel sa fonction onRestore quand elle existe, qui nous permettra de finaliser le chargement d’un objet quand c’est nécessaire. A noter que l’on peut également envoyer une table contenant des objets à cette fonction, comme elle est récursive, elle se chargera de charger tous les objets…

Petite astuce que vous avez sans doute remarqués: l’utilisation de l’instruction goto. Vous connaissez peut-être l’instruction continue, que l’on trouve dans beaucoup de langages de programmation, et qui permet de sauter une itération dans une boucle. Cette instruction n’existe pas en lua, mais on se sert ici de l’instruction goto pour faire comme si: elle permet de forcer notre programme à faire un saut jusqu’à une étiquette qu’on lui indique (ici l’étiquette ::continue::). C’est une astuce qui peut s’avérer très pratique…

Ainsi tout commence à s’interconnecter dans notre moteur, tout est lié. Mais nous avons encore du travail, car même si nous avons déjà bien avancé, même si le terrain est bien préparé, notre programme n’ouvre toujours qu’une simple fenêtre et une console, et rien de plus… Il nous faudra encore plusieurs devlogs pour commencer à vraiment s’occuper de la création de notre jeu.

Mais en attendant, bon code à tous!!!

Laisser un commentaire

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.