Cubism Demo
需求
功能需求
- 方块吸附放置
- 背景放满通关
- 方块扔出复回
表现需求
- 背景表现,灯光,材质
- 摆放表现
设计与实现
方块吸附放置
表现:待放方块进入背景之中会自动吸附到背景相对应的位置之上,旋转角度吸附到最贴近的90度倍数角上。
分析:实现的关键在于连续量转离散量。连续量是物体的位置和旋转;背景和方块都由很多小方块组成,位置匹配点也是小方块的轴心位置,所以位置离散量是一个小方块的位置为间隔,旋转离散量以90度为间隔。
想让待放方块位置匹配到背景之中,并且发生合理的旋转,需要找到待放方块和背景中一对一的那对子方块。
选择出合理的一对匹配点
减少候选子方块的数量有利于提高性能。所以选择已经进入背景的子方块作为待放方块的候选子方块,再从子方块里随便选一个方块作为中心,用范围检测找出最近的背景锚点子方块,再以背景锚点子方块为中心从候选子方块中找出最近的子方块作为轴心。改变待选方块轴心后,将待选方块放在背景锚点子方块的位置上,旋转角度进行“标准化”。
/// <summary>
/// 松手且进入背景范围,动态改变物体轴心,比较轴心子方块与背景子方块位置,将方块吸附到最近的位置
/// </summary>
public void CheckCubePos()
{
//if (firstArriveCubeTrans != null && !firstArriveCube.isGrabbed)
if (arrivedCubes.Count != 0 && !currentCube.isGrabbed)
{
//背景里每个子方块的位置作为锚点,由一个个带碰撞体的方块组成;
//物体里任何进入背景的每个子方块的位置可作为新轴心;
//确定轴心分三步,第一步确定一个临时轴心,默认为数组第一个子方块;
Transform pivotCube = arrivedCubes[0];
//第二步,由这个临时轴心确定一个最近的背景落脚点
//比较新轴心与背景方块的位置
//以比较点为中心使用重叠球形检测出最近的背景方块;
Collider[] BKcolliders = Physics.OverlapSphere(pivotCube.position, radius, 1 << LayerMask.NameToLayer("BackgroundCube"));
Collider nearestBKCollider = null;
float minDis = 1;
foreach (var collider in BKcolliders)
{
float temDis = Vector3.Distance(collider.transform.position, pivotCube.position);
print(collider.name + "与" + pivotCube.name + "距离为" + temDis);
if (temDis < minDis)
{
minDis = temDis;
nearestBKCollider = collider;
}
}
if (nearestBKCollider != null)
{
//第三步,由这个背景落脚点确定最终轴心,并设置轴心,从已经进入背景的方块找一个离最终点最近的
Transform nearestCube = null;
minDis = 1;
foreach (var cube in arrivedCubes)
{
float temDis = Vector3.Distance(cube.position, nearestBKCollider.transform.position);
print(cube.name + "与" + pivotCube.name + "距离为" + temDis);
if (temDis < minDis)
{
minDis = temDis;
nearestCube = cube;
}
}
pivotCube = nearestCube;//重置最终轴心;
SetNewPivot3(currentCube.transform, pivotCube.position);
//设置位置+旋转
赋值方块位置最近点+偏移值;
//Vector3 finalPos = nearestCollider.transform.position - pivotCube.localPosition * pivotCube.lossyScale.x;
//因为轴心以改变,最终位置就是最近背景方块的位置
Vector3 finalPos = nearestBKCollider.transform.position;
//旋转按照最近轴算
Quaternion finalQuaternion = Quaternion.Euler(CorrectAngle(currentCube.transform.eulerAngles.x), CorrectAngle(currentCube.transform.eulerAngles.y), CorrectAngle(currentCube.transform.eulerAngles.z));
currentCube.transform.SetPositionAndRotation(finalPos, finalQuaternion);
currentCube.GetComponent<Rigidbody>().isKinematic = true;
print(pivotCube.rotation.eulerAngles);
检查是否通关
//if (CheckIsFull())
//{
// Debug.LogError("通关!");
// UIMain.Instance.ShowPanel<WinPanel>();
//}
StartCoroutine(CheckIsFull3());
}
}
}
/ <summary>
/ 检查背景是否放满了方块
/ </summary>
IEnumerator CheckIsFull3()
{
//isFull = true;
yield return new WaitForFixedUpdate();//等待物理帧更新
isFull = true;
Collider[] colliders;
//对每个背景子方块进行重叠盒型检测,若都有东西,则返回真
for (int i = 1; i < BKCubes.Length; i++)
{
colliders = Physics.OverlapBox(BKCubes[i].position, (BKCubes[i].GetComponent<BoxCollider>().size * transform.lossyScale.x) / 2.0f,
Quaternion.identity, 1 << LayerMask.NameToLayer("GrabbableCube"));
print("通关条件Debug" + BKCubes[i].name + "范围内检测到的方块有");
for (int j = 0; j < colliders.Length; j++)
{
print("通关条件Debug" + colliders[j].name);
}
if (colliders.Length == 0)
{
isFull = false;
break;
}
}
if (isFull)
{
Debug.LogError("通关!");
UIMain.Instance.ShowPanel<WinPanel>();
}
}
改变轴心
改变轴心实际上就是改变父物体相对子物体的位置;欲改变相对位置,需要先解除父子关系,将原父物体设置到轴心位置上,然后将子物体设回父物体。
注意:
- 使用Transform.SetParent()方法设置父物体,似乎有一种“闪现”的逻辑在里面,具体表现在如果你在触发器内部设置父物体,触发器会连续触发,且只触发进入事件,而不触发出去事件。
- 之前用过不改变父子关系,只改变子物体位置方法去改变轴心。具体做法为先保存原子物体的位置,然后将父物体设置到轴心位置处,接着再把子物体位置还原为原先保存的位置。但是不知道为啥完全没用。
/// <summary>
/// 设置新轴心并且保持子物体世界坐标不变
/// </summary>
public void SetNewPivot3(Transform obj, Vector3 pivotPos)
{
//改变轴心实际上就是改变父物体相对子物体的位置
//改变父物体前先移除父子关系
Transform[] subTransforms = obj.GetComponentsInChildren<Transform>();
foreach (var item in subTransforms)
{
item.SetParent(environment, true);
}
//将准轴心物体放置到新轴心的世界坐标上
obj.position = pivotPos;
//父物体再设回来,此时轴心改变
foreach (var item in subTransforms)
{
item.SetParent(obj, true);
}
print("以重置轴心点");
}
坑:
- 由触发器造成的BUG很多;除了上面写的,在写判定是否放满时尤为突出。
- 最简单的放满判定方法时判定,进入背景的方块数是否到达最大数目,统计进入背景方块数是由触发器统计的,因为吸附效果需要瞬间更改位置,可能导致触发器离开事件不触发,进入背景的方块就会统计非常不准。
- 范围检测;第二种判定方法是范围检测,检测每个背景方块是否都有待放子方块。但是检测的结果很迷,虽然检测逻辑是在物体吸附逻辑之后,但不知道是不是具体执行时机的问题,每次结果都不太一样。
- 经过一个小试验,发现点问题,执行下面代码
private void FixedUpdate()
{
print("前" + cube.position);
cube.SetPositionAndRotation(cube.position + Vector3.forward * 3, cube.rotation);
print("后" + cube.position);
Collider[] colliders = Physics.OverlapBox(cube.position, cube.GetComponent<BoxCollider>().size / 2);
foreach (var item in colliders)
{
print("立刻检测" + item.name);
}
}
结果如下
感觉可能和帧更新的执行顺序有关,射线检测的位置没错,但是此时方块还没还没真正更新过去,导致没有被检测到,等下一个物理帧更新过后,再去检测,就正常了
总结:由于物理帧和普通帧的不同步,射线检测还是尽量在物理帧中去检测,因为物理帧(internal physics update)过后游戏物理世界才真正发生了变化(比如说碰撞体更新)。所以若有需求一帧瞬移且检测,最好间隔一个物理帧后再检测。
private void FixedUpdate()
{
print("前" + cube.position);
cube.SetPositionAndRotation(cube.position + Vector3.forward * 3, cube.rotation);
print("后" + cube.position);
Collider[] colliders = Physics.OverlapBox(cube.position, cube.GetComponent<BoxCollider>().size / 2);
foreach (var item in colliders)
{
print("立刻检测" + item.name);
}
StartCoroutine(OverlapFixed());
}
IEnumerator OverlapFixed()
{
yield return new WaitForFixedUpdate();//若改为yield return nullj,结果还是不准确的
Collider[] colliders = Physics.OverlapBox(cube.position, cube.GetComponent<BoxCollider>().size / 2);
foreach (var item in colliders)
{
print("等一物理帧检测" + item.name);
}
}