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这块可能近期不会涉及。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!