Overview
This guide describes how to create custom graph-generators that can be used in the random level generator.
Graph Generators
Patterna’s random levels are generated in a two-step process: First, a graph is generated, then this graph is passed to a level generator that fills the graph with information that makes a sovlable level.
The first part of this process can be customized using a custom GraphGenerator, the second by using a custom LevelGenerator. Here, we are dealing with the first step only (though there are a few hints at the end on how to generate actual levels). The GraphGenerator only determines
- the number of nodes,
- the position of the nodes,
- the edges between the nodes.
It can (in theory) also set all the other things (like node information, visibility, state etc.) but the default level generator overrides this information.
(Technically, there is a third step between the graph generator and the level generator in which a variation on the base graph is computed if the user asked for a variation on the graph. For most intents and purposes, this can be ignored.)
Setting up your project
The generators for the game are written in C# for use in Unity. This guide describes how to setup graph generators in Visual Studio. There are two parts of a generator: First, a C# assembly that defines the code for the graph generator. Second, there is a descriptor file that tells the game how to load the generator. We will take care of the assembly first.
Here are the steps needed to setup your Visual Studio project:
- Create a new class library project
- Find the files Assembly-CSharp.dll and UnityEngine.dll in the folder patterna_data/Managed/ in your Patterna installation directory and add them as references to your project.
- Go to the project settings and ensure that your project is set to the .Net Framework 3.5 (or lower) profile.
Now we are ready to write the actual code. We need to subclass the GraphGenerator class from Assembly-CSharp. There is only one method on that class that has to be implemented:
The method’s job is to generate a GameBoard and set that object’s Graph property. Additionally, there’s a method
that is called when the generator has been initialized. The string passed to the method is simply the path to the directory of the graph generator’s descriptor’s directory, so you can load custom data, if needed.
As an example, here is the code for a graph generator that generates a grid with random edges: Visit GitHub Gist[gist.github.com].
As a side node, you may notice that at no point at this time was the seed of the level exposed. The seed is used to initialize the random generator in the static RandomLevelHelper class; as long as you are only using methods from there to drive randomness, you will see consistent results when repeatedly using the same seed.
Creating the descriptor files
In the next step, we need to tell Patterna how to load your generator. In the Patterna directory, there is a subfolder patterna_data/Game/Modes. This folder contains a subfolder for each of the random generator modes in the game. Go ahead and create a new folder in there with a name of your choice. In that folder, you will need to put three things:
- a subfolder called Localization (see below)
- a file Mode.json (see below)
- your compiled assembly (e.g., PatternaLevelGen.dll)
First, the Localization folder contains a mapping of ids to strings for each available language. Take a look at the other modes; it is pretty much self-explanatory.
The meat of the descriptor data is the Mode.json file. Here is what this file would need to include for the example graph generator created above:
What is happening here? First, the outer JSON object describes a GameModeDescriptor object from the game. This consists of a field Author, where you can put your name as it should be shown in the game. Then there are fields Name and Description; these are ids that link to localizable strings from the Localization folder. Next, there are MinSizeHint and MaxSizeHint, these determine the values passed to your GenerateGraph method. The size selected by the user in the setup screen translates to this range.
The LevelGenerator field describes which algorithm to use to transform the graph generated by the GraphGenerator to an actual level, along with a bunch of parameters for that algorithm.
Finally, there is the GraphGenerator field. Note that the $type field on that is set to the path of our class in the PatternaLevelGen assembly, which is what we called the assembly of our graph generator. We can also set the public EdgeProbability field of our graph generator here.
That’s it! Your generator will now be loaded by the game.
A word on level generators
As you have seen above, the descriptor file tells the game to use the default level generator on top of the graph generator. This prevents us from generating more than the shape of the graph.
This can be remedied by implementing a custom level generator.
There are two methods of interest:
The second method is called when the user asks to abort level generation. It will generally be called from a different thread than the thread running the GenerateLevel method. If your level generator does not do any expensive operations, there is no need to do anything in the Abort method.
The GenerateLevel method on the other hand is where things are actually happening. It is expected that when this method finishes execution, the Board property of the argument to the method contains a valid level. In fact, you could use a graph generator that merely creates a board for a given size without any actual graph structure and do all that in the level generator. The argument passed to the method also contains all the settings set by the user.
One important piece of information to keep in mind when implementing level generators is that the level generator’s Clone method is used to create a separate copy of the level generator for each level that is generated. The default implementation of that method should cover most use cases, but in some cases you may want to manually do the cloning.