How to have objects with multiple parents capable of seperate rotation

Hi everyone I am currently making a game where the player basically rotates Rubik's cubes. What i need to do specifically is make it so the player is able to rotate a row of cubes (example X-axis row 2) But i want it so that the player is only able to rotate along one axis at a time in increments of 90 degrees. To do this i have created 27 cubes and 9 empty game objects as the rotational planes for each X,Y and Z axis. Where i am getting confused is how to switch the parents of the 27 cubes dependent on their position and then make it so the player can move them (e.g. by holding shift and running down the axis he/she is facing along)

At the moment i have some scripting done but its not working or complete. This was a test script to see if i could parent the left x-axis cubes to the emptyX1 gameobject and then make them rotate along the x-axis using a collision test. Am i on the right track?

var rotation = 1.0;

function OnCollisionEnter(collision : Collision) {

    Cube1.transform.parent = GameObject.Find(EmptyX1);  
    Cube4.transform.parent = GameObject.Find(EmptyX1);  
    Cube7.transform.parent = GameObject.Find(EmptyX1);  
    Cube10.transform.parent = GameObject.Find(EmptyX1);  
    Cube13.transform.parent = GameObject.Find(EmptyX1);  
    Cube16.transform.parent = GameObject.Find(EmptyX1);  
    Cube19.transform.parent = GameObject.Find(EmptyX1);  
    Cube22.transform.parent = GameObject.Find(EmptyX1);  
    Cube26.transform.parent = GameObject.Find(EmptyX1);  

    if (Input.GetKey ("left shift")) {
        transform.Rotate(Time.deltaTime * 45, 0, 0);  
    }
}

any help would be greatly appreciated as im starting to think i may have bitten off more then i can chew

Avoid using Find

You really shouldn't need to use the Find function - it's expensive. Just use a fairly stable parenting hierarchy, proper tags along with GameObject.FindWithTag and GameObject.FindGameObjectsWithTag, or if it comes down to it,Object.FindObjectOfType, or Object.FindObjectsOfType. Even if you do find it necessary, you probably only need to do it once at the start of a script and then you can cache the result.

The OnCollisionEnter usage as you describe seems confusing, but that's because I'm not sure what is colliding exactly. If I take your meaning correctly though, you intend to have your character "run" on the cube face and rotate the plane in the opposite direction that they are running about the side axis relative to your character. Is your character's gravity is always towards the center of the cube (annoying to transition at the corners as your gravity can change direction) or is the gravity always in in the same direction in the world? Unless your character can rotate the planes with the character's forward direction facing into the cube, the forward direction will be the up direction to check against and since your facing will always be the opposite direction of your gravity.

In the first two examples, I will assume that the character is always on the top face of the cube and cannot rotate with their forward direction into the cube.

With parenting

Parenting is very doable, but the problem I find is selecting and managing the parents. The problem comes when you rotate the centre pivot which also rotates 4 other pivots and you need to differentiate them - you could tag or name it specifically to solve this. To simplify finding the pivots (whether they are cubes or separate gameobjects),I recommend that the all share a common parent (rubix within my code here) so that they are in the same coordinate system. Also, at some point you have to clean up the parenting as it would swap when pieces rotate into position.

Using a shorter example for brevity, we assume that the player can only ever run on the top of the cube:

private static var currentAxis : Vector3;

