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 !
 
Top