Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice
  3. Join us on November 16th, 2023, between 1 pm and 9 pm CET for Ask the Experts Online on Discord and on Unity Discussions.
    Dismiss Notice
  4. Dismiss Notice

Varying parameters for the same Function / Use of params

Discussion in 'Scripting' started by Karrzun, Jan 26, 2022.

  1. Karrzun

    Karrzun

    Joined:
    Oct 26, 2017
    Posts:
    123
    Hi everyone!

    I'm currently working on a little project to generate 2D tile maps. For that, I wrote an interface IMapGenerator that demands the function
    Map GenerateMap(int width, int height);

    and several classes that implement this interface. Each class produces a different kind of map, e.g. I have one class for plain flatmaps, one for landscapes, one for taverns, one for caves, etc.

    Each kind of map is supposed to come with a set of optional steps that vary between maps. For example, landscape maps should have an optional amount of rivers (int parameter 0-5), whereas caves should optionally include rock formations like stalactites (bool parameter).

    The only thing I could come up with so far is to change the interface method to
    Map GenerateMap(int width, int height, params object[] options);

    , however I'm not particularly happy with that as it feels kind of dirty to me.

    Are there any other solutions that I'm missing without catching each case individually?


    Thank you in advance!

    Kind regards,

    Karrzun
     
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,780
    If different kinds of maps take different parameters, there's not a lot of point to extracting it into an interface at this low level.

    My favored pattern for complex setups like this is to have a MapConfigObject that contains what you need to build a particular type of map, and you create one of those, fill it out and pass it in and say "make me something like this."

    You COULD produce an "uber" map config object that has fields for every possible map your game could make, and that way you could just add that to your interface, but that's not a particularly good "code smell."

    In my Jetpack Kurt game, I flip it over and supply different heightmap providers to the same level-generation code. Not sure if this might be useful to you or not. In Jetpack Kurt, all of the heightmap-driven levels are created with a properly filled-out
    HMLConfiguration
    object:

    Code (csharp):
    1. // HML is "heightmap level"
    2.     public class HMLConfiguration
    3.     {
    4.         public    Vector3    Dimensions;
    5.  
    6.         public    IHeightmapProvider    HeightmapProvider;
    7.  
    8.         public    System.Func<Material>    GetSplatmapMaterial;
    9.         public    bool                    HasSplatMap;
    10.  
    11.         public    HeightmapMaterialScaleMode    MaterialScaleMode;
    12.  
    13.         public    GameObjectCollection TerrainFeaturesSmall;
    14.  
    15.         // invoked for each quad
    16.         public    System.Action<HMLConfiguration,Transform,Vector3> SquareCallback;
    17.  
    18.         public    int        SplatmapScale = 2;
    19. }
    And the IHeightmapProvider is:

    Code (csharp):
    1. using UnityEngine;
    2.  
    3. public interface IHeightmapProvider
    4. {
    5.     int        width { get; }
    6.     int        height { get; }
    7.  
    8.     float    GetHeightAt (int x, int y);
    9.  
    10.     // affects the "peakiness" of the terrain
    11.     float    PowerScale { get; set; }
    12.     float    ApplyFinalPowerScaling( float input);
    13. }
     
    Last edited: Jan 26, 2022
    Bunny83 likes this.
  3. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,539
    I can't really see your usage of this interface. An interface is supposed to provide a clear contract what kind of methods an object should provide without knowing what kind of object you're dealing with. This is all about abstraction. How exactly are you planing to use this interface in the end? If each map generator requires completely different and individual arguments, how would you actually handle calling this interface? A common solution is to have a third parameter of a configuration object base class that is used to provide additional information. If different generators require different set of parameters, you would create subclasses with the specific fields that a particular generator needs. Inside the method you would cast the provided base class into the expected specialized class. Of course such a case would fail if you provide the wrong config object type. Though we're back to the start which means you're actually loosing the whole idea of abstraction if you have to actually know what kind of generator is behind the interface in order to pass the right arguments.

    So to me that looks like a fundamental design flaw. Inheritance, interfaces, delegates are all tools to achieve a level of abstraction. Though abstraction only makes sense when you actually be able to treat different implementations in the same abstract way which would not be the case here.

    So you should ask yourself what is an object that implements this interface supposed to do and who and where is the information for this object provided and specified?
     
  4. Karrzun

    Karrzun

    Joined:
    Oct 26, 2017
    Posts:
    123
    Thank you both for your answers.

    That's what I did in a previous project but I ended up having like 80 fields making it quite impossible to read and debug that class.

    If I get that right, the both of you are talking about the same concept here.
    I wanted the top-level function call to be as "uniform" as possible to hook into it as easily as possible from the UI. See details down below. Do you think that's a necessary or desirable approach? Or would you just discard that and generate the aforementioned particular ConfigObject and feed that into the system?



    Maybe I can outline my concept a bit more and you'll tell me whether the interface is over-engineered or I'm just approaching it the wrong way.

    Basically, I want to navigate my menu which is a list of different kinds of maps (as mentioned above, i.e. flatmap, landscape, cave, ...).
    Step 1: I click one entry of the list and a new panel opens that displays the possible settings for this kind of map (width, height, rivers, rock formations, ...).
    Step 2: I select the desired options, I click a "Generate" button and the map is generated randomly within the given parameters.

    Instead of adding a new MonoBehaviour for every single kind of map, I wanted to have a single one that looks something like the following. The different generators are basic C# classes. Step 1 is supposed to set the correct generator calling the SetGenerator function, step 2 calls the GenerateMap function.
    Code (CSharp):
    1. using UnityEngine;
    2.  
    3.  
    4. public class MapGenerator : MonoBehaviour
    5. {
    6.     [SerializeField]
    7.     private GameObject mapGameObject;
    8.  
    9.     [SerializeField]
    10.     private GameObject tilePrefab;
    11.  
    12.     private IMapGenerator currentGenerator;
    13.  
    14.     public void SetGenerator(IMapGenerator generator) => currentGenerator = generator;
    15.  
    16.     public void GenerateMap(int width, int height, params object[] options)
    17.     {
    18.         Map map = currentGenerator.GenerateMap(width, height, options);
    19.         GenerateTiles(map);
    20.     }
    21.  
    22.     private void GenerateTiles(Map map)
    23.     {
    24.         for (int y = 0; y < map.Height; y++)
    25.         {
    26.             for (int x = 0; x < map.Width; x++)
    27.             {
    28.                 Vector3 position = new Vector3(x, y, 0);
    29.                GameObject newTile = Instantiate(tilePrefab, position, Quaternion.identity, mapGameObject.transform) as GameObject;
    30.                 newTile.name = $"Tile_{x}_{y}";
    31.             }
    32.         }
    33.     }
    34.  
    35. }