Search Unity

When appropriate to use inheritance versus components

Discussion in 'Game Design' started by BIGTIMEMASTER, Jun 27, 2022.

  1. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    Hey guys, this is a general game programming question, but I'll try to illustrate with a specific example just so it's easier to understand.

    Suppose you have a survival game. There are items that player may interact with. For example, we have these two classes:
    Foodstuffs (apple, banana, grapes, and so on)
    Vehicles (car, truck, and so on)

    The common link that Foodstuffs and Vehicles share is that they can both be interacted with.

    So, as I understand there are two main approaches: I add an "interactable" component to both the class Food and the class Vehicle.

    Alternatively, I derive Foodstuff and Vehicles from a parent class: Interactable Object class. And in that parent class lives an OnInteracted function. Perhaps some other shared functions may exist, but for example I think a single commonality is enough.

    As far as I can tell, the difference between inheritance and component structure here is in ergonomics? With components I need to remember to add the component to anybody who needs it, which does have some room for annoying human error.

    With inheritance I guess the worry is that I could possibly spin a tangled web, but I think so long as the chain is only 1 or 2 layers deep, what is the problem? Should some child class need to override a function that allows for all the flexibility I need, right? And if over the course of development, I realize that a banana is actually in a class of its own, then I can just change the parent class, no problem. Right?

    So, my major question is - is it just a matter of ergonomics? Are there performance or other technical considerations I am unaware of?

    In the past I've used something like a state pattern to handle input. I have a Base State class that has some common functions like OnConstructed and HandleSpacebarInput. Depending on the Child State class that is currently active, then I can easily map the same input functions many different ways. This also chunks my code in a very intuitive and easy to debug way.

    I see the same approach being viable to handling interactable objects. If I collide with a game object, we query its class. If it is interactable object class, we can call its OnInteracted function, and the child instance is going to add additional stuff on top of the parent's function or override it entirely if necessary.

    Another approach might be that we just send an interface message to the collision object, and should it implement the interface it can do whatever it wants. But if my Apples, Bananas, Pears and Kiwis are only differentiated by their data values (i.e banana is 10 calories, apple is 12, and each has its own sprite image), then it seems like making each of these things a separate game object that has to get a litany of components and each subscribe to interfaces is a lot more work?
     
  2. dgoyette

    dgoyette

    Joined:
    Jul 1, 2016
    Posts:
    4,195
    I think the main objection to an inheritance approach is that any given object could have various behaviors, not all of which can easily be associated with a base class.

    To extend your example, imagine you want to add a behavior that determines if an object will explode if something shoots it. And let's say you also add a basic exploding barrel to your game. But you decide that vehicles should also explode when you shoot them. You've already decided that Vehicle inherits from Interactable Object, but now it should also inherit from Exploding Object. But food shouldn't explode, so... What should a vehicle's base class be now? Interactable? or Exploding?

    Another nice thing about the component approach is you can easily decide later on to add some behavior to something. Maybe early in development you didn't think vehicles would explode, but later you decide that's a good idea. Slap an Explosive component on it, and you're good to go. No re-engineering all your inheritance.

    That doesn't mean not to use any inheritance. I use that too. But probably only when it satisfies the "is a" constraint.
     
    TonyLi and BIGTIMEMASTER like this.
  3. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    Also, follow up question - very general here, but in planning out some systems like this, I think the best way to start is have a two-column list, where, Column 1 has class similarities, Column 2 has class uniquities?

    Do you do something like that, or totally different?

    A quick example:

    upload_2022-6-27_8-26-34.png


    So from a list like that, you could start figuring out either what classes you need or what components you will need, based on shared commonalities?
     
  4. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    Ahhh, that totally makes sense.

    So in this case, I think both could be used. We might have the ergonomic boost of deriving most things from interactable object class, but should they also need to explode, that sort of behavior comes from a component - thus if bananas also need to explode, we can do so.

    But for something so common as calling an "on interacted" function, that still makes sense to you to let that come from a parent class?


    edit:

    The "is a" constraint - is this talking about the Liskov’s Substitution Principle?

    Idea being that if we changed InteractableObject class with Banana, it should still work? Because the banana should only be adding upon the parent class?

    I guess I don't understand that, because overriding parent class functions seems to be a useful thing to do that hasn't presented me any problems so far. But I think I'm not understanding the basic idea.
     
    Last edited: Jun 27, 2022
  5. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,696
    Two criteria you can use to follow dgoyette's advice are:

    1. Might using inheritance cause code duplication down the road? Taking the InteractableObject --> Vehicle, ExplodingObject --> Barrel example, if you wanted to explode vehicles, you might have to duplicate ExplodingObject's code in Vehicle. So in this case, "exploding" should be an attribute (component).

    2. Will inheritance add unused code to parent class? Say you add explosion code to InteractableObject so Vehicles can explode. Then the interactable Banana inherits this code that doesn't apply to it.
     
    BIGTIMEMASTER likes this.
  6. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    @TonyLi @dgoyette

    ah okay, that makes sense. Sorry I'm kind of scatter brained about all of this - just trying to get a vague picture of how some things might work so I can build a few test. This helps a lot.
     
  7. dgoyette

    dgoyette

    Joined:
    Jul 1, 2016
    Posts:
    4,195
    That's probably the formal version of what I mean, but I don't think so deeply about it. In practice, I nearly always use inheritance with abstract base classes, and concrete sub-classes. A simple example in my project is an "Energy Beam" controller. I have various types of lasers which do different things when they hit something. But all Energy Beams share some common functionality, like turning on/off, aiming, raycasting for hits, etc. All that functionality goes into a base class. Then I have sub-classes for all the specific kinds of beams that do different things compared to each other (burn things, make things grow/shrink, etc). So, a Burning Energy Beam "is a" Energy Beam. If I had a method that accepted an Energy Beam, I could pass it any of the subclasses and the method would work fine.

    So, I tend to use inheritance for behavior that really determines the core identity/nature of an object.

    But for things that could blow up if you hit them? To me that feels like nearly anything could do that, so I go with components. For something like that, I have a component I call a "Force Tracker", which handles all of the ways that forces could potentially "harm" the object. And if the force exceeds a threshold, a UnityEvent is called. The only thing I do for each object is tell it what method to call when that threshold is exceeded. Pretty easy.

    You'll probably find, though, that this is a topic that developers love to give opinions on, but it's almost always anecdotal advice based on their own personal preferences. So, probably the best thing is not to rigidly constrain yourself to one approach or another. If you've painted yourself into a corner with either inheritance or components, don't be afraid to pivot.
     
    BIGTIMEMASTER and TonyLi like this.
  8. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,696
    This is the best advice. Refactor as necessary. (But find a balance between refactoring and moving forward with new tasks.) Try to minimize dependencies between different functional parts of code so you can refactor one part without impacting other parts. We could mention C# interfaces, but that's just another detail.
     
    angrypenguin and BIGTIMEMASTER like this.
  9. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    Yeah that is my main worry, besides just ergonomics. So far the major pain point in my learn-to-code adventure was in making code with too much tight coupling. In a "simple" project I think I've mostly reduced that but I am exploring a few things to do next, and now that i understand the problem a little better I should be able to avoid it (hopefully :) ).

    My experience with interfaces so far is unreal blueprint implementation - I believe its the same idea though. Basically, an interface allows you to send a function or event call to a soft-reference of an object, and if that object implements the interface, then they will do whatever they want with the event/function.
    This reduces tight-coupling is the big idea, though my use of them has been pretty basic so far. With minimal moving pieces in my project it's not a big deal to use a couple hard references here and there for convenience's sake.

    Yeah I see a lot of debate about this approach versus that. In the end I always do just what I can understand and manage but good to get some sense of how others approach things.
     
  10. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,894
    I think it's worth mentioning that the component structure is kinda layered over inheritance. Remember, Unity is component based, but all unity Components are part of a greater inheritance tree, with the root class in most cases being UnityEngine.Object.

    To that end I've nearly always preferred a component structure over a purely inheritance one. Often, also, alongside some composition with Interfaces. When I do use inheritance, it's always very shallow; never more than the one base class and its immediate children.

    So with your initial example with vehicles and food being both 'interactable', personally I'd make that an interface (
    IInteractable
    ) as it just boils down to implementing a method or two. Then, for interactable stuff that all express similar behaviour, they could have a base class that implements this interface, with some immediate children. GetComponent<T> works on Interfaces after all.

    These days I'm using SerializeReference a lot too to serialize polymorphic data without the need for scriptable objects.
     
    BIGTIMEMASTER and Ryiah like this.
  11. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,620
    The following is probably far more than you care about the topic. ;)

    Inheritance is powerful and elegant... until it suddenly isn't.

    As you've been learning programming mostly related to Unity I would guess that you've only been using C#, and when you learned about inheritance there was a fundamental rule that a class can only extend one other class. The thing is that it's totally not a fundamental rule in computer science at all. "Multiple Inheritance" is a thing, it makes perfect sense, and other languages have it. Single Inheritance is a deliberate choice in C# (and some other languages) because it turns out that multiple inheritance... is tricky.

    Imagine that I've got a class, FuelTruck, which is both a vehicle and an explosive. In an ideal world it seems like a good idea to have it derive from both Vehicle and Explosive. In C++, as one example, that's a thing which you can absolutely do. And while things stay simple everyone is happy. The catch is that this can very easily introduce ambiguity to your code, and I'm sure you're aware by now of how much computers loathe ambiguity. Add that to the fact that things almost never stay simple and you can easily find yourself in a Deadly Diamond of Death situation, as one example. (Yes, it was really called that, by "Uncle Bob".)

    So given that the seemingly ideal solution turns out not to be ideal at all, other approaches are taken.

    Breaking objects into components and using "composition" is one of those. I can have GameObjects with both Vehicle and Explosive components. There is no ambiguity because both Vehicle and Explosive can have their own implementations of ApplyDamage(), and anything which calls them must call either or both explicitly. Everything is groovy, no worries. It is slightly more verbose for programmers, and extra objects do mean extra memory / instruction overheads, but they're usually negligible.*

    Another approach is the use of "Interfaces". These behave and can be used almost identically to classes, with one exception: they can declare methods (give them names) but they can not define them (have code in them). In this case I can have an object which, for instance, "derives" from Vehicle and "implements" IExplosive. It inherits everything from Vehicle as per usual, but all it gets from IExplosive is a bunch of methods signatures which it must implement. So if I have five disparate classes which implement IExplosive I need to manually provide an equivalent implementation in each of them, i.e. repeat some code.

    You may notice that each of these is essentially just committing to a subset of the pros and cons that Multiple Inheritance would bring. I suspect that's exactly why language designers often restrict us to Single Inheritance - it forces us to deliberately choose for ourselves, rather than get tripped up by it later on**. Single inheritance alone is enough to get most jobs done, and we've got a bunch of approaches to choose between for the remaining cases - components, interfaces, mix-ins, etc.


    Coming back to your question. Whether or not it's "all just ergonomics" is probably a semantic debate. At the small scale with simple stuff I'd say that yes, it essentially is. As projects grow it eventually becomes an architectural level design decision, partly rooted in all that computer science stuff in the paragraphs above.

    * There's actually another massive benefit to this approach, which is that composition of new object types can be lifted out of code and into data. For example, I don't have to write any code to make my explosive vehicle also be solid in Unity. I just go Component -> Add -> Collider. No code or recompilation needed. I can replace the Wheels with Thrusters in data and turn it into an explosive hover truck, etc.

    ** Whether or not that is effective and productive is a separate debate that shall never end. ;)
     
    Ryiah, BIGTIMEMASTER and spiney199 like this.
  12. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    Thanks a lot for sharing - I am working mostly in a vacuum so sometimes I wonder if I am doing things in a totally nonsense way. Really helpful to hear how others are doing things. What you've described here is similar to what I've done but a bit better it sounds like - using the interface. In my simple case I dont think it matters but if things grew more I think I'd want to do something more like you've described.

    That's greek to me. I'll have to google that at some point.


    @angrypenguin

    magisterial post!

    In refactoring my project and starting to think a bit about the next, I've taking some time to read more about programming. It is really hard to find plain language, practical explanations though. You've helped clear up quite a bit here.

    Currently most of my hands-on programming has been with unreal blueprints. But it's helped me grasp the basics so that I've been able to understand enough general programming that I was able to make a (not finished) chess game with Java (why java? I just thought making chess would be good general programming exercise and the best example that I found was in java, so I just did that). And a lot of the people I get programming advice/examples from are using unity c# so to me it's all just "programming" and I remain ignorant so far of major differences between languages.

    I can see how idea of multiple inheritance may be useful for like, refactoring. Maybe you already have the explosion and vehicle classes fully fleshed out, and you really that some new class could just plug into both? But I can imagine how that might get messy fast if you are still in a development phase creating new things and making many changes rapidly.

    I also got a better idea about the benefit of modularity using the compositional method - that makes sense. I think in my work so far, it has taken all my energy to learn so much kind of quickly so any tiny thing that makes my code more verbose makes me feel like it's overly complicated. But now that things are becoming clearer, I can see how implementing that sort of pattern from the start might pay off in the long run - especially if I work on a project that is more complex code-wise.

    I think the major thing for a solo developer like me is that I need some general patterns to follow so that it's not necessary to remember what every little script is doing - so long as I know the general pattern, then making changes throughout development should never be a brain twister. So far I think successfully put to use a couple patterns (like state pattern for input) and that made life so much easier rather than just having a litany of conditionals.

    Anyway, thanks everybody this has been both a great sanity check and also, I learned/understand a few important things better now. In coming weeks on my time off I'll try to test out some more of these ideas as I poke at exploring next project possibilities.
     
  13. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    So, as an example of composition, suppose we have many vehicle types in a game. Say car, truck, bicycle, boat. Let's suppose it's arcade representation - no physics simulation at all.

    Some things they all have in common is input to control them. WASD or joystick makes them go forward, back, left, right.

    Each vehicle must have its own visual representation, its own movement related variables (top speed, acceleration, deceleration, turn speed, etc.)

    In this case, you might derive each vehicle from a base class - but that base class can be as high level as your character class, basically, right? It's just something in the game that the player can control through input.

    Our car, truck, and certain types of boats might be able to explode. So for that we might use a ExplodableObject component that can be attached to them.

    That is my initial thought how a system like that might work. But as exercise, what if we did it with pure composition?

    How might that look?

    We have our base character class where we handle input. Would you just attach a vehicle component to it at runtime when necessary? And then the vehicle component might ask the question, "based on what sort of vehicle I am, do I also require the ExplodableObject component?"

    That seems a little confusing to me because we need to override or replace what is happening when the same input that normally moves character is handled. I guess this is not a good case to use composition approach, or am I imagining it the wrong way?

    Maybe you don't even have a base character class? You just have a "things in the game with a transform" class, and you build your character up from various components?

    Foot travel Movement component
    Vehicle travel movement component
    etc etc ?

    No reason that shouldn't work. But is anything gained? Anything lost?
    We know that the foot travel movement component is only going to be used by the player. Is there any benefit to moving some code into a component if it can only ever be used in one case?
     
    Last edited: Jun 28, 2022
  14. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    Here is a better visualization of this example:

    Screenshot 2022-06-28 163612.png


    So, we would like to have all of the data for each movement type in a .csv file. At some point, somewhere, we can swap which .csv file we are reading to get the data needed for whatever the current movement type is.

    Most of these movement types are all going to be the same - i.e. they share the same input related functions (press W to go forward). The only thing changing is the data variables (car goes faster, walking goes slower.)

    What is different is what animations play, what sfx plays, etc.

    And in some cases, we might have unique conditionals. E.G, kayak grants you stealth bonus or something.

    Now, we don't want a gigantic class for all vehicles, with a switch statement that is saying, "if I'm ____ type of vehicle, do all these things." Right? I mean that can work, but it's not so easy to search and debug I think.
     
  15. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,620
    It could be a GameObject with components such as:
    - Rigidbody
    - UserInputAdapter
    - Motor
    - SteeringController
    In turn it would have some child GameObjects which have "Wheel" or "Thruster" or whatever components which the Motor and SteeringController components use to do stuff.

    And when I say "could be", I mean that's exactly how the different vehicles in my game are all made.

    Close. You don't need a class for that. Transform already exists.

    I have four different vehicle types, each fundamentally different, and until fairly late in development there was no concept of a "Vehicle" in the game at all. Anything which cared about a vehicle just got a reference to either its GameObject or its Rigidbody, depending on the context.

    I did eventually add a "Vehicle" class, but it basically does some administrative stuff, such as storing the localisation ID for the vehicle's name, and data for a couple of the high-level rules of the game which are specific to vehicles.

    This sounds as if your character class is actually doing a little too much. A general rule of thumb is that component classes should be named after their function or role. I don't know what function a "Character" performs, because there are actually many things a "character" does and I'm not sure which one you're talking about. Can your vehicle engage in dialog? Does it have an inventory? If it's a thing which controls movement or input, I'd name it that.

    Ideally, in an environment such as Unity, I should be able to know what a GameObject does just by scrolling down the Inspector and looking at the component names.

    I would avoid having components ask such questions at runtime, precisely because it could turn things into a confusing mess. If this is based on the type of vehicle it is then that is known at design time and should be applied then and there.

    In short: anything which can be handled ahead of time should be handled ahead of time.

    Adding and removing components at runtime is a nice and powerful approach, though. I'd just keep it for when behaviours actually change, rather than using it to defer setup of stuff you know in advance. If I know my Vehicle musthave a Rogodbody then that should be added at design time. However, if I have a spell which makes a vehicle highly magnetic, then implementing that by adding a Magnetic component at runtime makes a lot of sense - it is indeed adding new behaviour.

    To assist at design time, in Unity you can add the [RequireComponent(...)] attribute to a MonoBehaviour class to tell it that one component requires another to be present, and it'll then automatically enforce that in the Editor for you. So if my Vehicle class requires a Rigidbody component, I can add [RequireComponent(typeof(Rigidbody)] just before the class definition, and then when I add a Vehicle component to a GameObject in the Editor it'll also add a Rigidbody for me, and it won't let me remove it while the Vehicle is present.

    Gotta go, but quick question: why a CSV rather than just using the Inspector?
     
    BIGTIMEMASTER likes this.
  16. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    Thanks a lot for response. A lot to read over carefully there.

    As to why use a csv - mainly just so that I can work in excel and use functions and stuff to balance things out. Just better ergonomics basically.
     
  17. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    If I understand correctly, basically your setup is like this:

    You have a class for each type of vehicle. And then, each class is assigned whatever components that it needs.

    So you might have Mars Rover and it has Engine component, Wheels Component, etc.

    And then you might have Hovercraft and it has thrusters, no wheels, etc.

    Suppose the only difference between two vehicles in a game is the static mesh and data variables (i.e movement speed, acceleration, etc.)

    In that case, do you still think its worthwhile to separate them into distinct gameobjects? Like, would you even need a Wheels component if the only difference between boat and car is how fast they move and a few conditionals like, boat can go on water but not land?

    Would you see a problem if all vehicles were only one class? Suppose I wanted some general rule for all vehicles. Like, "can travel on surface type _____." All vehicles must know which surfaces they are allowed to travel on might be a global rule for some games. In that case, with composition, you'd end up repeating the same code in multiple components? Or would that condition just be part of certain components? Like Wheels component knows that it can travel on road but not water. And boat component knows only water, not roads?


    How are you handling input?

    Like, is there some class that reads input, and then delegates it out to whoever or whatever is currently meant to be controlled?

    In my current project, I have a "controller" that reads all input (this is suggested use of the class in unreal), and then the controller passes input along to a state class. Each state class then delegates that input to functions/events that typically live on the character class. But I only have one type of character (character in my project just means humanoid avatar we will control. But in something like examples we've discussed here, you are right character has little context.)

    If I were building a project from ground up in unity that is more like the examples we have been discussing (you can walk as a human, ride a bike, drive a car, fly a plane, etc etc), then the compositional approach is something like this:

    input gets handled... somewhere. I guess isn't super important, just needs to be something that persistent across levels.

    gameobjects that might handle input would get an input handler component. We would need a way to identify input priority in that case? Like, in game suppose you are on foot, then you enter a vehicle. Both the human character gameobject and the car gameobject have input handler components, right? So, we have to track which is currently active?

    And then, consider boat versus car. They might have 99% same components. If there is special behavior that only will ever pertain to boats, that script can just live on the boat class? Or just for consistency's sake, you make it into a component as well?

    So, a "vehicle" then, before you have a vehicle class, is only known in game as a generic gameobject? And then this specific instance of gameobject has certain assigned components? And that specific configuration is the concept of a prefab?

    Supposing you want to identify a specific gameobject - how do you handle that? Or typically you dont need to? You only send interface calls, and should some component in that gameobject subscribe to the interface, then it will act accordingly?

    e.g. if I shoot a rifle and it strikes vehicle, and imagine i want vehicle to explode when shot five times, with this compositional approach you dont need to have any concept of vehicle, you just send interface message "hasBeenShot" and then some component on the vehicle DamageComponent might tally up the damage and report that out, and then the Explode Component might be listening and fire off its own events based on data the damage component is putting out?

    And the difference between vehicleA which explodes in 3 shots and vehicleB which explodes in 5 shots is just housed in the prefab? Like, the component is instance editable on a per prefab basis?
     
    Last edited: Jun 29, 2022
  18. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,620
    No, there's no class for specific vehicle types. It's as you described later:
    They're all just GameObjects with different components and/or children.

    Of course that might not be practical in all cases. For instance, if you need to identify things by their vehicle type then knowing the type of a vehicle somewhere is quite handy.

    I'd put it in a component. Partly for consistency, and partly for reusability.

    Boats float where most Vehicles don't, so I agree that it could make sense to implement that in the Boat class if I have one. But Boats aren't the only thing that float, and if I'm introducing "floating" as a mechanic into my game (or as a consideration into whatever software I'm making) then there's a good chance it'll be used again. So implementing it as a Bouyancy component which I can slap on any GameObject sounds good to me. And when someone wants to make a floating crate later on there's a good chance that I don't have to do anything.

    Most of the time this arises naturally out of whatever system the interaction is coming from. For instance, if a vehicle enters a trigger zone then an OnTriggerEnter(...) call is made to both which includes a reference to each.

    Or if I know I want the player to collect 10 specific items, I almost certainly have a list of those items somewhere. So when the player collects something I check to see if it's in that list.

    There are also cases where you need to know things such as "Is the vehicle which entered the zone the player's vehicle?" There are a few ways you could address that. One is to have a "Game Manager" which knows what GameObject the player is represented by, and you could ask it. Another is to have a component which tracks that, i.e. it is added when the player takes control and removed when they lose control, and you check if it's there. Another is to have a field in a component that says if it's player controlled, AI controlled, etc. The best way forward will depend on the game.

    Yeah, and it's beautiful. :)

    Where you say "interface message" it's either a function call or an event being raised. I don't use SendMessage or similar.

    Yep. This is the basis of how Unity works.
     
    spiney199 likes this.
  19. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    Awesome. I think I understand enough now that I can build some test to try out some of these ideas. I'll try out changing player control between a human and a vehicle, and both will be generic class, and with all of their behavior coming from components. That should confirm the basic idea. I guess then I might add some very different vehicles, like the boat, and also see at what point it becomes easier to define a new class or not.
     
  20. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    @angrypenguin

    I hope you don't mind a follow up question - and of course anybody's input is appreciated.

    But about a vehicle example using a composition approach, the question arises, how low level do I get in defining components?

    An engine component might be used to define an output of horsepower. This could be used by a car, motorboat, or rocket ship. Just outputs a float to define horsepower, and for further simulation you could have engine weight, type of fuel needed, etc. But let's only consider horsepower output for example.

    By itself, the only thing engine component does is turn on and off and make noise. It outputs the horsepower variable but if no component is listening for that then no bueno.

    Now then if we have a wheel component, that might listen for horsepower? And then maybe horsepower can get filtered through a curve to determine torque on the wheels. Just example to demonstrate composition, never mind actual physics sim.

    So, the chain of communication might go like this:

    W Input is pressed. Whoever is handling input sends message to player controlled gameobject (in this case our wheeled vehicle).

    First, engine ask is it on? If not, turns itself on. It begins updating its horsepower variable.

    Wheels component can also start reading the horsepower variable every frame. Then they can do whatever they want with it.

    Alternatively, maybe you are not doing any physics sim - just faking things like acceleration, braking, mass affecting turn speed, and so on. In a case like that, maybe wheels are just a data only class, not an entire component. They would only provide some info like, "can traverse offroad? max friction? likelihood to go flat? etc"

    If that is the case, then since there is no special behavior tied to wheels specifically, then the engine components horsepower variable would just be considered by a more general "wheeled vehicle" component.

    The boat, supposing it is still a generic gameobject and not it's own class, would have engine component, but it might have its own "boat vehicle" component which might intepret the engine horsepower it's own way. Or maybe the only difference is in adjusting the data parameters (top speed, turning speed, etc), and restricting surface it can travel on. in that case, do we need a difference between wheeled vehicle component and boat vehicle component?

    I suppose not, it could just be vehicle component. No sense duplicating a thing only to change the name. But in interest of futureproofing, might you go ahead and make the distinction for any reason?

    Is this example sensible?

    And in the case that you need to save persistent data associated with any prefab instance, then in that case you must have some class which holds onto the data?
     
    Last edited: Jun 29, 2022
  21. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    And here is one additional brainstorm:

    In interest of simplicity.... could we update components on a single "player controlled gameobject" on a state basis?

    Imagine we have all of our vehicles in the game world. When we click on them two things happen:
    1. Human static mesh dissappears
    2. Get info about the clicked object which can be used to select a data class, which informs what state to change to

    Rather than having a prefab for every vehicle type, this could be a data only class. When we click the vehicle gameobject, we could just read the name of the static mesh and that informs us what type it is. From that we can get our data class. FOr instance, we click gameobject and see that the assigned mesh is "car01", then we know that we should enter "Vehicle Control State." And we also know what specific vehicle is being controlled - "car01", so we could select the data object which would define how that vehicle handles. All of that discrimination could be handled just by a name convention. "WheeledVehicle_Car01". Just interpret that string and we know everything needed to get both our state and specific data values.

    Our state, on construction, might tell us which components to add to player controlled gameobject. And when a state is changed, first thing we do is wipe out previous components - maybe everything except camera for instance.

    So, we exit Human Control state, which means get rid of all components except the camera. Then we are in Vehicle Control state, which means add the Vehicle control component.

    This might have a few benefits... seems like less moving parts overall... we have state class which defines how input is handled (same as I've done before and I really liked this setup). We don't have to do anything special with camera system...

    I'll test it out of course but in my mind-map sketches this seems simpler overall I think.
     
  22. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,620
    Each object should have one clear role or responsibility. Often referred to as the "Single Responsibility Principle". It's debatable how much should be included in a "responsibility", it probably depends on the level of detail of your game.

    The components you described seem fundamentally sound to me, though. One "converts raw input to be useful for the vehicle", nice and simple. Another "controls how much force is fed to the wheels (and co)". That could well involve stuff like dividing the total engine horsepower over the number of powered wheels, and other engine-power-specific rules. And the other is the wheel itself, which... "approximately simulates a wheel, which can be powered or steered".

    They can all be well described in a sentence, and it gives a clear idea of what problem they solve. If you need more than a sentence, or if the sentence wouldn't be useful to someone unfamiliar with the project, that's where I'd think about splitting or combining things. But I wouldn't get carried away with getting that "right", it's just an ideal to aim for.

    Practically functional beats theoretically ideal.

    Gotta go, but the other thing worth keeping in mind is the "Principle of Least Surprise".
     
    TonyLi and BIGTIMEMASTER like this.
  23. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    Thanks so much!
     
  24. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,620
    I wouldn't use this kind of "magic string" approach. A few reasons.

    First of all, a typo can kill things, and there's no compiler to protect you. So this both makes things more error prone and sidesteps a mechanism which helps us avoid errors.

    Secondly, it's tying unrelated bits of data together. Or, put differently, it's forcing relationships to exist between bits of data which don't need it. Why does changing a name in Blender make a script I didn't touch start misbehaving? Oh. (See again: "Principle of Least Surprise".)

    Third, if/when you actually want to change it, you need to mess with 3D models or imported asset names or whatever.

    Fourth, there are just... better ways. Personally I'd consider having a data component with a variable in it which contains the type. That could be an enum or a System.Type, either of which can be populated from a drop-down list in the Inspector, which makes it easier to set the value while also ensuring only valid values can be entered.

    You could do this, but I'd just have the objects pre-created as a part of the prefab, and enable/disable them or similar. A couple of reasons. The more important one is that it allows me to just set up the component directly in the Inspector, either simplifying workflows, simplifying code, or both. The other is that it saves me from creating and destroying components repeatedly - which probably is negligible in this case, but is something I tend to minimise as a rule of thumb, to avoid poking the Garbage Collector.
     
    BIGTIMEMASTER likes this.
  25. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    @angrypenguin

    yes I completely agree about the magic string idea. Once i started actually poking at code I realized using an enumerator is going to be better. Im glad you've mentioned this Principle of Least Surprise - its one of those thigns I kind of had a notion about but not the words. That's the worst feeling, when you know there is this problem and it gives you anxiety but you can't quite pinpoint exactly what it is. Now I know the bastards name :).

    About the state setup - I think I understand what you mean. I was looking into that a little further and I acutally believe that in the case of this project idea I am playing with, we could do away with the states entirely and just switch input based on an enumerator. Because there would only be like two states total. In typical fashion, I started with a complicated idea but more I poke at it I am finding ways to scope it down in ways both to improve game design and game production.

    I am linking this thread in my project wiki - lot of valuable information here. Thanks again, it's hard to get people to talk about this sort of stuff because it takes a lot of thought and effort to consider - I really appreciate your input here. I better understand a lot of things plus I got some totally new ideas to work with as well. Definitely levelled up a bit.
     
    Last edited: Jul 1, 2022
    angrypenguin likes this.
  26. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    Here is some more wonderings.... I am having trouble understanding how composition approach may be better than inheritance in the following case:

    Suppose we are making an inventory/pickup system. Lots of items in the world you can pickup and then they will appear in your inventory. From there, based on what type of item it is, you can do different things with it.

    For example, pickup and apple or banana, then you can eat it. Pickup bullets or a grenade, you can kill things. Pickup hammer and nails, you can perform crafting, etc.

    I'll call these "pickup items."

    Ideally, we would like to define everything about our pickup items in a spreadsheet. There could be hundreds of them - it is not feasible to make a prefab for every single one, right?

    In our spreadsheet we can identify what mesh or sprite each item would use for graphic representation. We can define weight, rarity, etc.

    We also don't want to define what happens when you pickup an item more than once, right? We should have "pickup" logic in one place, and the only thing it will do it intake the item that's been picked up, destroy the graphic representation in the world, and cache some sort of pointer so that we can know, okay now the player has X item in their inventory. That pointer we can use to lookup the details we need from the spreadsheet, right?

    So I understand how with composition we might make a food entity. It can have a pickup component, an edible item component, a decomposition component, etc. But then from there, to define apple, banana, and an infinite number more of food items which differ only in data, you aren't making a prefab for every one, right?

    It would be better to have a base food class, and then it could, for example when an instance is spawned in the world, get a random name from a list, look that name up in our spreadsheet, and then get all the data which includes what sprite/mesh it uses, how much it weighs, how much calroies, etc etc.

    So in that case i can see how using a parent class (or actually, probably justt a single class for all food) to quickly derive many instances from reading a spreadsheet could make sense. But the composition approach, unless I misunderstand how its meant to work, would be massively more times work to setup?
     
  27. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,696
    As the author of the Dialogue System for Unity, it blows my mind when someone still tries to use a spreadsheet for branching dialogue like they just Back To The Future'd from 1980.

    But for large tables of data, I love spreadsheets. You can compare rows (e.g., sort by weight or damage), make mass changes (e.g., increase the price of everything by 20%), run formulae to adjust game balance, etc. -- all things you can't do with a collection of ScriptableObject assets unless you go to the effort of making a custom editor that reinvents the functionality of a spreadsheet.

    You can write an importer to read the spreadsheet and set up prefabs. Composition is no extra work. If the spreadsheet says the row (item) is edible with various edible stats, the importer can just add an Edible component.

    I think you're asking how far to decompose objects. Like, do you need subclasses or components for different fruit (no), or can you define each fruit's traits in a single Edible component (yes). For the fruit example, there's no benefit to decompose further than Edible.

    Here's an example where you would want to split off a trait into its own component instead of including it in Edible or making a subclass of Edible. Say a witch poisons an apple. You could define a PoisonEdible subclass (bad) or add an "is poisonous" trait to the Ediible class (also bad). PoisonEdible is bad for at least a few reasons: what if the apple is fine but is later poisoned? what if you want to make something else poisonous, like an assassin's dagger? That last reason also explains why you wouldn't want to add an "is poisonous" trait to Edible; in this case, you'd be repeating yourself by defining the poisonous trait in edibles and in weapons. Also, why carry around an "is poisonous" trait for edibles that aren't poisonous? It's just asking for trouble (bugs) later on. So poison is a good candidate to make a separate component.
     
    BIGTIMEMASTER likes this.
  28. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,620
    I don't see why not. Either way someone is entering equivalent data. I definitely agree that an external table has strengths, and slightly faster data entry can be one, but I don't think it's really the big one. (And you could get that with a little scripting in Unity anyway.) To me the big one would be enabling tweaking / updating data without needing to rebuild anything.

    I wouldn't use composition just to give an apple and a banana different names and sprites. I'd definitely have that in a parent class or a component shared by all pickups or something along those lines.

    But what happens when the player eats an apple or a banana? If we take Zelda: BOTW as an example, every type of food has potentially different effects. That I'd definitely consider using components for. My one Apple prefab (of which every Apple in the world is an instance) can have a HealEffect component with value = 4. My one Banana prefab can have a HealEffect component with value = 1, and also have a StrengthEffect component with value = 2.

    And yeah, if there's hundreds of types of food in my game, someone has to set up hundreds of food items and their effects, whether that's in a spreadsheet or a list of prefabs or coding them straight into C# (uhh... don't do that) or whatever.

    One thing to note is that you can have the best of both. You can have a PickupTypes table which has the name, sprite, and other properties common to all pickups, with a unique ID for each. Then you can have a separate PickupEffects table which can have multiple rows with a given PickupType's ID, giving it more than one effect. This is how databases work*.

    For the PickupEffect I'd almost certainly use inheritance. The base PickupEffect class would just have a name, value and duration, then some virtual methods for things such as OnConsumed, OnExpired, etc. Then when I write the derived classes HealhEffect I just need to implement OnConsumed to give the consumer more health, and when I write StrengthEffect I implement OnConsumed to add strength and OnExpired to set it back.

    Tangential tip: If you're going to localise your game into different languages then whether it's prefabs or tables or whatever, you don't want to put the names, descriptions, or other text for the player straight in the object data. You want that to go in a separate table where it's reference by ID so that your game can just load a different language table and do its thing.

    * Though of course databases super optimised for this specific type of data lookup. They're not just doing for loops looking for matching IDs.
     
    BIGTIMEMASTER likes this.
  29. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,620
    Actually, I overlooked a big one. If you're doing your game balancing in a spreadsheet via formulas or similar, then having all of your items in there means you can just re-export them into your game when you've made balance changes.
     
  30. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    @TonyLi , @angrypenguin

    thanks a lot guys. lots to great advice to consider.

    Localization was something i wondered about because my initial efforts - trying to get the simplest prototype imaginable - was just defining every item by a string so then i can store entire inventory as a string array and lookup items in table with same string. Probably not the best way but I needed to get something simple going cause I was stuck in "trying to conceptualize big complicated system" hell for too long.

    I'm going to repeat back some stuff in my own words to make sure I understand:

    So, with the Poison component - what does it actually get attached to? Suppose throughout the world there are container gameobjects. At game start it had 100% chance to spawn 10 apples. There isnt actually a reference to 10 distinct gameobjects there, right? Just an ID which is used to pull data (like sprite for display in the UI) from the data table.

    Next, player might transfer five apples from that container to their own personal container. There you just get a string (or whatever ID you use) remove it from one container, add to another. And if there is multiples involved perhaps you have an associated int. For displaying in the new container, again you just use the ID to lookup in the data table.

    I think that is the basics of passing the data around and displaying it?

    Say the player wants to poison one apple. Let's say the way to do that is right click UI icon in the personal container of the apples, and it's in a context dropdown menu. You select "poison 1 apple."

    Now for the players eyes, one apple is removed from the apples stack and a new "poisioned apple" appears in another stack.

    Later somebody will eat the apple. How do we get that Poisoned component involved? My understanding is that components live only on game objects. Is that incorrect? Without a flag in the data table, how do we determine that apple is poisoned? When and where would that poisioned component come into play?

    I think this kind of segues into angrypenguins example as well. So, say you have a special type of apple called BIG APPLE. And it makes you fly. For that obviously we want a FlyingMovement component. Other than that, it's same as as regular apple.

    So you would make a new prefab derived from the base Edibles class, and it would just share same components as apple except adding the FlyingMovement component.

    And then in the data table, you have a pointer to the prefab that should be instantiated if you dropped from container onto ground? Or just a pointer to components?

    I guess the fundamental question is, what data are you holding in the data table exactly? Should it point to components or classes?
    If player eats an apple and the apples GiveStrength component should follow from the parent classes OnConsumed method, how are you getting to that component? Do you spawn an apple prefab which hold those components so that you can access them?

    I think that may be my biggest concern. Since I am a solo developer, I dont want to create a system where in order to make game balances changes, I have a great checklist of caveats that I have to jump through. Ideally, I just edit a spreadsheet and reimport it, and then the game is updated.
     
  31. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    At what point does that become serious consideration?

    100's or items in an array? Thousands? Millions?
     
  32. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,696
    You can separate data from its visual representation. You don't need MonoBehaviours or GameObjects for the data side. On the data side, each type of item can be defined by a set of traits: Edible, Poison, etc. So a normal apple would have [Edible], but a poison apple would have [Edible, Poison]. Only stack items if they have the same set of traits.
     
    angrypenguin and BIGTIMEMASTER like this.
  33. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,620
    That level of optimisation becomes relevant when you're doing big data stuff. For the scale of data in video games using sorted lists and well known algorithms will easily get you where your performance needs to be, and are things you can refactor in later on if need be.

    In some table, you store the info used to populate the items and/or their components when you create them. E.g. there is an item type called "Apple" and it has this sprite and these effects and this description and so on. When the game spawns an item, it can find valid types via a table which defines them, and also info about what values to set on the spawned item. The latter could include adding extra components to it.

    You might also have other tables. For example, if it's relevant to your game you might have a table of objects spawned into the world, so that if the player poisons an Apple the game can keep track of it. But there are other ways to do that, too.
     
    BIGTIMEMASTER likes this.
  34. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    ah gotcha.

    so you might define a list of traits in the table, and from that you'd know what components to attach to a spawned gameobject if you needed to spawn one for that item.

    In that case then, you don't need to make separate prefabs for every item, right? Everything you need to know is right in the table, if i understand correctly.

    I like that idea a lot. It basically means you could have just a single "pickup items" class, and then using only the data table, you can define what components to attach to any spawned objects from the table.

    Or rather, you might have all of the components attached to the pickup class, and the listed traits in the table jsut tell you which ones to activate. So you can basically have a magic box type class that can become anything, you would just flag what behaviors you want from it?
     
    TonyLi likes this.
  35. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,894
    I'd personally just use scriptable objects for this sort of data stuff. It lives in Unity, is designer friendly, and there's a million and one tools and ways to comfortably edit large numbers of them. You can of course generate these from your EVE Online-eqsue spreadsheets if you so choose.

    Not to be rude, or a stick in the mud, but how much actual coding/designing have you done compared to the amount of discussion here? No better teacher than experience, and I pretty much learned what I know from diving head-first into it.
     
    BIGTIMEMASTER likes this.
  36. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    I wrote down review of everything I've done so far here: Project Alaska: Week 1 in Review - Project Alaska - GameDev.net

    But I am prototyping this in unreal, so here on the unity forums I am sticking to general programming concepts only. But I am interested in how people do this work in unity, because I think for certain stuff (like data management here) unity has an advantage.

    In general, if i am asking a question, its after i spent at least a few hours trying things and researching, but I'm still confused. People are free to answer or not, of course. I'd like to think people taking time to answer do so because they know i get work done.
     
    angrypenguin likes this.
  37. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,620
    Experience is accelerated quite a bit when it's augmented with quality advice. ;)

    Plus, while we all do have to self-teach constantly in this profession, it comes with risks - particularly early on. I've seen more than one person put significant amounts of practice into fundamentally misguided approaches purely because they didn't know what they didn't know, and didn't have anyone around to make things easier for them. I suggest looking up "The Four Stages of Competence".
     
    BIGTIMEMASTER likes this.
  38. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    yeah when i was younger i made a lot of effort to be self-sufficient. But i also wasted all my youth in the military and came out with no marketable skills. now i just try to get from A to B in the most efficient way possible. So I take as much help as I can get. There is too much to learn anyway, I can't possibly remember it all. best I can do is write things down and try to focus on principles, not details.

    also reason i stick to forums and not discord is becuase it's searchable for the future. if somebody ask a question i can answer i really dont care if it helps the individual appreciates or uses it - it's something there that somebody is going to find when they need it at some point
     
    Last edited: Jul 19, 2022
    TonyLi and angrypenguin like this.