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,55 @@
using UnityEngine;
namespace VRSDK.Locomotion
{
//this scripts handles aim marker position and rotation
public class VR_AimMarker : MonoBehaviour
{
[SerializeField] private GameObject marker = null;
[SerializeField] private BoxCollider collider = null;
[SerializeField] private float slopeLimit;
public GameObject Marker { get { return marker; } }
public BoxCollider Collider { get { return collider; } }
public float SlopeLimit { get { return slopeLimit; } }
private void Awake()
{
//disable marker
marker.gameObject.SetActive(false);
}
public void Hide()
{
marker.SetActive( false );
}
public void UpdatePositionAndRotation(VR_Controller controller, AimRaycastInfo info, bool active = true)
{
if (!marker.activeInHierarchy && active)
marker.SetActive( true );
marker.transform.position = info.hitPoint;
marker.transform.up = info.normal;
Vector2 controllerInput = controller.Input.GetJoystick().normalized;
Vector3 controllerDirection = new Vector3( controllerInput.x, 0.0f, controllerInput.y );
//get controller pointing direction in world space
controllerDirection = controller.transform.TransformDirection( controllerDirection );
//get marker forward in local space
Vector3 forward = marker.transform.InverseTransformDirection( marker.transform.forward);
//find the angle diference betwen the controller pointing direction and marker current forward
float angle = Vector2.SignedAngle( new Vector2( controllerDirection.x , controllerDirection.z ) , new Vector2( forward.x , forward.z ) );
//rotate marker in local space to match controller pointing direction
marker.transform.Rotate(Vector3.up , angle , Space.Self);
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 8c846e854350b9545a191d7ce52fe1ce
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 168243
packageName: VR Beats Kit
packageVersion: 2.0
assetPath: Assets/VRBeatsKit/Modules/VRSDK/VR/Teleporting/VR_AimMarker.cs
uploadId: 546658
@@ -0,0 +1,155 @@
using UnityEngine;
using System.Collections.Generic;
namespace VRSDK.Locomotion
{
//this scripts handle the raycasting part for teleporting
public abstract class VR_AimRaycaster : MonoBehaviour
{
[Tooltip( "How many subdivisions will have the teleport ray?, this could increase the numbers of linecast per frame, be careful." )]
[SerializeField] [Range(1 , 25)] protected int collisionAccuracy = 8;
[SerializeField] protected float validAngle = 60.0f;
[SerializeField] protected float invalidRayDistance = 2.0f;
[SerializeField] protected LayerMask validLayerMask;
public virtual AimRaycastInfo Raycast(List<Vector3> points , Transform rayController)
{
//get rayController angle
Vector3 rayControllerForward = rayController.forward;
Vector3 unalteredForward = new Vector3( rayController.forward.x, 0.0f, rayController.forward.z ).normalized;
float angle = Vector3.Angle( rayController.forward, unalteredForward );
//check if we are on a valid angle
if (angle > validAngle)
{
AimRaycastInfo info = new AimRaycastInfo();
info.hitPoint = Vector3.zero;
info.normal = Vector3.zero;
//clamp ray to a distance
info.validPoints = ClampToDistance(points , invalidRayDistance );
info.isValid = false;
return info;
}
//the points are valid?
if (points.Count <= 1)
return null;
RaycastHit hitInfo;
if (points.Count == 2)
{
if (Physics.Linecast( points[0], points[1] , out hitInfo , validLayerMask.value , QueryTriggerInteraction.Ignore))
{
return ProcessHitInfo(hitInfo , new List<Vector3> { points[0] , hitInfo.point } );
}
return null;
}
List<Vector3> validPoints = new List<Vector3>();
int subdivision = Mathf.CeilToInt( points.Count / collisionAccuracy);
int currentIndex = 0;
int nextIndex = subdivision;
for (int n = 0; n < subdivision; n++)
{
//check for collision in this segment
if (Physics.Linecast( points[currentIndex], points[nextIndex] , validLayerMask.value , QueryTriggerInteraction.Ignore))
{
//found a collision in this segment
//find collision in all points inside this segment
for (int j = currentIndex; j < nextIndex; j++)
{
validPoints.Add( points[j] );
if (Physics.Linecast( points[j], points[j + 1], out hitInfo , validLayerMask.value , QueryTriggerInteraction.Ignore))
{
validPoints.Add( hitInfo.point );
return ProcessHitInfo(hitInfo , validPoints);
}
}
}
else
{
//this segment dont has a collision, addall as valid points
for (int j = currentIndex; j <= nextIndex; j++)
{
validPoints.Add( points[j] );
}
}
//move to the next subdivision if we have one
currentIndex += subdivision;
nextIndex += subdivision;
//we found the end
if (currentIndex >= points.Count)
{
return null;
}
if (nextIndex >= points.Count)
nextIndex = points.Count - 1;
}
return null;
}
protected abstract AimRaycastInfo ProcessHitInfo(RaycastHit hitInfo, List<Vector3> validPoints);
private List<Vector3> ClampToDistance(List<Vector3> points , float d)
{
List<Vector3> validPoints = new List<Vector3>();
float currentSqrDistance = 0.0f;
for (int n = 0; n < points.Count - 1; n++)
{
float distance = (points[n] - points[n + 1]).sqrMagnitude;
validPoints.Add( points[n] );
if (distance + currentSqrDistance < d * d)
{
currentSqrDistance += distance;
}
else
{
Vector3 dir = ( points[n + 1] - points[n] ).normalized;
float diff = Mathf.Abs( Mathf.Abs( Mathf.Sqrt( currentSqrDistance ) ) - Mathf.Abs( d ) );
validPoints.Add( points[n] + (dir * diff) );
return validPoints;
}
}
return validPoints;
}
protected float GetSlopeAngle(Vector3 surfaceNormal)
{
return Vector3.Angle(surfaceNormal , Vector3.up);
}
}
public class AimRaycastInfo
{
public Vector3 hitPoint = Vector3.zero;
public Vector3 normal = Vector3.zero;
public List<Vector3> validPoints = new List<Vector3>();
public bool isValid = false;
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 4aced637571962d47a4b8ecb1f8e24a3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 168243
packageName: VR Beats Kit
packageVersion: 2.0
assetPath: Assets/VRBeatsKit/Modules/VRSDK/VR/Teleporting/VR_AimRaycaster.cs
uploadId: 546658
@@ -0,0 +1,27 @@
using System.Collections.Generic;
using UnityEngine;
namespace VRSDK.Locomotion
{
public class VR_CharacterAimRaycaster : VR_AimRaycaster
{
[SerializeField] private float characterRadiusOffset = 1.5f;
[SerializeField] private CharacterController characterController = null;
[SerializeField] private float slopeLimit = 20.0f;
protected override AimRaycastInfo ProcessHitInfo(RaycastHit hitInfo, List<Vector3> validPoints)
{
Vector3 start = hitInfo.point + ( hitInfo.normal * characterController.radius * characterRadiusOffset ) + hitInfo.normal;
Vector3 end = start + ( hitInfo.normal * characterController.height );
AimRaycastInfo info = new AimRaycastInfo();
info.hitPoint = hitInfo.point;
info.normal = hitInfo.normal;
info.validPoints = validPoints;
//check if the character can fit in this place
info.isValid = GetSlopeAngle(info.normal) < slopeLimit && !Physics.CheckCapsule( start, end, characterController.radius * characterRadiusOffset , validLayerMask.value , QueryTriggerInteraction.Ignore );
return info;
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: c74522154bbb4756af9570668ff48afc
timeCreated: 1601215740
AssetOrigin:
serializedVersion: 1
productId: 168243
packageName: VR Beats Kit
packageVersion: 2.0
assetPath: Assets/VRBeatsKit/Modules/VRSDK/VR/Teleporting/VR_CharacterAimRaycaster.cs
uploadId: 546658
@@ -0,0 +1,19 @@
using System.Collections.Generic;
using UnityEngine;
namespace VRSDK.Locomotion
{
//this is a preset for line teleport
[CreateAssetMenu( fileName = "LineTeleporPreset", menuName = "VRShooterKit/Create Line Teleport Preset" )]
public class VR_LineAimHandler : VR_TeleportAimHandler
{
public override List<Vector3> GetAllPoints(Ray aimRay)
{
Vector3 start = aimRay.origin;
Vector3 end = start + ( aimRay.direction * range );
return new List<Vector3>() {start , end };
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: bb5d9394e48fb6a46b092ad83be5e637
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 168243
packageName: VR Beats Kit
packageVersion: 2.0
assetPath: Assets/VRBeatsKit/Modules/VRSDK/VR/Teleporting/VR_LineAimHandler.cs
uploadId: 546658
@@ -0,0 +1,40 @@
using UnityEngine;
using System.Collections.Generic;
namespace VRSDK.Locomotion
{
/// Class for handling rendering of a teleporting line
[RequireComponent(typeof(LineRenderer))]
public class VR_LineRenderer : MonoBehaviour
{
[SerializeField] private Gradient validTeleportGradient = null;
[SerializeField] private Gradient invalidTeleportGradient = null;
private LineRenderer lineRender = null;
private void Awake()
{
lineRender = GetComponent<LineRenderer>();
}
public virtual void CleanRender()
{
lineRender.positionCount = 0;
}
public virtual void Render(List<Vector3> points , bool suitableForTeleporting)
{
lineRender.positionCount = points.Count;
lineRender.colorGradient = suitableForTeleporting ? validTeleportGradient : invalidTeleportGradient;
for(int n = 0; n < points.Count; n++)
{
lineRender.SetPosition(n , points[n]);
}
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 7022a95e98189194eb78c7579ad43acc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 168243
packageName: VR Beats Kit
packageVersion: 2.0
assetPath: Assets/VRBeatsKit/Modules/VRSDK/VR/Teleporting/VR_LineRenderer.cs
uploadId: 546658
@@ -0,0 +1,47 @@
using UnityEngine;
using System.Collections.Generic;
namespace VRSDK.Locomotion
{
//this is a preset for a parabolic teleport
[CreateAssetMenu(fileName = "ParabolicTeleporPreset", menuName = "VRShooterKit/Create Parabolic Teleport Preset")]
public class VR_ParabolicAimHandler : VR_TeleportAimHandler
{
[SerializeField] protected float projectileSpeed = 15.0f;
[SerializeField] protected float gravity = 9.8f;
[Tooltip("The fps for the fake projectile use for generate a parabolic line, more fps will produce more points so be careful")]
[Range(10 , 75)]
[SerializeField] protected int simulatedFPS = 40;
public override List<Vector3> GetAllPoints(Ray aimRay)
{
//calculate the fake delta time
float deltaTime = 1.0f / (float) simulatedFPS;
//calculate initial speed
Vector3 m = aimRay.direction * projectileSpeed * deltaTime;
Vector3 projectilePos = aimRay.origin;
List<Vector3> points = new List<Vector3>();
//we calculate the distance of the fake projectil using just x,z so it looks better
float squareRange = range * range;
Vector2 origin = new Vector2(aimRay.origin.x , aimRay.origin.z);
while ( (origin - new Vector2( projectilePos.x , projectilePos.z )).sqrMagnitude < squareRange)
{
points.Add( projectilePos );
//add gravity
m -= Vector3.up * gravity * deltaTime;
//move our fake projectil
projectilePos += m;
}
return points;
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 0388cd264756ae64ea86d7b60a3e6c1b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 168243
packageName: VR Beats Kit
packageVersion: 2.0
assetPath: Assets/VRBeatsKit/Modules/VRSDK/VR/Teleporting/VR_ParabolicAimHandler.cs
uploadId: 546658
@@ -0,0 +1,174 @@
using UnityEngine;
using System.Collections.Generic;
namespace VRSDK.Locomotion
{
public enum TeleporState
{
WaitingInput,
PreTeleport,
PostTeleport
}
//this script makes use of all the teleport components and handles the logic for teleporting
public class VR_TeleporManager : MonoBehaviour
{
[SerializeField] private VR_LineRenderer teleportLineRender = null;
[SerializeField] private VR_TeleportAimHandler aimHandler = null;
[SerializeField] private VR_AimRaycaster aimRaycaster = null;
[SerializeField] private VR_AimMarker aimMarker = null;
[SerializeField] private VR_TeleportHandler teleportHandler = null;
[SerializeField] private CharacterController characterController = null;
private VR_Controller leftController = null;
private VR_Controller rightController = null;
private VR_Controller activeController = null;
private VR_Controller lastActiveController = null;
private AimRaycastInfo lastRaycastInfo = null;
private TeleporState currentTeleportState = TeleporState.WaitingInput;
private bool isTeleporting = false;
private void Start()
{
rightController = VR_Manager.instance.Player.RightController;
leftController = VR_Manager.instance.Player.LeftController;
}
private void LateUpdate()
{
switch (currentTeleportState)
{
case TeleporState.WaitingInput:
WaitingInputUpdate();
break;
case TeleporState.PreTeleport:
PreTeleportUpdate();
break;
case TeleporState.PostTeleport:
PostTeleportUpdate();
break;
}
}
private void WaitingInputUpdate()
{
if (isTeleporting)
return;
//wait for the teleport to start
if ( IsMovingJoystick(leftController) || IsMovingJoystick(rightController) )
{
UpdateActiveController();
currentTeleportState = TeleporState.PreTeleport;
}
}
private bool IsMovingJoystick(VR_Controller controller)
{
return controller.Input.GetJoystick().magnitude > 0.25f;
}
//set the controller that makes the teleport intent
private void UpdateActiveController()
{
activeController = GetActiveController();
lastActiveController = activeController;
}
private VR_Controller GetActiveController()
{
if (IsMovingJoystick(leftController) && IsMovingJoystick(rightController))
{
return lastActiveController == null ? rightController : lastActiveController;
}
if ( IsMovingJoystick(leftController) )
return leftController;
if (IsMovingJoystick(rightController))
return rightController;
return null;
}
//wait for the player to decide where to teleport
private void PreTeleportUpdate()
{
UpdateActiveController();
//there is no active controller try to do a teleport
if (activeController == null)
{
//if we can teleport to the last AimRaycast
if (IsAimRaycastInfoSuitableForTeleporting(lastRaycastInfo))
{
DoTeleport( lastRaycastInfo );
}
//clean the line inmediatly
teleportLineRender.CleanRender();
//go to post teleport
currentTeleportState = TeleporState.PostTeleport;
return;
}
Ray controllerRay = new Ray( activeController.transform.position, activeController.transform.forward );
//use the aimhandler to generate all the line points
List<Vector3> points = aimHandler.GetAllPoints( controllerRay );
//use the raycaster
AimRaycastInfo info = aimRaycaster.Raycast( points , activeController.transform );
if (info != null)
{
teleportLineRender.Render( info.validPoints, info.isValid );
}
else
{
teleportLineRender.Render( points , false );
}
if (IsAimRaycastInfoSuitableForTeleporting( info ))
{
aimMarker.UpdatePositionAndRotation( activeController, info );
}
else
{
aimMarker.Hide();
}
lastRaycastInfo = info;
}
private bool IsAimRaycastInfoSuitableForTeleporting(AimRaycastInfo info)
{
return info != null && info.isValid;
}
private void DoTeleport(AimRaycastInfo info)
{
isTeleporting = true;
teleportHandler.DoTeleport( characterController , aimMarker.Marker.transform , delegate { isTeleporting = false; } );
}
private void PostTeleportUpdate()
{
aimMarker.Hide();
lastActiveController = null;
lastRaycastInfo = null;
currentTeleportState = TeleporState.WaitingInput;
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: c9432419a55c7ce45b8f3cb8c6dc94da
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 168243
packageName: VR Beats Kit
packageVersion: 2.0
assetPath: Assets/VRBeatsKit/Modules/VRSDK/VR/Teleporting/VR_TeleporManager.cs
uploadId: 546658
@@ -0,0 +1,18 @@
using UnityEngine;
using System.Collections.Generic;
using System.Linq;
namespace VRSDK.Locomotion
{
/// Basic class for creating the points of a teleporting line
/// this is a scritableobject so you can create diferent presets
public abstract class VR_TeleportAimHandler : ScriptableObject
{
[SerializeField] protected float range = 15.0f;
public abstract List<Vector3> GetAllPoints(Ray aimRay);
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 37d6bd64f39a6724d847793de196b5c9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 168243
packageName: VR Beats Kit
packageVersion: 2.0
assetPath: Assets/VRBeatsKit/Modules/VRSDK/VR/Teleporting/VR_TeleportAimHandler.cs
uploadId: 546658
@@ -0,0 +1,27 @@
using System.Collections.Generic;
using UnityEngine;
namespace VRSDK.Locomotion
{
public class VR_TeleportAimRaycaster : VR_AimRaycaster
{
[SerializeField] private float characterRadiusOffset = 1.5f;
[SerializeField] private CharacterController characterController = null;
[SerializeField] private float slopeLimit = 20.0f;
protected override AimRaycastInfo ProcessHitInfo(RaycastHit hitInfo, List<Vector3> validPoints)
{
Vector3 start = hitInfo.point + ( hitInfo.normal * characterController.radius * characterRadiusOffset ) + hitInfo.normal;
Vector3 end = start + ( hitInfo.normal * characterController.height );
AimRaycastInfo info = new AimRaycastInfo();
info.hitPoint = hitInfo.point;
info.normal = hitInfo.normal;
info.validPoints = validPoints;
//check if the character can fit in this place
info.isValid = GetSlopeAngle(info.normal) < slopeLimit && !Physics.CheckCapsule( start, end, characterController.radius * characterRadiusOffset , validLayerMask.value , QueryTriggerInteraction.Ignore );
return info;
}
}
}
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: f3732281f0ab459096cd967e7d6bd4a1
timeCreated: 1601750964
AssetOrigin:
serializedVersion: 1
productId: 168243
packageName: VR Beats Kit
packageVersion: 2.0
assetPath: Assets/VRBeatsKit/Modules/VRSDK/VR/Teleporting/VR_TeleportAimRaycaster.cs
uploadId: 546658
@@ -0,0 +1,66 @@
using System.Collections;
using UnityEngine;
using UnityEngine.Events;
using System;
namespace VRSDK.Locomotion
{
//this script handles the position change part of the teleport system
public class VR_TeleportHandler : MonoBehaviour
{
[SerializeField] private VR_ScreenFader screenFader = null;
[SerializeField] private bool useBlink = true;
[SerializeField] private float blinkTime = 0.5f;
[SerializeField] private UnityEvent onTeleport = null;
private Vector3 characterPosition = Vector3.zero;
private Vector3 teleportFoward = Vector3.zero;
private CharacterController affectedCharacterController = null;
public UnityEvent OnTeleport { get { return onTeleport; } }
/// Teleport a charactercontroller to a new position and rotation
public void DoTeleport(CharacterController characterController , Transform to , Action onTeleportComplete)
{
affectedCharacterController = characterController;
characterPosition = to.position + ( Vector3.up * characterController.height * 0.5f ) + (characterController.center * -1.0f);
teleportFoward = to.forward;
teleportFoward.y = 0.0f;
if (useBlink)
{
StartCoroutine( TeleportRoutine(onTeleportComplete) );
}
else
{
SetTeleportPositionAndRotation();
if (onTeleportComplete != null)
onTeleportComplete();
}
}
//we use a routine if we want to use a screen fading effect
private IEnumerator TeleportRoutine(Action onTeleportComplete)
{
yield return StartCoroutine( screenFader.Fade(0.0f , 1.0f , blinkTime) );
SetTeleportPositionAndRotation();
onTeleport.Invoke();
yield return StartCoroutine( screenFader.Fade( 1.0f, 0.0f, blinkTime ) );
if (onTeleportComplete != null)
onTeleportComplete();
}
//set final teleport position and rotation
private void SetTeleportPositionAndRotation()
{
affectedCharacterController.transform.position = characterPosition;
affectedCharacterController.transform.forward = teleportFoward;
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 92ef5427d00dc9f47b0d5a7c20dc5bd3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 168243
packageName: VR Beats Kit
packageVersion: 2.0
assetPath: Assets/VRBeatsKit/Modules/VRSDK/VR/Teleporting/VR_TeleportHandler.cs
uploadId: 546658