What does = actually do?

Hi, I’m new to Unity and scripting and this forum, so try not to bite my face off.

The thread title might be a bit misleading. I think I more or less know what = does, but I’ve ran across seemingly inconsistent behaviour, so even though I (think I) know how to fix my problem, I thought I should ask here to figure out what happened.

Background: I just started scripting, so the first thing I tried to do was make the camera revolve around a target (which will at some point be the player) by moving the mouse. I did that by making a new class that handles spherical coordinates around a custom axis. It can convert these to Vector3, or get a Vector3 and find the coordinates. It went well, until I tried to make the camera get closer to the player if there is a collider in the way and then when the collider’s gone, smoothly go back to its default position.

Problem: So I’ve made my class called SphericalCoords and to get that functionality I decided I needed three fields of its type (three SphericalCoords objects? Forgive the terminology if it’s wrong. Please correct me if it is) to have spherical coordinates for the max distance, min distance and position of the camera that is derived from the former.

So after having declared and set sphecoMax (for the max distance) I wrote:

SphericalCoords sphecoMin = sphecoMax; // this line is obviously not doing what I thought it would do sphecoMin.r = targetRayHit.point.magnitude;

Anyway the problem seems to be that the new variable doesn’t hold a copy of sphecoMax, it holds sphecoMax itself?

When I change sphecoMin.r, sphecoMax.r also changes…

I thought this wouldn’t happen because I’d seen and written similar code with Vector3 and it works but I checked it to make sure. And sure this code that is similar to the above works:

Vector3 testVector2 = testVector;
testVector2.y = 10;

y only changes in testVector2 and testVector remains unchanged…

So… I guess I have to make a method to duplicate a SphericalCoords object and do what I thought = would do.

But still, I want to clear this up so the question is: why does = work differently with Vector3? Can I make my class work like that? Did I miss something else?

Sorry for asking if this is already explained in tutorials. It feels pretty basic, but I haven’t ran across it yet, and thought it wouldn’t hurt to ask here.

It’s probably because SphericalCoords is a ‘class’, where as Vector3 is a ‘struct’.

In C# there are 2 major data types:

Reference Types - classes/interfaces/boxed values - these are objects that are referenced. When setting a variable to the object of another variable, they’ll both share the same object in memory.

Value Types - numeric primitives/structs/enums - these are values that are not referenced (unless explicitly boxed). When setting a variable to the value of another variable, they’ll both have distinct copies from one another in memory.

Change your SphericalCoords from class to struct.

1 Like

Vector2, Vector3, Vector4 are of type struct. When you use = you are making a copy not giving a pointer reference. If Vector2, Vector3, Vector4 was a class then what you propose would work because you are passing the reference and not the value.

Thanks, that must be it. Also for the links, I’ll read up on them.

Alright, follow up question. I read a bit, then turned it into a struct. Then my constructor stopped working, so I googled to see how to properly initialize a struct. I immediately came across people stating that mutable structs are evil… so I went back to try and make it work as a class. I searched about how to copy classes properly instead of referencing the same object and immediately came across answers like “copying an object is a terrible thing to do”.

So I have to do one of those two terrible evil things (unless I misunderstood what “mutable” means). Do you have any advice on which one to pick? Or is there a third solution I’m not aware of?

“mutable structs are evil”… no, not necessarily. Absolute statements that “x” is “evil” is often bs.

This data should be a struct. Just like Vector3 is a struct. You’re storing a value, not an object.

As for the constructor not working, what was the error? I’m betting it was something about not all of the fields be initialized. A 'struct’s constructor must initialize all fields to some value.

Can you show us your data type?

Yeah, that was the error. I didn’t want to initialize all fields immediately, so I searched around and came across what I mentioned last post.

You mean post the script? Sure, it could also be helpful for anyone who wants spherical coordinates, but I bet there’s something better for this purpose out there.

