Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

C# and dynamic methods parameters

Discussion in 'Scripting' started by SomehowLuke, Jul 27, 2022.

  1. SomehowLuke

    SomehowLuke

    Joined:
    Nov 11, 2015
    Posts:
    34
    Hi there,
    I have following problem I want to solve, but somehow I was not able to find the right solution how to do it in C#.

    I have a method in a parent class, lets say vehicle.cs
    Code (CSharp):
    1. protected virtual void InstantiateVehicle(Vector3 position, dynamic data)
    2. {
    3. // Does some parent stuff
    4. }
    Then I have a child class, car.cs, which is based on vehicle.cs
    Code (CSharp):
    1. protected override InstantiateVehicle(Vector3 position, Transform data)
    2. {
    3. //some child stuff
    4. }
    And there is a third class, bike.cs
    Code (CSharp):
    1. protected override InstantiateVehicle(Vector3 position, float data)
    2. {
    3. //some child stuff
    4. }
    As you can see I want to have dynamic parameter, that does the same stuff but sometimes I need a different type to work with. I always call the function by the parent object vehicle.InstantiateVehicle(...) and I would like to keep it this way and not changing all classes to the specific child vehicle with its own funciton.

    I have tried it with
    public virtual void InstantiateVehicle<T>(T data)

    but I do not know how to do this in the child class
    public override void InstantiateVehicle<float>(float data)
    public override void InstantiateVehicle<Transform>(transform data)

    does not work.

    Hope its clear what I want to achieve.
     
  2. ensiferum888

    ensiferum888

    Joined:
    May 11, 2013
    Posts:
    317
    I've never used dynamic but reading quickly on it it seems the variable type is resolved at run time.

    So in your overloaded methods keep the dynamic type but then in your code you can treat the data as a float or Transform. (I have not tested this so it could fail)

    Is there any reason you don't have your instantiate function only take the position and after that use another call to set the Transform or float value you want?
     
    SomehowLuke likes this.
  3. grizzly

    grizzly

    Joined:
    Dec 5, 2012
    Posts:
    357
    This:
    InstantiateVehicle<Transform>(transform data)


    Should be:
    InstantiateVehicle<Transform>(Vector3 position, Transform data)


    With:
    public virtual void InstantiateVehicle<T>(Vector3 position, T data)
     
    SomehowLuke likes this.
  4. SomehowLuke

    SomehowLuke

    Joined:
    Nov 11, 2015
    Posts:
    34

    Hi there, thanks for the answer, but this was actually just an example (not a really good one, sorry for that)
    So what I have is the parent with:

    public virtual void SetupData<T>(T data)
    {
    }

    and I have a child where I wanted to do this:

    public override void SetupData<float>(float data)
    {

    }

    But that does not work. I dont know how to work with the T in the child classes to have in one child the transform and in the other one the float and in future maybe other data types, who knows...
     
  5. SomehowLuke

    SomehowLuke

    Joined:
    Nov 11, 2015
    Posts:
    34
    I have tried it with dynamic but then I struggled with some kind of reference missing and as I understood it correctly, the Compiler does a runtime conversion and this costs performance therefore I have tried to make the parent class abstract with a type T and the childs define the value type they need but as you can see the post above, that does not work.

    I try to do this, because I have a global object pool that spawns the objects of different kinds. It takes the AssetReference and creates the objects and stores it internally in the object pool. Some classes now call the object pool and tell it to instantiate the object. These objects share the same interfacer IPoolObject but the behaviour sometimes needs different values (sometimes its a float, sometimes its a transform) therefore I want to give that IPoolObject also a SetupData(some data) without worring about, who gets this data at the end. But maybe I have to rethink the architecture...
     
  6. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    974
    I don't think you want to use dynamic. In addition to requiring extra references and likely limiting your targetable devices, it also carries a performance cost.

    Generics could work here. Since your method is called SetupData, I'm guessing you want to store the data that is passed in so you can work with it later. In that case, I think the variable type should be on the class rather than the function. Something like

    Code (csharp):
    1. public abstract class Vehicle<T> {
    2.   T data;
    3.  
    4.   public abstract void SetupData(T data);
    5. }
    6.  
    7. public class FloatingVehicle : Vehicle<float> {
    8.   public override void SetupData(float f) {
    9.     // manipulate float data here if needed
    10.     data = f;
    11.   }
    12. }
     
    mopthrow likes this.
  7. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,520
    Last I checked
    dynamic
    wasn't suitable for use on AOT platforms, such as iOS / Android.

    Might want to find out of that is still the case before you go too far down the
    dynamic
    rabbit hole. :)
     
  8. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    974
    I should have read your post more completely ‍:S

    The type coming out of your pool is IPoolObject, so you won't be able to put the type variable on the type.
    Your options are basically, 1) put the type variable on the type: IPoolObject<T> or 2) put the type variable on the method: SetupData<T>.

    The problem with 1, is that you can't store IPoolObject<float> in the same collection as IPoolObject<Transform>.
    The problem with 2, is that you can't override just SetupData<float> or SetupData<Transform> -- it has to be SetupData<T>.
    Either way, you need to do some casting.

    I'm assuming your pool provides an API to get a specific type of object. Otherwise, you wouldn't know what type of data to pass into SeupData anyways ..
    You should be able to modify your pool API to be generic, so that the returned value already has the correct type information to know how to call SetupData.
     
    SomehowLuke and grizzly like this.
  9. grizzly

    grizzly

    Joined:
    Dec 5, 2012
    Posts:
    357
    If you're willing to accept the overhead of a boxing/unboxing (relatively minor in comparison to the overall cost of instantiating), it would be simple to just pass an
    object

    Code (CSharp):
    1. public abstract class Base
    2. {
    3.     public virtual void SetupData (object data)
    4.     {
    5.     }
    6. }
    7.  
    8. public class Child : Base
    9. {
    10.     public override void SetupData (object data)
    11.     {    
    12.       if (data is float value)
    13.       {
    14.           Debug.Log($"Float is {value}");
    15.       }
    16.       else
    17.       {
    18.           throw new ArgumentException("Not a float");
    19.       }
    20.     }
    21. }
     
    Last edited: Jul 28, 2022
    SomehowLuke likes this.
  10. SomehowLuke

    SomehowLuke

    Joined:
    Nov 11, 2015
    Posts:
    34
    Thanks a lot for all the ideas :) I think I will do some more work on the object pool itself.

    My first thoughts were the following:

    there is an object pool that holds different kind of bullets. each bullet can have different behavior, one is following targets, another is just pushed by physics and one is doing a sine wave movement etc. so each bullet needs to have slightly different parameters for its behaviour.

    Bullet.cs is always the base of each bullet and the bullet specific behavior is in the child class.
    The Bullet.cs is based on an interface IPoolObject. I have created an interface because there is not only bullets that can be stored in the ObjectPool but also enemies for example and all other objects with that interface. So the object pool itself does not care about the final type, it cares only about the IPoolObject interface. And my idea was to add a SetupData(...) method to the interface with genereic parameters and define this in each child class, what SetupData does and needs.

    Of course the class that calls SetupData needs to know what kind of it needs but I wanted to prevent to use GetComponent<SomeClass>() during runtime because it gets called all the time the pooled object gets active and I cannot store the Component "SomeClass" in the executer class, because it does not know which object directly the object pool is taking next and which object is maybe already in use by another class.

    So I would have to create an own object pool for each type separately.
     
  11. SomehowLuke

    SomehowLuke

    Joined:
    Nov 11, 2015
    Posts:
    34
    Just a quick overview. maybe you have a hint or idea how to solve it.
    (I tried to simplify the code to the absolut basic)

    Here is my Interface IPoolObject and Bullets and Enemies have implemented this interface
    Code (CSharp):
    1.  
    2. using UnityEngine;
    3.  
    4. public interface IPoolObject
    5. {
    6.     public void InstantiateObject(Transform transformCreator);
    7.     public void SetupData(params float[] data);
    8. }
    And here I have the objectpool that gets created for each type of reference

    Code (CSharp):
    1. public class ObjectPool : MonoBehaviour
    2. {
    3.     private AssetReferenceGameObject poolObjectAddress;
    4.     private int iAmountOfObjectsInPool;
    5.  
    6.     private List<IPoolObject> listOfObjects;
    7.      
    8.     private Transform thisTransform;
    9.     private Action<ObjectPool> actionAfterCreation;
    10.     private int iCurrentSpawned;
    11.  
    12.     //#################
    13.     // CREATION
    14.     //#################
    15.  
    16.     private void Setup()
    17.     {
    18.         thisTransform = this.transform;
    19.         listOfObjects = new List<IPoolObject>();
    20.         iCurrentSpawned = 0;
    21.     }
    22.     public void Init(AssetReferenceGameObject reference, int iAmount)
    23.     {
    24.         Setup();
    25.      
    26.         poolObjectAddress = reference;
    27.         iAmountOfObjectsInPool = iAmount;
    28.      
    29.         CreatePoolObject();
    30.     }
    31.  
    32.      private void CreatePoolObject()
    33.     {
    34.         Addressables.InstantiateAsync(poolObjectAddress,
    35.                 thisTransform.position,
    36.                 thisTransform.rotation,
    37.                 thisTransform)
    38.             .Completed += PoolObjectCreated;
    39.     }
    40.  
    41.     private void PoolObjectCreated (AsyncOperationHandle<GameObject> createdPoolObject)
    42.     {
    43.         listOfObjects.Add(createdPoolObject.Result.gameObject.GetComponent<IPoolObject>());
    44.         listOfObjects[^1].SetupPoolObject(this);
    45.    
    46.         iCurrentSpawned++;
    47.         if (iCurrentSpawned < iAmountOfObjectsInPool)
    48.             CreatePoolObject();
    49.         else
    50.             actionAfterCreation(this);
    51.     }
    52.      
    53.     //#################
    54.     // USING
    55.     //#################
    56.  
    57.     public IPoolObject InstantiatePoolObject(Transform transformCreator, params float[] data)
    58.     {
    59.         if (listOfObjects.Count > 0)
    60.         {
    61.             var currentObject = listOfObjects[0];
    62.             currentObject.SetupData(data);
    63.             currentObject.InstantiateObject(transformCreator);
    64.             listOfObjects.RemoveAt(0);
    65.          
    66.             return currentObject;
    67.         }
    68.      
    69.         return null;
    70.     }
    71. }

    There is some more functions for returning an object to the pool etc. but not important for this example.
    What I have tried now from your feedback is to make the type dynamic for the object pool like this

    Code (CSharp):
    1. public class ObjectPool<T> : MonoBehaviour
    2. {
    3.     private AssetReferenceGameObject poolObjectAddress;
    4.     private int iAmountOfObjectsInPool;
    5.  
    6.     private List<T> listOfObjects;
    7.      
    8.     private Transform thisTransform;
    9.     private Action<ObjectPool<T>> actionAfterCreation;
    10.     private int iCurrentSpawned;
    11.  
    12.     //#################
    13.     // CREATION
    14.     //#################
    15.  
    16.     private void Setup()
    17.     {
    18.         thisTransform = this.transform;
    19.         listOfObjects = new List<T>();
    20.         iCurrentSpawned = 0;
    21.     }
    22.     public void Init(AssetReferenceGameObject reference, int iAmount)
    23.     {
    24.         Setup();
    25.      
    26.         poolObjectAddress = reference;
    27.         iAmountOfObjectsInPool = iAmount;
    28.      
    29.         CreatePoolObject();
    30.     }
    31.  
    32.      private void CreatePoolObject()
    33.     {
    34.         Addressables.InstantiateAsync(poolObjectAddress,
    35.                 thisTransform.position,
    36.                 thisTransform.rotation,
    37.                 thisTransform)
    38.             .Completed += PoolObjectCreated;
    39.     }
    40.  
    41.     private void PoolObjectCreated (AsyncOperationHandle<GameObject> createdPoolObject)
    42.     {
    43.         listOfObjects.Add(createdPoolObject.Result.gameObject.GetComponent<T>());
    44.         listOfObjects[^1].SetupPoolObject(this);
    45.    
    46.         iCurrentSpawned++;
    47.         if (iCurrentSpawned < iAmountOfObjectsInPool)
    48.             CreatePoolObject();
    49.         else
    50.             actionAfterCreation(this);
    51.     }
    52.      
    53.     //#################
    54.     // USING
    55.     //#################
    56.  
    57.     public IPoolObject InstantiatePoolObject(Transform transformCreator, params float[] data)
    58.     {
    59.         if (listOfObjects.Count > 0)
    60.         {
    61.             var currentObject = listOfObjects[0];
    62.             currentObject.SetupData(data);
    63.             currentObject.InstantiateObject(transformCreator);
    64.             listOfObjects.RemoveAt(0);
    65.          
    66.             return currentObject;
    67.         }
    68.      
    69.         return null;
    70.     }
    71. }
    But then I have the problem that the methods of the IPoolObject are not known anymore because Type T could be anything. How do I solve this problem? Or do I have to totally rethink the architecture?
     
  12. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,921
    I'm not sure if I follow your logic here and if a generic argument has any relevance here. However your generic argument needs a constraint to your interface. So you essentially ensure that your generic argument does implement that particular interface.

    Though be warned: You would need a seperate object pool for every type you want to use. Generics do not represent polymorphism but the opposite. You can use the same code with different types. Polymorphism is about using the same type but replace code.

    Code (CSharp):
    1. public class ObjectPool<T> : MonoBehaviour where T : IPoolObject
    Now you can use the ObjectPool class with a type that implements the IPoolObject interface. Though as I said one pool instance with a certain type can only handle this one type, nothing else.

    Maybe you're actually looking for plain polymorphism, though I don't see any place where the "dynamic" argument you originally talked about would come into play in your recent code. You currently pass a float params array to your "InstantiatePoolObject" method.

    C# is not a dynamic language. It's a strongly typed and compiled language. The special type
    dynamic
    is almost identical to the type
    object
    , just with a few additional features which are horrible slow. The dynamic type was only introduced to streamline the interface with dynamically typed scripting languages like IronPython. For more information see this documentation. In addition it's relevant to understand the difference between the normal CLR and the new DLR, why it exists and where / when to use it. In Unity you usually don't want to use it.

    As I said C# is not a dynamically typed language in the first place. Do you have one or two concrete usecases (how you actually want to call your method with which arguments) and also show how your implementation(s) of your SetupData would look like?
     
    SomehowLuke likes this.
  13. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    974
    I'm a little confused how you would use this pool.

    The code seems to just grab the next item in the pool, but you don't really know what kind of bullet that's going to be..
    Don't you want to be able to specify which type of object you need from the pool?
     
  14. SomehowLuke

    SomehowLuke

    Joined:
    Nov 11, 2015
    Posts:
    34
    Thanks a lot Bunny83 I did not know the "where" statement. That acutally helps a lot!

    Its a little hard to explain the logic :-D I mis-explained it a little. Its not ONE ObjectPool with all objects in it. There is an ObjectPoolManager that mangages all ObjectPools<T>. So I just call
    ObjectPoolManager.CreatePool (AssetReference, objectAmount)
    and the manager looks through all objects pools saved => checks if this asset reference has already been created, checks if it has enough objects created and gives back the reference to that specific object pool.

    Here a specific example:
    I have a AttackController. This AttackController has a
    [SerializeField] private AssetReferenceGameObject[] bulletAddress;

    The AttackController calls the ObjectPoolManager to create the bullets and saves the reference to it for later.
    Once the AttackController gets activated it does its thing (bulletPool.InstantiatePoolObject())

    It just spawns the bullets in a specific way (there is some more AttackControllers like AttackControllerMulitShot : AttackController which calls bulletPool.InstantiatePoolObject() several times at the same time etc.)

    The Bullet itself runs autarkic and has its own logic. As described earlier there is bullets that just move straight, some others move in a sine wave etc.)

    What I wanted to achive is, to be able to create all different kinds of Attacks with different kind of bullets. I could use the AttackControllerMultiShot with the regularBullet or the SineWaveBullet or whatever and it just works. Therefore I need a common denominator that it can work that way, but of course a RegularBullet needs different SetupData() than the SineWaveBullet. RegularBullet just needs a force vector for the direction to move the sine wave needs a amplitude and amplitude speed also.

    Thats why I was asking this question. But as I can see its not that easy to make it completely generic. with the where T : IPoolObject I can build up the object pool and it should work. I would limit the AttackController to the specific type with the SetupData of course but I just did it anyways, as you said C# is not that dynamic.

    Previously I have tried it with
    public void SetupData(params float[] data)
    {
    }

    which worked for most of the bullet at the beginning but after creating some more bullet types I needed more data than just floats (like transforms to follow an object)

    I guess I try to change the object pool to the genereic type and see how that turns out :)

    If you have any more ideas or hints, let me know :) I really appreciate your time!
    Thanks again and best,
    Luke
     
    Last edited: Jul 28, 2022
  15. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    974
    Each pool simply needs to know the type of objects within it, so define Pool as generic

    Code (csharp):
    1. public interface IPool<T> {
    2.   T GetObject();
    3.   void ReturnObject(T obj);
    4. }
    And your manager will need to track all the types of Pools

    Code (csharp):
    1. public interface IPoolManager {
    2.   void CreatePool<T>(AssetReferenceGameObject reference, int size);
    3.   IPool<T> GetPool<T>()
    4. }
    When it's all hung together, you should be able to ask your manager for a specific pool, and that specific pool for a concrete type. Then you can do whatever you like to the returned object, because it's concretely typed.

    Code (csharp):
    1. var bullet = manager.GetPool<SineBullet>().GetObject();
    2. bullet.InitializeSineWave()
    You can get fancier after that if you like. Things like having the pool automatically deactivate/activate the objects it works with, or adding an interface to the PoolManager to provide access to objects directly.
     
    SomehowLuke and Bunny83 like this.
  16. SomehowLuke

    SomehowLuke

    Joined:
    Nov 11, 2015
    Posts:
    34
    Thanks! I chose your setup and now it is more clear and works as intended :)
     
    eisenpony likes this.