Articles

Unity: hur man skapar 2D Tilemap programmatiskt

för video tutorial klicka här

om du behöver skapa olika Tilemaps av olika storlekar kolla in den andra delen av denna handledning

TileMap komponent infördes i Unity 2017.2 och avsevärt lättade 2D spelutvecklingsprocessen. Med versionen 2018.3 isometrisk Tilemap infördes ger stort stöd för 2.5 D spel. Nyligen hade jag en möjlighet att arbeta med denna komponent nära och utmanades med uppgiften att skapa plattor programmatiskt. I mitt sista spel har jag många nivåer och alla har samma Tilemap-baserade styrelse. Men styrelsen själv har nivå-unik inställning. Självklart, som en professionell utvecklare ville jag inte skapa 60 spelscener och måla alla nivåer för hand utan snarare ha en mekanism för att fylla brädet med rätt element beroende på den givna ingången. Om du är nyfiken på hur slutresultatet ser ut här är länk till spelet.

källkod finns på GitHub, vänligen hitta länken i slutet av denna handledning.

Jag använder den senaste tillgängliga Unity 2019.2.0.1 f. För att kunna arbeta med denna handledning bör du ha minst version 2018.3. Vi kommer att använda isometriskt rutnät men beskriven teknik är tillämplig på vilken typ som helst.

innan jag börjar rekommenderar jag starkt att läsa igenom denna lysande isometriska 2D-miljöer med Tilemap blogginlägg för att få en grundläggande förståelse för isometrisk Tilemap.

När vi arbetar i en 2D-miljö bör du skapa ett nytt 2d-projekt (se 2DAnd3DModeSettings)

det finns 3 huvudkomponenter som krävs för att uppnå vårt mål: en spelplan där spelet kommer att äga rum, en plats där man kan hålla en nivåbeskrivning och en viss kod som ansluter en till en annan.

Del 1. Skapa tavlan

låt oss börja med att importera nödvändiga bildtillgångar. Jag använder samma bilder som jag använde för mitt spel:

Used to create a base tilemap level

Used as a path

Used to mark the start and end points of a path

Vi kommer att hålla bilder i mappen kakel, helt enkelt dra och släppa dem där.

kakel setup

Obs! Du bör ha en korrekt ”Pixel Per enhet” – storlek inställd för varje kakelbild (för ytterligare detaljer se isometriska 2D-miljöer med Tilemap). För dessa bilder är värdet 1096.

Bildinställning

det är en bra praxis att separera olika lager på en scen i dedikerade spelobjekt med beskrivande namn. Föreställ dig den typiska spelskärmen som kan innehålla en spelzon, användargränssnitt, annonsplaceringar och så vidare.

Efter detta tillvägagångssätt skapar vi ett nytt tomt spelobjekt med namnet GameZone där brädet och alla spelelement kan placeras.

Nu är det dags att skapa faktiska brickor från bilderna vi importerade tidigare

Tryck på fönster -> 2D -> Kakelpalett

kakelpalett

kakelpalettvy öppnas. Klicka på ”Skapa ny palett” och skapa en palett:

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.

hitta GameZone I Hierarkiv, högerklicka på 2D-objekt i hierarkin. Ett nytt spelobjektGrid kommer att skapas.

styrelsens storlek kommer att vara 11×11 plattor och vi kan börja måla. Välj rutan borstverktyg (4: e elementet från vänster) i vyn ”Kakelpalett” och välj sedan ”rengör” kakel. Måla det i Scenvyn.

styrelse

När du är klar med målningen bör du manuellt komprimera TileMap bounds. För detta måste du välja Tilemap i Hierarkivyn, tryck på en inställningsknapp (gear) på Tilemap-komponenten och välj ”Komprimera Tilemap Bounds”

komprimera Tilemap bounds

bra gjort, spelbrädet är klart att användas! Vi återkommer till detta i del 3.

del 2. Nivådata

eftersom vi vill återanvända samma scen och samma spelbräda bör nivådata hållas någonstans. En enkel lösning för det är en enkel json-fil som beskriver hur varje nivå ska byggas.

det är viktigt att förstå vad vi försöker uppnå, det är därför jag måste säga några ord om mekaniken. I spelet finns föremål som rör sig längs vägen från början till slutet (ungefär som i Zuma) och spelarens mål är att förstöra dem alla. I denna handledning skapar vi den här vägen som kommer att vara unik för varje nivå.

