Skip to content

Commit

Permalink
Level structure diagnostics
Browse files Browse the repository at this point in the history
  • Loading branch information
OndrejNepozitek committed Nov 6, 2024
1 parent ccfa130 commit b1777ee
Show file tree
Hide file tree
Showing 25 changed files with 50,643 additions and 1 deletion.
241 changes: 241 additions & 0 deletions Runtime/Grid2D/Common/Diagnostics/LevelStructureDiagnostics.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using Edgar.Legacy.Core.MapDescriptions;
using UnityEngine;
using Debug = UnityEngine.Debug;

namespace Edgar.Unity.Diagnostics
{
public static class LevelStructureDiagnostics
{
public static Result Analyze(DungeonGeneratorLevelGrid2D level)
{
// var sw = new Stopwatch();
// sw.Start();

var resultsPerObject = new List<KeyValuePair<string, List<KeyValue>>>
{
new KeyValuePair<string, List<KeyValue>>("Generated level", Analyze(level.RootGameObject))
};

var roomTemplates = level.RoomInstances.Select(x => x.RoomTemplatePrefab).Distinct().ToList();
foreach (var roomTemplate in roomTemplates)
{
resultsPerObject.Add(new KeyValuePair<string, List<KeyValue>>(roomTemplate.name, Analyze(roomTemplate)));
}

var allResults = resultsPerObject
.Select(x => x.Value)
.SelectMany(x => x)
.ToList();
var identifiers = allResults
.Select(x => x.GetIdentifier())
.Distinct()
.OrderBy(x => x)
.ToList();

var result = new Result();
var sb = new StringBuilder();
sb.AppendLine("!! Generated level structure warning !!");
sb.AppendLine("It seems like the structure of the generated level game object does not match the structure of (some) room templates.");
sb.AppendLine("For example, if you change a Tag or a Component on a tilemap layer, you must configure the generator to reflect that.");
sb.AppendLine();
sb.AppendLine("SOLUTION: The easiest solution is to go to the Generator component and set the Tilemap Layers Structure field to 'From Example'.");
sb.AppendLine("Then, assign one of your room templates to the `Tilemap Layers Example` field to act as a template for the generated level.");
sb.AppendLine("More information: https://ondrejnepozitek.github.io/Edgar-Unity/docs/other/faq/#changes-to-a-room-template-are-lost-after-a-level-is-generated");
sb.AppendLine();
sb.AppendLine("DETECTED PROBLEMS:");
sb.AppendLine();

foreach (var identifier in identifiers)
{
var exampleItem = allResults.First(x => x.GetIdentifier() == identifier);
var valueToObjects = new Dictionary<string, List<string>>();
var missingObjects = new List<string>();

foreach (var results in resultsPerObject)
{
var item = results.Value.FirstOrDefault(x => x.GetIdentifier() == identifier);
if (item != null)
{
if (!valueToObjects.TryGetValue(item.Value, out var values))
{
values = new List<string>();
valueToObjects.Add(item.Value, values);
}
values.Add(results.Key);
}
else
{
if (exampleItem.Type == KeyValueType.LayerComponent)
{
// We don't want to report missing components on the layer if the layer itself is missing
var layerExists = results.Value.Any(x => x.Type == KeyValueType.Layer && x.TilemapLayer == exampleItem.TilemapLayer);
if (layerExists)
{
missingObjects.Add(results.Key);
}
}
else if (exampleItem.Type == KeyValueType.Layer)
{
// Do not report missing outline override
if (exampleItem.TilemapLayer != GeneratorConstantsGrid2D.OutlineOverrideLayerName)
{
missingObjects.Add(results.Key);
}
}
}
}

switch (exampleItem?.Type)
{
case KeyValueType.Layer:
{
if (missingObjects.Count > 0)
{
result.IsPotentialProblem = true;
sb.AppendLine($"Tilemap layer \"{exampleItem.TilemapLayer}\" missing:");
sb.AppendLine($" - missing in room templates: {FormatObjectNames(missingObjects)}");
sb.AppendLine($" - present in room templates: {FormatObjectNames(valueToObjects.Values.First())}");
}

break;
}

case KeyValueType.LayerComponent:
{
if (missingObjects.Count > 0)
{
result.IsPotentialProblem = true;
sb.AppendLine($"Tilemap layer \"{exampleItem.TilemapLayer}\" missing component \"{exampleItem.Key}\":");
sb.AppendLine($" - missing in room templates: {FormatObjectNames(missingObjects)}");
sb.AppendLine($" - present in room templates: {FormatObjectNames(valueToObjects.Values.First())}");
}

break;
}

case KeyValueType.LayerProperty:
{
if (valueToObjects.Count > 1)
{
result.IsPotentialProblem = true;
sb.AppendLine($"Tilemap layer \"{exampleItem.TilemapLayer}\" - difference in {exampleItem.Key} detected:");
foreach (var pair in valueToObjects)
{
sb.AppendLine($" - \"{pair.Key}\" in room templates: {FormatObjectNames(pair.Value)}");
}
}

break;
}
}
}

sb.AppendLine();
sb.AppendLine("NOTE: You can disable this warning if you uncheck the 'Analyze Level Structure' field inside the generator component.");

result.Summary = sb.ToString();

// Debug.LogWarning(sw.Elapsed);

return result;
}

private static string FormatObjectNames(List<string> names)
{
if (names.Count == 1)
{
return $"\"{names[0]}\"";
}

if (names.Count == 2)
{
return $"\"{names[0]}\", \"{names[1]}\"";
}

return $"\"{names[0]}\", \"{names[2]}\" and more";
}

private static List<KeyValue> Analyze(GameObject gameObject)
{
var result = new List<KeyValue>();
var tilemaps = RoomTemplateUtilsGrid2D.GetTilemaps(gameObject);

foreach (var tilemap in tilemaps)
{
var layerName = tilemap.gameObject.name;
result.Add(new KeyValue(layerName, KeyValueType.Layer, null, layerName));
result.Add(new KeyValue(layerName, KeyValueType.LayerProperty,"Tag", tilemap.gameObject.tag));
result.Add(new KeyValue(layerName, KeyValueType.LayerProperty, "Layer", LayerMask.LayerToName(tilemap.gameObject.layer)));

foreach (var component in tilemap.gameObject.GetComponents<Component>())
{
result.Add(new KeyValue(layerName, KeyValueType.LayerComponent, component.GetType().Name, component.GetType().Name));
}
}

return result;
}

private enum KeyValueType
{
Undefined,
Layer,
LayerProperty,
LayerComponent,
}

private class KeyValue
{
/// <summary>
/// Name of the layer that this concerns.
/// </summary>
public string TilemapLayer { get; }

/// <summary>
/// Type of the value.
/// </summary>
public KeyValueType Type { get; }

/// <summary>
/// Identifier of the tracked value. Can be:
/// - null when tracking Layer
/// - component name when tracking LayerComponent
/// - property name when tracking LayerProperty
/// </summary>
public string Key { get; }

/// <summary>
/// Tracked value. Can be:
/// - null for Layer and LayerComponent
/// - property value for LayerProperty
/// </summary>
public string Value { get; }

public KeyValue(string tilemapLayer, KeyValueType type, string key, string value)
{
TilemapLayer = tilemapLayer;
Key = key;
Value = value;
Type = type;
}

public string GetIdentifier()
{
return $"{(int)Type}_{Type}_{TilemapLayer}_{Key}";
}
}

public class Result : IDiagnosticResult
{
public string Name { get; set; }

public string Summary { get; set; }

public bool IsPotentialProblem { get; set; }
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 23 additions & 0 deletions Runtime/Grid2D/Common/Utils/PostProcessUtilsGrid2D.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Edgar.Unity.Diagnostics;
using UnityEngine;
using UnityEngine.Tilemaps;
using Object = UnityEngine.Object;
Expand Down Expand Up @@ -275,6 +276,28 @@ public static void DisableRoomTemplateColliders(DungeonGeneratorLevelGrid2D leve
}
}

public static void AnalyzeLevelStructure(DungeonGeneratorLevelGrid2D level)
{
if (!Application.isEditor)
{
return;
}

try
{
var result = LevelStructureDiagnostics.Analyze(level);
if (result.IsPotentialProblem)
{
Debug.LogWarning(result.Summary);
}
}
catch (Exception e)
{
Debug.LogError("Could not analyze level structure, see the exception below");
Debug.LogException(e);
}
}


/// <summary>
/// Disables colliders of individual tilemaps in a given room template.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public class PostProcessingConfigGrid2D

public bool DisableRoomTemplatesColliders = true;

public bool AnalyzeLevelStructure = true;

private bool IsTilemapsFromExample => TilemapLayersStructure == TilemapLayersStructureModeGrid2D.FromExample;

private bool IsTilemapsCustom => TilemapLayersStructure == TilemapLayersStructureModeGrid2D.Custom;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace Edgar.Unity
{
Expand Down Expand Up @@ -116,6 +115,14 @@ private void RegisterCallbacks(PriorityCallbacks<DungeonGeneratorPostProcessCall
PostProcessUtilsGrid2D.DisableRoomTemplateColliders(level);
});
}

if (config.AnalyzeLevelStructure)
{
callbacks.RegisterAfterAll((level) =>
{
PostProcessUtilsGrid2D.AnalyzeLevelStructure(level);
});
}
}
}
}
31 changes: 31 additions & 0 deletions Tests/Runtime/LevelStructureTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System.Collections;
using Edgar.Unity.Diagnostics;
using Edgar.Unity.Tests;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;

