From e433bfbd18a79826a050f0d77848da4c885b8589 Mon Sep 17 00:00:00 2001 From: Danyil Yedelkin Date: Sat, 25 Oct 2025 13:58:01 +0200 Subject: [PATCH 1/3] #2768 and #2767 (fixed bugs) fixed bugs with "Touch spell hit detection targets the player when aiming downwards" and "WeaponManager attack raycast layer mask collides with objects on the "Ignore Raycasts" layer" --- Assets/Scripts/Game/DaggerfallMissile.cs | 51 ++++++++++++++++++++---- Assets/Scripts/Game/WeaponManager.cs | 11 +++-- 2 files changed, 51 insertions(+), 11 deletions(-) diff --git a/Assets/Scripts/Game/DaggerfallMissile.cs b/Assets/Scripts/Game/DaggerfallMissile.cs index e19184f823..343b4cc07b 100644 --- a/Assets/Scripts/Game/DaggerfallMissile.cs +++ b/Assets/Scripts/Game/DaggerfallMissile.cs @@ -55,6 +55,8 @@ public class DaggerfallMissile : MonoBehaviour #region Fields + private static int TouchMask = -1; + const int coldMissileArchive = 376; const int fireMissileArchive = 375; const int magicMissileArchive = 379; @@ -390,15 +392,19 @@ void DoCollision(Collision collision, Collider other) public static DaggerfallEntityBehaviour GetEntityTargetInTouchRange(Vector3 aimPosition, Vector3 aimDirection) { - // Fire ray along caster facing - // Origin point of ray is set back slightly to fix issue where strikes against target capsules touching caster capsule do not connect + // Nudge origin slightly forward to avoid intersecting the caster capsule when looking down + const float originNudge = 0.01f; + Vector3 origin = aimPosition + aimDirection * originNudge; + RaycastHit hit; - aimPosition -= aimDirection * 0.1f; - Ray ray = new Ray(aimPosition, aimDirection); - if (Physics.SphereCast(ray, SphereCastRadius, out hit, TouchRange)) + Ray ray = new Ray(origin, aimDirection); + + if (Physics.SphereCast(ray, SphereCastRadius, out hit, TouchRange, GetTouchMask(), QueryTriggerInteraction.Ignore)) + { return hit.transform.GetComponent(); - else - return null; + } + + return null; } #endregion @@ -466,6 +472,37 @@ void DoAreaOfEffect(Vector3 position, bool ignoreCaster = false) missileReleased = true; } + /// + /// Returns a cached layer mask for touch targeting. Built lazily to avoid Unity init errors. + /// Excludes Player, Ignore Raycast, and Automap so the cast never hits the caster or UI layers. + /// + private static int GetTouchMask() + { + if (TouchMask != -1) + { + return TouchMask; + } + + int mask = Physics.DefaultRaycastLayers; + + int player = LayerMask.NameToLayer("Player"); + if (player >= 0) + { + mask &= ~(1 << player); + } + + mask &= ~(1 << Physics.IgnoreRaycastLayer); + + int automap = LayerMask.NameToLayer("Automap"); + if (automap >= 0) + { + mask &= ~(1 << automap); + } + + TouchMask = mask; + return TouchMask; + } + // Get missile aim position from player or enemy mobile Vector3 GetAimPosition() { diff --git a/Assets/Scripts/Game/WeaponManager.cs b/Assets/Scripts/Game/WeaponManager.cs index f26f276ad6..7340d21ec5 100644 --- a/Assets/Scripts/Game/WeaponManager.cs +++ b/Assets/Scripts/Game/WeaponManager.cs @@ -193,12 +193,15 @@ public enum MouseDirections void Start() { - //weaponSensitivity = DaggerfallUnity.Settings.WeaponSensitivity; mainCamera = GameObject.FindGameObjectWithTag("MainCamera"); - player = transform.gameObject; - playerLayerMask = ~(1 << LayerMask.NameToLayer("Player")); + player = gameObject; + + playerLayerMask = Physics.DefaultRaycastLayers; + playerLayerMask &= ~(1 << LayerMask.NameToLayer("Player")); + playerLayerMask &= ~(1 << Physics.IgnoreRaycastLayer); + _gesture = new Gesture(); - _longestDim = Math.Max(Screen.width, Screen.height); + _longestDim = Mathf.Max(Screen.width, Screen.height); SetMelee(ScreenWeapon); } From 16c670330afc9dc3632a2bd1740c3dfec10cefa3 Mon Sep 17 00:00:00 2001 From: Danyil Yedelkin Date: Mon, 27 Oct 2025 23:14:11 +0100 Subject: [PATCH 2/3] optimization for DaggerfallMissile.cs ### Summary This PR improves runtime performance of DaggerfallMissile by reducing GC allocations and minimizing Unity API overhead. ### Changes - Replaced `Physics.OverlapSphere()` + new `List<>` with `Physics.OverlapSphereNonAlloc()` and a reusable buffer to prevent per-frame heap allocations during AoE checks. - Cached references to `GameManager`, `WeaponManager`, and frequently used components (`Collider`, `EnemySenses`, `EnemyAttack`, `CharacterController`) to reduce expensive `GetComponent()` and singleton lookups. ### Impact - Eliminates GC spikes and microstutters during large-scale spell effects (fireballs, explosions, etc.). - Reduces CPU cost per missile instance, improving scalability in combat-heavy scenes. ### Verification - Tested AoE spells and ranged projectiles with multiple active missiles. - No change in gameplay behavior; only internal performance improvements. --- Assets/Scripts/Game/DaggerfallMissile.cs | 137 +++++++++++++++-------- 1 file changed, 90 insertions(+), 47 deletions(-) diff --git a/Assets/Scripts/Game/DaggerfallMissile.cs b/Assets/Scripts/Game/DaggerfallMissile.cs index 343b4cc07b..7588ca45de 100644 --- a/Assets/Scripts/Game/DaggerfallMissile.cs +++ b/Assets/Scripts/Game/DaggerfallMissile.cs @@ -14,6 +14,7 @@ using DaggerfallWorkshop.Utility; using DaggerfallWorkshop.Game.MagicAndEffects; using DaggerfallWorkshop.Game.Entity; +using System.Runtime.InteropServices; namespace DaggerfallWorkshop.Game { @@ -55,8 +56,6 @@ public class DaggerfallMissile : MonoBehaviour #region Fields - private static int TouchMask = -1; - const int coldMissileArchive = 376; const int fireMissileArchive = 375; const int magicMissileArchive = 379; @@ -66,6 +65,20 @@ public class DaggerfallMissile : MonoBehaviour public const float SphereCastRadius = 0.25f; public const float TouchRange = 3.0f; + private static int TouchMask = -1; + + private static readonly Collider[] aoeBuffer = new Collider[64]; + private readonly List tmpTargets = new List(32); + + // Cached references + private GameManager gm; + private Camera mainCamera; + private WeaponManager weaponManager; + private Collider casterCollider; + private EnemySenses cachedEnemySenses; + private EnemyAttack cachedEnemyAttack; + private CharacterController casterController; + Vector3 direction; Light myLight; SphereCollider myCollider; @@ -171,88 +184,106 @@ public DaggerfallEntityBehaviour[] Targets private void Awake() { audioSource = transform.GetComponent(); + + gm = GameManager.Instance; + mainCamera = gm.MainCamera; + weaponManager = gm.WeaponManager; + audioSource = GetComponent(); } private void Start() { // Setup light and shadows myLight = GetComponent(); - myLight.enabled = EnableLight; + myLight.enabled = EnableLight && DaggerfallUnity.Settings.EnableSpellLighting; forceDisableSpellLighting = !DaggerfallUnity.Settings.EnableSpellLighting; - if (forceDisableSpellLighting) myLight.enabled = false; - if (!DaggerfallUnity.Settings.EnableSpellShadows) myLight.shadows = LightShadows.None; + if (!DaggerfallUnity.Settings.EnableSpellShadows) + { + myLight.shadows = LightShadows.None; + } + initialRange = myLight.range; initialIntensity = myLight.intensity; - // Setup collider + // Setup collider and rigidbody myCollider = GetComponent(); myCollider.radius = ColliderRadius; - // Setup rigidbody myRigidbody = GetComponent(); myRigidbody.useGravity = false; - // Use payload when available + // Setup payload if (payload != null) { - // Set payload missile properties caster = payload.CasterEntityBehaviour; targetType = payload.Settings.TargetType; elementType = payload.Settings.ElementType; - // Set spell billboard anims automatically from payload for mobile missiles - if (targetType == TargetTypes.SingleTargetAtRange || - targetType == TargetTypes.AreaAtRange) + if (targetType == TargetTypes.SingleTargetAtRange || targetType == TargetTypes.AreaAtRange) { UseSpellBillboardAnims(); } } - // Setup senses - if (caster && caster != GameManager.Instance.PlayerEntityBehaviour) + // Cache caster components + if (caster) { - enemySenses = caster.GetComponent(); + casterCollider = caster.GetComponent(); + cachedEnemySenses = caster.GetComponent(); + cachedEnemyAttack = caster.GetComponent(); + casterController = caster.GetComponent(); + + var missileCollider = GetComponent(); + if (casterCollider && missileCollider) + { + Physics.IgnoreCollision(casterCollider, missileCollider); + } + } + + // Cache enemy senses (non-player casters only) + if (caster && caster != gm.PlayerEntityBehaviour) + { + enemySenses = cachedEnemySenses; } // Setup arrow if (isArrow) { - // Create and orient 3d arrow goModel = GameObjectHelper.CreateDaggerfallMeshGameObject(99800, transform); - MeshCollider arrowCollider = goModel.GetComponent(); + var arrowCollider = goModel.GetComponent(); arrowCollider.sharedMesh = goModel.GetComponent().sharedMesh; arrowCollider.convex = true; arrowCollider.isTrigger = true; - // Offset up so it comes from same place LOS check is done from Vector3 adjust; - if (caster != GameManager.Instance.PlayerEntityBehaviour) + if (caster != gm.PlayerEntityBehaviour) { - CharacterController controller = caster.transform.GetComponent(); adjust = caster.transform.forward * 0.6f; - adjust.y += controller.height / 3; + if (casterController) + { + adjust.y += casterController.height / 3f; + } } else { - // Adjust slightly downward to match bow animation - adjust = (GameManager.Instance.MainCamera.transform.rotation * -Caster.transform.up) * 0.11f; - // Offset forward to avoid collision with player - adjust += GameManager.Instance.MainCamera.transform.forward * 0.6f; - // Adjust to the right or left to match bow animation - if (!GameManager.Instance.WeaponManager.ScreenWeapon.FlipHorizontal) - adjust += GameManager.Instance.MainCamera.transform.right * 0.15f; + adjust = (gm.MainCamera.transform.rotation * -caster.transform.up) * 0.11f; + adjust += gm.MainCamera.transform.forward * 0.6f; + + var right = gm.MainCamera.transform.right * 0.15f; + if (!gm.WeaponManager.ScreenWeapon.FlipHorizontal) + { + adjust += right; + } else - adjust -= GameManager.Instance.MainCamera.transform.right * 0.15f; + { + adjust -= right; + } } goModel.transform.localPosition = adjust; goModel.transform.rotation = Quaternion.LookRotation(GetAimDirection()); goModel.layer = gameObject.layer; } - - // Ignore missile collision with caster (this is a different check to AOE targets) - if (caster) - Physics.IgnoreCollision(caster.GetComponent(), this.GetComponent()); } private void Update() @@ -444,29 +475,34 @@ void DoMissile() // AOE can strike any number of targets within range with an option to exclude caster void DoAreaOfEffect(Vector3 position, bool ignoreCaster = false) { - List entities = new List(); - transform.position = position; - // Collect AOE targets and ignore duplicates - Collider[] overlaps = Physics.OverlapSphere(position, ExplosionRadius); - for (int i = 0; i < overlaps.Length; i++) + int count = Physics.OverlapSphereNonAlloc(position, ExplosionRadius, aoeBuffer, GetTouchMask(), QueryTriggerInteraction.Ignore); + tmpTargets.Clear(); + + for (int i = 0; i < count; i++) { - DaggerfallEntityBehaviour aoeEntity = overlaps[i].GetComponent(); + var beh = aoeBuffer[i].GetComponent(); + if (!beh) + { + continue; + } - if (ignoreCaster && aoeEntity == caster) + if (ignoreCaster && beh == caster) + { continue; + } - if (aoeEntity && !targetEntities.Contains(aoeEntity)) + if (!targetEntities.Contains(beh)) { - entities.Add(aoeEntity); - //Debug.LogFormat("Missile hit target {0} by AOE", aoeEntity.name); + tmpTargets.Add(beh); } } - // Add collection to target entities - if (entities.Count > 0) - targetEntities.AddRange(entities); + if (tmpTargets.Count > 0) + { + targetEntities.AddRange(tmpTargets); + } impactDetected = true; missileReleased = true; @@ -645,7 +681,14 @@ void AssignBowDamageToTarget(Collider arrowHitCollider) else { Transform hitTransform = arrowHitCollider.gameObject.transform; - GameManager.Instance.WeaponManager.WeaponDamage(GameManager.Instance.WeaponManager.LastBowUsed, true, isArrowSummoned, hitTransform, hitTransform.position, goModel.transform.forward); + + GameManager.Instance.WeaponManager.WeaponDamage( + GameManager.Instance.WeaponManager.LastBowUsed, + true, + isArrowSummoned, + hitTransform, + hitTransform.position, + goModel.transform.forward); } } From 8b1af1cf1295e5aedf865623495c07af35e60237 Mon Sep 17 00:00:00 2001 From: Danyil Yedelkin <79505924+DanyilYedelkin@users.noreply.github.com> Date: Tue, 28 Oct 2025 22:50:53 +0100 Subject: [PATCH 3/3] Remove unused namespace from DaggerfallMissile.cs Removed unused 'System.Runtime.InteropServices' namespace. --- Assets/Scripts/Game/DaggerfallMissile.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Assets/Scripts/Game/DaggerfallMissile.cs b/Assets/Scripts/Game/DaggerfallMissile.cs index 7588ca45de..20ff7a5f2a 100644 --- a/Assets/Scripts/Game/DaggerfallMissile.cs +++ b/Assets/Scripts/Game/DaggerfallMissile.cs @@ -14,7 +14,6 @@ using DaggerfallWorkshop.Utility; using DaggerfallWorkshop.Game.MagicAndEffects; using DaggerfallWorkshop.Game.Entity; -using System.Runtime.InteropServices; namespace DaggerfallWorkshop.Game {