Unity中尝试实现3D魔方

前言:这期demo有点失败

需求:想了很久,以前都没时间做,就是在Unity中实现3D魔方,主要逻辑放在玩家操作逻辑上。

思路:整个demo由model和ctrler两个脚本组成。通过玩家点击获取方块,理论上单个方块可以确定三个面,但是玩家操作都是二维的,所以最终只能确定两个面。旋转时,我们把即将旋转的所有方块全部放在一个临时的GameObject下,旋转这个GameObject即实现旋转整个面。玩家松开鼠标时,就释放这个临时的GameObject。如果玩家没有点击方块,而是点击到空白处,就通过鼠标移动偏移量,判断并旋转整个魔方(实现了观察整个魔方的功能)

最终实现效果:玩家点击方块并移动鼠标时,判断是水平还是垂直操作?然后就可以获取所有将要参与旋转的方块,把这些方块变成蓝色便于观察,并且中途切换操作面(由水平操作变成垂直操作)是行不通的。玩家松开鼠标时,释放并归位(旋转度数始终要保持为整数,0或者90或者-90度)。点击空白处并拖动时,旋转整个魔方。但此demo依旧存在bug和设计上的不足:1.未实现shader(一个方块最多可展示3个面,三个面的颜色不同),视觉效果不好;2.不存在判断魔方是否已经拼接完成的逻辑;3.旋转整个魔方时,因为我是操作的欧拉角,所以会存在抖动(Unity的旋转矩阵变换我并不熟悉),于是我限制旋转整个魔方时,单次操作(鼠标按下松开)最多只能旋转90,并且x轴旋转和y轴旋转只能2选1,不能x轴的旋转未归0的情况下操作y轴旋转。若是直接操作四元数,表现效果会好很多;4.在获取即将旋转的所有方块时,我是通过判断x或者y轴是否相同来实现的,可是如果整个魔方是斜着的,那么x轴和y轴都不相同,则不会操作任何方块(bug),目前解决思路是,可以通过在魔方中间加入一个不会参与旋转的十字架,需要判断是否参与旋转的方块时,不要直接判断x或者y是否相同,而是判断是否在同一条直线上。

实现:

魔方model脚本,包含生成、旋转、旋转模型功能

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using DG.Tweening;

public class MagicCube : MonoBehaviour
{
    private readonly float speed = 0.8f;
    private readonly int length = 3;
    private readonly int width = 3;
    private readonly int height = 3;
    private readonly Vector3 gap = new Vector3(1.2f, 1.2f, 1.2f);

    public GameObject prefab;
    private Transform[] allCubes;
    private GameObject rotateObj;//临时旋转父物体
    private List<Transform> targetRotateCubes;
    private Vector3 originAngle;
    private DragDir m_dir;
    private Transform model;//整个魔方的Transform

    private void Start()
    {
        allCubes = new Transform[length * width * height];
        Spawn();
    }

    private void Spawn()
    {
        Vector3 pos = Vector3.zero;
        int index = 0;
        for (int i = 0; i < length; i++)
        {
            for (int j = 0; j < height; j++)
            {
                for (int k = 0; k < width; k++)
                {
                    pos.Set((i + 1) * gap.x, (j + 1) * gap.y, (k + 1) * gap.z);
                    GameObject obj = GameObject.Instantiate(prefab);
                    obj.transform.position = pos;
                    allCubes[index] = obj.transform;
                    index++;
                }
            }
        }
        model = new GameObject("Model").transform;
        model.parent = transform;
        Vector3 center = (allCubes[allCubes.Length - 1].position + allCubes[0].position) / 2f;
        model.position = center;
        for (int i = 0; i < allCubes.Length; i++)
        {
            allCubes[i].parent = model.transform;
        }
    }

    private List<Transform> GetCubes(DragDir dir, Vector3 pos)
    {
        List<Transform> result = new List<Transform>();

        for (int i = 0; i < allCubes.Length; i++)
        {
            bool b1 = dir == DragDir.Vertical && Mathf.Abs(allCubes[i].position.x - pos.x) <= 0.2f;
            bool b2 = dir == DragDir.Horizental && Mathf.Abs(allCubes[i].position.y - pos.y) <= 0.2f;
            if (b1 || b2)
            {
                result.Add(allCubes[i]);
            }
        }
        return result;
    }

    private Vector3 GetAngle(DragDir dir, float ratio)
    {
        m_dir = dir;
        Vector3 angle = Vector3.zero;
        switch (dir)
        {
            case DragDir.None:
                break;
            case DragDir.Horizental:
                angle = new Vector3(0, Mathf.Clamp(speed * -ratio, -90, 90), 0);
                break;
            case DragDir.Vertical:
                angle = new Vector3(Mathf.Clamp(speed * ratio, -90, 90), 0, 0);
                break;
        }
        return angle;
    }

