Cubism Demo
[toc]
需求
功能需求
- 方塊吸附放置
- 背景放滿通關
- 方塊扔出復回
表現需求
- 背景表現,燈光,材質
- 擺放表現
設計與實現
方塊吸附放置
表現:待放方塊進入背景之中會自動吸附到背景相對應的位置之上,旋轉角度吸附到最貼近的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);
}
}
結果如下
screenshot.png
感覺可能和幀更新的執行順序有關,射線檢測的位置沒錯,但是此時方塊還沒還沒真正更新過去,導致沒有被檢測到,等下一個物理幀更新過后,再去檢測,就正常了
總結:由于物理幀和普通幀的不同步,射線檢測還是盡量在物理幀中去檢測,因為物理幀(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);
}
}
%){_BQBM9~N31CP13OTW4ME.png