Articles

Unity: Comment créer un Tilemap 2D par programmation

Pour le tutoriel vidéo, cliquez ici

Si vous devez créer différents Tilemaps de différentes tailles, consultez la deuxième partie de ce tutoriel

Le composant Tilemap a été introduit dans Unity 2017.2 et a considérablement facilité le processus de développement de jeux 2D. Avec la version 2018.3, le Tilemap isométrique a été introduit, offrant un excellent support pour les jeux 2.5D. Récemment, j’ai eu l’occasion de travailler étroitement avec ce composant et j’ai été mis au défi de créer des tuiles par programmation. Dans mon dernier jeu, j’ai beaucoup de niveaux et tous ont le même plateau basé sur le Tilemap. Mais la carte elle-même a une configuration unique au niveau. Évidemment, étant un développeur professionnel, je ne voulais pas créer 60 scènes de jeu et peindre tous les niveaux à la main, mais plutôt avoir un mécanisme pour remplir le plateau avec les éléments appropriés en fonction de l’entrée donnée. Si vous êtes curieux de savoir à quoi ressemble le résultat final, voici un lien vers le jeu.

Le code source est disponible sur GitHub, veuillez trouver le lien à la fin de ce tutoriel.

J’utilise la dernière Unity 2019.2.0.1f disponible. Pour travailler avec ce tutoriel, vous devriez avoir au moins la version 2018.3. Nous utiliserons la grille isométrique mais la technique décrite est applicable à tout type.

Avant de commencer, je suggère fortement de lire cet environnement 2D isométrique brillant avec l’entrée de blog Tilemap pour obtenir une compréhension de base du Tilemap isométrique.

Comme nous travaillons dans un environnement 2D, vous devez configurer un nouveau projet 2D (reportez-vous à 2DAnd3DModeSettings)

Il y a 3 composants principaux nécessaires pour atteindre notre objectif: un plateau de jeu où le jeu aura lieu, un endroit où garder une description de niveau et un code qui se connecte l’un à l’autre.

Partie 1. Création du tableau

Commençons par importer les ressources d’image nécessaires. Je vais utiliser les mêmes images que j’ai utilisées pour mon jeu:

Used to create a base tilemap level

Used as a path

Used to mark the start and end points of a chemin

Nous garderons les images dans le dossier des tuiles, il suffit de les faire glisser et de les déposer là.

Configuration des tuiles

Remarque! Vous devriez avoir une taille correcte de « Pixel par unité » pour chaque image de tuile (pour plus de détails, voir Environnements 2D isométriques avec Tilemap). Pour ces images, la valeur est 1096.

Configuration de l’image

C’est une bonne pratique de séparer les différentes couches d’une scène en objets de jeu dédiés avec des noms descriptifs. Imaginez l’écran de jeu typique qui pourrait contenir une zone de jeu, une interface utilisateur, des placements publicitaires, etc.

Suivant cette approche, créons un nouvel objet de jeu vide nommé GameZone où le plateau et tous les éléments du jeu pourraient être placés.

Maintenant, il est temps de créer des tuiles réelles à partir des images que nous avons importées précédemment

Appuyez sur Window->2D ->Palette de tuiles

Palette de tuiles

La vue de la palette de tuiles sera ouverte. Cliquez sur « Créer une nouvelle palette » et créez une palette:

New Palette

Drag and drop tile images one by one to create an actual tiles.

Tiles setup

With those tiles we can finally create the game board which will be filled with elements programmatically on a level startup later.

Trouvez GameZone Dans la vue Hiérarchie, cliquez avec le bouton droit de la souris → Objet 2D → Carte de Tilemap isométrique. Un nouvel objet de jeu Grid sera créé.

La taille de la planche sera de 11×11 carreaux et nous pourrons commencer à peindre. Sélectionnez l’outil Pinceau Boîte (4ème élément à partir de la gauche) dans la vue « Palette de carreaux », puis sélectionnez la tuile « nettoyer ». Peignez-le dans la vue de la scène.

Conseil

Une fois que vous avez terminé la peinture, vous devez compresser manuellement les limites du tilemap. Pour cela, vous devez sélectionner Tilemap dans la vue Hiérarchie, appuyer sur un bouton paramètres (engrenage) du composant Tilemap et sélectionner « Compresser les limites de Tilemap »

Compresser les limites du Tilemap

Bien joué, le plateau de jeu est prêt à être utilisé! Nous y reviendrons dans la partie 3.

