Skip to content

Commit 2df9161

Browse files
authored
Update ProjectileVisual3D.cs
1 parent 6a2d827 commit 2df9161

1 file changed

Lines changed: 103 additions & 119 deletions

File tree

‎packages/com.midmanstudio.projectilesystem/Runtime/Visual/ProjectileVisual3D.cs‎

Lines changed: 103 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,15 @@
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

3714
using UnityEngine;
3815
using 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

Comments
 (0)