From 99c70886a570fd4532973bc1f4576522150304fa Mon Sep 17 00:00:00 2001 From: Konstantin Dyachenko Date: Sun, 29 Mar 2026 02:26:31 +0700 Subject: [PATCH] WorldGen --- Assets/Editor.meta | 8 + Assets/Editor/WorldGeneratorEditorWindow.cs | 315 ++++++++++ .../Editor/WorldGeneratorEditorWindow.cs.meta | 2 + Assets/Scenes/SampleScene.unity | 323 ++++++++-- Assets/Scripts.meta | 8 + Assets/Scripts/Player.meta | 8 + Assets/Scripts/Player/CameraFollow2D.cs | 31 + Assets/Scripts/Player/CameraFollow2D.cs.meta | 2 + .../Scripts/Player/SimplePlayerInputMover.cs | 109 ++++ .../Player/SimplePlayerInputMover.cs.meta | 2 + Assets/Scripts/WorldGen.meta | 8 + Assets/Scripts/WorldGen/ChunkTemplate.cs | 232 +++++++ Assets/Scripts/WorldGen/ChunkTemplate.cs.meta | 2 + .../WorldGen/InfiniteWorldGenerator.cs | 582 ++++++++++++++++++ .../WorldGen/InfiniteWorldGenerator.cs.meta | 2 + Assets/Scripts/WorldGen/ProceduralWorldArt.cs | 350 +++++++++++ .../WorldGen/ProceduralWorldArt.cs.meta | 2 + .../WorldGen/RuntimeWorldProfileFactory.cs | 237 +++++++ .../RuntimeWorldProfileFactory.cs.meta | 2 + .../Scripts/WorldGen/WorldAutotileProfile.cs | 126 ++++ .../WorldGen/WorldAutotileProfile.cs.meta | 2 + ProjectSettings/SceneTemplateSettings.json | 121 ++++ 22 files changed, 2430 insertions(+), 44 deletions(-) create mode 100644 Assets/Editor.meta create mode 100644 Assets/Editor/WorldGeneratorEditorWindow.cs create mode 100644 Assets/Editor/WorldGeneratorEditorWindow.cs.meta create mode 100644 Assets/Scripts.meta create mode 100644 Assets/Scripts/Player.meta create mode 100644 Assets/Scripts/Player/CameraFollow2D.cs create mode 100644 Assets/Scripts/Player/CameraFollow2D.cs.meta create mode 100644 Assets/Scripts/Player/SimplePlayerInputMover.cs create mode 100644 Assets/Scripts/Player/SimplePlayerInputMover.cs.meta create mode 100644 Assets/Scripts/WorldGen.meta create mode 100644 Assets/Scripts/WorldGen/ChunkTemplate.cs create mode 100644 Assets/Scripts/WorldGen/ChunkTemplate.cs.meta create mode 100644 Assets/Scripts/WorldGen/InfiniteWorldGenerator.cs create mode 100644 Assets/Scripts/WorldGen/InfiniteWorldGenerator.cs.meta create mode 100644 Assets/Scripts/WorldGen/ProceduralWorldArt.cs create mode 100644 Assets/Scripts/WorldGen/ProceduralWorldArt.cs.meta create mode 100644 Assets/Scripts/WorldGen/RuntimeWorldProfileFactory.cs create mode 100644 Assets/Scripts/WorldGen/RuntimeWorldProfileFactory.cs.meta create mode 100644 Assets/Scripts/WorldGen/WorldAutotileProfile.cs create mode 100644 Assets/Scripts/WorldGen/WorldAutotileProfile.cs.meta create mode 100644 ProjectSettings/SceneTemplateSettings.json diff --git a/Assets/Editor.meta b/Assets/Editor.meta new file mode 100644 index 00000000..54d83955 --- /dev/null +++ b/Assets/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8028fe033c76d4b4e82877336273ad43 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Editor/WorldGeneratorEditorWindow.cs b/Assets/Editor/WorldGeneratorEditorWindow.cs new file mode 100644 index 00000000..a2b2bdda --- /dev/null +++ b/Assets/Editor/WorldGeneratorEditorWindow.cs @@ -0,0 +1,315 @@ +using UnityEditor; +using UnityEngine; + +namespace InfiniteWorld.Editor +{ + public class WorldGeneratorEditorWindow : EditorWindow + { + private enum BrushMode + { + Erase, + Wall, + Environment + } + + private WorldAutotileProfile profile; + private ChunkTemplate template; + private SerializedObject serializedProfile; + private Vector2 profileScroll; + private Vector2 chunkScroll; + private BrushMode brushMode = BrushMode.Wall; + private bool isPainting; + private int pendingWidth = 16; + private int pendingHeight = 16; + + [MenuItem("Tools/Infinite World/World Builder")] + public static void Open() + { + GetWindow("World Builder"); + } + + private void OnGUI() + { + DrawToolbar(); + EditorGUILayout.Space(6f); + + EditorGUILayout.BeginHorizontal(); + DrawChunkEditor(); + GUILayout.Space(8f); + DrawProfileEditor(); + EditorGUILayout.EndHorizontal(); + } + + private void DrawToolbar() + { + EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); + template = (ChunkTemplate)EditorGUILayout.ObjectField(template, typeof(ChunkTemplate), false, GUILayout.Width(position.width * 0.38f)); + profile = (WorldAutotileProfile)EditorGUILayout.ObjectField(profile, typeof(WorldAutotileProfile), false, GUILayout.Width(position.width * 0.38f)); + + if (GUILayout.Button("New Chunk", EditorStyles.toolbarButton, GUILayout.Width(80f))) + { + CreateChunkAsset(); + } + + if (GUILayout.Button("New Profile", EditorStyles.toolbarButton, GUILayout.Width(80f))) + { + CreateProfileAsset(); + } + + EditorGUILayout.EndHorizontal(); + } + + private void DrawChunkEditor() + { + EditorGUILayout.BeginVertical(GUILayout.Width(position.width * 0.52f)); + EditorGUILayout.LabelField("Chunk Painter", EditorStyles.boldLabel); + + if (template == null) + { + EditorGUILayout.HelpBox("Create or assign a ChunkTemplate asset. Paint walls and environment directly on the chunk grid and mark exits on each side.", MessageType.Info); + EditorGUILayout.EndVertical(); + return; + } + + template.EnsureCellData(); + pendingWidth = EditorGUILayout.IntField("Width", pendingWidth == 0 ? template.width : pendingWidth); + pendingHeight = EditorGUILayout.IntField("Height", pendingHeight == 0 ? template.height : pendingHeight); + + EditorGUILayout.BeginHorizontal(); + if (GUILayout.Button("Resize")) + { + Undo.RecordObject(template, "Resize Chunk Template"); + template.Resize(pendingWidth, pendingHeight); + EditorUtility.SetDirty(template); + } + if (GUILayout.Button("Clear")) + { + Undo.RecordObject(template, "Clear Chunk Template"); + template.Clear(); + EditorUtility.SetDirty(template); + } + if (GUILayout.Button("Border From Exits")) + { + Undo.RecordObject(template, "Apply Border Walls"); + template.ApplyBorderWallsFromExits(3); + EditorUtility.SetDirty(template); + } + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(4f); + EditorGUILayout.LabelField("Brush", EditorStyles.miniBoldLabel); + brushMode = (BrushMode)GUILayout.Toolbar((int)brushMode, new[] { "Erase", "Wall", "Environment" }); + + EditorGUILayout.Space(4f); + EditorGUILayout.LabelField("Exits", EditorStyles.miniBoldLabel); + EditorGUILayout.BeginHorizontal(); + DrawExitToggle("Top", ChunkExit.Top); + DrawExitToggle("Right", ChunkExit.Right); + DrawExitToggle("Bottom", ChunkExit.Bottom); + DrawExitToggle("Left", ChunkExit.Left); + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(8f); + chunkScroll = EditorGUILayout.BeginScrollView(chunkScroll, GUILayout.ExpandHeight(true)); + Rect gridRect = GUILayoutUtility.GetRect(template.width * 24f, template.height * 24f, GUILayout.ExpandWidth(false), GUILayout.ExpandHeight(false)); + DrawGrid(gridRect); + EditorGUILayout.EndScrollView(); + + EditorGUILayout.HelpBox("Gray = floor, brown = wall, green = environment. Drag with the selected brush to paint the chunk visually.", MessageType.None); + EditorGUILayout.EndVertical(); + } + + private void DrawProfileEditor() + { + EditorGUILayout.BeginVertical(GUILayout.ExpandWidth(true)); + EditorGUILayout.LabelField("Tile Profile", EditorStyles.boldLabel); + + if (profile == null) + { + EditorGUILayout.HelpBox("Assign a WorldAutotileProfile to map real tiles onto ground, walls, and environment.", MessageType.Info); + EditorGUILayout.EndVertical(); + return; + } + + if (serializedProfile == null || serializedProfile.targetObject != profile) + { + serializedProfile = new SerializedObject(profile); + } + + serializedProfile.Update(); + profileScroll = EditorGUILayout.BeginScrollView(profileScroll); + + EditorGUILayout.PropertyField(serializedProfile.FindProperty("baseGroundTile")); + EditorGUILayout.Space(6f); + EditorGUILayout.LabelField("Wall Autotile", EditorStyles.miniBoldLabel); + SerializedProperty walls = serializedProfile.FindProperty("wallTiles"); + DrawWallGrid(walls); + + EditorGUILayout.Space(6f); + EditorGUILayout.PropertyField(serializedProfile.FindProperty("environmentTiles"), true); + + EditorGUILayout.EndScrollView(); + serializedProfile.ApplyModifiedProperties(); + if (GUI.changed) + { + EditorUtility.SetDirty(profile); + } + EditorGUILayout.EndVertical(); + } + + private void DrawWallGrid(SerializedProperty walls) + { + DrawTriple(walls, "outerTopLeft", "top", "outerTopRight"); + DrawTriple(walls, "left", "center", "right"); + DrawTriple(walls, "outerBottomLeft", "bottom", "outerBottomRight"); + DrawDouble(walls, "innerTopLeft", "innerTopRight"); + DrawDouble(walls, "innerBottomLeft", "innerBottomRight"); + } + + private void DrawTriple(SerializedProperty root, string a, string b, string c) + { + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.PropertyField(root.FindPropertyRelative(a), GUIContent.none); + EditorGUILayout.PropertyField(root.FindPropertyRelative(b), GUIContent.none); + EditorGUILayout.PropertyField(root.FindPropertyRelative(c), GUIContent.none); + EditorGUILayout.EndHorizontal(); + } + + private void DrawDouble(SerializedProperty root, string a, string b) + { + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.PropertyField(root.FindPropertyRelative(a), GUIContent.none); + EditorGUILayout.PropertyField(root.FindPropertyRelative(b), GUIContent.none); + EditorGUILayout.EndHorizontal(); + } + + private void DrawExitToggle(string label, ChunkExit exit) + { + bool current = template.GetExit(exit); + bool next = GUILayout.Toggle(current, label, "Button"); + if (next == current) + { + return; + } + + Undo.RecordObject(template, "Toggle Chunk Exit"); + switch (exit) + { + case ChunkExit.Top: + template.exitTop = next; + break; + case ChunkExit.Right: + template.exitRight = next; + break; + case ChunkExit.Bottom: + template.exitBottom = next; + break; + case ChunkExit.Left: + template.exitLeft = next; + break; + } + EditorUtility.SetDirty(template); + } + + private void DrawGrid(Rect rect) + { + const float cellSize = 24f; + Event evt = Event.current; + + for (int y = 0; y < template.height; y++) + { + for (int x = 0; x < template.width; x++) + { + Rect cellRect = new Rect(rect.x + x * cellSize, rect.y + (template.height - 1 - y) * cellSize, cellSize - 1f, cellSize - 1f); + EditorGUI.DrawRect(cellRect, GetCellColor(x, y)); + + if ((evt.type == EventType.MouseDown || evt.type == EventType.MouseDrag) && cellRect.Contains(evt.mousePosition) && evt.button == 0) + { + isPainting = true; + PaintCell(x, y); + evt.Use(); + } + } + } + + if (evt.type == EventType.MouseUp) + { + isPainting = false; + } + + if (isPainting) + { + Repaint(); + } + } + + private Color GetCellColor(int x, int y) + { + if (template.GetWall(x, y)) + { + return new Color(0.52f, 0.36f, 0.22f, 1f); + } + + if (template.GetEnvironment(x, y)) + { + return new Color(0.2f, 0.54f, 0.25f, 1f); + } + + return new Color(0.4f, 0.4f, 0.4f, 1f); + } + + private void PaintCell(int x, int y) + { + Undo.RecordObject(template, "Paint Chunk Template"); + switch (brushMode) + { + case BrushMode.Erase: + template.SetWall(x, y, false); + template.SetEnvironment(x, y, false); + break; + case BrushMode.Wall: + template.SetWall(x, y, true); + break; + case BrushMode.Environment: + template.SetEnvironment(x, y, true); + break; + } + EditorUtility.SetDirty(template); + } + + private void CreateChunkAsset() + { + string path = EditorUtility.SaveFilePanelInProject("Create Chunk Template", "ChunkTemplate", "asset", "Choose where to save the chunk template."); + if (string.IsNullOrEmpty(path)) + { + return; + } + + ChunkTemplate asset = CreateInstance(); + asset.Resize(16, 16); + asset.ApplyBorderWallsFromExits(3); + AssetDatabase.CreateAsset(asset, path); + AssetDatabase.SaveAssets(); + template = asset; + pendingWidth = asset.width; + pendingHeight = asset.height; + Selection.activeObject = asset; + } + + private void CreateProfileAsset() + { + string path = EditorUtility.SaveFilePanelInProject("Create World Profile", "WorldAutotileProfile", "asset", "Choose where to save the tile profile."); + if (string.IsNullOrEmpty(path)) + { + return; + } + + WorldAutotileProfile asset = CreateInstance(); + AssetDatabase.CreateAsset(asset, path); + AssetDatabase.SaveAssets(); + profile = asset; + serializedProfile = new SerializedObject(profile); + Selection.activeObject = asset; + } + } +} diff --git a/Assets/Editor/WorldGeneratorEditorWindow.cs.meta b/Assets/Editor/WorldGeneratorEditorWindow.cs.meta new file mode 100644 index 00000000..f9479991 --- /dev/null +++ b/Assets/Editor/WorldGeneratorEditorWindow.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: ff365d4e73b85cc4987c7984d5f8c7cb \ No newline at end of file diff --git a/Assets/Scenes/SampleScene.unity b/Assets/Scenes/SampleScene.unity index 7b420f6e..a92279bf 100644 --- a/Assets/Scenes/SampleScene.unity +++ b/Assets/Scenes/SampleScene.unity @@ -13,7 +13,7 @@ OcclusionCullingSettings: --- !u!104 &2 RenderSettings: m_ObjectHideFlags: 0 - serializedVersion: 9 + serializedVersion: 10 m_Fog: 0 m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} m_FogMode: 3 @@ -38,13 +38,12 @@ RenderSettings: m_ReflectionIntensity: 1 m_CustomReflection: {fileID: 0} m_Sun: {fileID: 0} - m_IndirectSpecularColor: {r: 0, g: 0, b: 0, a: 1} m_UseRadianceAmbientProbe: 0 --- !u!157 &3 LightmapSettings: m_ObjectHideFlags: 0 - serializedVersion: 12 - m_GIWorkflowMode: 1 + serializedVersion: 13 + m_BakeOnSceneLoad: 0 m_GISettings: serializedVersion: 2 m_BounceScale: 1 @@ -67,9 +66,6 @@ LightmapSettings: m_LightmapParameters: {fileID: 0} m_LightmapsBakeMode: 1 m_TextureCompression: 1 - m_FinalGather: 0 - m_FinalGatherFiltering: 1 - m_FinalGatherRayCount: 256 m_ReflectionCompression: 2 m_MixedBakeMode: 2 m_BakeBackend: 1 @@ -104,7 +100,7 @@ NavMeshSettings: serializedVersion: 2 m_ObjectHideFlags: 0 m_BuildSettings: - serializedVersion: 2 + serializedVersion: 3 agentTypeID: 0 agentRadius: 0.5 agentHeight: 2 @@ -117,7 +113,7 @@ NavMeshSettings: cellSize: 0.16666667 manualTileSize: 0 tileSize: 256 - accuratePlacement: 0 + buildHeightMesh: 0 maxJobWorkers: 0 preserveTilesOutsideBounds: 0 debug: @@ -135,6 +131,7 @@ GameObject: - component: {fileID: 519420031} - component: {fileID: 519420029} - component: {fileID: 519420030} + - component: {fileID: 519420033} m_Layer: 0 m_Name: Main Camera m_TagString: MainCamera @@ -172,6 +169,7 @@ MonoBehaviour: serializedVersion: 2 m_Bits: 1 m_VolumeTrigger: {fileID: 0} + m_VolumeFrameworkUpdateModeOption: 2 m_RenderPostProcessing: 0 m_Antialiasing: 0 m_AntialiasingQuality: 2 @@ -179,8 +177,19 @@ MonoBehaviour: m_Dithering: 0 m_ClearDepth: 1 m_AllowXRRendering: 1 + m_AllowHDROutput: 1 + m_UseScreenCoordOverride: 0 + m_ScreenSizeOverride: {x: 0, y: 0, z: 0, w: 0} + m_ScreenCoordScaleBias: {x: 0, y: 0, z: 0, w: 0} m_RequiresDepthTexture: 0 m_RequiresColorTexture: 0 + m_TaaSettings: + m_Quality: 3 + m_FrameInfluence: 0.1 + m_JitterScale: 1 + m_MipBias: 0 + m_VarianceClampScale: 0.9 + m_ContrastAdaptiveSharpening: 0 m_Version: 2 --- !u!20 &519420031 Camera: @@ -196,9 +205,17 @@ Camera: m_projectionMatrixMode: 1 m_GateFitMode: 2 m_FOVAxisMode: 0 + m_Iso: 200 + m_ShutterSpeed: 0.005 + m_Aperture: 16 + m_FocusDistance: 10 + m_FocalLength: 50 + m_BladeCount: 5 + m_Curvature: {x: 2, y: 11} + m_BarrelClipping: 0.25 + m_Anamorphism: 0 m_SensorSize: {x: 36, y: 24} m_LensShift: {x: 0, y: 0} - m_FocalLength: 50 m_NormalizedViewPortRect: serializedVersion: 2 x: 0 @@ -232,14 +249,29 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 519420028} + serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: -10} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} - m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &519420033 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 519420028} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 98ee4fb5b3ebf80478e6e25afa8fd337, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::InfiniteWorld.CameraFollow2D + target: {fileID: 0} + smoothTime: 0.18 + offset: {x: 0, y: 0, z: -10} --- !u!1 &619394800 GameObject: m_ObjectHideFlags: 0 @@ -269,55 +301,29 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 073797afb82c5a1438f328866b10b3f0, type: 3} m_Name: m_EditorClassIdentifier: - m_ComponentVersion: 1 + m_ComponentVersion: 2 m_LightType: 4 m_BlendStyleIndex: 0 m_FalloffIntensity: 0.5 m_Color: {r: 1, g: 1, b: 1, a: 1} m_Intensity: 1 m_LightVolumeIntensity: 1 - m_LightVolumeIntensityEnabled: 0 + m_LightVolumeEnabled: 0 m_ApplyToSortingLayers: 00000000 m_LightCookieSprite: {fileID: 0} m_DeprecatedPointLightCookieSprite: {fileID: 0} m_LightOrder: 0 + m_AlphaBlendOnOverlap: 0 m_OverlapOperation: 0 m_NormalMapDistance: 3 m_NormalMapQuality: 2 m_UseNormalMap: 0 - m_ShadowIntensityEnabled: 0 + m_ShadowsEnabled: 0 m_ShadowIntensity: 0.75 + m_ShadowSoftness: 0 + m_ShadowSoftnessFalloffIntensity: 0.5 m_ShadowVolumeIntensityEnabled: 0 m_ShadowVolumeIntensity: 0.75 - m_Vertices: - - position: {x: 0.9985302, y: 0.9985302, z: 0} - color: {r: 0.70710677, g: 0.70710677, b: 0, a: 0} - uv: {x: 0, y: 0} - - position: {x: 0.9985302, y: 0.9985302, z: 0} - color: {r: 0, g: 0, b: 0, a: 1} - uv: {x: 0, y: 0} - - position: {x: -0.9985302, y: 0.9985302, z: 0} - color: {r: -0.70710677, g: 0.70710677, b: 0, a: 0} - uv: {x: 0, y: 0} - - position: {x: -0.9985302, y: 0.9985302, z: 0} - color: {r: 0, g: 0, b: 0, a: 1} - uv: {x: 0, y: 0} - - position: {x: -0.99853003, y: -0.9985304, z: 0} - color: {r: -0.70710665, g: -0.7071069, b: 0, a: 0} - uv: {x: 0, y: 0} - - position: {x: -0.99853003, y: -0.9985304, z: 0} - color: {r: 0, g: 0, b: 0, a: 1} - uv: {x: 0, y: 0} - - position: {x: 0.99853003, y: -0.9985304, z: 0} - color: {r: 0.70710665, g: -0.7071069, b: 0, a: 0} - uv: {x: 0, y: 0} - - position: {x: 0.99853003, y: -0.9985304, z: 0} - color: {r: 0, g: 0, b: 0, a: 1} - uv: {x: 0, y: 0} - - position: {x: 0, y: 0, z: 0} - color: {r: 0, g: 0, b: 0, a: 1} - uv: {x: 0, y: 0} - m_Triangles: 030001000800020000000100030002000100050003000800040002000300050004000300070005000800060004000500070006000500010007000800000006000700010000000700 m_LocalBounds: m_Center: {x: 0, y: -0.00000011920929, z: 0} m_Extent: {x: 0.9985302, y: 0.99853027, z: 0} @@ -342,11 +348,240 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 619394800} + serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} - m_RootOrder: 1 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &669282973 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 669282975} + - component: {fileID: 669282974} + - component: {fileID: 669282978} + - component: {fileID: 669282977} + - component: {fileID: 669282976} + m_Layer: 0 + m_Name: Player + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &669282974 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 669282973} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: c9187e81c6ec8da4599e04a1694ec18b, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::InfiniteWorld.SimplePlayerInputMover + moveSpeed: 5 +--- !u!4 &669282975 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 669282973} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!70 &669282976 +CapsuleCollider2D: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 669282973} + m_Enabled: 1 + serializedVersion: 3 + m_Density: 1 + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_ForceSendLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_ForceReceiveLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_ContactCaptureLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_CallbackLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_IsTrigger: 0 + m_UsedByEffector: 0 + m_CompositeOperation: 0 + m_CompositeOrder: 0 + m_Offset: {x: 0, y: 0} + m_Size: {x: 0.5, y: 1} + m_Direction: 0 +--- !u!212 &669282977 +SpriteRenderer: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 669282973} + m_Enabled: 1 + m_CastShadows: 0 + m_ReceiveShadows: 0 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 0 + m_RayTraceProcedural: 0 + m_RayTracingAccelStructBuildFlagsOverride: 0 + m_RayTracingAccelStructBuildFlags: 1 + m_SmallMeshCulling: 1 + m_ForceMeshLod: -1 + m_MeshLodSelectionBias: 0 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: a97c105638bdf8b4a8650670310a4cd3, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 0 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_GlobalIlluminationMeshLod: 0 + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 + m_MaskInteraction: 0 + m_Sprite: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_FlipX: 0 + m_FlipY: 0 + m_DrawMode: 0 + m_Size: {x: 1, y: 1} + m_AdaptiveModeThreshold: 0.5 + m_SpriteTileMode: 0 + m_WasSpriteAssigned: 0 + m_SpriteSortPoint: 0 +--- !u!50 &669282978 +Rigidbody2D: + serializedVersion: 5 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 669282973} + m_BodyType: 0 + m_Simulated: 1 + m_UseFullKinematicContacts: 0 + m_UseAutoMass: 0 + m_Mass: 1 + m_LinearDamping: 0 + m_AngularDamping: 0.05 + m_GravityScale: 1 + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_Interpolate: 0 + m_SleepingMode: 1 + m_CollisionDetection: 0 + m_Constraints: 0 +--- !u!1 &947604398 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 947604400} + - component: {fileID: 947604399} + m_Layer: 0 + m_Name: World + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &947604399 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 947604398} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 6bacd3a0ac13e6f4a94548426dd89ebb, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::InfiniteWorld.InfiniteWorldGenerator + player: {fileID: 0} + profile: {fileID: 0} + templates: [] + chunkSize: 16 + generationRadius: 2 + seed: 12345 +--- !u!4 &947604400 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 947604398} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1660057539 &9223372036854775807 +SceneRoots: + m_ObjectHideFlags: 0 + m_Roots: + - {fileID: 519420032} + - {fileID: 619394802} + - {fileID: 947604400} + - {fileID: 669282975} diff --git a/Assets/Scripts.meta b/Assets/Scripts.meta new file mode 100644 index 00000000..38cd2ae6 --- /dev/null +++ b/Assets/Scripts.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0444b525419aa14429d7e31a7e4b7fef +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Player.meta b/Assets/Scripts/Player.meta new file mode 100644 index 00000000..71c3f357 --- /dev/null +++ b/Assets/Scripts/Player.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ff30f393052d2674e94fd1d887712090 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Player/CameraFollow2D.cs b/Assets/Scripts/Player/CameraFollow2D.cs new file mode 100644 index 00000000..57824659 --- /dev/null +++ b/Assets/Scripts/Player/CameraFollow2D.cs @@ -0,0 +1,31 @@ +using UnityEngine; + +namespace InfiniteWorld +{ + public class CameraFollow2D : MonoBehaviour + { + [SerializeField] private Transform target; + [SerializeField] private float smoothTime = 0.18f; + [SerializeField] private Vector3 offset = new Vector3(0f, 0f, -10f); + + private Vector3 velocity; + + private void LateUpdate() + { + if (target == null) + { + SimplePlayerInputMover player = FindFirstObjectByType(); + if (player == null) + { + return; + } + + target = player.transform; + } + + Vector3 desiredPosition = target.position + offset; + desiredPosition.z = offset.z; + transform.position = Vector3.SmoothDamp(transform.position, desiredPosition, ref velocity, smoothTime); + } + } +} diff --git a/Assets/Scripts/Player/CameraFollow2D.cs.meta b/Assets/Scripts/Player/CameraFollow2D.cs.meta new file mode 100644 index 00000000..154140ba --- /dev/null +++ b/Assets/Scripts/Player/CameraFollow2D.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 98ee4fb5b3ebf80478e6e25afa8fd337 \ No newline at end of file diff --git a/Assets/Scripts/Player/SimplePlayerInputMover.cs b/Assets/Scripts/Player/SimplePlayerInputMover.cs new file mode 100644 index 00000000..b638fb69 --- /dev/null +++ b/Assets/Scripts/Player/SimplePlayerInputMover.cs @@ -0,0 +1,109 @@ +using UnityEngine; +using UnityEngine.InputSystem; + +namespace InfiniteWorld +{ + public class SimplePlayerInputMover : MonoBehaviour + { + [SerializeField] private float moveSpeed = 5f; + + private InputAction moveAction; + private Rigidbody2D rb; + private Vector2 moveInput; + + private void Awake() + { + rb = GetComponent(); + if (rb == null) + { + rb = gameObject.AddComponent(); + } + + ConfigurePhysics(); + EnsureVisual(); + + moveAction = new InputAction("Move", InputActionType.Value); + moveAction.AddCompositeBinding("2DVector") + .With("Up", "/w") + .With("Down", "/s") + .With("Left", "/a") + .With("Right", "/d"); + moveAction.AddCompositeBinding("2DVector") + .With("Up", "/upArrow") + .With("Down", "/downArrow") + .With("Left", "/leftArrow") + .With("Right", "/rightArrow"); + moveAction.AddBinding("/leftStick"); + } + + private void OnEnable() + { + moveAction?.Enable(); + } + + private void OnDisable() + { + moveAction?.Disable(); + } + + private void OnDestroy() + { + moveAction?.Dispose(); + } + + private void Update() + { + if (moveAction == null) + { + return; + } + + moveInput = moveAction.ReadValue().normalized; + if (moveInput.x != 0f) + { + Vector3 scale = transform.localScale; + scale.x = Mathf.Abs(scale.x) * Mathf.Sign(moveInput.x); + transform.localScale = scale; + } + } + + private void FixedUpdate() + { + if (rb == null) + { + return; + } + + Vector2 target = rb.position + moveInput * (moveSpeed * Time.fixedDeltaTime); + rb.MovePosition(target); + } + + private void ConfigurePhysics() + { + rb.gravityScale = 0f; + rb.freezeRotation = true; + rb.interpolation = RigidbodyInterpolation2D.Interpolate; + rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous; + + CapsuleCollider2D collider = GetComponent(); + if (collider == null) + { + collider = gameObject.AddComponent(); + } + collider.direction = CapsuleDirection2D.Vertical; + collider.size = new Vector2(0.55f, 0.8f); + collider.offset = new Vector2(0f, -0.05f); + } + + private void EnsureVisual() + { + SpriteRenderer renderer = GetComponent(); + if (renderer == null) + { + renderer = gameObject.AddComponent(); + } + renderer.sprite = ProceduralWorldArt.CreatePlayerSprite(); + renderer.sortingOrder = 10; + } + } +} diff --git a/Assets/Scripts/Player/SimplePlayerInputMover.cs.meta b/Assets/Scripts/Player/SimplePlayerInputMover.cs.meta new file mode 100644 index 00000000..fcadd7da --- /dev/null +++ b/Assets/Scripts/Player/SimplePlayerInputMover.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c9187e81c6ec8da4599e04a1694ec18b \ No newline at end of file diff --git a/Assets/Scripts/WorldGen.meta b/Assets/Scripts/WorldGen.meta new file mode 100644 index 00000000..3360bd87 --- /dev/null +++ b/Assets/Scripts/WorldGen.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 96adefd3df6641c40b9624eb12185ea4 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/WorldGen/ChunkTemplate.cs b/Assets/Scripts/WorldGen/ChunkTemplate.cs new file mode 100644 index 00000000..b3fd162c --- /dev/null +++ b/Assets/Scripts/WorldGen/ChunkTemplate.cs @@ -0,0 +1,232 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace InfiniteWorld +{ + [CreateAssetMenu(menuName = "Infinite World/Chunk Template", fileName = "ChunkTemplate")] + public class ChunkTemplate : ScriptableObject + { + [Min(4)] public int width = 16; + [Min(4)] public int height = 16; + + [Header("Exits")] + public bool exitTop = true; + public bool exitRight = true; + public bool exitBottom = true; + public bool exitLeft = true; + + [SerializeField] private List cells = new List(); + + public int CellCount => width * height; + + public void EnsureCellData() + { + int target = Mathf.Max(1, width * height); + while (cells.Count < target) + { + cells.Add(new ChunkCellData()); + } + + while (cells.Count > target) + { + cells.RemoveAt(cells.Count - 1); + } + } + + public void Resize(int newWidth, int newHeight) + { + newWidth = Mathf.Max(4, newWidth); + newHeight = Mathf.Max(4, newHeight); + + List oldCells = new List(cells); + int oldWidth = width; + int oldHeight = height; + + width = newWidth; + height = newHeight; + cells = new List(newWidth * newHeight); + + for (int i = 0; i < newWidth * newHeight; i++) + { + cells.Add(new ChunkCellData()); + } + + for (int y = 0; y < Mathf.Min(oldHeight, newHeight); y++) + { + for (int x = 0; x < Mathf.Min(oldWidth, newWidth); x++) + { + int oldIndex = y * oldWidth + x; + if (oldIndex < oldCells.Count) + { + cells[Index(x, y)] = oldCells[oldIndex]; + } + } + } + } + + public void Clear() + { + EnsureCellData(); + for (int i = 0; i < cells.Count; i++) + { + cells[i] = new ChunkCellData(); + } + } + + public bool GetWall(int x, int y) + { + return IsInBounds(x, y) && cells[Index(x, y)].wall; + } + + public bool GetEnvironment(int x, int y) + { + return IsInBounds(x, y) && cells[Index(x, y)].environment; + } + + public void SetWall(int x, int y, bool value) + { + if (!IsInBounds(x, y)) + { + return; + } + + EnsureCellData(); + ChunkCellData data = cells[Index(x, y)]; + data.wall = value; + if (value) + { + data.environment = false; + } + cells[Index(x, y)] = data; + } + + public void SetEnvironment(int x, int y, bool value) + { + if (!IsInBounds(x, y)) + { + return; + } + + EnsureCellData(); + ChunkCellData data = cells[Index(x, y)]; + data.environment = value; + if (value) + { + data.wall = false; + } + cells[Index(x, y)] = data; + } + + public bool GetExit(ChunkExit exit) + { + return exit switch + { + ChunkExit.Top => exitTop, + ChunkExit.Right => exitRight, + ChunkExit.Bottom => exitBottom, + ChunkExit.Left => exitLeft, + _ => false + }; + } + + public int ExitCount() + { + int count = 0; + count += exitTop ? 1 : 0; + count += exitRight ? 1 : 0; + count += exitBottom ? 1 : 0; + count += exitLeft ? 1 : 0; + return count; + } + + public void ApplyBorderWallsFromExits(int openingWidth = 3) + { + EnsureCellData(); + Clear(); + + for (int x = 0; x < width; x++) + { + SetWall(x, 0, true); + SetWall(x, height - 1, true); + } + + for (int y = 0; y < height; y++) + { + SetWall(0, y, true); + SetWall(width - 1, y, true); + } + + CarveExit(ChunkExit.Top, openingWidth); + CarveExit(ChunkExit.Right, openingWidth); + CarveExit(ChunkExit.Bottom, openingWidth); + CarveExit(ChunkExit.Left, openingWidth); + } + + public void CarveExit(ChunkExit exit, int openingWidth = 3) + { + if (!GetExit(exit)) + { + return; + } + + int half = Mathf.Max(1, openingWidth) / 2; + switch (exit) + { + case ChunkExit.Top: + for (int x = width / 2 - half; x <= width / 2 + half; x++) + { + SetWall(x, height - 1, false); + SetWall(x, height - 2, false); + } + break; + case ChunkExit.Right: + for (int y = height / 2 - half; y <= height / 2 + half; y++) + { + SetWall(width - 1, y, false); + SetWall(width - 2, y, false); + } + break; + case ChunkExit.Bottom: + for (int x = width / 2 - half; x <= width / 2 + half; x++) + { + SetWall(x, 0, false); + SetWall(x, 1, false); + } + break; + case ChunkExit.Left: + for (int y = height / 2 - half; y <= height / 2 + half; y++) + { + SetWall(0, y, false); + SetWall(1, y, false); + } + break; + } + } + + private int Index(int x, int y) + { + return y * width + x; + } + + private bool IsInBounds(int x, int y) + { + return x >= 0 && y >= 0 && x < width && y < height; + } + } + + public enum ChunkExit + { + Top, + Right, + Bottom, + Left + } + + [Serializable] + public struct ChunkCellData + { + public bool wall; + public bool environment; + } +} diff --git a/Assets/Scripts/WorldGen/ChunkTemplate.cs.meta b/Assets/Scripts/WorldGen/ChunkTemplate.cs.meta new file mode 100644 index 00000000..a7711bd3 --- /dev/null +++ b/Assets/Scripts/WorldGen/ChunkTemplate.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: bae8eeae2da7d3f4396883671b297a47 \ No newline at end of file diff --git a/Assets/Scripts/WorldGen/InfiniteWorldGenerator.cs b/Assets/Scripts/WorldGen/InfiniteWorldGenerator.cs new file mode 100644 index 00000000..a9fea258 --- /dev/null +++ b/Assets/Scripts/WorldGen/InfiniteWorldGenerator.cs @@ -0,0 +1,582 @@ +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.Tilemaps; + +namespace InfiniteWorld +{ + public class InfiniteWorldGenerator : MonoBehaviour + { + [Header("References")] + [SerializeField] private Transform player; + [SerializeField] private WorldAutotileProfile profile; + + [Header("Chunk Settings")] + [SerializeField] private int chunkSize = 16; + [SerializeField] private int generationRadius = 2; + [SerializeField] private int seed = 12345; + + [Header("Rock Noise")] + [SerializeField] private float macroNoiseScale = 0.05f; + [SerializeField] private float detailNoiseScale = 0.12f; + [SerializeField] private float ridgeNoiseScale = 0.18f; + [SerializeField] private float wallThreshold = 0.6f; + [SerializeField] private float rockBias = 0.04f; + [SerializeField] private int smoothingPasses = 2; + + [Header("Global Passes")] + [SerializeField] private float passNoiseScale = 0.018f; + [SerializeField] private float passDetailScale = 0.041f; + [SerializeField] private float passThreshold = 0.22f; + [SerializeField] private float passFeather = 0.12f; + + [Header("Environment")] + [SerializeField] private float environmentNoiseScale = 0.19f; + [SerializeField] private float environmentThreshold = 0.7f; + + private readonly Dictionary chunks = new Dictionary(); + private Grid grid; + private Transform chunkRoot; + private Vector2Int lastGeneratedCenter = new Vector2Int(int.MinValue, int.MinValue); + private WorldAutotileProfile runtimeFallbackProfile; + + private void Awake() + { + EnsureSceneInfrastructure(); + EnsureRuntimeData(); + TryFindPlayer(); + } + + private void Update() + { + if (player == null && !TryFindPlayer()) + { + return; + } + + Vector2Int playerChunk = WorldToChunk(player.position); + if (playerChunk == lastGeneratedCenter) + { + return; + } + + lastGeneratedCenter = playerChunk; + GenerateAround(playerChunk); + } + + private void EnsureSceneInfrastructure() + { + grid = GetComponentInChildren(); + if (grid == null) + { + GameObject gridObject = new GameObject("Grid", typeof(Grid)); + gridObject.transform.SetParent(transform, false); + grid = gridObject.GetComponent(); + } + + Transform existingChunkRoot = grid.transform.Find("Chunks"); + if (existingChunkRoot == null) + { + GameObject root = new GameObject("Chunks"); + root.transform.SetParent(grid.transform, false); + chunkRoot = root.transform; + } + else + { + chunkRoot = existingChunkRoot; + } + } + + private void EnsureRuntimeData() + { + if (profile == null || !profile.HasAnyAssignedTiles()) + { + runtimeFallbackProfile = RuntimeWorldProfileFactory.CreateFallbackProfile(); + } + } + + private bool TryFindPlayer() + { + if (player != null) + { + return true; + } + + SimplePlayerInputMover mover = FindFirstObjectByType(); + if (mover == null) + { + return false; + } + + player = mover.transform; + return true; + } + + private void GenerateAround(Vector2Int centerChunk) + { + for (int y = -generationRadius; y <= generationRadius; y++) + { + for (int x = -generationRadius; x <= generationRadius; x++) + { + Vector2Int coord = new Vector2Int(centerChunk.x + x, centerChunk.y + y); + if (chunks.ContainsKey(coord)) + { + continue; + } + + GeneratedChunk chunk = CreateChunk(coord); + chunks.Add(coord, chunk); + BuildChunkData(coord, chunk); + RenderChunk(coord); + RefreshNeighborBorders(coord); + } + } + } + + private GeneratedChunk CreateChunk(Vector2Int coord) + { + GameObject chunkObject = new GameObject($"Chunk_{coord.x}_{coord.y}"); + chunkObject.transform.SetParent(chunkRoot, false); + chunkObject.transform.localPosition = new Vector3(coord.x * chunkSize, coord.y * chunkSize, 0f); + + Tilemap ground = CreateTilemap("Ground", chunkObject.transform, 0, false); + Tilemap walls = CreateTilemap("Walls", chunkObject.transform, 1, true); + Tilemap environment = CreateTilemap("Environment", chunkObject.transform, 2, false); + + return new GeneratedChunk(chunkObject.transform, ground, walls, environment, new bool[chunkSize, chunkSize], new bool[chunkSize, chunkSize]); + } + + private Tilemap CreateTilemap(string name, Transform parent, int sortingOrder, bool addCollision) + { + GameObject tilemapObject = new GameObject(name, typeof(Tilemap), typeof(TilemapRenderer)); + tilemapObject.transform.SetParent(parent, false); + + TilemapRenderer renderer = tilemapObject.GetComponent(); + renderer.sortingOrder = sortingOrder; + + if (addCollision) + { + Rigidbody2D rb = tilemapObject.GetComponent(); + if (rb == null) + { + rb = tilemapObject.AddComponent(); + } + rb.bodyType = RigidbodyType2D.Static; + + CompositeCollider2D composite = tilemapObject.GetComponent(); + if (composite == null) + { + composite = tilemapObject.AddComponent(); + } + composite.geometryType = CompositeCollider2D.GeometryType.Polygons; + + TilemapCollider2D collider = tilemapObject.GetComponent(); + if (collider == null) + { + collider = tilemapObject.AddComponent(); + } + collider.compositeOperation = Collider2D.CompositeOperation.Merge; + } + + return tilemapObject.GetComponent(); + } + + private void BuildChunkData(Vector2Int coord, GeneratedChunk chunk) + { + int margin = Mathf.Max(2, smoothingPasses + 1); + int sampleSize = chunkSize + margin * 2; + bool[,] sampled = new bool[sampleSize, sampleSize]; + + for (int y = 0; y < sampleSize; y++) + { + for (int x = 0; x < sampleSize; x++) + { + int localX = x - margin; + int localY = y - margin; + Vector2Int worldCell = ChunkToWorldCell(coord, localX, localY); + sampled[x, y] = SampleRock(worldCell); + } + } + + for (int pass = 0; pass < smoothingPasses; pass++) + { + sampled = SmoothSampledMask(sampled); + } + + for (int y = 0; y < chunkSize; y++) + { + for (int x = 0; x < chunkSize; x++) + { + chunk.WallMask[x, y] = sampled[x + margin, y + margin]; + } + } + + BuildEnvironment(coord, chunk); + } + + private bool[,] SmoothSampledMask(bool[,] source) + { + int width = source.GetLength(0); + int height = source.GetLength(1); + bool[,] result = new bool[width, height]; + + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + int solidNeighbors = CountSampledWallNeighbors(source, x, y); + if (solidNeighbors >= 5) + { + result[x, y] = true; + } + else if (solidNeighbors <= 2) + { + result[x, y] = false; + } + else + { + result[x, y] = source[x, y]; + } + } + } + + return result; + } + + private int CountSampledWallNeighbors(bool[,] sampled, int x, int y) + { + int width = sampled.GetLength(0); + int height = sampled.GetLength(1); + int count = 0; + + for (int oy = -1; oy <= 1; oy++) + { + for (int ox = -1; ox <= 1; ox++) + { + if (ox == 0 && oy == 0) + { + continue; + } + + int nx = x + ox; + int ny = y + oy; + if (nx < 0 || ny < 0 || nx >= width || ny >= height) + { + count++; + continue; + } + + if (sampled[nx, ny]) + { + count++; + } + } + } + + return count; + } + + private bool SampleRock(Vector2Int worldCell) + { + float macro = Mathf.PerlinNoise((worldCell.x + seed * 0.13f) * macroNoiseScale, (worldCell.y - seed * 0.17f) * macroNoiseScale); + float detail = Mathf.PerlinNoise((worldCell.x - seed * 0.23f) * detailNoiseScale, (worldCell.y + seed * 0.19f) * detailNoiseScale); + float ridge = 1f - Mathf.Abs(Mathf.PerlinNoise((worldCell.x + seed * 0.31f) * ridgeNoiseScale, (worldCell.y + seed * 0.29f) * ridgeNoiseScale) * 2f - 1f); + + float rockValue = macro * 0.62f + detail * 0.18f + ridge * 0.20f + rockBias; + if (IsInsideGlobalPass(worldCell)) + { + rockValue -= 0.45f; + } + + return rockValue >= wallThreshold; + } + + private bool IsInsideGlobalPass(Vector2Int worldCell) + { + float primary = Mathf.PerlinNoise((worldCell.x + seed * 0.41f) * passNoiseScale, (worldCell.y - seed * 0.43f) * passNoiseScale); + float detail = Mathf.PerlinNoise((worldCell.x - seed * 0.17f) * passDetailScale, (worldCell.y + seed * 0.23f) * passDetailScale); + float ridged = Mathf.Abs(primary * 2f - 1f); + float warped = Mathf.Lerp(ridged, Mathf.Abs(detail * 2f - 1f), 0.35f); + return warped <= passThreshold + passFeather * detail; + } + + private void BuildEnvironment(Vector2Int coord, GeneratedChunk chunk) + { + for (int y = 0; y < chunkSize; y++) + { + for (int x = 0; x < chunkSize; x++) + { + if (chunk.WallMask[x, y]) + { + chunk.EnvironmentMask[x, y] = false; + continue; + } + + if (HasAdjacentOpenTiles(chunk.WallMask, x, y, 1)) + { + chunk.EnvironmentMask[x, y] = false; + continue; + } + + Vector2Int worldCell = ChunkToWorldCell(coord, x, y); + float noise = Mathf.PerlinNoise((worldCell.x + seed * 0.53f) * environmentNoiseScale, (worldCell.y - seed * 0.61f) * environmentNoiseScale); + chunk.EnvironmentMask[x, y] = noise >= environmentThreshold; + } + } + } + + private bool HasAdjacentOpenTiles(bool[,] wallMask, int x, int y, int radius) + { + for (int oy = -radius; oy <= radius; oy++) + { + for (int ox = -radius; ox <= radius; ox++) + { + int nx = x + ox; + int ny = y + oy; + if (nx < 0 || ny < 0 || nx >= chunkSize || ny >= chunkSize) + { + continue; + } + + if (!wallMask[nx, ny]) + { + return true; + } + } + } + + return false; + } + + private void RenderChunk(Vector2Int coord) + { + if (!chunks.TryGetValue(coord, out GeneratedChunk chunk)) + { + return; + } + + WorldAutotileProfile activeProfile = profile != null && profile.HasAnyAssignedTiles() ? profile : runtimeFallbackProfile; + if (activeProfile == null) + { + return; + } + + chunk.Ground.ClearAllTiles(); + chunk.Walls.ClearAllTiles(); + chunk.Environment.ClearAllTiles(); + + for (int y = 0; y < chunkSize; y++) + { + for (int x = 0; x < chunkSize; x++) + { + Vector3Int localCell = new Vector3Int(x, y, 0); + chunk.Ground.SetTile(localCell, activeProfile.baseGroundTile); + + if (chunk.WallMask[x, y]) + { + Vector2Int worldCell = ChunkToWorldCell(coord, x, y); + AutoTileShape shape = ResolveWallShape(worldCell); + chunk.Walls.SetTile(localCell, activeProfile.GetWallTile(shape)); + } + else if (chunk.EnvironmentMask[x, y]) + { + TileBase tile = PickEnvironmentTile(ChunkToWorldCell(coord, x, y), activeProfile); + if (tile != null) + { + chunk.Environment.SetTile(localCell, tile); + } + } + } + } + } + + private void RefreshNeighborBorders(Vector2Int coord) + { + RenderChunk(coord + Vector2Int.up); + RenderChunk(coord + Vector2Int.right); + RenderChunk(coord + Vector2Int.down); + RenderChunk(coord + Vector2Int.left); + } + + private AutoTileShape ResolveWallShape(Vector2Int worldCell) + { + bool top = HasWallAt(worldCell + Vector2Int.up); + bool right = HasWallAt(worldCell + Vector2Int.right); + bool bottom = HasWallAt(worldCell + Vector2Int.down); + bool left = HasWallAt(worldCell + Vector2Int.left); + bool topLeft = HasWallAt(worldCell + new Vector2Int(-1, 1)); + bool topRight = HasWallAt(worldCell + new Vector2Int(1, 1)); + bool bottomRight = HasWallAt(worldCell + new Vector2Int(1, -1)); + bool bottomLeft = HasWallAt(worldCell + new Vector2Int(-1, -1)); + + if (!top && !left) + { + return AutoTileShape.OuterTopLeft; + } + + if (!top && !right) + { + return AutoTileShape.OuterTopRight; + } + + if (!bottom && !right) + { + return AutoTileShape.OuterBottomRight; + } + + if (!bottom && !left) + { + return AutoTileShape.OuterBottomLeft; + } + + if (top && left && !topLeft) + { + return AutoTileShape.InnerTopLeft; + } + + if (top && right && !topRight) + { + return AutoTileShape.InnerTopRight; + } + + if (bottom && right && !bottomRight) + { + return AutoTileShape.InnerBottomRight; + } + + if (bottom && left && !bottomLeft) + { + return AutoTileShape.InnerBottomLeft; + } + + if (!top) + { + return AutoTileShape.Top; + } + + if (!right) + { + return AutoTileShape.Right; + } + + if (!bottom) + { + return AutoTileShape.Bottom; + } + + if (!left) + { + return AutoTileShape.Left; + } + + return AutoTileShape.Center; + } + + private bool HasWallAt(Vector2Int worldCell) + { + Vector2Int coord = new Vector2Int(Mathf.FloorToInt(worldCell.x / (float)chunkSize), Mathf.FloorToInt(worldCell.y / (float)chunkSize)); + if (!chunks.TryGetValue(coord, out GeneratedChunk chunk)) + { + return SampleRock(worldCell); + } + + int localX = worldCell.x - coord.x * chunkSize; + int localY = worldCell.y - coord.y * chunkSize; + if (localX < 0 || localY < 0 || localX >= chunkSize || localY >= chunkSize) + { + return SampleRock(worldCell); + } + + return chunk.WallMask[localX, localY]; + } + + private TileBase PickEnvironmentTile(Vector2Int worldCell, WorldAutotileProfile activeProfile) + { + if (activeProfile.environmentTiles == null || activeProfile.environmentTiles.Count == 0) + { + return null; + } + + float total = 0f; + for (int i = 0; i < activeProfile.environmentTiles.Count; i++) + { + EnvironmentTileEntry entry = activeProfile.environmentTiles[i]; + if (entry != null && entry.tile != null) + { + total += Mathf.Max(0.01f, entry.weight); + } + } + + if (total <= 0f) + { + return null; + } + + float selector = Hash01(worldCell.x, worldCell.y, seed + 701); + float threshold = selector * total; + float cumulative = 0f; + + for (int i = 0; i < activeProfile.environmentTiles.Count; i++) + { + EnvironmentTileEntry entry = activeProfile.environmentTiles[i]; + if (entry == null || entry.tile == null) + { + continue; + } + + cumulative += Mathf.Max(0.01f, entry.weight); + if (threshold <= cumulative) + { + return entry.tile; + } + } + + return activeProfile.environmentTiles[0].tile; + } + + private Vector2Int WorldToChunk(Vector3 position) + { + return new Vector2Int( + Mathf.FloorToInt(position.x / chunkSize), + Mathf.FloorToInt(position.y / chunkSize)); + } + + private Vector2Int ChunkToWorldCell(Vector2Int coord, int localX, int localY) + { + return new Vector2Int(coord.x * chunkSize + localX, coord.y * chunkSize + localY); + } + + private static int Hash(int x, int y, int seed) + { + int hash = x; + hash = hash * 397 ^ y; + hash = hash * 397 ^ seed; + hash = (hash << 13) ^ hash; + return hash * (hash * hash * 15731 + 789221) + 1376312589; + } + + private static float Hash01(int x, int y, int seed) + { + return (Hash(x, y, seed) & int.MaxValue) / (float)int.MaxValue; + } + + private readonly struct GeneratedChunk + { + public GeneratedChunk(Transform root, Tilemap ground, Tilemap walls, Tilemap environment, bool[,] wallMask, bool[,] environmentMask) + { + Root = root; + Ground = ground; + Walls = walls; + Environment = environment; + WallMask = wallMask; + EnvironmentMask = environmentMask; + } + + public Transform Root { get; } + public Tilemap Ground { get; } + public Tilemap Walls { get; } + public Tilemap Environment { get; } + public bool[,] WallMask { get; } + public bool[,] EnvironmentMask { get; } + } + } +} diff --git a/Assets/Scripts/WorldGen/InfiniteWorldGenerator.cs.meta b/Assets/Scripts/WorldGen/InfiniteWorldGenerator.cs.meta new file mode 100644 index 00000000..509f46f3 --- /dev/null +++ b/Assets/Scripts/WorldGen/InfiniteWorldGenerator.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 6bacd3a0ac13e6f4a94548426dd89ebb \ No newline at end of file diff --git a/Assets/Scripts/WorldGen/ProceduralWorldArt.cs b/Assets/Scripts/WorldGen/ProceduralWorldArt.cs new file mode 100644 index 00000000..bfbc36cb --- /dev/null +++ b/Assets/Scripts/WorldGen/ProceduralWorldArt.cs @@ -0,0 +1,350 @@ +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.Tilemaps; + +namespace InfiniteWorld +{ + public static class ProceduralWorldArt + { + private const int TextureSize = 32; + private static readonly Dictionary SpriteCache = new Dictionary(); + private static readonly Dictionary TileCache = new Dictionary(); + + public static Sprite CreatePlayerSprite() + { + const string key = "player_sprite"; + if (SpriteCache.TryGetValue(key, out Sprite cached)) + { + return cached; + } + + Color transparent = new Color(0f, 0f, 0f, 0f); + Texture2D texture = CreateTexture(key, transparent); + Color cloak = new Color(0.13f, 0.33f, 0.79f, 1f); + Color trim = new Color(0.85f, 0.92f, 1f, 1f); + Color face = new Color(0.99f, 0.89f, 0.72f, 1f); + + FillRect(texture, 10, 4, 12, 16, cloak); + FillRect(texture, 9, 18, 14, 7, cloak); + FillRect(texture, 12, 24, 8, 5, face); + FillRect(texture, 11, 23, 10, 1, trim); + FillRect(texture, 8, 12, 3, 10, trim); + FillRect(texture, 21, 12, 3, 10, trim); + FillRect(texture, 12, 0, 3, 4, cloak); + FillRect(texture, 17, 0, 3, 4, cloak); + texture.Apply(); + + Sprite sprite = CreateSprite(texture); + SpriteCache[key] = sprite; + return sprite; + } + + public static TileBase CreateSolidTile(string key, Color color, float noiseStrength = 0.04f) + { + if (TileCache.TryGetValue(key, out Tile cached)) + { + return cached; + } + + Texture2D texture = CreateTexture(key, color); + ApplyNoise(texture, noiseStrength, 31); + texture.Apply(); + Tile tile = CreateTile(texture, key); + TileCache[key] = tile; + return tile; + } + + public static TileBase CreateFeatureTile(string key, Color fill, Color border, bool top, bool right, bool bottom, bool left, InnerCornerMask innerCornerMask = InnerCornerMask.None, bool solidCollider = false) + { + if (TileCache.TryGetValue(key, out Tile cached)) + { + return cached; + } + + Texture2D texture = CreateTexture(key, fill); + ApplyNoise(texture, 0.05f, 71); + DrawBorder(texture, top, right, bottom, left, border, 4); + CarveInnerCorner(texture, innerCornerMask); + texture.Apply(); + + Tile tile = CreateTile(texture, key); + if (solidCollider) + { + tile.colliderType = Tile.ColliderType.Grid; + } + TileCache[key] = tile; + return tile; + } + + public static TileBase CreateDecorationTile(string key, Color baseColor, Color accentColor, DecorationPattern pattern) + { + if (TileCache.TryGetValue(key, out Tile cached)) + { + return cached; + } + + Color transparent = new Color(0f, 0f, 0f, 0f); + Texture2D texture = CreateTexture(key, transparent); + + switch (pattern) + { + case DecorationPattern.Bush: + DrawDisc(texture, 16, 15, 8, baseColor); + DrawDisc(texture, 10, 13, 6, baseColor); + DrawDisc(texture, 22, 13, 6, baseColor); + DrawDisc(texture, 14, 17, 3, accentColor); + DrawDisc(texture, 21, 17, 3, accentColor); + break; + case DecorationPattern.Flower: + DrawDisc(texture, 16, 12, 2, baseColor); + DrawDisc(texture, 12, 16, 2, baseColor); + DrawDisc(texture, 20, 16, 2, baseColor); + DrawDisc(texture, 16, 20, 2, baseColor); + DrawDisc(texture, 16, 16, 2, accentColor); + DrawLine(texture, 16, 0, 16, 12, new Color(0.2f, 0.5f, 0.2f, 1f)); + break; + default: + DrawDisc(texture, 12, 12, 4, baseColor); + DrawDisc(texture, 20, 18, 3, accentColor); + DrawDisc(texture, 9, 20, 2, accentColor); + break; + } + + texture.Apply(); + Tile tile = CreateTile(texture, key); + TileCache[key] = tile; + return tile; + } + + private static Texture2D CreateTexture(string key, Color fillColor) + { + Texture2D texture = new Texture2D(TextureSize, TextureSize, TextureFormat.RGBA32, false) + { + filterMode = FilterMode.Point, + wrapMode = TextureWrapMode.Clamp, + name = key + }; + + Color[] pixels = new Color[TextureSize * TextureSize]; + for (int i = 0; i < pixels.Length; i++) + { + pixels[i] = fillColor; + } + + texture.SetPixels(pixels); + return texture; + } + + private static Tile CreateTile(Texture2D texture, string key) + { + Sprite sprite = CreateSprite(texture); + Tile tile = ScriptableObject.CreateInstance(); + tile.sprite = sprite; + tile.name = key; + tile.hideFlags = HideFlags.HideAndDontSave; + return tile; + } + + private static Sprite CreateSprite(Texture2D texture) + { + Sprite sprite = Sprite.Create(texture, new Rect(0f, 0f, texture.width, texture.height), new Vector2(0.5f, 0.5f), TextureSize); + sprite.name = texture.name; + return sprite; + } + + private static void ApplyNoise(Texture2D texture, float strength, int offset) + { + if (strength <= 0f) + { + return; + } + + for (int y = 0; y < texture.height; y++) + { + for (int x = 0; x < texture.width; x++) + { + Color pixel = texture.GetPixel(x, y); + if (pixel.a <= 0f) + { + continue; + } + + float noise = Mathf.PerlinNoise((x + offset) * 0.21f, (y + offset) * 0.21f) - 0.5f; + texture.SetPixel(x, y, pixel * (1f + noise * strength * 2f)); + } + } + } + + private static void DrawBorder(Texture2D texture, bool top, bool right, bool bottom, bool left, Color color, int thickness) + { + if (top) + { + FillRect(texture, 0, TextureSize - thickness, TextureSize, thickness, color); + } + + if (bottom) + { + FillRect(texture, 0, 0, TextureSize, thickness, color); + } + + if (left) + { + FillRect(texture, 0, 0, thickness, TextureSize, color); + } + + if (right) + { + FillRect(texture, TextureSize - thickness, 0, thickness, TextureSize, color); + } + } + + private static void CarveInnerCorner(Texture2D texture, InnerCornerMask innerCornerMask) + { + if (innerCornerMask == InnerCornerMask.None) + { + return; + } + + Color transparent = new Color(0f, 0f, 0f, 0f); + const int radius = 8; + + if ((innerCornerMask & InnerCornerMask.TopLeft) != 0) + { + CutCorner(texture, 0, TextureSize - 1, radius, transparent, 1, -1); + } + + if ((innerCornerMask & InnerCornerMask.TopRight) != 0) + { + CutCorner(texture, TextureSize - 1, TextureSize - 1, radius, transparent, -1, -1); + } + + if ((innerCornerMask & InnerCornerMask.BottomRight) != 0) + { + CutCorner(texture, TextureSize - 1, 0, radius, transparent, -1, 1); + } + + if ((innerCornerMask & InnerCornerMask.BottomLeft) != 0) + { + CutCorner(texture, 0, 0, radius, transparent, 1, 1); + } + } + + private static void CutCorner(Texture2D texture, int originX, int originY, int radius, Color transparent, int dirX, int dirY) + { + for (int x = 0; x < radius; x++) + { + for (int y = 0; y < radius; y++) + { + float dx = x / (float)radius; + float dy = y / (float)radius; + if (dx * dx + dy * dy > 1f) + { + continue; + } + + int px = originX + x * dirX; + int py = originY + y * dirY; + if (px >= 0 && px < TextureSize && py >= 0 && py < TextureSize) + { + texture.SetPixel(px, py, transparent); + } + } + } + } + + private static void DrawDisc(Texture2D texture, int centerX, int centerY, int radius, Color color) + { + int sqrRadius = radius * radius; + for (int y = -radius; y <= radius; y++) + { + for (int x = -radius; x <= radius; x++) + { + if (x * x + y * y > sqrRadius) + { + continue; + } + + int px = centerX + x; + int py = centerY + y; + if (px >= 0 && px < TextureSize && py >= 0 && py < TextureSize) + { + texture.SetPixel(px, py, color); + } + } + } + } + + private static void DrawLine(Texture2D texture, int x0, int y0, int x1, int y1, Color color) + { + int dx = Mathf.Abs(x1 - x0); + int sx = x0 < x1 ? 1 : -1; + int dy = -Mathf.Abs(y1 - y0); + int sy = y0 < y1 ? 1 : -1; + int error = dx + dy; + + while (true) + { + if (x0 >= 0 && x0 < TextureSize && y0 >= 0 && y0 < TextureSize) + { + texture.SetPixel(x0, y0, color); + } + + if (x0 == x1 && y0 == y1) + { + break; + } + + int twiceError = 2 * error; + if (twiceError >= dy) + { + error += dy; + x0 += sx; + } + + if (twiceError <= dx) + { + error += dx; + y0 += sy; + } + } + } + + private static void FillRect(Texture2D texture, int x, int y, int width, int height, Color color) + { + for (int py = y; py < y + height; py++) + { + if (py < 0 || py >= TextureSize) + { + continue; + } + + for (int px = x; px < x + width; px++) + { + if (px < 0 || px >= TextureSize) + { + continue; + } + + texture.SetPixel(px, py, color); + } + } + } + } + + public enum DecorationPattern + { + Bush, + Flower, + Rocks + } + + [System.Flags] + public enum InnerCornerMask + { + None = 0, + TopLeft = 1, + TopRight = 2, + BottomRight = 4, + BottomLeft = 8 + } +} diff --git a/Assets/Scripts/WorldGen/ProceduralWorldArt.cs.meta b/Assets/Scripts/WorldGen/ProceduralWorldArt.cs.meta new file mode 100644 index 00000000..df0942f3 --- /dev/null +++ b/Assets/Scripts/WorldGen/ProceduralWorldArt.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: fc8c4a69ee2e7a046adf658709613591 \ No newline at end of file diff --git a/Assets/Scripts/WorldGen/RuntimeWorldProfileFactory.cs b/Assets/Scripts/WorldGen/RuntimeWorldProfileFactory.cs new file mode 100644 index 00000000..d6f3c7eb --- /dev/null +++ b/Assets/Scripts/WorldGen/RuntimeWorldProfileFactory.cs @@ -0,0 +1,237 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace InfiniteWorld +{ + public static class RuntimeWorldProfileFactory + { + public static WorldAutotileProfile CreateFallbackProfile() + { + WorldAutotileProfile profile = ScriptableObject.CreateInstance(); + profile.name = "RuntimeFallbackWorldProfile"; + + Color grass = new Color(0.35f, 0.62f, 0.26f, 1f); + Color wallFill = new Color(0.48f, 0.37f, 0.22f, 1f); + Color wallBorder = new Color(0.29f, 0.21f, 0.12f, 1f); + + profile.baseGroundTile = ProceduralWorldArt.CreateSolidTile("base_grass", grass, 0.06f); + profile.wallTiles.center = ProceduralWorldArt.CreateFeatureTile("wall_center", wallFill, wallBorder, false, false, false, false, InnerCornerMask.None, true); + profile.wallTiles.top = ProceduralWorldArt.CreateFeatureTile("wall_top", wallFill, wallBorder, true, false, false, false, InnerCornerMask.None, true); + profile.wallTiles.right = ProceduralWorldArt.CreateFeatureTile("wall_right", wallFill, wallBorder, false, true, false, false, InnerCornerMask.None, true); + profile.wallTiles.bottom = ProceduralWorldArt.CreateFeatureTile("wall_bottom", wallFill, wallBorder, false, false, true, false, InnerCornerMask.None, true); + profile.wallTiles.left = ProceduralWorldArt.CreateFeatureTile("wall_left", wallFill, wallBorder, false, false, false, true, InnerCornerMask.None, true); + profile.wallTiles.outerTopLeft = ProceduralWorldArt.CreateFeatureTile("wall_outer_tl", wallFill, wallBorder, true, false, false, true, InnerCornerMask.None, true); + profile.wallTiles.outerTopRight = ProceduralWorldArt.CreateFeatureTile("wall_outer_tr", wallFill, wallBorder, true, true, false, false, InnerCornerMask.None, true); + profile.wallTiles.outerBottomRight = ProceduralWorldArt.CreateFeatureTile("wall_outer_br", wallFill, wallBorder, false, true, true, false, InnerCornerMask.None, true); + profile.wallTiles.outerBottomLeft = ProceduralWorldArt.CreateFeatureTile("wall_outer_bl", wallFill, wallBorder, false, false, true, true, InnerCornerMask.None, true); + profile.wallTiles.innerTopLeft = ProceduralWorldArt.CreateFeatureTile("wall_inner_tl", wallFill, wallBorder, false, false, false, false, InnerCornerMask.TopLeft, true); + profile.wallTiles.innerTopRight = ProceduralWorldArt.CreateFeatureTile("wall_inner_tr", wallFill, wallBorder, false, false, false, false, InnerCornerMask.TopRight, true); + profile.wallTiles.innerBottomRight = ProceduralWorldArt.CreateFeatureTile("wall_inner_br", wallFill, wallBorder, false, false, false, false, InnerCornerMask.BottomRight, true); + profile.wallTiles.innerBottomLeft = ProceduralWorldArt.CreateFeatureTile("wall_inner_bl", wallFill, wallBorder, false, false, false, false, InnerCornerMask.BottomLeft, true); + + profile.environmentTiles.Add(new EnvironmentTileEntry + { + id = "Bush", + tile = ProceduralWorldArt.CreateDecorationTile("env_bush", new Color(0.15f, 0.45f, 0.18f, 1f), new Color(0.23f, 0.58f, 0.25f, 1f), DecorationPattern.Bush), + weight = 1.4f + }); + profile.environmentTiles.Add(new EnvironmentTileEntry + { + id = "FlowerBlue", + tile = ProceduralWorldArt.CreateDecorationTile("env_flower_blue", new Color(0.44f, 0.62f, 0.96f, 1f), new Color(0.98f, 0.91f, 0.52f, 1f), DecorationPattern.Flower), + weight = 0.75f + }); + profile.environmentTiles.Add(new EnvironmentTileEntry + { + id = "Rock", + tile = ProceduralWorldArt.CreateDecorationTile("env_rock", new Color(0.48f, 0.48f, 0.52f, 1f), new Color(0.69f, 0.69f, 0.74f, 1f), DecorationPattern.Rocks), + weight = 0.9f + }); + + return profile; + } + + public static List CreateFallbackTemplates(int size) + { + List templates = new List + { + CreateCross(size), + CreateStraightHorizontal(size), + CreateStraightVertical(size), + CreateCorner(size, true, true, false, false), + CreateCorner(size, false, true, true, false), + CreateCorner(size, false, false, true, true), + CreateCorner(size, true, false, false, true), + CreateTJunction(size, true, true, true, false), + CreateTJunction(size, false, true, true, true), + CreateTJunction(size, true, false, true, true), + CreateTJunction(size, true, true, false, true) + }; + + return templates; + } + + private static ChunkTemplate CreateCross(int size) + { + ChunkTemplate template = CreateTemplate("Runtime_Cross", size, size, true, true, true, true); + DrawRoom(template, 3, 3, size - 6, size - 6); + DrawPillar(template, 4, 4); + DrawPillar(template, size - 5, 4); + DrawPillar(template, 4, size - 5); + DrawPillar(template, size - 5, size - 5); + ScatterEnvironment(template, 3); + return template; + } + + private static ChunkTemplate CreateStraightHorizontal(int size) + { + ChunkTemplate template = CreateTemplate("Runtime_Straight_H", size, size, false, true, false, true); + DrawRoom(template, 2, 4, size - 4, size - 8); + DrawSideAlcoves(template, horizontal: true); + ScatterEnvironment(template, 2); + return template; + } + + private static ChunkTemplate CreateStraightVertical(int size) + { + ChunkTemplate template = CreateTemplate("Runtime_Straight_V", size, size, true, false, true, false); + DrawRoom(template, 4, 2, size - 8, size - 4); + DrawSideAlcoves(template, horizontal: false); + ScatterEnvironment(template, 2); + return template; + } + + private static ChunkTemplate CreateCorner(int size, bool top, bool right, bool bottom, bool left) + { + ChunkTemplate template = CreateTemplate($"Runtime_Corner_{top}_{right}_{bottom}_{left}", size, size, top, right, bottom, left); + DrawRoom(template, 3, 3, size - 6, size - 6); + if (!top) + { + DrawWallLine(template, 5, size - 5, size - 6, size - 5); + } + if (!right) + { + DrawWallLine(template, size - 5, 5, size - 5, size - 6); + } + if (!bottom) + { + DrawWallLine(template, 5, 4, size - 6, 4); + } + if (!left) + { + DrawWallLine(template, 4, 5, 4, size - 6); + } + ScatterEnvironment(template, 2); + return template; + } + + private static ChunkTemplate CreateTJunction(int size, bool top, bool right, bool bottom, bool left) + { + ChunkTemplate template = CreateTemplate($"Runtime_T_{top}_{right}_{bottom}_{left}", size, size, top, right, bottom, left); + DrawRoom(template, 2, 2, size - 4, size - 4); + DrawPillar(template, size / 2, size / 2); + ScatterEnvironment(template, 3); + return template; + } + + private static ChunkTemplate CreateTemplate(string name, int width, int height, bool top, bool right, bool bottom, bool left) + { + ChunkTemplate template = ScriptableObject.CreateInstance(); + template.name = name; + template.Resize(width, height); + template.exitTop = top; + template.exitRight = right; + template.exitBottom = bottom; + template.exitLeft = left; + template.ApplyBorderWallsFromExits(3); + return template; + } + + private static void DrawRoom(ChunkTemplate template, int xMin, int yMin, int width, int height) + { + for (int x = xMin; x < xMin + width; x++) + { + template.SetWall(x, yMin, true); + template.SetWall(x, yMin + height - 1, true); + } + + for (int y = yMin; y < yMin + height; y++) + { + template.SetWall(xMin, y, true); + template.SetWall(xMin + width - 1, y, true); + } + + template.CarveExit(ChunkExit.Top, 3); + template.CarveExit(ChunkExit.Right, 3); + template.CarveExit(ChunkExit.Bottom, 3); + template.CarveExit(ChunkExit.Left, 3); + } + + private static void DrawPillar(ChunkTemplate template, int x, int y) + { + for (int py = -1; py <= 1; py++) + { + for (int px = -1; px <= 1; px++) + { + template.SetWall(x + px, y + py, true); + } + } + } + + private static void DrawSideAlcoves(ChunkTemplate template, bool horizontal) + { + if (horizontal) + { + DrawWallLine(template, template.width / 2 - 1, 5, template.width / 2 - 1, 7); + DrawWallLine(template, template.width / 2 + 1, template.height - 8, template.width / 2 + 1, template.height - 6); + } + else + { + DrawWallLine(template, 5, template.height / 2 - 1, 7, template.height / 2 - 1); + DrawWallLine(template, template.width - 8, template.height / 2 + 1, template.width - 6, template.height / 2 + 1); + } + } + + private static void DrawWallLine(ChunkTemplate template, int x0, int y0, int x1, int y1) + { + int dx = Math.Sign(x1 - x0); + int dy = Math.Sign(y1 - y0); + int x = x0; + int y = y0; + + while (true) + { + template.SetWall(x, y, true); + if (x == x1 && y == y1) + { + break; + } + + if (x != x1) + { + x += dx; + } + + if (y != y1) + { + y += dy; + } + } + } + + private static void ScatterEnvironment(ChunkTemplate template, int spacing) + { + for (int y = 2; y < template.height - 2; y += spacing + 2) + { + for (int x = 2; x < template.width - 2; x += spacing + 3) + { + if (!template.GetWall(x, y)) + { + template.SetEnvironment(x, y, ((x + y) & 1) == 0); + } + } + } + } + } +} diff --git a/Assets/Scripts/WorldGen/RuntimeWorldProfileFactory.cs.meta b/Assets/Scripts/WorldGen/RuntimeWorldProfileFactory.cs.meta new file mode 100644 index 00000000..28148519 --- /dev/null +++ b/Assets/Scripts/WorldGen/RuntimeWorldProfileFactory.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e5b485257bdc222448c314a7cd173845 \ No newline at end of file diff --git a/Assets/Scripts/WorldGen/WorldAutotileProfile.cs b/Assets/Scripts/WorldGen/WorldAutotileProfile.cs new file mode 100644 index 00000000..c4852559 --- /dev/null +++ b/Assets/Scripts/WorldGen/WorldAutotileProfile.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.Tilemaps; + +namespace InfiniteWorld +{ + [CreateAssetMenu(menuName = "Infinite World/World Autotile Profile", fileName = "WorldAutotileProfile")] + public class WorldAutotileProfile : ScriptableObject + { + public TileBase baseGroundTile; + public AutoTileDefinition wallTiles = new AutoTileDefinition(); + public List environmentTiles = new List(); + + public TileBase GetWallTile(AutoTileShape shape) + { + return wallTiles != null ? wallTiles.GetTile(shape) : null; + } + + public bool HasAnyAssignedTiles() + { + if (baseGroundTile != null) + { + return true; + } + + if (wallTiles != null && wallTiles.HasAnyAssignedTiles()) + { + return true; + } + + for (int i = 0; i < environmentTiles.Count; i++) + { + if (environmentTiles[i] != null && environmentTiles[i].tile != null) + { + return true; + } + } + + return false; + } + } + + public enum AutoTileShape + { + Center, + Top, + Right, + Bottom, + Left, + OuterTopLeft, + OuterTopRight, + OuterBottomRight, + OuterBottomLeft, + InnerTopLeft, + InnerTopRight, + InnerBottomRight, + InnerBottomLeft + } + + [Serializable] + public class AutoTileDefinition + { + public TileBase center; + public TileBase top; + public TileBase right; + public TileBase bottom; + public TileBase left; + public TileBase outerTopLeft; + public TileBase outerTopRight; + public TileBase outerBottomRight; + public TileBase outerBottomLeft; + public TileBase innerTopLeft; + public TileBase innerTopRight; + public TileBase innerBottomRight; + public TileBase innerBottomLeft; + + public TileBase GetTile(AutoTileShape shape) + { + TileBase tile = shape switch + { + AutoTileShape.Center => center, + AutoTileShape.Top => top, + AutoTileShape.Right => right, + AutoTileShape.Bottom => bottom, + AutoTileShape.Left => left, + AutoTileShape.OuterTopLeft => outerTopLeft, + AutoTileShape.OuterTopRight => outerTopRight, + AutoTileShape.OuterBottomRight => outerBottomRight, + AutoTileShape.OuterBottomLeft => outerBottomLeft, + AutoTileShape.InnerTopLeft => innerTopLeft, + AutoTileShape.InnerTopRight => innerTopRight, + AutoTileShape.InnerBottomRight => innerBottomRight, + AutoTileShape.InnerBottomLeft => innerBottomLeft, + _ => center + }; + + return tile ?? center; + } + + public bool HasAnyAssignedTiles() + { + return center != null || + top != null || + right != null || + bottom != null || + left != null || + outerTopLeft != null || + outerTopRight != null || + outerBottomRight != null || + outerBottomLeft != null || + innerTopLeft != null || + innerTopRight != null || + innerBottomRight != null || + innerBottomLeft != null; + } + } + + [Serializable] + public class EnvironmentTileEntry + { + public string id = "Environment"; + public TileBase tile; + [Min(0f)] public float weight = 1f; + } +} diff --git a/Assets/Scripts/WorldGen/WorldAutotileProfile.cs.meta b/Assets/Scripts/WorldGen/WorldAutotileProfile.cs.meta new file mode 100644 index 00000000..fadc8263 --- /dev/null +++ b/Assets/Scripts/WorldGen/WorldAutotileProfile.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e36b055e81273a54c8e736e2ef74fa50 \ No newline at end of file diff --git a/ProjectSettings/SceneTemplateSettings.json b/ProjectSettings/SceneTemplateSettings.json new file mode 100644 index 00000000..ede5887b --- /dev/null +++ b/ProjectSettings/SceneTemplateSettings.json @@ -0,0 +1,121 @@ +{ + "templatePinStates": [], + "dependencyTypeInfos": [ + { + "userAdded": false, + "type": "UnityEngine.AnimationClip", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEditor.Animations.AnimatorController", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.AnimatorOverrideController", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEditor.Audio.AudioMixerController", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.ComputeShader", + "defaultInstantiationMode": 1 + }, + { + "userAdded": false, + "type": "UnityEngine.Cubemap", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.GameObject", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEditor.LightingDataAsset", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.LightingSettings", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.Material", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEditor.MonoScript", + "defaultInstantiationMode": 1 + }, + { + "userAdded": false, + "type": "UnityEngine.PhysicsMaterial", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.PhysicsMaterial2D", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.Rendering.PostProcessing.PostProcessProfile", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.Rendering.PostProcessing.PostProcessResources", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.Rendering.VolumeProfile", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEditor.SceneAsset", + "defaultInstantiationMode": 1 + }, + { + "userAdded": false, + "type": "UnityEngine.Shader", + "defaultInstantiationMode": 1 + }, + { + "userAdded": false, + "type": "UnityEngine.ShaderVariantCollection", + "defaultInstantiationMode": 1 + }, + { + "userAdded": false, + "type": "UnityEngine.Texture", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.Texture2D", + "defaultInstantiationMode": 0 + }, + { + "userAdded": false, + "type": "UnityEngine.Timeline.TimelineAsset", + "defaultInstantiationMode": 0 + } + ], + "defaultDependencyTypeInfo": { + "userAdded": false, + "type": "", + "defaultInstantiationMode": 1 + }, + "newSceneOverride": 0 +} \ No newline at end of file