0
\$\begingroup\$

I am trying to make a Rubik's cube simulator. I built the Cube by arranging 27 pieces in appropriate place. I wanna simulate the cube like This.
This is what my cube looks like.

Cube

I successfully wrote a simple script to rotate the whole cube while dragging the mouse on background. I was also able to rotate sides with key inputs like U, F etc. For this I created 6 empty game objects (UpLayer, FrontLayer etc.) and positioned them in centre of corresponding sides. I store every piece in an array. Now, when the user presses a key, suppose U, I

  1. Make all 9 pieces of upper layer child of empty game object UperLayer at runtime.
  2. Rotate the UperLayer object around its local y axis 90 degrees.
  3. Make required changes in the array.
  4. Un-parent all the pieces.

It works perfectly for every layer. But now, I wanna rotate the layer when user drags it with mouse (Check the above link). I can't think of any approach to do so. I need help.

\$\endgroup\$
1
  • \$\begingroup\$ I don’t know enough about Unity in particular to provide example code, but it sounds like you basically just want to map mouse drags in particular areas to the same code the keypresses activated. If that’s the case then I suggest recording mouse drags with a known expected result and drawing them as lines relative to the cube, to get a feeling for when dragging should activate a particular button. Then “just” figure out the math to categorize drags appropriately. \$\endgroup\$ – Ryan1729 Jun 23 '20 at 12:22
0
\$\begingroup\$

Here's an outline of the steps.

  1. Cast a ray through the mouse position (using Camera.ScreenPointToRay()) to hit a boxcollider on your sub-cube. This will give you the position and normal of the point.

  2. You have two potential axes of rotation: the two axes perpendicular to the normal. But we don't yet know which one the player wants to rotate around.

    Add a small multiple of each of these axes to your hit point in the world, and project those back to the screen (using Camera.WorldToScreenPoint()), then subtract the position of the cursor and normalize. Now you have two unit vectors in screenspace representing the valid drag directions. Store them to use on subsequent frames.

  3. While the player keeps the mouse button held, subtract the current mouse position from the screen position where they first clicked to get the displacement of the drag so far.

    Take the dot product of this displacement with each of the screenspace unit vectors you computed previously to get the pixel travel in that direction. Take the absolute value of each. Whichever one first exceeds a sensitivity threshold you choose (accounting for a little wobble when starting the drag), that one gets chosen as the rotation direction, and the opposite axis becomes the axis of rotation.

    Now we're ready to start rotating.

  4. Gather all 8-9 sub-cubes in the row/column perpendicular to your chosen rotation axis, and store them in a collection, along with their initial offset from the center of the cube, and their initial orientation.

  5. Form a rotation quaternion spinning about your rotation axis, by a number of degrees proportional to your drag's pixel distance along your rotation direction vector.

    If desired, keep track of a second rotation quaternion that starts at the identity, and "chases" the one above at a controlled speed or blend rate, so you can smooth sudden changes, and use this one to transform your sub-cubes.

  6. Transform all your sub-cubes' initial offsets & rotations by this quaternion, and place them there.

  7. When the player releases, find the closest multiple of 90 degrees to their last rotation angle, and blend your transformation quaternion to that value. Don't let the player start a new drag until this is complete.

  8. Once you've finished blending to the rounded rotation, you can clear all these drag state variables and re-enable a new drag.

    This is also a good time to go apply a small fix-up to any moved cubes, ensuring they've landed exactly at integer coordinates, and that their orientation is one of the 24 cardinal orientations, so you prevent any rounding error from accumulating over repeated drags.

If you have trouble with any particular one of the steps above, folks here should be able to give you more guidance on how to achieve it.


Here's a working example of this in code, now that I've had a chance to test it.

Rubik's Cube in action

This assumes your sub-cubes are axis-aligned, positioned at -1, 0, 1 on each axis, and have box colliders we can use to detect raycast hits or gather them with an overlap query. I also assume you use a single fixed camera angle.

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

public class Rubik : MonoBehaviour
{
    public float minimumDragPixels = 3.0f;
    public float dragSensitivity = 3.0f;
    public float snapSpeed = 90.0f;

    Camera _camera;

    Collider[] _subCubes = new Collider[9];
    Vector3[] _originalPositions = new Vector3[9];
    Quaternion[] _originalOrientations = new Quaternion[9];

