3D-сцены красивее: цвета, свет, небо, дымка; домики с крышами, замок, шатры, деревья, камни

Цвет -- в _BaseColor (URP), раньше всё было белым. Настройка 3D-рендерера добавляет его в URP Asset
каждого уровня качества (в проекте URP задан только в Quality) + отдельная команда меню.
Карта мира: замок со стенами и башнями, города -- домики, лагеря -- шатры с костром, кристаллы точек, рощи.
3D-поселение: здания с двускатными крышами и приметами, ели и лиственные, песчаные берега, снег на скалах.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Alexander
2026-11-04 15:03:16 +01:00
co-authored by Claude Opus 5.5
parent 9a6e5a2e7b
commit 9a91926b2c
7 changed files with 866 additions and 101 deletions
+35 -14
View File
@@ -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<UniversalRenderPipelineAsset>();
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()
+431
View File
@@ -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}
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: c1c34719f0605c247a3575d077f3d656
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+92 -52
View File
@@ -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<Renderer>().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<MeshRenderer>().sharedMaterial = mat;
go.AddComponent<MeshRenderer>().sharedMaterial = Map3D.TexturedMat(groundTexture);
go.AddComponent<MeshCollider>().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<Renderer>().sharedMaterial = Mat(new Color(0.25f, 0.5f, 0.85f));
water.GetComponent<Renderer>().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<Collider>());
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<Renderer>().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<Renderer>().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<BoxCollider>(); // тап по зданию попадает в него целиком
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<Collider>());
p.transform.SetParent(body, false);
p.transform.localPosition = localPos;
p.transform.localScale = scale;
p.GetComponent<Renderer>().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);
@@ -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;
}
+195 -13
View File
@@ -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<Color, Material> materials = new Dictionary<Color, Material>();
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<MeshFilter>().sharedMesh = mesh;
var r = go.AddComponent<MeshRenderer>();
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<Vector3>();
var t = new List<int>();
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<Vector3> v, List<int> 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<Vector3>();
var t = new List<int>();
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<Light>(FindObjectsSortMode.None))
if (l.type == LightType.Directional) return;
var sunGo = new GameObject("Sun");
var sun = sunGo.AddComponent<Light>();
sun.type = LightType.Directional;
sun.intensity = 1.15f;
if (l.type == LightType.Directional) { sun = l; break; }
if (sun == null)
{
sun = new GameObject("Sun").AddComponent<Light>();
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;
}
}
}
+98 -20
View File
@@ -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<MeshFilter>().sharedMesh = mesh;
var r = go.AddComponent<MeshRenderer>();
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<Camera>(), 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<color=#BBBBBB>разбит, вернётся через {FormatMinutes(left)}</color>"
: $"{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;