Refactoring, optimizations

This commit is contained in:
2015-06-13 21:36:32 +03:00
parent 9ac0deec0b
commit 778d732866
37 changed files with 169 additions and 66 deletions

View File

@ -0,0 +1,235 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using TransportGame.Business;
using TransportGame.Model;
using TransportGame.Model.Road;
using TransportGame.Utils;
namespace TransportGame.Generator
{
public class BuildingGenerator
{
Random random = new Random();
private float LotSquareMinSize { get { return ConfigManager.Buildgen.LotSquareMinSize; } }
private float LotSquareMaxSize { get { return ConfigManager.Buildgen.LotSquareMaxSize; } }
private float LotSpacing { get { return ConfigManager.Buildgen.LotSpacing; } }
private Map map;
QuadTree<RoadNode> nodeTree;
QuadTree<BuildingLot> lotTree;
private void AllocateLots()
{
// Generate lots for every segment
foreach (var pair in map.RoadNetwork.ArticulationSegments)
{
bool didSomething;
var segment = pair.Value;
var dir = segment.Direction;
var perp = dir.RotateDeg(90);
float width0 = ConfigManager.Roadgen.SidewalkWidth + ConfigManager.Roadgen.LaneWidth * segment.LanesTo1;
float width1 = ConfigManager.Roadgen.SidewalkWidth + ConfigManager.Roadgen.LaneWidth * segment.LanesTo2;
Vector2 start = segment.Terminal1.Position;
Vector2 end = segment.Terminal2.Position;
Vector2 posL = start, posR = start, nposL, nposR;
int attempts = 0;
do
{
didSomething = false;
float sizeL = random.NextSingle(LotSquareMinSize, LotSquareMaxSize), sizeR = random.NextSingle(LotSquareMinSize, LotSquareMaxSize);
nposL = posL + dir * sizeL;
nposR = posR + dir * sizeR;
// Left side
if ((posL - end).LengthSq >= sizeL * sizeL)
{
didSomething = true;
// Build lot squares
Vector2[] left = new Vector2[4];
left[0] = posL + perp * (width0 + LotSpacing + sizeL);
left[1] = posL + perp * (width0 + LotSpacing);
left[2] = nposL + perp * (width0 + LotSpacing);
left[3] = nposL + perp * (width0 + LotSpacing + sizeL);
BuildingLot lot = new BuildingLot(sizeL, left);
if (CanAllocate(posL, lot))
lotTree.Add(lot);
// Advance
posL = nposL;
}
// Right side
if ((posR - end).LengthSq >= sizeR * sizeR)
{
didSomething = true;
// Build lot squares
Vector2[] right = new Vector2[4];
right[0] = posR - perp * (width1 + LotSpacing + sizeR);
right[1] = posR - perp * (width1 + LotSpacing);
right[2] = nposR - perp * (width1 + LotSpacing);
right[3] = nposR - perp * (width1 + LotSpacing + sizeR);
BuildingLot lot = new BuildingLot(sizeR, right);
if (CanAllocate(posL, lot))
lotTree.Add(lot);
// Advance
posR = nposR;
}
if (!didSomething)
attempts++;
} while (attempts < ConfigManager.Buildgen.MaxLotAttempts);
}
// Done
map.BuildingLots = lotTree.ToList();
}
private bool CanAllocate(Vector2 pos, BuildingLot lot0)
{
if (lot0.Points.Any(p => !lotTree.Boundary.Contains(p)))
return false;
// Test other lots
Rectangle lotArea = new Rectangle(
pos.X - 2f * LotSquareMaxSize,
pos.Y - 2f * LotSquareMaxSize,
pos.X + 2f * LotSquareMaxSize,
pos.Y + 2f * LotSquareMaxSize);
foreach (var lot in lotTree.Query(lotArea))
{
if (BuildingLot.DoesIntersect(lot0, lot))
return false;
}
// Test nearby roads
Rectangle roadArea = new Rectangle(
pos.X - 1.1f * ConfigManager.Roadgen.HighwaySegmentLength,
pos.Y - 1.1f * ConfigManager.Roadgen.HighwaySegmentLength,
pos.X + 1.1f * ConfigManager.Roadgen.HighwaySegmentLength,
pos.Y + 1.1f * ConfigManager.Roadgen.HighwaySegmentLength);
foreach (var node in nodeTree.Query(roadArea))
{
foreach (var segment in node.ArticulationSegments)
{
if (BuildingLot.DoesIntersect(lot0, segment.AsLineSegment()))
return false;
}
}
return true;
}
private void GenerateBuildings()
{
foreach (var lot in map.BuildingLots)
map.Buildings.Add(GenerateBuilding(lot));
}
Polygon GeneratePrimitivePolygon(BuildingLot lot)
{
List<Vector2> points = new List<Vector2>();
int sides = random.Next(4, 7); // Number of sides
float angle = 2 * (float)Math.PI / sides; // Angle between sides
bool ok;
do
{
// Reset
ok = true;
points.Clear();
// Generate radius, start position and direction
float radius = random.NextSingle(lot.Size * 0.25f, lot.Size * 0.5f); // Length of a side
Vector2 current = lot.Position + new Vector2(random.NextSingle(-lot.Size / 2, lot.Size / 2), random.NextSingle(-lot.Size / 2, lot.Size / 2));
Vector2 dir = new Vector2(random.NextSingle(), random.NextSingle()).Normalized * radius;
// Generate polygon
for (int i = 0; i < sides; i++)
{
points.Add(current);
// Make sure every point is inside
if (!lot.IsInside(current))
{
ok = false;
break;
}
current = current + dir;
dir = dir.Rotate(angle);
}
} while (!ok);
return new Polygon(points);
}
private Building GenerateBuilding(BuildingLot lot)
{
Building b = new Building();
int levelCount = random.Next(1, ConfigManager.Buildgen.MaxBuildingLevels);
b.LevelHeights = new float[levelCount];
b.Polygons = new Polygon[levelCount][];
for (int i = levelCount - 1; i >= 0; --i)
{
List<Polygon> polys = new List<Polygon>();
for (int j = 0; j < 1 + random.Next(ConfigManager.Buildgen.MaxPolygonsPerLevel); j++)
polys.Add(GeneratePrimitivePolygon(lot));
if (i + 1 < levelCount)
{
polys.AddRange(b.Polygons[i + 1]);
b.LevelHeights[i] = random.NextSingle(0, b.LevelHeights[i + 1]);
}
else
{
b.LevelHeights[i] = random.NextSingle(ConfigManager.Buildgen.MinBuildingHeight, ConfigManager.Buildgen.MaxBuildingHeight) * map.GetPopulation(lot.Position);
}
for (int j = 0; j < random.Next(ConfigManager.Buildgen.MaxPolygonsPerLevel); j++)
polys.Add(GeneratePrimitivePolygon(lot));
b.Polygons[i] = polys.ToArray();
}
return b;
}
public void Generate(Map map)
{
this.map = map;
map.Buildings = new List<Building>();
// Construct node tree
nodeTree = new QuadTree<RoadNode>(0, 0, map.Width, map.Height);
foreach (var pair in map.RoadNetwork.Nodes)
nodeTree.Add(pair.Value);
lotTree = new QuadTree<BuildingLot>(0, 0, map.Width, map.Height);
// Allocate lots
AllocateLots();
GenerateBuildings();
}
}
}