Partie 2. Données de niveau

Comme nous voulons réutiliser la même scène et le même plateau de jeu, les données de niveau doivent être conservées quelque part. Une solution simple pour cela est un simple fichier json qui décrira comment chaque niveau doit être construit.

Il est important de comprendre ce que nous essayons de réaliser, c’est pourquoi je dois dire quelques mots sur la mécanique. Dans le jeu, il y a des objets qui se déplacent le long du chemin du début à la fin (à peu près comme dans Zuma) et le but du joueur est de tous les détruire. Dans ce tutoriel, nous allons créer ce chemin qui sera unique pour chaque niveau.

D’accord, revenons au projet.

Il existe plusieurs façons d’accéder à une donnée externe dans un runtime à partir d’un script. Ici, nous utiliserons des dossiers de ressources

Créons un nouveau dossier — Fichiers — et un sous-dossier Ressources. C’est l’endroit où nous voulons conserver les données, alors créez un nouveau fichier — Niveaux.json et placez-le là.

Pour les besoins du tutoriel, nous n’aurons que deux champs pour décrire chaque niveau:

  • number – un int pour identifier un chemin de niveau
  • — le chemin basé sur des tuiles que nous voulons créer par programme. Tableau de valeurs où la première valeur est le point de départ et la dernière valeur est le point final.

C’est le fichier que je vais utiliser. Ne vous inquiétez pas de ces valeurs dans path, nous y reviendrons plus tard.

{
"levels":
},
{
"number": 2,
"path":
},
{
"number": 3,
"path":
}
]
}

Créons un autre dossier — Scripts — et maintenant le codage commence enfin.

Nous voulons accéder aux données du fichier dans le code, nous avons donc besoin d’une classe de modèle pour cela. Il est temps de créer notre tout premier script – LevelsData. Il n’est pas destiné à être instancié par Unity, donc les méthodes MonoBehaviour et StartUpdate doivent être supprimées. Dans le fichier ci-dessus, nous pouvons voir que l’élément racine est un array de niveaux où chaque niveau doit avoir un int numéro de champ et un int array chemin de champ. N’oubliez pas non plus de mettre l’annotation .


public class LevelsData
{
public LevelData levels;
public class LevelData
{
public int number;
public int path;
}
}

Bien, maintenant nous avons le fichier et le modèle. L’étape suivante consiste à se transformer l’un en l’autre. Créons un autre script – GameZone — et attachons-le à l’objet GameZone sur la scène. Ce script sera utilisé plus tard pour configurer l’ensemble du plateau de jeu.

Suivez le principe de responsabilité unique créons encore un autre script – LevelsDataLoader – qui fera toute la transformation. Attachez-le également à l’objet GameZone.

public class LevelsDataLoader : MonoBehaviour
{
private const string LevelsPath = "Levels";
public Dictionary<int, LevelsData.LevelData> ReadLevelsData()
{
var jsonFile = Resources.Load(LevelsPath, typeof(TextAsset)) as TextAsset;
if (jsonFile == null)
{
throw new ApplicationException("Levels file is not accessible");
}
var loadedData = JsonUtility.FromJson<LevelsData>(jsonFile.text);
return loadedData.levels.ToDictionary(level => level.number, level => level);
}
}

Cette classe chargera les données et les renverra sous forme de dictionnaire où la clé est le numéro de niveau et les données sont les données de niveau elles-mêmes.

Maintenant, nous devrions pouvoir accéder aux données dans le script GameZone.

public class GameZone : MonoBehaviour
{
private Dictionary<int, LevelsData.LevelData> _levelsData;
private LevelsDataLoader _dataLoader;private void Awake()
{
_dataLoader = GetComponent<LevelsDataLoader>();
}private void Start()
{
_levelsData = _dataLoader.ReadLevelsData();Debug.Log(_levelsData.Count + " levels have been stored in the dictionary!");
}
}

Revenez à Unity, appuyez sur le bouton de lecture et vérifiez la console — vous devriez voir le message « 3 niveaux ont été stockés dans le dictionnaire! »

Partie 3. Connexion de la Carte et des données

Félicitations, vous avez atteint la dernière et la plus intéressante partie de ce tutoriel. Comment allons-nous réellement connecter la carte et les données? Continuez à lire pour le découvrir!

