From 587fc46f1ab036e2ba1539375f7f233a60575e3b Mon Sep 17 00:00:00 2001 From: Matt F Date: Thu, 25 Jun 2026 18:22:39 -0700 Subject: [PATCH] Adding Post Processing and lights to the tesla tower --- .../Art/Materials/M_Electrical_Glow.mat | 6 +- Assets/_Project/Scenes/Levels/9Player.unity | 50 ---- .../_Project/Scripts/Combat/TeslaArcVisual.cs | 255 +++++++++++++++--- Assets/_Project/Scripts/Combat/TowerCombat.cs | 2 - .../Settings/DefaultVolumeProfile.asset | 28 +- .../Settings/Render Settings/URP-High.asset | 2 +- .../Settings/Render Settings/URP-Medium.asset | 2 +- .../Settings/Render Settings/URP-Ultra.asset | 2 +- 8 files changed, 241 insertions(+), 106 deletions(-) diff --git a/Assets/_Project/Art/Materials/M_Electrical_Glow.mat b/Assets/_Project/Art/Materials/M_Electrical_Glow.mat index 1dd11fe..558d2db 100644 --- a/Assets/_Project/Art/Materials/M_Electrical_Glow.mat +++ b/Assets/_Project/Art/Materials/M_Electrical_Glow.mat @@ -105,7 +105,7 @@ Material: - _AddPrecomputedVelocity: 0 - _AlphaClip: 0 - _AlphaToMask: 0 - - _Blend: 0 + - _Blend: 2 - _BlendModePreserveSpecular: 1 - _BumpScale: 1 - _ClearCoatMask: 0 @@ -114,8 +114,8 @@ Material: - _Cutoff: 0.5 - _DetailAlbedoMapScale: 1 - _DetailNormalMapScale: 1 - - _DstBlend: 10 - - _DstBlendAlpha: 10 + - _DstBlend: 1 + - _DstBlendAlpha: 1 - _EnvironmentReflections: 1 - _GlossMapScale: 0 - _Glossiness: 0 diff --git a/Assets/_Project/Scenes/Levels/9Player.unity b/Assets/_Project/Scenes/Levels/9Player.unity index 76b7f7f..32942ce 100644 --- a/Assets/_Project/Scenes/Levels/9Player.unity +++ b/Assets/_Project/Scenes/Levels/9Player.unity @@ -12226,55 +12226,6 @@ Mesh: - serializedVersion: 1 m_IndexStart: 0 m_IndexCount: 0 ---- !u!1 &832575517 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 832575519} - - component: {fileID: 832575518} - m_Layer: 0 - m_Name: Global Volume - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!114 &832575518 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 832575517} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 172515602e62fb746b5d573b38a5fe58, type: 3} - m_Name: - m_EditorClassIdentifier: - m_IsGlobal: 1 - priority: 0 - blendDistance: 0 - weight: 1 - sharedProfile: {fileID: 11400000, guid: 10fc4df2da32a41aaa32d77bc913491c, type: 2} ---- !u!4 &832575519 -Transform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 832575517} - serializedVersion: 2 - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 1, y: 1, z: 1} - m_ConstrainProportionsScale: 0 - m_Children: [] - m_Father: {fileID: 0} - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &839198900 GameObject: m_ObjectHideFlags: 0 @@ -28745,7 +28696,6 @@ SceneRoots: m_ObjectHideFlags: 0 m_Roots: - {fileID: 410087041} - - {fileID: 832575519} - {fileID: 441239881} - {fileID: 167151709} - {fileID: 1507514109} diff --git a/Assets/_Project/Scripts/Combat/TeslaArcVisual.cs b/Assets/_Project/Scripts/Combat/TeslaArcVisual.cs index 20de293..c4e7828 100644 --- a/Assets/_Project/Scripts/Combat/TeslaArcVisual.cs +++ b/Assets/_Project/Scripts/Combat/TeslaArcVisual.cs @@ -6,35 +6,92 @@ namespace TD.Combat { /// /// Local-only electrical-arc visual for - /// towers (Tesla Coil). Subscribes to and draws a - /// jagged line from the tower's emitter point to every enemy hit, flashed for a moment. + /// towers (Tesla Coil). Subscribes to and draws an + /// arced, jittering bolt from the tower's emitter to every enemy hit, re-randomized each + /// frame so it crackles, then hidden after a short flash. /// /// /// Pure visualization — runs on every peer from the replicated fire broadcast; nothing - /// here is networked. Arcs are pooled s reused across ticks. + /// here is networked. Bolts are pooled s reused across ticks. + /// The glow (bloom) comes from the material + an HDR color above the bloom threshold; see + /// and the project's post-processing Volume. /// [RequireComponent(typeof(TowerCombat))] public class TeslaArcVisual : MonoBehaviour { - [Tooltip("Point the arcs originate from — the central top of the coil. " + + [Tooltip("Point the bolts originate from — the central top of the coil. " + "If unset, falls back to one unit above the tower root.")] [SerializeField] private Transform emitter; - [Tooltip("Material for the arc lines. Assign an unlit/additive material for a glow. " + - "If unset, a fallback Sprites/Default material is created at runtime.")] + [Tooltip("Material for the bolts. Use an UNLIT ADDITIVE material for a convincing " + + "glow (a Lit material won't bloom well on a thin line). If unset, a fallback " + + "additive material is created at runtime.")] [SerializeField] private Material arcMaterial; - [SerializeField] private Color arcColor = new Color(0.6f, 0.85f, 1f, 1f); + [Tooltip("Base bolt color. Multiplied by Emission Intensity to push it into HDR so " + + "Bloom picks it up.")] + [ColorUsage(true, true)] + [SerializeField] private Color arcColor = new Color(0.55f, 0.8f, 1f, 1f); + + [Tooltip("HDR multiplier on the color. Must exceed the Bloom threshold (>1) for the " + + "bolt to glow. Raise for a hotter, brighter arc.")] + [SerializeField] private float emissionIntensity = 4f; + [SerializeField] private float arcWidth = 0.06f; - [Tooltip("How long each arc flash stays visible, in seconds.")] + + [Tooltip("How long each flash stays visible, in seconds.")] [SerializeField] private float arcDuration = 0.12f; - [Tooltip("Number of points per arc. More = more jagged detail.")] - [SerializeField] private int segmentsPerArc = 6; - [Tooltip("Random offset applied to interior arc points for the lightning look.")] - [SerializeField] private float jitter = 0.2f; + + [Tooltip("Points per bolt. More = smoother arc and finer crackle. ~12-20 looks good.")] + [SerializeField] private int segmentsPerArc = 16; + + [Tooltip("Overall arc/bow as a fraction of bolt length (0 = straight line). The bolt " + + "lifts toward the middle for an electrical-arc curve.")] + [SerializeField] private float arcHeight = 0.18f; + + [Tooltip("Random crackle displacement (world units) at the middle of the bolt, " + + "tapering to 0 at both ends so it stays anchored to the coil and enemy.")] + [SerializeField] private float jitter = 0.18f; + + [Header("Light flash")] + [Tooltip("Pulse a real point light at the emitter when firing so the bolts actually " + + "illuminate nearby enemies (emission/bloom alone don't cast light).")] + [SerializeField] private bool castLight = true; + + [Tooltip("Color of the flash light.")] + [SerializeField] private Color lightColor = new Color(0.6f, 0.85f, 1f); + + [Tooltip("Peak intensity of the flash. The light flickers between this and ~70% of " + + "it while firing for an electrical pulse.")] + [SerializeField] private float lightIntensity = 8f; + + [Tooltip("Range of the emitter flash light — covers enemies near the coil.")] + [SerializeField] private float lightRange = 12f; + + [Tooltip("Also flash a small light at each bolt's IMPACT point, so enemies struck at " + + "the far edge of range get lit by the bolt — not just those near the coil.")] + [SerializeField] private bool castImpactLight = true; + + [Tooltip("Peak intensity of each impact light (usually lower than the emitter flash).")] + [SerializeField] private float impactLightIntensity = 4f; + + [Tooltip("Range of each impact light — only needs to cover the struck enemy, so keep it small.")] + [SerializeField] private float impactLightRange = 5f; + + [Tooltip("Performance cap on simultaneous impact lights. URP limits how many lights " + + "affect each object, and many towers add up — targets beyond this cap rely on " + + "the emitter flash. Raise carefully.")] + [SerializeField] private int maxImpactLights = 8; private TowerCombat combat; private readonly List pool = new List(); + private Light flashLight; + private readonly List impactLightPool = new List(); + + // Snapshot of the current flash so Update can re-randomize the bolts each frame. + private Vector3[] activeTargets; + private int activeCount; + private bool active; private float hideAt = -1f; private void Awake() => combat = GetComponent(); @@ -51,31 +108,120 @@ namespace TD.Combat private void Update() { - if (hideAt >= 0f && Time.time >= hideAt) + if (!active) return; + + if (Time.time >= hideAt) { - for (int i = 0; i < pool.Count; i++) pool[i].enabled = false; - hideAt = -1f; + HideAll(); + active = false; + return; } + + // Re-draw every frame with fresh jitter so the bolts crackle while visible. + RedrawBolts(); } private void HandleAreaFired(Vector3[] positions) { - if (positions == null) return; + if (positions == null || positions.Length == 0) return; + activeTargets = positions; // RPC hands us a fresh array each call — safe to keep + activeCount = positions.Length; + active = true; + hideAt = Time.time + arcDuration; + + RedrawBolts(); + } + + private void RedrawBolts() + { Vector3 origin = emitter != null ? emitter.position : transform.position + Vector3.up; - for (int i = 0; i < positions.Length; i++) + for (int i = 0; i < activeCount; i++) { var lr = GetLine(i); - DrawArc(lr, origin, positions[i]); + DrawBolt(lr, origin, activeTargets[i]); lr.enabled = true; } - // Disable any leftover lines from a previous, larger tick. - for (int i = positions.Length; i < pool.Count; i++) + // Disable any leftover bolts from a previous, larger tick. + for (int i = activeCount; i < pool.Count; i++) pool[i].enabled = false; - hideAt = Time.time + arcDuration; + // Pulse the emitter light so the coil/source lights nearby enemies. + if (castLight) + { + var fl = GetFlashLight(); + fl.intensity = lightIntensity * Random.Range(0.7f, 1f); // electrical flicker + fl.enabled = true; + } + + // Pulse a light at each bolt's impact point so far-struck enemies get lit by the + // bolt itself. Capped for performance; extra targets rely on the emitter flash. + if (castImpactLight) + { + int lit = Mathf.Min(activeCount, maxImpactLights); + for (int i = 0; i < lit; i++) + { + var il = GetImpactLight(i); + il.transform.position = activeTargets[i]; + il.intensity = impactLightIntensity * Random.Range(0.7f, 1f); + il.enabled = true; + } + for (int i = lit; i < impactLightPool.Count; i++) + impactLightPool[i].enabled = false; + } + } + + private void HideAll() + { + for (int i = 0; i < pool.Count; i++) + pool[i].enabled = false; + + if (flashLight != null) flashLight.enabled = false; + for (int i = 0; i < impactLightPool.Count; i++) + impactLightPool[i].enabled = false; + } + + // Lazily creates a small world-space point light for a bolt's impact point. + private Light GetImpactLight(int index) + { + while (impactLightPool.Count <= index) + { + var go = new GameObject($"ArcImpactLight_{impactLightPool.Count}"); + go.transform.SetParent(transform, worldPositionStays: true); + + var l = go.AddComponent(); + l.type = LightType.Point; + l.color = lightColor; + l.range = impactLightRange; + l.shadows = LightShadows.None; + l.intensity = 0f; + l.enabled = false; + impactLightPool.Add(l); + } + return impactLightPool[index]; + } + + // Lazily creates the point light at the emitter (parented so it tracks the coil). + private Light GetFlashLight() + { + if (flashLight == null) + { + var go = new GameObject("ArcFlashLight"); + Transform parent = emitter != null ? emitter : transform; + go.transform.SetParent(parent, worldPositionStays: false); + go.transform.localPosition = Vector3.zero; + + flashLight = go.AddComponent(); + flashLight.type = LightType.Point; + flashLight.color = lightColor; + flashLight.range = lightRange; + flashLight.shadows = LightShadows.None; // perf: brief flashes don't need shadows + flashLight.intensity = 0f; + flashLight.enabled = false; + } + return flashLight; } private LineRenderer GetLine(int index) @@ -84,33 +230,74 @@ namespace TD.Combat { var go = new GameObject($"Arc_{pool.Count}"); go.transform.SetParent(transform, worldPositionStays: false); + var lr = go.AddComponent(); - lr.useWorldSpace = true; - lr.widthMultiplier = arcWidth; - lr.numCapVertices = 2; - lr.textureMode = LineTextureMode.Stretch; + lr.useWorldSpace = true; + lr.widthMultiplier = arcWidth; + lr.numCapVertices = 4; + lr.numCornerVertices = 4; + lr.alignment = LineAlignment.View; // face the camera, like a billboard + lr.textureMode = LineTextureMode.Stretch; lr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; - lr.material = arcMaterial != null - ? arcMaterial - : new Material(Shader.Find("Sprites/Default")); - lr.startColor = lr.endColor = arcColor; - lr.enabled = false; + lr.material = arcMaterial != null ? arcMaterial : CreateFallbackMaterial(); + + // HDR color so Bloom in the post stack picks the bolt up as a glow. + Color hdr = arcColor * Mathf.Max(1f, emissionIntensity); + hdr.a = 1f; + lr.startColor = lr.endColor = hdr; + + lr.enabled = false; pool.Add(lr); } return pool[index]; } - private void DrawArc(LineRenderer lr, Vector3 a, Vector3 b) + // Additive unlit fallback so bolts still glow if no material is assigned. Prefer + // assigning a real material in the inspector. + private static Material CreateFallbackMaterial() + { + var shader = Shader.Find("Universal Render Pipeline/Particles/Unlit") + ?? Shader.Find("Sprites/Default"); + return new Material(shader); + } + + // Builds an arced, jagged bolt from a to b. A smooth sine bow gives the overall arc; + // tapered random perpendicular offsets give the electrical crackle. Endpoints stay + // exactly on the coil and the enemy. + private void DrawBolt(LineRenderer lr, Vector3 a, Vector3 b) { int n = Mathf.Max(2, segmentsPerArc); lr.positionCount = n; + + Vector3 delta = b - a; + float len = delta.magnitude; + Vector3 dir = len > 1e-4f ? delta / len : Vector3.forward; + + // Bow toward world-up, projected perpendicular to the bolt (any perpendicular for + // near-vertical bolts), plus a sideways axis for 3D crackle. + Vector3 bowDir = Vector3.up - dir * Vector3.Dot(Vector3.up, dir); + if (bowDir.sqrMagnitude < 1e-4f) bowDir = Vector3.Cross(dir, Vector3.right); + bowDir.Normalize(); + Vector3 sideDir = Vector3.Cross(dir, bowDir); + + float bow = arcHeight * len; + for (int s = 0; s < n; s++) { float t = s / (float)(n - 1); - Vector3 p = Vector3.Lerp(a, b, t); - // Jitter interior points only — endpoints stay anchored to the coil and enemy. + Vector3 p = a + delta * t; + + // Smooth arc — sine peaks at the middle, zero at both ends. + float curve = Mathf.Sin(t * Mathf.PI); + p += bowDir * (bow * curve); + + // Crackle on interior points, tapered to nothing at the ends. if (s != 0 && s != n - 1) - p += Random.insideUnitSphere * jitter; + { + p += bowDir * (Random.Range(-jitter, jitter) * curve); + p += sideDir * (Random.Range(-jitter, jitter) * curve); + } + lr.SetPosition(s, p); } } diff --git a/Assets/_Project/Scripts/Combat/TowerCombat.cs b/Assets/_Project/Scripts/Combat/TowerCombat.cs index 1c37234..25006a3 100644 --- a/Assets/_Project/Scripts/Combat/TowerCombat.cs +++ b/Assets/_Project/Scripts/Combat/TowerCombat.cs @@ -473,8 +473,6 @@ namespace TD.Combat { var multiplier = GetOwnerBuffManager()?.GetMultiplier(BuffStat.Damage) ?? 1f; float effectiveDamage = p.Damage * multiplier; - Debug.Log("multiplier: " + multiplier); - Debug.Log("Effective damage: " + effectiveDamage); target.TakeDamage(effectiveDamage, p.DamageType, owner); ApplyStatusEffect(p, target, owner); } diff --git a/Assets/_Project/Settings/DefaultVolumeProfile.asset b/Assets/_Project/Settings/DefaultVolumeProfile.asset index 66d98de..1c6d468 100644 --- a/Assets/_Project/Settings/DefaultVolumeProfile.asset +++ b/Assets/_Project/Settings/DefaultVolumeProfile.asset @@ -37,13 +37,13 @@ MonoBehaviour: active: 1 shadows: m_OverrideState: 1 - m_Value: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_Value: {r: 0.5458793, g: 0.6841803, b: 0.8207547, a: 1} highlights: m_OverrideState: 1 - m_Value: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_Value: {r: 0.9811321, g: 0.7474893, b: 0.50445, a: 1} balance: m_OverrideState: 1 - m_Value: 0 + m_Value: -25 --- !u!114 &-8104416584915340131 MonoBehaviour: m_ObjectHideFlags: 3 @@ -161,10 +161,10 @@ MonoBehaviour: active: 1 postExposure: m_OverrideState: 1 - m_Value: 0 + m_Value: -0.2 contrast: m_OverrideState: 1 - m_Value: 0 + m_Value: 20 colorFilter: m_OverrideState: 1 m_Value: {r: 1, g: 1, b: 1, a: 1} @@ -173,7 +173,7 @@ MonoBehaviour: m_Value: 0 saturation: m_OverrideState: 1 - m_Value: 0 + m_Value: -25 --- !u!114 &-6288072647309666549 MonoBehaviour: m_ObjectHideFlags: 3 @@ -192,7 +192,7 @@ MonoBehaviour: m_Value: 0 intensity: m_OverrideState: 1 - m_Value: 0 + m_Value: 0.3 response: m_OverrideState: 1 m_Value: 0.8 @@ -214,7 +214,7 @@ MonoBehaviour: active: 1 mode: m_OverrideState: 1 - m_Value: 0 + m_Value: 2 neutralHDRRangeReductionMode: m_OverrideState: 1 m_Value: 2 @@ -363,10 +363,10 @@ MonoBehaviour: m_Value: 1 threshold: m_OverrideState: 1 - m_Value: 0.9 + m_Value: 0.7 intensity: m_OverrideState: 1 - m_Value: 0 + m_Value: 1 scatter: m_OverrideState: 1 m_Value: 0.7 @@ -410,10 +410,10 @@ MonoBehaviour: active: 1 temperature: m_OverrideState: 1 - m_Value: 0 + m_Value: -8 tint: m_OverrideState: 1 - m_Value: 0 + m_Value: 5 --- !u!114 &-581120513425526550 MonoBehaviour: m_ObjectHideFlags: 3 @@ -855,7 +855,7 @@ MonoBehaviour: active: 1 intensity: m_OverrideState: 1 - m_Value: 0 + m_Value: 0.1 --- !u!114 &6940869943325143175 MonoBehaviour: m_ObjectHideFlags: 3 @@ -890,7 +890,7 @@ MonoBehaviour: m_Value: {x: 0.5, y: 0.5} intensity: m_OverrideState: 1 - m_Value: 0 + m_Value: 0.35 smoothness: m_OverrideState: 1 m_Value: 0.2 diff --git a/Assets/_Project/Settings/Render Settings/URP-High.asset b/Assets/_Project/Settings/Render Settings/URP-High.asset index 074ee15..330830a 100644 --- a/Assets/_Project/Settings/Render Settings/URP-High.asset +++ b/Assets/_Project/Settings/Render Settings/URP-High.asset @@ -45,7 +45,7 @@ MonoBehaviour: m_MainLightShadowsSupported: 1 m_MainLightShadowmapResolution: 4096 m_AdditionalLightsRenderingMode: 1 - m_AdditionalLightsPerObjectLimit: 2 + m_AdditionalLightsPerObjectLimit: 6 m_AdditionalLightShadowsSupported: 1 m_AdditionalLightsShadowmapResolution: 4096 m_AdditionalLightsShadowResolutionTierLow: 256 diff --git a/Assets/_Project/Settings/Render Settings/URP-Medium.asset b/Assets/_Project/Settings/Render Settings/URP-Medium.asset index 3b9f2ab..c5c5444 100644 --- a/Assets/_Project/Settings/Render Settings/URP-Medium.asset +++ b/Assets/_Project/Settings/Render Settings/URP-Medium.asset @@ -45,7 +45,7 @@ MonoBehaviour: m_MainLightShadowsSupported: 1 m_MainLightShadowmapResolution: 2048 m_AdditionalLightsRenderingMode: 1 - m_AdditionalLightsPerObjectLimit: 2 + m_AdditionalLightsPerObjectLimit: 4 m_AdditionalLightShadowsSupported: 1 m_AdditionalLightsShadowmapResolution: 2048 m_AdditionalLightsShadowResolutionTierLow: 256 diff --git a/Assets/_Project/Settings/Render Settings/URP-Ultra.asset b/Assets/_Project/Settings/Render Settings/URP-Ultra.asset index 9564d20..46ffed0 100644 --- a/Assets/_Project/Settings/Render Settings/URP-Ultra.asset +++ b/Assets/_Project/Settings/Render Settings/URP-Ultra.asset @@ -45,7 +45,7 @@ MonoBehaviour: m_MainLightShadowsSupported: 1 m_MainLightShadowmapResolution: 8192 m_AdditionalLightsRenderingMode: 1 - m_AdditionalLightsPerObjectLimit: 2 + m_AdditionalLightsPerObjectLimit: 8 m_AdditionalLightShadowsSupported: 1 m_AdditionalLightsShadowmapResolution: 8192 m_AdditionalLightsShadowResolutionTierLow: 256