Search Unity

Scripting component correct usage?

Discussion in 'Scripting' started by Talthilas, May 29, 2015.

  1. Talthilas

    Talthilas

    Joined:
    Apr 1, 2014
    Posts:
    44
    Hi,

    I have a general question about building a gameobject (say that represents a "Mage" unit) and how to properly implement script components in unity to represent that mage unit. Everything is better described with an example, so let me use one here.

    Lets say i have a base class 'BaseUnit'. BaseUnit has a few public fields like health, combat rating, defence rating etc. Lets say I create a derived class 'Mage', which inherits for BaseUnit but as a few more fields like mana, spell power etc. This is the general pattern I am used to when creating classes. You start at the most basic level that is common to all units, then you start creating unique classes that derive from a base class(es) and build them up further. Inheritance is a common programming technique as we all know, and there are many examples of it in the unity documentation. Now enter script components.....

    Looking through the unity tutorials and examples over my time working with unity, I notice that i never see a 'Mage' (or similar) script being added directly as a component to a game object. Instead I see scripts like health (that store and modify health) and the like added to game objects as separate script components, eventually compounding to give you a complete 'Mage' game object. Essentially my take on why they do it this way, is to reduce duplicate code, and have a sort of generic script that you can attach to anything to give you a fixed functionality. That sounds great to me, but it also confuses me into where my Mage class comes into play. Why have a mage class at all? If you are going to break down the Mage class into a bunch of script components that store and modify different values relating to the mage, then creating a Mage class seems a bit redundant, or at least, I'm not sure how to use it.

    The main benefit I could see with having a mage class (or similar) comes down to persistence (saving to file). I would envisage having a list of List<Mage>in the scene, that keeps track of all the mages. I could then save/load this list for persistence purposes. However if I am storing all my mage data in separate script components on a gameobject (like the health script for example), rather than in a central mage class/script, then i would essentially have to search through every script component, and pull out the data I want to save from each one. Would it not be easier just to store all my Mage relevant data (health, mana etc) in fields in the Mage script, rather than as fields in separate script components on a gameobject?

    Any help would be appreciated. Thanks.

    Aaron.
     
  2. Jodon

    Jodon

    Joined:
    Sep 12, 2010
    Posts:
    434
    You've got a good understanding so far. It look me a while to move from standard OOP hierarchies to Components. Components are much better in my opinion. You're right, you don't have a "Mage" class (or at least you don't need one). Instead, you have things like a Health component and a Mana component and behaviours that hook up to those components to do special things like cast spells and perhaps take damage on impacts.

    And then you could have a List<Mage> if you want to for some reasons, but there's always a way around it (which is always the correct way, I've found). Let's think about saving data. Well we've already decided a Mage doesn't exist, rather it's the face that a Mage has Health and Mana and can cast spells, so why can't a Mage also be Saveable? Why is it just Mages who get to be Saveable? Why can't other things in your scene also get to be Saveable? And then the logic continues... Saveable is some type of Component, or system, or something that isn't tied to the concept of Mage but its own concept.

    It's probably a good exercise to think through that on your own, but some people have already done it for you: https://www.assetstore.unity3d.com/en/#!/content/3675. Basically, you mark-up your classes, telling the save system which fields to save just like you do for Unity.
     
    Talthilas likes this.
  3. Talthilas

    Talthilas

    Joined:
    Apr 1, 2014
    Posts:
    44
    Hi Jodon,

    Thanks for your reply. This is where i get confused. The unity documentation shows both the use of inherited classes to build a something like a mage class, and also heavy use of separate component scripts, which store and modify separate fields of mage data. They both seem to work contradictory to each other. Having a mage class centralizes all related mage data, while separate component scripts break that data up into loosely related scripts?

    In terms of saving data. You say "mark-up your classes", but if i don't have a mage class, then what class am I marking up exactly? The only two things i can think of to do without a mage class, is to serialize the 'mage' gameobject itself, along with its components, or to retrieve all the relevant mage data from each field, in each component, on each mage gameobject, then save it to disk in some sort of organized fashion... which is what a mage class is for.

    I tend to favour just saving the relevant mage data fields (such has health points, mana points etc) for each mage gameobject, and rebuilding based on this data when i load a game, rather than serialize all the mage gameobjects themselves. Serialization is slow and inefficient (in terms of disk space) for large amounts of data in my experience.
     
  4. Fajlworks

    Fajlworks

    Joined:
    Sep 8, 2014
    Posts:
    344
    You're on the right track, that is the ideal approach in Unity when dealing with classes; creating small compound scripts that will eventually make up a Mage (GameObject) in your case.

    I agree that it might look counterintuitive having all those small scripts interact with each other (hence breaking modularity in a bigger scope), but I think it's all about how you design your scripts. Instead of a Mage class I would go and create a simple script to contain all basic info about a character, a CharacterSheet script.

    Please refer to the 3rd post of this thread where I provide a more detailed usage:
    http://forum.unity3d.com/threads/scripting-an-rpg-like.328643/#post-2130880

    As for serializing data, that should be no problem! Since CharacterSheet acts as your centralised script for your GameObject, you can create an interface:

    Code (CSharp):
    1. public interface ISheetSerializable
    2. {
    3.      JSONObject Serialize();
    4.      MonoBehaviour Deserialize(JSONObject json);
    5. }
    You can get JSONObject from this link:
    https://www.assetstore.unity3d.com/en/#!/content/710

    Idea is you add that interface to all components you want to serialize. Do note you have to implement what gets serialized/deserialized for each class manually.

    Now, in Unity 5.x you can call:
    Code (CSharp):
    1. public class CharacterSheet
    2. {
    3.      // rest of code is in another thread
    4.  
    5.      public string Save()
    6.      {
    7.           JSONObject json = new JSONObject();
    8.           json.AddField( "strength", strength );
    9.           //etc... stats go here
    10.  
    11.           ISheetSerializable array = GetComponentsInChildren<ISheetSerializable>();
    12.           for( int i = 0; i < array.Length; i++)
    13.           {
    14.                ISheetSerializable comp = array[i];
    15.                MonoBehaviour script = comp as MonoBehaviour;
    16.                if (script == null)
    17.                     continue;
    18.  
    19.                json.AddField("compName_"+i, script.GetType().Name );
    20.                json.AddField("compData_"+i, comp.Serialize() );
    21.           }
    22.           json.AddField("count", array.Length);
    23.  
    24.           return json.str;
    25.      }
    26.  
    27.      public void Load(string rawJson)
    28.      {
    29.             JSONObject json = new JSONObject( rawJson );
    30.  
    31.             strength = json.GetInt("strength");
    32.             // etc... other stats
    33.  
    34.             int count = json.GetInt("count");
    35.             for (int i = 0; i < count; i++)
    36.             {
    37.                  string scriptName =  json.GetField("compName_"+i).str;
    38.  
    39.                  MonoBehaviour script = gameObject.AddComponentX( scriptName );
    40.                  ISheetSerializable comp = script as ISheetSerializable;
    41.                  if ( comp != null )
    42.                       comp.Deserialize( json.GetField("compData_"+i) );
    43.             }
    44.      }
    45. }
    46.  
    47.  
    48. // extra helper functions below
    49. public static class JSONObjectExtras
    50. {
    51.     public static int GetInt(this JSONObject jsonObj, string name)
    52.     {
    53.         return jsonObj.GetInt(name, 0);
    54.     }
    55.  
    56.     public static int GetInt(this JSONObject jsonObj, string name, int fallback)
    57.     {
    58.         int result = 0;
    59.         jsonObj.GetField(ref result, name, fallback);
    60.  
    61.         return result;
    62.     }
    63. }
    64.  
    65. // solution posted by Unity forum member NA-RA-KU
    66. public static class GameObjectExtras
    67. {
    68.     public static MonoBehaviour AddComponentX(this GameObject obj, string className)
    69.     {
    70.  
    71.         //We need to fetch the Type
    72.         System.Type MyScriptType = System.Type.GetType (className + ",Assembly-CSharp");
    73.         //Now that we have the Type we can use it to Add Component
    74.         return obj.AddComponent(MyScriptType) as MonoBehaviour;
    75.     }
    76. }
    Great thing about this approach is that CharacterSheet does not contain any game logic, and can be used to describe both players and enemies. You could potentially save players into json, upload their data to server online and download when you want to create AI controlled enemy by populating it with that players data; imagine something like DarkSouls when other player invades your game.

    Also when designing scripts always assume both player and enemies will use them. You can always utilize interfaces when dealing with CharacterController / AIController scripts. I would recommend NOT to create BaseController and then let CharacterController and AIController inherit from it, since their implementation is in most cases totally different.

    Sorry for the wall of text!! :) It takes a while to get out of the OOP context when working in Unity, but you certainly took the right approach. Like they say: "When in Rome, do as the Romans do". Good luck!

    EDIT: Fixed typo in code.
     
    Last edited: May 29, 2015
    Talthilas and Deleted User like this.
  5. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,338
    WARNING: WALL OF TEXT

    @porters, this isn't about what's wrong or right, it's more about design philosophy.

    You could make a game where GameObjects generally have a single script attached to it, and use a combination of deep hireachies and interfaces to share behaviour.

    Or, you could have a game where GameObjects have several scripts on them, each controlling an aspect of how the object behaves.

    Note that the second way of doing things is not in any way opposed to normal OOP practices. You'll just generally end up with shallower hireachies. Take something like a health script. You'd add a Health script to anything that needs to have health, and then give it methods like "TakeDamage" and whatever - and things like weapons would check the stuff they hit for instances of the health script and apply damage if they found it.

    BUT, you would still have a bunch of subclasses of the Health script. Say you were creating StarCraft. Both the Zerg, Protoss and Terran units have somewhat different health mechanics - the zerg regenerate life slowly, the protoss have half their health in shields that's recharged quickly out of combat, etc. So in that case, you would have a ZergHealth, a ProtossHealth and a TerranHealth script that shared a lot of common behaviour.

    In any game where you have a single player character and a bunch of enemies, your character can have a special health script that sends messages about it's current health to the GUI, and so on.


    You can do that, or you could do a more classic OOP approach, where you have your Mage class that inherits from a MagicUser class and so on. I find that it's often easier to have a complete overview of how things work if there's a single script controlling enemies. There's a downside, though:

    Imagine that you're making StarCraft again. You want to make the Zerg Queen unit. If you're not that familiar with the goddamned classic that is Brood War (shame on you), I'll remind you that this is a flying spellcaster.

    Now, being a flying unit is definitely different from being a walking unit - how the unit navigates the world is distinctly different from how other units navigate the world. So, you'll want to make a FlyingUnit subclass of Unit.

    On the other hand, a spellcaster unity is distinctly different from a normal Unit - it has spells, and energy that it uses to to cast those spells. So you'll want a SpellCasterUnit subclass of Unit.

    Now you're in a deep problem. Since there's both spell casters that walk and don't walk, and flyers that can and can't cast spells, there's not really a good way to build this hireachy. You'll either have to solve this issue with a single, linear hireachy. If you had access to multiple inheritance, this would be easy to solve, but you're out of luck here (for a pretty good reason).

    This is where a component-based design comes in handy. If you have movement and abilities in seperate scripts, you can select what kind of Movement script and what kind of Ability script you want your Queen to use. This is, by the way, not something that's unique to Unity. Any application written in an OO language that doesn't support multiple inheritance (Java*, C#) have to use a component-like architecture if a class needs behaviour from two other classes.

    Finally, note that your component scripts doesn't actually have to be MonoBehaviours. I've got a set of patroller classes that other scripts instantiates, and asks for coordinates from. That's kind of a third way of solving this kind of issue that's not discussed much.


    * Java introduced "default methods" to interfaces in Java 8, which somewhat helps with issues like this.
     
    Talthilas, eisenpony, Suddoha and 3 others like this.
  6. Deleted User

    Deleted User

    Guest

    At the end of the day, even small scripts are objects. An Object is just something that can perform a set of methods. So even though we might call Health and Mana "components", they are still objects.
     
  7. Talthilas

    Talthilas

    Joined:
    Apr 1, 2014
    Posts:
    44
    Thanks for the input guys. I've decided to just use script components for health, inventory etc. and move away from derived classes somewhat. In terms of persistence, I was thinking of just creating a structure for units such as mage etc, that would store any relevant info. I would then create a list of these structures and serialize/deserialize the list and rebuild by mage gameobjects from this data. That way I don't need to serialize the game objects or their components, just the relevant data to the unit.