using System.Collections.Generic; using UnityEngine; namespace Code.OfficeGame.BattleMode.Actions { public class EntityPreviewVisuals : MonoBehaviour { [Header("Материалы для MKToon")] [Tooltip("Полупрозрачный материал для статичного слепка в точке снапшота")] [SerializeField] private Material snapshotGhostMaterial; [Tooltip( "Опционально: материал для самой движущейся модели во время превью (если null — меняется только Rim/Tint)")] [SerializeField] private Material activePreviewMaterial; [Header("Настройки подсветки MKToon (если activePreviewMaterial не задан)")] [SerializeField] private Color previewAlbedoTint = new Color(0.6f, 0.85f, 1f, 1f); [SerializeField, ColorUsage(true, true)] private Color previewRimColor = new Color(0f, 1.5f, 2f, 1f); private SkinnedMeshRenderer[] _skinnedRenderers; private MeshRenderer[] _meshRenderers; // Кэш оригинальных материалов private readonly Dictionary _originalMaterials = new(); // Пул объектов для статичного слепка (чтобы не делать Instantiate и new Mesh каждый цикл превью) private GameObject _snapshotGhostRoot; private readonly List _bakedMeshes = new(); private readonly List _ghostMeshFilters = new(); private readonly List _ghostMeshRenderers = new(); private MaterialPropertyBlock _propBlock; private static readonly int AlbedoColorId = Shader.PropertyToID("_AlbedoColor"); private static readonly int RimColorId = Shader.PropertyToID("_RimColor"); private void Awake() { _propBlock = new MaterialPropertyBlock(); _skinnedRenderers = GetComponentsInChildren(); _meshRenderers = GetComponentsInChildren(); // Сохраняем родные материалы MKToon foreach (var sr in _skinnedRenderers) _originalMaterials[sr] = sr.sharedMaterials; foreach (var mr in _meshRenderers) _originalMaterials[mr] = mr.sharedMaterials; InitializeGhostPool(); } private void InitializeGhostPool() { _snapshotGhostRoot = new GameObject($"{gameObject.name}_SnapshotGhost"); _snapshotGhostRoot.SetActive(false); // Создаём легковесные меш-объекты под каждый SkinnedMeshRenderer персонажа for (int i = 0; i < _skinnedRenderers.Length; i++) { var partGo = new GameObject($"GhostPart_{i}"); partGo.transform.SetParent(_snapshotGhostRoot.transform, false); var mf = partGo.AddComponent(); var mr = partGo.AddComponent(); // Отключаем тени у призрака для чистоты картинки и производительности mr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; var bakedMesh = new Mesh { name = $"BakedGhostMesh_{i}" }; mf.sharedMesh = bakedMesh; _bakedMeshes.Add(bakedMesh); _ghostMeshFilters.Add(mf); _ghostMeshRenderers.Add(mr); } } /// /// Вызывается перед стартом цикла PreviewBattleLoopAsync (в момент снятия снапшота) /// public void ShowPreviewState(bool spawnStaticSnapshotGhost = true) { // 1. Запекаем полупрозрачную статую в текущей точке снапшота if (spawnStaticSnapshotGhost && snapshotGhostMaterial != null) { _snapshotGhostRoot.transform.SetPositionAndRotation(transform.position, transform.rotation); for (int i = 0; i < _skinnedRenderers.Length; i++) { var sr = _skinnedRenderers[i]; sr.BakeMesh(_bakedMeshes[i]); // Мгновенный снимок позы скелета! var ghostTransform = _ghostMeshFilters[i].transform; ghostTransform.SetPositionAndRotation(sr.transform.position, sr.transform.rotation); // ghostTransform.localScale = sr.transform.lossyScale; // Назначаем прозрачный материал на все саб-меши int subMeshCount = _bakedMeshes[i].subMeshCount; Material[] ghostMats = new Material[Mathf.Max(1, subMeshCount)]; for (int m = 0; m < ghostMats.Length; m++) ghostMats[m] = snapshotGhostMaterial; _ghostMeshRenderers[i].sharedMaterials = ghostMats; } _snapshotGhostRoot.SetActive(true); } // 2. Меняем внешний вид самой модели, которая сейчас побежит в превью ApplyPreviewLookToMainModel(); } /// /// Вызывается в блоке finally в PreviewBattleLoopAsync /// public void HidePreviewState() { if (_snapshotGhostRoot != null) _snapshotGhostRoot.SetActive(false); RestoreMainModelLook(); } private void ApplyPreviewLookToMainModel() { foreach (var (renderer, origMats) in _originalMaterials) { if (renderer == null) continue; // Способ 1: Если задан отдельный материал для превью — подменяем sharedMaterials if (activePreviewMaterial != null) { Material[] previewMats = new Material[origMats.Length]; for (int i = 0; i < previewMats.Length; i++) previewMats[i] = activePreviewMaterial; renderer.sharedMaterials = previewMats; } // Способ 2: Иначе подкрашиваем родной MKToon через MaterialPropertyBlock (без аллокаций) else { renderer.GetPropertyBlock(_propBlock); _propBlock.SetColor(AlbedoColorId, previewAlbedoTint); _propBlock.SetColor(RimColorId, previewRimColor); renderer.SetPropertyBlock(_propBlock); } } } private void RestoreMainModelLook() { foreach (var (renderer, origMats) in _originalMaterials) { if (renderer == null) continue; if (activePreviewMaterial != null) { renderer.sharedMaterials = origMats; } else { renderer.SetPropertyBlock(null); // Сбрасывает все переопределения PropertyBlock } } } private void OnDestroy() { foreach (var mesh in _bakedMeshes) { if (mesh != null) Destroy(mesh); } if (_snapshotGhostRoot != null) Destroy(_snapshotGhostRoot); } } }