可以与Tilemaps一起使用的过程模式(第一部分)
Many creators have used procedural generation to add some diversity to their game. Some notable mentions include the likes of Minecraft, or more recently, Enter the Gungeon and Descenders. This post explains some of the algorithms you can use with Tilemap, introduced as a 2D feature in Unity 2017.2, and RuleTile.
许多创造者使用程序生成为游戏增加了一些多样性。 一些值得注意的提及包括Minecraft之类 ,或者最近提到的Enter the Gungeon和Descenders 。 这篇文章解释了一些可以与Tilemap一起使用的算法,该算法在Unity 2017.2和RuleTile中作为2D功能引入。
With procedurally created maps, you can make sure that no two plays of your game are the same. You can use various inputs, such as time or the current level of the player to ensure that the content changes dynamically even after the game has been built.
使用按程序创建的地图,您可以确保没有两个游戏玩法相同。 您可以使用各种输入,例如时间或玩家的当前级别,以确保即使在构建游戏之后,内容也会动态更改。
这篇博客文章是关于什么的? (What is this blog post about?)
We’ll take a look at some of the most common methods of creating a procedural world, and a couple of custom variations that I have created. Here’s an example of what you may be able to create after reading this article. Three algorithms are working together to create one map, using a Tilemap and a RuleTile:
我们将介绍一些创建过程世界的最常用方法,以及我创建的一些自定义变体。 这是阅读本文后可能创建的示例。 三种算法正在共同使用Tilemap和RuleTile创建一个地图:
When we’re generating a map with any of the algorithms, we will receive an int array which contains all of the new data. We can then take this data and continue to modify it or render it to a tilemap.
当我们使用任何一种算法生成地图时,我们都会收到一个包含所有新数据的int数组。 然后,我们可以获取这些数据并继续对其进行修改或将其呈现为tilemap。
Good to know before you read further:
在您进一步阅读之前,很高兴知道:
- The way we distinguish between what’s a tile and what isn’t is by using binary. 1 being on and 0 being off. 我们通过使用二进制来区分什么是图块和什么不是图块。 1打开,0关闭。
- We will store all of our maps into a 2D integer array, which is returned to the user at the end of each function (except for when we render). 我们会将所有地图存储到2D整数数组中,该数组在每个函数的末尾返回给用户(渲染时除外)。
-
I will use the array function GetUpperBound() to get the height and width of each map so that we have fewer variables going into each function, and cleaner code.
我将使用数组函数GetUpperBound()来获取每个地图的高度和宽度,以使进入每个函数的变量更少,代码更简洁 。
-
I often use Mathf.FloorToInt(), this is because the Tilemap coordinate system starts at the bottom left and using Mathf.FloorToInt() allows us to round the numbers to an integer.
我经常使用Mathf.FloorToInt() ,这是因为Tilemap坐标系从左下角开始,并且使用Mathf.FloorToInt()允许我们将数字四舍五入为整数。
- All of the code provided in this blog post is in C#. 本博客文章中提供的所有代码都在C#中。
产生阵列 (Generate Array)
GenerateArray creates a new int array of the size given to it. We can also say whether the array should be full or empty (1 or 0). Here’s the code:
GenerateArray创建一个给定大小的新int数组。 我们也可以说数组应该是满还是空(1或0)。 这是代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
public static int[,] GenerateArray(int width, int height, bool empty)
{
int[,] map = new int[width, height];
for (int x = 0; x < map.GetUpperBound(0); x++)
{
for (int y = 0; y < map.GetUpperBound(1); y++)
{
if (empty)
{
map[x, y] = 0;
}
else
{
map[x, y] = 1;
}
}
}
return map;
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
public static int [ , ] GenerateArray ( int width , int height , bool empty )
{
int [ , ] map = new int [ width , height ] ;
for ( int x = 0 ; x < map . GetUpperBound ( 0 ) ; x ++ )
{
for ( int y = 0 ; y < map . GetUpperBound ( 1 ) ; y ++ )
{
if ( empty )
{
map [ x , y ] = 0 ;
}
else
{
map [ x , y ] = 1 ;
}
}
}
return map ;
}
|
渲染图 (Render Map)
This function is used to render our map to the tilemap. We cycle through the width and height of the map, only placing tiles if the array has a 1 at the location we are checking.
此功能用于将地图渲染为tilemap。 我们在地图的宽度和高度之间循环,仅当数组在我们要检查的位置上为1时才放置图块。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
public static void RenderMap(int[,] map, Tilemap tilemap, TileBase tile)
{
//Clear the map (ensures we dont overlap)
tilemap.ClearAllTiles();
//Loop through the width of the map
for (int x = 0; x < map.GetUpperBound(0) ; x++)
{
//Loop through the height of the map
for (int y = 0; y < map.GetUpperBound(1); y++)
{
// 1 = tile, 0 = no tile
if (map[x, y] == 1)
{
tilemap.SetTile(new Vector3Int(x, y, 0), tile);
}
}
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
public static void RenderMap ( int [ , ] map , Tilemap tilemap , TileBase tile )
{
//Clear the map (ensures we dont overlap)
tilemap . ClearAllTiles ( ) ;
//Loop through the width of the map
for ( int x = 0 ; x < map . GetUpperBound ( 0 ) ; x ++ )
{
//Loop through the height of the map
for ( int y = 0 ; y < map . GetUpperBound ( 1 ) ; y ++ )
{
// 1 = tile, 0 = no tile
if ( map [ x , y ] == 1 )
{
tilemap . SetTile ( new Vector3Int ( x , y , 0 ) , tile ) ;
}
}
}
}
|
更新地图 (Update Map)
This function is used only to update the map, rather than rendering again. This way we can use less resources as we aren’t redrawing every single tile and its tile data.
此功能仅用于更新地图,而不用于再次渲染。 这样,由于我们不需要重绘每个图块及其图块数据,因此可以使用更少的资源。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public static void UpdateMap(int[,] map, Tilemap tilemap) //Takes in our map and tilemap, setting null tiles where needed
{
for (int x = 0; x < map.GetUpperBound(0); x++)
{
for (int y = 0; y < map.GetUpperBound(1); y++)
{
//We are only going to update the map, rather than rendering again
//This is because it uses less resources to update tiles to null
//As opposed to re-drawing every single tile (and collision data)
if (map[x, y] == 0)
{
tilemap.SetTile(new Vector3Int(x, y, 0), null);
}
}
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public static void UpdateMap ( int [ , ] map , Tilemap tilemap ) //Takes in our map and tilemap, setting null tiles where needed
{
for ( int x = 0 ; x < map . GetUpperBound ( 0 ) ; x ++ )
{
for ( int y = 0 ; y < map . GetUpperBound ( 1 ) ; y ++ )
{
//We are only going to update the map, rather than rendering again
//This is because it uses less resources to update tiles to null
//As opposed to re-drawing every single tile (and collision data)
if ( map [ x , y ] == 0 )
{
tilemap . SetTile ( new Vector3Int ( x , y , 0 ) , null ) ;
}
}
}
}
|
佩林噪声 (Perlin Noise)
Perlin noise can be used in various ways. The first way we can use it is to create a top layer for our map. This is as simple as just getting a new point using our current x position and a seed.
佩林噪声可以多种方式使用。 我们使用它的第一种方法是为地图创建顶层。 这就像使用我们当前的x位置和种子获得一个新点一样简单。
简单 (Simple)
This generation takes the simplest form of implementing Perlin Noise into level generation. We can use the Unity function for Perlin Noise to help us, so there is no fancy programming going into it. We are also going to ensure that we have whole numbers for our tilemap by using the function Mathf.FloorToInt().
这一代采用将Perlin Noise实施到电平生成中的最简单形式。 我们可以将Unity函数用于Perlin Noise来帮助我们,因此不需要花哨的编程。 我们还将通过使用Mathf.FloorToInt()函数来确保图块的整数 。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
public static int[,] PerlinNoise(int[,] map, float seed)
{
int newPoint;
//Used to reduced the position of the Perlin point
float reduction = 0.5f;
//Create the Perlin
for (int x = 0; x < map.GetUpperBound(0); x++)
{
newPoint = Mathf.FloorToInt((Mathf.PerlinNoise(x, seed) - reduction) * map.GetUpperBound(1));
//Make sure the noise starts near the halfway point of the height
newPoint += (map.GetUpperBound(1) / 2);
for (int y = newPoint; y >= 0; y--)
{
map[x, y] = 1;
}
}
return map;
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
public static int [ , ] PerlinNoise ( int [ , ] map , float seed )
{
int newPoint ;
//Used to reduced the position of the Perlin point
float reduction = 0.5f ;
//Create the Perlin
for ( int x = 0 ; x < map . GetUpperBound ( 0 ) ; x ++ )
{
newPoint = Mathf . FloorToInt ( ( Mathf . PerlinNoise ( x , seed ) - reduction ) * map . GetUpperBound ( 1 ) ) ;
//Make sure the noise starts near the halfway point of the height
newPoint += ( map . GetUpperBound ( 1 ) / 2 ) ;
for ( int y = newPoint ; y >= 0 ; y -- )
{
map [ x , y ] = 1 ;
}
}
return map ;
}
|
This is how it looks rendered onto a tilemap:
这是呈现在tilemap上的样子:
平滑的 (Smoothed)
We can also take this function and smooth it out. Set intervals to record the Perlin height, then smooth between the points. This function ends up being slightly more advanced, as we have to take into account Lists of integers for our intervals.
我们也可以使用此功能并使其平滑。 设置间隔以记录Perlin高度,然后在两点之间平滑。 该功能最终会稍微先进一些,因为我们必须考虑间隔的整数列表。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
public static int[,] PerlinNoiseSmooth(int[,] map, float seed, int interval)
{
//Smooth the noise and store it in the int array
if (interval > 1)
{
int newPoint, points;
//Used to reduced the position of the Perlin point
float reduction = 0.5f;
//Used in the smoothing process
Vector2Int currentPos, lastPos;
//The corresponding points of the smoothing. One list for x and one for y
List<int> noiseX = new List<int>();
List<int> noiseY = new List<int>();
//Generate the noise
for (int x = 0; x < map.GetUpperBound(0); x += interval)
{
newPoint = Mathf.FloorToInt((Mathf.PerlinNoise(x, (seed * reduction))) * map.GetUpperBound(1));
noiseY.Add(newPoint);
noiseX.Add(x);
}
points = noiseY.Count;
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
public static int [ , ] PerlinNoiseSmooth ( int [ , ] map , float seed , int interval )
{
//Smooth the noise and store it in the int array
if ( interval > 1 )
{
int newPoint , points ;
//Used to reduced the position of the Perlin point
float reduction = 0.5f ;
//Used in the smoothing process
Vector2Int currentPos , lastPos ;
//The corresponding points of the smoothing. One list for x and one for y
List < int > noiseX = new List < int > ( ) ;
List < int > noiseY = new List < int > ( ) ;
//Generate the noise
for ( int x = 0 ; x < map . GetUpperBound ( 0 ) ; x += interval )
{
newPoint = Mathf . FloorToInt ( ( Mathf . PerlinNoise ( x , ( seed * reduction ) ) ) * map . GetUpperBound ( 1 ) ) ;
noiseY . Add ( newPoint ) ;
noiseX . Add ( x ) ;
}
points = noiseY . Count ;
|
For the first part of this function, we’re first checking to see if the interval is more than one. If it is, we then generate the noise. We do this at intervals to allow for smoothing. The next part is to work through smoothing the points.
对于此功能的第一部分,我们首先检查一下间隔是否大于一个。 如果是,则我们将产生噪声。 我们会定期进行此操作以使平滑。 下一部分是通过平滑点来进行工作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
//Start at 1 so we have a previous position already
for (int i = 1; i < points; i++)
{
//Get the current position
currentPos = new Vector2Int(noiseX[i], noiseY[i]);
//Also get the last position
lastPos = new Vector2Int(noiseX[i - 1], noiseY[i - 1]);
//Find the difference between the two
Vector2 diff = currentPos - lastPos;
//Set up what the height change value will be
float heightChange = diff.y / interval;
//Determine the current height
float currHeight = lastPos.y;
//Work our way through from the last x to the current x
for (int x = lastPos.x; x < currentPos.x; x++)
{
for (int y = Mathf.FloorToInt(currHeight); y > 0; y--)
{
map[x, y] = 1;
}
currHeight += heightChange;
}
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
//Start at 1 so we have a previous position already
for ( int i = 1 ; i < points ; i ++ )
{
//Get the current position
currentPos = new Vector2Int ( noiseX [ i ] , noiseY [ i ] ) ;
//Also get the last position
lastPos = new Vector2Int ( noiseX [ i - 1 ] , noiseY [ i - 1 ] ) ;
//Find the difference between the two
Vector2 diff = currentPos - lastPos ;
//Set up what the height change value will be
float heightChange = diff . y / interval ;
//Determine the current height
float currHeight = lastPos . y ;
//Work our way through from the last x to the current x
for ( int x = lastPos . x ; x < currentPos . x ; x ++ )
{
for ( int y = Mathf . FloorToInt ( currHeight ) ; y > 0 ; y -- )
{
map [ x , y ] = 1 ;
}
currHeight += heightChange ;
}
}
}
|
The smoothing happens through the following steps:
平滑通过以下步骤进行:
- Get the current position and the last position 获取当前位置和最后位置
- Get the difference between the two positions, the key information we want is the difference in the y-axis 获取两个位置之间的差异,我们想要的关键信息是y轴上的差异
- Next, we determine how much we should change the hit by, this is done by dividing the y difference by the interval variable. 接下来,我们确定应更改点击量的多少,方法是将y差除以区间变量。
- Now we can start setting the positions. We’ll work our way down to zero 现在我们可以开始设置位置了。 我们将努力降至零
- When we do hit 0 on the y-axis, we will add the height change to the current height and repeat the process for the next x position 当我们确实在y轴上命中0时,我们会将高度更改添加到当前高度,并为下一个x位置重复该过程
- Once we have done every position between the last position and the current position, we will move on to the next point 一旦完成了最后一个位置和当前位置之间的每个位置,我们将继续进行下一个点
If the interval is less than one, we simply use the previous function to do the work for us.
如果间隔小于一个,我们只需使用上一个函数为我们完成工作即可。
1
2
3
4
5
6
7
|
else
{
//Defaults to a normal Perlin gen
map = PerlinNoise(map, seed);
}
return map;
|
1
2
3
4
5
6
7
|
else
{
//Defaults to a normal Perlin gen
map = PerlinNoise ( map , seed ) ;
}
return map ;
|
Let’s see how it looks rendered:
让我们看看它的外观如何:
随机漫步 (Random Walk)
随机步行上衣 (Random Walk Top)
The way this algorithm works is by flipping a coin. We then get one of two results. If the result is heads, we move up one block, if the result is tails we instead move down one block. This creates some height to our level by always moving either up or down. The only downside to this algorithm is that it looks very blocky. Let’s take a look at how it works.
该算法的工作方式是掷硬币。 然后,我们得到两个结果之一。 如果结果为正面,则向上移动一个块;如果结果为正面,则向下移动一个块。 通过始终向上或向下移动,可以为我们的水平创建一些高度。 该算法的唯一缺点是它看起来非常块状。 让我们看看它是如何工作的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
public static int[,] RandomWalkTop(int[,] map, float seed)
{
//Seed our random
System.Random rand = new System.Random(seed.GetHashCode());
//Set our starting height
int lastHeight = Random.Range(0, map.GetUpperBound(1));
//Cycle through our width
for (int x = 0; x < map.GetUpperBound(0); x++)
{
//Flip a coin
int nextMove = rand.Next(2);
//If heads, and we aren't near the bottom, minus some height
if (nextMove == 0 && lastHeight > 2)
{
lastHeight--;
}
//If tails, and we aren't near the top, add some height
else if (nextMove == 1 && lastHeight < map.GetUpperBound(1) - 2)
{
lastHeight++;
}
//Circle through from the lastheight to the bottom
for (int y = lastHeight; y >= 0; y--)
{
map[x, y] = 1;
}
}
//Return the map
return map;
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
public static int [ , ] RandomWalkTop ( int [ , ] map , float seed )
{
//Seed our random
System . Random rand = new System . Random ( seed . GetHashCode ( ) ) ;
//Set our starting height
int lastHeight = Random . Range ( 0 , map . GetUpperBound ( 1 ) ) ;
//Cycle through our width
for ( int x = 0 ; x < map . GetUpperBound ( 0 ) ; x ++ )
{
//Flip a coin
int nextMove = rand . Next ( 2 ) ;
//If heads, and we aren't near the bottom, minus some height
if ( nextMove == 0 && lastHeight > 2 )
{
lastHeight -- ;
}
//If tails, and we aren't near the top, add some height
else if ( nextMove == 1 && lastHeight < map . GetUpperBound ( 1 ) - 2 )
{
lastHeight ++ ;
}
//Circle through from the lastheight to the bottom
for ( int y = lastHeight ; y >= 0 ; y -- )
{
map [ x , y ] = 1 ;
}
}
//Return the map
return map ;
}
|
This generation gives us more of a smooth height compared to the Perlin noise generation.
与Perlin噪声相比,这一代为我们提供了更高的平滑高度。
随机行走顶部平滑 (
Random Walk Top Smoothed)
This generation gives us more of a smooth height compared to the Perlin noise generation.
与Perlin噪声相比,这一代为我们提供了更高的平滑高度。
This Random Walk variation allows for a much smoother finish than the previous version. We can do this by adding two new variables to our function:
与以前的版本相比,这种随机游走的变化可以使平滑处理更为流畅。 我们可以通过在函数中添加两个新变量来做到这一点:
- The first variable is used to determine how long we have held our current height. This is an integer and is reset when we change the height. 第一个变量用于确定保持当前高度的时间。 这是一个整数,当我们更改高度时会重置。
- The second variable is an input for the function and is used as our minimum section width for the height. This will make more sense when you have seen the function 第二个变量是函数的输入,并用作我们的最小截面宽度。 看到该功能后,这将更有意义
Now we know what we need to add. Let’s have a look at the function:
现在我们知道需要添加什么了。 让我们看一下函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
public static int[,] RandomWalkTopSmoothed(int[,] map, float seed, int minSectionWidth)
{
//Seed our random
System.Random rand = new System.Random(seed.GetHashCode());
//Determine the start position
int lastHeight = Random.Range(0, map.GetUpperBound(1));
//Used to determine which direction to go
int nextMove = 0;
//Used to keep track of the current sections width
int sectionWidth = 0;
//Work through the array width
for (int x = 0; x <= map.GetUpperBound(0); x++)
{
//Determine the next move
nextMove = rand.Next(2);
//Only change the height if we have used the current height more than the minimum required section width
if (nextMove == 0 && lastHeight > 0 && sectionWidth > minSectionWidth)
{
lastHeight--;
sectionWidth = 0;
}
else if (nextMove == 1 && lastHeight < map.GetUpperBound(1) && sectionWidth > minSectionWidth)
{
lastHeight++;
sectionWidth = 0;
}
//Increment the section width
sectionWidth++;
//Work our way from the height down to 0
for (int y = lastHeight; y >= 0; y--)
{
map[x, y] = 1;
}
}
//Return the modified map
return map;
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
public static int [ , ] RandomWalkTopSmoothed ( int [ , ] map , float seed , int minSectionWidth )
{
//Seed our random
System . Random rand = new System . Random ( seed . GetHashCode ( ) ) ;
//Determine the start position
int lastHeight = Random . Range ( 0 , map . GetUpperBound ( 1 ) ) ;
//Used to determine which direction to go
int nextMove = 0 ;
//Used to keep track of the current sections width
int sectionWidth = 0 ;
//Work through the array width
for ( int x = 0 ; x <= map . GetUpperBound ( 0 ) ; x ++ )
{
//Determine the next move
nextMove = rand . Next ( 2 ) ;
//Only change the height if we have used the current height more than the minimum required section width
if ( nextMove == 0 && lastHeight > 0 && sectionWidth > minSectionWidth )
{
lastHeight -- ;
sectionWidth = 0 ;
}
else if ( nextMove == 1 && lastHeight < map . GetUpperBound ( 1 ) && sectionWidth > minSectionWidth )
{
lastHeight ++ ;
sectionWidth = 0 ;
}
//Increment the section width
sectionWidth ++ ;
//Work our way from the height down to 0
for ( int y = lastHeight ; y >= 0 ; y -- )
{
map [ x , y ] = 1 ;
}
}
//Return the modified map
return map ;
}
|
As you can see from the gif below, the smoothing of the random walk algorithm allows for some nice flat pieces within the level.
从下面的gif中可以看出,随机游走算法的平滑处理可以在关卡中提供一些不错的平坦片段。
结论 (Conclusion)
I hope this has inspired you to start using some form of procedural generation within your projects. If you want to learn more about procedural generating maps, check out the Procedural Generation Wiki or Roguebasin.com, which are both great resources.
我希望这启发了您开始在项目中使用某种形式的过程生成。 如果您想了解有关过程生成图的更多信息,请查看Procedural Generation Wiki或Roguebasin.com ,它们都是很棒的资源。
You can look out for the next post in the series to see how we can use procedural generation to create cave systems.
您可以查看该系列的下一篇文章,以了解如何使用程序生成来创建洞穴系统。
If you make something cool using procedural generation feel free to message me on Twitter or leave a comment below!
如果您使用程序生成功能使自己很酷,请随时在Twitter上给我发消息或在下面发表评论!
Unite Berlin的 2D程序生成 (2D Procedural Generation at Unite Berlin)
Want to hear more about it and get a live demo? I’m also talking about Procedural Patterns to use with Tilemaps at Unite Berlin, in the expo hall mini theater on June 20th. I’ll be around after the talk if you’d like to have a chat in person!
想了解更多并获得现场演示吗? 我还将在6月20日于博览会大厅迷你剧院的Unite Berlin讨论与Tilemaps配合使用的过程模式。 谈话结束后,如果您想亲自聊天,我会在附近的!
翻译自: https://blogs.unity3d.com/2018/05/29/procedural-patterns-you-can-use-with-tilemaps-part-i/