Files
FueraDeEscala/Assets/Game/Test/Scripts/GhostTrailEmitter.cs
Robii Aragon d5241c3bf7 Add AntMan scaling, ghost trail and test prefab
Introduce a set of test tools and demo assets for an AntMan-style scaling effect:

- Add AntManScaleController: handles smooth scale transitions (absolute/relative modes), pivot preservation, queued toggles and events.
- Add GhostTrailEmitter and GhostTrailGhost: spawn fading ghost meshes (supports skinned mesh baking, material fallback, lifetime and pooling limit).
- Add GKTInventoryScaleToggle: inventory-gated input wrapper to toggle scale with optional Ctrl bypass and auto reference resolution.
- Add AntManTestSceneBootstrap and TestBootstrap prefab to quickly construct a test scene (cube, light, camera) with components configured.
- Add AntManScaleTest scene and related prefab/meta files, plus .vsconfig for Unity workload.
- Update SampleScene LFS pointer (scene file checksum/size).

These changes provide a reusable demo and components to test scaling visuals and interactions during development.
2026-03-19 18:53:27 -07:00

239 lines
6.7 KiB
C#

using System.Collections.Generic;
using UnityEngine;
public class GhostTrailEmitter : MonoBehaviour
{
[Header("Referencias")]
[SerializeField] private AntManScaleController scaleController;
[SerializeField] private Material ghostMaterial;
[SerializeField] private Transform ghostSourceTransform;
[SerializeField] private MeshFilter sourceMeshFilter;
[SerializeField] private MeshRenderer sourceMeshRenderer;
[SerializeField] private SkinnedMeshRenderer sourceSkinnedMeshRenderer;
[SerializeField] private bool useSkinnedMeshBake = true;
[SerializeField] private bool useScaleControllerScaleOnSkinnedGhost = true;
[Header("Spawn")]
[SerializeField] [Min(0.01f)] private float spawnInterval = 0.05f;
[SerializeField] [Min(1)] private int maxActiveGhosts = 32;
[Header("Fade")]
[SerializeField] [Min(0.01f)] private float ghostLifetime = 0.35f;
[SerializeField] [Range(0f, 1f)] private float startAlpha = 0.45f;
[SerializeField] [Range(0f, 1f)] private float endAlpha = 0f;
[Header("Debug")]
[SerializeField] private bool showGhostCountLogs;
private readonly List<GhostTrailGhost> _activeGhosts = new List<GhostTrailGhost>();
private Material _fallbackMaterial;
private bool _isEmitting;
private float _spawnTimer;
private void Awake()
{
if (scaleController == null)
{
scaleController = GetComponent<AntManScaleController>();
}
ResolveRenderSources();
if (ghostMaterial == null)
{
_fallbackMaterial = GetFallbackMaterial();
}
}
private void OnEnable()
{
if (scaleController != null)
{
scaleController.TransitionStarted += HandleTransitionStarted;
scaleController.TransitionCompleted += HandleTransitionCompleted;
}
}
private void OnDisable()
{
if (scaleController != null)
{
scaleController.TransitionStarted -= HandleTransitionStarted;
scaleController.TransitionCompleted -= HandleTransitionCompleted;
}
}
private void LateUpdate()
{
if (!_isEmitting)
{
return;
}
_spawnTimer += Time.deltaTime;
while (_spawnTimer >= spawnInterval)
{
_spawnTimer -= spawnInterval;
SpawnGhost();
}
}
private void HandleTransitionStarted(Vector3 from, Vector3 to)
{
_isEmitting = true;
_spawnTimer = 0f;
SpawnGhost();
}
private void HandleTransitionCompleted(Vector3 scale)
{
_isEmitting = false;
}
private void SpawnGhost()
{
CleanupNullGhosts();
EnforceGhostLimit();
if (!TryGetGhostRenderData(out Mesh meshToUse, out Material materialToUse, out Transform sourceTransform, out bool ownsMesh, out bool fromSkinnedMesh))
{
return;
}
GameObject ghostObject = new GameObject("GhostTrail");
ghostObject.transform.SetPositionAndRotation(sourceTransform.position, sourceTransform.rotation);
ghostObject.transform.localScale = GetGhostScale(sourceTransform, fromSkinnedMesh);
GhostTrailGhost ghost = ghostObject.AddComponent<GhostTrailGhost>();
ghost.Initialize(
meshToUse,
materialToUse,
ghostLifetime,
startAlpha,
endAlpha,
this,
ownsMesh
);
_activeGhosts.Add(ghost);
if (showGhostCountLogs)
{
Debug.Log($"[GhostTrailEmitter] Ghosts activos: {_activeGhosts.Count}", this);
}
}
public void NotifyGhostDestroyed(GhostTrailGhost ghost)
{
_activeGhosts.Remove(ghost);
}
private void CleanupNullGhosts()
{
for (int i = _activeGhosts.Count - 1; i >= 0; i--)
{
if (_activeGhosts[i] == null)
{
_activeGhosts.RemoveAt(i);
}
}
}
private void EnforceGhostLimit()
{
while (_activeGhosts.Count >= maxActiveGhosts)
{
GhostTrailGhost oldestGhost = _activeGhosts[0];
_activeGhosts.RemoveAt(0);
if (oldestGhost != null)
{
Destroy(oldestGhost.gameObject);
}
}
}
private void ResolveRenderSources()
{
if (ghostSourceTransform == null)
{
ghostSourceTransform = transform;
}
if (sourceSkinnedMeshRenderer == null)
{
sourceSkinnedMeshRenderer = ghostSourceTransform.GetComponentInChildren<SkinnedMeshRenderer>(true);
}
if (sourceMeshFilter == null)
{
sourceMeshFilter = ghostSourceTransform.GetComponentInChildren<MeshFilter>(true);
}
if (sourceMeshRenderer == null)
{
sourceMeshRenderer = ghostSourceTransform.GetComponentInChildren<MeshRenderer>(true);
}
}
private Material GetFallbackMaterial()
{
if (sourceSkinnedMeshRenderer != null)
{
return sourceSkinnedMeshRenderer.sharedMaterial;
}
if (sourceMeshRenderer != null)
{
return sourceMeshRenderer.sharedMaterial;
}
return null;
}
private bool TryGetGhostRenderData(out Mesh mesh, out Material material, out Transform sourceTransform, out bool ownsMesh, out bool fromSkinnedMesh)
{
ResolveRenderSources();
sourceTransform = ghostSourceTransform != null ? ghostSourceTransform : transform;
material = ghostMaterial != null ? ghostMaterial : (_fallbackMaterial != null ? _fallbackMaterial : GetFallbackMaterial());
mesh = null;
ownsMesh = false;
fromSkinnedMesh = false;
if (useSkinnedMeshBake && sourceSkinnedMeshRenderer != null)
{
Mesh bakedMesh = new Mesh();
sourceSkinnedMeshRenderer.BakeMesh(bakedMesh);
mesh = bakedMesh;
ownsMesh = true;
fromSkinnedMesh = true;
sourceTransform = sourceSkinnedMeshRenderer.transform;
}
else if (sourceMeshFilter != null)
{
mesh = sourceMeshFilter.sharedMesh;
if (sourceMeshFilter.transform != null)
{
sourceTransform = sourceMeshFilter.transform;
}
}
return mesh != null && material != null;
}
private Vector3 GetGhostScale(Transform sourceTransform, bool fromSkinnedMesh)
{
if (fromSkinnedMesh && useScaleControllerScaleOnSkinnedGhost && scaleController != null)
{
return scaleController.GetCurrentScale();
}
return sourceTransform.lossyScale;
}
}