Search Unity

What is the best way to specify a parent of prefab to instantiate?

Discussion in 'Scripting' started by daretsuki, Jan 18, 2022.

  1. daretsuki

    daretsuki

    Joined:
    Mar 22, 2018
    Posts:
    6
    I work in Unity on some, let's say, personal projects.

    I want to create a reusable, easy to use script for a designer which only things it allows is to instantiate a prefab under another game object in a scene hierarchy.

    What's the best way to specify a parent?

    Since the script is going to be attached to premade prefabs, we can't specify a direct link to a parent in a scene, also they don't yet exist, but will in the future, so I can't use this script:

    Code (CSharp):
    1.  
    2. public class SpawnerByParentLink : MonoBehaviour
    3. {
    4.    [SerializeField] private Transform _parent; // direct link set from inspector
    5.    [SerializeField] private Transform _prefab;
    6.  
    7.    public Transform Spawn()
    8.    {
    9.        return Instantiate(_prefab, _parent);
    10.    }
    11. }
    I could allow a user to specify a name of a parent directly and search for it in a scene hierarchy, but then we go into trouble when renaming things, it won't update automatically. I could somehow allow a user to choose from dropdown with any existing names of prefabs and theyre children in the project, so it will automatically updates when changed, but seems so complex for this problem. Also GameObject.Find is slow, and they say you never should use it! I could somehow create some global dictionary to store all names and game object, so search becomes really fast, but it still seems to me like to much hassle, isn't there better in-build solution already? I usually would not care about performance when a project starts, so I would use finds everywhere, then maybe refactor.

    Premature optimization is the root of all evil? Am I blindly quoting someone?

    Code (CSharp):
    1. public class SpawnerByParentName : MonoBehaviour
    2. {
    3.    [SerializeField] private string _parentName;
    4.    [SerializeField] private Transform _prefab;
    5.  
    6.    public Transform Spawn()
    7.    {
    8.        var parent = GameObject.Find(_parentName).transform;
    9.        return Instantiate(_prefab, parent);
    10.    }
    11. }
    I could allow a user to specify a tag of a parent. Searching by tag is bit faster, I've seen some articles... Also I can have a dropdown with existing autofilling tags in the inspector to choose from (TagSelector attribute). But then for any game object I want to use as a parent of a prefab parent I have to specify tags which seems strange, then if for other objects I don't do it I feel a lack of consistency in the project, I don't know...

    Code (CSharp):
    1. public class SpawnerByParentTag : MonoBehaviour
    2. {
    3.    [TagSelector] [SerializeField] private string _parentTag;
    4.    [SerializeField] private Transform _prefab;
    5.  
    6.    public Transform Spawn()
    7.    {
    8.        var parent = GameObject.FindWithTag(_parentTag).transform;
    9.        return Instantiate(_prefab, parent);
    10.    }
    11.  
    12. }
    Of course there are always trade-offs. I can make dumb and simple things, but quick and working now, although it can be harder to scale later (maybe not) when project is ultra huge, or I can make smart, complex subsystems now which give excellent performance and maintainability, but you can spend days, weeks working on them, instead of developing real gameplay features.

    And now how to explain my concerns on interview if you want to apply for a job?

    Maybe I shouldn't care so much especially when a project starts small for now, since I can improve things later when needed.

    My other thoughts are to search for existing plugins in assetstore or just google to make life easier, but with no success for now.

    Anyway, anything I do I feel stupid and that I am struggling with problems that big game studios solved long ago.

    How would you do it?
     
  2. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,909
    Since you're aware of how to pass a Transform reference to the Instantiate method, your question basically boils down to "What is the best way to get a reference to a Transform?"

    There is no one-size-fits-all "best" way to get a reference to anything in Unity. It depends on the situation. It depends on which object exists where and when. Typically the simplest, fastest way is some combination of setting references directly in the inspector, and saving references to objects as they are instantiated for later use in other code. But as you noted, there are many options. The answer comes down to how the objects in question are brought into being.
     
    Kurt-Dekker likes this.
  3. daretsuki

    daretsuki

    Joined:
    Mar 22, 2018
    Posts:
    6
    That's correct - abstracting the problem more I really need a reference to a Transform, but I can't see a convienient way to do this when it's in another prefab as a child. So I can look for it by name, tag only for now.

    You just made me to think, that maybe I could somehow drag a child of a prefab to another prefab as link (I had to lock inspectors for that), but that's not possible, I'll try more. It could be nice since I could take a name from it and I don't know, search for it in hierarchy. Still, there might be few objects of same names spawned with (Clone) suffix and so on and how to choose which one is which, that's hard man. Or, I just have to make sure there only one object with such a name in the scene, otherwise an exception should occur.

    Or maybe I'm just doing it wrong. Maybe you shouldn't ever spawn object like that anywhere you want. Maybe as a good practice you should spawn a prefab using direct reference only under current prefab's already existing child (where the spawner script is attached).
     
    Last edited: Jan 18, 2022
  4. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,909
    There's no good reason to directly reference a child object of another prefab like that. Make a script on that prefab that references its own child, then delegate the work of spawning the thing to that script or borrow the reference from that script.

    Much of good programming is abstracting things away so that no single piece of code needs in depth knowledge of the workings of some other piece.
     
  5. AnimalMan

    AnimalMan

    Joined:
    Apr 1, 2018
    Posts:
    1,164
    So if you don’t know who the parent is going to be; which object I mean. It’s sort of a variable that the player decides. I suggest putting all viable would be parents into a list. So you are not searching the hierarchy or using find or tag, you just already have them in a nice little list. You just iterate through that list, or like you say throw it into a drop down.

    hey I’m no optimal coding expert, but I haven’t used game object find, or game object find with tag since my very first projects quite a number of years ago.

    so I don’t know how you want to do it, but when those parents objects exist since they are integral to your prefabs, upon instantiation of the prefab why not throw that prefabs child into a list that is attached to a script on your main camera.

    so maybe you can be like hey Camera.main get component ListScript and throw getchild 3 into it whenever prefab comes into existence (assuming the parent of the next prefab is the child 3 of the last prefab)
     
  6. daretsuki

    daretsuki

    Joined:
    Mar 22, 2018
    Posts:
    6
    Thanks for replies.
    Actually I generalized my question, so I can find a solution for my original and similar situations later.

    But, the original problem is that I have a button nested in a prefab, when I click it I want to spawn new UI Panel under Canvas - it's currently placed in hierarchy, but it could be also a prefab, it should not matter, it just have to be spawned in the scene when I want to use it as a parent.

    And sure, I just could write a line of code searching for it, but I want to keep it editable from inspector as much as possible, this is why I'm asking.

    @PraetorBlue Originally I did it, I just get the Canvas object from my dependency injection container coding, so it's fast. And then there is a custom script on canvas object with a method to instantiate the desired prefab. I just thought, could be there a better way to do this? Maybe I should stick to this rule of being responsible for own children only, but just be able do all stuff from inspector.

    Then button to click is also spawned dynamically, and attaching handlers to it should be done smart, I don't want to hardcode as much specific things, so my scripts are reusable. Going forward, the handler to the button should be a method on a custom canvas script, or even better a spawner component (I mentioned in first post) to spawn desired object. I'll have to think more about it. I'll get back when I figure myself out.

    @AnimalMan It's a lot do to, I would have to add spawned objects to a list every time, I wanted to avoid it. Although I'll think about it.
     
  7. AnimalMan

    AnimalMan

    Joined:
    Apr 1, 2018
    Posts:
    1,164
    Did you try storing the parent object inside the prefab deactivated, to detach and reparent when needed? This will be the only other suggestion I can make.

    GL
     
  8. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,531
    So I'm trying to figure out what you're trying to do here...

    You have a prefab, lets call it "Prefab A", and in that prefab you want to have a script that instantiates another prefab, "Prefab B", as child of a Transform that is outside of the scope of the "Prefab A".

    And the problem you're running into is that since "Prefab A" can only reference either objects in itself, or assets (which aren't themselves instantiated), you need a way to reach out into the scene and "find" the Transform that "Prefab A" will instantiate "Prefab B" in.

    ...

    The problem isn't just the fact you can't reference those objects.

    It's why to do with why you can't.

    How do we know that object exists?

    How do we know 1 or Many of those objects exist?

    ...

    Now personally how I deal with such a situation is that a prefab doesn't do this job!

    So I'm trying to think of a situations that this might occur...

    1) We are in a "item selection screen", there is a prefab that we create multiple instances of for every "item" that can be viewed. When the button is clicked a separate container has some visual avatar of that item displayed.

    e.g. think Skyrim's inventory, as you highlight each item, the right side of the screen displays the visuals for the item.


    Thing is... if I was doing this. The button doesn't ACTUALLY know what it has to spawn.

    Rather instead I'd use a factory pattern... the "inventory screen" has a factory on it that receives a prefab for the buttons that it will stamp out in the UI for each item. That prefab will contain a script that consumes what it needs to display. Something like this:

    Code (csharp):
    1. public class InventoryLineItem : MonoBehaviour
    2. {
    3.     public event System.EventHandler Clicked;
    4.     public Text TitleDisplay;
    5.     public Text WeightDisplay;
    6.     public Text ValueDisplay;
    7.  
    8.     public InventoryInfo Item { get; private set; }
    9.  
    10.     public void Stamp(InventoryInfo item)
    11.     {
    12.         this.Item = item;
    13.         TitleDisplay.text = item.Title;
    14.         WeightDisplay.text = item.Weight;
    15.         ValueDisplay.text = item.Value;
    16.     }
    17.  
    18.     //this could be called via a UnityEvent
    19.     public void SignalClicked() { Clicked?.Invoke(this, System.EventArgs.Empty); }
    20. }
    (note - this is a particularly adhoc approach... I actually would generalize this more to make it modular. This was literally what I was just doing over the weekend... this is what that looks like this: )

    The stamper:
    Code (csharp):
    1. using UnityEngine;
    2. using System.Collections.Generic;
    3.  
    4. using com.spacepuppy;
    5. using com.spacepuppy.Events;
    6. using com.spacepuppy.Utils;
    7.  
    8. using com.vivarium.Factories;
    9.  
    10. namespace com.vivarium.UI.Stampers
    11. {
    12.  
    13.     public class AssetStamper : MonoBehaviour, IStamper
    14.     {
    15.  
    16. #if UNITY_EDITOR
    17.         public const string PROP_SOURCETYPE = nameof(_sourceType);
    18.         public const string PROP_TARGETS = nameof(_targets);
    19.         public const string PROP_ONSTAMPEDEVENT = nameof(_onStampedByIndex);
    20. #endif
    21.  
    22.  
    23.         #region Fields
    24.  
    25.         [SerializeField]
    26.         private TypeReference _sourceType;
    27.  
    28.         [SerializeField]
    29.         [ReorderableArray]
    30.         private List<StampedTarget> _targets = new List<StampedTarget>();
    31.  
    32.         [SerializeField]
    33.         [DisplayName("On Stamped (Triggered by index modulo length)")]
    34.         private SPEvent _onStampedByIndex = new SPEvent();
    35.  
    36.         #endregion
    37.  
    38.         #region Properties
    39.  
    40.         public List<StampedTarget> Targets => _targets;
    41.  
    42.         #endregion
    43.  
    44.         #region IStamper Interface
    45.  
    46.         private void SignalEvent(object source, int index)
    47.         {
    48.             if (_onStampedByIndex.HasReceivers)
    49.             {
    50.                 int i = _onStampedByIndex.TargetCount > 0 ? (index % _onStampedByIndex.TargetCount) : 0;
    51.                 _onStampedByIndex.ActivateTriggerAt(i, this, source);
    52.             }
    53.         }
    54.  
    55.         public void Stamp(object source, int index)
    56.         {
    57.             foreach(var targ in _targets)
    58.             {
    59.                 targ.Stamp(source);
    60.             }
    61.             this.SignalEvent(source, index);
    62.         }
    63.  
    64.         public void Stamp(IAssetTemplate source, int index)
    65.         {
    66.             foreach (var targ in _targets)
    67.             {
    68.                 targ.Stamp(source);
    69.             }
    70.             this.SignalEvent(source, index);
    71.         }
    72.  
    73.         public void Stamp(IUserItem source, int index)
    74.         {
    75.             foreach (var targ in _targets)
    76.             {
    77.                 targ.Stamp(source);
    78.             }
    79.             this.SignalEvent(source, index);
    80.         }
    81.  
    82.         public void Stamp(IUserItemTemplate source, int index)
    83.         {
    84.             foreach (var targ in _targets)
    85.             {
    86.                 targ.Stamp(source);
    87.             }
    88.             this.SignalEvent(source, index);
    89.         }
    90.  
    91.         public void Stamp(IAssignment source, int index)
    92.         {
    93.             foreach (var targ in _targets)
    94.             {
    95.                 targ.Stamp(source);
    96.             }
    97.             this.SignalEvent(source, index);
    98.         }
    99.  
    100.         public void Stamp(IAssignmentTemplate source, int index)
    101.         {
    102.             foreach (var targ in _targets)
    103.             {
    104.                 targ.Stamp(source);
    105.             }
    106.             this.SignalEvent(source, index);
    107.         }
    108.  
    109.         #endregion
    110.  
    111.     }
    112.  
    113. }
    The way we target a specific field:
    Code (csharp):
    1. using UnityEngine;
    2. using UnityEngine.AddressableAssets;
    3. using System.Collections.Generic;
    4.  
    5. using com.spacepuppy;
    6. using com.spacepuppy.Dynamic;
    7. using com.spacepuppy.UI;
    8. using com.spacepuppy.Utils;
    9.  
    10. using com.vivarium.Factories;
    11. using Cysharp.Threading.Tasks;
    12.  
    13. namespace com.vivarium.UI.Stampers
    14. {
    15.  
    16.     [System.Serializable]
    17.     public struct StampedTarget
    18.     {
    19.  
    20.         #region Fields
    21.  
    22.         private static readonly System.Type[] _supportedTypes = new System.Type[] { typeof(TMPro.TMP_Text), typeof(UnityEngine.UI.Text), typeof(UnityEngine.UI.Image) };
    23.  
    24.         [SerializeField]
    25.         private string _memberName;
    26.  
    27.         [SerializeReference]
    28.         [TypeRestriction(typeof(TMPro.TMP_Text), typeof(UnityEngine.UI.Text), typeof(UnityEngine.UI.Image), AllowProxy = false, HideTypeDropDown = false)]
    29.         private UnityEngine.Component _target;
    30.  
    31.         [SerializeField]
    32.         [Tooltip("A format string applied to the member stamped with in the standard C# format 'Some text around the property {0:0.00} formatted as a number with digits'.\r\n\r\nSprites ignore this.")]
    33.         private string _formatting;
    34.  
    35.         #endregion
    36.  
    37.         #region CONSTRUCTOR
    38.  
    39.         public StampedTarget(Component target, string memberName, string formatting = null)
    40.         {
    41.             _memberName = memberName;
    42.             _target = ObjUtil.GetAsFromSource(_supportedTypes, target) as Component;
    43.             _formatting = formatting ?? string.Empty;
    44.         }
    45.  
    46.         #endregion
    47.  
    48.         #region Properties
    49.  
    50.         public string MemberName
    51.         {
    52.             get => _memberName;
    53.             set => _memberName = value;
    54.         }
    55.  
    56.         public UnityEngine.Component Target
    57.         {
    58.             get => _target;
    59.             set => _target = ObjUtil.GetAsFromSource(_supportedTypes, value) as Component;
    60.         }
    61.  
    62.         public string Formatting
    63.         {
    64.             get => _formatting;
    65.             set => _formatting = value;
    66.         }
    67.  
    68.         public string text
    69.         {
    70.             get => TextConfigurationDecorator.TryGetText(_target);
    71.             set => TextConfigurationDecorator.TrySetText(_target, value);
    72.         }
    73.  
    74.         public Sprite sprite
    75.         {
    76.             get => (_target is UnityEngine.UI.Image img && img) ? img.sprite : null;
    77.             set {
    78.                 if (_target is UnityEngine.UI.Image img && img) img.sprite = value;
    79.             }
    80.         }
    81.  
    82.         #endregion
    83.  
    84.         #region SetValue Methods
    85.  
    86.         public void SetValue(string sval)
    87.         {
    88.             this.text = !string.IsNullOrEmpty(_formatting) ? string.Format(_formatting, sval) : sval;
    89.         }
    90.  
    91.         public void SetValue(decimal? dval)
    92.         {
    93.             this.text = !string.IsNullOrEmpty(_formatting) ? string.Format(_formatting, dval) : (dval != null ? dval.Value.ToString("0") : string.Empty);
    94.         }
    95.  
    96.         public void SetValue(double? dval)
    97.         {
    98.             this.text = !string.IsNullOrEmpty(_formatting) ? string.Format(_formatting, dval) : (dval != null ? dval.Value.ToString("0") : string.Empty);
    99.         }
    100.  
    101.         public void SetValue(int? ival)
    102.         {
    103.             this.text = !string.IsNullOrEmpty(_formatting) ? string.Format(_formatting, ival) : (ival != null ? ival.Value.ToString("0") : string.Empty);
    104.         }
    105.  
    106.         public void SetValue(System.Guid? guid)
    107.         {
    108.             this.text = !string.IsNullOrEmpty(_formatting) ? string.Format(_formatting, guid) : (guid != null ? guid.Value.ToString() : string.Empty);
    109.         }
    110.  
    111.         public void SetValue(System.DateTime? dt)
    112.         {
    113.             this.text = !string.IsNullOrEmpty(_formatting) ? string.Format(_formatting, dt) : (dt != null ? dt.Value.ToString() : string.Empty);
    114.         }
    115.  
    116.         public void SetValue(Sprite val)
    117.         {
    118.             if(_target is UnityEngine.UI.Image img)
    119.             {
    120.                 img.sprite = val;
    121.             }
    122.             else
    123.             {
    124.                 this.text = !string.IsNullOrEmpty(_formatting) ? string.Format(_formatting, val) : val?.ToString() ?? string.Empty;
    125.             }
    126.         }
    127.  
    128.         public async UniTask SetValue(AssetReferenceSprite asset)
    129.         {
    130.             this.SetValue(await asset.LoadAssetAsync());
    131.         }
    132.  
    133.         public async UniTask SetValue(AssetReference asset)
    134.         {
    135.             this.SetValue(await asset.LoadAssetAsync<UnityEngine.Object>());
    136.         }
    137.  
    138.         public void SetValue(object val)
    139.         {
    140.             if (val is Sprite spr)
    141.             {
    142.                 this.SetValue(spr);
    143.             }
    144.             else if (val is AssetReferenceSprite aspr)
    145.             {
    146.                 _ = this.SetValue(aspr);
    147.             }
    148.             else if(val is AssetReference asset)
    149.             {
    150.                 _ = this.SetValue(asset);
    151.             }
    152.             else
    153.             {
    154.                 this.text = !string.IsNullOrEmpty(_formatting) ? string.Format(_formatting, val) : val?.ToString() ?? string.Empty;
    155.             }
    156.         }
    157.  
    158.         #endregion
    159.  
    160.         #region Stamp Methods
    161.  
    162.         public void Stamp(object source)
    163.         {
    164.             switch (source)
    165.             {
    166.                 case IUserItem item:
    167.                     this.Stamp(item);
    168.                     break;
    169.                 case IUserItemTemplate item:
    170.                     this.Stamp(item);
    171.                     break;
    172.                 case IAssignment assign:
    173.                     this.Stamp(assign);
    174.                     break;
    175.                 case IAssignmentTemplate assign:
    176.                     this.Stamp(assign);
    177.                     break;
    178.                 default:
    179.                     this.StampReflectively(source);
    180.                     break;
    181.             }
    182.         }
    183.  
    184.         public void Stamp(IAssetTemplate source)
    185.         {
    186.             switch (_memberName)
    187.             {
    188.                 case nameof(IAssetTemplate.AssetGuid):
    189.                     this.SetValue(source?.AssetGuid);
    190.                     break;
    191.                 case nameof(IAssetTemplate.DisplayName):
    192.                     this.SetValue(source?.DisplayName);
    193.                     break;
    194.                 case nameof(IAssetTemplate.Description):
    195.                     this.SetValue(source?.Description);
    196.                     break;
    197.                 case nameof(IAssetTemplate.Icon):
    198.                     _ = this.SetValue(source?.Icon);
    199.                     break;
    200.                 default:
    201.                     switch (source)
    202.                     {
    203.                         case IUserItem item:
    204.                             this.Stamp(item);
    205.                             break;
    206.                         case IUserItemTemplate item:
    207.                             this.Stamp(item);
    208.                             break;
    209.                         case IAssignment assign:
    210.                             this.Stamp(assign);
    211.                             break;
    212.                         case IAssignmentTemplate assign:
    213.                             this.Stamp(assign);
    214.                             break;
    215.                         default:
    216.                             this.StampReflectively(source);
    217.                             break;
    218.                     }
    219.                     break;
    220.             }
    221.         }
    222.  
    223.         public void Stamp(IUserItem source)
    224.         {
    225.             switch (_memberName)
    226.             {
    227.                 case nameof(IUserItemTemplate.AssetGuid):
    228.                     this.SetValue(source?.AssetGuid);
    229.                     break;
    230.                 case nameof(IUserItemTemplate.DisplayName):
    231.                     this.SetValue(source?.Template?.DisplayName);
    232.                     break;
    233.                 case nameof(IUserItemTemplate.Description):
    234.                     this.SetValue(source?.Template?.Description);
    235.                     break;
    236.                 case nameof(IUserItemTemplate.Icon):
    237.                     _ = this.SetValue(source?.Template?.Icon);
    238.                     break;
    239.                 case nameof(IUserItemTemplate.Id):
    240.                     this.SetValue(source?.Template?.Id);
    241.                     break;
    242.                 case nameof(IUserItemTemplate.Price):
    243.                     this.SetValue(source?.Template?.Price);
    244.                     break;
    245.                 case nameof(IUserItemTemplate.Prefab):
    246.                     _ = this.SetValue(source?.Template?.Prefab);
    247.                     break;
    248.                 case nameof(IUserItem.UnlockDate):
    249.                     this.SetValue(source?.UnlockDate);
    250.                     break;
    251.                 default:
    252.                     this.StampReflectively(source);
    253.                     break;
    254.             }
    255.         }
    256.  
    257.         public void Stamp(IUserItemTemplate source)
    258.         {
    259.             switch (_memberName)
    260.             {
    261.                 case nameof(IUserItemTemplate.AssetGuid):
    262.                     this.SetValue(source?.AssetGuid);
    263.                     break;
    264.                 case nameof(IUserItemTemplate.DisplayName):
    265.                     this.SetValue(source?.DisplayName);
    266.                     break;
    267.                 case nameof(IUserItemTemplate.Description):
    268.                     this.SetValue(source?.Description);
    269.                     break;
    270.                 case nameof(IUserItemTemplate.Icon):
    271.                     _ = this.SetValue(source?.Icon);
    272.                     break;
    273.                 case nameof(IUserItemTemplate.Id):
    274.                     this.SetValue(source?.Id);
    275.                     break;
    276.                 case nameof(IUserItemTemplate.Price):
    277.                     this.SetValue(source?.Price);
    278.                     break;
    279.                 case nameof(IUserItemTemplate.Prefab):
    280.                     _ = this.SetValue(source?.Prefab);
    281.                     break;
    282.                 default:
    283.                     this.StampReflectively(source);
    284.                     break;
    285.             }
    286.         }
    287.  
    288.         public void Stamp(IAssignment source)
    289.         {
    290.             switch (_memberName)
    291.             {
    292.                 case nameof(IAssignmentTemplate.AssetGuid):
    293.                     this.SetValue(source?.AssetGuid);
    294.                     break;
    295.                 case nameof(IAssignmentTemplate.DisplayName):
    296.                     this.SetValue(source?.Template?.DisplayName);
    297.                     break;
    298.                 case nameof(IAssignmentTemplate.Description):
    299.                     this.SetValue(source?.Template?.Description);
    300.                     break;
    301.                 case nameof(IAssignmentTemplate.Icon):
    302.                     _ = this.SetValue(source?.Template?.Icon);
    303.                     break;
    304.                 case nameof(IAssignmentTemplate.Tag):
    305.                     this.SetValue(source?.Template?.Tag);
    306.                     break;
    307.                 case nameof(IAssignmentTemplate.StamperPrefab):
    308.                     _ = this.SetValue(source?.Template?.StamperPrefab);
    309.                     break;
    310.                 case nameof(IAssignmentTemplate.Reward):
    311.                     _ = this.SetValue(source?.Template?.Reward);
    312.                     break;
    313.                 case nameof(IAssignment.Status):
    314.                     this.SetValue(source?.Status);
    315.                     break;
    316.                 default:
    317.                     this.StampReflectively(source);
    318.                     break;
    319.             }
    320.         }
    321.  
    322.         public void Stamp(IAssignmentTemplate source)
    323.         {
    324.             switch (_memberName)
    325.             {
    326.                 case nameof(IAssignmentTemplate.AssetGuid):
    327.                     this.SetValue(source?.AssetGuid);
    328.                     break;
    329.                 case nameof(IAssignmentTemplate.DisplayName):
    330.                     this.SetValue(source?.DisplayName);
    331.                     break;
    332.                 case nameof(IAssignmentTemplate.Description):
    333.                     this.SetValue(source?.Description);
    334.                     break;
    335.                 case nameof(IAssignmentTemplate.Icon):
    336.                     _ = this.SetValue(source?.Icon);
    337.                     break;
    338.                 case nameof(IAssignmentTemplate.Tag):
    339.                     this.SetValue(source?.Tag);
    340.                     break;
    341.                 case nameof(IAssignmentTemplate.StamperPrefab):
    342.                     _ = this.SetValue(source?.StamperPrefab);
    343.                     break;
    344.                 case nameof(IAssignmentTemplate.Reward):
    345.                     _ = this.SetValue(source?.Reward);
    346.                     break;
    347.                 default:
    348.                     this.StampReflectively(source);
    349.                     break;
    350.             }
    351.         }
    352.  
    353.         private void StampReflectively(object source)
    354.         {
    355.             if (source == null) this.SetValue((object)null);
    356.  
    357.             int attempts = 0;
    358.  
    359.         Retry:
    360.             var member = DynamicUtil.GetValueSetterMemberFromType(source.GetType(), _memberName, null, false);
    361.             if (member != null)
    362.             {
    363.                 this.SetValue(DynamicUtil.GetValueDirect(source, member));
    364.             }
    365.             else if(source is IAssetState state && state.Template != null)
    366.             {
    367.                 member = DynamicUtil.GetValueSetterMemberFromType(state.Template.GetType(), _memberName, null, false);
    368.                 if (member != null)
    369.                 {
    370.                     this.SetValue(DynamicUtil.GetValueDirect(state.Template, member));
    371.                 }
    372.             }
    373.             else if(source is IAssetSource s && !object.ReferenceEquals(s.AssetState, source))
    374.             {
    375.                 attempts++;
    376.                 if (attempts > 4) return; //we only travel through the hierarchy 4 times... longer than that and we assume it's circular
    377.  
    378.                 source = s.AssetState;
    379.                 goto Retry;
    380.             }
    381.         }
    382.  
    383.         #endregion
    384.  
    385.     }
    386.  
    387. }
    Note that the switch statements are to just avoid reflecting commonly used fields for commonly used types. They're just optimizations... but in general the stamper doesn't actually care what it receives as a source.

    How this ends up looking in the inspector:
    upload_2022-1-18_1-31-40.png

    Now my inventory ui script would create these on the fly. Something like this:
    Code (csharp):
    1. public class InventoryUIController
    2. {
    3.     public InventoryLineItem ButtonPrefab;
    4.     public Transform LineItemContainer; //this should be some layout controlled container, think VerticalLayoutGroup
    5.     public Transform AvatarContainer;
    6.  
    7.     public void ShowItems()
    8.     {
    9.         foreach(Transform child in LineItemContainer)
    10.         {
    11.             Destory(child.gameObject); //clear out container
    12.         }
    13.  
    14.         InventoryLineItem first = null;
    15.         var inv = Services.Get<InventoryService>();
    16.         foreach(InventoryInfo item in inv.UserItems)
    17.         {
    18.             var btn = Instantiate(ButtonPrefab, LineItemContainer);
    19.             btn.Stamp(item);
    20.             btn.Clicked += (s,e) => { ShowItem((InventoryLineItem)s.Item); };
    21.             if (first == null) first = btn;
    22.         }
    23.      
    24.         if(first) ShowItem(first.Item);
    25.     }
    26.  
    27.     public void ShowItem(InventoryLineItem item)
    28.     {
    29.         foreach(Transform child in AvatarContainer)
    30.         {
    31.             Destory(child.gameObject); //clear out container
    32.         }
    33.  
    34.         Instantiate(item.Avatar, AvatarContainer);
    35.     }
    36. }
    Basiaclly this controller facilitates the joining of the 2 containers relationship.

    The containers, and the prefabs, don't actually care where they're spawned or whose doing the spawning. That's all managed by the factory that does that. The prefabs and containers act merely as templates.

    If it's imparative that the button did more, well, this "Stamp" method can consume anything it needs. This is what's called "dependency injection"... don't get fooled by the crazy DI frameworks that exist out there. A dependency injection is merely the concept of an object receiving its dependencies when created.

    So technically speaking you could have passed the "AvatarContainer" into the "Stamp" method and it did take care of that itself.

    I went with an event approach though because I only like giving an object the dependencies it truly needs. Making it handle the display implies that it MUST show avatars. But what if I wanted to reuse the InventoryLineItem somewhere that the avatar doesn't get displayed? Instead on click it "uses" the time. But keeping it even based, the factory gets to decide what happens.

    That's what makes it a factory. The factory puts the parts together based on some configuration. And that include what it's used for. The button is a button... may that button do X, Y, Z is up to the factory. Just like a button in a car is just a button, it's up to the factory worker to wire up what the button does... the button doesn't do that. The button just has an actuator that signals it was pushed... the wire harness output of the button actually gets connected to what it does (the Clicked event) by the factory worker.

    ....


    Of course, I may not know what your actual target goal is.

    Could you explain what it is you're actually expecting to do? Not the "code you want to write", but the mechnical goal you expect to accomplish/generalize.
     
    Last edited: Jan 18, 2022
  9. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,329
    It is possible to serialize cross-prefab / scene references and then resolve them at runtime. Creating a user-friendly custom property drawer for these references would require some work though.

    But the basic concept would be something like this:

    Code (CSharp):
    1. public class SpawnerByParentLink : MonoBehaviour
    2. {
    3.    [SerializeField] private Reference _parent;
    4.    [SerializeField] private Transform _prefab;
    5.  
    6.    public Transform Spawn(Transform parent)
    7.    {
    8.        return Instantiate(_prefab, _parent.Get<Transform>());
    9.    }
    10. }
    11.  
    12. public class Referenceable : MonoBehaviour, ISerializationCallbackReceiver
    13. {
    14.     private static Dictionary<string, Object> references = new Dictionary<string, Object>();
    15.  
    16.     [SerializeField]
    17.     private string guid;
    18.  
    19.     [SerializeField]
    20.     private Object reference;
    21.  
    22.     private void OnEnable() => references[guid] = reference;
    23.  
    24.     public static T Get<T>(string guid) where T : Object
    25.     {
    26.         return references.TryGet(guid, out T result) ? result : null;
    27.     }
    28.  
    29.     #if UNITY_EDITOR
    30.     public static string GetGuid(Object reference)
    31.     {
    32.         return GlobalObjectId.GetGlobalObjectIdSlow(reference);
    33.     }
    34.     #endif
    35.  
    36.     public void OnBeforeSerialize()
    37.     {
    38.         #if UNITY_EDITOR
    39.         guid = reference == null ? "" : GetGuid(reference);
    40.         #endif
    41.     }
    42.  
    43.     public void OnAfterDeserialize() { }
    44. }
    45.  
    46. [Serializable]
    47. public class Reference
    48. {
    49.     [SerializeField] private string guid;
    50.  
    51.     [SerializeField] private Object reference;
    52.  
    53.     public T Get<T>() where T : Object
    54.     {
    55.         if(reference == null)
    56.         {
    57.             reference = Referenceable.Get<T>(guid);
    58.         }
    59.  
    60.         return reference as T;
    61.     }
    62.  
    63.     #if UNITY_EDITOR
    64.     private void OnValidate()
    65.     {
    66.         if(reference != null)
    67.         {
    68.             guid = Referenceable.GetGuid(reference);
    69.         }
    70.     }
    71.     #endif
    72. }
    There could be some existing solutions out there that make this easy to do.
     
    Last edited: Jan 18, 2022
  10. daretsuki

    daretsuki

    Joined:
    Mar 22, 2018
    Posts:
    6
    My question branched out to another problem related to code design (how to call it?), I may create another thread related to that soon.

    Original problem of specifying a parent transform, when it does not yet exist, but we expect it to be there when we want to spawn a prefab under it (in any way useful, by name, reference, whatever) just may not be applicable for every situation.

    @lordofduct In your case and it is that buttons are created dynamically, same for attaching click handlers, even displaying text - so I can see some other problems may arise.
    We can attach the spawner script directly in button prefab, so when clicked you want to spawn a different inventory item in neighbour ui panel, update some texts and a lot more, it's matter of identifying a content to display.
    It is matter of asking is it a good design approach to this problem for a long run. In your case you delegate a responsibility to parent - I guess it could be the prefab root game object which contains a script(s) responsible for instantiating, presenting everything, certianly communicating with some external systems.

    But still , I believe the solution I propose might make sense in some simple situations, maybe prototyping ui or gameplay quickly, when we really just about instantiating something and forget. (If I click button, then spawn this ui panel and I don't care (as a button) what happens next to it, good bye). I can see it might cause some trouble, where do we store spawned instance reference so we can close it later. As I said that's different story I have to figure out.

    @SisusCo This is a kind of solution. But yeah.. It requires some work and it seems like scripts (Referenceable) you need to attach to everything you want to reference? Am I correct? I would like to keep it to max one attachable script (Spawner) + maybe some other mechanism to identify/search the object. I wonder is there a way to somehow recognize that new object/prefab has just been created IN EDITOR and and store the information (names?) then have a dropdown in Spawner and choose a name, still some work to do anyway
     
  11. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,329
    Yes this requires attaching a Component to the GameObject to hold the serialized reference to the target component and pair it with a guid. I don't think there's any way around that unfortunately.

    It however would theoretically be possible to make this process invisible to the designer with some clever editor coding.

    The Referenceable component could be attached to the GameObject automatically when the designer drags one to the Reference field, so in practice it feels basically indistinguishable from a normal Object field (except now it just magically supports references across scenes and prefabs).

    The Referenceable component could also he hidden using HideFlags.HideInInspector and could maybe be visualized instead similarly to how Addressables are, with a compact field below the GameObject header.

    I've heard about some game devs using a system like this to great effect, but I haven't personally used one in any bigger project (beyond some small proof-of-concept experiments).


    If you prefer a dropdown over an Object reference field, it would probably also be possible to pull of with something similar to the Referenceable components.

    As long as you can somehow reliably detect when these Components are added (e.g. the Reset event) and destroyed (maybe possible with OnDestroy and the ExecuteAlways attribute?) you would be able to create a ScriptableObject asset which holds a list of all Referenceables that exist in all scenes and prefabs across the project and then populate a dropdown based on this.

    Alternatively adding and removing referenceables could be done manually through the ScriptableObject's Inspector or a custom EditorWindow (it could work similarly to the Addressables Group window).
     
  12. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,531
    The open and close of my post though is

    "what are you trying to do?"

    What are you proposing... I'm having a hard time figuring out what it is you want to accomplish. Where would you use this? Why is it a prefab if it's hard coded what it spawns? What's going on?

    I gave an example of a problem I have, and how I resolved it.

    You're giving the "how I want to resolve it", but no problem has been explained.

    This is why I ended my post with:
    Cause to me, what you've described sounds like a backwards way of designing your project.

    But maybe there's a use case I can't imagine that has this use.

    ...

    But as you can tell... the approach is not going to be straight forward. Because as I stated:

    Say the "id" you reference by... name, int, guid, tag, whatever... what if 2 of them exist? What if 20 of them exist? What if it doesn't exist?

    I mean sure... you can loosey goose it and just assume only 1 will exist. In which case... go with any of the ideas you mentioned. GameObject.Find, Find by tag, have a multiton that serves out the objects that you can search by some id:

    Code (csharp):
    1. public class ParentReferenceToken : MonoBehaviour
    2. {
    3.     public static readonly HashSet<ParentReferenceToken> AvailableTokens = new HashSet<ParentReferenceToken>();
    4.  
    5.     void Awake() { AvailableTokens.Add(this); }
    6.     void OnDestroy() { AvailableTokens.Remove(this); }
    7.  
    8.     public string Id;
    9. }
    10.  
    11. public class ParentRefFinder : MonoBehaviour
    12. {
    13.     public GameObject Prefab;
    14.     public string Id;
    15.  
    16.     public void Spawn()
    17.     {
    18.         var t = ParentReferenceToken.AvailableToken.FirstOrDefault(o => o.Id == this.Id);
    19.         if (t) Instantiate(Prefab, t.transform);
    20.     }
    21. }
    (note - this could be a Dictionary instead of a HashSet, but that goes back to the what if more than 1 with the same Id exists??? Going to need more validation code added then to keep from removing the wrong element from the dict. So my example uses the simpler implementation that does a lookup by enumerating)

    All of these are valid for doing it.

    They seem hacky though... because they are. Because the design fundamently is backwards. That is, in my honest opinion.

    ...

    I still wonder though.

    What use case is this for?
     
    Last edited: Jan 18, 2022
  13. AnimalMan

    AnimalMan

    Joined:
    Apr 1, 2018
    Posts:
    1,164
    Maybe the answer is to use Gameobject Find with tag and just modify the tag as OP had originally planned. I don’t think he is happy with writing lots of code. So I guess just use gameobject find as originally planned.