function OnCollisionEnter(collision : Collision) {
    //define some buttons in the input manager = more configurable
    if(Input.GetButton("Run")) {
        //Get the root of the rubix 
        //All cubes and pivots have a shared parent/grandparent
        var rubix : Transform = transform.parent;
        if(rubix.tag == pivot) rubix = rubix.parent;

        //Figure out which way is up
        var sideUpDot : float  = Vector3.Dot(rubix.forward, 
                                             collision.transform.forward);
        var upUpDot : float = Vector3.Dot(rubix.right,
                                          collision.transform.forward);

        //Setup for parenting
        var angle : float = -45;
        var pivot : Transform;
        var alignedCubes : Array = new Array();
        if(Mathf.Abs(upUpDot) > Mathf.Abs(sideUpDot)) {
            //Check if we need to clear parenting
            if(currentAxis != rubix.forward) {
                currentAxis = rubix.forward;
                //get all grandchildren
                for(var child : Transform in rubix)
                    if(child.tag == "pivot") //centerPivot is also tagged "pivot"
                        for(var grandchild : Transform in child)
                            alignedCubes.push(grandchild);
                for(var child : Transform in alignedCubes)
                    child.parent = rubix;
                alignedCubes = new Array();
            } //Clear parenting

            for(var child : Transform in rubix)
                if(child.localPosition.z == transform.localPosition.z) {
                    if(child.name == "centerPivot" ||
                       child.tag == "pivot" && pivot == null) pivot = child;
                    else alignedCubes.push(child);
                } //Check for aligned cubes
            if(upUpDot >=0) angle = 45;
        } //Check parenting on the forward axis
        else {
            //Check if we need to clear parenting
            if(currentAxis != rubix.right) {
                currentAxis = rubix.right;
                //get all grandchildren
                for(var child : Transform in rubix)
                    if(child.tag == "pivot") //centerPivot is also tagged "pivot"
                        for(var grandchild : Transform in child)
                            alignedCubes.push(grandchild);
                for(var child : Transform in alignedCubes)
                    child.parent = rubix;
                alignedCubes = new Array();
            } //Clear parenting

            for(var child : Transform in rubix)
                if(child.localPosition.x == transform.localPosition.x) {
                    if(child.name == "centerPivot" ||
                       child.tag == "pivot" && pivot == null) pivot = child;
                    else alignedCubes.push(child);
                } //Check for aligned cubes
            if(sideUpDot >=0) angle = 45;
        } //Setup on for the cube's side axis

        if(pivot != null) { //Should always be true
            //Add Parenting if it is needed
            if(alignedCubes.length > 6) //Should always be true;
                for(var child : Transform in alignedCubes)
                    child.parent = pivot;
            //Rotate
            pivot.Rotate(currentAxis, Time.deltaTime * angle);
        } //We have a pivot
    } //We're running
} //OnCollisionEnter(collision : Collision)

Without parenting

The same brief example without parenting:

//These act like parenting would
private static var plane0 : Array = new Array();
private static var plane1 : Array = new Array();
private static var plane2 : Array = new Array();

private static var currentAxis : Vector3;