View File

@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 387d471e5e9dff54b9cb832e01b969ec
timeCreated: 1433340433
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using TransportGame.Generator;
using TransportGame.Model;
using TransportGame.Utils;
namespace TransportGame.Generator
{
/// <summary>
/// Complete city generator. Generates everything, from terrain to buildings
/// </summary>
public class CityGenerator
{
/// <summary>
/// Generates a city
/// </summary>
/// <param name="width">Width</param>
/// <param name="height">Height</param>
/// <returns>City</returns>
public Map Generate(int width, int height)
{
Map map;
// Generate terrain
TerrainGenerator terrainGen = new TerrainGenerator();
map = terrainGen.Generate(width, height);
// Generate population map
PopulationCentersGenerator populationGen = new PopulationCentersGenerator();
populationGen.Generate(map);
// Generate roads
RoadGenerator roadGenerator = new RoadGenerator();
roadGenerator.Generate(map);
// Generate buildings
BuildingGenerator buildingGenerator = new BuildingGenerator();
buildingGenerator.Generate(map);
// Done
return map;
}
}
}

View File

@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 723d5ad7f5932f649971961655fff523
timeCreated: 1432200350
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using TransportGame.Model;
using TransportGame.Noise;
namespace TransportGame.Generator
{
public class PopulationCentersGenerator
{
System.Random random = new System.Random();
public void Generate(Map map)
{
// Generate range
float mp = (float)(map.Width * map.Height) / (1024 * 1024); // For 4k x 4k range should be around 900
map.PopulationCenterRange = mp * 31f + 496.66f; // For 2k x 2k range should be around 600
// Generate a number of points
int maxPoints = 16 * (int) Math.Sqrt(mp);
int points = random.Next(maxPoints / 2, maxPoints);
for (int i = 0; i < points; ++i)
{
int px, py;
do
{
px = random.Next(map.Width / 8, 7 * map.Width / 8);
py = random.Next(map.Height / 8, 7 * map.Height / 8);
}
while (map.IsWater(px, py));
map.PopulationCenters.Add(new Vector2(px, py));
}
}
}
}

