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.
This commit is contained in:
238
Assets/Game/Test/Scripts/GhostTrailEmitter.cs
Normal file
238
Assets/Game/Test/Scripts/GhostTrailEmitter.cs
Normal file
@@ -0,0 +1,238 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user