function OnCollisionEnter(collision : Collision) {
    //define some buttons in the input manager = more configurable
    if(Input.GetButton("Run")) {
        //Get the root of the rubix 
        //All 26 cubes have the same parent (center cube hidden, so why have it?)
        var rubix : Transform = transform.parent;

        //Figure out which way is to the side
        var sideUpDot : float  = Vector3.Dot(rubix.forward, 
                                             collision.transform.forward);
        var upUpDot : float = Vector3.Dot(rubix.right,
                                          collision.transform.forward);

        //Setup for rotation
        var angle : float = -45;
        var alignedCubes : Array = new Array();
        if(Mathf.Abs(upUpDot) > Mathf.Abs(sideUpDot)) {
            //Check if we need to clear the planes
            if(currentAxis != rubix.forward) {
                currentAxis = rubix.forward;
                plane0 = new Array();
                plane1 = new Array();
                plane2 = new Array();
            } //Clear the planes

            //Check the if the plane is already setup;
            if(plane0.length > 0 && 
               plane0[0].localPosition.z == transform.localPosition.z)
                   alignedCubes = plane0;
            else if(plane1.length > 0 &&
               plane1[0].localPosition.z == transform.localPosition.z)
                   alignedCubes = plane1;
            else if(plane2.length > 0 &&
               plane2[0].localPosition.z == transform.localPosition.z)
                   alignedCubes = plane2;
            else {//Setup the plane
                for(var child : Transform in rubix)
                    if(child.localPosition.z == transform.localPosition.z) 
                        alignedCubes.push(child);
                if(alignedCubes.length < 7) //should never happen
                    {Debug.Log("plane misaligned"); return;}
                if(plane0.length == 0) plane0 = alignedCubes;
                else if(plane1.length == 0) plane1 = alignedCubes;
                else if(plane2.length == 0) plane2 = alignedCubes;
                else {Debug.Log("cubes misaligned"); return;} //Should never happen
            } //Setup the planes
            if(upUpDot >=0) angle = 45;
        } //Setup the forward axis
        else {
            //Check if we need to clear the planes
            if(currentAxis != rubix.right) {
                currentAxis = rubix.right;
                plane0 = new Array();
                plane1 = new Array();
                plane2 = new Array();
            } //Clear the planes

            //Check the if the plane is already setup;
            if(plane0.length > 0 && 
               plane0[0].localPosition.x == transform.localPosition.x)
                   alignedCubes = plane0;
            else if(plane1.length > 0 &&
               plane1[0].localPosition.x == transform.localPosition.x)
                   alignedCubes = plane1;
            else if(plane2.length > 0 &&
               plane2[0].localPosition.x == transform.localPosition.x)
                   alignedCubes = plane2;
            else { //Setup the plane
                for(var child : Transform in rubix)
                    if(child.localPosition.x == transform.localPosition.x) 
                        alignedCubes.push(child);
                if(alignedCubes.length < 7) //should never happen
                    {Debug.Log("plane misaligned"); return;}
                if(plane0.length == 0) plane0 = alignedCubes;
                else if(plane1.length == 0) plane1 = alignedCubes;
                else if(plane2.length == 0) plane2 = alignedCubes;
                else {Debug.Log("cubes misaligned"); return;} //Should never happen
            } //Setup the planes
            if(sideUpDot >=0) angle = 45;
        } //Setup the side axis

        //Rotate
        for(var child : Transform in alignedCubes)
            child.RotateAround(rubix.position, currentAxis, Time.deltaTime * angle);
    } //We're running
} //OnCollisionEnter(collision : Collision)

The whole thing

If you cannot assume how the character will collide with the cube, let alone which direction is up or down or forward relative to your rotations, then it becomes a fair bit longer. If the colliders to which this is attached are not aligned with the rubix, you would need to check which face of the rubix is most parallel or negatively parallel to one of the normals of the collision.contacts as well.

The full version looks more like:

//enum for facing
enum Axes { Front, Back, Right, Left, Top, Bottom}

//These act like parenting would
private static var plane0 : Array = new Array();
private static var plane1 : Array = new Array();
private static var plane2 : Array = new Array();

private static var currentAxis : Vector3;    

