Search Unity

Healthy Dependency - Which way of doing it?

Discussion in 'Scripting' started by Nigey, Feb 12, 2016.

  1. Nigey

    Nigey

    Joined:
    Sep 29, 2013
    Posts:
    1,129
    Hi Guys,

    I have an AI Controller. It has several lists of different interface types. I want to create a method that adds an object to the correct list via reflection.

    This is what I have right now:

    Class passing data to AIController
    Code (CSharp):
    1.     public void Socket(ISocket _socket)
    2.     {
    3.         IController controller = transform.root.GetComponentInChildren<IController>();
    4.  
    5.         if(controller != null)
    6.         {
    7.             Type extensionType = (Type)GetComponent<ISensorAI>();
    8.  
    9.             if(extensionType == null)
    10.             {
    11.                 extensionType = (Type)GetComponent<IShootAI>();
    12.             }
    13.  
    14.             controller.AddExtension((Type)extensionType);
    15.         }
    16.  
    17.         _socketed = true;
    18.         this.enabled = false;
    19.     }

    AIController Class receiving and storing data
    Code (CSharp):
    1.         [SerializeField] private List<ISensorAI> _sensors = new List<ISensorAI>();
    2.         [SerializeField] private List<IShootAI> _weapons = new List<IShootAI>();
    3.  
    4.         public void AddExtension(Type type)
    5.         {
    6.             if (type.Equals(typeof(ISensorAI)))
    7.             {
    8.                 _sensors.Add(type as ISensorAI);
    9.             }
    10.             else if (type.Equals(typeof(IShootAI)))
    11.             {
    12.                 _weapons.Add(type as IShootAI);
    13.             }
    14.             else
    15.             {
    16.                 #if UNITY_EDITOR
    17.                 Debug.Log(string.Format("IController::AddExtension - unrecognised type parsed : {0}", type.GetType().ToString()));
    18.                 #endif
    19.             }
    20.         }
    Firstly the code doesn't actually work at runtime. It compiles, but it says:
    I can probably figure that out myself after some digging, but I'm not sure if this is the best way to do this.. As the number of types grows, this'll become a code smell. My spidersense is tingling lol. What's the best way to avoid it?
     
  2. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,334
    You're trying to add a Type object to a list of Objects. The type ISensorAI is not the same thing as an object that has the type ISensorAI.

    What you want is along the lines of:

    Code (csharp):
    1. public void AddExtension(object extension) {
    2.     var sensor = object as ISensorAI;
    3.     if(sensor != null) {
    4.         _sensors.Add(sensor);
    5.     }
    6.     ....
    7. }
    Though I'd have to see your code to tell for sure. What are you trying to do?
     
  3. Nigey

    Nigey

    Joined:
    Sep 29, 2013
    Posts:
    1,129
    Aaah I'm now with you. So I've now set it up like this, and it works:

    Code (CSharp):
    1. public void AddExtension(object objType)
    2.         {
    3.             var objSensor = objType as ISensorAI;
    4.             var objShooter = objType as IShootAI;
    5.  
    6.             if (objSensor != null)
    7.             {
    8.                 _sensors.Add(objSensor as ISensorAI);
    9.             }
    10.             else if (objShooter != null)
    11.             {
    12.                 _weapons.Add(objType as IShootAI);
    13.             }
    14.             else
    15.             {
    16.                 #if UNITY_EDITOR
    17.                 Debug.Log(string.Format("IController::AddExtension - unrecognised type parsed : {0}", objType.GetType().ToString()));
    18.                 #endif
    19.             }
    20.         }
    This is a bit of a rigid system though. I'd like to improve it.

    Basically the AIController calls action's on a list of AI components (the lists you see there). The AIController calls all the AI interfaces each loop. So the object's behavior is defined by it's components, not by itself. So an object's functionality will change by what objects are attached, as and when you add/remove them. Like so:

    Code (CSharp):
    1.     public class AIController : RTSBase, IController
    2.     {
    3.         public bool activeController { get { return _activeController;} }
    4.  
    5.         [SerializeField] private float _timeStep = 0.25f;
    6.         [SerializeField] private bool _activeController = true;
    7.         [SerializeField] private List<GameObject> _targets = new List<GameObject>();
    8.         [SerializeField] private List<ISensorAI> _sensors = new List<ISensorAI>();
    9.         [SerializeField] private List<IShootAI> _weapons = new List<IShootAI>();
    10.         private IMoveAI _mover;
    11.         private IBehaviour _behaviour;
    12.  
    13.         protected override bool Initialise()
    14.         {
    15.             Activate();
    16.             return base.Initialise();
    17.         }
    18.  
    19.         public void AddExtension(object objType)
    20.         {
    21.             var objSensor = objType as ISensorAI;
    22.             var objShooter = objType as IShootAI;
    23.  
    24.             if (objSensor != null)
    25.             {
    26.                 _sensors.Add(objSensor as ISensorAI);
    27.             }
    28.             else if (objShooter != null)
    29.             {
    30.                 _weapons.Add(objType as IShootAI);
    31.             }
    32.             else
    33.             {
    34.                 #if UNITY_EDITOR
    35.                 Debug.Log(string.Format("IController::AddExtension - unrecognised type parsed : {0}", objType.GetType().ToString()));
    36.                 #endif
    37.             }
    38.         }
    39.  
    40.         public void Activate()
    41.         {
    42.             _activeController = true;
    43.  
    44.             _sensors.AddRange(GetComponentsInChildren<ISensorAI>());
    45.             _weapons.AddRange(GetComponentsInChildren<IShootAI>());
    46.             _mover = GetComponentInChildren<IMoveAI>();
    47.             _behaviour = GetComponentInChildren<IBehaviour>();
    48.             StartCoroutine(ActionArtificialIntelligence());
    49.         }
    50.        
    51.         public void Deactivate()
    52.         {
    53.             _activeController = false;
    54.         }
    55.  
    56.         private IEnumerator ActionArtificialIntelligence()
    57.         {
    58.             while (_activeController)
    59.             {
    60.                 if(_sensors != null)
    61.                 {
    62.                     for (int i = 0; i < _sensors.Count; i++)
    63.                     {
    64.                         _sensors[i].SenseAction(ref _targets);
    65.                     }
    66.                 }
    67.                
    68.                 if (_weapons != null && _targets.Count > 0)
    69.                 {
    70.                     for (int i = 0; i < _weapons.Count; i++)
    71.                     {
    72.                         _weapons[i].ShootAction(_targets.ToArray());
    73.                     }
    74.                 }
    75.                
    76.                 if (_behaviour != null)
    77.                 {
    78.                     _behaviour.BehaviourAction(_targets, _weapons.ToArray(), _mover);
    79.                 }
    80.  
    81.                 yield return new WaitForSeconds(_timeStep);
    82.             }
    83.  
    84.             #if UNITY_EDITOR
    85.             Debug.Log(string.Format("AIController::ActionArtificialIntelligence - Ended on GameObject {0}", gameObject.name));
    86.             #endif
    87.         }
    88.     }
     
  4. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,334
    I'd set the hierarchy up like this instead:

    Code (csharp):
    1. interface IShipComponent {
    2.     void DoAction(List<GameObject> targets);
    3. }
    4.  
    5. abstract class ShootComp : IShipComponent {
    6.     public void DoAction(List<GameObject> targets) {
    7.         ShootAction(targets);
    8.     }
    9.  
    10.     protected abstract void ShootAction(List<GameObject> targets);
    11. }
    12.  
    13. abstract class SensorComp : IShipComponent {
    14.     public void DoAction(List<GameObject> targets) {
    15.         SenseAction(targets);
    16.     }
    17.  
    18.     protected abstract void SenseAction(List<GameObject> targets);
    19. }
    That will massively reduce the complexity of your code - you don't have to if- over the type of the components - just have a list of IComponents, and let them handle what they're doing. Then you can use inheritance to specify specific types of Shooting and Sensing components.


    An alternative is to keep two different interfaces, and use overloading instead:

    Code (csharp):
    1. public void AddExtension(IShootAI shootAi)  {
    2.     _weapons.Add(shootAI);
    3. }
    4.  
    5. public void AddExtension(ISensorAI sensor) {
    6.     _sensors.Add(sensor);
    7. }
    That requires that it's never ambiguous in the calling code if you're adding a shooter or a sensor, but since they don't share any type (other than object), that should probably not happen.


    By the way, you're doing this:
    Code (csharp):
    1. _sensors[i].SenseAction(ref _targets);
    The ref keyword is not neccessary - targets is a list, which is a reference type, so it's always passed by reference. You only need ref when you want something that's usually passed by value (a struct or a primitive like a number) by reference.
     
    Nigey likes this.
  5. Nigey

    Nigey

    Joined:
    Sep 29, 2013
    Posts:
    1,129
    Thanks for all that :). I had no idea Lists worked like that.

    I was thinking about doing it the way you suggested there. The thing is that eventually the objects will need to share some information:
    • Movement will need to know stopping point for weapons to be in range.
    • Something needs to be aware of all the weapons, vs the enemies weapons to find whether it's worth the fighting, see if possible to kite, and the like.
    • Movement needs to be told whether they need direct line of sight.
    • Behavior needs to know the number/power of weapons they have vs the enemies, to know whether to take an offensive/defensive stance, or flee.
    So the components cannot be totally ambiguous with each other all the time. I'm not sure about whether that means their execution order (in terms of type), matter as well. I'm not sure if that would make any difference on the AIController? Perhaps I just need to sleep on it lol.

    Oh one more random Q by the way. Do you help on the forums just for the challenge? Or it s a job? Or is it something you do for 5 mins while you're not working? Or indie or? There's so many clever peeps on here I'm wondering what they're on here for, as a lot are only really helping.
     
  6. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,334
    You've got a complex problem at hand. I'd try to write out several solutions, and see how the code ends up looking - both inside the AI controller, and outside in the code using it.

    Since you have a AI controller, it sound like the Movement should not need to maintain a link to the current weapons and sensors. Instead, when the controller needs to move somewhere, it should send the relevant weapons and sensors to the movement script. So you can write something like "hey, Movement script, given this weapon, move me in range of this enemy. Here's the sensors you will need for that"

    That will probably give much prettier code than if you tell the Movement script "please move me in range", and then the movement script needs to go look through your fields for what enemy it needs to be in range of, what weapons it needs to consider, and what sensors are available.


    It's for when I need a break from work. Or I'm waiting for a model import bar to fill up. It's mostly for my own amusement, though I know that explaining things helps me reflect on them and learn things myself. There's also a ton to be gained from hanging around here - I would not know about either Redorderable lists or UnityEvents if I hadn't picked up on threads about them when they popped up.

    The crowd of regulars here seems to be everything from people like me with a day job using Unity, to self-employed indies, freelancers and hobbyists. To see what I do, check the bottom of my sig.
     
  7. Nigey

    Nigey

    Joined:
    Sep 29, 2013
    Posts:
    1,129
    Yeah I agree. It's a careful matter of choosing what communicates with what. At this point I'm thinking, because there'll be multiple weapons and sensors:

    • Sensors - are only there to sense and take data. Like they would in real life. So they find and collect a list of targets, and show their 'danger rating', in terms of armour/health/dps. In the sense of an MVC it's simply the view, in the sense of collecting data atleast lol.
    • Weapons - because they are not a singular collective of all the weapons, but are instead potentially many of the same or different weapon components attached. So they'll just be fed the targets and their priority. They will handle by themselves what targets are within reach vs what targets are highest in priority to fire at.
    • Behaviour - unlike the others it does actually do some thinking about itself as a unit. Because it's the personality type of the unit. So it'll check it's aggressiveness, the target's cumulative danger ratings (fed into it from the sensors), and check it's own (by getting the cumulative danger rating of itself from the weapons and body). It'll then feed behaviour back to movement. It'll also force weapons to turn off/on, based on player input commands.
    I'm thinking of something as simple as that for now, that I can expand on later. I just need to make sure I pick a design that is extensible for the AI's expansion. Like you said, I think I'll draw out some different potential diagrams of how this'll work..

    . Yeah that sounds exactly the same as me. It breaks up the day job, and it's a awesome place to find super talented people, and see what they're learning/showing/sharing with each other... and of course get help like this lol :p