Synth Spelunkers – Mapocalpyse

Creating A Map Import Tool

Now, if you’re already familiar with Synth Spelunkers, you’ll know that it’s a rhythm-puzzle game where each level takes place on a grid, which is made up of a series of tiles. Each of these tiles may have various types of effects when a Spelunker enters them, or may activate these effects every so many beats of the song. If you’re not already familiar with Synth Spelunkers, hopefully you get at least some of the idea now.

The Old System

Until recently, we had a simple system for making maps implemented that made use of the Tiled Map Editor. These maps were created as a grid that simply specified what type of tile was in what position. This was then output as a simple CSV file (Comma Separated Values), and that file could be used to generate a map in game.

However, none of the attributes for the tiles in the map were set. All of this had to be done manually, which meant that if any changes to the layout of the map had to be changed, the entire thing would have to be re-imported and setup would begin from scratch again. As you’d expect, this was a colossal time-waster. This new system however, resolves that issue.

The New System

The new map import system is one of my own creation. It still makes use of the Tiled Map Editor, however it now makes use of other function that the editor can provide. Namely, it is now exported in .JSON format. This format allows a significantly larger amount of data to be exported with along with the map layout. Using this, we’re able to create a consistent record of all the links and parameters of a level, that will then be automatically set up when importing it.

So how does it all work? Well, it relies on Tiled’s ability to create layers. The entire level is split into two layers. The Tile layer, and the Object layer. The Tile layer handles what tiles are created where, whilst the Object layer is used to determine which tiles do what.

When creating an object in Tiled, you’re able to assign any number of custom parameters to it. Using this, we can specify exactly what aspects of the tile we want to edit from the default values. So these are things like bools that determine if a Spike Trap starts active, or alternates on beats, and ints that determine when spikes change states, or the location of the tile a switch or button will activate.

Regardless of what parameters we assign to an individual tile, it will always be output the same way in .JSON. And that is part of the power of a .JSON system. It’s exceptionally flexible.

On the Unity end, first we have to have a class for all of the .JSON data to be stored in. With the way that Tiled outputs its .JSON data, this means that I had to create a bit of a Matryoshka doll of classes to support the structure.

//Classes are set up like this to support the JSON output format of the Tiled editor.
[System.Serializable]
public class Properties
{
 //Tiled doesn't support lists or vectors, so co-ords must be individual ints to minimise parsing.
 public int locationX = -1;
 public int locationY = -1;
 public int activates0x = -1;
 public int activates0y = -1;
 public int pairedSwitch0x = -1;
 public int pairedSwitch0y = -1;
 public bool startsActive = false;
 public bool alternate = false;
 public int beatsOn = -1;
 public int beatsOff = -1;
 public int maxSpawns = -1;
 public int spawnDirection = -1;
 public int exitsNeeded = -1;

 public Properties() 
 {

 }
}

[System.Serializable]
public class Objects
{
 public int gid = -1;
 public Properties properties = new Properties();

 public Objects() 
 {

 }
}

[System.Serializable]
public class Layers
{
 public List<int> data = new List<int>();
 public List<Objects> objects = new List<Objects>();

 public Layers() 
 {

 }
}

[System.Serializable]
public class MapInputClass
{
 public int height = -1;
 public List<Layers> layers = new List<Layers>();
 public int width = -1;

 public MapInputClass(int ht, List<Layers> lys, int wt) 
 {
 height = ht;
 layers = lys;
 width = wt;
 }

 public MapInputClass()
 {

 }
}

Now, looking at this you may think “But wait, does EVERY tile have to specify things like maxSpawns even if they’re not a spawn tile?” And therein is part of the beauty of .JSON. As I mentioned earlier, it’s handled in a way that is intelligent enough for it to be exceptionally flexible. If a tile doesn’t have a parameter, it’s simply treated as being the default value. This way, every single piece of tile data can be encapsulated within a single (albeit over-sized) stack of classes.

Now, to actually use the data, I made use of the previous custom editor created by Corey, one of the HDG programmers, to find all files with the .JSON extension and get the data from the correct one when importing the map. This .JSON string is then passed into the class I mentioned above like this:

 //Import the map from a json string.
 theMap = JsonUtility.FromJson<MapInputClass>(jsonText);

Beautifully simple, no?

And now I have a list of every tile object and every parameter I need. First, I access the Data list from Layer[0] (the tile layer), and feed that into a loop for instantiating the tiles in the world. This creates the physical map. (This also made use of  mostly pre-existing code from the previous system.)

After that,  I just need to step through the list of objects and assign the values accordingly. To do this, I need to make sure I’m looking at the list of objects on the object layer (Layers[1]), as there are none on the tile layer.

//Get all of the tile objects.
 List<Objects> tileList = theMap.layers[1].objects;&nbsp;

Then, for each one I look at its GID (graphic ID). I do this because tiled uses tiles sets to determine what tile is in what position. When an object is created from a tile image, this GID informs you which tile that was. Based on this result, I run the appropriate function for assigning these custom values.

//Get hit point in local space.
for (int i = 0; i < tileList.Count; i++)
{
switch(tileList[i].gid)
{
case 3: //Spawn
SetUpSpawn(tileList[i].properties, map);
break;
case 5: //Permanent Switch
SetUpPermaSwitch(tileList[i].properties, map);
break;
case 6: //Spike
SetUpSpike(tileList[i].properties, map);
break;
case 7: //Exit
SetUpExit(tileList[i].properties, map);
break;
case 8: //Temporary Switch
SetUpTempSwitch(tileList[i].properties, map);
break;
}
}

And here is where I account for the “But spikes don’t use maxSpawns issue”. By which I mean when dealing with spikes, I simple don’t access or try to assign maxSpawns to anything, because, well, even if it’s not the default value (-1), I’ve got no where to put it. I also made sure to include warnings in case things that shouldn’t be default are default.

//Setup a given Spike Tile
void SetUpSpike(Properties props, MapExpanded map)
{
SpikeTile tile = null;

//Make sure location was entered correctly. Get the tile at that location.
if (props.locationX > -1 && props.locationY > -1)
{
tile = map.GetTile(props.locationX, props.locationY).GetComponent<SpikeTile>();
}
else
{
Debug.LogWarning("Spike Tile Location Not Set!");
}

//Set up alternation
if(props.alternate)
{
if(props.beatsOff > -1 && props.beatsOff > -1)
{
tile.alternate = true;
tile.offForBeats = props.beatsOff;
tile.onForBeats = props.beatsOn;
}
else
{
Debug.LogWarning("Alternation Times Not Set!");
}
}

if (props.startsActive)
{
tile.isActive = true;
}
}

Now, there are a few irritating flaws with this system. A lot of them stem from the fact that Tiled doesn’t support any sort of vectors or lists. So to reduce the amount of manual parsing I have to do, locations for any tile is divided into tileX and tileY parameters.

This also means that if you want to have a switch activate multiple other tiles, you’ll need an activates1x, activates1y, activates2x, activates2y etc. in both the tile parameters and the class for every one you want (not to mention then the logic for individually implementing them). This is without doubt the biggest drawback for this system.

The way that Tiled handles objects also means that the locations for objects need to be manually set for setup to work. By which I mean the object has so give the locationX, and locationY of the tile that object’s parameters should be applied to.

Overall though, this new system does mean that making a single change to the map does not require the entire map to be re-linked from the ground up. Thus, this should save us a tremendous amount of time as we push this game towards release.

Advertisements

One comment

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

w

Connecting to %s