diff --git a/Assets/Editor/City3DSetup.cs b/Assets/Editor/City3DSetup.cs index a70690e..4e6cd66 100644 --- a/Assets/Editor/City3DSetup.cs +++ b/Assets/Editor/City3DSetup.cs @@ -57,13 +57,25 @@ public static class City3DSetup Debug.Log($"[3D-ГОРОД] Сцена создана: {ScenePath}. 3D-рендерер: индекс {rendererIndex}. Нажмите Play."); } - // Второй рендерер URP -- обычный (3D). Возвращает его индекс в списке рендереров (или -1) + // Второй рендерер URP -- обычный (3D). Добавляется в URP Asset из Graphics Settings и из КАЖДОГО уровня качества + // (в этом проекте URP задан только в Quality). Возвращает индекс рендерера в списке (или -1) + [MenuItem("Tools/Bogatyri/3D: добавить 3D-рендерер в URP")] + static void AddRendererMenu() + { + int index = EnsureRenderer3D(); + Debug.Log(index >= 0 ? $"[3D] 3D-рендерер подключён (индекс {index}). Можно запускать карту мира и City3D." + : "[3D] URP Asset не найден ни в Graphics, ни в Quality Settings."); + } + static int EnsureRenderer3D() { - var pipeline = GraphicsSettings.defaultRenderPipeline as UniversalRenderPipelineAsset; - if (pipeline == null) + var pipelines = new System.Collections.Generic.List(); + void Add(RenderPipelineAsset a) { if (a is UniversalRenderPipelineAsset u && !pipelines.Contains(u)) pipelines.Add(u); } + Add(GraphicsSettings.defaultRenderPipeline); + for (int q = 0; q < QualitySettings.names.Length; q++) Add(QualitySettings.GetRenderPipelineAssetAt(q)); + if (pipelines.Count == 0) { - Debug.LogWarning("[3D-ГОРОД] В Graphics Settings нет URP Asset -- камера будет рисовать рендерером по умолчанию"); + Debug.LogWarning("[3D-ГОРОД] Нет URP Asset ни в Graphics, ни в Quality Settings -- камера будет рисовать рендерером по умолчанию"); return -1; } @@ -75,17 +87,26 @@ public static class City3DSetup AssetDatabase.SaveAssets(); } - var so = new SerializedObject(pipeline); - var list = so.FindProperty("m_RendererDataList"); - for (int i = 0; i < list.arraySize; i++) - if (list.GetArrayElementAtIndex(i).objectReferenceValue == renderer) return i; - - list.arraySize++; - list.GetArrayElementAtIndex(list.arraySize - 1).objectReferenceValue = renderer; - so.ApplyModifiedProperties(); - EditorUtility.SetDirty(pipeline); + int result = -1; + foreach (var pipeline in pipelines) + { + var so = new SerializedObject(pipeline); + var list = so.FindProperty("m_RendererDataList"); + int found = -1; + for (int i = 0; i < list.arraySize; i++) + if (list.GetArrayElementAtIndex(i).objectReferenceValue == renderer) found = i; + if (found < 0) + { + list.arraySize++; + list.GetArrayElementAtIndex(list.arraySize - 1).objectReferenceValue = renderer; + so.ApplyModifiedProperties(); + EditorUtility.SetDirty(pipeline); + found = list.arraySize - 1; + } + if (result < 0) result = found; + } AssetDatabase.SaveAssets(); - return list.arraySize - 1; + return result; } static void AddToBuildSettings() diff --git a/Assets/Scenes/City3D.unity b/Assets/Scenes/City3D.unity new file mode 100644 index 0000000..c4fee8e --- /dev/null +++ b/Assets/Scenes/City3D.unity @@ -0,0 +1,431 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 10 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.55, g: 0.58, b: 0.62, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 3 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 13 + m_BakeOnSceneLoad: 0 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 0 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 2 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 1 + m_PVRFilteringGaussRadiusAO: 1 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0} + m_LightingSettings: {fileID: 0} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 3 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + buildHeightMesh: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1 &110794169 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 110794174} + - component: {fileID: 110794173} + - component: {fileID: 110794172} + - component: {fileID: 110794171} + - component: {fileID: 110794170} + m_Layer: 0 + m_Name: Main Camera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &110794170 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 110794169} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: e8645f0028574904969b5802443f100f, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::City3DCamera + pitch: 52 + yaw: 45 + distance: 26 + minDistance: 8 + maxDistance: 55 + target: {x: 0, y: 0, z: 0} + bounds: + serializedVersion: 2 + x: 0 + y: 0 + width: 48 + height: 48 +--- !u!114 &110794171 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 110794169} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: a79441f348de89743a2939f4d699eac1, type: 3} + m_Name: + m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.UniversalAdditionalCameraData + m_RenderShadows: 1 + m_RequiresDepthTextureOption: 2 + m_RequiresOpaqueTextureOption: 2 + m_CameraType: 0 + m_Cameras: [] + m_RendererIndex: -1 + m_VolumeLayerMask: + serializedVersion: 2 + m_Bits: 1 + m_VolumeTrigger: {fileID: 0} + m_VolumeFrameworkUpdateModeOption: 2 + m_RenderPostProcessing: 0 + m_Antialiasing: 0 + m_AntialiasingQuality: 2 + m_StopNaN: 0 + 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!81 &110794172 +AudioListener: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 110794169} + m_Enabled: 1 +--- !u!20 &110794173 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 110794169} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 2 + m_BackGroundColor: {r: 0.55, g: 0.72, b: 0.9, a: 1} + 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_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + near clip plane: 0.3 + far clip plane: 300 + field of view: 40 + orthographic: 0 + orthographic size: 5 + m_Depth: 0 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 3 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!4 &110794174 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 110794169} + 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!1 &1225367365 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1225367367} + - component: {fileID: 1225367366} + m_Layer: 0 + m_Name: City3D + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1225367366 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1225367365} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 44d822c903694362bef912c055adda5b, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::City3DController +--- !u!4 &1225367367 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1225367365} + 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!1 &1935466178 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1935466180} + - component: {fileID: 1935466179} + m_Layer: 0 + m_Name: Sun + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!108 &1935466179 +Light: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1935466178} + m_Enabled: 1 + serializedVersion: 13 + m_Type: 1 + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_Intensity: 1.2 + m_Range: 10 + m_SpotAngle: 30 + m_InnerSpotAngle: 21.80208 + m_CookieSize2D: {x: 0.5, y: 0.5} + m_Shadows: + m_Type: 2 + m_Resolution: -1 + m_CustomResolution: -1 + m_Strength: 1 + m_Bias: 0.05 + m_NormalBias: 0.4 + m_NearPlane: 0.2 + m_CullingMatrixOverride: + e00: 1 + e01: 0 + e02: 0 + e03: 0 + e10: 0 + e11: 1 + e12: 0 + e13: 0 + e20: 0 + e21: 0 + e22: 1 + e23: 0 + e30: 0 + e31: 0 + e32: 0 + e33: 1 + m_UseCullingMatrixOverride: 0 + m_Cookie: {fileID: 0} + m_DrawHalo: 0 + m_Flare: {fileID: 0} + m_RenderMode: 0 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingLayerMask: 1 + m_Lightmapping: 4 + m_LightShadowCasterMode: 0 + m_AreaSize: {x: 1, y: 1} + m_BounceIntensity: 1 + m_ColorTemperature: 6570 + m_UseColorTemperature: 0 + m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0} + m_UseBoundingSphereOverride: 0 + m_UseViewFrustumForShadowCasterCull: 1 + m_ForceVisible: 0 + m_ShapeRadius: 0.025 + m_ShadowAngle: 0 + m_LightUnit: 1 + m_LuxAtDistance: 1 + m_EnableSpotReflector: 1 +--- !u!4 &1935466180 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1935466178} + serializedVersion: 2 + m_LocalRotation: {x: 0.40821794, y: -0.23456973, z: 0.10938166, w: 0.8754261} + 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: 110794174} + - {fileID: 1935466180} + - {fileID: 1225367367} diff --git a/Assets/Scenes/City3D.unity.meta b/Assets/Scenes/City3D.unity.meta new file mode 100644 index 0000000..872b8bc --- /dev/null +++ b/Assets/Scenes/City3D.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: c1c34719f0605c247a3575d077f3d656 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/City3D/City3DController.cs b/Assets/Scripts/City3D/City3DController.cs index 00b09d2..00bde43 100644 --- a/Assets/Scripts/City3D/City3DController.cs +++ b/Assets/Scripts/City3D/City3DController.cs @@ -88,10 +88,7 @@ public class City3DController : MonoBehaviour cameraRig.target = new Vector3(City3DLayout.Size / 2f, 0, City3DLayout.Size / 2f); cameraRig.Apply(); - // Материал URP по умолчанию берём у примитива: шейдер точно есть в сборке - var probe = GameObject.CreatePrimitive(PrimitiveType.Cube); - baseMaterial = new Material(probe.GetComponent().sharedMaterial); - Destroy(probe); + Map3D.SetUpLook(cam, 30f, 85f); // солнце, небо, дымка вдали layout = City3DLayout.Create(); map = layout.Map; @@ -105,11 +102,7 @@ public class City3DController : MonoBehaviour RefreshGround(); } - Material Mat(Color color) - { - var m = new Material(baseMaterial) { color = color }; - return m; - } + static Material Mat(Color color) => Map3D.Mat(color); void BuildGround() { @@ -146,9 +139,7 @@ public class City3DController : MonoBehaviour filterMode = FilterMode.Point, wrapMode = TextureWrapMode.Clamp }; - var mat = Mat(Color.white); - mat.mainTexture = groundTexture; - go.AddComponent().sharedMaterial = mat; + go.AddComponent().sharedMaterial = Map3D.TexturedMat(groundTexture); go.AddComponent().sharedMesh = mesh; // Вода -- плоскость над руслом @@ -158,50 +149,35 @@ public class City3DController : MonoBehaviour water.transform.SetParent(transform, false); water.transform.position = new Vector3(37f, -0.25f, n / 2f); water.transform.localScale = new Vector3(9f, 0.05f, n); - water.GetComponent().sharedMaterial = Mat(new Color(0.25f, 0.5f, 0.85f)); + water.GetComponent().sharedMaterial = Mat(new Color(0.3f, 0.58f, 0.85f)); } void BuildObstacles() { - var trunk = Mat(new Color(0.45f, 0.3f, 0.18f)); - var leaves = Mat(new Color(0.18f, 0.48f, 0.2f)); - var stone = Mat(new Color(0.55f, 0.55f, 0.58f)); + int i = 0; foreach (var o in layout.Obstacles) { - var root = new GameObject(o.Id); - root.transform.SetParent(transform, false); Vector2 center = Vector2.zero; foreach (var c in o.Cells) center += new Vector2(c.x + 0.5f, c.y + 0.5f); center /= o.Cells.Count; - float h = City3DLayout.HeightAt(center.x, center.y); - root.transform.position = new Vector3(center.x, h, center.y); - if (o.IsTree) - { - Part(root, PrimitiveType.Cylinder, new Vector3(0, 0.5f, 0), new Vector3(0.25f, 0.5f, 0.25f), trunk); - Part(root, PrimitiveType.Sphere, new Vector3(0, 1.4f, 0), new Vector3(1.1f, 1.3f, 1.1f), leaves); - } - else - { - float s = o.Cells.Count > 1 ? 1.8f : 0.9f; - Part(root, PrimitiveType.Sphere, new Vector3(0, s * 0.3f, 0), new Vector3(s, s * 0.7f, s * 0.9f), stone); - } - obstacleViews[o.Id] = root; + var ground = new Vector3(center.x, City3DLayout.HeightAt(center.x, center.y), center.y); + i++; + var view = o.IsTree + ? Map3D.Tree(transform, o.Id, ground, 1.0f + (i * 37 % 10) * 0.05f, i) // ели и лиственные вперемешку + : Map3D.Rock(transform, o.Id, ground, o.Cells.Count > 1 ? 1.7f : 0.85f, i); + obstacleViews[o.Id] = view.gameObject; } } + // Трава кустиками -- только украшение: прячется под зданием и возвращается void BuildGrass() { - var grass = Mat(new Color(0.35f, 0.65f, 0.3f)); - foreach (var p in layout.Grass) + Color[] greens = { new Color(0.3f, 0.6f, 0.25f), new Color(0.38f, 0.66f, 0.28f), new Color(0.45f, 0.68f, 0.3f) }; + for (int i = 0; i < layout.Grass.Count; i++) { - var g = GameObject.CreatePrimitive(PrimitiveType.Cube); - Destroy(g.GetComponent()); - g.name = "Grass"; - g.transform.SetParent(transform, false); - g.transform.position = new Vector3(p.x, City3DLayout.HeightAt(p.x, p.y) + 0.08f, p.y); - g.transform.localScale = new Vector3(0.25f, 0.16f, 0.25f); - g.transform.rotation = Quaternion.Euler(0, p.x * 37f, 0); - g.GetComponent().sharedMaterial = grass; + Vector2 p = layout.Grass[i]; + var g = Map3D.Shape(transform, "Grass", Map3D.Cone, greens[i % greens.Length], + new Vector3(p.x, City3DLayout.HeightAt(p.x, p.y), p.y), Vector2.one * 0.28f, 0.22f + (i % 3) * 0.06f).gameObject; grassViews.Add(g); } } @@ -227,9 +203,13 @@ public class City3DController : MonoBehaviour for (int x = 0; x < n; x++) { CellKind k = map.KindAt(x, y); - Color c = k == CellKind.Water ? new Color(0.3f, 0.45f, 0.7f) - : k == CellKind.Mountain ? new Color(0.5f, 0.48f, 0.45f) - : Color.Lerp(new Color(0.35f, 0.58f, 0.3f), new Color(0.42f, 0.66f, 0.34f), Mathf.PerlinNoise(x * 0.3f, y * 0.3f)); + bool shore = k == CellKind.Ground && (map.KindAt(x - 1, y) == CellKind.Water || map.KindAt(x + 1, y) == CellKind.Water + || map.KindAt(x, y - 1) == CellKind.Water || map.KindAt(x, y + 1) == CellKind.Water); + float hm = map.HeightAt(x, y) / 1000f; + Color c = k == CellKind.Water ? new Color(0.32f, 0.5f, 0.72f) + : k == CellKind.Mountain ? Color.Lerp(new Color(0.48f, 0.45f, 0.42f), new Color(0.85f, 0.86f, 0.88f), Mathf.Clamp01((hm - 3.2f) / 1.5f)) // снег на вершинах + : shore ? new Color(0.78f, 0.72f, 0.52f) // песчаный берег + : Color.Lerp(new Color(0.33f, 0.56f, 0.27f), new Color(0.45f, 0.66f, 0.32f), Mathf.PerlinNoise(x * 0.25f, y * 0.25f)); if (grid && k == CellKind.Ground) { int zone = map.ZoneAt(x, y); @@ -271,18 +251,65 @@ public class City3DController : MonoBehaviour return true; } + // Кровля по типу здания + static Color RoofFor(int type) + { + switch (Types[type].Name) + { + case "Ратуша": return new Color(0.65f, 0.15f, 0.12f); + case "Госпиталь": return new Color(0.45f, 0.55f, 0.65f); + case "Казармы": return new Color(0.4f, 0.15f, 0.12f); + case "Каменоломня": + case "Шахта": return new Color(0.35f, 0.35f, 0.38f); + default: return Color.Lerp(Types[type].Color, new Color(0.3f, 0.15f, 0.1f), 0.55f); + } + } + + // Здание на примитивах: стены и двускатная крыша в «единичном» объёме Body (растягивается под габарит и высоту), + // у некоторых -- свои приметы: башня Ратуши, флаг Казарм, крест Госпиталя, тёмный вход Шахты GameObject CreateBuildingView(string id, int type) { var t = Types[type]; var root = new GameObject(id + "_" + t.Name); root.transform.SetParent(transform, false); - var body = GameObject.CreatePrimitive(PrimitiveType.Cube); - body.name = "Body"; - body.transform.SetParent(root.transform, false); - body.GetComponent().sharedMaterial = Mat(t.Color); - var door = Part(root, PrimitiveType.Cube, Vector3.zero, Vector3.one, Mat(new Color(0.25f, 0.18f, 0.12f))); + var body = new GameObject("Body").transform; + body.SetParent(root.transform, false); + var hit = body.gameObject.AddComponent(); // тап по зданию попадает в него целиком + hit.center = new Vector3(0, 0.5f, 0); + hit.size = Vector3.one; + + Color walls = Color.Lerp(t.Color, Color.white, 0.3f), roofColor = RoofFor(type); + Unit(body, PrimitiveType.Cube, new Vector3(0, 0.31f, 0), new Vector3(1f, 0.62f, 1f), walls); + var top = Map3D.Shape(body, "Roof", Map3D.Roof, roofColor, Vector3.zero, new Vector2(1.08f, 1.12f), 0.38f).transform; + top.localPosition = new Vector3(0, 0.62f, 0); + switch (t.Name) + { + case "Ратуша": + Unit(body, PrimitiveType.Cube, new Vector3(0.3f, 0.7f, 0.3f), new Vector3(0.28f, 1.4f, 0.28f), walls); + var spire = Map3D.Shape(body, "Spire", Map3D.Pyramid, roofColor, Vector3.zero, Vector2.one * 0.4f, 0.4f, 45f).transform; + spire.localPosition = new Vector3(0.3f, 1.4f, 0.3f); + break; + case "Казармы": + Unit(body, PrimitiveType.Cylinder, new Vector3(-0.4f, 0.75f, 0.4f), new Vector3(0.03f, 0.75f, 0.03f), new Color(0.35f, 0.25f, 0.15f)); + Unit(body, PrimitiveType.Cube, new Vector3(-0.32f, 1.35f, 0.4f), new Vector3(0.16f, 0.12f, 0.01f), new Color(0.8f, 0.15f, 0.15f)); + break; + case "Госпиталь": + Unit(body, PrimitiveType.Cube, new Vector3(0, 0.38f, -0.505f), new Vector3(0.08f, 0.28f, 0.01f), new Color(0.85f, 0.1f, 0.1f)); + Unit(body, PrimitiveType.Cube, new Vector3(0, 0.38f, -0.505f), new Vector3(0.24f, 0.08f, 0.01f), new Color(0.85f, 0.1f, 0.1f)); + break; + case "Шахта": + Unit(body, PrimitiveType.Cube, new Vector3(0, 0.2f, -0.505f), new Vector3(0.35f, 0.4f, 0.02f), new Color(0.1f, 0.1f, 0.12f)); + break; + case "Ферма": + // грядки перед амбаром + for (int i = 0; i < 3; i++) + Unit(body, PrimitiveType.Cube, new Vector3(-0.3f + i * 0.3f, 0.02f, -0.62f), new Vector3(0.22f, 0.04f, 0.2f), new Color(0.55f, 0.75f, 0.25f)); + break; + } + + var door = Part(root, PrimitiveType.Cube, Vector3.zero, Vector3.one, Mat(new Color(0.28f, 0.18f, 0.1f))); door.name = "Door"; // показывает, куда повёрнуто здание - var foundation = Part(root, PrimitiveType.Cube, Vector3.zero, Vector3.one, Mat(new Color(0.6f, 0.57f, 0.5f))); + var foundation = Part(root, PrimitiveType.Cube, Vector3.zero, Vector3.one, Mat(new Color(0.62f, 0.58f, 0.52f))); foundation.name = "Foundation"; var labelGo = new GameObject("Label"); @@ -298,6 +325,17 @@ public class City3DController : MonoBehaviour return root; } + // Деталь в единичном объёме здания (Body растягивается под габарит и высоту) + static void Unit(Transform body, PrimitiveType type, Vector3 localPos, Vector3 scale, Color color) + { + var p = GameObject.CreatePrimitive(type); + Destroy(p.GetComponent()); + p.transform.SetParent(body, false); + p.transform.localPosition = localPos; + p.transform.localScale = scale; + p.GetComponent().sharedMaterial = Mat(color); + } + // Положение, поворот, фундамент по данным карты (или призрака при переносе) void UpdateBuildingView(string id) { if (map.TryGetBuilding(id, out var r)) LayoutView(buildingViews[id], buildingType[id], r.X, r.Y, r.Rotation); } @@ -313,9 +351,11 @@ public class City3DController : MonoBehaviour float top = max / 1000f, bottom = min / 1000f - 0.3f; root.transform.position = new Vector3(x + w / 2f, top, y + h / 2f); + // Body -- в размерах без поворота и повёрнут сам: конёк крыши и приметы поворачиваются вместе со зданием var body = root.transform.Find("Body"); - body.localScale = new Vector3(w - 0.3f, t.Height, h - 0.3f); - body.localPosition = new Vector3(0, t.Height / 2f, 0); + body.localScale = new Vector3(t.W - 0.3f, t.Height, t.H - 0.3f); + body.localPosition = Vector3.zero; + body.localRotation = Quaternion.Euler(0, -rot, 0); var foundation = root.transform.Find("Foundation"); foundation.localScale = new Vector3(w - 0.1f, top - bottom, h - 0.1f); foundation.localPosition = new Vector3(0, (bottom - top) / 2f, 0); diff --git a/Assets/Scripts/WorldMap/IntelligenceOverlay.cs b/Assets/Scripts/WorldMap/IntelligenceOverlay.cs index 8c5de56..1b8eaa9 100644 --- a/Assets/Scripts/WorldMap/IntelligenceOverlay.cs +++ b/Assets/Scripts/WorldMap/IntelligenceOverlay.cs @@ -54,7 +54,11 @@ public class IntelligenceOverlay : MonoBehaviour }; pixels = new Color32[gridW * gridH]; // 1 пиксель = 1 клетка карты; текстура натянута на всю землю (UV 0..1 -- от левого нижнего угла карты) - if (ground != null) ground.sharedMaterial.mainTexture = fog; + if (ground != null) + { + if (ground.sharedMaterial.HasProperty("_BaseMap")) ground.sharedMaterial.SetTexture("_BaseMap", fog); + if (ground.sharedMaterial.HasProperty("_MainTex")) ground.sharedMaterial.SetTexture("_MainTex", fog); + } RefreshFog(); } @@ -86,7 +90,9 @@ public class IntelligenceOverlay : MonoBehaviour int y = cy * CellSize + CellSize / 2 - WorldMapLayout.HalfHeight; MapVisibility v = intel.GetVisibility(x, y); byte a = v == MapVisibility.Observed ? (byte)0 : v == MapVisibility.Explored ? explored : unexplored; - Color c = Color.Lerp(GroundColor, FogColor, a / 255f); // туман -- в цвете земли + // трава разных оттенков (пятнами), поверх -- туман + Color grass = Color.Lerp(GroundColor, new Color(0.42f, 0.55f, 0.3f), Mathf.PerlinNoise(cx * 0.35f, cy * 0.35f)); + Color c = Color.Lerp(grass, FogColor, a / 255f); c.a = 1f; pixels[cy * gridW + cx] = c; } diff --git a/Assets/Scripts/WorldMap/Map3D.cs b/Assets/Scripts/WorldMap/Map3D.cs index e1640bf..236d2bb 100644 --- a/Assets/Scripts/WorldMap/Map3D.cs +++ b/Assets/Scripts/WorldMap/Map3D.cs @@ -6,11 +6,15 @@ using UnityEngine.Rendering.Universal; // 3D НА ПРИМИТИВАХ: общие помощники для карты мира и 3D-поселения (переход на 2.5D, 04.11.2026). // Материал берётся у встроенного примитива (шейдер URP по умолчанию точно есть в сборке), цвета кешируются. +// Свои простые формы (конус, пирамида, двускатная крыша) -- для деревьев, шатров, крыш. // Подписи (TextMeshPro) поворачиваются к камере (FaceCamera). public static class Map3D { static Material baseMaterial; static readonly Dictionary materials = new Dictionary(); + static Mesh cone, pyramid, roof; + + // ---------------- Материалы ---------------- public static Material Mat(Color color) { @@ -23,12 +27,32 @@ public static class Map3D } if (!materials.TryGetValue(color, out var m) || m == null) { - m = new Material(baseMaterial) { color = color }; + m = new Material(baseMaterial); + SetColor(m, color); + if (m.HasProperty("_Smoothness")) m.SetFloat("_Smoothness", 0.12f); // матовые, без пластикового блеска materials[color] = m; } return m; } + // URP берёт цвет из _BaseColor, встроенный шейдер -- из _Color: ставим оба + public static void SetColor(Material m, Color color) + { + if (m.HasProperty("_BaseColor")) m.SetColor("_BaseColor", color); + if (m.HasProperty("_Color")) m.SetColor("_Color", color); + } + + // Материал с текстурой (земля): своя копия, не из кеша + public static Material TexturedMat(Texture texture) + { + var m = new Material(Mat(Color.white)); + if (m.HasProperty("_BaseMap")) m.SetTexture("_BaseMap", texture); + if (m.HasProperty("_MainTex")) m.SetTexture("_MainTex", texture); + return m; + } + + // ---------------- Простые формы ---------------- + // Объект на земле: основание size.x * size.y (по X и Z), высота height, низ -- на уровне ground.y public static Renderer Block(Transform parent, string name, PrimitiveType type, Color color, Vector3 ground, Vector2 size, float height, bool collider = false) @@ -46,6 +70,144 @@ public static class Map3D return r; } + // Своя форма (конус, пирамида, крыша): основание size.x * size.y, высота height, низ -- на ground + public static Renderer Shape(Transform parent, string name, Mesh mesh, Color color, Vector3 ground, Vector2 size, float height, float yaw = 0f) + { + var go = new GameObject(name); + go.transform.SetParent(parent, false); + go.transform.position = ground; + go.transform.rotation = Quaternion.Euler(0, yaw, 0); + go.transform.localScale = new Vector3(size.x, height, size.y); + go.AddComponent().sharedMesh = mesh; + var r = go.AddComponent(); + r.sharedMaterial = Mat(color); + return r; + } + + // Конус: основание -- круг диаметром 1 на y = 0, вершина -- (0, 1, 0) + public static Mesh Cone => cone != null ? cone : cone = BuildCone(10); + // Пирамида: основание 1 x 1, вершина -- (0, 1, 0) + public static Mesh Pyramid => pyramid != null ? pyramid : pyramid = BuildCone(4, 45f, 0.7071f); + // Двускатная крыша: основание 1 x 1, конёк вдоль X на высоте 1 + public static Mesh Roof => roof != null ? roof : roof = BuildRoof(); + + static Mesh BuildCone(int sides, float startDeg = 0f, float radius = 0.5f) + { + var v = new List(); + var t = new List(); + for (int i = 0; i < sides; i++) + { + float a0 = (startDeg + 360f * i / sides) * Mathf.Deg2Rad, a1 = (startDeg + 360f * (i + 1) / sides) * Mathf.Deg2Rad; + Vector3 p0 = new Vector3(Mathf.Cos(a0) * radius, 0, Mathf.Sin(a0) * radius); + Vector3 p1 = new Vector3(Mathf.Cos(a1) * radius, 0, Mathf.Sin(a1) * radius); + TwoSided(v, t, p0, Vector3.up, p1); // бок -- свои вершины: грани с резкими нормалями + TwoSided(v, t, p0, p1, Vector3.zero); // дно + } + var mesh = new Mesh { name = "Cone" + sides, vertices = v.ToArray(), triangles = t.ToArray() }; + mesh.RecalculateNormals(); + mesh.RecalculateBounds(); + return mesh; + } + + // Треугольник с обеих сторон: у каждой стороны свои вершины и своя нормаль + static void TwoSided(List v, List t, Vector3 p, Vector3 q, Vector3 r) + { + int i = v.Count; + v.Add(p); v.Add(q); v.Add(r); + t.Add(i); t.Add(i + 1); t.Add(i + 2); + i = v.Count; + v.Add(p); v.Add(r); v.Add(q); + t.Add(i); t.Add(i + 1); t.Add(i + 2); + } + + static Mesh BuildRoof() + { + Vector3 a = new Vector3(-0.5f, 0, -0.5f), b = new Vector3(0.5f, 0, -0.5f), c = new Vector3(0.5f, 0, 0.5f), d = new Vector3(-0.5f, 0, 0.5f); + Vector3 r0 = new Vector3(-0.5f, 1, 0), r1 = new Vector3(0.5f, 1, 0); + var v = new List(); + var t = new List(); + void Tri(Vector3 p, Vector3 q, Vector3 r) => TwoSided(v, t, p, q, r); + Tri(a, r0, r1); Tri(a, r1, b); // скат к -Z + Tri(c, r1, r0); Tri(c, r0, d); // скат к +Z + Tri(d, r0, a); Tri(b, r1, c); // торцы + Tri(a, b, c); Tri(a, c, d); // дно + var mesh = new Mesh { name = "Roof", vertices = v.ToArray(), triangles = t.ToArray() }; + mesh.RecalculateNormals(); + mesh.RecalculateBounds(); + return mesh; + } + + // ---------------- Готовые фигуры ---------------- + + // Домик: стены w x d (по X и Z), высота стен h, двускатная крыша; поворот yaw + public static Transform House(Transform parent, string name, Vector3 ground, float w, float d, float h, Color walls, Color roofColor, float yaw = 0f) + { + var root = new GameObject(name).transform; + root.SetParent(parent, false); + root.position = ground; + root.rotation = Quaternion.Euler(0, yaw, 0); + var body = Block(root, "Walls", PrimitiveType.Cube, walls, ground, new Vector2(w, d), h); + body.transform.localPosition = new Vector3(0, h / 2f, 0); + body.transform.localRotation = Quaternion.identity; + var top = Shape(root, "Roof", Roof, roofColor, ground, new Vector2(w * 1.12f, d * 1.15f), Mathf.Max(0.25f, Mathf.Min(w, d) * 0.55f)); + top.transform.localPosition = new Vector3(0, h, 0); + top.transform.localRotation = Quaternion.identity; + return root; + } + + static readonly Color Trunk = new Color(0.42f, 0.28f, 0.16f); + static readonly Color[] Leaves = { new Color(0.2f, 0.46f, 0.2f), new Color(0.26f, 0.52f, 0.22f), new Color(0.32f, 0.55f, 0.2f) }; + static readonly Color[] Pine = { new Color(0.12f, 0.36f, 0.22f), new Color(0.16f, 0.4f, 0.24f) }; + + // Дерево: ель (конусы) или лиственное (шар). variant -- для разнообразия, scale -- размер + public static Transform Tree(Transform parent, string name, Vector3 ground, float scale, int variant) + { + var root = new GameObject(name).transform; + root.SetParent(parent, false); + root.position = ground; + var trunk = Block(root, "Trunk", PrimitiveType.Cylinder, Trunk, ground, Vector2.one * 0.18f * scale, 0.6f * scale); + if (variant % 3 == 0) + { + var crown = Block(root, "Crown", PrimitiveType.Sphere, Leaves[variant % Leaves.Length], ground + Vector3.up * 0.45f * scale, + new Vector2(1.0f, 0.95f) * scale, 1.05f * scale); + } + else + { + Color c = Pine[variant % Pine.Length]; + Shape(root, "Pine1", Cone, c, ground + Vector3.up * 0.35f * scale, Vector2.one * 1.0f * scale, 1.0f * scale); + Shape(root, "Pine2", Cone, c, ground + Vector3.up * 0.8f * scale, Vector2.one * 0.72f * scale, 0.85f * scale); + } + return root; + } + + static readonly Color[] Stones = { new Color(0.55f, 0.55f, 0.56f), new Color(0.5f, 0.48f, 0.45f), new Color(0.62f, 0.6f, 0.58f) }; + + // Валун: сплюснутый шар, повёрнутый случайно по variant + public static Transform Rock(Transform parent, string name, Vector3 ground, float size, int variant) + { + var root = new GameObject(name).transform; + root.SetParent(parent, false); + root.position = ground; + var r = Block(root, "Stone", PrimitiveType.Sphere, Stones[variant % Stones.Length], ground + Vector3.down * 0.1f * size, + new Vector2(size, size * 0.85f), size * 0.65f); + r.transform.rotation = Quaternion.Euler(variant * 7 % 20, variant * 53 % 360, 0); + if (size > 1.2f) + { + var small = Block(root, "Stone2", PrimitiveType.Sphere, Stones[(variant + 1) % Stones.Length], + ground + new Vector3(size * 0.4f, 0, size * 0.3f), Vector2.one * size * 0.45f, size * 0.35f); + } + return root; + } + + // Флаг на древке + public static void Flag(Transform parent, Vector3 ground, float height, Color color) + { + Block(parent, "Pole", PrimitiveType.Cylinder, new Color(0.35f, 0.25f, 0.15f), ground, Vector2.one * 0.06f, height); + var f = Block(parent, "Flag", PrimitiveType.Cube, color, ground + new Vector3(0.22f, height - 0.32f, 0), new Vector2(0.42f, 0.03f), 0.28f); + } + + // ---------------- Подписи, свет, рендерер ---------------- + // Подпись над объектом, всегда лицом к камере public static TextMeshPro Label(Transform parent, string text, Vector3 position, float fontSize) { @@ -64,11 +226,11 @@ public static class Map3D return tmp; } - // Камере -- обычный 3D-рендерер URP (второй в списке; добавляет Tools -> Bogatyri -> 3D-город). + // Камере -- обычный 3D-рендерер URP (добавляет Tools -> Bogatyri -> 3D-город: создать сцену City3D). // Нет его -- остаётся рендерер по умолчанию, и в лог пишется, что делать. public static void UseRenderer3D(Camera cam) { - var pipeline = GraphicsSettings.currentRenderPipeline as UniversalRenderPipelineAsset; + var pipeline = (GraphicsSettings.currentRenderPipeline ?? QualitySettings.renderPipeline) as UniversalRenderPipelineAsset; var data = cam != null ? cam.GetUniversalAdditionalCameraData() : null; if (pipeline == null || data == null) return; var list = pipeline.rendererDataList; @@ -77,19 +239,39 @@ public static class Map3D Debug.LogWarning("[3D] В URP нет обычного 3D-рендерера: выполните Tools -> Bogatyri -> 3D-город: создать сцену City3D (он добавится). Без него 3D рисуется без света."); } - // Солнце и общий свет, если в сцене их нет (2D-сцены света не имели) - public static void EnsureSun() + // Солнце, общий свет, небо и дымка вдали (2D-сцены света не имели) + public static void SetUpLook(Camera cam, float fogStart, float fogEnd) { + Light sun = null; foreach (var l in Object.FindObjectsByType(FindObjectsSortMode.None)) - if (l.type == LightType.Directional) return; - var sunGo = new GameObject("Sun"); - var sun = sunGo.AddComponent(); - sun.type = LightType.Directional; - sun.intensity = 1.15f; + if (l.type == LightType.Directional) { sun = l; break; } + if (sun == null) + { + sun = new GameObject("Sun").AddComponent(); + sun.type = LightType.Directional; + } + sun.color = new Color(1f, 0.95f, 0.85f); // тёплое солнце + sun.intensity = 1.25f; sun.shadows = LightShadows.Soft; - sunGo.transform.rotation = Quaternion.Euler(50f, -30f, 0f); - RenderSettings.ambientMode = AmbientMode.Flat; - RenderSettings.ambientLight = new Color(0.55f, 0.58f, 0.62f); + sun.shadowStrength = 0.65f; + sun.transform.rotation = Quaternion.Euler(48f, -35f, 0f); + + RenderSettings.ambientMode = AmbientMode.Trilight; // небо голубое, горизонт светлый, снизу -- тёплая земля + RenderSettings.ambientSkyColor = new Color(0.62f, 0.72f, 0.86f); + RenderSettings.ambientEquatorColor = new Color(0.55f, 0.6f, 0.58f); + RenderSettings.ambientGroundColor = new Color(0.35f, 0.32f, 0.25f); + + Color sky = new Color(0.66f, 0.8f, 0.94f); + RenderSettings.fog = true; + RenderSettings.fogMode = FogMode.Linear; + RenderSettings.fogColor = sky; + RenderSettings.fogStartDistance = fogStart; + RenderSettings.fogEndDistance = fogEnd; + if (cam != null) + { + cam.clearFlags = CameraClearFlags.SolidColor; + cam.backgroundColor = sky; + } } } diff --git a/Assets/Scripts/WorldMap/WorldMapController.cs b/Assets/Scripts/WorldMap/WorldMapController.cs index da02881..6218411 100644 --- a/Assets/Scripts/WorldMap/WorldMapController.cs +++ b/Assets/Scripts/WorldMap/WorldMapController.cs @@ -214,8 +214,8 @@ public class WorldMapController : MonoBehaviour // ---------------- КАРТА ---------------- - // Карта лежит на земле (2.5D): замок -- стена с башней, свой город -- кубик на белой площадке, - // соседи -- кубики, лагеря -- цилиндры, ресурсные точки -- ромбы. Подписи висят над объектами. + // Карта лежит на земле (2.5D): замок -- стены с башнями, свой город и соседи -- домики на площадке, + // лагеря -- шатры с костром, ресурсные точки -- кристаллы цвета ресурса. Подписи висят над объектами. Renderer BuildMap() { float width = WorldMapLayout.HalfWidth / 1000f * 2f; @@ -225,39 +225,114 @@ public class WorldMapController : MonoBehaviour foreach (var node in World.Nodes) { Vector3 pos = ToWorld(node); + int seed = node.id.GetHashCode() & 0x7fff; switch (node.type) { case MapNodeType.BossCastle: - Map3D.Block(mapRoot, node.id, PrimitiveType.Cube, CastleColor, pos, Vector2.one * CastleSize, CastleHeight * 0.6f); - Map3D.Block(mapRoot, node.id + "_Keep", PrimitiveType.Cube, new Color(0.3f, 0.08f, 0.08f), pos, Vector2.one * CastleSize * 0.5f, CastleHeight * 1.8f); - Map3D.Label(mapRoot, node.displayName, pos + Vector3.up * (CastleHeight * 1.8f + 1.1f), 4f); - garrisonLabels.Add((node, Map3D.Label(mapRoot, "", pos + Vector3.up * (CastleHeight * 1.8f + 0.45f), 3f))); + BuildCastle(node, pos); + Map3D.Label(mapRoot, node.displayName, pos + Vector3.up * (CastleHeight * 1.8f + 1.4f), 4f); + garrisonLabels.Add((node, Map3D.Label(mapRoot, "", pos + Vector3.up * (CastleHeight * 1.8f + 0.75f), 3f))); break; case MapNodeType.PlayerCity: - // Белая площадка -- чтобы свой город сразу бросался в глаза - Map3D.Block(mapRoot, node.id + "_Frame", PrimitiveType.Cube, Color.white, pos, Vector2.one * (CitySize + 0.35f), 0.12f); - Map3D.Block(mapRoot, node.id, PrimitiveType.Cube, PlayerCityColor, pos + Vector3.up * 0.12f, Vector2.one * CitySize, CityHeight); - Map3D.Label(mapRoot, node.displayName, pos + Vector3.up * (CityHeight + 0.75f), 3.2f); + // Белая площадка и синие крыши -- чтобы свой город сразу бросался в глаза + Map3D.Block(mapRoot, node.id + "_Frame", PrimitiveType.Cube, Color.white, pos, Vector2.one * (CitySize + 0.45f), 0.08f); + BuildTown(node.id, pos + Vector3.up * 0.08f, new Color(0.92f, 0.88f, 0.78f), PlayerCityColor, seed, 4); + Map3D.Flag(mapRoot, pos + new Vector3(0.55f, 0.08f, 0.55f), 1.5f, new Color(0.95f, 0.8f, 0.2f)); + Map3D.Label(mapRoot, node.displayName, pos + Vector3.up * (CityHeight + 1.0f), 3.2f); break; case MapNodeType.NeighborCity: - Map3D.Block(mapRoot, node.id, PrimitiveType.Cube, NeighborCityColor, pos, Vector2.one * CitySize, CityHeight); - garrisonLabels.Add((node, Map3D.Label(mapRoot, "", pos + Vector3.up * (CityHeight + 0.85f), 2.6f))); + Map3D.Block(mapRoot, node.id + "_Yard", PrimitiveType.Cube, new Color(0.62f, 0.55f, 0.42f), pos, Vector2.one * (CitySize + 0.25f), 0.05f); + BuildTown(node.id, pos + Vector3.up * 0.05f, new Color(0.85f, 0.75f, 0.58f), new Color(0.62f, 0.25f, 0.18f), seed, 3); + garrisonLabels.Add((node, Map3D.Label(mapRoot, "", pos + Vector3.up * (CityHeight + 1.05f), 2.6f))); break; case MapNodeType.Camp: - // Цилиндр, а не куб: лагерь не путается с городом - var sprite = Map3D.Block(mapRoot, node.id, PrimitiveType.Cylinder, CampColor, pos, Vector2.one * CampSize, CampHeight); - var label = Map3D.Label(mapRoot, "", pos + Vector3.up * (CampHeight + 0.8f), 2.3f); + // Шатры разбойников с костром; цвет главного шатра -- разбит лагерь или нет + var sprite = Map3D.Shape(mapRoot, node.id, Map3D.Cone, CampColor, pos, Vector2.one * CampSize, CampHeight * 1.6f); + Map3D.Shape(mapRoot, node.id + "_Tent2", Map3D.Cone, new Color(0.55f, 0.42f, 0.3f), pos + new Vector3(0.45f, 0, -0.25f), Vector2.one * CampSize * 0.6f, CampHeight * 1.1f); + Map3D.Block(mapRoot, node.id + "_Fire", PrimitiveType.Cube, new Color(1f, 0.55f, 0.15f), pos + new Vector3(-0.35f, 0, -0.35f), Vector2.one * 0.14f, 0.12f); + var label = Map3D.Label(mapRoot, "", pos + Vector3.up * (CampHeight * 1.6f + 0.85f), 2.3f); campViews.Add((node, sprite, label)); break; } } + BuildScenery(width, height); return ground; } + // Замок Змея Горыныча: квадрат стен, четыре угловые башни с красными крышами, высокая главная башня + void BuildCastle(MapNode node, Vector3 pos) + { + Color stone = new Color(0.5f, 0.45f, 0.42f), dark = new Color(0.32f, 0.28f, 0.27f), roofRed = new Color(0.6f, 0.12f, 0.1f); + float half = CastleSize / 2f, wall = 0.18f, wallH = CastleHeight * 0.55f; + Map3D.Block(mapRoot, node.id + "_WallN", PrimitiveType.Cube, stone, pos + new Vector3(0, 0, half - wall / 2), new Vector2(CastleSize, wall), wallH); + Map3D.Block(mapRoot, node.id + "_WallS", PrimitiveType.Cube, stone, pos + new Vector3(0, 0, -half + wall / 2), new Vector2(CastleSize, wall), wallH); + Map3D.Block(mapRoot, node.id + "_WallE", PrimitiveType.Cube, stone, pos + new Vector3(half - wall / 2, 0, 0), new Vector2(wall, CastleSize), wallH); + Map3D.Block(mapRoot, node.id + "_WallW", PrimitiveType.Cube, stone, pos + new Vector3(-half + wall / 2, 0, 0), new Vector2(wall, CastleSize), wallH); + foreach (var corner in new[] { new Vector3(1, 0, 1), new Vector3(-1, 0, 1), new Vector3(1, 0, -1), new Vector3(-1, 0, -1) }) + { + Vector3 c = pos + corner * (half - 0.12f); + Map3D.Block(mapRoot, node.id + "_Tower", PrimitiveType.Cylinder, stone, c, Vector2.one * 0.42f, CastleHeight); + Map3D.Shape(mapRoot, node.id + "_TowerRoof", Map3D.Cone, roofRed, c + Vector3.up * CastleHeight, Vector2.one * 0.55f, 0.5f); + } + Map3D.Block(mapRoot, node.id + "_Keep", PrimitiveType.Cube, dark, pos, Vector2.one * CastleSize * 0.42f, CastleHeight * 1.8f); + Map3D.Shape(mapRoot, node.id + "_KeepRoof", Map3D.Pyramid, roofRed, pos + Vector3.up * CastleHeight * 1.8f, Vector2.one * CastleSize * 0.5f, 0.7f, 45f); + Map3D.Flag(mapRoot, pos + Vector3.up * (CastleHeight * 1.8f + 0.6f), 0.6f, new Color(0.15f, 0.55f, 0.2f)); + } + + // Городок: несколько домиков разного размера на площадке города + void BuildTown(string id, Vector3 pos, Color walls, Color roofColor, int seed, int houses) + { + var rng = new System.Random(seed); + Vector3[] spots = { new Vector3(-0.28f, 0, -0.22f), new Vector3(0.28f, 0, -0.2f), new Vector3(-0.2f, 0, 0.3f), new Vector3(0.25f, 0, 0.28f) }; + for (int i = 0; i < houses && i < spots.Length; i++) + { + float w = 0.38f + (float)rng.NextDouble() * 0.12f, h = 0.3f + (float)rng.NextDouble() * 0.25f; + Map3D.House(mapRoot, id + "_House" + i, pos + spots[i], w, w * 0.8f, h, walls, roofColor, rng.Next(2) * 90f); + } + } + + // Рощи и камни между объектами -- только украшение (не мешают тапам и походам) + void BuildScenery(float width, float height) + { + var rng = new System.Random(WorldMapLayout.DefaultSeed); + int trees = 0; + for (int attempt = 0; attempt < 3000 && trees < 170; attempt++) + { + float x = ((float)rng.NextDouble() - 0.5f) * (width - 1f), z = ((float)rng.NextDouble() - 0.5f) * (height - 1f); + if (!FarFromNodes(x, z, 1.7f)) continue; + // рощами: рядом с деревом -- ещё 1-2 + int group = 1 + rng.Next(3); + for (int g = 0; g < group; g++) + { + float gx = x + ((float)rng.NextDouble() - 0.5f) * 1.2f, gz = z + ((float)rng.NextDouble() - 0.5f) * 1.2f; + if (!FarFromNodes(gx, gz, 1.4f)) continue; + Map3D.Tree(mapRoot, "Tree", new Vector3(gx, 0, gz), 0.55f + (float)rng.NextDouble() * 0.35f, rng.Next(6)); + trees++; + } + } + for (int i = 0, rocks = 0; i < 600 && rocks < 30; i++) + { + float x = ((float)rng.NextDouble() - 0.5f) * (width - 1f), z = ((float)rng.NextDouble() - 0.5f) * (height - 1f); + if (!FarFromNodes(x, z, 1.5f)) continue; + Map3D.Rock(mapRoot, "Rock", new Vector3(x, 0, z), 0.3f + (float)rng.NextDouble() * 0.35f, rng.Next(9)); + rocks++; + } + } + + bool FarFromNodes(float x, float z, float distance) + { + foreach (var n in World.Nodes) + { + float dx = n.x / 1000f - x, dz = n.y / 1000f - z; + if (dx * dx + dz * dz < distance * distance) return false; + } + return true; + } + // Земля -- плоскость с текстурой: её раскрашивает туман (IntelligenceOverlay). UV (0,0) -- левый нижний угол карты Renderer CreateGround(float width, float height) { @@ -271,7 +346,7 @@ public class WorldMapController : MonoBehaviour go.transform.SetParent(mapRoot, false); go.AddComponent().sharedMesh = mesh; var r = go.AddComponent(); - r.sharedMaterial = new Material(Map3D.Mat(GroundColor)) { color = Color.white }; + r.sharedMaterial = Map3D.TexturedMat(Texture2D.whiteTexture); // текстуру с туманом даст IntelligenceOverlay return r; } @@ -287,7 +362,7 @@ public class WorldMapController : MonoBehaviour mapCamera.mapHalfSize = new Vector2(WorldMapLayout.HalfWidth / 1000f, WorldMapLayout.HalfHeight / 1000f); mapCamera.distance = StartDistance; - Map3D.EnsureSun(); // в 2D-сцене света не было + Map3D.SetUpLook(mapCamera.GetComponent(), 22f, 60f); // солнце, небо, дымка вдали (в 2D-сцене света не было) CenterOnMyCity(); } @@ -312,7 +387,7 @@ public class WorldMapController : MonoBehaviour foreach (var (node, sprite, label) in campViews) { long left = World.CampRespawnInMs(node.id, now); - sprite.sharedMaterial = Map3D.Mat(left > 0 ? CampClearedColor : CampColor); + sprite.sharedMaterial = Map3D.Mat(left > 0 ? new Color(0.4f, 0.4f, 0.4f) : CampColor); label.text = left > 0 // разбил его сам игрок -- это он знает ? $"{node.displayName}\nразбит, вернётся через {FormatMinutes(left)}" : $"{node.displayName}, ур. {node.level}\n{GarrisonText(node)}"; @@ -329,8 +404,11 @@ public class WorldMapController : MonoBehaviour { MapNode node = World.GetNode(p.id); Vector3 pos = ToWorld(node); - var sprite = Map3D.Block(mapRoot, p.id, PrimitiveType.Cube, PointColors[p.kind], pos, Vector2.one * PointSize, PointHeight); - sprite.transform.rotation = Quaternion.Euler(0, 45, 0); // ромб -- не путается с городом и лагерем + // Кристалл цвета ресурса на камне -- не путается с городом и лагерем + var holder = new GameObject(p.id).transform; // исчезнет точка -- уберём всё вместе + holder.SetParent(mapRoot, false); + Map3D.Rock(holder, "Base", pos, PointSize * 0.9f, p.kind + 3); + var sprite = Map3D.Shape(holder, "Crystal", Map3D.Pyramid, PointColors[p.kind], pos + Vector3.up * 0.1f, Vector2.one * PointSize * 0.75f, PointHeight * 1.8f, 45f); var label = Map3D.Label(mapRoot, "", pos + Vector3.up * (PointHeight + 0.75f), 2.2f); view = (sprite, label); pointViews[p.id] = view;