423 lines
14 KiB
C#
423 lines
14 KiB
C#
using UnityEngine;
|
||
using UnityEngine.UI;
|
||
using DG.Tweening;
|
||
using System.Collections;
|
||
using AibisDream;
|
||
using AibisDream.FixSystem;
|
||
using AibisDream.Kit;
|
||
|
||
public class HeatMapController : MonoBehaviour
|
||
{
|
||
public Material heatMaterial; // 需要传递数据的材质
|
||
|
||
public Transform[] Pos;
|
||
|
||
public Button coolingButton;
|
||
|
||
private BodyModuleSystem bodyModuleSystem;
|
||
|
||
public GameObject heatmapGameobject;
|
||
|
||
public RectTransform imageToMove; // 需要移动的Image
|
||
public Transform imageToRotate; // 需要旋转的Image
|
||
[Header("拖拽")]
|
||
[Tooltip("在不改变视觉和布局的前提下,向左右/上下扩展拖拽命中范围。")]
|
||
[SerializeField] private Vector2 dragHitAreaExpansion = new Vector2(5.2f, 0.3f);
|
||
[SerializeField, Min(0f)] private float fanStopDuration = 0.15f;
|
||
[Tooltip("相对 Sprite 矩形中心的额外旋转轴偏移(本地空间,单位与 Transform 一致)。用于校准扇毂不在图心时的偏心。")]
|
||
[SerializeField] private Vector2 fanPivotLocalOffset;
|
||
[Header("低温背景")]
|
||
[SerializeField] private Color coldBaseColor = new Color(0.005f, 0.02f, 0.55f, 1f);
|
||
[SerializeField] private Color coldHighlightColor = new Color(0f, 0.3f, 1f, 1f);
|
||
[SerializeField, Min(0.01f)] private float coldNoiseScale = 1.8f;
|
||
[SerializeField, Range(0f, 1f)] private float coldNoiseStrength = 0.45f;
|
||
public Vector3 inPos; // Heatmap进入位置
|
||
public Vector3 outPos; // Heatmap退出位置
|
||
|
||
|
||
private Vector3 imageInitialPos; // Image的初始位置
|
||
|
||
[HideInInspector] private Vector4[] points = new Vector4[7]; // 存储6个点的数组,(x, y, z) = center, w = temperature
|
||
|
||
[HideInInspector] private Vector4[] properties = new Vector4[7]; // 存储6个盒子大小的数组, (x,y) = boxsize
|
||
|
||
private float uftempture = 1;
|
||
private float backtemture = 1;
|
||
|
||
private float modulesTempture = 1;
|
||
|
||
private float tempture = 1;
|
||
private Tween _temptureTween;
|
||
private Tween _fanSpeedTween;
|
||
private float _fanAngularSpeed;
|
||
private SpriteRenderer _fanRenderer;
|
||
private Vector3 _fanLocalPivot; // 相对 imageToRotate 的本地旋转轴
|
||
private bool _fanPivotReady;
|
||
|
||
private const float FanAngularSpeed = 1800f;
|
||
|
||
/// <summary>热力图面板当前是否在显示(用于 SetHeat 决定硬切还是平滑)。</summary>
|
||
public bool IsHeatMapVisible =>
|
||
heatmapGameobject != null && heatmapGameobject.activeInHierarchy;
|
||
|
||
public float Tempture
|
||
{
|
||
get => tempture;
|
||
set
|
||
{
|
||
if (tempture != value)
|
||
{
|
||
tempture = value;
|
||
SetDataToMaterial(); // 调用方法更新材质数据
|
||
}
|
||
}
|
||
}
|
||
|
||
public void ShowHeatMap()
|
||
{
|
||
StartCoroutine(ShowHeatMapCoroutine());
|
||
}
|
||
|
||
private IEnumerator ShowHeatMapCoroutine()
|
||
{
|
||
// 初始化Heatmap位置
|
||
|
||
heatmapGameobject.transform.localPosition = outPos;
|
||
heatmapGameobject.SetActive(true);
|
||
SetDataToMaterial();
|
||
heatmapGameobject.transform.DOLocalMove(inPos, 1f);
|
||
yield return new WaitForSeconds(1f); // 等待动画完成
|
||
heatmapGameobject.GetComponent<DraggableUI>().UnlockDrag();
|
||
coolingButton.interactable = false;
|
||
}
|
||
|
||
public void HideHeatMap()
|
||
{
|
||
var draggableUI = heatmapGameobject.GetComponent<DraggableUI>();
|
||
heatmapGameobject.transform.DOLocalMove(outPos, 1f).OnComplete(() => { draggableUI.UnlockDrag(); heatmapGameobject.SetActive(false);});
|
||
|
||
}
|
||
|
||
public void StartCooling()
|
||
{
|
||
coolingButton.interactable = false;
|
||
StartCoroutine(ReduceValueCoroutine());
|
||
}
|
||
|
||
IEnumerator ReduceValueCoroutine()
|
||
{
|
||
var draggableUI = heatmapGameobject.GetComponent<DraggableUI>();
|
||
draggableUI.LockDrag();
|
||
|
||
// 存储初始位置
|
||
imageInitialPos = imageToMove.localPosition;
|
||
|
||
// 移动和旋转动画
|
||
imageToMove.DOLocalMoveX(imageInitialPos.x + 3.5f, 1f);
|
||
StartFan();
|
||
|
||
bool coolingSucceeded = Tempture < 2;
|
||
if (coolingSucceeded)
|
||
{
|
||
Debug.Log("降温成功");
|
||
float duration = 3.5f;
|
||
SetTemptureSmooth(0.5f, duration);
|
||
AudioManager.Instance.PlaySfx("event:/FollowInput/cooler_cooling");
|
||
yield return new WaitForSeconds(duration);
|
||
}
|
||
else
|
||
{
|
||
Debug.Log("降温失败");
|
||
yield return new WaitForSeconds(3f);
|
||
}
|
||
|
||
yield return StopFanSmoothly();
|
||
|
||
// 恢复初始状态
|
||
imageToMove.DOLocalMove(imageInitialPos, 1f);
|
||
|
||
if (coolingSucceeded)
|
||
{
|
||
yield return new WaitForSeconds(1.5f);
|
||
DialogController.Instance.StartDialogNode("降温成功");
|
||
bodyModuleSystem.OnCooling();
|
||
}
|
||
else
|
||
{
|
||
DialogController.Instance.StartDialogNode("降温失败");
|
||
}
|
||
}
|
||
|
||
private void Update()
|
||
{
|
||
if (imageToRotate == null || Mathf.Approximately(_fanAngularSpeed, 0f))
|
||
return;
|
||
|
||
EnsureFanPivot();
|
||
float angle = _fanAngularSpeed * Time.deltaTime;
|
||
// 绕 Sprite 几何中心转,避免 pivot 不在扇叶中心时整圈公转(看起来像散架)。
|
||
Vector3 worldPivot = imageToRotate.TransformPoint(_fanLocalPivot);
|
||
imageToRotate.RotateAround(worldPivot, imageToRotate.forward, angle);
|
||
}
|
||
|
||
private void EnsureFanPivot()
|
||
{
|
||
if (_fanPivotReady)
|
||
return;
|
||
|
||
_fanLocalPivot = Vector3.zero;
|
||
if (imageToRotate == null)
|
||
{
|
||
_fanPivotReady = true;
|
||
return;
|
||
}
|
||
|
||
_fanRenderer = imageToRotate.GetComponent<SpriteRenderer>();
|
||
if (_fanRenderer != null && _fanRenderer.sprite != null)
|
||
{
|
||
var sprite = _fanRenderer.sprite;
|
||
// transform 在 sprite.pivot;矩形几何中心相对 pivot 的本地偏移。
|
||
// Aseprite Canvas 空间 pivot 会远超出 [0,1],直接 Rotate 会整圈公转。
|
||
Vector2 size = sprite.rect.size;
|
||
Vector2 pivotPx = sprite.pivot;
|
||
float ppu = sprite.pixelsPerUnit;
|
||
if (ppu > 0f)
|
||
{
|
||
_fanLocalPivot = new Vector3(
|
||
(size.x * 0.5f - pivotPx.x) / ppu,
|
||
(size.y * 0.5f - pivotPx.y) / ppu,
|
||
0f);
|
||
}
|
||
}
|
||
|
||
_fanLocalPivot += (Vector3)fanPivotLocalOffset;
|
||
_fanPivotReady = true;
|
||
}
|
||
|
||
private void StartFan()
|
||
{
|
||
_fanSpeedTween?.Kill();
|
||
_fanSpeedTween = null;
|
||
_fanPivotReady = false;
|
||
EnsureFanPivot();
|
||
_fanAngularSpeed = FanAngularSpeed;
|
||
}
|
||
|
||
private IEnumerator StopFanSmoothly()
|
||
{
|
||
_fanSpeedTween?.Kill();
|
||
|
||
if (fanStopDuration <= 0f)
|
||
{
|
||
_fanAngularSpeed = 0f;
|
||
_fanSpeedTween = null;
|
||
yield break;
|
||
}
|
||
|
||
_fanSpeedTween = DOTween.To(
|
||
() => _fanAngularSpeed,
|
||
value => _fanAngularSpeed = value,
|
||
0f,
|
||
fanStopDuration)
|
||
.SetEase(Ease.OutQuad)
|
||
.SetTarget(this);
|
||
|
||
yield return _fanSpeedTween.WaitForCompletion();
|
||
_fanAngularSpeed = 0f;
|
||
_fanSpeedTween = null;
|
||
}
|
||
|
||
void Start()
|
||
{
|
||
bodyModuleSystem = FindObjectOfType<BodyModuleSystem>();
|
||
ApplyDragHitArea();
|
||
EnsureFanPivot();
|
||
SetDataToMaterial();
|
||
}
|
||
|
||
private void ApplyDragHitArea()
|
||
{
|
||
if (heatmapGameobject == null)
|
||
return;
|
||
|
||
var dragGraphic = heatmapGameobject.GetComponent<Graphic>();
|
||
if (dragGraphic == null)
|
||
{
|
||
Debug.LogWarning("[HeatMapController] 拖拽对象缺少 Graphic,无法扩展拖拽命中范围。", heatmapGameobject);
|
||
return;
|
||
}
|
||
|
||
float horizontal = Mathf.Max(0f, dragHitAreaExpansion.x);
|
||
float vertical = Mathf.Max(0f, dragHitAreaExpansion.y);
|
||
|
||
// Graphic 使用负 raycastPadding 扩展命中区;不会改变 RectTransform,
|
||
// 因而不会移动右上角按钮或拉伸热力图内容。
|
||
dragGraphic.raycastPadding = new Vector4(-horizontal, -vertical, -horizontal, -vertical);
|
||
}
|
||
|
||
void OnDestroy()
|
||
{
|
||
_temptureTween?.Kill();
|
||
_fanSpeedTween?.Kill();
|
||
_fanAngularSpeed = 0f;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 热点尺寸(世界单位):自身 Sprite/Image → 父级 Sprite → RectTransform。
|
||
/// UFheatmap 等空 Sprite 锚点会回退到父级 Module Pic,避免静默变成 (1,1) 导致热区偏小。
|
||
/// </summary>
|
||
private static bool TryGetHotspotSize(Transform t, out Vector2 size)
|
||
{
|
||
size = default;
|
||
if (t == null) return false;
|
||
|
||
if (TryGetSpriteLocalSize(t, out size))
|
||
return true;
|
||
|
||
// 空 SpriteRenderer / 纯锚点:沿父链找有 sprite 的模块图
|
||
for (var p = t.parent; p != null; p = p.parent)
|
||
{
|
||
if (TryGetSpriteLocalSize(p, out size))
|
||
return true;
|
||
}
|
||
|
||
var image = t.GetComponent<Image>();
|
||
if (image != null && image.sprite != null)
|
||
{
|
||
var b = image.sprite.bounds.size;
|
||
size = new Vector2(b.x, b.y);
|
||
return true;
|
||
}
|
||
|
||
if (t is RectTransform rt)
|
||
{
|
||
size = new Vector2(rt.rect.width, rt.rect.height);
|
||
return size.x > 0f || size.y > 0f;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
private static bool TryGetSpriteLocalSize(Transform t, out Vector2 size)
|
||
{
|
||
size = default;
|
||
var sr = t.GetComponent<SpriteRenderer>();
|
||
if (sr == null || sr.sprite == null)
|
||
return false;
|
||
|
||
var b = sr.sprite.bounds.size;
|
||
var ls = t.lossyScale;
|
||
size = new Vector2(b.x * Mathf.Abs(ls.x), b.y * Mathf.Abs(ls.y));
|
||
return size.x > 0.001f || size.y > 0.001f;
|
||
}
|
||
|
||
public void SetDataToMaterial()
|
||
{
|
||
// 逻辑温度 0~2 → shader 侧发热强度;各段端点连续,避免跨档跳变。
|
||
// 保持原有温度映射,冷色替换只由 shader 处理。
|
||
if (tempture <= 1f)
|
||
{
|
||
float t = Mathf.Clamp01(tempture);
|
||
uftempture = Mathf.Lerp(0.85f, 2.8f, t);
|
||
modulesTempture = Mathf.Lerp(0.55f, 1.6f, t);
|
||
backtemture = Mathf.Lerp(0.7f, 2.25f, t);
|
||
}
|
||
else
|
||
{
|
||
float t = Mathf.Clamp01(tempture - 1f);
|
||
uftempture = Mathf.Lerp(2.8f, 3.2f, t);
|
||
modulesTempture = Mathf.Lerp(1.6f, 2.3f, t);
|
||
backtemture = Mathf.Lerp(2.25f, 3.4f, t);
|
||
}
|
||
|
||
if (Pos == null || heatMaterial == null)
|
||
return;
|
||
|
||
for (int i = 0; i < 7; i++)
|
||
{
|
||
if (Pos[i] == null)
|
||
{
|
||
points[i] = Vector4.zero;
|
||
properties[i] = Vector4.zero;
|
||
continue;
|
||
}
|
||
|
||
Transform currentTransform = Pos[i].GetComponent<Transform>();
|
||
|
||
if (i == 0)
|
||
{
|
||
points[i] = new Vector4(Pos[i].position.x, Pos[i].position.y, uftempture, 0);
|
||
}
|
||
else if (i == 6)
|
||
{
|
||
points[i] = new Vector4(Pos[i].position.x, Pos[i].position.y, backtemture, 0);
|
||
}
|
||
else
|
||
{
|
||
points[i] = new Vector4(Pos[i].position.x, Pos[i].position.y, modulesTempture,
|
||
0); // (x, y, z, temperature)
|
||
}
|
||
|
||
if (!TryGetHotspotSize(currentTransform, out var boxSize))
|
||
{
|
||
Debug.LogWarning($"[HeatMapController] Pos[{i}] '{Pos[i].name}' 无法取得热点尺寸,回退为 (1,1)。请检查 Sprite 或父级 Module Pic。", Pos[i]);
|
||
boxSize = Vector2.one;
|
||
}
|
||
|
||
properties[i] = new Vector4(boxSize.x, boxSize.y, 0.0f, 0.0f);
|
||
}
|
||
|
||
// 将数据传递给 Shader
|
||
heatMaterial.SetVectorArray("_Points", points);
|
||
heatMaterial.SetVectorArray("_Properties", properties);
|
||
heatMaterial.SetColor("_ColdBaseColor", coldBaseColor);
|
||
heatMaterial.SetColor("_ColdHighlightColor", coldHighlightColor);
|
||
heatMaterial.SetFloat("_ColdNoiseScale", coldNoiseScale);
|
||
heatMaterial.SetFloat("_ColdNoiseStrength", coldNoiseStrength);
|
||
}
|
||
|
||
public void SetTemptureDirect(float value)
|
||
{
|
||
_temptureTween?.Kill();
|
||
Tempture = value;
|
||
}
|
||
|
||
public void SetTemptureSmooth(float targetValue, float duration)
|
||
{
|
||
_temptureTween?.Kill();
|
||
_temptureTween = DOTween.To(() => tempture, x => Tempture = x, targetValue, duration)
|
||
.SetEase(Ease.InOutQuad)
|
||
.SetTarget(this);
|
||
}
|
||
|
||
#if UNITY_EDITOR
|
||
private void OnDrawGizmosSelected()
|
||
{
|
||
if (imageToRotate == null)
|
||
return;
|
||
|
||
// 编辑器下也能预览旋转轴(不依赖 Play 时缓存)。
|
||
var sr = imageToRotate.GetComponent<SpriteRenderer>();
|
||
Vector3 localPivot = (Vector3)fanPivotLocalOffset;
|
||
if (sr != null && sr.sprite != null && sr.sprite.pixelsPerUnit > 0f)
|
||
{
|
||
var sprite = sr.sprite;
|
||
Vector2 size = sprite.rect.size;
|
||
Vector2 pivotPx = sprite.pivot;
|
||
float ppu = sprite.pixelsPerUnit;
|
||
localPivot += new Vector3(
|
||
(size.x * 0.5f - pivotPx.x) / ppu,
|
||
(size.y * 0.5f - pivotPx.y) / ppu,
|
||
0f);
|
||
}
|
||
|
||
Vector3 worldPivot = imageToRotate.TransformPoint(localPivot);
|
||
Gizmos.color = new Color(1f, 0.45f, 0.1f, 0.9f);
|
||
Gizmos.DrawWireSphere(worldPivot, 0.08f);
|
||
Gizmos.color = new Color(0.3f, 0.9f, 0.55f, 0.7f);
|
||
Gizmos.DrawLine(worldPivot + Vector3.left * 0.2f, worldPivot + Vector3.right * 0.2f);
|
||
Gizmos.DrawLine(worldPivot + Vector3.down * 0.2f, worldPivot + Vector3.up * 0.2f);
|
||
}
|
||
#endif
|
||
}
|