UNITY VR · 기업협약 팀 프로젝트
뛰뛰빵빵 VR (D2B2) 개발일지
이동 시스템 · 신호등 판정 · 미션 NPC · 타임라인 안전가이드 · 기획 오류 수정 경험
목차
이동 시스템 — 3차에 걸친 구조 개선
VR 교육 콘텐츠에서 이동 시스템을 어떻게 설계할지가 초반의 가장 큰 고민이었습니다.
단순 조이스틱 이동은 VR 멀미를 유발하고, 몰입감을 해쳤습니다.
실제로 팔을 흔드는 동작으로 이동해야 한다는 방향을 잡고, 세 번에 걸쳐 구조를 개선했습니다.
손이 Body 충돌체 → MoveFront 충돌체를 순서대로 통과해야만 이동 허용.swingTime(0.5초) 이내에 양쪽이 모두 활성화되지 않으면 ResetFlags()로 초기화.
'우연한 접촉'과 '의도한 스윙'을 구분하기 위한 첫 번째 시도였습니다.
// ScControllerCtrl.cs
private float swingTime = 0.5f; // 타임윈도우
private bool isBody = false; // Body 콜라이더 접촉 여부
private bool isFront = false; // MoveFront 콜라이더 접촉 여부
private void HandleTrigger(ref bool flag)
{
flag = true;
// 양쪽 모두 true여야 이동 허용
if (isBody && isFront && !isMoving)
isMoving = true;
// swingTime 이후 자동 리셋
if (resetRoutine != null) StopCoroutine(resetRoutine);
resetRoutine = StartCoroutine(ResetFlags());
}
IEnumerator ResetFlags()
{
yield return new WaitForSeconds(swingTime);
isBody = false; isFront = false; isMoving = false;
}
⚠️ 발견된 문제점
① 손이 우연히 두 콜라이더를 스치면 이동 트리거 (오작동)
② 이동 방향이 XR Origin forward 기준 → 고개를 돌리면 엉뚱한 방향으로 이동
③ 단일 구조로는 양손 개별 판별 불가
ScControllerCtrl을 rightHand / leftHand 오브젝트에 각각 부착.
이동 방향을 transform.forward(플레이어 body 기준)로 고정해 고개 방향 문제 해소.
교육 대상이 어린이·노약자 포함이라 팔 스윙이 어려운 사용자를 위해 스틱 보조 이동도 병행 추가.
// ScPlayerCtrl.HandSwing.cs
[SerializeField] ScControllerCtrl rightHand;
[SerializeField] ScControllerCtrl leftHand;
CharacterController characterController;
private void Update()
{
isRightHandMoving = rightHand.IsMoving;
isLeftHandMoving = leftHand.IsMoving;
// 어느 한 손만 스윙해도 이동
if (isRightHandMoving || isLeftHandMoving)
Moving();
}
private void Moving()
{
// body forward 기준 — 고개 방향 무관
characterController.Move(
transform.forward * Time.deltaTime * moveSpeed);
}
🔧 git 수정 이력: [fix] 이동 인식 버그 수정
초기에 leftHand.IsMoving 갱신 타이밍이 Update와 맞지 않아 이동이 씹히는 버그 발생.
InputMgr 이벤트 콜백 시점에 IsMoving 상태를 읽도록 수정해 해결.
손 높이: handPos.y > headTransform.position.y 조건으로 손 올림 인식.
고개 좌우 확인: Quaternion.Inverse(body) * head의 Y 오일러각으로 상대 회전각 계산.
50° 이상 → isLookRight / isLookLeft 활성화.
// HandUp: 손 Y > 머리 Y 이면 손 올림 인식
private void OnLeftHandUp(Vector3 handPos)
{
float headY = headTransform.position.y;
isLeftHandUp = handPos.y > headY;
}
// LookAround: 플레이어 body 기준 상대 회전각 계산
Quaternion relativeRot =
Quaternion.Inverse(transform.rotation) * headTransform.rotation;
float currentLookAngle = relativeRot.eulerAngles.y;
if (currentLookAngle > 180f) currentLookAngle -= 360f; // -180 ~ 180
미션 시스템 — 하드코딩 제거 리팩토링
기업협약 프로젝트라 기획 변경이 잦았다. 미션 텍스트를 코드에 직접 하드코딩하다 보니 챕터나 미션 수가 바뀔 때마다 개발자가 코드를 수정해야 하는 병목이 생겼습니다.
git 커밋 기록: [fix] MissionList 키값 쌩으로 넣던거 수정 — 이 커밋이 구조 전환의 계기였습니다.
// 키를 직접 삽입 (실제 코드에 주석으로 남아있음)
string titleKey = "Ch1_MissionText_0";
string mission1Key = "Ch1_MissionText_1";
string mission2Key = "Ch1_MissionText_2";
missionTextMap[titleKey] = langMgr.GetText(titleKey);
missionTextMap[mission1Key] =
$"1.{langMgr.GetText(mission1Key)}";
// Ch2도 동일하게 반복 → 중복 코드 누적
// LocalizationTable 자동 탐색
foreach (var kv in table.Values)
{
var parts = kv.Key.Split('_');
if (parts.Length > 2 &&
parts[0] == ChapterPrefix.ToString() &&
parts[1] == CategoryFilter.ToString())
{
string text = parts[2] == "0"
? kv.Value
: $"{parts[2]}. {kv.Value}";
missionTextMap[kv.Key] = text;
}
}
// 새 챕터 = CSV 행 추가만으로 끝
코드 수정 없이 확장
새 챕터/미션 = CSV 한 줄 추가만으로 UI 자동 반영
언어 자동 연동
SelectedLocale 기준 한국어·영어 자동 전환
중복 코드 제거
챕터별 반복 블록 완전 제거, 유지보수성 향상
미션 체크 UI · 클리어 UI
미션 텍스트 하나하나를 ScMissionBoxTextCheck 컴포넌트로 관리하고,MissionBoxTextCheckType enum(none / Title / Base / Clear)으로 상태를 명확히 분리했습니다.
Title 타입 보호: 미션 제목이 실수로 완료 처리되는 버그를 원천 차단.
완료 시 취소선 + 회색 텍스트로 시각적 피드백. 내부 타입을 Clear로 전환해 클리어 UI와 자동 연동.
public void SetCheck(bool check)
{
// Title 타입은 변경 차단 — 실수 방지
if (checkType == MissionBoxTextCheckType.Title)
{
Debug.LogError("Cannot change Title type");
return;
}
isCheck = check;
// 완료 시 취소선 / 미완료 시 Normal
tmp.fontStyle = check
? FontStyles.Strikethrough
: FontStyles.Normal;
// 타입 전환: Clear or Base
checkType = check
? MissionBoxTextCheckType.Clear
: MissionBoxTextCheckType.Base;
SetTypeChange(checkType);
// 색상: Base=White, Clear=Gray, Title=White+Bold
}
모든 미션 완료 시 GetMissionState() == true인 항목만 복제해 클리어 패널에 출력.
코루틴 대신 UniTask async/await로 팝업 타이밍 제어. 닉네임 개인화 메시지 포함.
private void Start()
{
// Clear 상태 미션만 복제 출력
foreach (var mission in missionListPanel.MissionListTests)
{
if (mission.GetMissionState()) // true = Clear 상태
Instantiate(mission.gameObject, clearPanel.transform);
}
MissionClearImageFadeOut().Forget();
}
private async UniTask MissionClearImageFadeOut()
{
await UniTask.Delay((int)(missionClearUIDelay * 1000));
missionClearUI.gameObject.SetActive(true); // 팝업 등장
}
신호등 & 횡단보도 — 복합 조건 판정
📌 시행착오: 처음에는 횡단보도 진입 즉시 실패 처리 → 발이 조금이라도 빨간불 영역에 닿으면 바로 안전가이드 씬 전환.
테스트 시 불만 다수 발생. limitTime(1초) 판정 유예 코루틴 추가로 개선.
또한 [fix] 움직임 멈출 때도 OnPlayerMoving 이벤트 발생하도록 수정 커밋으로 isWalk false 처리 버그도 해결.
Material의 Shader를 Lit ↔ Unlit으로 교체해서 점멸 효과 구현.MaxTime / 4 이하 남으면 점멸 시작 + isBlink = true 설정.
IEnumerator ApplyBlink()
{
bool highlightOff = false;
while (true)
{
highlightOff = !highlightOff;
m_MeshRenderer.material.shader =
highlightOff ? unlitShader : litShader;
yield return new WaitForSeconds(blinkInterval);
}
}
isColorRed · isBlink · isLeftHandUp · isWalk 네 가지 조건을 OnTriggerStay에서 순서대로 체크.
private void OnTriggerStay(Collider other)
{
// 1순위: 빨간불 or 점멸 → 즉시 실패
if (isColorRed || isBlink)
{
ScRespawn.Instance.Respawn();
if (isBlink)
{ LoadScene(Sg04_TrafficBlink); return; }
if (isColorRed)
{ LoadScene(Sg08_Jaywalking); return; }
}
// 2순위: 손 올림 미션 클리어 체크
if (Hand.isLeftHandUp && !isHandUp)
{ OnCheckMissionClear?.Invoke(); isHandUp = true; }
// 3순위: 멈춤 감지 → 판정 유예 코루틴 (1초 후 미이동이면 실패)
if (!isWalk && coroutine == null)
coroutine = StartCoroutine(StopMoveTimeLimit());
// 4순위: 손 내림 감지 → 판정 유예 코루틴
if (!Hand.isLeftHandUp && coroutine == null)
coroutine = StartCoroutine(HandDownTimeLimit());
}
| 조건 | 의미 | 이동 씬 |
|---|---|---|
isColorRed |
신호등 빨간불 상태 | Sg08_Jaywalking |
isBlink |
점멸 중 (전환 경고) | Sg04_TrafficBlink |
!isLeftHandUp |
손 올림 미확인 | Sg07_HandSignal |
isWalk == false |
횡단 중 멈춤 | Sg05_SafeWalk |
안전선 진입 감지 → limitTime초 코루틴 시작.
시간 내 빠져나오면 OnTriggerExit에서 StopAllCoroutines()로 취소.
판정 유예: 발이 살짝 걸렸다가 즉시 빠져나오면 실패하지 않도록 수정한 이유는 테스터들이 이건 너무 가혹하다 애들인데 짜증나서 바로 꺼버릴거다 해서 추가된 기능입니다.
IEnumerator TimeLimit(Collider other)
{
yield return new WaitForSeconds(limitTime);
if (isSafety) // 아직도 안전선 위에 있으면 실패
LoadScene(ScScene.Sg01_SafetyLine);
}
private void OnTriggerExit(Collider other)
{
isSafety = false;
StopAllCoroutines(); // 벗어나면 판정 취소
coroutine = null;
}
GuideNPC & 타임라인 안전가이드
왜 InverseTransformPoint를 썼나?
NPC를 플레이어 '뒤 1m' 위치에 고정하려면 월드 좌표로 저장하면 플레이어 회전 시 오프셋이 틀어집니다.InverseTransformPoint로 로컬 오프셋 저장 → TransformPoint로 매 프레임 정확한 월드 목표 재계산.
DOTween OnComplete 재귀로 이동 중 계속 추적, 멈추면 자동 종료.
private void Awake()
{
Manager.Instance.GameMgr.OnPlayerMoving += OnPlayerMoving;
// 초기 상대 오프셋 저장 (플레이어 로컬 좌표계)
offset = player.transform.InverseTransformPoint(transform.position);
}
private void FollowPlayer()
{
// 매번 월드 목표 좌표 재계산
Vector3 newTarget = player.transform.TransformPoint(offset);
if (moveTween != null && moveTween.IsActive())
moveTween.Kill(); // 기존 트윈 즉시 제거
moveTween = transform
.DOMove(newTarget, 1f)
.SetEase(Ease.Linear)
.SetAutoKill(true)
.OnComplete(() =>
{
if (isMove) // 아직 이동 중이면
FollowPlayer(); // 재귀 호출
});
}
실패 후 단순 UI가 아닌 NPC가 직접 설명하는 영상형 교육이 필요했습니다.
Unity Timeline을 활용해 캐릭터 애니메이션 · 자막(커스텀 TextTrack) · 카메라 · SFX · VFX를 하나의 타임라인에서 시간축 기준으로 통합 관리.
GuideNpc 트랙
SignalReceiver로 애니메이션 전환 시점 제어. VFX(Stylized Smoke) 활성화도 Timeline 관리.
자막 트랙 (커스텀)
TextTrackHsy.cs — TMP TextMesh를 Timeline Clip으로 제어. 각 대사가 정확한 타임코드에 출력/숨김.
카메라 전환
ScreenFader로 씬 전환 페이드 연출. 모든 트랙이 동일 Timeline Asset에서 동기화.
도입 배경
기획자 요청 교육 흐름에 맞춰 정밀하게 타이밍을 잡으려면 Timeline이 최적이었다.
기획 오류 수정 경험
이 경험이 협업 방식을 가장 많이 바꿔놓은 사건이었습니다.
기획서 수령 & 첫 구현
기획서에 '횡단보도 진입 전 좌우 확인 후 이동'이라고 기재.
→ '고개를 좌우로 50° 이상 돌리면 이동 허용'으로 해석.ChekLookAround() 코루틴으로 headTransform 회전각을 매 프레임 감지하도록 구현 완료.
중간 점검 — 기획자 직접 플레이
기획자가 시연하며 '손을 들고 좌우 확인해야 한다'고 설명.
'좌우 확인'이 고개 회전 단독이 아니라 '손을 올린 채 좌우 확인하는 복합 동작'임을 파악.isLeftHandUp AND isLookAround 두 조건을 함께 충족해야 하는 구조로 변경 필요.
빠른 수정 & 협업 방식 개선
isLeftHandUp 조건을 AND로 추가해 수정 완료.
이후부터 기획안에 모호한 부분이 있으면 플로우차트로 먼저 작성해 기획자에게 공유 후 확인받는 방식으로 협업 개선.
기획·개발 간 해석 충돌을 사전 차단.
💡 이 경험에서 배운 것
기획서 한 줄이 개발자-기획자 간 다르게 해석될 수 있다.
구현을 시작하기 전에 '내가 이해한 동작이 맞나요?'를 플로우차트로 공유하고 확인받는 습관이 나중의 수정 비용을 가장 효과적으로 줄이는 방법이었습니다.
'Unity > 프로젝트' 카테고리의 다른 글
| [유니티/Unity] 몬스터헌터 모작 - 네트워크 팀 프로젝트 (0) | 2025.04.07 |
|---|---|
| [유니티/Unity] 포켓몬 로얄 - 팀 프로젝트 (1) | 2025.01.24 |
| [팀 포폴] 클래시로얄 라이크(포켓몬 로얄) -잡담 (0) | 2025.01.16 |