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. Dismiss Notice

Class or struct?

Discussion in 'Scripting' started by Deleted User, May 12, 2014.

  1. Deleted User

    Deleted User

    Guest

    I guess this question has been answered before, but would like to know some more info on it. I have done a custom struct that is just a Vector2 with int values instead of float. Is nothing fancy is just x,y and few operators and functions, not all the functions a real Vector2 have as I don't need them.

    What I'm not sure is about performance, here is my struct:

    Code (csharp):
    1.  
    2. public struct Vector2Int {
    3.     public int x,y;
    4.  
    5.     //Simula Vector2.zero
    6.     static public Vector2Int zero
    7.     {
    8.         get
    9.         {
    10.             return new Vector2Int(0,0);
    11.         }
    12.     }
    13.  
    14.     //Simula Vector2.one
    15.     static public Vector2Int one
    16.     {
    17.         get
    18.         {
    19.             return new Vector2Int(1,1);
    20.         }
    21.     }
    22.  
    23.     //Simula Vector2.up
    24.     static public Vector2Int up
    25.     {
    26.         get
    27.         {
    28.             return new Vector2Int(0,1);
    29.         }
    30.     }
    31.  
    32.     //Simula Vector2.right
    33.     static public Vector2Int right
    34.     {
    35.         get
    36.         {
    37.             return new Vector2Int(1,0);
    38.         }
    39.     }
    40.  
    41.     //Vector2Int con overload
    42.     public Vector2Int (int a, int b)
    43.     {
    44.         x = a;
    45.         y = b;
    46.     }
    47.  
    48.     //Operatore addizione
    49.     static public Vector2Int operator +(Vector2Int a, Vector2Int b)
    50.     {
    51.         return new Vector2Int(a.x + b.x, a.y + b.y);
    52.     }
    53.  
    54.     //Operatore sottrazione
    55.     static public Vector2Int operator -(Vector2Int a, Vector2Int b)
    56.     {
    57.         return new Vector2Int(a.x - b.x, a.y - b.y);
    58.     }
    59.  
    60.     //Operatore moltiplicazione
    61.     static public Vector2Int operator *(Vector2Int a, Vector2Int b)
    62.     {
    63.         return new Vector2Int(a.x * b.x, a.y * b.y);
    64.     }
    65.  
    66.     //Overrride funzione ToString
    67.     public override string ToString()
    68.     {
    69.         return "(" + x + ", " + y + ")";
    70.     }
    71. }
    I have the same also for Vector3 and Vector4, but no need to past them too.
    I don't need to have it serializable in editor as is always set at runtime, but I'm not sure if would be better as class rather than struct.
     
    Last edited by a moderator: May 12, 2014
  2. Munchy2007

    Munchy2007

    Joined:
    Jun 16, 2013
    Posts:
    1,731
    Last edited: May 12, 2014
  3. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,716
    From what I recall, Unity has big issue serializing struct that are not native to itself.
    If serialization is not an issue for you, that could work.

    Also, like Munchy's link explains, it also depends of your use. Unity's Vector2-3-4 are useful because of the way they are handled. In Unity, when you pass a Vector around, you don't want to manually make copies of it all the time. Having it as a struct makes sense.
     
  4. numberkruncher

    numberkruncher

    Joined:
    Feb 18, 2012
    Posts:
    953
    When passing large struct types around it is better to pass by reference instead of by value. I made this change to my matrix manipulation logic and it made a massive difference when I profiled the code:
    Code (csharp):
    1.  
    2. public void CalculateSomeMatrix(out Matrix4x4 mat) {
    3.     Matrix4x4 someOtherMatrix = transform.localToWorldMatrix;
    4.     DoSomethingToMatrix(out mat, ref someOtherMatrix);
    5. }
    6.  
    7. private void DoSomethingToMatrix(out Matrix4x4 mat, ref Matrix4x4 otherMat) { ... }
    8.  
    9. // Output matrix is written straight to result memory without
    10. // needing to be copied several times as method returns.
    11. Matrix4x4 result;
    12. CalculateSomeMatrix(out result);
    13.  
     
  5. Deleted User

    Deleted User

    Guest

    I use it only for movements in a grid based system, a vector2 of ints help me to get straight into a 2D array with just a simple function. I use the values also to add to the real transform of the object for movement in worldspace. This is the only use for now.

    Here a small portion of the movement code:

    Code (csharp):
    1.  
    2. using UnityEngine;
    3.  
    4. public class PlayerBehaviour : MonoBehaviour {
    5.  
    6.     private Transform t;                //Questo transform (caching)
    7.     private EXMainCamera cam;           //La camera principale
    8.     private float nStep;                //Ritardo per il prossimo passo
    9.  
    10.     public DIRECTION direction;         //Direzione corrente
    11.     public Vector2Int position;         //Posizione nella griglia
    12.  
    13.     public EXTerrain terrain;           //Il tipo di terreno corrente
    14.        
    15.     //Prepara il giocaotre
    16.     void Start () {
    17.         t = this.transform;
    18.  
    19.         if (!cam)
    20.             cam = Camera.main.GetComponent<EXMainCamera> ();
    21.  
    22.         position = new Vector2Int (3, 1);
    23.  
    24.         MoveInGrid (direction, position);
    25.     }
    26.  
    27.     //Movimento nella griglia
    28.     public void MoveInGrid(DIRECTION dir, Vector2Int cell) {
    29.  
    30.         if(EXSystem.Navigation.CanMoveInGrid(cell)) {
    31.             position = cell;
    32.             terrain = EXSystem.Navigation.ReturnTerrainInGrid (position);
    33.             t.position = new Vector3(position.x + .5f, -position.y - .5f, t.position.z);
    34.         }
    35.  
    36.         direction = dir;
    37.  
    38.         cam.CameraFollow ();
    39.  
    40.         nStep = Time.time + terrain.friction;
    41.     }
    42.    
    43.     // Input di movimento
    44.     void Update () {
    45.  
    46.         if (Input.GetKeyDown ("Up") Time.time > nStep)
    47.             MoveInGrid(DIRECTION.North, new Vector2Int(position.x,position.y - 1));
    48.    
    49.     }
    50. }
     
    Last edited by a moderator: May 12, 2014
  6. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,377
    Your struct is fine. It's 2 integers. It's only 8 bytes in size for those two fields. I wouldn't worry much about it. You don't need to pass as ref or anything like that... it's smaller in size than Vector3 is, and the same size as Vector2.

    I will say though that I don't like the idea of calling it a Vector, because it doesn't act like a Vector, which comes from math and has very specific properties to it that yours does not (and can not) have.
     
  7. Deleted User

    Deleted User

    Guest

    Yeah but is more about recognition for myself as I'm the only one that is gonna work with the code, so I'm ok with it. But I get your point.
     
  8. numberkruncher

    numberkruncher

    Joined:
    Feb 18, 2012
    Posts:
    953
    One thing that I would always do with custom structs is to implement the IEquatable namespace since this helps to avoid boxing/unboxing which occur when using certain .NET collection types (the generic ones). Also, instead of using static properties for constant values, just use readonly values since this is a little faster.

    Here is how I would probably implement this struct (warning, not tested):
    Code (csharp):
    1.  
    2. public struct Vector2Int : IEquatable<Vector2Int> {
    3.  
    4.     public static readonly Vector2Int zero = new Vector2Int(0, 0);
    5.     public static readonly Vector2Int one = new Vector2Int(1, 1);
    6.     public static readonly Vector2Int up = new Vector2Int(0, 1);
    7.     public static readonly Vector2Int right = new Vector2Int(1, 0);
    8.  
    9.     public int x;
    10.     public int y;
    11.    
    12.     public Vector2Int(int x, int y) {
    13.         this.x = x;
    14.         this.y = y;
    15.     }
    16.    
    17.     public Vector2Int traverse {
    18.         get {
    19.             Vector2Int value;
    20.             value.x = y;
    21.             value.y = x;
    22.             return value;
    23.         }
    24.     }
    25.     public Vector2 inverse {
    26.         get {
    27.             Vector2Int value;
    28.             value.x = -x;
    29.             value.y = -y;
    30.             return value;
    31.         }
    32.     }
    33.    
    34.     public override string ToString() {
    35.         return string.Format("X: {0}, Y: {0}", x, y);
    36.     }
    37.    
    38.     public bool Equals(Vector2Int other) {
    39.         return x == other.x  y == other.y;
    40.     }
    41.    
    42.     public override bool Equals(object obj) {
    43.         if (!(obj is Vector2Int))
    44.             return false;
    45.         Vector2Int other = (Vector2Int)obj;
    46.         return x == other.x  y == other.y;
    47.     }
    48.    
    49.     public override int GetHashCode() {
    50.         return x ^ y;
    51.     }
    52.    
    53.     public static bool operator == (Vector2Int lhs, Vector2Int rhs) {
    54.         return lhs.x == rhs.x  lhs.y == rhs.y;
    55.     }
    56.     public static bool operator != (Vector2Int lhs, Vector2Int rhs) {
    57.         return lhs.x != rhs.x  lhs.y != rhs.y;
    58.     }
    59.  
    60.     public static Vector2Int operator + (Vector2Int lhs, Vector2Int rhs) {
    61.         lhs.x += rhs.x;
    62.         lhs.y += rhs.y;
    63.         return  lhs;
    64.     }
    65.     public static Vector2Int operator - (Vector2Int lhs, Vector2Int rhs) {
    66.         lhs.x -= rhs.x;
    67.         lhs.y -= rhs.y;
    68.         return  lhs;
    69.     }
    70.     public static Vector2Int operator - (Vector2Int value) {
    71.         value.x = -value.x;
    72.         value.y = -value.y;
    73.         return  value;
    74.     }
    75.  
    76.     public static Vector2Int operator * (Vector2Int lhs, Vector2Int rhs) {
    77.         lhs.x *= rhs.x;
    78.         lhs.y *= rhs.y;
    79.         return  lhs;
    80.     }
    81.     public static Vector2Int operator / (Vector2Int lhs, Vector2Int rhs) {
    82.         lhs.x /= rhs.x;
    83.         lhs.y /= rhs.y;
    84.         return  lhs;
    85.     }
    86.     public static Vector2Int operator % (Vector2Int lhs, Vector2Int rhs) {
    87.         lhs.x %= rhs.x;
    88.         lhs.y %= rhs.y;
    89.         return  lhs;
    90.     }
    91.  
    92.     public static implicit operator Vector2Int(Vector2 vec) {
    93.         Vector2Int result;
    94.         result.x = (int)vec.x;
    95.         result.y = (int)vec.y;
    96.         return result;
    97.     }
    98.     public static implicit operator Vector2Int(Vector3 vec) {
    99.         Vector2Int result;
    100.         result.x = (int)vec.x;
    101.         result.y = (int)vec.y;
    102.         return result;
    103.     }
    104.     public static implicit operator Vector2(Vector2Int vec) {
    105.         Vector2 result;
    106.         result.x = vec.x;
    107.         result.y = vec.y;
    108.         return result;
    109.     }
    110.     public static implicit operator Vector3(Vector2Int vec) {
    111.         Vector3 result;
    112.         result.x = vec.x;
    113.         result.y = vec.y;
    114.         return result;
    115.     }
    116.  
    117. }
    118.  
     
    Last edited: May 12, 2014
  9. numberkruncher

    numberkruncher

    Joined:
    Feb 18, 2012
    Posts:
    953
    I have just updated that source code with implicit conversion operators for the Unity Vector2 and Vector3 types.
     
  10. numberkruncher

    numberkruncher

    Joined:
    Feb 18, 2012
    Posts:
    953
    I would agree, "Point" or "Point2" would be a more accurate name.
     
  11. Deleted User

    Deleted User

    Guest

    Thanks for the tips, didn't know with readonly would be faster. So for what I understand from reading also the msdn docs, adding System.IEquatable interface would prevent the use of Reflection and would be faster when using for example a List
     
  12. numberkruncher

    numberkruncher

    Joined:
    Feb 18, 2012
    Posts:
    953
    It simply avoids an allocation each time yourStruct.Equals(anotherStruct) is used. This is applicable in cases when using generic collections like List, HashSet, Dictionary, etc.

    For instance:
    Code (csharp):
    1.  
    2. List<Vector2Int> listOfPoints = FetchListOfPoint();
    3.  
    4. if (listOfPoints.Contains(Vector2Int.zero)) {
    5.     // If IEquatable<Vector2Int> is implemented, we just avoided potentially a LOT of allocations!
    6. }
    7.  
    The boxing allocation occurs since you are otherwise passing a value type into a function which expects a reference. If you cast to (object) then you incur the allocation since the value has to be 'boxed' into a reference type.
     
    Last edited: May 12, 2014
  13. Dustin-Horne

    Dustin-Horne

    Joined:
    Apr 4, 2013
    Posts:
    4,568
    Not exactly... The biggest reason for IEquatable<T> is so you can do comparisons without boxing and unboxing occurring, which sometimes happen when you don't realize it, as well as for use with Generics. It allows you to explicitly define your equality comparison.

    Another point of consideration is garbage collection. I don't typically get overly hung up on the "16 byte" rule. To illustrate why that rule is in place, let's say you have the following function:

    Code (csharp):
    1.  
    2. DoSomething(Foo bar);
    3.  
    Now, if "Foo" is a class, then only the 32-bit (4 byte) pointer to that instance is passed. Whereas if it's a struct, a copy of the object gets passed. Let's say your struct itself is actually 16 bytes. You could pass 4 integers (or 4 class references) to a function for the same approximate cost of passing 1 struct. And since the struct gets copied, you're allocating more memory, however it's on the stack (not the heap) so it's more efficient and not subject to garbage collection.

    So, if you're not passing your structs around a lot, even if they're larger than 16 bytes it may not hurt you... especially if you're creating a lot of them as you won't suffer from allocation on the heap and garbage collection.

    There is another catch though... and that is when using collections. Any time an array has to be resized or added to another array... it has to copy all of that data (and with larger structs this becomes more expensive). This is one place where generics can bite you as well. Consider the following:

    Code (csharp):
    1.  
    2. //Define generic list of Foo with initial capacity of 4 elements
    3. List<Foo> fooList = new List<Foo>(4);
    4.  
    5. //Add 100 "Foos" to the list
    6. for(var i = 0; i < 100; i++)
    7. {
    8.    fooList.Add(new Foo());
    9. }
    10.  
    By default a List starts with a capacity of "0" but her we specified it as 4 just for fun. If we didn't specify it, the first time you add an item it will set the capcity to "4", then it will double every time you exceed it (so when add the 5th element it becomes 8, when you add the 8th element it becomes 16, etc.).

    Internally the List stores items in an array so every time that array has to be resized it results in a copy of the data... so in the case of structs actually anything larger than 4 bytes (32-bit) will be more costly to copy/resize than a reference type because it's copying more data.

    One more point to note... recursion. If you're writing recursive functions it can come back to haunt you. First of all, every function call itself is stored on the stack... As the function recurses with classes it passes a reference (32-bit pointer)... but with structs it passes a copy so if you have large structs you can start eating up memory.

    Just some things to chew on and think about. Use what makes sense to you and your application. The best thing to do is just try... do some real world tests and see how it impacts your application. It's pretty simple to just change from Class to Struct (you don't even have to remove your IEquatable implementation) and see what the performance is like.
     
  14. Deleted User

    Deleted User

    Guest

    I guess I can't tell yet how far I'm going with this performance wise as I'm doing a simple operation that is hard to quantify for now, but I appreciate all the technical info, is really something to keep in mind if stuff start falling apart later on. And yeah changing from struct to class isn't a big deal, so I'm safe to go on and change later without a load of work.

    Thanks everyone for the insight into the matter.