    /// <summary>
    /// 旋转
    /// </summary>
    /// <param name="dir"></param>
    /// <param name="pos"></param>
    /// <param name="ratio"></param>
    public void Rotate(DragDir dir, Vector3 pos, float ratio)
    {
        if (rotateObj == null)
        {
            targetRotateCubes = GetCubes(dir, pos);
            Vector3 center = (targetRotateCubes[targetRotateCubes.Count - 1].position + targetRotateCubes[0].position) / 2f;//这是通过第一个和最后一个坐标获取中心点,这是不准确的
            rotateObj = new GameObject();
            rotateObj.transform.position = center;
            rotateObj.transform.eulerAngles = Vector3.zero;
            for (int i = 0; i < targetRotateCubes.Count; i++)
            {
                targetRotateCubes[i].parent = rotateObj.transform;
                MeshRenderer mr = targetRotateCubes[i].GetComponent<MeshRenderer>();
                mr.material.color = Color.blue;
            }
        }
        Vector3 angle = GetAngle(dir, ratio);
        rotateObj.transform.eulerAngles = angle;
    }

    /// <summary>
    /// 结束单次旋转
    /// </summary>
    public void EndRotate()
    {
        if (rotateObj == null)
            return;

        float angle = 0;
        switch (m_dir)
        {
            case DragDir.None:
                break;
            case DragDir.Horizental:
                angle = rotateObj.transform.eulerAngles.y;
                break;
            case DragDir.Vertical:
                angle = rotateObj.transform.eulerAngles.x;
                break;
        }
        if (angle >= 0 && (angle <= 45f || angle >= -45f))
        {
            angle = 0;
        }
        else if (angle > 45f && angle <= 90f)
        {
            angle = 90f;
        }
        else if (angle <= -45f && angle >= -90f)
        {
            angle = -90f;
        }
        switch (m_dir)
        {
            case DragDir.None:
                break;
            case DragDir.Horizental:
                rotateObj.transform.eulerAngles = new Vector3(0, angle, 0);
                break;
            case DragDir.Vertical:
                rotateObj.transform.eulerAngles = new Vector3(angle, 0, 0);
                break;
        }
        for (int i = 0; i < targetRotateCubes.Count; i++)
        {
            MeshRenderer mr = targetRotateCubes[i].GetComponent<MeshRenderer>();
            mr.material.color = Color.white;
            targetRotateCubes[i].parent = model;
        }
        GameObject.Destroy(rotateObj);
        rotateObj = null;
    }

    /// <summary>
    /// 旋转整个魔方
    /// </summary>
    /// <param name="dir"></param>
    /// <param name="ratio"></param>
    public void RotateModel(DragDir dir, float ratio)
    {
        Vector3 angle = GetAngle(dir, ratio);
        model.eulerAngles = angle;
    }
}

Ctrler脚本,主要逻辑在Update里写的,主要是一些射线检测、鼠标位移逻辑

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

public enum DragDir
{
    None,
    Horizental,
    Vertical
}
public class Ctrler : MonoBehaviour
{
    private readonly string cubeTag = "cube";
    private readonly float moveOffsetThreshold = 1f;

    private DragDir currentDir;
    private Vector3 lastPos;
    private Vector3 dragOffset;
    private Transform selectCube;
    private MagicCube model;

    private void Start()
    {
        model = GetComponent<MagicCube>();
    }

    private void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            lastPos = Input.mousePosition;
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit hitInfo;
            if (Physics.Raycast(ray, out hitInfo))
            {
                if (hitInfo.collider.CompareTag(cubeTag))
                {
                    selectCube = hitInfo.collider.transform;
                }
            }
        }
        if (Input.GetMouseButton(0))
        {
            dragOffset = Input.mousePosition - lastPos;
            if (Mathf.Abs(dragOffset.x) >= moveOffsetThreshold || Mathf.Abs(dragOffset.y) >= moveOffsetThreshold)
            {
                if (currentDir == DragDir.None)//一旦确认方向后,在抬起鼠标前不可再次更改
                {
                    if (Mathf.Abs(dragOffset.x) > Mathf.Abs(dragOffset.y))
                    {
                        currentDir = DragDir.Horizental;
                    }
                    else
                    {
                        currentDir = DragDir.Vertical;
                    }
                }

                float ratio = 0;
                if (currentDir == DragDir.Horizental)
                {
                    ratio = dragOffset.x;
                }
                else if (currentDir == DragDir.Vertical)
                {
                    ratio = dragOffset.y;
                }

                if (selectCube != null)
                {
                    model.Rotate(currentDir, selectCube.position, ratio);
                }
                else
                {
                    model.RotateModel(currentDir, ratio);
                }
            }
        }
        else
        {
            selectCube = null;
            lastPos = Vector3.zero;
            currentDir = DragDir.None;
            model.EndRotate();
        }
    }
}

 

 

 

 

总结:

虽然不存在大的bug,但是demo依旧是失败的,未能实现现实中的魔方操作。主要问题存在于旋转和shader这块。有空时会多加学习四元数这块,Shader这块可能近期不会涉及。

 

posted @ 2022-12-21 15:03  军酱不是酱  阅读(216)  评论(1编辑  收藏  举报