Search Unity

  1. New Unity Live Help updates. Check them out here!

    Dismiss Notice

Typecast a Generic T to Vector3

Discussion in 'Scripting' started by omegabytestudio, Jan 31, 2017.

  1. omegabytestudio

    omegabytestudio

    Joined:
    Nov 9, 2015
    Posts:
    77
    Hi there!

    I started using Generics for the first time, and I got an issue.

    here an example of my function :

    void test<T>( T parTarget)
    {
    if (parTarget as Vector3)
    Vector3 TempTarget = parTarget as Vector3;
    }

    As you probably know, I cant use as Vector3 as Vector3 is a structure. I searched around the web for an answer, but didn't find anything for my specific case.

    If you have an idea, go for it! :)
     
  2. gorbit99

    gorbit99

    Joined:
    Jul 14, 2015
    Posts:
    1,175
    Cast it:
    if (parTarget.type == typeof(Vector3))
    Vector3 TempTarget = (Vector3)parTarget;
     
    omegabytestudio likes this.
  3. KelsoMRK

    KelsoMRK

    Joined:
    Jul 18, 2010
    Posts:
    5,468
    Generally speaking if you're doing these kinds of type checks to a generic argument then a generic argument is not an appropriate solution to your problem. It looks like you want multiple overloads of the test method that take different arguments. What else could you pass into test and what would you do with it on the other end?
     
    Shin_S, TaleOf4Gamers, Kiwasi and 2 others like this.
  4. omegabytestudio

    omegabytestudio

    Joined:
    Nov 9, 2015
    Posts:
    77
    @gorbit99

    I tried and it didn't work.

    Well I only tried with the (Vector3)partarget part. Does the conditionnal typeof(Vector3) change anything for the following code?

    @KelsoMRK

    It will ether be a GameObject or a Vector3.

    I'm using Generic because well, I thought it was this kind of situation that I needed to do that.

    Also, the Generic Parameter goes tru like 4 function before getting there, and I didn't want to duplicate each function for a different overload.
     
  5. KelsoMRK

    KelsoMRK

    Joined:
    Jul 18, 2010
    Posts:
    5,468
    Is it literally a logical branch based on whether it's a GameObject or a Vector3? There's not really any commonality between them. What if you pass in something else?

    Again, sounds like you need 2 test methods - one that takes a GameObject and one that takes a Vector3.
     
  6. LeftyRighty

    LeftyRighty

    Joined:
    Nov 2, 2012
    Posts:
    5,148
    omegabytestudio likes this.
  7. MV10

    MV10

    Joined:
    Nov 6, 2015
    Posts:
    1,889
    Another vote for method overloading. Another clue that generics isn't the right paradigm is that the type constraint syntax wouldn't allow you to constrain <T> to both a struct and a class (either a class in general or a specific base class). Both of these signatures would cause an error:

    Code (csharp):
    1. void test<T>() where T : struct, GameObject { ... }
    2.  
    3. void test<T>() where T: struct, class { ... }
    I'm guessing if your two use-cases are a Vector3 and a GameObject, you want to test against the GameObject's position. Overloading would look like this:

    Code (csharp):
    1. void test(Vector3 target)
    2. {
    3.     // do some kind of test against the target
    4. }
    5.  
    6. void test(GameObject target)
    7. {
    8.     test(target.position); // call the Vector3 version
    9. }
     
    Kiwasi, omegabytestudio and KelsoMRK like this.
  8. omegabytestudio

    omegabytestudio

    Joined:
    Nov 9, 2015
    Posts:
    77
    I see your points, it does make sense.

    It just annoys me that I will have to make 2 function for the same call.

    I had it working with the generic, but it wasn't optimal nor respecting any kind of best practice.

    If it wasn't a gameobject in the generic, I would do a parObject.ToString(), giving me something like (1,1,1). Then take this string and substring it to create a vector3 with it.

    @MV10 the reason why I didn't want to overload the function, was because the function goes tru like 3 classes, and some of them are derived classes of a base class. So I would have to duplicate the function at all those place.
    (Ex : CombatManager -> WeaponManager -> Weapon -> Skill ect..)
     
  9. LeftyRighty

    LeftyRighty

    Joined:
    Nov 2, 2012
    Posts:
    5,148
  10. KelsoMRK

    KelsoMRK

    Joined:
    Jul 18, 2010
    Posts:
    5,468
    But it's not the same call. It can't be because you're performing different operations :)

    That sounds scary and horrible. Please don't do that.
     
    omegabytestudio likes this.
  11. MV10

    MV10

    Joined:
    Nov 6, 2015
    Posts:
    1,889
    If it's duplicated, just put it in the base class:

    Code (csharp):
    1. public abstract class BaseCombatManager
    2. {
    3.  
    4.     public virtual void test(Vector3 target)
    5.     {
    6.         // implementation that all derived classes can use, or can override if necessary
    7.     }
    8.  
    9. }
     
    omegabytestudio likes this.
  12. omegabytestudio

    omegabytestudio

    Joined:
    Nov 9, 2015
    Posts:
    77
    @LeftyRighty
    Yeah I will read your link tonight, seems interresting. Is it a different take on inheritance?

    @KelsoMRK

    It's horrible, that's why I'm looking for a solution. :)

    You are right that it's not the same call. I'm tryhard to keep it working my way, but you have a good point. My function does something different for each type of object, so it doesn't make sens to keep them in the same call. Stop shoving the truth in my face! :)

    @MV10
    CombatManager -> WeaponManager -> Weapon -> Skill aren't derived classes , they call each other and check if everything is okay then call the next one. Like Combat check if the player alive, weaponmanager check if weapon is equiped ect..

    I have some derived classe like Weapon is the parent classes of every weapon, and skill of every skill. I'm using the base classes function when I can.

    But Maybe I should change my structure.

    @LeftyRighty @KelsoMRK @MV10

    I don't know if it's good practice in OOP :

    I will give you an example of what i have right now :
    Code (csharp):
    1.  
    2. In combatManager.cs:
    3. Attack<T>(T parTarget)
    4. {
    5. [...] Some check up[...]
    6.   WeaponManager.Attack(parTarget);
    7. }
    8.  
    9. WeaponManager.cs:
    10. Attack<T>(T parTarget)
    11. {
    12.   [...] Some check up[...]
    13.   EquippedWeapon.Attack(parTarget);
    14. }
    15.  
    16. Weapon.cs
    17. Attack<T>(T parTarget)
    18. {
    19.   [...] Some check up[...]
    20.   if (parTarget as Gameobject)
    21.     Do this
    22.   else
    23.      Get Vector3 and do this
    24. }
    25.  
    TL;DR : Each classes call the next one.

    That's a short/gross example of what I'm doing right now. I don't know if it's good practice, but I followed a guide that was doing it like this.

    Should I just :

    Code (csharp):
    1.  
    2. In combatManager.cs:
    3. Attack<T>(T parTarget)
    4. {
    5. [...] Some check up[...]
    6.   WeaponManager.EquipedWeapon.Skill.Attack(parTarget);
    7. }
    8.  
    And do my check up in a previous call?

    Is it good practice?

    Thank you!

    Sorry to abuse you guys, I learned from scratch and I have no idea about good habit of coding in C#.
     
    Last edited: Jan 31, 2017
  13. MV10

    MV10

    Joined:
    Nov 6, 2015
    Posts:
    1,889
  14. omegabytestudio

    omegabytestudio

    Joined:
    Nov 9, 2015
    Posts:
    77
    Oh I couldn't find the tag, didnt know this forum was using BBcode.

    It's pseudo code, so I'm not sure if it will be clear.
     
  15. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,658
    Why?

    Step back and ask yourself why you are doing this. What situations are there that you are going to call this with a GameObject or a Vector3, and do you really need it to maintain that identity throughout the function stack?

    Ideally you would abandon one or the other, and only hand one type through the whole stack. In other words overload the first method, and not any of the others.

    If you really do need to maintain both parameter throughout, I would actually box the parameters together. Create a TargetData class with both a Vector3 and a GameObject. Then you can simply do your operations on that. It's still ugly, but not as ugly as using generics.
     
    MV10 and omegabytestudio like this.
  16. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    6,980
    This sounds eerily like a problem I had when I was designing some of my AI stuff for use through the editor by my artists/designers.

    In doing so they wanted to be able to have AI units target positions, gameobjects, components, or any which weary 'Transform' related object that might result from some sequence of commands they wired together using our T&I system (sort of like UnityEvent).

    So I created them the 'ComplexTarget':
    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using System.Collections.Generic;
    4. using System.Linq;
    5.  
    6. using com.spacepuppy;
    7. using com.spacepuppy.AI.Sensors;
    8. using com.spacepuppy.Geom;
    9. using com.spacepuppy.Utils;
    10.  
    11. namespace com.spacepuppy.AI
    12. {
    13.  
    14.     public enum ComplexTargetType
    15.     {
    16.         Null = 0,
    17.         Aspect = 1,
    18.         Transform = 2,
    19.         Vector2 = 3,
    20.         Vector3 = 4
    21.     }
    22.  
    23.     public struct ComplexTarget
    24.     {
    25.  
    26.         public static IPlanarSurface DefualtSurface;
    27.  
    28.         #region Fields
    29.  
    30.         public readonly ComplexTargetType TargetType;
    31.         private readonly object _target;
    32.         private readonly Vector3 _vector;
    33.         private readonly IPlanarSurface _surface;
    34.  
    35.         #endregion
    36.  
    37.         #region CONSTRUCTOR
    38.  
    39.         public ComplexTarget(IAspect aspect)
    40.         {
    41.             if (aspect != null)
    42.             {
    43.                 TargetType = ComplexTargetType.Aspect;
    44.                 _target = aspect;
    45.             }
    46.             else
    47.             {
    48.                 TargetType = ComplexTargetType.Null;
    49.                 _target = null;
    50.             }
    51.             _vector = Vector3.zero;
    52.             _surface = DefualtSurface;
    53.         }
    54.  
    55.         public ComplexTarget(Transform target)
    56.         {
    57.             if (target != null)
    58.             {
    59.                 TargetType = ComplexTargetType.Transform;
    60.                 _target = target;
    61.             }
    62.             else
    63.             {
    64.                 TargetType = ComplexTargetType.Null;
    65.                 _target = null;
    66.             }
    67.             _vector = Vector3.zero;
    68.             _surface = DefualtSurface;
    69.         }
    70.  
    71.         public ComplexTarget(Vector2 location)
    72.         {
    73.             TargetType = ComplexTargetType.Vector2;
    74.             _target = null;
    75.             _vector = (Vector3)location;
    76.             _surface = DefualtSurface;
    77.         }
    78.  
    79.         public ComplexTarget(Vector3 location)
    80.         {
    81.             TargetType = ComplexTargetType.Vector3;
    82.             _target = null;
    83.             _vector = location;
    84.             _surface = DefualtSurface;
    85.         }
    86.  
    87.         public ComplexTarget(IAspect aspect, IPlanarSurface surface)
    88.         {
    89.             if (aspect != null)
    90.             {
    91.                 TargetType = ComplexTargetType.Aspect;
    92.                 _target = aspect;
    93.             }
    94.             else
    95.             {
    96.                 TargetType = ComplexTargetType.Null;
    97.                 _target = null;
    98.             }
    99.             _vector = Vector3.zero;
    100.             _surface = surface;
    101.         }
    102.  
    103.         public ComplexTarget(Transform target, IPlanarSurface surface)
    104.         {
    105.             if (target != null)
    106.             {
    107.                 TargetType = ComplexTargetType.Transform;
    108.                 _target = target;
    109.             }
    110.             else
    111.             {
    112.                 TargetType = ComplexTargetType.Null;
    113.                 _target = null;
    114.             }
    115.             _vector = Vector3.zero;
    116.             _surface = surface;
    117.         }
    118.  
    119.         public ComplexTarget(Vector2 location, IPlanarSurface surface)
    120.         {
    121.             TargetType = ComplexTargetType.Vector2;
    122.             _target = null;
    123.             _vector = (Vector3)location;
    124.             _surface = surface;
    125.         }
    126.  
    127.         public ComplexTarget(Vector3 location, IPlanarSurface surface)
    128.         {
    129.             TargetType = ComplexTargetType.Vector2;
    130.             _target = null;
    131.             _vector = location;
    132.             _surface = surface;
    133.         }
    134.  
    135.         #endregion
    136.  
    137.         #region Properties
    138.  
    139.         public Vector2 Position2D
    140.         {
    141.             get
    142.             {
    143.                 switch (this.TargetType)
    144.                 {
    145.                     case ComplexTargetType.Null:
    146.                         return Vector2.zero;
    147.                     case ComplexTargetType.Aspect:
    148.                         var a = _target as IAspect;
    149.                         if (a.IsNullOrDestroyed()) return Vector2.zero;
    150.                         else if (_surface == null) return ConvertUtil.ToVector2(a.transform.position);
    151.                         else return _surface.ProjectPosition2D(a.transform.position);
    152.                     case ComplexTargetType.Transform:
    153.                         var t = _target as Transform;
    154.                         if (t.IsNullOrDestroyed()) return Vector2.zero;
    155.                         else if (_surface == null) return ConvertUtil.ToVector2(t.position);
    156.                         else return _surface.ProjectPosition2D(t.position);
    157.                     case ComplexTargetType.Vector2:
    158.                         return ConvertUtil.ToVector2(_vector);
    159.                     case ComplexTargetType.Vector3:
    160.                         if (_surface == null) return ConvertUtil.ToVector2(_vector);
    161.                         else return _surface.ProjectPosition2D(_vector);
    162.                     default:
    163.                         return Vector2.zero;
    164.                 }
    165.             }
    166.         }
    167.  
    168.         public Vector3 Position
    169.         {
    170.             get
    171.             {
    172.                 switch (this.TargetType)
    173.                 {
    174.                     case ComplexTargetType.Null:
    175.                         return Vector3.zero;
    176.                     case ComplexTargetType.Aspect:
    177.                         var a = _target as IAspect;
    178.                         if (a == null) return Vector3.zero;
    179.                         else return a.transform.position;
    180.                     case ComplexTargetType.Transform:
    181.                         var t = _target as Transform;
    182.                         if (t == null) return Vector3.zero;
    183.                         else return t.position;
    184.                     case ComplexTargetType.Vector2:
    185.                         if (_surface == null) return _vector.SetZ(0f);
    186.                         else return _surface.ProjectPosition3D(ConvertUtil.ToVector2(_vector));
    187.                     case ComplexTargetType.Vector3:
    188.                         return _vector;
    189.                     default:
    190.                         return Vector3.zero;
    191.                 }
    192.             }
    193.         }
    194.  
    195.         public IAspect TargetAspect { get { return _target as IAspect; } }
    196.  
    197.         public Transform Transform
    198.         {
    199.             get
    200.             {
    201.                 switch (TargetType)
    202.                 {
    203.                     case ComplexTargetType.Aspect:
    204.                         var a = _target as IAspect;
    205.                         if (a == null) return null;
    206.                         else return a.transform;
    207.                     case ComplexTargetType.Transform:
    208.                         return _target as Transform;
    209.                     default:
    210.                         return null;
    211.                 }
    212.             }
    213.         }
    214.  
    215.         public bool IsNull
    216.         {
    217.             get
    218.             {
    219.                 switch (this.TargetType)
    220.                 {
    221.                     case ComplexTargetType.Null:
    222.                         return true;
    223.                     case ComplexTargetType.Aspect:
    224.                     case ComplexTargetType.Transform:
    225.                         return _target.IsNullOrDestroyed();
    226.                     case ComplexTargetType.Vector2:
    227.                     case ComplexTargetType.Vector3:
    228.                     default:
    229.                         return false;
    230.                 }
    231.             }
    232.         }
    233.  
    234.         #endregion
    235.  
    236.         #region Static Methods
    237.  
    238.         public static ComplexTarget FromObject(object targ)
    239.         {
    240.             if (targ == null) return new ComplexTarget();
    241.  
    242.             if (targ is Component)
    243.             {
    244.                 if (targ is IAspect)
    245.                     return new ComplexTarget(targ as IAspect);
    246.                 else if (targ is Transform)
    247.                     return new ComplexTarget(targ as Transform);
    248.                 else
    249.                     return new ComplexTarget((targ as Component).transform);
    250.             }
    251.             else if (targ is GameObject)
    252.                 return new ComplexTarget((targ as GameObject).transform);
    253.             else if (targ is Vector2)
    254.                 return new ComplexTarget((Vector2)targ);
    255.             else if (targ is Vector3)
    256.                 return new ComplexTarget((Vector3)targ);
    257.             else
    258.                 return new ComplexTarget();
    259.         }
    260.  
    261.         public static ComplexTarget Null { get { return new ComplexTarget(); } }
    262.  
    263.         //public static implicit operator ComplexTarget(IAspect o)
    264.         //{
    265.         //    return new ComplexTarget(o);
    266.         //}
    267.  
    268.         public static implicit operator ComplexTarget(Transform o)
    269.         {
    270.             return new ComplexTarget(o);
    271.         }
    272.  
    273.         public static implicit operator ComplexTarget(Vector2 v)
    274.         {
    275.             return new ComplexTarget(v);
    276.         }
    277.  
    278.         public static implicit operator ComplexTarget(Vector3 v)
    279.         {
    280.             return new ComplexTarget(v);
    281.         }
    282.  
    283.         public static implicit operator ComplexTarget(GameObject go)
    284.         {
    285.             if (go == null) return new ComplexTarget();
    286.             else return new ComplexTarget(go.transform);
    287.         }
    288.  
    289.         public static implicit operator ComplexTarget(Component c)
    290.         {
    291.             if (c == null) return new ComplexTarget();
    292.             if (c is IAspect)
    293.                 return new ComplexTarget(c as IAspect);
    294.             else if (c is Transform)
    295.                 return new ComplexTarget(c as Transform);
    296.             else
    297.                 return new ComplexTarget(c.transform);
    298.         }
    299.  
    300.         #endregion
    301.  
    302.     }
    303.  
    304. }
    305.  
    All with implicit conversion built in.

    It's a dirty dirty trick... but it gets the job done within the system they desired.

    And you can see my accessing it here in this slapped together movement style we did in a game jam:
    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using System.Collections.Generic;
    4.  
    5. using com.spacepuppy;
    6. using com.spacepuppy.AI;
    7. using com.spacepuppy.Cameras;
    8. using com.spacepuppy.Movement;
    9. using com.spacepuppy.UserInput;
    10. using com.spacepuppy.Utils;
    11.  
    12. namespace com.mansion.Entities.Actors.Zombie
    13. {
    14.  
    15.     public class ZombieWalkMovementStyle : SPComponent, IMovementStyle
    16.     {
    17.  
    18.         #region Fields
    19.  
    20.         [SerializeField()]
    21.         private float _speed = 1.0f;
    22.         [SerializeField()]
    23.         [Tooltip("Turn speed in degrees per second")]
    24.         private float _turnSpeed = 90f;
    25.         [SerializeField()]
    26.         private float _speedDuringTurn = 0.5f;
    27.  
    28.         [SerializeField()]
    29.         [DefaultFromSelf(UseEntity = true)]
    30.         private ZombieAnimator _animator;
    31.  
    32.         [System.NonSerialized()]
    33.         private IEntity _entity;
    34.         [System.NonSerialized()]
    35.         private MovementMotor _motor;
    36.  
    37.         [System.NonSerialized()]
    38.         private ComplexTarget _target;
    39.  
    40.         #endregion
    41.  
    42.         #region CONSTRUCTOR
    43.  
    44.         protected override void Awake()
    45.         {
    46.             base.Awake();
    47.  
    48.             _entity = SPEntity.Pool.GetFromSource<IEntity>(this);
    49.             _motor = this.GetComponent<MovementMotor>();
    50.  
    51.             _target = ComplexTarget.Null;
    52.         }
    53.  
    54.         #endregion
    55.  
    56.         #region Properties
    57.  
    58.         public float Speed
    59.         {
    60.             get { return _speed; }
    61.             set { _speed = value; }
    62.         }
    63.  
    64.         public ComplexTarget Target
    65.         {
    66.             get { return _target; }
    67.             set { _target = value; }
    68.         }
    69.        
    70.         #endregion
    71.  
    72.         #region Methods
    73.  
    74.         #endregion
    75.  
    76.         #region IMovementStyle Interface
    77.  
    78.         void IMovementStyle.OnActivate(IMovementStyle lastStyle, bool stateIsStacking)
    79.         {
    80.  
    81.         }
    82.  
    83.         void IMovementStyle.OnDeactivate(IMovementStyle nextStyle, bool stateIsStacking)
    84.         {
    85.  
    86.         }
    87.  
    88.         void IMovementStyle.OnPurgedFromStack()
    89.         {
    90.  
    91.         }
    92.  
    93.         void IMovementStyle.UpdateMovement()
    94.         {
    95.             if (Game.Paused) return;
    96.  
    97.             if(_entity.Stalled || _target.IsNull || _entity.HealthMeter.Health <= 0f)
    98.             {
    99.                 //idle
    100.                 if(_entity.HealthMeter.Health > 0f) _motor.Controller.Move(Vector3.up * Game.GRAV * Time.deltaTime);
    101.                 _animator.DefaultMovementAnimations.PlayIdle();
    102.                 return;
    103.             }
    104.  
    105.             Vector3 dir = (_target.Position - _motor.Controller.transform.position).SetY(0f);
    106.             float dist = dir.magnitude;
    107.                
    108.             //TODO - determine if distance is close, and if said close thing is player, to do attack
    109.  
    110.             if (dist > 0.3f)
    111.             {
    112.                 dir.Normalize();
    113.  
    114.                 Vector3 forw = _motor.Controller.transform.forward;
    115.                 float a = VectorUtil.AngleBetween(forw, dir);
    116.                 forw = Vector3.RotateTowards(forw, dir, _turnSpeed * Mathf.Deg2Rad * Time.deltaTime, 0.0f);
    117.  
    118.                 var speed = (a < _turnSpeed) ? _speed : _speedDuringTurn;
    119.                 Vector3 mv = forw * speed + Vector3.up * Game.GRAV;
    120.  
    121.                 _motor.Controller.Move(mv * Time.deltaTime);
    122.  
    123.                 _motor.Controller.transform.rotation = Quaternion.LookRotation(forw, Vector3.up);
    124.  
    125.                 _animator.DefaultMovementAnimations.PlayWalk(speed);
    126.             }
    127.             else
    128.             {
    129.  
    130.                 //TODO - change this out for attack
    131.                 var mv = Vector3.up * Game.GRAV;
    132.                 _motor.Controller.Move(mv * Time.deltaTime);
    133.                 _animator.DefaultMovementAnimations.PlayIdle();
    134.             }
    135.  
    136.         }
    137.  
    138.         void IMovementStyle.OnUpdateMovementComplete()
    139.         {
    140.  
    141.         }
    142.  
    143.         #endregion
    144.  
    145.     }
    146.  
    147. }
    148.  
    With this, someone in the editor using our T&I system (like UnityEvent) could tell the zombie to target just about anything no matter how they have it referenced and not have to manhandle it into the correct type. Rather the implicit conversion does it for us.
     
    MV10, omegabytestudio and Kiwasi like this.
  17. omegabytestudio

    omegabytestudio

    Joined:
    Nov 9, 2015
    Posts:
    77
    @BoredMormon

    It's a case of derived class and skills not working the same way. All skills use the base Skills class, but some require gameobject for targeting, others don't, and some accept both.

    @lordofduct

    At first I thought it looked crazy complicated, then I read your code and it's actually a pretty good idea, and makes a lot of sense.(Is misspelling Defualt a kind of trademark? :p)

    I might try your idea in my code, thanks for the example! :)
     
  18. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,658
    In which case go for the boxing solution. Which is pretty much what @lordofduct suggested.
     
    omegabytestudio likes this.
  19. omegabytestudio

    omegabytestudio

    Joined:
    Nov 9, 2015
    Posts:
    77
    And that worked!

    Did I simpler version of @lordofduct script, and does the job!

    Thank you everyone, special thanks to @BoredMormon and @lordofduct for the final solution!
     
  20. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    6,980
    I hadn't even noticed that........ lol.

    That's just my bad typing skills.

    Sure I type 100 word like series of characters per minute. Doesn't mean they're actual words. :p

    Happy it worked for you. Yeah, it could definitely be simplified. The IPlanarSurface is definitely not needed (it's used in 2.5d games where the gameworld may actual swerve to and frow in 3d, but the gameplay remains 2d).
     
unityunity