View File

@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 186756c7c690a3d4c8e633e3007523e0
timeCreated: 1432200350
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,316 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using TransportGame.Business;
using TransportGame.Model;
using TransportGame.Model.Road;
using TransportGame.Utils;
using Vector2 = TransportGame.Model.Vector2;
namespace TransportGame.Generator
{
public class RoadGenerator
{
class RoadGeneratorSegment
{
public RoadNode Terminal1;
public Vector2 Terminal2Pos;
public RoadNode Terminal2;
public bool Highway;
public int Time;
public RoadGeneratorSegment(RoadNode term1, Vector2 term2pos, bool highway, int time = 0)
{
Terminal1 = term1;
Terminal2Pos = term2pos;
Highway = highway;
Time = time;
}
public override string ToString()
{
string str = String.Format("(gensegment, {0}->", Terminal1);
if (Terminal2 == null)
str += Terminal2Pos.ToString();
else str += Terminal2.ToString();
str += ", ";
if (Highway)
str += "highway, ";
str += String.Format("time={0})", Time);
return str;
}
}
QuadTree<RoadNode> qtree;
List<RoadGeneratorSegment> queue;
System.Random random = new System.Random();
Map map;
private float HighwaySegmentLength { get { return ConfigManager.Roadgen.HighwaySegmentLength; } }
private float DefaultBranchPopulationTreshold { get { return ConfigManager.Roadgen.DefaultBranchPopulationTreshold; } }
private float DefaultBranchProbability { get { return ConfigManager.Roadgen.DefaultBranchProbability; } }
private float DefaultSegmentLength { get { return ConfigManager.Roadgen.DefaultSegmentLength; } }
private float SteepnessLimit { get { return ConfigManager.Roadgen.SteepnessLimit; } }
private float SlopeLimit { get { return ConfigManager.Roadgen.SlopeLimit; } }
private float RoadSegmentAngleLimit { get { return ConfigManager.Roadgen.RoadSegmentAngleLimit; } }
private float RoadSnapDistance { get { return ConfigManager.Roadgen.RoadSnapDistance; } }
private float MinNodeDistance { get { return ConfigManager.Roadgen.MinNodeDistance; } }
private int MaximumRandomStraightAngle { get { return ConfigManager.Roadgen.MaximumRandomStraightAngle; } }
private int MaximumBranchAngleVariation { get { return ConfigManager.Roadgen.MaximumBranchAngleVariation; } }
private float HighwayBranchPopulationTreshold { get { return ConfigManager.Roadgen.HighwayBranchPopulationTreshold; } }
private float HighwayBranchProbability { get { return ConfigManager.Roadgen.HighwayBranchProbability; } }
private int HighwayBranchDelay { get { return ConfigManager.Roadgen.HighwayBranchDelay; } }
private int MaximumIntersectingRoads { get { return ConfigManager.Roadgen.MaximumIntersectingRoads; } }
public RoadGenerator()
{
}
public void Initialize(Map map)
{
this.map = map;
map.RoadNetwork = new RoadNetwork();
qtree = new QuadTree<RoadNode>(0, 0, map.Width, map.Height);
queue = new List<RoadGeneratorSegment>();
// Generate positions
Vector2 center = new Vector2(map.Width / 2, map.Height / 2);
int maxDistanceFromCenter = map.Width / 3;
Vector2 p0, p1, p2; // p2 goes in opposite direction
do
{
// Generate point close to center of the map
float gen_x = random.Next(-maxDistanceFromCenter, maxDistanceFromCenter) + random.NextSingle();
float gen_y = random.Next(-maxDistanceFromCenter, maxDistanceFromCenter) + random.NextSingle();
p0 = center + new Vector2(gen_x, gen_y);
// Generate a random direction
Vector2 dir = Vector2.FromDegrees(random.Next(360)) * (HighwaySegmentLength / 2);
p1 = p0 + dir;
p2 = p0 - dir;
} while (IsObstacle(p0) || IsObstacle(p1) || IsObstacle(p2));
// Create root node
var node0 = map.RoadNetwork.CreateNode(p0);
qtree.Add(node0);
// Create & enqueue segments
queue.Add(new RoadGeneratorSegment(node0, p1, true));
queue.Add(new RoadGeneratorSegment(node0, p2, true));
}
public void Step()
{
var segment = queue.OrderBy(x => x.Time).First();
queue.Remove(segment);
// Check local constraints
if (CheckLocalConstraints(segment))
{
RoadSegment createdSegment;
// Finish to create segment
if (segment.Terminal2 != null)
createdSegment = map.RoadNetwork.CreateArticulationSegment(segment.Terminal1, segment.Terminal2);
else
createdSegment = map.RoadNetwork.CreateArticulationSegment(segment.Terminal1, segment.Terminal2Pos);
qtree.Add(createdSegment.Terminal2);
createdSegment.LanesTo1 = createdSegment.LanesTo2 = (segment.Highway) ? 3 : 1;
// Use global goals to get new segments
foreach (var newSegment in GlobalGoals(createdSegment))
{
newSegment.Time += segment.Time + 1;
queue.Add(newSegment);
}
}
}
private bool IsObstacle(Vector2 p)
{
return !map.IsInside(p.X, p.Y) || map.IsWater(p.X, p.Y) || map.GetSteepness(p.X, p.Y) > SteepnessLimit;
}
public void Generate(Map map)
{
Initialize(map);
int iterationCount = (map.Width * map.Height) / 512;
for (int i = 0; i < iterationCount && queue.Count > 0; i++)
Step();
}
private IEnumerable<RoadGeneratorSegment> GlobalGoals(RoadSegment segment)
{
Vector2 prevPos = segment.Terminal2.Position;
Vector2 dir = segment.Direction;
bool highway = (segment.LanesTo1 >= 3);
bool highwayBranched = false;
// Going straight
Vector2 straight = prevPos + dir * ((highway) ? HighwaySegmentLength : DefaultSegmentLength);
float straightPopulation = map.GetPopulation(straight);
// Highways...
if (highway)
{
Vector2 randomStraight = prevPos + HighwaySegmentLength * dir.RotateDeg(random.Next(-MaximumRandomStraightAngle, MaximumRandomStraightAngle));
float randomPopulation = map.GetPopulation(randomStraight);
if (randomPopulation > straightPopulation)
yield return new RoadGeneratorSegment(segment.Terminal2, randomStraight, highway);
else
yield return new RoadGeneratorSegment(segment.Terminal2, straight, highway);
// Branch highway
if (Math.Max(straightPopulation, randomPopulation) > HighwayBranchPopulationTreshold)
{
if (random.NextSingle() < HighwayBranchProbability)
{
Vector2 leftBranch = prevPos + HighwaySegmentLength * dir.RotateDeg(-90 + random.Next(-MaximumBranchAngleVariation, MaximumBranchAngleVariation));
yield return new RoadGeneratorSegment(segment.Terminal2, leftBranch, highway, HighwayBranchDelay);
highwayBranched = true;
}
if (random.NextSingle() < HighwayBranchProbability)
{
Vector2 rightBranch = prevPos + HighwaySegmentLength * dir.RotateDeg(90 + random.Next(-MaximumBranchAngleVariation, MaximumBranchAngleVariation));
yield return new RoadGeneratorSegment(segment.Terminal2, rightBranch, highway, HighwayBranchDelay);
highwayBranched = true;
}
}
// Don't allow more branches
if (highwayBranched)
yield break;
}
else if (random.NextSingle() < straightPopulation)
yield return new RoadGeneratorSegment(segment.Terminal2, straight, false);
// Branch normal road
if (straightPopulation > DefaultBranchPopulationTreshold)
{
if (random.NextSingle() < DefaultBranchProbability * straightPopulation)
{
Vector2 leftBranch = prevPos + HighwaySegmentLength * dir.RotateDeg(-90 + random.Next(-MaximumBranchAngleVariation, MaximumBranchAngleVariation));
yield return new RoadGeneratorSegment(segment.Terminal2, leftBranch, false, (highway) ? HighwayBranchDelay : 0);
}
if (random.NextSingle() < DefaultBranchProbability * straightPopulation)
{
Vector2 rightBranch = prevPos + HighwaySegmentLength * dir.RotateDeg(90 + random.Next(-MaximumBranchAngleVariation, MaximumBranchAngleVariation));
yield return new RoadGeneratorSegment(segment.Terminal2, rightBranch, false, (highway) ? HighwayBranchDelay : 0);
}
}
}
private bool CheckLocalConstraints(RoadGeneratorSegment segment)
{
// Constraint #1: check for obstacles
if (IsObstacle(segment.Terminal2Pos))
return false;
// Constraint #2: slope
float segmentLength = (segment.Highway) ? HighwaySegmentLength : DefaultSegmentLength;
float levelDiff = map.GetHeight((int)segment.Terminal1.X, (int)segment.Terminal1.Y) -
map.GetHeight((int)segment.Terminal2Pos.X, (int)segment.Terminal2Pos.Y);
float sinSlope = Math.Abs(levelDiff) / segmentLength;
if (Math.Asin(sinSlope) > SlopeLimit)
return false;
// Constraint #3: Number of intersecting roads
if (segment.Terminal1.ArticulationSegmentIds.Count > MaximumIntersectingRoads)
return false;
// Constraint #4: intersections & snapping
Rectangle queryArea = new Rectangle(
Math.Min(segment.Terminal1.X, segment.Terminal2Pos.X) - 3 * HighwaySegmentLength,
Math.Min(segment.Terminal1.Y, segment.Terminal2Pos.Y) - 3 * HighwaySegmentLength,
Math.Max(segment.Terminal1.X, segment.Terminal2Pos.X) + 3 * HighwaySegmentLength,
Math.Max(segment.Terminal1.Y, segment.Terminal2Pos.Y) + 3 * HighwaySegmentLength);
IEnumerable<int> segmentIds = Enumerable.Empty<int>();
// Look for nearby segments
foreach (var node in qtree.Query(queryArea))
{
if (node == segment.Terminal1)
continue;
// Too close to another node in the area
if ((node.Position - segment.Terminal2Pos).LengthSq < MinNodeDistance * MinNodeDistance)
return false;
segmentIds = segmentIds.Concat(node.ArticulationSegmentIds);
}
// Filter & sort the segments by distance
segmentIds = segmentIds.Distinct().OrderBy(id =>
LineSegment.Distance(map.RoadNetwork.ArticulationSegments[id].AsLineSegment(), segment.Terminal2Pos));
foreach (var segmentId in segmentIds)
{
var other = map.RoadNetwork.ArticulationSegments[segmentId];
var line1 = new LineSegment(segment.Terminal1.Position, segment.Terminal2Pos);
var line2 = other.AsLineSegment();
Vector2? inters = LineSegment.Intersect(line1, line2);
// Case #1: there is an intersection with another segment. We cut the rest of the segment
if (inters.HasValue && inters.Value != segment.Terminal1.Position)
{
// Check angle between segments
float cos = Vector2.Dot((line1.P1 - line1.P0).Normalized, (line2.P1 - line2.P0).Normalized);
if (Math.Abs(Math.Acos(cos)) < RoadSegmentAngleLimit)
return false;
// Split segment
var newNode = map.RoadNetwork.SplitArticulationSegment(other, inters.Value);
segment.Terminal2Pos = inters.Value;
segment.Terminal2 = newNode;
return true;
}
// Case #2: no intersection, but the point is close enough to an existing intersection
if ((segment.Terminal2Pos - other.Terminal2.Position).LengthSq <= RoadSnapDistance * RoadSnapDistance)
{
// Check angle between intersecting segments
foreach (var intersSeg in other.Terminal2.ArticulationSegments)
{
float cos = Vector2.Dot(line1.Direction, intersSeg.Direction);
if (Math.Abs(Math.Acos(cos)) < RoadSegmentAngleLimit)
return false;
}
segment.Terminal2Pos = other.Terminal2.Position;
segment.Terminal2 = other.Terminal2;
return true;
}
//// TODO: Case #3: the point is close enough to an existing road segment
//float dist = LineSegment.Distance(line2, segment.Terminal2Pos);
//if (dist < RoadSnapDistance)
//{
// float proj0 = (float)Math.Sqrt((line2.P0 - segment.Terminal2Pos).LengthSq - dist * dist);
// float percent = proj0 /
//}
}
return true;
}
}
}