Tout d’abord, plaçons les tuiles horizontal et start_stop créées dans la première partie du tutoriel dans le dossier Ressources sous le dossier Tuiles. Ajoutez ensuite un nouveau script — TilesResourcesLoader — – une classe d’assistance statique pour charger des tuiles à partir de ce dossier dans un runtime.

public static class TilesResourcesLoader
{
private const string PathHorizontal = "horizontal";
private const string StartStop = "start_stop"; public static Tile GetPathHorizontalTile()
{
return GetTileByName(PathHorizontal);
} public static Tile GetStartStopTile()
{
return GetTileByName(StartStop);
} private static Tile GetTileByName(string name)
{
return (Tile) Resources.Load(name, typeof(Tile));
}
}

Comme dernière étape, nous devrions placer ces tuiles sur le plateau au démarrage de la scène. Revenons au script GameZone. Tout d’abord, nous devons simuler la sélection du niveau, dans le jeu réel, cela se produit généralement chaque fois qu’un utilisateur appuie sur un bouton de niveau. Par souci de simplicité, ajoutons un niveau de champ public à GameZone et changeons sa valeur en 1 pour démarrer. Je vais d’abord vous montrer le script final:

public class GameZone : MonoBehaviour
{
public int Level; private const int FieldLineSize = 11;
private const int FieldTotalTiles = FieldLineSize * FieldLineSize;
private Dictionary<int, LevelsData.LevelData> _levelsData; private void Start()
{
_levelsData = GetComponent<LevelsDataLoader>().ReadLevelsData();
SetupTiles();
} private void SetupTiles()
{
var baseLevel = GetComponentsInChildren<Tilemap>(); var localTilesPositions = new List<Vector3Int>(FieldTotalTiles);
foreach (var pos in baseLevel.cellBounds.allPositionsWithin)
{
Vector3Int localPlace = new Vector3Int(pos.x, pos.y, pos.z);
localTilesPositions.Add(localPlace);
} SetupPath(localTilesPositions, baseLevel);
} private void SetupPath(List<Vector3Int> localTilesPositions, Tilemap baseLevel)
{
var path = _levelsData.path;
var pathHorizontalTile = TilesResourcesLoader.GetPathHorizontalTile();
var first = path.First();
var last = path.Last();
foreach (var localPosition in localTilesPositions.GetRange(first, Math.Abs(first - last)))
{
baseLevel.SetTile(localPosition, pathHorizontalTile);
} var startStopTile = TilesResourcesLoader.GetStartStopTile();
baseLevel.SetTile(localTilesPositions, startStopTile);
baseLevel.SetTile(localTilesPositions, startStopTile);
}
}

Wow, c’est beaucoup d’action! Laisse-moi te guider.

Dans la méthode SetupTiles, nous devons d’abord obtenir le tilemap lui-même car nous avons besoin de connaître les positions des tuiles pour le changer. Pour ce faire, nous utilisons la méthode tilemap.cellBounds.allPositionsWithin qui renvoie toutes les positions des tuiles à partir de la toute première — dans la configuration donnée, il s’agit d’une tuile la plus basse.

Reportez-vous à l’image suivante où chaque nombre représente l’index dans la liste localTilesPositions.

Carte de tilemap numérotée

Vous souvenez-vous des valeurs que nous utilisons dans le chemin dans Levels.json? Comme vous l’avez peut-être déjà deviné, ces valeurs sont les index des tuiles du tableau. Tout ce que vous devez faire maintenant est d’avoir une image de feuille de triche pour vous aider à construire des niveaux. C’est celui que j’ai utilisé pendant le développement:

Mon laid numéroté tilemap

Dans cet exemple, nous définissons une ligne horizontale dans le chemin, veuillez vous référer à la méthode SetupPath. La partie clé est la boucle suivante:

foreach (var localPosition in localTilesPositions.GetRange(first, Math.Abs(first - last)))
{
baseLevel.SetTile(localPosition, pathHorizontalTile);
}

Ici, nous parcourons localTilesPositions pour trouver ceux pour définir la tuile souhaitée — horizontale dans ce cas.

Remarque! la méthode GetRange a deux paramètres – index et count.

Afin de marquer les positions de début et de fin du chemin, la tuile start_stop est utilisée.

Voici le résultat de notre travail acharné:

Résultat final

Essayez maintenant de changer le champ de numéro de niveau du script GameZone de 1 à 2 ou 3 et vous verrez que le chemin est chargé correctement pour le niveau donné.

Après