Articles

Unity: programmatisch maken van 2D Tilemap

Voor video tutorial Klik hier

Als u verschillende Tilemaps van verschillende groottes moet maken, bekijk dan het tweede deel van deze tutorial

Tilemap component werd geà ntroduceerd in Unity 2017.2 en heeft het 2D spelontwikkelingsproces aanzienlijk vergemakkelijkt. Met de versie 2018.3 werd isometrische Tilemap geïntroduceerd die grote ondersteuning biedt voor 2.5 D-games. Onlangs kreeg ik de kans om met dit onderdeel nauw samen te werken en werd uitgedaagd met de taak om programmatisch tegels te maken. In mijn laatste spel heb ik veel levels en ze hebben allemaal hetzelfde Tilemap-gebaseerde bord. Maar het bord zelf heeft niveau-unieke setup. Uiteraard, als een professionele ontwikkelaar wilde ik niet om 60 game scènes te maken en schilderen alle niveaus met de hand, maar eerder een mechanisme om het bord te vullen met de juiste elementen, afhankelijk van de gegeven input. Als je nieuwsgierig bent hoe het eindresultaat eruit ziet hier is link naar het spel.

broncode is beschikbaar op GitHub, vind de link aan het einde van deze tutorial.

Ik gebruik de laatst beschikbare Unity 2019.2.0.1 f. Om te werken met deze tutorial moet u ten minste versie 2018.3. We zullen isometrisch raster gebruiken, maar de beschreven techniek is van toepassing op elk type.

voordat u begint, raad ik ten zeerste aan om deze briljante isometrische 2D-omgevingen te lezen met Tilemap-blog om een basiskennis van isometrische Tilemap te krijgen.

omdat we in een 2D-omgeving werken, moet u een nieuw 2D-project opzetten (refereer naar 2DAnd3DModeSettings)

Er zijn 3 hoofdcomponenten nodig om ons doel te bereiken: een spelbord waar het spel zal plaatsvinden, een plaats waar een niveau beschrijving en een aantal code die de ene verbindt met de andere te houden.

deel 1. Het Board

aanmaken laten we beginnen met het importeren van de benodigde image assets. Ik zal dezelfde beelden gebruiken die ik voor mijn spel gebruikte:

Used to create a base tilemap level

Used as a path

Used to mark the start and end points of a pad

We houden afbeeldingen in de map Tiles, slepen ze daar gewoon neer.

Tiles setup

note! U moet een juiste “Pixel Per eenheid” grootte ingesteld per elke tegel afbeelding (voor meer details zie isometrische 2D-omgevingen met Tilemap). Voor deze afbeeldingen is de waarde 1096.

Image setup

het is een goede gewoonte om verschillende lagen op een scène te scheiden in specifieke spelobjecten met beschrijvende namen. Stel je het typische spelscherm voor dat een Gamezone, gebruikersinterface, advertentieplaatsingen en ga zo maar door kan bevatten.

volgens deze aanpak maken we een nieuw leeg spelobject genaamd GameZone waar het bord en alle spelelementen kunnen worden geplaatst.

Nu is het tijd voor het maken van de werkelijke tegels van de beelden die we eerder ingevoerde

Druk op Window -> 2D> Tegel Palet

Tegel Palet

de Tegel Palet weergeven wordt geopend. Klik op” Create new Palette ” (nieuw palet aanmaken) en maak een palet aan:

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.

zoek GameZone in Hiërarchieweergave, klik met de rechtermuisknop op → 2D Object → isometrische Tilemap. Een nieuw spelobject Grid zal worden aangemaakt.

De grootte van het bord zal 11×11 tegels zijn en we kunnen beginnen met schilderen. Selecteer het kader penseel Gereedschap (4e element van links) in de” tegel palet “weergave, selecteer vervolgens” schoon ” tegel. Schilder het in de Scèneweergave.

Board

nadat u klaar bent met het schilderen moet u handmatig TileMap bounds comprimeren. Hiervoor moet u Tilemap selecteren in de Hiërarchieweergave, op een knop Instellingen (tandwiel) op de TileMap-component drukken en “Tilemap Bounds comprimeren”

comprimeer Tilemap bounds

goed gedaan, het spelbord is klaar om te worden gebruikt! We komen er in deel 3 op terug.

deel 2. Level data

omdat we dezelfde scene en hetzelfde spelbord willen hergebruiken, moeten de level data ergens bewaard worden. Een eenvoudige oplossing voor dat is een eenvoudig json-bestand dat zal beschrijven hoe elk niveau moet worden gebouwd.

Het is belangrijk om te begrijpen wat we proberen te bereiken, daarom moet ik een paar woorden zeggen over de mechanica. In het spel zijn er objecten die langs het pad van het begin tot het einde bewegen (vrijwel zoals in Zuma) en het doel van de speler is om ze allemaal te vernietigen. In deze tutorial zullen we dit pad maken dat uniek is voor elk niveau.