I just finished working turning it into a class again so I’m posting the class, but you’re right, it’s probably better off as a struct.

Fair warning: It’s messy, it has some commented out code and, I’m willing to bet, it’s full of bad programming practices.
If anyone here has any advice to make it better or more concise, let me know.
If it’s terrible, also let me know. I won’t get better otherwise.

This is the spherical coordinates class:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SphericalCoords {

    public float fi;
    public float theta;
    public float r;

    public Vector3 vector;

    public Vector3 vectorUp;
    public Vector3 vectorForward;
    public Vector3 vectorRight;

    float thlimit;

    float costh;
    float sinth;
    float cosfi;
    float sinfi;

    float coordinateUp;
    float coordinateForward;
    float coordinateRight;

    //constructors
    public SphericalCoords()
    {
        thlimit = 0.01f;

        vector = Vector3.forward;

        CartesianToSpherical(vector);
    }

    public SphericalCoords(float fedFi, float fedTheta, float fedR)
    {
        fi = fedFi;
        theta = fedTheta;
        r = fedR;
        SphericalToCartesian();

        vectorUp = Vector3.up;
        vectorForward = Vector3.forward;
        vectorRight = Vector3.right;

        vector = coordinateRight * vectorRight + coordinateUp * vectorUp + coordinateForward * vectorForward;
    }

    public SphericalCoords(Vector3 fedVector)
    {
        CartesianToSpherical(fedVector);
        thlimit = 0.01f;
    }

    /*private void Start()
    {
        CartesianToSpherical(Vector3.one+Vector3.up);
        Debug.Log(fi);
        Debug.Log(theta);
        Debug.Log(vector);
        ChangeOriginRotation(Vector3.one, new Vector3(1,1,-2), true);
        Debug.Log(fi);
        Debug.Log(theta);
        Debug.Log(vector);
    }*/

    //Moves vector on a sphere by changing spherical coordinates.
    //The cartesian system is defined by vectorUp vectorForward and vectorRight.
    public void MoveOnSphere(float fiDis=0f, float thetaDis=0f, float rDis=0f)
    {
        fi += fiDis;//mouseX;
        theta += thetaDis;//mouseY;
        theta = Mathf.Clamp(theta, thlimit, Mathf.PI - thlimit);
        r += rDis;

        SphericalToCartesian();

        vector = coordinateRight * vectorRight + coordinateUp * vectorUp + coordinateForward * vectorForward;
    }

    //Takes vector, gives spherical coordinates.
    //The cartesian system has no rotation.
    public void CartesianToSpherical(Vector3 convertee)
    {
        r = convertee.magnitude;
        theta = Mathf.Acos(Mathf.Clamp(convertee.y / r, -1, 1));
        fi = Mathf.Atan2(convertee.x , convertee.z);

        vectorUp = Vector3.up;
        vectorForward = Vector3.forward;
        vectorRight = Vector3.right;

        vector = convertee;
    }

    //Changes origin orientation. Possible to also change vector rotation or stay where it was.
    public void ChangeOriginRotation(Vector3 newUp, Vector3 newForward, bool vectorUnchanged = false)
    {
        newUp.Normalize();
        newForward.Normalize();
        Vector3 newRight = Vector3.Cross(newUp, newForward);

        vectorUp = newUp;
        vectorForward = newForward;
        vectorRight = newRight;

        if (vectorUnchanged)
        {
            theta = Vector3.Angle(newUp, vector)*Mathf.Deg2Rad;

            Vector3 projection = Vector3.ProjectOnPlane(vector, newUp).normalized;
            fi = Vector3.Angle(newForward, projection) * Mathf.Deg2Rad;
            if ((Vector3.Cross(newForward, newRight) + Vector3.Cross(newForward, projection).normalized).magnitude
                < Vector3.Cross(newForward, newRight).magnitude)
            {
                fi = -fi + 2 * Mathf.PI;
            }
            SphericalToCartesian();
            vector = coordinateRight * vectorRight + coordinateUp * vectorUp + coordinateForward * vectorForward;
        }
        else
        {
            vector = coordinateUp * newUp + coordinateForward * newForward + coordinateRight * newRight;
        }
    }

    public void DuplicateSphericalCoords(SphericalCoords duplicator)
    {
        thlimit = duplicator.thlimit;
        vector = duplicator.vector;
        CartesianToSpherical(vector);
        ChangeOriginRotation(duplicator.vectorUp, duplicator.vectorForward, true);
    }

    void SphericalToCartesian()
    {
        costh = Mathf.Cos(theta);
        sinth = Mathf.Sin(theta);
        cosfi = Mathf.Cos(fi);
        sinfi = Mathf.Sin(fi);

        coordinateUp = r * costh;
        coordinateForward = r * cosfi * sinth;
        coordinateRight = r * sinfi * sinth;
    }

/*
    void FindOriginAngles(Vector3 newUp, Vector3 newForward, Vector3 newRight)
    {
        Vector3 addedCoordinates = (vectorUp + vectorForward + vectorRight);
        addedCoordinates.Normalize();
        Vector3 projection = Vector3.ProjectOnPlane(addedCoordinates, newUp).normalized;

        originTheta = Mathf.Acos(Vector3.Dot(newUp, addedCoordinates));
        originFi = Mathf.Acos(Vector3.Dot(newForward, projection));
        if ((Vector3.Cross(newForward, newRight) + Vector3.Cross(newForward, projection).normalized).magnitude
            < Vector3.Cross(newForward, newRight).magnitude)
        {
            originFi = -originFi + 2 * Mathf.PI;
        }
    }*/
}