namespace Edgar.Unity.Edgar.Tests.Runtime
{
public class LevelStructureTests : TestBase
{
[UnitySetUp]
public IEnumerator SetUp()
{
LoadScene("LevelStructure");
yield return null;
}

[Test]
public void LevelStructure()
{
var generator = GetDungeonGenerator();
var payload = (DungeonGeneratorPayloadGrid2D) generator.Generate();
var level = payload.GeneratedLevel;
var result = LevelStructureDiagnostics.Analyze(level);

Assert.That(result.IsPotentialProblem, Is.True);
Assert.That(result.Summary.Replace("\r\n", "\n"), Is.EqualTo("!! Generated level structure warning !!\nIt seems like the structure of the generated level game object does not match the structure of (some) room templates.\nFor example, if you change a Tag or a Component on a tilemap layer, you must configure the generator to reflect that.\n\nSOLUTION: The easiest solution is to go to the Generator component and set the Tilemap Layers Structure field to 'From Example'.\nThen, assign one of your room templates to the `Tilemap Layers Example` field to act as a template for the generated level.\nMore information: https://ondrejnepozitek.github.io/Edgar-Unity/docs/other/faq/#changes-to-a-room-template-are-lost-after-a-level-is-generated\n\nDETECTED PROBLEMS:\n\nTilemap layer \"Walls\" missing:\n - missing in room templates: \"Missing Walls\"\n - present in room templates: \"Generated level\", \"Different Tag\" and more\nTilemap layer \"Collideable\" - difference in Tag detected:\n - \"Untagged\" in room templates: \"Generated level\", \"Baseline\" and more\n - \"Respawn\" in room templates: \"Different Tag\"\n - \"EditorOnly\" in room templates: \"Different Tag 2\"\nTilemap layer \"Floor\" - difference in Layer detected:\n - \"Default\" in room templates: \"Generated level\", \"Baseline\" and more\n - \"Water\" in room templates: \"Different Layer\"\nTilemap layer \"Other 3\" missing component \"Animator\":\n - missing in room templates: \"Generated level\", \"Baseline\" and more\n - present in room templates: \"Extra Component\"\n\nNOTE: You can disable this warning if you uncheck the 'Analyze Level Structure' field inside the generator component.\n"));
}
}
}
3 changes: 3 additions & 0 deletions Tests/Runtime/LevelStructureTests.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions Tests/Runtime/Scenes/LevelStructure.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit b1777ee

Please sign in to comment.