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. Dismiss Notice

Question Reusable classes best practice in C#

Discussion in 'Scripting' started by AMU4u, Jul 3, 2023.

  1. AMU4u

    AMU4u

    Joined:
    Feb 16, 2015
    Posts:
    14
    Hello,

    Lets say I have a Resource class that has a name and cost. I also have a class called Place that can hold these resource classes in a List. There will be several places, but only two resources; Passenger and Ore. Places can have either, both or neither of the two resources. The cost of either of the two resources can vary between places.

    Currently I have a public List<Resource>() resources inside of the Place class where the individual resources will be stored. The problem is each time I create a new Place prefab in the scene, I am forced to create a new list of resources for that specific place, retyping "Passenger" and "Ore" over and over again.

    I realize I could just have each Place start with the full list of resources and just delete the entries that place won't use, but that doesn't feel very robust.

    My question -> Is there a way to create the resources in a "main" list, and when I create a new Place prefab, the list of available resources is based on that main list? So down the road if I want to add descriptions or new resources I only have to do it in that one main list and then easily add and modify it per Place?

    Thank you!
     
  2. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,769
    I feel like you need to look into scriptable objects.
     
    _geo__, dlorre, lordofduct and 3 others like this.
  3. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,503
    To me it sounds like this can be represented as four variables in a "Place":
    - passengerAmount
    - passengerCost
    - oreAmount
    - oreCost

    If it is as you say it is, and there really are only two resources and those just have costs... why make it any more complicated than it needs to be?
     
    AMU4u and spiney199 like this.
  4. AMU4u

    AMU4u

    Joined:
    Feb 16, 2015
    Posts:
    14
    I ran my question through AI and it mentioned using getter:setters for the resource class in the same way you are saying here inside of Place. With a getter/Setter each resource would be one variable inside of Place. From there in the resource class I could have an enabled/disabled bool that controls if the Place can use it or not.

    Then I'd only need to change the base resource class to add any needed logic later. If a wild hair appeared and I did want to add resources, I'd just need to add another getter/setter in place, and all places would have access to the resource class.

    What do you all think of that approach? Seem pretty efficient? Thanks again!
     
  5. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,563
    Efficient in what way?

    Keep in mind that you have already given up a lot of efficiency by spending time asking theoretical questions about unwritten software.

    Every approach can work, everybody has opinions on which approach is good or bad, only YOU will know once you have used it for a significant period of time.

    You should consider writing the game first and seeing how it goes.
     
  6. AMU4u

    AMU4u

    Joined:
    Feb 16, 2015
    Posts:
    14
    As a professional developer I have come to appreciate strategizing my coding practices so I can write clean, efficient code. The, "just write it" method works, and without it I probably wouldn't make as much money as I do. I spend 8 hours a day fixing code written this way, can't imagine doing it for a hobby, as well. Thanks for the input!
     
  7. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,563
    Me too.

    And guess what?

    When you are not fixing code written this way, you will be fixing brittle over-wrought over-engineered computer-information-science (CIS) major code that was planned meticulously in advance for years and years and exists in 57 different confluence wiki pages and after all this amount of time, it's a royal pain in the @#@#$@% to work with because to make even the slightest change requires touching 57 nested classes and five interfaces that were so poorly designed from the start that the person writing it MUST have been a masochist.

    Meanwhile, games is games and games needs nimble. Write it, toss it, write it again, play it and decide it still sucks, toss it and rewrite it and eventually... EVENTUALLY you might find fun.
     
  8. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,503
    ... you should already know far more than that AI could have regurgitated at you.

    List out the pros and cons of at least 3 significantly different appproaches, then we can doscuss them.
     
    Kurt-Dekker likes this.
  9. halley

    halley

    Joined:
    Aug 26, 2013
    Posts:
    1,834
    I have always likened software development as a discipline closest to carpentry. There are exquisite sculptures, functional cabinetry, and knocked-together framing for the walls of buildings. Regardless of purpose or skill, the average useful longevity of items is about a generation, even if the material lasts longer.

    When writing code for games, 90% of the code is mass production toymaking. Just enough cuts in the wood to describe the train or truck or duck or spinning top, and no more.
     
  10. Owen-Reynolds

    Owen-Reynolds

    Joined:
    Feb 15, 2012
    Posts:
    1,913
    Sure, having a single list of "real" objects and having everyone using them point to them through links is a real thing. It's just a little confusing in a language without pointers and a little more confusing to make it work with Unity serialization.

    You main list would look like:
    public List<Resources> R;
    . In our minds, it "owns" the Resources it links to (like most lists). Next, each of your "places" linking to some of them could use:
    public List<Resources> myResources;
    , which is exactly the same type! But we're going to be sure it only references items in
    R
    . It's just a funny thing that in C# "list of resources" and "list of links to resources" looks the same.

    But we want to serialize this and make Unity happy. If we want to drag a Resource into an Inspector slot,
    Resources
    needs to inherit from
    Component
    (which is also if we want Unity to serialize our links as simple links and not duplicate full instances). The easiest way of being a Component is making them ScriptableObjects, as spiney wrote (well, the easiest way is making them into prefabs, which work like ScriptableObjects, but then you have to remember they aren't meant to be used as prefabs). But, that forces you to create your real Resources as Assets, so you no longer need the master list
    R
    (you can still have it, and drag in each resource instance). Then, of course, users such as
    myResources
    also drag in links, which Unity understands are merely links.

    It's possible to have
    Resource
    as a normal class. That means each will pop-up in
    R
    and be filled-in there, but then no one else can link to them through the Inspector. Instead, other lists might use an extra field with the ID of the resource it wants: "RO1", WD3" ... (each Resource would have it's own ID written in it). At start-up you'd match them up and fill in the links. Basically, you're serializing by-hand. It's not too bad, but it's what you do when you don't have shortcuts like Unity provides.
     
    AMU4u likes this.
  11. dlorre

    dlorre

    Joined:
    Apr 12, 2020
    Posts:
    700
    If you make it a ScriptableObject then you can access the List from the inspector and drag stuff onto it:

    upload_2023-7-4_5-1-34.png

    I like getters and setters very much but I don't see how it would make your life easier here.
     
  12. AMU4u

    AMU4u

    Joined:
    Feb 16, 2015
    Posts:
    14
    In this example isn't the only thing making the parent resouce immutable stuff like name us not adding incorrect data to the Place list?
     
  13. AMU4u

    AMU4u

    Joined:
    Feb 16, 2015
    Posts:
    14
    ok so far this is what I have
    The resource
    Code (CSharp):
    1. public class Resource
    2. {
    3.     public int cost;
    4. }
    the Place script
    Code (CSharp):
    1. public class Place : MonoBehaviour
    2. {
    3.     public Resource Passenger;
    4.     public Resource Ore;
    Now, in the inspector I see
    upload_2023-7-4_15-9-12.png
    If I want to add more resources I simply add them to the Place script which will add it to all places, then I would either create a function to manage that new resource or simply manually interact with it with each place.
    Is there something I am missing here or does this seem pretty scalable when considering down the road there will be hundreds of places?
    Thanks for the replies :)
     
  14. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,503
    What does "scalable" mean for you?

    Really though, it doesn't matter. You've got something there, you'll learn more from using it than you will from over-theorising with us. We don't know anything about the rest of your game, so we can't know what problems you're likely to run into.

    You can make changes down the road if something turns out to be less than ideal. Getting something going so you can learn from it is far more useful than analysis paralysis.

    Do. Learn. Do again.
     
  15. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,378
    I have questions...

    What is a Resource?

    To give you an idea of what I mean... so we have a 'Transform' and that Transform has a member called 'position' that is type Vector3. A Vector3 can be used to represent anything that can be represented as 3 floats... like a 3-space cartesian position. Which in the case of Transform is the 'position'. A Transform having a "position" describes the "Transform"... that's what Transforms do... they have positions (as well as rotations and scales).

    Another thing that Transform's have is children. You can enumerate the Transform to get the children, and you can call methods like GetChild to get a child Transform at some index. Transforms have children. Thing is children are distinct from positions. Positions describe the Transform, where as the children are related to the Transform. The children are distinct from their parent Transform. A child can be removed from its parent Transform and placed inside another Transform.

    This concept about Transform and its children is what is called in the biz "object identity". What makes an object and object, rather than a value, is that it has identity.

    The values 5, "hello", and <0,0,0> don't have identity. They're just values. What they actually represent is distinct from the value itself. An object on the other hand is more than just its values that describe it. A Transform has a position of <0,0,0>, but <0,0,0> does not adequately describe the Transform. There is MORE to the Transform than just its position. If another Transform had a position of <0,0,0> that other Transform wouldn't magically be the same Transform as the first. They're just 2 Transforms with the same position.

    ...

    So... what is a "Resource"? In that what is the class called "Resource"?

    For that matter, what is a "Place"? What is the purpose of "Place"? And what does it mean for a "Place" to have a property named "Ore" or "Passenger" of type "Resource"?

    And why I say this is that the word "Resource" gives me a heavy implication of "object identity". Resource is often a distinct thing. Resource doesn't sound like a value... Resource doesn't feel like "int" or "vector". The fact I can have an Ore and a Passenger feels to me like having 2 different resources should be distinctly different.

    But like... what about Ore and Passenger make them distinct from one another?

    ...

    To be specific say I have 2 places.

    And both places have these Resource's on them, Ore and Passenger.

    And they have a cost on them....

    Can place A have its Ore property set with a Resource of cost 5, and place B have its Ore property set with a Resource of cost 7? And a Place only ever has a single Ore property that has a specific cost associated with it. Meaning that this 'Ore' property is really describing how much it costs to purchase Ore from this Place.

    If so... then Resource is more like a value, like how a Vector3 is. If that's so... well then I'd like define them as a struct to convey this. I might even call the type something like "ResourceInfo" or "ResourceCost".

    But lets say instead a "Place" is really a representation of a place in space in our game. It's a node in our world where an object can stand. Say we have a grid based game about loading resources onto a train. The train cars described as a grid of "places" on which can only stand a single passenger, a single ore, or a passenger and ore simultaneously. But never more than 1 ore, and never more than 1 passenger. Lets even say these passengers and ores can be moved around the train. Sure a given passenger may have a different cost than another passenger, same with an ore. But this is because they are distinct. This is a 5 dollar hunk of gold, and that's a 6 dollar hunk of gold. But they are distinct hunks of gold from one another. Even if you had 2 hunks of gold that are both worth/cost 5... they are distinctly different resources.

    Well now these things have object identity and using a class seems more fitting to me.

    But also... I find the reliance on serialization to represent them to be weird. This means every Place will start with a Passenger and an Ore. Is this intended? All places have exactly 1 Passenger and 1 Ore on start?

    Note this is counter to your List approach which implied that each Place could have any number of Passenger's or Ore's, including 0... and that the name "Passenger" and "Ore" were distinct to the type Resource rather than to Place.

    This implication felt like you were saying that Resources have object identity. Which led me to what is with the name "Passenger" and "Ore"? If those are types of Resources... couldn't that be an enum that better implied that Passenger/Ore are descriptors of distinct Resources? But then I got a vibe from the thread that maybe its because really only 1 Passenger or Ore exist... which the ScriptableObject suggestion sort of fit into. To which the serialization of the classes was like "but now you have tons of "Ore"s all distinct when not completely losing the object identity. Which then making them properties really pushed home the idea that maybe they have no identity whatsoever and that really these are just values... but then why call that a "Resource"? It's a weird name for this.

    ...

    I don't know... I'm super confused as to what it means for a Place to have a property called Passenger and a property called Ore.
     
    Last edited: Jul 5, 2023
  16. Owen-Reynolds

    Owen-Reynolds

    Joined:
    Feb 15, 2012
    Posts:
    1,913
    That doesn't make what I think you were asking about. The way Unity works, if you see a place in the Inspector to enter values (like in your picture) that's not considered a link. Instead, each of your Places has it's own personal set of Resources. In Unity, to make it count as a link you have to see a single slot which you can drag something into (with {Resource} written there and that little circle). To get that, you need the class Resource to be either a Monobehavior or a ScriptableObject. You then have to create the actual Resources as Assets (either Prefabs or use the menu to make ScriptableObjects). Then you can drag them in.

    It seems tricky, but once you use Unity for a while it's the obvious way it has to work.
     
  17. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,104
    I agree that the
    Resource
    class and in the latest iteration doesn't feel like a fitting abstractions.

    I think the concept of a "resource" (ore, passenger) and the "amount" of a resource (2 ores, 5 passengers) should be kept separate to avoid confusion, and to make it possible to refer to a resource without having to bundle it up with an amount.

    In your Place class, you can get away with just serializing the amounts as integers, to keep the workflow in the Inspector simple. However you shouldn't serialize your resource objects directly in each Place class, but instead serialize the data (like description) for each resource separately, for example in a scriptable object.

    So something more like this maybe:
    Code (CSharp):
    1. public sealed class Place : MonoBehaviour
    2. {
    3.     [SerializeField] private ResourceAmounts cost;
    4.     public ResourceAmounts Cost => cost;
    5. }
    Code (CSharp):
    1. [Serializable]
    2. public sealed record ResourceAmounts : IReadOnlyCollection<ResourceAmount>
    3. {
    4.     [SerializeField, Min(0)] private int passenger;
    5.     [SerializeField, Min(0)] private int ore;
    6.  
    7.     public ResourceAmount Passenger => new ResourceAmount(ResourceType.Passenger, passenger);
    8.     public ResourceAmount Ore => new ResourceAmount(ResourceType.Passenger, passenger);
    9.  
    10.     public ResourceAmount this[ResourceType type] => type switch
    11.     {
    12.         ResourceType.Passenger => Passenger,
    13.         ResourceType.Ore => Ore,
    14.         _ => throw new IndexOutOfRangeException(type.ToString())
    15.     };
    16.  
    17.     public int Count => this.Count();
    18.  
    19.     public IEnumerator<ResourceAmount> GetEnumerator()
    20.     {
    21.         if(passenger > 0) yield return Passenger;
    22.         if(ore > 0) yield return Ore;
    23.     }
    24.  
    25.     IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    26. }
    Code (CSharp):
    1. [Serializable]
    2. public struct ResourceAmount : IEquatable<ResourceAmount>
    3. {
    4.     [SerializeField] ResourceType resource;
    5.     [SerializeField, Min(0)] int amount;
    6.  
    7.     public ResourceType Resource => resource;
    8.     public int Amount => amount;
    9.  
    10.     public ResourceAmount(ResourceType resource, int amount)
    11.     {
    12.         this.resource = resource;
    13.         this.amount = amount;
    14.     }
    15.  
    16.     public bool Equals(ResourceAmount other) => resource == other.resource && amount == other.amount;
    17.     public override bool Equals(object obj) => obj is ResourceAmount resourceAmount && Equals(resourceAmount);
    18.     public override int GetHashCode() => HashCode.Combine(resource, amount, Resource, Amount);
    19.     public static implicit operator ResourceType(ResourceAmount resourceAmount) => resourceAmount.resource;
    20.     public static implicit operator int(ResourceAmount resourceAmount) => resourceAmount.amount;
    21. }
    Code (CSharp):
    1. public enum ResourceType
    2. {
    3.     None = 0,
    4.     Passenger = 1,
    5.     Ore = 2
    6. }
    Code (CSharp):
    1. [CreateAssetMenu(fileName = "New Resource", menuName = "Resources/New Resource")]
    2. public sealed class Resource : ScriptableObject
    3. {
    4.     private static readonly Dictionary<ResourceType, Resource> instances = new();
    5.     [SerializeField] private ResourceType type;
    6.     [SerializeField]  private string description;
    7.  
    8.     public ResourceType Type => type;
    9.     public string Description => description;
    10.  
    11.     public static Resource Get(ResourceType type)
    12.     {
    13.         if(!instances.TryGetValue(type, out Resource result))
    14.         {
    15.             result = Resources.Load<Resource>("Resources/" + type.ToString());
    16.             instances.Add(type, result);
    17.         }
    18.         return result;
    19.     }
    20.  
    21.     public static implicit operator Resource(ResourceType type) => Get(type);
    22.     public static implicit operator ResourceType(Resource resource) => resource.Type;
    23. }
     
    Last edited: Jul 5, 2023
  18. Owen-Reynolds

    Owen-Reynolds

    Joined:
    Feb 15, 2012
    Posts:
    1,913
    Yeah, I thought OP was asking for a standard pattern, but I'm not so sure now. The pattern is a big fat class holding generic data for metals, woods and stone such as name, description, icon, sound when clicked on, base stack limit ... . Then users, for example, crafting recipes or the OP's places, hold things like [(2,linkToOre1), (6,linkToStone3)] to say this requires 3 of ore1 and 6 of stone3.

    Technically that's the FlyWheel pattern from the official Design Pattern book. But no one knows that and it's easier to think of it as a standard SQL database way of avoided repeated data through foreign keys into master tables.
     
  19. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,104
    Yeah the flyweight pattern, i.e. having the Places hold direct references to shared Resource assets, would also be a totally valid option.

    Since Inspector usability of the Place component was a key pain point the OP had, I tried to optimize for that as much as possible in my example. If the Place class contained direct references to Resource assets, and then new Resource types were introduced to the project later on, references to those would be missing from all pre-existing Place instances.