View File

@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: ca14a99c3a7bbee468354076f2d6f486
timeCreated: 1431067754
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml.Serialization;
using TransportGame.Business;
using TransportGame.Model;
using TransportGame.Noise;
using TransportGame.Utils;
using TransportGame.Utils.Algorithms;
using UnityEngine;
namespace TransportGame.Generator
{
public class TerrainGenerator
{
/// <summary>
/// Gets or sets the underlying noise generator
/// </summary>
public NoiseGenerator Noise { get; set; }
private System.Random random = new System.Random();
public TerrainGenerator()
{
Noise = new PerlinNoiseGenerator();
if (ConfigManager.Tergen == null)
throw new Exception("Not initialized!");
Noise.Octaves = ConfigManager.Tergen.NoiseOctaves;
Noise.NonLinearPower = ConfigManager.Tergen.NoiseNonLinearPower;
Noise.Scale = ConfigManager.Tergen.ElevationScale;
}
public Map Generate(int width, int height)
{
// Create map
Map map = new Map(width, height);
// Pick a random biome
map.Biome = PickBiome();
Logger.Info("Picked biome: {0}", map.Biome.Name);
// Generate elevation
GenerateElevation(map);
// Generate water level
float waterAmount = random.NextSingle(map.Biome.Moisture.Minimum, map.Biome.Moisture.Maximum);
map.WaterLevel = Mathf.Pow(waterAmount, ConfigManager.Tergen.WaterNonLinearPower) * map.Biome.Height;
return map;
}
private Biome PickBiome()
{
int biomeCount = BiomeManager.Biomes.Count();
int biome = random.Next(biomeCount);
return BiomeManager.Biomes.ElementAt(biome);
}
private void GenerateElevation(Map map)
{
for (int x = 0; x < map.Width; ++x)
for (int y = 0; y < map.Height; ++y)
map.Heightmap[x, y] = Noise.Generate(x, y, 0, 1);
}
}
}

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f4042531a480f4149bdda36c67975b0c
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData: