컴퓨터 설정까지

This commit is contained in:
dldydtn9755-crypto
2026-07-28 14:08:45 +09:00
parent d02166e83e
commit dd4f318a9c
28 changed files with 1321 additions and 376 deletions

View File

@@ -1,84 +0,0 @@
using System.Collections;
using UnityEngine;
public class PlayerPushToPoint : MonoBehaviour
{
[Header("Player")]
[SerializeField] private Transform xrOrigin;
[SerializeField] private Transform playerCamera;
[SerializeField] private CharacterController characterController;
[Header("Target")]
[SerializeField] private Transform pushDirectionPoint;
[Header("Push")]
[SerializeField] private float pushDuration = 0.2f;
private bool hasPushed;
private bool isPushing;
// 타임라인에서 충돌 순간에 호출
public void PushPlayerOnce()
{
if (hasPushed || isPushing)
return;
if (xrOrigin == null || playerCamera == null || pushDirectionPoint == null)
{
Debug.LogWarning("플레이어 밀기 설정이 비어 있습니다.");
return;
}
hasPushed = true;
StartCoroutine(PushRoutine());
}
private IEnumerator PushRoutine()
{
isPushing = true;
// 카메라의 현재 위치가 목표 지점까지 가기 위해 필요한 이동량
Vector3 totalMovement =
pushDirectionPoint.position - playerCamera.position;
// VR 플레이어 높이는 변경하지 않음
totalMovement.y = 0f;
float elapsed = 0f;
Vector3 movedAmount = Vector3.zero;
while (elapsed < pushDuration)
{
elapsed += Time.deltaTime;
float t = Mathf.Clamp01(elapsed / pushDuration);
// 처음에 빠르게 밀리고 끝에서 부드럽게 멈춤
float easedT = 1f - Mathf.Pow(1f - t, 3f);
Vector3 targetMovement = totalMovement * easedT;
Vector3 frameMovement = targetMovement - movedAmount;
if (characterController != null &&
characterController.enabled)
{
characterController.Move(frameMovement);
}
else
{
xrOrigin.position += frameMovement;
}
movedAmount = targetMovement;
yield return null;
}
isPushing = false;
}
// 반복 테스트할 때만 사용
public void ResetPush()
{
hasPushed = false;
}
}

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1c11f945087bd984d9e4dbe8f0632a20
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,152 @@
using System.Collections;
using UnityEngine;
using UnityEngine.Events;
public class JunpyoCollapseEvent : MonoBehaviour
{
[Header("구준표")]
[SerializeField] private Animator junpyoAnimator;
[SerializeField] private string collapseTrigger = "Collapse";
[Header("플레이어 시선")]
[SerializeField] private Transform xrOrigin;
[SerializeField] private Transform playerCamera;
[SerializeField] private Transform lookTarget;
[Tooltip("구준표 쪽으로 화면이 돌아가는 시간")]
[SerializeField] private float lookDuration = 0.4f;
[Tooltip("화면 전환을 시작하고 몇 초 뒤 쓰러질지")]
[SerializeField] private float collapseStartDelay = 0.15f;
[Header("페이드 타이밍")]
[Tooltip("쓰러지기 시작한 뒤 페이드 아웃까지 기다리는 시간")]
[SerializeField] private float fadeDelay = 1.3f;
[Header("쓰러진 뒤 실행")]
[SerializeField] private UnityEvent onCollapseFinished;
private bool hasPlayed;
public void PlayCollapse()
{
if (hasPlayed)
return;
if (junpyoAnimator == null)
{
Debug.LogWarning(
"[JunpyoCollapseEvent] 구준표 Animator가 연결되지 않았습니다."
);
return;
}
if (xrOrigin == null ||
playerCamera == null ||
lookTarget == null)
{
Debug.LogWarning(
"[JunpyoCollapseEvent] 플레이어 시선 설정이 비어 있습니다."
);
return;
}
hasPlayed = true;
StartCoroutine(CollapseRoutine());
}
private IEnumerator CollapseRoutine()
{
// 구준표 방향으로 화면 전환 시작
StartCoroutine(LookAtJunpyoRoutine());
// 화면이 돌아가기 시작한 뒤 잠깐 기다림
yield return new WaitForSeconds(collapseStartDelay);
// 구준표 쓰러지는 애니메이션 실행
junpyoAnimator.ResetTrigger(collapseTrigger);
junpyoAnimator.SetTrigger(collapseTrigger);
// 쓰러지는 모습을 보여준 뒤 페이드 아웃
yield return new WaitForSeconds(fadeDelay);
onCollapseFinished?.Invoke();
}
private IEnumerator LookAtJunpyoRoutine()
{
Vector3 cameraForward = playerCamera.forward;
cameraForward.y = 0f;
Vector3 targetDirection =
lookTarget.position - playerCamera.position;
targetDirection.y = 0f;
if (cameraForward.sqrMagnitude < 0.001f ||
targetDirection.sqrMagnitude < 0.001f)
{
yield break;
}
cameraForward.Normalize();
targetDirection.Normalize();
// 현재 시선에서 구준표까지 필요한 좌우 회전각
float totalAngle = Vector3.SignedAngle(
cameraForward,
targetDirection,
Vector3.up
);
float duration = Mathf.Max(0.01f, lookDuration);
float elapsed = 0f;
float previousAngle = 0f;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
float t = Mathf.Clamp01(elapsed / duration);
// 부드럽게 시작하고 부드럽게 멈춤
float easedT = t * t * (3f - 2f * t);
float currentAngle = totalAngle * easedT;
float frameAngle = currentAngle - previousAngle;
// 카메라 현재 위치를 중심으로 XR Origin 회전
// 플레이어 위치가 옆으로 밀리는 현상을 방지
xrOrigin.RotateAround(
playerCamera.position,
Vector3.up,
frameAngle
);
previousAngle = currentAngle;
yield return null;
}
}
public void ResetCollapse()
{
StopAllCoroutines();
hasPlayed = false;
if (junpyoAnimator != null)
junpyoAnimator.ResetTrigger(collapseTrigger);
}
[ContextMenu("Test Collapse")]
private void TestCollapse()
{
PlayCollapse();
}
[ContextMenu("Reset Collapse")]
private void TestResetCollapse()
{
ResetCollapse();
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 72d7bab8fc449ae44a7d202606c5382e

View File

@@ -0,0 +1,84 @@
using System.Collections;
using UnityEngine;
public class JunpyoRescueEvent : MonoBehaviour
{
[Header("Event Components")]
[SerializeField] private JunpyoRunToPoint junpyoRun;
[SerializeField] private PlayerPushToPoint playerPush;
[SerializeField] private SpotlightFall spotlightFall;
[Header("Timing")]
[Tooltip("구준표가 출발한 뒤 플레이어를 밀 때까지 시간")]
[SerializeField] private float pushDelay = 1.0f;
[Tooltip("플레이어를 민 뒤 조명이 떨어질 때까지 시간")]
[SerializeField] private float spotlightDelayAfterPush = 0.1f;
private bool hasPlayed;
public void PlayRescueEvent()
{
if (hasPlayed)
return;
if (junpyoRun == null ||
playerPush == null ||
spotlightFall == null)
{
Debug.LogWarning(
"[JunpyoRescueEvent] 연결되지 않은 컴포넌트가 있습니다."
);
return;
}
hasPlayed = true;
StartCoroutine(RescueRoutine());
}
private IEnumerator RescueRoutine()
{
// 1. 구준표가 달려오기 시작
junpyoRun.RunToStopPoint();
// 2. 구준표가 거의 도착할 때까지 대기
yield return new WaitForSeconds(pushDelay);
// 3. 플레이어 밀기
playerPush.PushPlayerOnce();
// 4. 밀린 직후 잠깐 기다림
yield return new WaitForSeconds(spotlightDelayAfterPush);
// 5. 조명 떨어뜨리기
spotlightFall.DropSpotlight();
}
public void ResetEvent()
{
StopAllCoroutines();
hasPlayed = false;
if (junpyoRun != null)
junpyoRun.ResetMovement();
if (playerPush != null)
playerPush.ResetPush();
if (spotlightFall != null)
spotlightFall.ResetFall();
}
[ContextMenu("Test Rescue Event")]
private void TestRescueEvent()
{
PlayRescueEvent();
}
[ContextMenu("Reset Rescue Event")]
private void TestResetEvent()
{
ResetEvent();
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 459517eb5f032fd4e9672d8c30c93d74

View File

@@ -0,0 +1,70 @@
using System.Collections;
using UnityEngine;
using UnityEngine.Events;
public class JunpyoRescueSequence : MonoBehaviour
{
[Header("구준표 구조 연출")]
[SerializeField] private JunpyoRescueEvent rescueEvent;
[Header("대화 시작 타이밍")]
[Tooltip("구준표가 달려오고 플레이어를 민 뒤, 대화를 시작하기까지의 전체 시간")]
[SerializeField] private float dialogueStartDelay = 1.5f;
[Header("연출 종료 후 실행")]
[Tooltip("여기에 기존 대화 시작 함수를 연결")]
[SerializeField] private UnityEvent onRescueFinished;
private bool hasPlayed;
public void PlaySequence()
{
if (hasPlayed)
return;
if (rescueEvent == null)
{
Debug.LogWarning(
"[JunpyoRescueSequence] Rescue Event가 연결되지 않았습니다."
);
return;
}
hasPlayed = true;
StartCoroutine(SequenceRoutine());
}
private IEnumerator SequenceRoutine()
{
// 1. 구준표 달려오기 + 플레이어 밀기
rescueEvent.PlayRescueEvent();
// 2. 구조 연출이 끝날 때까지 기다림
yield return new WaitForSeconds(dialogueStartDelay);
// 3. 구조 연출 종료 후 대화 시작
onRescueFinished?.Invoke();
}
public void ResetSequence()
{
StopAllCoroutines();
hasPlayed = false;
if (rescueEvent != null)
rescueEvent.ResetEvent();
}
[ContextMenu("Test Sequence")]
private void TestSequence()
{
PlaySequence();
}
[ContextMenu("Reset Sequence")]
private void TestResetSequence()
{
ResetSequence();
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a6460eb96d882e34d9d11a86f188b0cd

View File

@@ -0,0 +1,104 @@
using System.Collections;
using UnityEngine;
public class JunpyoRunToPoint : MonoBehaviour
{
[Header("Junpyo")]
[SerializeField] private Transform junpyo;
[Header("Target")]
[SerializeField] private Transform junpyoStopPoint;
[Header("Movement")]
[SerializeField] private float moveDuration = 1.2f;
[SerializeField] private bool faceMoveDirection = true;
private bool isMoving;
private bool hasMoved;
// 대화가 시작될 때 호출
public void RunToStopPoint()
{
if (isMoving || hasMoved)
return;
if (junpyo == null || junpyoStopPoint == null)
{
Debug.LogWarning("구준표 또는 JunpyoStopPoint가 연결되지 않았습니다.");
return;
}
hasMoved = true;
StartCoroutine(MoveRoutine());
}
private IEnumerator MoveRoutine()
{
isMoving = true;
Vector3 startPosition = junpyo.position;
Quaternion startRotation = junpyo.rotation;
Vector3 targetPosition = junpyoStopPoint.position;
targetPosition.y = startPosition.y;
Vector3 direction = targetPosition - startPosition;
direction.y = 0f;
Quaternion targetRotation = startRotation;
if (faceMoveDirection && direction.sqrMagnitude > 0.001f)
targetRotation = Quaternion.LookRotation(direction.normalized);
float elapsed = 0f;
while (elapsed < moveDuration)
{
elapsed += Time.deltaTime;
float t = Mathf.Clamp01(elapsed / moveDuration);
junpyo.position = Vector3.Lerp(
startPosition,
targetPosition,
t
);
if (faceMoveDirection)
{
junpyo.rotation = Quaternion.Slerp(
startRotation,
targetRotation,
t
);
}
yield return null;
}
// 마지막 위치를 정확하게 맞춤
junpyo.position = targetPosition;
if (faceMoveDirection)
junpyo.rotation = targetRotation;
isMoving = false;
}
public void ResetMovement()
{
hasMoved = false;
isMoving = false;
}
[ContextMenu("Test Run")]
private void TestRun()
{
RunToStopPoint();
}
[ContextMenu("Reset Run")]
private void TestResetRun()
{
ResetMovement();
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 91bed9cabff116f43a42e695be8d8a95

View File

@@ -0,0 +1,134 @@
using System.Collections;
using UnityEngine;
public class PlayerPushToPoint : MonoBehaviour
{
[Header("Player")]
[SerializeField] private Transform xrOrigin;
[SerializeField] private Transform playerCamera;
[SerializeField] private CharacterController characterController;
[Header("Push Direction")]
[SerializeField] private Transform pushDirectionPoint;
[Header("Push Settings")]
[SerializeField] private float pushDistance = 0.9f;
[SerializeField] private float pushDuration = 0.18f;
private bool hasPushed;
private bool isPushing;
public void PushPlayerOnce()
{
if (hasPushed || isPushing)
return;
if (xrOrigin == null ||
playerCamera == null ||
pushDirectionPoint == null)
{
Debug.LogWarning(
"[PlayerPushToPoint] 연결되지 않은 오브젝트가 있습니다."
);
return;
}
hasPushed = true;
StartCoroutine(PushRoutine());
}
private IEnumerator PushRoutine()
{
isPushing = true;
// 플레이어에서 포인트로 향하는 방향
Vector3 direction =
pushDirectionPoint.position - playerCamera.position;
// 수평 방향만 사용
direction.y = 0f;
if (direction.sqrMagnitude < 0.001f)
{
Debug.LogWarning(
"[PlayerPushToPoint] 밀기 방향을 계산할 수 없습니다."
);
hasPushed = false;
isPushing = false;
yield break;
}
direction.Normalize();
Vector3 startPosition = xrOrigin.position;
Vector3 targetPosition =
startPosition + direction * pushDistance;
// 시작 높이를 그대로 고정
float fixedY = startPosition.y;
targetPosition.y = fixedY;
bool controllerWasEnabled =
characterController != null &&
characterController.enabled;
// 밀리는 동안 바닥이나 장애물 때문에 위로 올라가지 않도록 끔
if (controllerWasEnabled)
characterController.enabled = false;
float duration = Mathf.Max(0.01f, pushDuration);
float elapsed = 0f;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
float t = Mathf.Clamp01(elapsed / duration);
float easedT = 1f - Mathf.Pow(1f - t, 3f);
Vector3 nextPosition =
Vector3.Lerp(startPosition, targetPosition, easedT);
// 매 프레임 Y값 강제 고정
nextPosition.y = fixedY;
xrOrigin.position = nextPosition;
yield return null;
}
// 마지막 위치 정확히 고정
xrOrigin.position = targetPosition;
if (controllerWasEnabled)
characterController.enabled = true;
isPushing = false;
}
public void ResetPush()
{
StopAllCoroutines();
hasPushed = false;
isPushing = false;
if (characterController != null &&
!characterController.enabled)
{
characterController.enabled = true;
}
}
[ContextMenu("Test Push")]
private void TestPush()
{
PushPlayerOnce();
}
[ContextMenu("Reset Push")]
private void TestResetPush()
{
ResetPush();
}
}

View File

@@ -0,0 +1,154 @@
using System.Collections;
using UnityEngine;
public class SpotlightFall : MonoBehaviour
{
[Header("떨어질 조명")]
[SerializeField] private Transform spotlight;
[Header("도착 위치")]
[SerializeField] private Transform fallEndPoint;
[Header("낙하 설정")]
[SerializeField] private float fallDuration = 1f;
[Header("충돌 사운드")]
[SerializeField] private AudioSource impactAudioSource;
[SerializeField] private AudioClip impactSound;
[Range(0f, 1f)]
[SerializeField] private float impactVolume = 1f;
[Tooltip("조명이 도착하기 몇 초 전에 소리를 재생할지")]
[SerializeField] private float impactSoundLeadTime = 0.12f;
private Vector3 startPosition;
private Quaternion startRotation;
private bool hasFallen;
private bool isFalling;
private void Awake()
{
if (spotlight != null)
{
startPosition = spotlight.position;
startRotation = spotlight.rotation;
}
}
public void DropSpotlight()
{
if (hasFallen || isFalling)
return;
if (spotlight == null)
{
Debug.LogWarning(
"[SpotlightFall] Spotlight가 연결되지 않았습니다."
);
return;
}
if (fallEndPoint == null)
{
Debug.LogWarning(
"[SpotlightFall] Fall End Point가 연결되지 않았습니다."
);
return;
}
hasFallen = true;
StartCoroutine(FallRoutine());
}
private IEnumerator FallRoutine()
{
isFalling = true;
Vector3 fallStartPosition = spotlight.position;
Vector3 targetPosition = fallEndPoint.position;
float duration = Mathf.Max(0.01f, fallDuration);
float elapsed = 0f;
bool impactSoundPlayed = false;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
float t = Mathf.Clamp01(elapsed / duration);
// 처음에는 느리고 아래로 갈수록 빠르게 낙하
float acceleratedT = t * t * t;
spotlight.position = Vector3.Lerp(
fallStartPosition,
targetPosition,
acceleratedT
);
// 완전히 도착하기 조금 전에 와장창 소리 재생
if (!impactSoundPlayed &&
duration - elapsed <= impactSoundLeadTime)
{
impactSoundPlayed = true;
PlayImpactSound();
}
yield return null;
}
// 마지막 위치 정확히 맞추기
spotlight.position = targetPosition;
// Lead Time이 너무 작아 소리가 실행되지 않았을 경우 대비
if (!impactSoundPlayed)
{
PlayImpactSound();
}
isFalling = false;
}
private void PlayImpactSound()
{
if (impactAudioSource == null || impactSound == null)
return;
impactAudioSource.PlayOneShot(
impactSound,
impactVolume
);
}
public void ResetFall()
{
StopAllCoroutines();
hasFallen = false;
isFalling = false;
if (impactAudioSource != null)
impactAudioSource.Stop();
if (spotlight != null)
{
spotlight.position = startPosition;
spotlight.rotation = startRotation;
}
}
[ContextMenu("Test Fall")]
private void TestFall()
{
DropSpotlight();
}
[ContextMenu("Reset Fall")]
private void TestResetFall()
{
ResetFall();
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f21d1c5f5053dcd46b78113e83920072