Search Unity

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

Code Structure: Simple, Flexible, Flat Approach

Discussion in 'Scripting' started by Rick Love, Jul 7, 2015.

  1. Rick Love

    Rick Love

    Joined:
    Oct 23, 2014
    Posts:
    76
    When I first came to Unity, I tried to use Code Patterns from business software (MVVM, etc.) and quickly found that I was fighting Unity.

    Unity has a powerful component based design that focuses on game object behavior.

    I have discovered the following principles:
    • Single Script -> Single Purpose
    • Just make a New Script for the specific purpose and keep the script simple.
    • Don't modify an existing script to work in a different context
    • Don't design a script to work in multiple contexts
    • Never use inheritance with game scripts

    This covers 80% of cases. However, what about the cases when there is a conflict or interaction between multiple scripts.

    Examples:

    • A character status that prevents the character from jumping
    • Getting points for various game events that should be given to specific players
    • Networking in general
    • Customizing objects after instantiation

    So, how do we control script dependency in Unity while still relying on Unity's design for the simple cases?

    My Idea

    First of all, my idea is driven by a single design principle:

    Scripts should never reference each other, however they can reference a common script built for the purpose of coordination.

    With that in mind, every script should define two types of state:
    • Script State
    • Shared State
    Script state are all the fields/properties of that script class and work as Unity intended being set in the editor, etc.

    Shared state on the other hand is data that must be shared among multiple components. It is defined as the fields/properties on the shared component.

    Every data should start as script state and should only move up to shared state when necessary.

    The scripts for a specific type of game object would reference only the specific game objects shared type. So each type of game object would have its own Shared component.

    If reusable code is helpful, it should be implemented in static methods of helper classes where the relevant values are passed as parameters.

    Summary
    • Each type of game object would have a Data component, i.e. EnemyData
    • Each type of game object would have multiple behaviours that might use that data component, i.e. MoveEnemy would use EnemyData (There could be a helper class that would provide a direct reference to the data class, i.e. class MoveEnemy : MonoBehaviourWithData<EnemyData>)
    • Static methods in helper classes would provide any common logic (reusable code).

    Advantages

    The primary advantage of this is that it is simple to implement and provides a specific path from Unity's default pattern to a more organized approach.

    Another major point is that it does not create an abstraction away from specific game objects. Layers of abstraction are the root of all evil code complexity. In this model, the only shared code is the code in specific helper methods that have access only to the data passed to them via parameters.


    Have you used this pattern before?
    What pros and cons can you see with it?
    How would you improve it?
    What alternative would you use?
     
    luochuanyuewu likes this.
  2. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    I just keep tight control on my dependencies, rather then eliminating dependencies altogether. Dependencies are kept strictly one way. I also build my code up in systems. Systems are generally aware of and dependent on themselves. Each system has only a couple of clearly defined entrance and exit points. System dependence is also managed one way.

    Your data class strikes me as a global variable, with all of the attendant evils. Sure, it's only global to a GameObject. But letting every system into the same collection point seems to be messy.

    As a disclaimer my coding experiance was originally built up in process automation for chemical plants. It's a fairly different world. I've dragged across a some of my experiance from there, and built the rest inside of Unity.

    I'm also curious about your anti inheritance stance, and your anti reuse stance. I tend to use inheritance a lot, mainly a single level. It makes swapping components out, and therefore changing behaviour, very simple. Especially if your scripts really are built for a single job.
     
    landon912 likes this.
  3. 420BlazeIt

    420BlazeIt

    Joined:
    Aug 14, 2014
    Posts:
    102
    I never though of this before. Good idea! :D
     
  4. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,848
    This strikes me as a possible sticking point. The very notion of a "type of game object" is a bit antithetical to Unity's component-driven approach. That is, ideally, you don't have types of game objects; you just have a bunch of game objects with various combinations of behaviors.

    As soon as you define an "object type" with a certain set of shared data, and various components that work with that shared data, you've effectively coupled all those components together. Yes, it's a fairly controlled coupling, and so better than direct references... but even so.

    I wonder if a better solution might be had via Unity's new messaging system. Let's take the example of an effect that prevents jumping. You could define a CanJumpProvider interface, with a "CanJump" method that sets/clears some flag in the event data. The jumper component would invoke this event on itself — indirectly calling any components on the same game object that provide a CanJump method — and then inspect the data to decide whether to do the jump.

    This requires rather uncomfortable gyrations though, since the messaging system doesn't allow for returning a result (except via the event data parameter). An alternative would be to essentially do it yourself: define a CanJumpProvider interface whose CanJump method simply returns a bool. Then in Start you would use GetComponents to iterate over all components, find those that implement the interface, and tuck them away so you can invoke them later.

    Of course the practical problem with all of these is that it makes the incremental cost of adding a new bit of functionality (where you have to declare the interface, implement it, find instances of it, etc.) quite high. All that overhead may actually not be worth it in some cases.

    On the other hand, I've seen Unity projects with over 6500 separate uses of some singleton, creating a massive intertwined mess of spaghetti that was extremely difficult to modify or extend. So anything that reduces coupling is worth it in my book!
     
  5. Rick Love

    Rick Love

    Joined:
    Oct 23, 2014
    Posts:
    76
    Inheritance is good for systems and truly abstract classes (like collections, etc.).

    However, it is not good for defining behavior of unique objects types:

    Example: The player is a ship and the enemy is a ship, they can both be destroyed, they can both move, then can both be hit by asteroids, etc.

    However, their uniqueness is their defining points not their similarities. The lines of code that are similar are simple and very few. But the lines of code that are unique are many.

    Inheritance would make me spend most my time trying to segregate their similarity from their uniqueness, create problems in execution order (where the similar code should execute mixed in with the unique code), and at the end of the day I would have saved time just typing the same 3 common lines in two different places.

    It is better to start with completely independent classes. If I identify code that is common between the two classes, has a simple abstraction, and is long enough to warrant worrying about, then I can make a helper class with a static method that contains that code and call it from both unique scripts.

    Inheritance on the other hand creates a permanent coupling that leads to increasingly more complex interactions where changes to the base class cause undesirable affects.

    Again, if something has a pure solution (definite best solution) that will never change, inheritance is fine. However, if something ever might change, inheritance will introduce a layer of abstraction that always needs to be handled and will often cause more work because of side effects.

    MonoBehaviourWithData<DataComponent> is an example of a base class that provides a specific optimization without any real logic in makes a good case for inheritance. I could use it to cache references to the data component object, cache references to other components, etc. None of this effects behavior and so the inheritance hierarchy cannot cause side effects.

    It's hard to define this, but I hope that makes sense.
     
  6. Rick Love

    Rick Love

    Joined:
    Oct 23, 2014
    Posts:
    76
    The messaging system is an interesting option. I can see it being useful for some cases, but I wonder how it works with partially initialized objects.

    Example problem:

    When I instantiate an asteroid, I want it to have a random position off the screen, unless it was instantiated when a larger asteroid was broken and in that case it should keep its assigned position. Also, the asteroid should have a random velocity and random angular velocity.

    Question:

    Who randomizes the asteroid's position (before the first frame of the asteroid)?
    Who randomizes the asteroids velocity?

    Option 1:

    The object that instantiates the asteroid should randomize its position.
    The MoveAsteroid component randomizes only its velocity in the start method.

    (The asteroid's position and velocity are both randomized in completely separate places.)

    Option 2:

    Create a AsteroidHelper.CreateAsteroid() set of methods. These methods can have different overloads to randomize the position or velocity as needed immediately after instantiating the Asteroid.

    Option 3:

    The MoveAsteroid component randomizes its own position and velocity in its start method. It has a shouldRandomizePosition field that is used by the start method to control whether to randomize the position or not.

    Whatever instantiates it, must get a reference to the MoveAsteroid component and set the shouldRandomizePosition field.

    Option 4:

    The MoveAsteroid componentrandomzies its own position and velocity in the start method. It uses a AsteroidData component to check the shouldRandomizePosition field.

    Whatever instantiates it, must get a reference to the AsteroidData component and set the shouldRandomizePosition field.


    After typing this out, I see that for this specific problem Option 2 (using a static method in a helper class) causes the cleanest dependency chain.

    I don't believe the messaging system is an option in this case because I do not know that it will be executed before the first frame. In addition, having a dependency on a single use interface is not any better than having a dependency on the component itself and just creates more boilerplate code.

    I'll try to think of some other problems to examine.


    Update: I thought of another way to do this.

    Option 5: MicroComponents

    Create a bunch of tiny reusable components:

    - TranslateWithVelocity : MonoBehaviorWithData<IHaveVelocity>
    - RotateWithAngularVelocity : MonoBehaviorWithData<IHaveAngularVelocity>
    - RandomizeStartPosition (has fields to determine range of values set in the editor)
    - RandomizeStartVelocity : MonoBehaviorWithData<IHaveVelocity>
    - RandomizeStartAngularVelocity : MonoBehaviorWithData<IHaveAngularVelocity>

    With a data component that is composed of the neccessary interfaces for this type of object.

    - AsteroidData: MonoBehavior, IHaveVelocity, IHaveAngularVelocity

    And the base class that provides a reference to this as an interface:

    MonoBehaviourWithData<T> with cached reference to T data

    (This MonoBehaviourWithData class could display an error in the editor if no data component were present in the game object that has the required interfaces.)

    Now, the object that instantiates the asteroid could add (or remove) the RandomizeStartPosition component without knowing anything about the object.


    Althought this Option 5 could create many reusable components, each one only represents a single line of code. However, for more complex game logic this could be a useful option.
     
    Last edited: Jul 8, 2015
  7. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,848
    In a case like that, I've always been happy letting whatever instantiated the object decide how it should be initialized. It's usually the one that "knows" the initial context, and so is in a position to make that decision.

    So if a "fragment" asteroid (resulting from blowing up a bigger one) is in any way different from an "intact" asteroid, then the creator should actually instantiate a completely different prefab. Or, if they're exactly the same except for their initial position, then the creator should set the position explicitly. In neither case should the Start method of any component on either kind of asteroid be mucking with the transform position — it should assume that's already been set.

    (Incidentally, you can indeed depend on messages being dispatched before the next frame. There's nothing asynchronous or deferred about them.)

    In other contexts, I often use static factory functions in a class to allow instantiators greater (but strictly limited) control over how objects are created. In Unity, that goes a bit against the grain, because in most cases you shouldn't be instantiating individual components; you should be instantiating prefabs. But occasionally it's worth having a helper/manager class whose job it is to help instantiate certain types of prefabs, which then boils down to your Option 2. It's still up to the instantiator to decide which factory method to call, and where the new object should be created (perhaps by passing parameters to the factory methods). But if the logic is nontrivial and we're doing it in multiple places, the helper class can wrap up that redundant code so each such place becomes a one-liner.
     
  8. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    You are missing the power of component based design. You would never build a component for a playership or a component for the enemyship. That is way to many responsibilities for a single component.

    Instead you would build a destructible component, a collision component, a propulsion component and an input component. You can use inheritance then to help define each component tree. You might have an abstract input class, and a derived class for PlayerInput and one for AIInput. You could do the same if required for your propulsion component for different types of engines. The key is to keep all of the dependencies at the top level of this structure.