Projectile Extendibility request/ suggestions

Hi,
I need to extended the Opsive Grenade to tie it into my own ability system (just some small stuff like getting damage modifies and triggering some custom events).
The problem I need to solve is that I want an event to fire when the grenade spawns objects, like the explosion, so it can have context data from my ability system passed into/ attached to it.

So, I went to see if such an event already exists, which it does not. This is fine, because I can extend the class.
To extend the class, I needed to go into Opsive code and make Destruct() virtual at line 368 in ProjectileBase.cs.
I believe that Destruct was not intended to be overridden because it invokes the OnDestruct event, and since events cannot be invoked by child classes, it would cause issues.
To solve this I created an wrapper method for the OnDestruct event in ProjectileBase, and called the wrapper method in my overridden Destruct method in the child class.

The main issue here is that I have modified Opsive code, which is reverted after UCC updates.

Could an OnSpawnDestructionObject event be added to the Destruct method in ProjectileBase, or could I be guided on a more sustainable way to extend the class.

Here is how I extended the Destruct method.
C#:
 public class MBSGrenade : Grenade
    {
        public event Action<GameObject> OnSpawnDestructionObject = delegate { }; //Line Added

        public override void Destruct(Vector3 hitPosition, Vector3 hitNormal)
        {
            InvokeOnDestruct(hitPosition, hitNormal);
            m_ProjectileOwner?.OnProjectileDestruct(this, hitPosition, hitNormal);

            for (int i = 0; i < m_SpawnedObjectsOnDestruction.Length; ++i)
            {
                if (m_SpawnedObjectsOnDestruction[i] == null)
                {
                    continue;
                }

                var spawnedObject = m_SpawnedObjectsOnDestruction[i].Instantiate(hitPosition, hitNormal, m_NormalizedGravity);
                OnSpawnDestructionObject.Invoke(spawnedObject); //Line Added
                if (spawnedObject == null)
                {
                    continue;
                }
                var explosion = spawnedObject.GetCachedComponent<MBSExplosion>();
                if (explosion != null)
                {
                    explosion.Explode(m_ImpactDamageData, m_Owner, m_OwnerDamageSource);
                }
            }

            // The component and collider no longer need to be enabled after the object has been destroyed.
            if (m_Collider != null)
            {
                m_Collider.enabled = false;
            }
            if (m_ParticleSystem != null)
            {
                m_ParticleSystem.Stop();
            }
            m_Destroyed = true;
            m_DestroyEvent = null;
            enabled = false;

            // The destructible should be destroyed.
#if ULTIMATE_CHARACTER_CONTROLLER_VERSION_2_MULTIPLAYER
            if (NetworkObjectPool.IsNetworkActive()) {
                // The object may have already been destroyed over the network.
                if (!m_GameObject.activeSelf) {
                    return;
                }
                NetworkObjectPool.Destroy(m_GameObject);
                return;
            }
#endif
            ObjectPoolBase.Destroy(m_GameObject);
        }
    }

I have this same issue with the lack of an "OnAquireTarget" event (Public Event Action<Transform or GameObject>) on the "Crosshair monitor" script.

Additionally, I extended the Spawnpoint "GetPlacement()" method to include an argument for object size, so I could be returned spawn positions for multiple objects (say I wanted 10 guys to spawn in a large square spawn point, I need to include their model size. Otherwise the first one will spawn, and the rest will not find valid positions.)
Having the ability to spawn multiple characters from one large region would be nice if it was built in.

If any of these features are built in, and I just missed how to implement them the normal way, please let me know/ point me in the direction of relevant documentation.
 
Last edited:
Yes, I'll change that method to virtual. I will look at making the crosshairs monitor more extensible. Extending the spawn point also makes sense.
 
Cool! I'll provide my code changes to the crosshair monitor and spawnpoint, on the offchance it makes things easier. I will say that they are more in the line of "quick-and-dirty" fixes than proper implementations because I wanted to touch as little as possible.

Crosshairs:
Added line
C#:
public event System.Action<Transform> OnAquiredTarget= delegate { };
anywhere in the class scope variable declarations.

added invoke
C#:
OnAquiredTarget.Invoke(target);
at the end of the update method. Ideally I would change this to only invoke when a new target is selected/ lost. I use this event with an "EnemyHealthMonitor" which displays the health of the enemy that the crosshairs are pointed at.
1677759585149.png

Spawnpoint:
C#:
        public virtual bool GetPlacement(GameObject spawningObject, ref Vector3 position, ref Quaternion rotation, float objectSize=-1)//Parameter Added
        {
            position = RandomPosition(0);

            objectSize = objectSize >= 0 ? objectSize : m_Size; //Line Added

            // Ensure the spawn point is clear of any obstructing objects.
            if (m_CheckForObstruction) {
                var attempt = 0;
                var success = false;
                while (attempt < m_PlacementAttempts) {
                    if (m_Shape == SpawnShape.Point) {
                        // A point will always succeed.
                        success = true;
                    } else if (m_Shape == SpawnShape.Sphere) {
                        // Ignore any collisions with itself. //Radius argument in OverlapSphere changed
                        var overlapCount = Physics.OverlapSphereNonAlloc(position, objectSize / 2, m_ObstructionColliders, m_ObstructionLayers, QueryTriggerInteraction.Ignore);
                        if (spawningObject != null) {
                            for (int i = overlapCount - 1; i > -1; --i) {
                                if (!m_ObstructionColliders[i].transform.IsChildOf(spawningObject.transform)) {
                                    break;
                                }
                                overlapCount--;
                            }
                        }
                        success = overlapCount == 0;
                        if (success) {
                            break;
                        }
                    } else { // Box.
                        var extents = Vector3.zero;
                        extents.x = extents.z = objectSize / 2; //Line changed m_Size-> objectSize
                        extents.y = m_GroundSnapHeight / 2;
                        ....

When spawning enemy waves, my wave manager utilizes this change by
C++:
            Vector3 position = spawnpoint.transform.position;
            Quaternion rotation = spawnpoint.transform.rotation;
            float size = enemyPrefab.GetComponentInChildren<Collider>().bounds.size.magnitude;
            if (spawnpoint.GetPlacement(enemyPrefab, ref position, ref rotation, size))
            {
                GameObject spawnedObject = Instantiate(enemyPrefab, position, rotation);
                //register to the enemy onDeath event
                EventHandler.RegisterEvent<Vector3, Vector3, GameObject>(spawnedObject, "OnDeath", OnDeath);
                enemiesRemaining++;
            }
            else
            {
                Debug.Log($"spawn placement failed for {enemyPrefab.name}");
            }
If you look at the image above, the two enemy atlases were spawned from one spawnpoint using this.

Hope this helps, and thanks for taking a look and considering.
 
Top