Skip to content

Commit 743659b

Browse files
authored
Create ProjectileVisual3D.cs
1 parent bc7d867 commit 743659b

1 file changed

Lines changed: 323 additions & 0 deletions

File tree

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
// ProjectileVisual3D.cs — 3D pool visual
2+
//
3+
// Inherits ProjectileVisualBase.
4+
// Handles: MeshRenderer (oriented mesh), optional TrailRenderer.
5+
//
6+
// ORIENTATION:
7+
// Uses Quaternion.LookRotation(direction) so the mesh forward (+Z) aligns
8+
// with the travel direction. For bullet/capsule meshes this gives the
9+
// correct nose-forward appearance.
10+
//
11+
// MESH SELECTION PRIORITY:
12+
// 1. ProjectileConfigSO.CustomShape mesh (if assigned and Is3D)
13+
// 2. ProjectileConfigSO.ProjectileSprite → baked quad (2D fallback for 3D)
14+
// 3. Default unit cube (last resort — replace with your own primitive)
15+
//
16+
// END-USER EXTENSION:
17+
// Derive from this class and override OnInitialise3D(cfg) to swap in
18+
// custom materials, particle systems, etc.:
19+
//
20+
// public class RocketVisual : ProjectileVisual3D
21+
// {
22+
// [SerializeField] ParticleSystem _exhaustFlames;
23+
//
24+
// protected override void OnInitialise3D(ProjectileConfigSO cfg)
25+
// {
26+
// base.OnInitialise3D(cfg);
27+
// _exhaustFlames.Play();
28+
// }
29+
//
30+
// protected override void OnReturnToPool()
31+
// {
32+
// _exhaustFlames.Stop();
33+
// base.OnReturnToPool();
34+
// }
35+
// }
36+
37+
using UnityEngine;
38+
using MidManStudio.Projectiles.Config;
39+
40+
namespace MidManStudio.Projectiles.Visuals
41+
{
42+
[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
43+
public class ProjectileVisual3D : ProjectileVisualBase
44+
{
45+
#region Inspector
46+
47+
[Header("3D Renderers")]
48+
[SerializeField] private MeshFilter _meshFilter;
49+
[SerializeField] private MeshRenderer _meshRenderer;
50+
51+
[Header("Trail (optional)")]
52+
[SerializeField] private TrailRenderer _trailRenderer;
53+
54+
[Header("Scale")]
55+
[Tooltip("Uniform scale multiplier applied on top of the config's FullSizeX.")]
56+
[SerializeField] private float _scaleMultiplier = 1f;
57+
58+
[Header("Material Fallback")]
59+
[Tooltip("Material used when the config has no sprite/atlas. " +
60+
"Should be a simple lit or unlit opaque material.")]
61+
[SerializeField] private Material _fallbackMaterial;
62+
63+
#endregion
64+
65+
#region State
66+
67+
private static Mesh _defaultCubeMesh; // shared across all instances
68+
private Material _instancedMaterial; // per-instance to avoid shared-material side-effects
69+
private bool _trailConfigured;
70+
private ushort _cachedConfigId;
71+
private bool _configInitialised;
72+
73+
#endregion
74+
75+
#region Unity Lifecycle
76+
77+
protected override void Awake()
78+
{
79+
base.Awake();
80+
if (_meshFilter == null) _meshFilter = GetComponent<MeshFilter>();
81+
if (_meshRenderer == null) _meshRenderer = GetComponent<MeshRenderer>();
82+
}
83+
84+
private void OnDestroy()
85+
{
86+
// Clean up per-instance material to avoid leaks
87+
if (_instancedMaterial != null)
88+
Destroy(_instancedMaterial);
89+
}
90+
91+
#endregion
92+
93+
#region ProjectileVisualBase
94+
95+
protected override void ApplyRotation(Vector3 dir)
96+
{
97+
// 3D: align mesh forward (+Z) with travel direction
98+
if (dir.sqrMagnitude < 0.001f) return;
99+
100+
Vector3 up = Mathf.Abs(Vector3.Dot(dir.normalized, Vector3.up)) > 0.99f
101+
? Vector3.forward
102+
: Vector3.up;
103+
104+
transform.rotation = Quaternion.LookRotation(dir.normalized, up);
105+
}
106+
107+
protected override void OnInitialise(ProjectileConfigSO cfg)
108+
{
109+
bool configChanged = !_configInitialised || _cachedConfigId != ConfigId;
110+
if (configChanged)
111+
{
112+
_cachedConfigId = ConfigId;
113+
_trailConfigured = false;
114+
_configInitialised = true;
115+
}
116+
117+
ApplyMesh(cfg);
118+
ApplyMaterial(cfg);
119+
ApplyScale(cfg);
120+
ApplyTrail(cfg);
121+
122+
// Sub-class hook
123+
OnInitialise3D(cfg);
124+
}
125+
126+
protected override void OnReturnToPool()
127+
{
128+
_configInitialised = false;
129+
_trailConfigured = false;
130+
131+
if (_meshRenderer != null)
132+
_meshRenderer.enabled = true;
133+
134+
if (_trailRenderer != null)
135+
{
136+
_trailRenderer.emitting = false;
137+
_trailRenderer.enabled = false;
138+
_trailRenderer.Clear();
139+
}
140+
141+
OnCleanup3D();
142+
}
143+
144+
public override void HideProjectile()
145+
{
146+
if (_meshRenderer != null) _meshRenderer.enabled = false;
147+
if (_trailRenderer != null) _trailRenderer.emitting = false;
148+
}
149+
150+
#endregion
151+
152+
#region Sub-class Hooks
153+
154+
/// <summary>
155+
/// Called at the end of <see cref="OnInitialise"/> after mesh, material,
156+
/// scale, and trail have been applied. Override to add game-specific setup.
157+
/// </summary>
158+
protected virtual void OnInitialise3D(ProjectileConfigSO cfg) { }
159+
160+
/// <summary>
161+
/// Called at the start of <see cref="OnReturnToPool"/> before the
162+
/// base class resets renderers. Override to stop particles/FX etc.
163+
/// </summary>
164+
protected virtual void OnCleanup3D() { }
165+
166+
#endregion
167+
168+
#region Visual Setup
169+
170+
private void ApplyMesh(ProjectileConfigSO cfg)
171+
{
172+
if (_meshFilter == null) return;
173+
174+
Mesh mesh = null;
175+
176+
// Priority 1: custom shape SO
177+
if (cfg?.CustomShape != null)
178+
{
179+
mesh = cfg.CustomShape.GetMesh();
180+
}
181+
182+
// Priority 2: built-in default cube
183+
if (mesh == null || mesh.vertexCount == 0)
184+
{
185+
mesh = GetDefaultCube();
186+
}
187+
188+
_meshFilter.sharedMesh = mesh;
189+
}
190+
191+
private void ApplyMaterial(ProjectileConfigSO cfg)
192+
{
193+
if (_meshRenderer == null) return;
194+
195+
// Try to use the config sprite's texture
196+
Texture2D tex = cfg?.ProjectileSprite?.texture;
197+
198+
if (tex != null)
199+
{
200+
// Re-use or create a per-instance material so we don't stomp shared material
201+
if (_instancedMaterial == null)
202+
{
203+
var srcMat = _meshRenderer.sharedMaterial ?? _fallbackMaterial;
204+
if (srcMat != null)
205+
_instancedMaterial = new Material(srcMat);
206+
}
207+
208+
if (_instancedMaterial != null)
209+
{
210+
_instancedMaterial.mainTexture = tex;
211+
_meshRenderer.material = _instancedMaterial;
212+
}
213+
}
214+
else if (_fallbackMaterial != null)
215+
{
216+
_meshRenderer.sharedMaterial = _fallbackMaterial;
217+
}
218+
219+
_meshRenderer.enabled = true;
220+
_meshRenderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
221+
_meshRenderer.receiveShadows = false;
222+
}
223+
224+
private void ApplyScale(ProjectileConfigSO cfg)
225+
{
226+
float size = cfg != null ? cfg.FullSizeX * _scaleMultiplier : _scaleMultiplier;
227+
float aspectY = (cfg != null && cfg.FullSizeX > 0.001f)
228+
? cfg.FullSizeY / cfg.FullSizeX : 1f;
229+
230+
// x = length (along forward/travel), y = height, z = width
231+
transform.localScale = new Vector3(size, size * aspectY, size * aspectY);
232+
}
233+
234+
private void ApplyTrail(ProjectileConfigSO cfg)
235+
{
236+
if (_trailRenderer == null) return;
237+
238+
if (cfg == null || !cfg.HasTrail)
239+
{
240+
_trailRenderer.enabled = false;
241+
_trailRenderer.emitting = false;
242+
_trailConfigured = false;
243+
return;
244+
}
245+
246+
if (!_trailConfigured)
247+
{
248+
if (cfg.TrailMaterial != null)
249+
{
250+
if (cfg.UseSharedTrailMaterial)
251+
_trailRenderer.sharedMaterial = cfg.TrailMaterial;
252+
else
253+
_trailRenderer.material = cfg.TrailMaterial;
254+
}
255+
256+
if (cfg.UseGradientOverride && cfg.TrailGradient != null)
257+
_trailRenderer.colorGradient = cfg.TrailGradient;
258+
259+
_trailRenderer.time = cfg.TrailTime;
260+
_trailRenderer.startWidth = cfg.TrailStartWidth;
261+
_trailRenderer.endWidth = cfg.TrailEndWidth;
262+
_trailRenderer.numCapVertices = cfg.TrailCapVertices;
263+
_trailRenderer.minVertexDistance = cfg.TrailMinVertexDistance;
264+
_trailRenderer.alignment = LineAlignment.View;
265+
266+
_trailRenderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
267+
_trailRenderer.receiveShadows = false;
268+
_trailRenderer.generateLightingData = false;
269+
_trailRenderer.motionVectorGenerationMode = MotionVectorGenerationMode.ForceNoMotion;
270+
271+
_trailConfigured = true;
272+
}
273+
274+
_trailRenderer.Clear();
275+
_trailRenderer.enabled = true;
276+
_trailRenderer.emitting = true;
277+
}
278+
279+
#endregion
280+
281+
#region Default Mesh
282+
283+
private static Mesh GetDefaultCube()
284+
{
285+
if (_defaultCubeMesh != null && _defaultCubeMesh.vertexCount > 0)
286+
return _defaultCubeMesh;
287+
288+
// Elongated capsule-like box: 2 units long (Z), 0.15 units wide (X/Y)
289+
// Tip at +Z, tail at -Z
290+
float hw = 0.075f, hl = 0.5f;
291+
292+
_defaultCubeMesh = new Mesh { name = "DefaultProjectile3D" };
293+
_defaultCubeMesh.vertices = new Vector3[]
294+
{
295+
// Front (+Z)
296+
new(-hw, -hw, hl), new( hw, -hw, hl),
297+
new( hw, hw, hl), new(-hw, hw, hl),
298+
// Back (-Z)
299+
new(-hw, -hw, -hl), new( hw, -hw, -hl),
300+
new( hw, hw, -hl), new(-hw, hw, -hl),
301+
};
302+
_defaultCubeMesh.uv = new Vector2[]
303+
{
304+
new(0,0), new(1,0), new(1,1), new(0,1),
305+
new(0,0), new(1,0), new(1,1), new(0,1),
306+
};
307+
_defaultCubeMesh.triangles = new int[]
308+
{
309+
0,2,1, 0,3,2, // front
310+
5,7,4, 5,6,7, // back
311+
3,6,2, 3,7,6, // top
312+
1,5,0, 1,4,5, // bottom (note: was 0,4,5 — winding corrected)
313+
0,7,3, 0,4,7, // left
314+
2,6,1, 1,6,5, // right
315+
};
316+
_defaultCubeMesh.RecalculateNormals();
317+
_defaultCubeMesh.RecalculateBounds();
318+
return _defaultCubeMesh;
319+
}
320+
321+
#endregion
322+
}
323+
}

0 commit comments

Comments
 (0)