function OnCollisionEnter(collision : Collision) {
    //define some buttons in the input manager = more configurable
    if(Input.GetButton("Run")) {

        //All 26 cubes have the same parent (center cube hidden, so why have it?)
        var rubix : Transform = transform.parent;   

        //Figure out which side we are colliding with
        //Assumes the face's normal is aligned with the rubix's
        var faceNormal : Vector3 = collision.contacts[0].normal;

        //***If the character's forward can face into the cube***
        //Decide what to use as a relative up - most orthogonal
        var collisionUp;
        var normUpDot : float = Vector3.Dot(faceNormal,
                                            collision.transform.up);
        var normForeDot : float = Vector3.Dot(faceNormal, 
                                              collision.transform.forward);
        if(Mathf.Abs(normUpDot) < Mathf.Abs(normForeDot))
            collisionUp = collision.transform.up;
        else collisionUp = collision.transform.forward;
        //*******************************************************

        //Figure out which way is to the side
        var sideUpDot : float;
        var upUpDot : float;
        var angle : float = -45;
        var axis : Vector3;
        //which of the remaining faces
        if(faceNormal == rubix.forward || faceNormal == -rubix.forward) {
            //Select the most orthogonal as right
            sideUpDot = Vector3.Dot(rubix.right, collisionUp);
            upUpDot = Vector3.Dot(rubix.up, collisionUp);
            if(Mathf.Abs(upUpDot) > Mathf.Abs(sideUpDot)) {
                axis = rubix.up;
                if(upUpDot >=0) angle = 45;
            }
            else {
                axis = rubix.right;
                if(sideUpDot >=0) angle = 45;
            }
        } //if the +/- forward axis is our relative up axis
        else if(faceNormal == rubix.right || faceNormal == -rubix.right) {
            //Select the most orthogonal as right
            sideUpDot = Vector3.Dot(rubix.forward, collisionUp);
            upUpDot = Vector3.Dot(rubix.up, collisionUp);
            if(Mathf.Abs(upUpDot) > Mathf.Abs(sideUpDot)) {
                axis = rubix.up;
                if(upUpDot >=0) angle = 45;
            }
            else {
                axis = rubix.forward;
                if(sideUpDot >=0) angle = 45;
            }
        } //if the +/- right axis is our relative up axis
        else {
            //Select the most orthogonal as right
            sideUpDot = Vector3.Dot(rubix.forward, collisionUp);
            upUpDot = Vector3.Dot(rubix.right, collisionUp);
            if(Mathf.Abs(upUpDot) > Mathf.Abs(sideUpDot)) {
                axis = rubix.up;
                if(upUpDot >=0) angle = 45;
            }
            else {
                axis = rubix.forward;
                if(sideUpDot >=0) angle = 45; 
            }
        }//if the +/- up axis is our relative up axis

        if(axis == null) { Debug.Log("Invalid axis."); return;}//Shouldn't happen

        //Check if we need to clear the planes
        if(currentAxis != axis) {
            currentAxis = axis;
            plane0 = new Array();
            plane1 = new Array();
            plane2 = new Array();
        } //Clear the planes

        //Setup for rotation
        var alignedCubes : Array = new Array();
        if(axis == rubix.forward) {
            //Check the if the plane is already setup;
            if(plane0.length > 0 && 
               plane0[0].localPosition.z == transform.localPosition.z)
                   alignedCubes = plane0;
            else if(plane1.length > 0 &&
               plane1[0].localPosition.z == transform.localPosition.z)
                   alignedCubes = plane1;
            else if(plane2.length > 0 &&
               plane2[0].localPosition.z == transform.localPosition.z)
                   alignedCubes = plane2;
            else {//Setup the plane
                for(var child : Transform in rubix)
                    if(child.localPosition.z == transform.localPosition.z) 
                        alignedCubes.push(child);
                if(alignedCubes.length < 7) //should never happen
                    {Debug.Log("plane misaligned"); return;}
                if(plane0.length == 0) plane0 = alignedCubes;
                else if(plane1.length == 0) plane1 = alignedCubes;
                else if(plane2.length == 0) plane2 = alignedCubes;
                else {Debug.Log("cubes misaligned"); return;} //Should never happen
            } //Setup the plane
        } //Setup the forward axis
        else if(axis == rubix.up) {
            //Check the if the plane is already setup;
            if(plane0.length > 0 && 
               plane0[0].localPosition.y == transform.localPosition.y)
                   alignedCubes = plane0;
            else if(plane1.length > 0 &&
               plane1[0].localPosition.y == transform.localPosition.y)
                   alignedCubes = plane1;
            else if(plane2.length > 0 &&
               plane2[0].localPosition.y == transform.localPosition.y)
                   alignedCubes = plane2;
            else {//Setup the plane
                for(var child : Transform in rubix)
                    if(child.localPosition.y == transform.localPosition.y) 
                        alignedCubes.push(child);
                if(alignedCubes.length < 7) //should never happen
                    {Debug.Log("plane misaligned"); return;}
                if(plane0.length == 0) plane0 = alignedCubes;
                else if(plane1.length == 0) plane1 = alignedCubes;
                else if(plane2.length == 0) plane2 = alignedCubes;
                else {Debug.Log("cubes misaligned"); return;} //Should never happen
            } //Setup the plane
        } //Setup the up axis
        else {
            //Check the if the plane is already setup;
            if(plane0.length > 0 && 
               plane0[0].localPosition.x == transform.localPosition.x)
                   alignedCubes = plane0;
            else if(plane1.length > 0 &&
               plane1[0].localPosition.x == transform.localPosition.x)
                   alignedCubes = plane1;
            else if(plane2.length > 0 &&
               plane2[0].localPosition.x == transform.localPosition.x)
                   alignedCubes = plane2;
            else { //Setup the plane
                for(var child : Transform in rubix)
                    if(child.localPosition.x == transform.localPosition.x) 
                        alignedCubes.push(child);
                if(alignedCubes.length < 7) //should never happen
                    {Debug.Log("plane misaligned"); return;}
                if(plane0.length == 0) plane0 = alignedCubes;
                else if(plane1.length == 0) plane1 = alignedCubes;
                else if(plane2.length == 0) plane2 = alignedCubes;
                else {Debug.Log("cubes misaligned"); return;} //Should never happen
            } //Setup the plane
        } //Setup the side axis

        //Rotate
        for(var child : Transform in alignedCubes)
            child.RotateAround(rubix.position, currentAxis, Time.deltaTime * angle);
    } //We're running
} //OnCollisionEnter(collision : Collision)

