Speed Stack 패턴등록 알고리즘

John Jean·2025년 8월 23일

Unity

목록 보기
18/19

어떻게 패턴으로 인식을 하는가

이 스크립트는 사용자가 어떤 위치에서 시작하든 컵 좌표를 정규화(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 처리 */ }
}

🍏 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 관리

이상 패턴인식 관리였습니다.

profile
크래프톤 6기 정글러

0개의 댓글