Search Unity

How to simplify checking which script on objects?

Discussion in 'Scripting' started by Xhitman, Jan 15, 2019.

  1. Xhitman

    Xhitman

    Joined:
    Oct 30, 2015
    Posts:
    452
    There are two type units in my RTS game:
    Robot unit use "RobotAI" script and "RobotData" Class to store data, such as attack, defence
    NonRobot unit use "NonRobotAI" script and "NonRobotData" Class.

    You can see in the attached code, I need to check very often the type of unit to decide what to access.
    Is there any way to simplify?

    Code (CSharp):
    1.  // Call when load unit to scene
    2.     public static void ArmorMove(Transform unit)
    3.     {
    4.         float multiplier = 1;
    5.  
    6.         // Armor
    7.         Trait total = new Trait();
    8.         List<TraitName> _traitList = unit.GetComponent<RobotAI>() ? unit.GetComponent<RobotAI>().data.traits : unit.GetComponent<NonRobotUnitAI>().data.traits;    
    9.         foreach (TraitName tn in _traitList)
    10.         {
    11.             Trait t = new Trait();
    12.             t = traitList.Find(x => x.traitName == tn);
    13.             multiplier += t.armor;
    14.         }
    15.         if (unit.GetComponent<RobotAI>())
    16.             unit.GetComponent<RobotAI>().data.armorMax *= multiplier;
    17.         else if(unit.GetComponent<NonRobotUnitAI>())
    18.             unit.GetComponent<NonRobotUnitAI>().data.armorMax *= multiplier;      
    19.  
    20.         // Move Speed
    21.         multiplier = 1;
    22.         foreach (TraitName tn in _traitList)
    23.         {
    24.             Trait t = new Trait();
    25.             t = traitList.Find(x => x.traitName == tn);
    26.             multiplier += t.moveSpeed;
    27.         }
    28.         unit.GetComponent<NavMeshAgent>().speed *= multiplier;      
    29.     }
     
  2. romatallinn

    romatallinn

    Joined:
    Dec 26, 2015
    Posts:
    161
    Use Inheritance

    Both RobotAI and NonRobotAI share some similar data structures. I can see that they both have ".data.armorMax" as well as ".data.traits". So why not to extract this property (probably the entire data thing) into the super class?

    It will allow you just to retrieve this one super class and all its data no matter what the actual type of AI it is.

    So at the end you will be able to do this:

    Code (CSharp):
    1. BasicAI aiComponent = unit.GetComponent<BasicAI>(); // This will retrieve the stuff that BasicAI defines from both RobotAI and NonRobotAi, if they both inherit it.
    2.  
    3. // ...
    4.  
    5. if (aiComponent) aiComponent.data.armorMax *= multiplier;
    6.  
    So no need to differentiate between the classes. Moreover, it will work if you will add another type of AI, and no need in code rewriting, but only as long as it inherits BasicAI. Make it GodAI, for example.

    There are a lot of vids about the topic. For example, THIS.

    NB! It is a better practice to retrieve the component only once and then store it locally, because GetComponent<T>() is a function that takes unnecessary power/time when you call it many times within the same place/script. You can see an example of it in the code snippet above.
     
    Last edited: Jan 16, 2019
  3. Xhitman

    Xhitman

    Joined:
    Oct 30, 2015
    Posts:
    452
    Is it?

    RobotAI : UnitAI
    NonRobotAI : UnitAI
    UnitAI : MonoBehaviour

    I need to rewrite everything?

    But the reason I have two script is robotAI code much more than NonRobotAI, 4 to 5 times, and their behavior very different.
    If I have a general UnitAI script for them. The UnitAI code will become very large and waste lot of memory for useless variables?
     
    Last edited: Jan 16, 2019
  4. Munchy2007

    Munchy2007

    Joined:
    Jun 16, 2013
    Posts:
    1,735
    Last edited: Jan 16, 2019
  5. daxiongmao

    daxiongmao

    Joined:
    Feb 2, 2016
    Posts:
    412
    First thing is that you should only have to get your components maximum once per function call. Right now you are calling it 3-4 times for each. Just call them one time in the function and reuse it.

    After that you need to profile and see if it's really an issue. Will depend on the number of units etc. how often called.

    You could look at trying to refactor your data structures but I don't think it's going to help reduce the calls too much.
    If you are really calling it a lot. You will have to start caching things in lists or dictionaries.
    You could have a dictionary with transform keys to maybe a base class of your data or behavior and just do a cast.

    But you really need to profile it. To see if it's even needed.
     
  6. Xhitman

    Xhitman

    Joined:
    Oct 30, 2015
    Posts:
    452
    Yes, I access them again and again. Somehow I think I am wrong. A longer example attached

    The situation is: Robot attack target, missile, laser or whatever. When missile hit target. the missile script call trait check function to find out the final damage made on target (reduce or increase because of traits from robot, robot's allies, target, target's allies). So when there are many missiles, projectiles, laser flying around in scene, the trait check workload will be heavy.

    Code (CSharp):
    1.  public static float FinalDamage(Transform attacker, Transform defender, float weaponDamage, DamageType damageType, TextMeshProUGUI damageText)
    2.     {
    3.         // 1. attacker damage buff
    4.         // 2. attacker ally damage buff
    5.         // 3. defender ressitance buff
    6.         // 4. defender ally resistance buff
    7.         // 5. Critical Damage chance check            
    8.              
    9.         float extraChance;
    10.  
    11.         Robot attackerData = attacker.GetComponent<RobotAI>().data;
    12.         Robot defenderData = defender.GetComponent<RobotAI>().data;
    13.         List<Transform> attackerAllies = new List<Transform>();
    14.         List<Transform> defenderAllies = new List<Transform>();
    15.  
    16.         //SkillEffect skillEffect = Data.battleManager.skillEffect;
    17.  
    18.         // temporary checkList
    19.         List<Transform> checkList = new List<Transform>();
    20.  
    21.         // Get allies
    22.         if (attacker.CompareTag(Data.TagPlayerUnit))
    23.         {
    24.             attackerAllies = Data.playerUnits;
    25.             defenderAllies = Data.enemyUnits;
    26.         }
    27.  
    28.         else if (attacker.CompareTag(Data.TagPEnemyUnit))
    29.         {
    30.             attackerAllies = Data.enemyUnits;
    31.             defenderAllies = Data.playerUnits;
    32.         }
    33.  
    34.         // Get extra chance
    35.         extraChance = GetExtraChance(attacker, defender, attackerAllies, defenderAllies);
    36.  
    37.         Trait attackerTraitSum = new Trait();
    38.         // Ally
    39.         foreach (Transform unit in attackerAllies)
    40.         {
    41.             List<TraitName> unitTraits = unit.GetComponent<RobotAI>().data.traits;
    42.             foreach (TraitName tn in unitTraits)
    43.             {
    44.                 Trait t = traitList.Find(x => x.traitName == tn);
    45.                 float distance = Vector3.Distance(attacker.position, unit.position);
    46.  
    47.                 // 1. chance check
    48.                 // 2. range and armor percentage check
    49.                 if (Random.Range(0, 100f) <= t.triggerChance)
    50.                 {
    51.                     if (distance <= t.range && attackerData.armor / attackerData.armorMax <= t.triggerArmorPercent)
    52.                         TraitSumUp(t, attackerTraitSum, extraChance);
    53.                 }
    54.             }
    55.         }
    56.  
    57.         // Self
    58.         foreach (TraitName tn in attackerData.traits)
    59.         {
    60.             Trait t = traitList.Find(x => x.traitName == tn);
    61.             float distance = Vector3.Distance(attacker.position, defender.position);
    62.  
    63.             // 1. chance check
    64.             // 2. range and armor percentage check
    65.             if (Random.Range(0, 100f) <= t.triggerChance)
    66.             {
    67.                 if (distance <= t.range && attackerData.armor / attackerData.armorMax <= t.triggerArmorPercent)
    68.                     TraitSumUp(t, attackerTraitSum, extraChance);
    69.             }
    70.         }
    71.  
    72.         Trait defenderTraitSum = new Trait();
    73.         // Defender Allies
    74.         foreach (Transform unit in defenderAllies)
    75.         {
    76.             List<TraitName> unitTraits = unit.GetComponent<RobotAI>().data.traits;
    77.             foreach (TraitName tn in unitTraits)
    78.             {
    79.                 Trait t = traitList.Find(x => x.traitName == tn);
    80.                 float distance = Vector3.Distance(defender.position, unit.position);
    81.  
    82.                 // 1. chance check
    83.                 // 2. range and armor percentage check
    84.                 if (Random.Range(0, 100f) <= t.triggerChance)
    85.                 {
    86.                     if (distance <= t.range && defenderData.armor / defenderData.armorMax <= t.triggerArmorPercent)
    87.                         TraitSumUp(t, defenderTraitSum, extraChance);
    88.                 }
    89.             }
    90.         }
    91.  
    92.         // Defender
    93.         foreach (TraitName tn in defenderData.traits)
    94.         {
    95.             Trait t = traitList.Find(x => x.traitName == tn);
    96.             float distance = Vector3.Distance(defender.position, defender.position);
    97.  
    98.             // 1. chance check
    99.             // 2. range and armor percentage check
    100.             if (Random.Range(0, 100f) <= t.triggerChance)
    101.             {
    102.                 if (distance <= t.range && defenderData.armor / defenderData.armorMax <= t.triggerArmorPercent)
    103.                     TraitSumUp(t, defenderTraitSum, extraChance);
    104.             }
    105.         }
    106.  
    107.  
    108.         // attack*(100/(100+defense))
    109.         float powerMultiplier;
    110.         if (damageType == DamageType.Melee)
    111.             powerMultiplier = attackerTraitSum.power + attackerTraitSum.meleePower;
    112.         else
    113.             powerMultiplier = attackerTraitSum.power + attackerTraitSum.shotPower;
    114.  
    115.         float _damage = weaponDamage * powerMultiplier;
    116.         // damage multiplier
    117.         if (attackerTraitSum.finalDamageMultiplier != 0)
    118.         {
    119.             _damage = _damage * attackerTraitSum.finalDamageMultiplier;
    120.             damageText.color = Color.yellow;
    121.         }
    122.  
    123.         float defenderResist=1;
    124.  
    125.         switch (damageType)
    126.         {
    127.             case DamageType.Energy:
    128.                 defenderResist = defenderTraitSum.energyResist;
    129.                 break;
    130.             case DamageType.Melee:
    131.                 defenderResist = defenderTraitSum.meleeResist;
    132.                 break;
    133.             case DamageType.Projectile:
    134.                 defenderResist = defenderTraitSum.projectileResist;
    135.                 break;
    136.             case DamageType.Missile:
    137.                 defenderResist = defenderTraitSum.missileResist;
    138.                 break;
    139.         }
    140.         _damage = _damage * (100 / (100 + defenderResist));      
    141.         return _damage;
    142.     }
     
  7. Xhitman

    Xhitman

    Joined:
    Oct 30, 2015
    Posts:
    452
    Is it?
    Code (CSharp):
    1.  
    2.  
    3. // file 1
    4. public interface DataInterface
    5. {
    6.     List<TraitName> GetTrait();
    7.  
    8. }
    9.  
    10. //  file 2
    11. public class RobotAI : MonoBehaviour, DataInterface
    12. {  
    13.     public Robot data = new Robot();
    14. public List<TraitName> GetTrait()
    15.     {
    16.         return data.traits;      
    17.     }
    18. }
    19.  
    20. // file 3
    21. public class NonRobotUnitAI : MonoBehaviour, DataInterface
    22. {
    23.    
    24.     public NonRobotUnit data = new NonRobotUnit();
    25.  
    26.     public List<TraitName> GetTrait()
    27.     {
    28.         return data.traits;
    29.     }
    30. }
    31.  
    32. // file 4
    33. public static void ArmorMove(Transform unit)
    34.     {    
    35.         // List<TraitName> _traitList = unit.GetComponent<RobotAI>() ? unit.GetComponent<RobotAI>().data.traits : unit.GetComponent<NonRobotUnitAI>().data.traits;    
    36.         List<TraitName> _traitList = ?????
    37. }
    but how to access GetTrait()?
     
  8. romatallinn

    romatallinn

    Joined:
    Dec 26, 2015
    Posts:
    161
    For a given snippet,
    List<TraitName> _traitList = unit.GetComponent<DataInterface>().GetTrait();


    But I really think that the class inheritance would work here better than interfaces, because as you can see, this GetTrait method is absolutely the same in both classes.

    Although it may indeed vary, depends on how you implemented the AI systems as a whole.

    Yes, something like this.

    I wouldn't say everything, but you need to extract something that they have in common into UnitAI. For example, this GetTrait method.

    Then don't put them into UnitAI, but into RobotAI. That is why you are creating a separate class RobotAI; you define some additional variables that needed there, but not needed in other types of AIs.

    Make sure to understand the concept of virtual/abstract method and its overriding. For example, both AIs have a method named "CalculatePath()" or whatsoever. However, both of them implement this function differently. So, you can define the function in UnitAI; just leave it empty. And then override it in both subclasses. In the end, you will be able to call it using UnitAI, but the actual functionality will be implemented in the subclasses, and it will be totally different.

    By the way, the same thing applies to data variable. I now can see you have two different classes defining it, but I'm sure you can simplify it a lot by inheritance.
     
    Last edited: Jan 16, 2019
  9. Xhitman

    Xhitman

    Joined:
    Oct 30, 2015
    Posts:
    452
    Thank so much. I think I know the direction now.
     
    romatallinn likes this.
  10. Munchy2007

    Munchy2007

    Joined:
    Jun 16, 2013
    Posts:
    1,735
    Quite possibly.

    I'd use inheritance and an abstract function if all or most of my derived classes needed to implement it (and the implementation was different for each one), otherwise I'd probably use an interface. Using an interface would mean I wouldn't be forced into inheriting from a single base class to use that function, making it more flexible.

    At the end of the day it usually comes down to personal preference as long as it works and is maintainable; There's more than one way to skin a cat after all.
     
    Last edited: Jan 16, 2019
  11. Xhitman

    Xhitman

    Joined:
    Oct 30, 2015
    Posts:
    452
    Because I am just a beginner and learn by google, no any programming education, there are many concepts I don't know or know a bit but don't know where to apply.

    Interface is the 2nd time I use. The first time was I try to access a button long time ago, so I forget everything.
     
  12. daxiongmao

    daxiongmao

    Joined:
    Feb 2, 2016
    Posts:
    412
    For the inheritance stuff I think you had it right like this.
    RobotAI : UnitAI
    NonRobotAI : UnitAI
    UnitAI : MonoBehaviour

    Your UnitAI class can have anything that is the same between the two Robot and NonRobotAI.
    Code (CSharp):
    1.  
    2. //Then you would do a
    3. var unit = GetComponent<UnitAI>();
    4.  
    5. //If the data you need is on the base class then you can just do.
    6. var traits = unit.traits;
    7.  
    8. //if you need something from one of the parent classes you can do
    9. if(unit is RobotAI)
    10. {
    11.     var robot = unit as RobotAI;
    12. }
    13.  
    This should be faster than a GetComponent call because i believe those have to go into the C++. But again profiling is the only way to really know how much is needed.

    The code you posted in reply to me i see you have lots of List<Transform>. But you seem to already know what types are in these lists.

    Code (CSharp):
    1.  
    2. // Get allies
    3.         if (attacker.CompareTag(Data.TagPlayerUnit))
    4.         {
    5.             attackerAllies = Data.playerUnits;
    6.             defenderAllies = Data.enemyUnits;
    7.         }
    8.         else if (attacker.CompareTag(Data.TagPEnemyUnit))
    9.         {
    10.             attackerAllies = Data.enemyUnits;
    11.             defenderAllies = Data.playerUnits;
    12.         }
    13.  
    You should try to make these lists be List<RobotAI>. List<NonRobotAI> or if it can be mixed then make them List<UnitAI>. This will save you a GetComponet call because you are doing that somewhere else and hopefully only one time.

    If you needed to find it at some point you can just do a find and check the UnitAI.trasform == transform.
    Doing this not very often like on a spawn or a kill should not be too bad.
    If you do this a lot then you could look at using a better data structure like Hashmap or dictonary.
    Dictionary<transform, UnitAI> units; And you can do a faster lookup.

    But its usually better to work in the types you are interested in instead of the generic Transform or GameObject, you can always get them from your component.