Improvements

I recommend smoothing the angles/rotations so that they snap when they are close to 90's, particularly when you try to rotate on a different axis. If you want to smooth your rotations, add inertia and make it look cool, or just to have a better idea of the state of the rubix, one way is that you could always store that data at the parent level in a separate script which the collider script will write to.

  • If you add a script to the rubix parent which contains public non-static variables for the rotation actions, storing the three current angles, the cubes on those planes of rotation, the speed of each rotation and the axis (X or Z (or Y if you can run on other sides of the cube than the top)).
  • In your collisions, if the axis on the parent is the same and if the plane of rotation's aligned cubes are present (adding them if they are not), in stead of rotating, you would add to the speed of the given rotational plane, clamping at a max speed (do it in a function, that way you can make the max speed a non-static variable and tweak it).
  • If the axis is not the same, you would check if the three angles are increments of 90 or close enough (add a helper function to the parent script to make it smoothly snap when they are close enough). If the angles are increments of 90 on all three angles, clear out the aligned cubes, zero the speeds, change the axis and then add the aligned cubes and speeds as you would if the direction were the same. If the angles are not close enough, just return.
  • In the Update function of the rubix, you would perform the actual rotations about the axis indicated at the speeds indicated and after you rotate, you would then decrease the speeds towards 0. Speed additions would be negative if the rotation were about a negative axis in the above code.

In general, I think you're on the right track "re-parenting" the objects. However, I'm wondering why you're using OnCollisionEnter ... the way I would approach this: Let the player select which row to turn, and in which direction (which "plane"). That would be the time where I'd switch the parents; then let the player do the turn.

To make things simply, you might consider using some more convenient datastructures, like lists for each "rotation plane". So instead of having lots and lots of statements to assign the parents, you'd just have a few loops (possibly, you'd end up with a single loop in a method that you call with different parameters). The tradeoff is that you'd have to once drag all the cubes into the component (script / MonoBehavior) that has those arrays/lists.

sorry i left out, the reason i have the OnCollisionEnter is because the player physically runs around the cube to rotate it. So there is a little character that can run around all sides (Im hoping to be able to use scripts ive found based on super mario galaxy) and then the player e.g. holds "shift" and runs forward and this brings that plane/gameobject with the appropriate cubes backwards so the edge they are facing comes towards them rotating around the axis they are facing down. I dont know how i forgot to mention that.