Hitscan penetration

Cheo

Member
Hello everyone, here are a few modifications to enable penetration for hitscan shots !

First of all, let's start with making penetrable surfaces by adding the following to the Surface Type script :

C#:
        [SerializeField] protected bool m_Penetrable = true;
        [SerializeField] protected float m_PenetrationMod = 0.5f;
        public bool Penetrable { get { return m_Penetrable; } }
        public float PenetrationMod {  get { return m_PenetrationMod; } }

Next, we also have to edit the Surface Type Inspector in order to display these parameters and save them :

Code:
        public override void OnInspectorGUI()
        {
            base.OnInspectorGUI();

            EditorGUI.BeginChangeCheck();
            EditorGUILayout.PropertyField(PropertyFromName("m_ImpactEffects"), true);
            EditorGUILayout.PropertyField(PropertyFromName("m_Penetrable"));
            EditorGUILayout.PropertyField(PropertyFromName("m_PenetrationMod"));
            if (EditorGUI.EndChangeCheck()) {
                Shared.Editor.Utility.EditorUtility.RecordUndoDirtyObject(target, "Change Value");
                serializedObject.ApplyModifiedProperties();
            }
        }

Let's then get to the Shootable Weapon script, create these parameters :

C#:
        [SerializeField] protected bool m_Penetrate = true;
        [SerializeField] protected float m_PenetrationMod = 0.5f;
        [SerializeField] protected int m_MaxPenetrations = 5;
        public bool Penetrate { get { return m_Penetrate; } set { m_Penetrate = value; } }
        public float PenetrationMod { get { return m_PenetrationMod; } set { m_PenetrationMod = value; } }
        public int MaxPenetrations { get { return m_MaxPenetrations; } set { m_MaxPenetrations = value; } }
 

Cheo

Member
And now change the HitscanFire method :

