이 스크립트는 사용자가 어떤 위치에서 시작하든 컵 좌표를 정규화(0,0 기준으로 평행이동) 한 뒤, 허용 오차를 둔 조합 매칭으로 패턴 일치를 판정합니다.
추가로, 진행 중 최대 도달 가능성 을 계산해 아무리 쌓아도 완성 불가면 즉시 실패 처리합니다. 컵이 넘어지면 실시간 모니터링으로 유효 집합에서 제외되어 판정이 재평가됩니다.
시작 위치는 상관이 없습니다.
테이블 어디서든 같은 패턴으로 인정됩니다.
오차 허용: tolerance = cupDiameter * toleranceFactor 로 미세 오차 허용
조합 매칭 + 가능성 평가로 성공/실패를 즉시 알려줌
이 때 좌표는 Vector2Int로 라운딩하므로 격자 단위를 설계와 맞추는 것이 중요합니다
컵을 올리면 월드 좌표를 정수 그리드로 변환해 저장합니다.
public void RegisterCup(Vector3 worldPos, GameObject cupObject)
{
Vector2Int gridPos = new Vector2Int(
Mathf.RoundToInt(worldPos.x),
Mathf.RoundToInt(worldPos.y)
);
if (!placedCups.ContainsKey(gridPos))
{
placedCups.Add(gridPos, cupObject);
// 넘어짐 모니터링 등록
cupTracks[cupObject] = new CupTrack { go = cupObject, rb = cupObject.GetComponent<Rigidbody>() };
CheckForPass(); // 즉시 패턴 일치 시도
if (passInProgress) return;
EvaluateFeasibility(); // 완주 가능성 평가
}
}
패턴 좌표와 후보 좌표를 모두 (minX,minY)를 원점으로 평행이동해 비교합니다.
private List<Vector2Int> NormalizePositions(List<Vector2Int> positions)
{
int minX = positions.Min(p => p.x);
int minY = positions.Min(p => p.y);
return positions.Select(p => new Vector2Int(p.x - minX, p.y - minY)).ToList();
}
배치된 컵이 정답 개수 이상이면, 가능한 모든 조합을 만들어 정규화 후 허용 오차 내에서 1:1 매칭이 되는지 확인합니다.
var combinations = GetCombinations(placedPositions, requiredCount);
foreach (var subset in combinations)
{
var normalizedSubset = NormalizePositions(subset);
bool matched = true;
foreach (Vector2Int target in normalizedPattern)
{
bool found = normalizedSubset.Any(actual => Vector2.Distance(actual, target) < tolerance);
if (!found) { matched = false; break; }
}
if (matched) { /* PASS 처리 */ }
}
일치하는 조합을 찾으면 해당 컵들을 고정시켜 이후 넘어져도 무효화되지 않게 하고, 파티클/점수/다음 단계로 넘깁니다.
foreach (var pos in passedSubset)
{
if (placedCups.TryGetValue(pos, out GameObject cupObj) && cupObj != null)
{
var rb = cupObj.GetComponent<Rigidbody>();
var collider = rb != null ? rb.GetComponent<MeshCollider>() : null;
if (collider != null) collider.isTrigger = true;
if (rb != null) rb.useGravity = false;
successLock.Add(cupObj); // 이후 모니터링 제외
}
}
passInProgress = true;
StartCoroutine(SuccessEffectAndDestroyRoutine());
generatrParticles.GenerateParticle();
scoreManager.Instance.AddScore(1);
주기적으로 컵의 기울기(Up 벡터와 월드 Up의 각) 를 측정해 연속 시간이 임계치를 넘으면 “넘어짐”으로 간주하고 제거/재평가합니다.
float tilt = Vector3.Angle(t.up, Vector3.up);
bool looksNotUpright = tilt > maxTiltDeg;
if (looksNotUpright)
{
track.notUprightAccum += pollInterval;
if (track.notUprightAccum >= fallConfirmTime)
{
OnCupFallen(track.go);
}
}
else track.notUprightAccum = 0f;
아직 PASS는 아니지만, 이미 쌓은 것 + 앞으로 쌓을 수 있는 컵(remainingStacks) 을 더해도 정답 개수에 못 미치면 실패로 간주합니다.
int required = currentPattern.cupPositions.Count;
HashSet<Vector2Int> matchedKeys;
int matched = FindBestOverlap(out matchedKeys); // 현 배치에서 최대로 맞출 수 있는 개수 추정
int potentialMax = matched + Mathf.Max(0, remainingStacks);
if (potentialMax < required)
{
GameManager.Instance.OnPatternFailed();
}
정수 격자: 너무 거칠면 오판정, 너무 촘촘하면 라운딩 충돌.
tolerance: 컵 지름과 사용자 손 떨림을 반영해 0.6~0.9 사이에서 현장 보정 권장.
넘어짐 파라미터: maxTiltDeg, fallConfirmTime, pollInterval을 게임 템포에 맞게 튜닝.
Register: 월드→격자 라운딩, placedCups에 저장
CheckForPass: 조합→정규화→허용 오차 매칭
PASS: 락/연출/점수/다음 패턴
Monitor: 기울기 누적→넘어지면 제거 후 재평가
Feasibility: 도달 불가 시 즉시 실패
cupDiameter = 2f, toleranceFactor = 0.75f
maxTiltDeg = 15~25, fallConfirmTime = 0.2~0.35, pollInterval = 0.03~0.07
remainingStacks는 스폰/배치 로직에서 감소하도록 일관되게 관리
마지막으로, 씬에 빈 오브젝트를 만들고 CupStackManager를 붙인 뒤
CupPatternData 연결
스폰 로직에서 RegisterCup(worldPos, cup) 호출
remainingStacks 관리
이상 패턴인식 관리였습니다.