feat: SongCreator 씬 완성 — Beat Sage URL 지원, info.dat 메타데이터 자동 추출

- BeatSageUploader: audio_url 지원(UploadFromUrl), PollAndDownload 공통화, ZIP 500 오류 3회 재시도
- BeatSageConverter: info.dat 파싱(SongMetadata), BPM 자동 감지 → 노트 타이밍 변환에 적용
- SongCreatorManager: title/BPM 필수 입력 제거, 난이도 4개 자동 선택, GenerateFlowFromUrl 버그 수정
- NasPublisher: audioPath null 허용(URL 흐름에서 로컬 파일 없는 경우 스킵)
- .gitignore/.gitattributes 초기 설정

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-21 23:37:34 +09:00
commit 4dad9e5d5b
1068 changed files with 175146 additions and 0 deletions
@@ -0,0 +1,296 @@
using UnityEngine;
using VRSDK.Events;
namespace VRSDK
{
//this script controls the lever, the lever is completely based on physics so it can respond to any collision
public class VR_Lever : VR_Grabbable
{
[SerializeField] private Transform transformBase = null;
[SerializeField] private int solverIterations = 10;
[SerializeField] private OnValueChangeEvent onValueChange = null;
[SerializeField] private bool shouldBackToStartingPosition = false;
[SerializeField] private float backForce = 2.0f;
private HingeJoint joint = null;
private float initialAngle = 0.0f;
private float lastValue = -1.0f;
private Vector3 initialDir = Vector3.zero;
private float movementRange = 0.0f;
private Collider thisCollider = null;
private Collider triggerHandCollider = null;
private bool isIgnoringHandCollision = false;
private const float MAX_FORCE = 1000.0f;
public OnValueChangeEvent OnValueChange { get { return onValueChange; } }
protected override void Awake()
{
base.Awake();
canUseDropZone = false;
thisCollider = GetComponent<Collider>();
joint = GetComponent<HingeJoint>();
rb = GetComponent<Rigidbody>();
//solver iterations help to get a better physics behaviour
//but be careful this can slowdown the game
rb.solverIterations = solverIterations;
//we dont need this
shouldFly = false;
autoGrab = false;
//ignore base collision
Collider[] colliderArray = transformBase.GetComponentsInChildren<Collider>();
for (int n = 0; n < colliderArray.Length; n++)
{
Physics.IgnoreCollision( thisCollider, colliderArray[n] );
}
//initialize the joint
SetUpHingeJoint();
OnGrabStateChange.AddListener( OnGrabStateChangeCallback );
}
private void SetUpHingeJoint()
{
initialAngle = GetCurrentAngle();
movementRange = Mathf.Abs( joint.limits.min - joint.limits.max );
initialDir = transform.InverseTransformDirection( Vector3.up );
joint.useMotor = false;
joint.useSpring = shouldBackToStartingPosition;
//cancel all forces
rb.linearVelocity = Vector3.zero;
rb.angularVelocity = Vector3.zero;
//set back force
JointSpring spring = joint.spring;
spring.spring = backForce;
spring.targetPosition = 0.0f;
joint.spring = spring;
}
protected override void Update()
{
base.Update();
float newValue = Mathf.Clamp( GetClampAngle() / movementRange, 0.0f, 1.0f );
if (newValue != lastValue)
{
onValueChange.Invoke( newValue );
lastValue = newValue;
}
if (isIgnoringHandCollision && Vector3.Distance( triggerHandCollider.transform.position, RightInteractPoint.position ) > interactDistance)
{
Physics.IgnoreCollision( thisCollider, triggerHandCollider, false );
isIgnoringHandCollision = false;
}
}
private void OnGrabStateChangeCallback(GrabState state)
{
if (state == GrabState.Grab)
{
if (thisCollider != null)
{
thisCollider.enabled = false;
}
triggerHandCollider = activeController.GetComponent<Collider>();
JointSpring spring = joint.spring;
spring.spring = MAX_FORCE;
joint.spring = spring;
joint.useMotor = false;
joint.useSpring = true;
}
else if (state == GrabState.UnGrab)
{
if (triggerHandCollider != null)
{
Physics.IgnoreCollision( thisCollider, triggerHandCollider, true );
isIgnoringHandCollision = true;
}
else
{
isIgnoringHandCollision = false;
}
if (thisCollider != null)
{
thisCollider.enabled = true;
}
if (shouldBackToStartingPosition)
{
SetupBackLever();
}
}
}
private void SetupBackLever()
{
JointSpring spring = joint.spring;
spring.spring = backForce;
spring.targetPosition = 0.0f;
joint.spring = spring;
joint.useMotor = false;
joint.useSpring = true;
}
private float GetCurrentAngle()
{
if (joint.axis.x > 0.0f)
return WrapAngle( transform.rotation.eulerAngles.x );
else if (joint.axis.y > 0.0f)
return WrapAngle( transform.rotation.eulerAngles.y );
else if (joint.axis.z > 0.0f)
return WrapAngle( transform.rotation.eulerAngles.z );
return 0.0f;
}
//convert angle to human readable
private float WrapAngle(float angle)
{
angle %= 360;
if (angle > 180)
return angle - 360;
return angle;
}
protected override void GrabUpdate()
{
base.GrabUpdate();
UpdateJoint();
Transform highlightPoint = activeController.ControllerType == VR_ControllerType.Right ? HighlightPointRightHand : HighlightPointLeftHand;
float distance = ( highlightPoint.transform.position - activeController.GrabPoint.transform.position ).magnitude;
if (distance > interactDistance)
{
currentGrabState = GrabState.Drop;
RaiseOnGrabStateChangeEvent( GrabState.Drop );
}
}
protected override void DropUpdate()
{
GrabController.SetVisibility( true );
rb.linearVelocity = Vector3.zero;
rb.angularVelocity = Vector3.zero;
joint.useSpring = false;
activeController.CleanCurrentGrab();
activeController = null;
RaiseOnGrabStateChangeEvent( GrabState.Drop );
currentGrabState = GrabState.UnGrab;
RaiseOnGrabStateChangeEvent( GrabState.UnGrab );
}
public override void OnGrabSuccess(VR_Controller controller)
{
activeController = controller;
joint.useSpring = true;
currentGrabState = GrabState.Grab;
RaiseOnGrabStateChangeEvent( GrabState.Grab );
GrabController.SetVisibility( !GetCurrentHandAnimationSettings().hideHandOnGrab );
}
private float GetAngle()
{
if (joint.axis.x > 0.0f)
{
Vector3 dir = transformBase.transform.position - activeController.GrabPoint.transform.position;
Vector3 myDir = transformBase.up * -1.0f;
return Vector2.SignedAngle( new Vector2( dir.z, dir.y ), new Vector2( 0.0f, myDir.y ) );
}
else if (joint.axis.z > 0.0f)
{
Vector3 dir = transformBase.transform.position - activeController.GrabPoint.transform.position;
Vector3 myDir = transformBase.up * -1.0f;
return Vector2.SignedAngle( new Vector2( dir.x * -1.0f, dir.y ), new Vector2( myDir.x, myDir.y ) );
}
else if (joint.axis.y > 0.0f)
{
Vector3 dir = transformBase.transform.position - activeController.GrabPoint.transform.position;
Vector3 myDir = transformBase.right *-1.0f;
return Vector2.SignedAngle( new Vector2( dir.x, dir.z ), new Vector2( myDir.x, myDir.z ) );
}
return 0.0f;
}
private float GetClampAngle()
{
if (joint.axis.z > 0.0f)
return Vector2.Angle( new Vector2( transform.up.x, transform.up.y ), new Vector2( initialDir.x, initialDir.y ) );
if (joint.axis.x > 0.0f)
return Vector2.Angle( new Vector2( transform.up.z, transform.up.y ), new Vector2( initialDir.z, initialDir.y ) );
return 0.0f;
}
private void UpdateJoint()
{
float visualAngle = GetAngle() - initialAngle;
JointSpring spring = joint.spring;
spring.targetPosition = visualAngle;
joint.spring = spring;
}
}
}