    IEnumerator Start() {
        _camera = Camera.main;
        Vector3 camForward = _camera.transform.forward;
        float axisSign = Mathf.Sign(camForward.x * camForward.y * camForward.z);

        while(true) {
            yield return null;

            // Keep waiting until the player presses the mouse button.
            if (!Input.GetMouseButton(0))
                continue;

            // Step 1: Fire a ray through the mouse position.
            Vector2 clickPosition = Input.mousePosition;
            var ray = _camera.ScreenPointToRay(clickPosition);

            // If we didn't hit anything, try again next frame.
            if (!Physics.Raycast(ray, out RaycastHit hit))
                continue;

            // Step 2: find the two potential axes of rotation.
            int normalAxis = Mathf.Abs(Mathf.RoundToInt(Vector3.Dot(
                    hit.normal,
                    new Vector3(0, 1, 2)
                )));

            Vector3 rotationAxis = Vector3.zero;
            Vector3 alternativeAxis = Vector3.zero;

            rotationAxis[(normalAxis + 1) % 3] = 1;
            alternativeAxis[(normalAxis + 2) % 3] = 1;

            // This sign fiddling just ensures our drag direction matches the visible rotation.
            float signFlip = axisSign * Mathf.Sign(Vector3.Dot(rotationAxis, camForward) * Mathf.Sign(Vector3.Dot(alternativeAxis, camForward)));
            Vector2 rotationDirection = signFlip* ScreenDirection(clickPosition, hit.point, alternativeAxis); 
            Vector2 alternativeDirection = -signFlip * ScreenDirection(clickPosition, hit.point, rotationAxis);

            // Step 3: wait until we've dragged far enough to pick an axis.
            float signedDistance;
            do {
                yield return null;

                Vector2 mousePosition = Input.mousePosition;
                signedDistance = DistanceAlong(clickPosition, mousePosition, rotationDirection);
                if (Mathf.Abs(signedDistance) > minimumDragPixels)
                    break;

                signedDistance = DistanceAlong(clickPosition, mousePosition, alternativeDirection);
                if (Mathf.Abs(signedDistance) > minimumDragPixels) {
                    rotationAxis = alternativeAxis;
                    rotationDirection = alternativeDirection;
                    break;
                }

            } while (Input.GetMouseButton(0));

            // Step 4: Gather the 8-9 sub-cubes we need to rotate.
            Vector3 extents = Vector3.one - 0.9f * rotationAxis;
            extents = extents * 2.0f;
            int subCubeCount = Physics.OverlapBoxNonAlloc(hit.collider.transform.position, extents, _subCubes);

            for(int i = 0; i < subCubeCount; i++) {
                var subCube = _subCubes[i].transform;
                _originalPositions[i] = subCube.position;
                _originalOrientations[i] = subCube.rotation;
            }

            // Step 5-6: Rotate the group by the input angle each frame.
            float angle = 0.0f;
            while (Input.GetMouseButton(0)) {
                angle = signedDistance * dragSensitivity;
                RotateGroup(angle, rotationAxis, subCubeCount);

                yield return null;
                Vector2 mousePosition = Input.mousePosition;
                signedDistance = DistanceAlong(clickPosition, mousePosition, rotationDirection);
            }

            // Step 7: After release, snap the angle to a multiple of 90.
            float snappedAngle = Mathf.Round(angle / 90.0f) * 90.0f;

            while(angle != snappedAngle) {
                angle = Mathf.MoveTowards(angle, snappedAngle, snapSpeed * Time.deltaTime);

                RotateGroup(angle, rotationAxis, subCubeCount);
                yield return null;
            }

            // Step 8: Loop back and wait for the next drag.
            // TODO: Consider correcting for any accumulated rounding errors.
        }
    }

    Vector2 ScreenDirection(Vector2 screenPoint, Vector3 worldPoint, Vector3 worldDirection) {
        Vector2 shifted = _camera.WorldToScreenPoint(worldPoint + worldDirection);

        return (shifted - screenPoint).normalized;
    }

    float DistanceAlong(Vector2 clickPosition, Vector2 currentPosition, Vector2 direction) {
        return Vector2.Dot(currentPosition - clickPosition, direction);
    }

    void RotateGroup(float angle, Vector3 axis, int count) {
        
        Quaternion rotation = Quaternion.AngleAxis(angle, axis);

        for (int i = 0; i < count; i++) {
            var subCube = _subCubes[i].transform;
            subCube.position = rotation * _originalPositions[i];
            subCube.rotation = rotation * _originalOrientations[i];
        }
    }
}
\$\endgroup\$
6
  • \$\begingroup\$ It work's when the cube is fixed but doesn't work when rotated. \$\endgroup\$ – Manoj Bhatt Jul 4 '20 at 18:02
  • \$\begingroup\$ Have you considered orbiting your camera around the cube instead? \$\endgroup\$ – DMGregory Jul 4 '20 at 18:47
  • \$\begingroup\$ Didn't think of it. This should work. Thanks \$\endgroup\$ – Manoj Bhatt Jul 4 '20 at 18:51
  • \$\begingroup\$ I added a script to orbit camera around cube on mouse drag but now when the camera orbits 180 degrees, rotation of layers is reversed. How can I fix it? \$\endgroup\$ – Manoj Bhatt Jul 6 '20 at 8:49
  • \$\begingroup\$ See where axisSign is computed outside to loop? Move that whole calculation nside the loop, so you repeat it with the latest camera orientation all the time. \$\endgroup\$ – DMGregory Jul 6 '20 at 11:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.