And this is some code I’ve written to test it:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SphericalCoordsChecker : MonoBehaviour {

    public Transform cam;
    public Transform target;
    public SphericalCoords sphecoMax = new SphericalCoords();
    public SphericalCoords sphecoMin = new SphericalCoords();
    public SphericalCoords sphecoFinal = new SphericalCoords();
    float click;

    float mouseX, mouseY;
    float mouseSensitivity = 0.1f;

    Ray targetToCam;
    RaycastHit targetRayHit;

    void Awake () {
        sphecoMax.CartesianToSpherical(transform.position - target.position);
        //initially I'd written sphecoMax = new SphericalCoords(); here instead of at the
        //top (I had declared them at the top), but it gave me a NullReferenceException.
        //If you know why, let me know.
    }
    
    void Update () {
       
        click = Input.GetAxisRaw("Fire1");

        mouseX = Input.GetAxis("Mouse X") * mouseSensitivity;
        mouseY = Input.GetAxis("Mouse Y") * mouseSensitivity;

        sphecoMax.MoveOnSphere(mouseX, mouseY, 0);

        AnchorCollisionChecker();

        transform.position = target.position + sphecoFinal.vector;

        if (click == 1)
        {
            sphecoMax.ChangeOriginRotation(cam.up, cam.forward, true);
        }
    }

    void AnchorCollisionChecker()
    {
        targetToCam.origin = target.position;
        targetToCam.direction = sphecoMax.vector;

        sphecoMin.DuplicateSphericalCoords(sphecoMax);

        if (Physics.Raycast(targetToCam, out targetRayHit, sphecoMax.r))
        {
            Vector3 rayHitPoint = targetRayHit.point;
           
            sphecoMin.r = rayHitPoint.magnitude;
        }

        sphecoFinal.DuplicateSphericalCoords(sphecoMax);

        if (sphecoMax.r > sphecoMin.r)
        {
            sphecoFinal.r = sphecoMin.r;
        }

        sphecoFinal.MoveOnSphere();
    }
}

I know using System.Collections;using System.Collections.Generic; are unnecessary but I’m keeping them until I’m done with the code in case I need them.

OK… so yeah, I can see why you probably having some issues.