OK, terug naar het project.

Er zijn meerdere manieren om toegang te krijgen tot externe gegevens in een runtime vanuit een script. Hier gebruiken we Resources mappen

laten we een nieuwe map — bestanden — en een submap Resources maken. Dat is de plaats waar we de gegevens willen bewaren, dus maak een nieuw bestand-Levels.JSON en plaats het daar.

voor de tutorial doeleinden hebben we slechts twee velden om elk niveau te beschrijven:

  • getal — een int om een niveau
  • pad te identificeren-het op tegels gebaseerde pad dat we programmatisch willen maken. Array van waarden waarbij de eerste waarde het beginpunt is en de laatste waarde het eindpunt.

Dit is het bestand dat ik zal gebruiken. Maak je geen zorgen over die waarden in path, we komen er later op terug.

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

laten we een andere map maken — Scripts — en nu begint de codering eindelijk.

we willen toegang krijgen tot de gegevens uit het bestand in de code, daarom hebben we er een modelklasse voor nodig. Het is tijd om ons allereerste script aan te maken — LevelsData. Het is niet bedoeld om te worden geïnstitutionaliseerd door Unity, dus MonoBehaviour en StartUpdate methoden moeten worden verwijderd. Uit het bovenstaande bestand kunnen we zien dat het root element een array van niveaus is waarbij elk niveau één int veldnummer en één int array veldpad zou moeten hebben. Vergeet ook niet om annotatie te plaatsen.


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

Nice, nu hebben we het bestand en het model. De volgende stap is om de ene in de andere te transformeren. Laten we een ander script maken — GameZone — en het koppelen aan het GameZone object op de scène. Dit script zal later gebruikt worden om het hele spelbord op te zetten.

volg Single responsibility principe Laten we nog een ander script maken — LevelsDataLoader — dat alle transformatie zal doen. Voeg het ook toe aan het GameZone object.

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);
}
}

Deze klasse zal de gegevens laden en retourneren als een woordenboek waar de sleutel het niveau nummer is en de gegevens de niveau gegevens zelf.

nu zouden we toegang moeten hebben tot de gegevens in GameZone script.

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!");
}
}

schakel terug naar Unity, druk op de afspeelknop en controleer de console — u zou het bericht ” 3 niveaus zijn opgeslagen in het woordenboek!”

Part 3. Het verbinden van het Forum en de Data

Gefeliciteerd, je hebt het laatste en meest interessante deel van deze tutorial bereikt. Hoe verbinden we het bord met de data? Blijf lezen om het uit te vinden!

allereerst plaatsen we horizontal en start_stop tiles aangemaakt in het eerste deel van de tutorial in de map hulpbronnen onder de map tegels. Voeg dan een nieuw script toe – TilesResourcesLoader — een statische helperklasse om tegels uit die map in een runtime te laden.

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));
}
}

als laatste stap zouden we deze tiles op het bord moeten plaatsen bij het opstarten van de scene. Laten we teruggaan naar het GameZone script. Allereerst moeten we de niveauselectie simuleren, in het echte spel gebeurt het meestal wanneer een gebruiker op een niveauknop drukt. Laten we omwille van de eenvoud een publiek veldniveau toevoegen aan GameZone en deze waarde wijzigen in 1 Voor start. Ik zal je eerst het laatste script laten zien:

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, dat is veel actie! Laat me je er doorheen leiden.

in de SetupTiles methode zouden we eerst de tilemap zelf moeten krijgen omdat we de posities van tiles moeten weten om het te veranderen. Om dit te bereiken gebruiken we de tilemap.cellBounds.allPositionsWithin methode die alle posities van tiles retourneert vanaf de allereerste — in de gegeven configuratie is het een down-most tile.

refereer naar de volgende afbeelding waar elk getal de index vertegenwoordigt in de localTilesPositions lijst.

genummerde tilemap

herinnert u zich de waarden die we gebruiken in het pad inLevels.json? Zoals je misschien al geraden hebt zijn deze waarden de indexen van de tiles in de array. Alles wat je nu hoeft te doen is om een cheatsheet afbeelding om u te helpen de bouw van niveaus. Die heb ik gebruikt tijdens de ontwikkeling.:

Mijn lelijke genummerde tilemap

In dit voorbeeld maken we een horizontale lijn in het pad, raadpleeg dan de SetupPath methode. Het belangrijkste deel is de volgende lus:

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

Hier herhalen we localTilesPositions om degenen te vinden die de gewenste tile — horizontaal in dit geval instellen.

opmerking! GetRange methode heeft twee parameters — index en aantal.

om de begin-en eindposities van het pad te markeren wordt de tegel start_stop gebruikt.

Hier is het resultaat van ons harde werk:

eindresultaat

Nu proberen om het niveau te veranderen veld nummer van de GameZone script van 1 naar 2 of 3 en je zal zien dat het pad correct is geladen voor het gegeven niveau.

daarna