1- // ProjectileVisual3D.cs — 3D pool visual
1+ // ProjectileVisual3D.cs — 3D pool visual
2+ // ADDED: Awake() explicitly disables any SpriteRenderer found on same GameObject.
3+ // Users who duplicate a 2D prefab to make the 3D one otherwise see sprite on top of mesh.
24//
3- // Inherits ProjectileVisualBase.
4- // Handles: MeshRenderer (oriented mesh), optional TrailRenderer.
5+ // PREFAB SETUP (what the prefab needs — nothing else):
6+ // • MeshFilter — holds the bullet/capsule mesh
7+ // • MeshRenderer — holds the bullet material
8+ // • ProjectileVisual3D — this script
9+ // • LocalPoolReturn — pool return (add manually or via LocalObjectPool auto-setup)
10+ // • Optional: TrailRenderer for bullet trail
511//
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- // }
12+ // NO SpriteRenderer, NO Sprite, NO 2D anything.
3613
3714using UnityEngine ;
3815using MidManStudio . Projectiles . Config ;
@@ -44,28 +21,27 @@ public class ProjectileVisual3D : ProjectileVisualBase
4421 {
4522 #region Inspector
4623
47- [ Header ( "3D Renderers" ) ]
24+ [ Header ( "3D Renderers (auto-found if null) " ) ]
4825 [ SerializeField ] private MeshFilter _meshFilter ;
4926 [ SerializeField ] private MeshRenderer _meshRenderer ;
5027
5128 [ Header ( "Trail (optional)" ) ]
5229 [ SerializeField ] private TrailRenderer _trailRenderer ;
5330
5431 [ Header ( "Scale" ) ]
55- [ Tooltip ( "Uniform scale multiplier applied on top of the config's FullSizeX." ) ]
32+ [ Tooltip ( "Multiplier on top of config FullSizeX." ) ]
5633 [ SerializeField ] private float _scaleMultiplier = 1f ;
5734
5835 [ Header ( "Material Fallback" ) ]
59- [ Tooltip ( "Material used when the config has no sprite/atlas. " +
60- "Should be a simple lit or unlit opaque material." ) ]
36+ [ Tooltip ( "Used when config has no sprite/atlas. Assign a simple Lit or Unlit material." ) ]
6137 [ SerializeField ] private Material _fallbackMaterial ;
6238
6339 #endregion
6440
6541 #region State
6642
67- private static Mesh _defaultCubeMesh ; // shared across all instances
68- private Material _instancedMaterial ; // per-instance to avoid shared-material side-effects
43+ private static Mesh _defaultCapsuleMesh ;
44+ private Material _instancedMaterial ;
6945 private bool _trailConfigured ;
7046 private ushort _cachedConfigId ;
7147 private bool _configInitialised ;
@@ -77,15 +53,29 @@ public class ProjectileVisual3D : ProjectileVisualBase
7753 protected override void Awake ( )
7854 {
7955 base . Awake ( ) ;
56+
8057 if ( _meshFilter == null ) _meshFilter = GetComponent < MeshFilter > ( ) ;
8158 if ( _meshRenderer == null ) _meshRenderer = GetComponent < MeshRenderer > ( ) ;
59+
60+ // FIX: kill any stray SpriteRenderer from duplicated 2D prefabs.
61+ // A SpriteRenderer will override the MeshRenderer in the hierarchy and
62+ // make the bullet look like a shaking 2D sprite in 3D space.
63+ var sr = GetComponent < SpriteRenderer > ( ) ;
64+ if ( sr != null )
65+ {
66+ sr . enabled = false ;
67+ Debug . LogWarning (
68+ $ "[ProjectileVisual3D] SpriteRenderer found on '{ name } ' and disabled. " +
69+ "Remove it from the prefab — 3D visuals use MeshRenderer only." ,
70+ this ) ;
71+ }
8272 }
8373
8474 private void OnDestroy ( )
8575 {
86- // Clean up per-instance material to avoid leaks
87- if ( _instancedMaterial != null )
88- Destroy ( _instancedMaterial ) ;
76+ if ( _instancedMaterial != null ) Destroy ( _instancedMaterial ) ;
77+ if ( _defaultCapsuleMesh != null && Application . isEditor )
78+ { /* leave for editor reloads */ }
8979 }
9080
9181 #endregion
@@ -94,13 +84,11 @@ private void OnDestroy()
9484
9585 protected override void ApplyRotation ( Vector3 dir )
9686 {
97- // 3D: align mesh forward (+Z) with travel direction
9887 if ( dir . sqrMagnitude < 0.001f ) return ;
9988
89+ // Align mesh +Z (forward) with travel direction — correct for capsule/bullet meshes
10090 Vector3 up = Mathf . Abs ( Vector3 . Dot ( dir . normalized , Vector3 . up ) ) > 0.99f
101- ? Vector3 . forward
102- : Vector3 . up ;
103-
91+ ? Vector3 . forward : Vector3 . up ;
10492 transform . rotation = Quaternion . LookRotation ( dir . normalized , up ) ;
10593 }
10694
@@ -118,8 +106,6 @@ protected override void OnInitialise(ProjectileConfigSO cfg)
118106 ApplyMaterial ( cfg ) ;
119107 ApplyScale ( cfg ) ;
120108 ApplyTrail ( cfg ) ;
121-
122- // Sub-class hook
123109 OnInitialise3D ( cfg ) ;
124110 }
125111
@@ -128,8 +114,7 @@ protected override void OnReturnToPool()
128114 _configInitialised = false ;
129115 _trailConfigured = false ;
130116
131- if ( _meshRenderer != null )
132- _meshRenderer . enabled = true ;
117+ if ( _meshRenderer != null ) _meshRenderer . enabled = true ;
133118
134119 if ( _trailRenderer != null )
135120 {
@@ -151,16 +136,10 @@ public override void HideProjectile()
151136
152137 #region Sub-class Hooks
153138
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>
139+ /// <summary>Override to add custom 3D setup (particles, sounds, etc.).</summary>
158140 protected virtual void OnInitialise3D ( ProjectileConfigSO cfg ) { }
159141
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>
142+ /// <summary>Override to stop/reset FX before returning to pool.</summary>
164143 protected virtual void OnCleanup3D ( ) { }
165144
166145 #endregion
@@ -170,41 +149,24 @@ protected virtual void OnCleanup3D() { }
170149 private void ApplyMesh ( ProjectileConfigSO cfg )
171150 {
172151 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
152+ Mesh mesh = cfg ? . CustomShape ? . GetMesh ( ) ;
183153 if ( mesh == null || mesh . vertexCount == 0 )
184- {
185- mesh = GetDefaultCube ( ) ;
186- }
187-
154+ mesh = GetDefaultCapsule ( ) ;
188155 _meshFilter . sharedMesh = mesh ;
189156 }
190157
191158 private void ApplyMaterial ( ProjectileConfigSO cfg )
192159 {
193160 if ( _meshRenderer == null ) return ;
194161
195- // Try to use the config sprite's texture
196162 Texture2D tex = cfg ? . ProjectileSprite ? . texture ;
197-
198163 if ( tex != null )
199164 {
200- // Re-use or create a per-instance material so we don't stomp shared material
201165 if ( _instancedMaterial == null )
202166 {
203- var srcMat = _meshRenderer . sharedMaterial ?? _fallbackMaterial ;
204- if ( srcMat != null )
205- _instancedMaterial = new Material ( srcMat ) ;
167+ var src = _meshRenderer . sharedMaterial ?? _fallbackMaterial ;
168+ if ( src != null ) _instancedMaterial = new Material ( src ) ;
206169 }
207-
208170 if ( _instancedMaterial != null )
209171 {
210172 _instancedMaterial . mainTexture = tex ;
@@ -216,19 +178,18 @@ private void ApplyMaterial(ProjectileConfigSO cfg)
216178 _meshRenderer . sharedMaterial = _fallbackMaterial ;
217179 }
218180
219- _meshRenderer . enabled = true ;
220- _meshRenderer . shadowCastingMode = UnityEngine . Rendering . ShadowCastingMode . Off ;
221- _meshRenderer . receiveShadows = false ;
181+ _meshRenderer . enabled = true ;
182+ _meshRenderer . shadowCastingMode = UnityEngine . Rendering . ShadowCastingMode . Off ;
183+ _meshRenderer . receiveShadows = false ;
222184 }
223185
224186 private void ApplyScale ( ProjectileConfigSO cfg )
225187 {
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 ) ;
188+ float length = cfg != null ? cfg . FullSizeX * _scaleMultiplier : _scaleMultiplier ;
189+ float width = cfg != null && cfg . FullSizeX > 0.001f
190+ ? ( cfg . FullSizeY / cfg . FullSizeX ) * length : length * 0.2f ;
191+ // X = length (travel axis), Y = Z = width (perpendicular)
192+ transform . localScale = new Vector3 ( width , width , length ) ;
232193 }
233194
234195 private void ApplyTrail ( ProjectileConfigSO cfg )
@@ -278,44 +239,67 @@ private void ApplyTrail(ProjectileConfigSO cfg)
278239
279240 #endregion
280241
281- #region Default Mesh
242+ #region Default Capsule Mesh
282243
283- private static Mesh GetDefaultCube ( )
244+ /// <summary>
245+ /// Elongated capsule oriented along +Z (forward = travel direction).
246+ /// 0.15 radius, 1.0 half-length — looks like a bullet tracer when scaled.
247+ /// </summary>
248+ private static Mesh GetDefaultCapsule ( )
284249 {
285- if ( _defaultCubeMesh != null && _defaultCubeMesh . vertexCount > 0 )
286- return _defaultCubeMesh ;
250+ if ( _defaultCapsuleMesh != null && _defaultCapsuleMesh . vertexCount > 0 )
251+ return _defaultCapsuleMesh ;
252+
253+ // Simple elongated hexagonal prism (6 sides, ~capsule shaped)
254+ int sides = 6 ;
255+ float radius = 0.08f ;
256+ float halfLen = 0.5f ; // along Z
287257
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 ;
258+ var verts = new System . Collections . Generic . List < Vector3 > ( ) ;
259+ var uvs = new System . Collections . Generic . List < Vector2 > ( ) ;
260+ var tris = new System . Collections . Generic . List < int > ( ) ;
291261
292- _defaultCubeMesh = new Mesh { name = "DefaultProjectile3D" } ;
293- _defaultCubeMesh . vertices = new Vector3 [ ]
262+ // Ring of vertices at +Z (tip) and -Z (tail)
263+ for ( int i = 0 ; i < sides ; i ++ )
294264 {
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 [ ]
265+ float a = i / ( float ) sides * Mathf . PI * 2f ;
266+ float x = Mathf . Cos ( a ) * radius ;
267+ float y = Mathf . Sin ( a ) * radius ;
268+ verts . Add ( new Vector3 ( x , y , halfLen ) ) ; uvs . Add ( new Vector2 ( i / ( float ) sides , 1f ) ) ;
269+ verts . Add ( new Vector3 ( x , y , - halfLen ) ) ; uvs . Add ( new Vector2 ( i / ( float ) sides , 0f ) ) ;
270+ }
271+ // Tip center (+Z) and tail center (-Z)
272+ int tipIdx = verts . Count ; verts . Add ( new Vector3 ( 0 , 0 , halfLen ) ) ; uvs . Add ( new Vector2 ( 0.5f , 1f ) ) ;
273+ int tailIdx = verts . Count ; verts . Add ( new Vector3 ( 0 , 0 , - halfLen ) ) ; uvs . Add ( new Vector2 ( 0.5f , 0f ) ) ;
274+
275+ // Side quads
276+ for ( int i = 0 ; i < sides ; i ++ )
303277 {
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 [ ]
278+ int next = ( i + 1 ) % sides ;
279+ int a0 = i * 2 , a1 = i * 2 + 1 ;
280+ int b0 = next * 2 , b1 = next * 2 + 1 ;
281+ tris . AddRange ( new [ ] { a0 , b0 , a1 , b0 , b1 , a1 } ) ;
282+ }
283+ // Tip cap
284+ for ( int i = 0 ; i < sides ; i ++ )
308285 {
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 ;
286+ int next = ( i + 1 ) % sides ;
287+ tris . AddRange ( new [ ] { tipIdx , i * 2 , next * 2 } ) ;
288+ }
289+ // Tail cap
290+ for ( int i = 0 ; i < sides ; i ++ )
291+ {
292+ int next = ( i + 1 ) % sides ;
293+ tris . AddRange ( new [ ] { tailIdx , next * 2 + 1 , i * 2 + 1 } ) ;
294+ }
295+
296+ _defaultCapsuleMesh = new Mesh { name = "DefaultProjectile3D_Capsule" } ;
297+ _defaultCapsuleMesh . SetVertices ( verts ) ;
298+ _defaultCapsuleMesh . SetUVs ( 0 , uvs ) ;
299+ _defaultCapsuleMesh . SetTriangles ( tris , 0 ) ;
300+ _defaultCapsuleMesh . RecalculateNormals ( ) ;
301+ _defaultCapsuleMesh . RecalculateBounds ( ) ;
302+ return _defaultCapsuleMesh ;
319303 }
320304
321305 #endregion
0 commit comments