Your SphericalCoords class/struct is way too big. Like, why are all those fields/variables members of SphericalCoords?

There’s a lot wrong going on here with your design.

For starters the ‘mutable is evil’ comments you may have read… they’re talking about things like your ‘MoveOnSphere’ and ‘CartesianToSpherical’ methods. Note how they’re member methods, as opposed to static methods. Where as with Vector3 you have methods like Vector3.Lerp, which is static. You pass in Vector3’s and get new Vector3’s back. Note, the methods being static isn’t what makes them immutable rather than mutable… it’s that the syntax of the method is clear that you pass in one value, and get back another value. The argument about mutable structs is because sometimes the syntax isn’t clear what value you’ve modified.

Take for instance Vector3.Lerp and how it’s used:

CODE A

transform.position = Vector3.Lerp(transform.position, someTargetPosition, 0.4f);

It’s clear that you passed in the position, and you’re setting the position to the output of Lerp. There’s no question about what is being set. Where as lets pretend Lerp was a member level method, it might look like this:

CODE B

transform.position.Lerp(someTargetPosition, 0.4f);

This is BAD… for starters it won’t work. Because position is a property a copy is returned, so you’ve just lerped the copy and it never gets set to anything. The way this method would have to be done is like this:

CODE C

var v = transform.position;
v.Lerp(someTargetPosition, 0.4f);
transform.position = v;

Not only is it longer and more verbose, but it also can be written in a manner that looks correct but is not!

Where as the static method (the way unity has it designed) forces you to write in a syntax that it’s clear what’s going on. There’s no way to write something like CODE B with the static Lerp method.

As for your class…

This whole structure should only have fields for what makes spherical coordinates. phi, rho, and theta (oh, and it’s phi, not fi). Anything that is constant, like thetalimit, should be a ‘const’. And a lot of the other fields really only necessary within some method’s scope, so therefore shouldn’t be class/struct level fields at all.

I also presume from the looks of your methods that the fields should be limited on set, and shouldn’t be freely set. So you may want to use properties to control that. Or just have a Normalize method to constrict them since really any angle is fine technically.

You should really be starting with something like this:

[System.Serializable]
public struct SphericalCoords
{

    public float phi;
    public float theta;
    public float r;

    #region CONSTRUCTOR

    public SphericalCoords(float phi, float theta, float r)
    {
//normalize these values if that's what you want
        this.phi = phi;
        this.theta = theta;
        this.r = r;
    }

    #endregion
 
    #region Static Methods
 
    public static Vector3 ToCartesian(SphericalCoords sv)
    {
        float st = Mathf.Sin(sv.theta);
        return new Vector3(sv.r * st * Mathf.Cos(sv.phi), sv.r * st * Mathf.Sin(sv.phi), sv.r * Mathf.Cos(sv.theta));
    }

    public static SphericalCoords ToSpherical(Vector3 v)
    {
        float r = v.magnitude;
        return new SphericalCoords(Mathf.Atan(v.y / v.x), Mathf.Acos(v.z / r), r);
    }

//add in your methods as you want them:
    public static SphericalCoords MoveOnSphere(SphericalCoords sv, float deltaPhi, float deltaTheta, float delatRho) {
        //TODO - implement
    }

    #endregion

}

Note how I don’t have things like ‘coordinateUp’, and when I want to convert to cartesian I return a Vector3 rather than set fields on the SphericalCoord. Why would SphericalCoord contain fields for cartesian coordinates? It’s supposed to be Spherical… not cartesian!

Thanks a lot for all your help! I gave it a quick read since I have to get some sleep, but I’ll read carefully tomorrow. I’ll scrap it and try again.

I added vectorUp etc. because I wanted the origin to be able to change rotation but obviously my implementation is very very flawed. Next time, I’ll have to find a way to keep it smaller and tidier.

Only a sith deals in absolutes

1 Like