The ByChance Framework allows game developers to provide an infinite amount of unique levels for both 2D and 3D games. Easy to integrate into video games of all genres, ByChance enables you to generate complex levels including all of their game components and comes with useful post-processing algorithms that can be applied afterwards in order to ensure a great gaming experience.
The whole framework has been created by Nick Prühs and Denis Vaz Alves for their Master Thesis.
- Getting Started
- Generating Your First Level
- Customizing the Chunk Library
- Configuring the Level Generator
- Adapting the Level Generation Process
- Level Generation Events
- Logging with the ByChance Framework
- Extending ByChance
- Best Practice
- Next Steps
- Unity Integration
The core of the ByChance Framework is a generic level generation algorithm that is able to construct 2D and 3D levels alike. Thus, you only need to understand the framework fundamentals once, and will then be able to create levels for games of all genres.
ByChance is a .NET Framework class library written in C# that follows the .NET patterns and idioms presented in the book Framework Design Guidelines by Cwalina, Abrams et al. This page will explain the basics of integrating the ByChance Framework for creating random levels in your games. All code samples are written in C#.
The easiest way of integrating ByChance in your project is grabbing the latest binaries from GitHub. Just download the library and reference it in your project.
ByChance is open source under the MIT license. Feel free to get the latest sources from GitHub and add all files to your project.
ByChance thinks of a game level as a bounded space consisting of a limited number of level building blocks called chunks. Each chunk contains information about its extents, its position and rotation as well as about where to align it to the existing level and where to add game elements like enemies, items or levers.
The only thing you need to do before generating your first level is to setup a chunk library. This chunk library holds a set of chunk templates that is used by the level generator for generating the game levels. For 2D levels this could look like this:
ChunkLibrary2D chunkLibrary = new ChunkLibrary2D();
After having instantiated the chunk library, you need to add chunk templates to be used by the level generator. For 2D levels, you have to specify at least the width and height of these templates:
ChunkTemplate2D chunkTemplate = new ChunkTemplate2D(new Vector2F(30f, 50f));
chunkLibrary.AddChunkTemplate(chunkTemplate);
Next, the level generator needs to know how chunks constructed with this template can be put together. ByChance uses to concept of contexts for specifying where chunks can be aligned. Every chunk has to contain at least one single context describing the relative position at which it may be aligned to other chunks:
chunkTemplate.AddContext(new Vector2F(15f, 0f));
chunkTemplate.AddContext(new Vector2F(15f, 50f));
Now, our chunk template has two contexts, one at the top-center and one at the bottom-center. Note that while the framework will work with a chunk library that contains only one chunk template, the resulting level is sure to be dull. The more choices the framework has in regard to chunk templates, the more interesting the final result will be.
As soon as the chunk library has been properly set up, it can be passed to the level generator, along with the desired level size:
LevelGenerator2D levelGenerator = new LevelGenerator2D();
Level2D level = levelGenerator.GenerateLevel(chunkLibrary, new Vector2F(800f, 600f));
After this, you can access the content of the resulting level by using the accessor methods of the Level class:
foreach (Chunk2D chunk in level)
{
// Do something with the chunk.
}
Every chunk has a relative weight that tells the level generator how often a specific chunk should be added to the level. A chunk with a weight of 3 is added to the level about three times as often as a chunk with a weight of 1. The easiest way to specify these weights is by passing them to the constructor for chunk templates:
ChunkTemplate2D chunkTemplate = new ChunkTemplate2D(new Vector2F(30f, 50f), 3);
Additionally, chunks and contexts can be tagged in order to enable to definition of domain-specific rules, such as whether two given contexts can be aligned at all. This allows attaching treasure rooms to boss rooms, for example, and will be discussed in detail later.
ChunkTemplate2D chunkTemplate = new ChunkTemplate2D(new Vector2F(30f, 50f), "boss");
Every chunk may contain one or more tagged anchors describing the relative position at which game elements can be added:
chunkTemplate.AddAnchor(new Vector2F(10f, 10f), "treasure");
chunkTemplate.AddAnchor(new Vector2F(20f, 20f), "stairs");
Apart from assigning contexts and anchors to chunk templates, the ByChance Framework also offers the option to allow the rotation of chunks during the level generation process. This can be very useful when defining floor or corner chunks and can keep the number of chunk definitions at a minimum.
ChunkTemplate2D chunkTemplate = new ChunkTemplate2D(new Vector2F(30f, 50f), true);
ByChance always rotates by 90° in order to reduce the number of iterations. 3D chunks are always rotated by 90° around the y-axis. If the chunk has been rotated by 360° around the y-axis, it is rotated by 90° around the x-axis after. If the chunk has been rotated by 360° around the x-axis, it is rotated by 90° around the z-axis after. This leads to a total of 64 possible rotations of each 3D chunk. You might want to consider adding rotated versions of the chunk to the chunk library instead in order to trade memory for running time.
For more advanced scenarios, the level generator can be configured with additional parameters. First, you need to import the configuration namespace of the framework:
using ByChance.Configuration;
Now, you can access the level generator configuration through the LevelGenerator.Configuration property. The following sections summarize the possibilities of customizing the level generation process.
If your chunk library contains many chunks with very similar extents, this can lead to boring repetitive patterns. In the Angry Bots Infinite showcase, we decided to tag door contexts in order to avoid artifacts of this kind: Doors that were intended to lead to other rooms were prevented from being aligned to each other.
This is done by implementing the IContextAlignmentRestriction interface and setting the corresponding property of the level generator configuration:
public class DoorContextAlignmentRestriction : IContextAlignmentRestriction
{
public bool CanBeAligned(Context first, Context second, object level)
{
return !(first.Tag.Equals("Door") && second.Tag.Equals("Door"));
}
}
levelGenerator.Configuration.ContextAlignmentRestrictions.Add(new DoorContextAlignmentRestriction());
As we know, every chunk has a relative weight that tells the level generator how often a specific chunk should be added to the level. However, the level generator takes account which contexts these chunks are added at. You can override the weight of a chunk template with respect to the context that is aligned at by providing your own implementation of IChunkDistribution:
public class RampingDifficultyDistribution : IChunkDistribution
{
public int GetEffectiveWeight(Context freeLevelContext, Context chunkCandidateContext, object level)
{
// Get position of initial chunk.
var level3D = (Level3D)level;
var initialChunk = level3D.First(chunk => string.Equals(chunk.Tag, "Entry"));
var initialContext = initialChunk.GetContext(0);
// Get distance to chunk candiate.
var chunkCandidate = (Chunk3D)chunkCandidateContext.Source;
var distance = chunkCandidate.Position - initialChunk.Position;
if (chunkCandidate.Tag.Equals("Easy"))
{
// Less easy rooms at the end of the level.
return chunkCandidateContext.Source.Weight - (int)distance.Length;
}
else
{
// More hard rooms at the end of the level.
return chunkCandidateContext.Source.Weight + (int)distance.Length;
}
}
}
levelGenerator.Configuration.ChunkDistribution = new RampingDifficultyDistribution();
Since it is the nature of the level generation algorithm to fill out the level boundaries as much as possible, the resulting level often shows unwanted patterns. A typical example is a corridor that leads nowhere. To avoid this, some kind of post-processing is required after the level has been generated.
We implemented a way to run through an arbitrary number of post-processing steps called policies that can greatly improve the layout of the levels. Each level generator holds a list of those policies which is empty by default. You can add several policies to your level generator instance, which are called directly after the level generation process in the order in which they were added.
The first framework policy checks all open contexts in the level and connects pairs of contexts that are within a certain offset to each other, specified via the constructor of the policy. The probability of this to occur increases if your chunks have similar dimensions and their contexts have similar relative positions.
AlignAdjacentContextsPolicy policy = new AlignAdjacentContextsPolicy(0.1f);
levelGenerator.Configuration.PostProcessingPolicies.Add(policy);
The second one finds all chunks within the level that have open contexts and discards them. As deleting chunks opens up previously aligned contexts of neighboring chunks, this process is repeated until the first iteration in which no chunk is discarded.
DiscardOpenChunksPolicy policy = new DiscardOpenChunksPolicy();
levelGenerator.Configuration.PostProcessingPolicies.Add(policy);
You can restrict which chunks to discard by specifying your own implementation of IDiscardOpenChunksRestriction for the policy:
public class DiscardOpenFloorsRestriction : IDiscardOpenChunksRestriction
{
public bool ShouldBeDiscarded(Chunk chunk)
{
// Discard open floors.
return chunk.Tag.Equals("Floor");
}
}
DiscardOpenChunksPolicy policy = new DiscardOpenChunksPolicy();
policy.DiscardOpenChunksRestriction = new DiscardOpenFloorsRestriction();
levelGenerator.Configuration.PostProcessingPolicies.Add(policy);
The third framework policy cleans up all open contexts. This policy is useful for discarding unused contexts before drawing the level or performing further operations on it.
DiscardOpenContextsPolicy policy = new DiscardOpenContextsPolicy();
levelGenerator.Configuration.PostProcessingPolicies.Add(policy);
Just like for open chunks, you can specify which open contexts to discard by providing your own implementation of IDiscardOpenContextsRestriction:
public class DiscardOpenDoorsRestriction : IDiscardOpenContextsRestriction
{
public bool ShouldDiscardContext(Context context)
{
// Discard doors that are leading nowhere.
return context.Tag.Equals("Door");
}
}
DiscardOpenContextsPolicy policy = new DiscardOpenContextsPolicy();
policy.DiscardOpenContextsRestriction = new DiscardOpenDoorsRestriction();
levelGenerator.Configuration.PostProcessingPolicies.Add(policy);
You can easily add further policies by implementing the IPostProcessingPolicy interface provided by the framework.
In some cases, you don’t want the framework to pick the first level chunk for you. Let’s assume that you want to place a crossing chunk at the level center, and that this chunk is the first one in your chunk library. Then you can set this chunk as initial level chunk as follows:
// Create empty level.
Vector2F levelExtents = new Vector2F(800f, 600f);
Level2D level = new Level2D(levelExtents);
// Set starting chunk.
Chunk2D startingChunk = new Chunk2D(chunkTemplate);
Vector2F chunkPosition = (levelExtents - startingChunk.Extents) / 2;
level.SetStartingChunk(startingChunk, chunkPosition);
// Generate level.
LevelGenerator2D levelGenerator = new LevelGenerator2D();
levelGenerator.GenerateLevel(chunkLibrary, level);
If you want to generate a given level again, for example because you built a level editor that is based on the ByChance Framework, you can make use of the level seed. This seed is always written to the log files explained below, and it can be passed to the level generator by instantiating a pseudo-random number generator with the desired seed:
// Set seed.
long seed = 12345L;
Random2 random = new Random2(seed);
// Generate level.
LevelGenerator2D levelGenerator = new LevelGenerator2D();
Vector2F levelExtents = new Vector2F(800f, 600f);
Level2D level = levelGenerator.GenerateLevel(chunkLibrary, levelExtents, random);
The LevelGenerator will raise the ProgressChanged event whenever a chunk has been added to the current level. You can access the current progress (between 0.0 and 1.0) from the passed ProgressChangedEventArgs object.
To maximize compatibility with other platforms (such as web players or mobile devices), ByChance no longer uses NLog for writing log files.
You can enable logging by passing your own implementation of ILevelGenerationLogger to the level generator implementation:
public class UnityLevelGenerationLogger : ILevelGenerationLogger
{
/// <summary>
/// Logs the specified message to the Unity console.
/// </summary>
/// <param name="message">Message to log.</param>
public void LogMessage(string message)
{
Debug.Log(message);
}
}
levelGenerator.Configuration.Logger = new UnityLevelGenerationLogger();
The framework uses NLog for writing verbose log output to a file next to the binary of your game called ByChance.log. You can change the logging behavior in the configuration file NLog.config.
By default, the level generation stops as soon as the level can’t be expanded any more in any direction, or more precisely, as soon as no chunk template from the chunk library can be added to any context.
However, you might need a different (earlier) condition to stop level generation, for instance after having added a specific amount of rooms to your dungeon. All you need to do is create a new script deriving from ByChance.Unity.Extension.ByChanceTerminationCondition and implement the method ConditionIsMet:
public class MaximumChunkCountTerminationCondition : ITerminationCondition
{
public int MaximumChunkCount { get; set; }
public bool ConditionIsMet(object level)
{
var level3D = (Level3D)level;
return level3D.Count >= this.MaximumChunkCount;
}
}
levelGenerator.Configuration.TerminationConditions.Add(new MaximumChunkCountTerminationCondition());
The level generation will stop as soon as any termination condition is fulfilled.
Clearly, the level generation time increases with the number of chunks that are placed. Thus, given a fixed level size, you'll want to use chunks that are bigger than you smallest level unit. For example, if you're generating a map that consists of 128 x 128 tiles, you'll want to define chunks that are bigger that one tile in size.
You’ve learned how to integrate the ByChance Framework into your game and how to have the level generator create random levels the way you want them to be. In case you run into any issues, head over to our issue tracker and we'll investigate immediately. If you need help, don’t hesitate to ask a question.
Finally, when you’re finished creating your awesome game with ByChance, we’d love to [hear from you](mailto:dev@npruehs.de?subject=ByChance Framework Showcase)!
An integration into the Unity3D game engine is readily available at the Asset Store.