C#:
        private void HitscanFire(float strength)
        {
            // The hitscan should be fired from the center of the camera so the hitscan will always hit the correct crosshairs location.
            var useLookPosition = m_FireInLookSourceDirection && !m_CharacterLocomotion.ActiveMovementType.UseIndependentLook(false);
            var firePoint = useLookPosition ? m_LookSource.LookPosition(true) : m_ShootableWeaponPerspectiveProperties.FirePointLocation.position;
            var fireDirection = FireDirection(firePoint);
            var fireRay = new Ray(firePoint, fireDirection);
            // Prevent the ray between the character and the look source from causing a false collision.
            if (useLookPosition && !m_CharacterLocomotion.FirstPersonPerspective) {
                var direction = m_CharacterTransform.InverseTransformPoint(firePoint);
                direction.y = 0;
                fireRay.origin = fireRay.GetPoint(direction.magnitude);
            }
            var hitCount = Physics.RaycastNonAlloc(fireRay, m_HitscanRaycastHits, m_HitscanFireRange * strength, m_ImpactLayers.value, m_HitscanTriggerInteraction);
            var hasHit = false;

#if UNITY_EDITOR
            if (hitCount == m_MaxHitscanCollisionCount) {
                Debug.LogWarning($"Warning: The maximum number of colliders have been hit by {m_GameObject.name}. Consider increasing the Max Hitscan Collision Count value.", this);
            }
#endif

            int penetrationCount = 0;
            float currentDamage = m_DamageAmount;

            for (int i = 0; i < hitCount; ++i) {
                var closestRaycastHit = QuickSelect.SmallestK(m_HitscanRaycastHits, hitCount, i, m_RaycastHitComparer);
                var hitGameObject = closestRaycastHit.transform.gameObject;
                // The character can't shoot themself.
                if (hitGameObject.transform.IsChildOf(m_CharacterTransform)
#if FIRST_PERSON_CONTROLLER
                    // The cast should not hit any colliders who are a child of the camera.
                    || hitGameObject.GetCachedParentComponent<FirstPersonController.Character.FirstPersonObjects>() != null
#endif
                        ) {
                    continue;
                }

                // The shield can absorb some (or none) of the damage from the hitscan.
                //var damageAmount = m_DamageAmount;

                var damageAmount = currentDamage;



#if ULTIMATE_CHARACTER_CONTROLLER_MELEE
                ShieldCollider shieldCollider;
                if ((shieldCollider = hitGameObject.GetCachedComponent<ShieldCollider>()) != null) {
                    damageAmount = shieldCollider.Shield.Damage(this, damageAmount);
                }
#endif

                // Allow a custom event to be received.
                EventHandler.ExecuteEvent<float, Vector3, Vector3, GameObject, object, Collider>(closestRaycastHit.transform.gameObject, "OnObjectImpact", damageAmount, closestRaycastHit.point, m_ImpactForce * strength * fireDirection, m_Character, this, closestRaycastHit.collider);
                if (m_OnHitscanImpactEvent != null) {
                    m_OnHitscanImpactEvent.Invoke(damageAmount, closestRaycastHit.point, m_ImpactForce * strength * fireDirection, m_Character);
                }

                // If the shield didn't absorb all of the damage then it should be applied to the character.
                if (damageAmount > 0) {
                    var damageTarget = DamageUtility.GetDamageTarget(hitGameObject);
                    if (damageTarget != null) {
                        var pooledDamageData = GenericObjectPool.Get<DamageData>();
                        pooledDamageData.SetDamage(this, damageAmount, closestRaycastHit.point, fireDirection, m_ImpactForce * strength, m_ImpactForceFrames, 0, closestRaycastHit.collider);
                        var damageProcessorModule = m_CharacterLocomotion.gameObject.GetCachedComponent<DamageProcessorModule>();
                        if (damageProcessorModule != null) {
                            damageProcessorModule.ProcessDamage(m_DamageProcessor, damageTarget, pooledDamageData);
                        } else {
                            if (m_DamageProcessor == null) { m_DamageProcessor = DamageProcessor.Default; }
                            m_DamageProcessor.Process(damageTarget, pooledDamageData);
                        }
                        GenericObjectPool.Return(pooledDamageData);
                    } else {
                        // If the damage target exists it will apply a force to the rigidbody in addition to procesing the damage. Otherwise just apply the force to the rigidbody.
                        if (m_ImpactForce > 0 && closestRaycastHit.rigidbody != null && !closestRaycastHit.rigidbody.isKinematic) {
                            closestRaycastHit.rigidbody.AddForceAtPosition((m_ImpactForce * strength * MathUtility.RigidbodyForceMultiplier) * fireDirection, closestRaycastHit.point);
                        }
                    }
                }

                // Spawn a tracer which moves to the hit point.
                if (m_Tracer != null) {
                    SchedulerBase.ScheduleFixed(m_TracerSpawnDelay, AddHitscanTracer, closestRaycastHit.point);
                }

                // An optional state can be activated on the hit object.
                if (!string.IsNullOrEmpty(m_ImpactStateName)) {
                    var characterLocomotion = hitGameObject.GetCachedParentComponent<UltimateCharacterLocomotion>();
                    StateManager.SetState(characterLocomotion != null ? characterLocomotion.gameObject : hitGameObject, m_ImpactStateName, true);
                    // If the timer isn't -1 then the state should be disabled after a specified amount of time. If it is -1 then the state
                    // will have to be disabled manually.
                    if (m_ImpactStateDisableTimer != -1) {
                        StateManager.DeactivateStateTimer(hitGameObject, m_ImpactStateName, m_ImpactStateDisableTimer);
                    }
                }

                // The surface manager will apply effects based on the type of bullet hit.
                SurfaceManager.SpawnEffect(closestRaycastHit, m_SurfaceImpact, m_CharacterLocomotion.GravityDirection, m_CharacterLocomotion.TimeScale, m_Item.GetVisibleObject());

                hasHit = true;
                penetrationCount++;

                

                if (m_Penetrate)
                {
                    if (hitGameObject.GetComponent<SurfaceIdentifier>())
                    {
                        if (hitGameObject.GetComponent<SurfaceIdentifier>().SurfaceType.Penetrable)
                        {
                            currentDamage *= hitGameObject.GetComponent<SurfaceIdentifier>().SurfaceType.PenetrationMod;
                        }
                        else
                        {
                            break;
                        }
                    }
                    else
                    {
                        currentDamage *= m_PenetrationMod;
                    }
                }

                if (m_Penetrate && penetrationCount >= m_MaxPenetrations || !m_Penetrate)
                {
                    break;
                }
            }

            // A tracer should still be spawned if no object was hit.
            if (!hasHit && m_Tracer != null) {
                SchedulerBase.ScheduleFixed(m_TracerSpawnDelay, AddHitscanTracer,
                    MathUtility.TransformPoint(firePoint, Quaternion.LookRotation(fireDirection), new Vector3(0, 0, m_TracerDefaultLength)));
            }
        }
 

Cheo

Member
Lastly, we have to edit the Shootable Weapon Inspector as well, I personally added these under the "Impact" foldout :

C#:
                    EditorGUILayout.PropertyField(PropertyFromName("m_Penetrate"));
                    EditorGUILayout.PropertyField(PropertyFromName("m_PenetrationMod"));
                    EditorGUILayout.PropertyField(PropertyFromName("m_MaxPenetrations"));

And that's it ! Here is an example from my current project (kinda weird I know) :


I initially struggled to apprehend the creation of this mechanic, but once I found this for loop in Shootable Weapon the idea naturally arose ! It really isn't that complicated, and I think Opsive should add it to UCC ! Anyway don't hesitate to try it out for yourself and share your thoughts and ideas !
 

Cheo

Member
Hi, if I remember correctly I implemented it in my last project on Unity 2021.2.3f1 and UCC 2.4.3. No changes were made to the SurfaceType and ShootableWeapon in the latest updates, so you should be good to go ! Please let me know if you managed to reproduce it and are satisfied with the result !
 
Top