okej, tillbaka till projektet.

det finns flera sätt att komma åt en extern data i en runtime från ett skript. Här kommer vi att använda resurser mappar

låt oss skapa en ny mapp-filer-och en undermapp resurser. Det är den plats vi vill behålla data, så skapa en ny fil-nivåer.json och placera den där.

för handledningsändamål har vi bara två fält för att beskriva varje nivå:

  • nummer — en int för att identifiera en nivå
  • sökväg — den kakelbaserade sökvägen som vi vill skapa programmatiskt. Array av värden där första värdet är startpunkten och det sista värdet är slutpunkten.

det här är filen Jag ska använda. Oroa dig inte för dessa värden i path, vi kommer till det senare.

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

låt oss skapa en annan mapp — skript — och nu börjar kodningen äntligen.

Vi vill komma åt data från filen i koden, så vi behöver en modellklass för den. Det är dags att skapa vårt allra första skript – LevelsData. Det är inte tänkt att instansieras av Unity, såMonoBehaviour ochStartUpdate metoder bör tas bort. Från filen ovan kan vi se att rotelementet är ett array av nivåer där varje nivå ska ha en int fältnummer och en int array fältväg. Glöm inte att sätta annotation.


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

trevligt, nu har vi filen och modellen. Nästa steg är att förvandla en till en annan. Låt oss skapa ett annat skript – GameZone — och bifoga det till objektet GameZone på scenen. Detta skript kommer att användas senare för att ställa in hela spelplanen.

följ principen om enskilt ansvar låt oss skapa ännu ett skript — LevelsDataLoader — som kommer att göra all omvandling. Bifoga det till objektet GameZone också.

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

den här klassen laddar data och returnerar den som en ordbok där nyckeln är nivånumret och data är själva nivådata.

Nu ska vi kunna komma åt data iGameZone 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!");
}
}

byt tillbaka till Unity, tryck på play — knappen och kontrollera konsolen-du bör se meddelandet ” 3 nivåer har lagrats i ordboken!”

del 3. Ansluta styrelsen och Data

Grattis, du har nått den sista och mest intressanta delen av denna handledning. Hur kommer vi faktiskt att ansluta styrelsen och uppgifterna? Fortsätt läsa för att ta reda på det!

Låt oss först och främst placera horizontal och start_stop plattor skapade i den första delen av handledningen i mappen resurser under mappen plattor. Lägg sedan till ett nytt skript — TilesResourcesLoader — en statisk hjälparklass för att ladda brickor från den mappen under en körning.

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

som det sista steget bör vi placera dessa plattor på brädet vid scenstart. Låt oss gå tillbaka till skriptet GameZone. Först och främst måste vi simulera nivåvalet, i det riktiga spelet händer det vanligtvis när en användare trycker på en nivåknapp. För enkelhetens skull låt oss lägga till en offentlig fältnivå till GameZone och ändra det värde till 1 för start. Jag kommer att visa dig det slutliga skriptet först:

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, det är mycket action! Låt mig gå igenom det.

iSetupTiles – metoden bör vi först få tilemap själv eftersom vi behöver veta positionerna för plattor för att ändra den. För att uppnå detta använder vi tilemap.cellBounds.allPositionsWithin — metoden som returnerar alla positioner för plattor från och med den allra första-i den givna konfigurationen är det en nere kakel.

se följande bild där varje nummer representerar indexet i listan localTilesPositions.

numrerad tilemap

kommer du ihåg de värden vi använder i sökvägen i Levels.json? Som du kanske har gissat redan är dessa värden indexerna för plattorna i matrisen. Allt du behöver göra nu är att ha en cheatsheet-bild som hjälper dig att bygga nivåer. Det är den jag har använt under utvecklingen:

min fula numrerade TileMap

i det här exemplet ställer vi in en horisontell linje i sökvägen, se metoden SetupPath. Nyckeldelen är följande slinga:

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

här itererar vi över localTilesPositions för att hitta de som ska ställa in önskad kakel — horisontell i det här fallet.

Obs! GetRange metoden har två parametrar — index och räkning.

för att markera banans start-och slutpositioner användsstart_stop – kakel.

här är resultatet av vårt hårda arbete:

slutresultat

försök nu ändra nivånummerfältet för skriptet GameZone från 1 till 2 eller 3 så ser du att sökvägen är korrekt laddad för den angivna nivån.

efteråt