- Home /
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
As a side note, Find() is inherently slow. Ins$$anonymous$$d of calling it 9 times, searching the same object, call it once, assing it to a var, and then simply set all the parents to that var. If EmptyX1 is always the same object, you could even do this single Find() in Start().
Answer by skovacs1 · Aug 23, 2010 at 10:47 PM
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.
Answer by jashan · Aug 23, 2010 at 06:37 AM
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.
Answer by Dangermouse · Aug 23, 2010 at 08:45 AM
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.
This isn't an answer to the question. Add notes like this as comments where asked or to the question or edit the question to properly reflect your meaning.
Your answer
Follow this Question
Related Questions
Find Transform in the scene 2 Answers
Properly Rotating Child Objects by Script 1 Answer
Set parent of instantiated object. 0 Answers
Parenting from code doesn't really change the hierarchy 1 Answer
Locating Position of Object not Working? 2 Answers