Compare commits

11 Commits

Author SHA1 Message Date
jongjae0305 c335995a9a feat: update song selection, score UI, and song creator features
- SongSelectManager/SongDetailPanel: 곡 선택 및 상세 패널 개선
- SongCreatorManager: 곡 생성 기능 추가
- FinalScoreLabel/ScoreManager: 결과 화면 점수 UI 업데이트
- MarqueeText: 마퀴 텍스트 컴포넌트 개선
- NoteData/SongController: 노트 데이터 및 컨트롤러 보완

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 17:29:50 +09:00
whdwo798 72dad1ce4c docs: update project handoff 2026-05-29 00:53:49 +09:00
whdwo798 fb59fc36f7 feat: polish result screen UI 2026-05-29 00:51:32 +09:00
whdwo798 b46ccddbdb feat: polish game HUD scoring and results 2026-05-29 00:32:21 +09:00
jongjae0305 c4330aa544 docs: add NAS config template 2026-05-28 19:03:34 +09:00
jongjae0305 03105a4f85 feat: polish VR gameplay and sync tools 2026-05-28 19:01:20 +09:00
jongjae0305 ee34d79a66 docs: update html code review docs 2026-05-26 19:12:06 +09:00
jongjae0305 abd3c9bb36 fix: widen note lanes and clear warnings 2026-05-26 18:54:56 +09:00
jongjae0305 182d2c90b9 fix: stabilize VR UI and song playback 2026-05-26 18:21:58 +09:00
jongjae0305 5e5e918c10 docs: update project handoff status 2026-05-26 17:18:02 +09:00
jongjae0305 10e9ebae45 feat: improve VR menu pointer and BeatSaber flow 2026-05-26 17:13:02 +09:00
100 changed files with 7475 additions and 1068 deletions
+1
View File
@@ -7,5 +7,6 @@
*.wav binary *.wav binary
*.mp3 binary *.mp3 binary
*.ogg binary *.ogg binary
*.mp4 binary
*.fbx binary *.fbx binary
*.asset binary *.asset binary
+17
View File
@@ -9,6 +9,7 @@
# IDE / Generated # IDE / Generated
*.csproj *.csproj
*.csproj.user
*.slnx *.slnx
*.sln *.sln
.vscode/ .vscode/
@@ -19,5 +20,21 @@
# Credentials — never commit # Credentials — never commit
.env .env
/env
/cookies.txt
/Assets/StreamingAssets/nas_config.json /Assets/StreamingAssets/nas_config.json
/Assets/StreamingAssets/nas_config.json.meta /Assets/StreamingAssets/nas_config.json.meta
# Local tool output
/Captures/
/tools/unity-mcp-server/node_modules/
/Assets/_Recovery/
/Assets/_Recovery.meta
# Local video sources / superseded test clips
/Assets/img/*.mkv
/Assets/img/*.mkv.meta
/Assets/img/No Copyright Neon Lights Modern Animated Loop Background - Free Footage - Motion Stock Footage.mp4
/Assets/img/No Copyright Neon Lights Modern Animated Loop Background - Free Footage - Motion Stock Footage.mp4.meta
/Assets/img/neon_background_unity.mp4
/Assets/img/neon_background_unity.mp4.meta
+946
View File
@@ -0,0 +1,946 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using UnityEditor;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace VRBeats.EditorTools
{
[InitializeOnLoad]
internal static class UnityCodexBridgeServer
{
private const int PreferredPort = 19744;
private const int MaxPortAttempts = 5;
private const string AutoStartPrefKey = "VRBeats.CodexBridge.AutoStart";
private const int MaxLogs = 250;
private static readonly ConcurrentQueue<BridgeJob> Jobs = new ConcurrentQueue<BridgeJob>();
private static readonly List<LogEntry> Logs = new List<LogEntry>();
private static readonly object LogLock = new object();
private static TcpListener _listener;
private static Thread _serverThread;
private static bool _running;
private static int _port = PreferredPort;
private static int _logIndex;
static UnityCodexBridgeServer()
{
if (IsBackgroundEditorProcess())
return;
Application.logMessageReceived -= OnLogMessageReceived;
Application.logMessageReceived += OnLogMessageReceived;
EditorApplication.update -= ProcessJobs;
EditorApplication.update += ProcessJobs;
EditorApplication.quitting -= StopServer;
EditorApplication.quitting += StopServer;
AssemblyReloadEvents.beforeAssemblyReload -= StopServer;
AssemblyReloadEvents.beforeAssemblyReload += StopServer;
if (EditorPrefs.GetBool(AutoStartPrefKey, true))
StartServer();
}
[MenuItem("Tools/Codex Bridge/Start Server")]
private static void StartServerMenu()
{
StartServer();
}
[MenuItem("Tools/Codex Bridge/Stop Server")]
private static void StopServerMenu()
{
StopServer();
}
[MenuItem("Tools/Codex Bridge/Auto Start")]
private static void ToggleAutoStart()
{
bool enabled = !EditorPrefs.GetBool(AutoStartPrefKey, true);
EditorPrefs.SetBool(AutoStartPrefKey, enabled);
if (enabled)
StartServer();
}
[MenuItem("Tools/Codex Bridge/Auto Start", true)]
private static bool ValidateToggleAutoStart()
{
Menu.SetChecked("Tools/Codex Bridge/Auto Start", EditorPrefs.GetBool(AutoStartPrefKey, true));
return true;
}
[MenuItem("Tools/Codex Bridge/Capture Game View")]
private static void CaptureGameViewMenu()
{
BridgeResponse response = CaptureGameView(new Dictionary<string, string>());
Debug.Log("[CodexBridge] Capture result: " + response.Body);
}
[MenuItem("Tools/Codex Bridge/Open Sync Calibration")]
private static void OpenSyncCalibrationMenu()
{
SyncCalibrationOverlay.Open();
}
private static void StartServer()
{
if (IsBackgroundEditorProcess())
return;
if (_running)
return;
try
{
_listener = CreateListener();
_running = true;
_serverThread = new Thread(ServerLoop)
{
IsBackground = true,
Name = "UnityCodexBridgeServer"
};
_serverThread.Start();
Debug.Log("[CodexBridge] Listening on http://127.0.0.1:" + _port);
}
catch (Exception ex)
{
_running = false;
try
{
if (_listener != null)
_listener.Stop();
}
catch
{
// Ignore cleanup failures after a failed bind.
}
finally
{
_listener = null;
}
Debug.LogWarning("[CodexBridge] Failed to start: " + ex.Message);
}
}
private static void StopServer()
{
_running = false;
try
{
if (_listener != null)
_listener.Stop();
}
catch
{
// Ignore shutdown races.
}
_listener = null;
if (_serverThread != null && _serverThread.IsAlive)
_serverThread.Join(200);
_serverThread = null;
}
private static TcpListener CreateListener()
{
Exception lastException = null;
for (int i = 0; i < MaxPortAttempts; i++)
{
int port = PreferredPort + i;
TcpListener listener = null;
try
{
listener = new TcpListener(IPAddress.Loopback, port);
listener.Server.ExclusiveAddressUse = true;
listener.Start();
_port = port;
return listener;
}
catch (Exception ex)
{
lastException = ex;
try
{
if (listener != null)
listener.Stop();
}
catch
{
// Ignore cleanup failures while trying fallback ports.
}
}
}
throw lastException ?? new SocketException();
}
private static bool IsBackgroundEditorProcess()
{
string commandLine = Environment.CommandLine;
return Application.isBatchMode ||
commandLine.IndexOf("AssetImportWorker", StringComparison.OrdinalIgnoreCase) >= 0 ||
commandLine.IndexOf("-batchMode", StringComparison.OrdinalIgnoreCase) >= 0;
}
private static void ServerLoop()
{
while (_running)
{
try
{
TcpClient client = _listener.AcceptTcpClient();
ThreadPool.QueueUserWorkItem(_ => HandleClient(client));
}
catch
{
if (_running)
Thread.Sleep(100);
}
}
}
private static void HandleClient(TcpClient client)
{
using (client)
{
try
{
client.ReceiveTimeout = 5000;
client.SendTimeout = 5000;
BridgeRequest request = ReadRequest(client.GetStream());
BridgeResponse response;
if (request == null)
{
response = BridgeResponse.Json(400, "{\"ok\":false,\"error\":\"invalid_request\"}");
}
else if (request.Method == "OPTIONS")
{
response = BridgeResponse.Json(204, string.Empty);
}
else
{
BridgeJob job = new BridgeJob(request);
Jobs.Enqueue(job);
if (!job.Done.Wait(TimeSpan.FromSeconds(10)))
response = BridgeResponse.Json(504, "{\"ok\":false,\"error\":\"unity_main_thread_timeout\"}");
else
response = job.Response;
}
WriteResponse(client.GetStream(), response);
}
catch (Exception ex)
{
try
{
WriteResponse(client.GetStream(),
BridgeResponse.Json(500, "{\"ok\":false,\"error\":" + JsonString(ex.Message) + "}"));
}
catch
{
// Client has already gone away.
}
}
}
}
private static BridgeRequest ReadRequest(Stream stream)
{
StreamReader reader = new StreamReader(stream, Encoding.UTF8, false, 4096, true);
string requestLine = reader.ReadLine();
if (string.IsNullOrEmpty(requestLine))
return null;
string[] requestParts = requestLine.Split(' ');
if (requestParts.Length < 2)
return null;
int contentLength = 0;
string line;
while (!string.IsNullOrEmpty(line = reader.ReadLine()))
{
int separatorIndex = line.IndexOf(':');
if (separatorIndex <= 0)
continue;
string headerName = line.Substring(0, separatorIndex).Trim();
string headerValue = line.Substring(separatorIndex + 1).Trim();
if (string.Equals(headerName, "Content-Length", StringComparison.OrdinalIgnoreCase))
int.TryParse(headerValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out contentLength);
}
string body = string.Empty;
if (contentLength > 0)
{
char[] buffer = new char[contentLength];
int read = reader.ReadBlock(buffer, 0, contentLength);
body = new string(buffer, 0, read);
}
Uri uri = new Uri("http://127.0.0.1" + requestParts[1]);
return new BridgeRequest(requestParts[0].ToUpperInvariant(), uri.AbsolutePath, ParseQuery(uri.Query), body);
}
private static Dictionary<string, string> ParseQuery(string query)
{
Dictionary<string, string> values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
if (string.IsNullOrEmpty(query))
return values;
string trimmed = query[0] == '?' ? query.Substring(1) : query;
string[] pairs = trimmed.Split('&');
foreach (string pair in pairs)
{
if (string.IsNullOrEmpty(pair))
continue;
int separatorIndex = pair.IndexOf('=');
string key = separatorIndex >= 0 ? pair.Substring(0, separatorIndex) : pair;
string value = separatorIndex >= 0 ? pair.Substring(separatorIndex + 1) : string.Empty;
values[Uri.UnescapeDataString(key)] = Uri.UnescapeDataString(value.Replace("+", " "));
}
return values;
}
private static void WriteResponse(Stream stream, BridgeResponse response)
{
if (response == null)
response = BridgeResponse.Json(500, "{\"ok\":false,\"error\":\"null_response\"}");
byte[] body = Encoding.UTF8.GetBytes(response.Body ?? string.Empty);
string headers =
"HTTP/1.1 " + response.StatusCode + " " + StatusText(response.StatusCode) + "\r\n" +
"Content-Type: " + response.ContentType + "; charset=utf-8\r\n" +
"Content-Length: " + body.Length.ToString(CultureInfo.InvariantCulture) + "\r\n" +
"Access-Control-Allow-Origin: http://localhost\r\n" +
"Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n" +
"Access-Control-Allow-Headers: Content-Type\r\n" +
"Connection: close\r\n\r\n";
byte[] headerBytes = Encoding.UTF8.GetBytes(headers);
stream.Write(headerBytes, 0, headerBytes.Length);
if (body.Length > 0)
stream.Write(body, 0, body.Length);
}
private static string StatusText(int statusCode)
{
switch (statusCode)
{
case 200: return "OK";
case 204: return "No Content";
case 400: return "Bad Request";
case 404: return "Not Found";
case 405: return "Method Not Allowed";
case 500: return "Internal Server Error";
case 504: return "Gateway Timeout";
default: return "OK";
}
}
private static void ProcessJobs()
{
while (Jobs.TryDequeue(out BridgeJob job))
{
try
{
job.Response = Execute(job.Request);
}
catch (Exception ex)
{
job.Response = BridgeResponse.Json(500,
"{\"ok\":false,\"error\":" + JsonString(ex.Message) + "}");
}
finally
{
job.Done.Set();
}
}
}
private static BridgeResponse Execute(BridgeRequest request)
{
switch (request.Path)
{
case "/health":
case "/state":
return GetHealth();
case "/capture":
return CaptureGameView(request.Query);
case "/logs":
return GetLogs(request.Query);
case "/scene/roots":
return GetSceneRoots();
case "/scene/objects":
return GetSceneObjects(request.Query);
case "/object":
return GetObjectDetails(request.Query);
case "/play":
return SetPlayState(true, false);
case "/pause":
return SetPlayState(true, true);
case "/stop":
return SetPlayState(false, false);
case "/sync/open":
SyncCalibrationOverlay.Open();
return BridgeResponse.Json(200, "{\"ok\":true,\"opened\":\"sync_calibration\"}");
case "/transform":
if (request.Method != "POST")
return BridgeResponse.Json(405, "{\"ok\":false,\"error\":\"method_not_allowed\"}");
return SetTransform(request.Body);
default:
return BridgeResponse.Json(404, "{\"ok\":false,\"error\":\"not_found\"}");
}
}
private static BridgeResponse GetHealth()
{
Scene scene = SceneManager.GetActiveScene();
string body =
"{\"ok\":true" +
",\"bridge\":\"unity-codex-bridge\"" +
",\"port\":" + _port.ToString(CultureInfo.InvariantCulture) +
",\"unityVersion\":" + JsonString(Application.unityVersion) +
",\"projectPath\":" + JsonString(Directory.GetCurrentDirectory()) +
",\"scene\":" + JsonString(scene.IsValid() ? scene.name : string.Empty) +
",\"isPlaying\":" + Bool(EditorApplication.isPlaying) +
",\"isPaused\":" + Bool(EditorApplication.isPaused) +
"}";
return BridgeResponse.Json(200, body);
}
private static BridgeResponse CaptureGameView(Dictionary<string, string> query)
{
int width = GetInt(query, "width", 1280, 320, 4096);
int height = GetInt(query, "height", 720, 180, 4096);
string relativePath = "Captures/latest.png";
string absolutePath = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), relativePath));
Directory.CreateDirectory(Path.GetDirectoryName(absolutePath));
Camera camera = ResolveCaptureCamera();
string cameraName = camera != null ? camera.name : string.Empty;
bool usedCameraRender = false;
if (camera != null)
{
RenderTexture previousTarget = camera.targetTexture;
RenderTexture previousActive = RenderTexture.active;
RenderTexture renderTexture = RenderTexture.GetTemporary(width, height, 24, RenderTextureFormat.ARGB32);
Texture2D texture = null;
try
{
camera.targetTexture = renderTexture;
RenderTexture.active = renderTexture;
camera.Render();
texture = new Texture2D(width, height, TextureFormat.RGB24, false);
texture.ReadPixels(new Rect(0, 0, width, height), 0, 0);
texture.Apply();
File.WriteAllBytes(absolutePath, texture.EncodeToPNG());
usedCameraRender = true;
}
finally
{
camera.targetTexture = previousTarget;
RenderTexture.active = previousActive;
RenderTexture.ReleaseTemporary(renderTexture);
if (texture != null)
UnityEngine.Object.DestroyImmediate(texture);
}
}
if (!usedCameraRender)
ScreenCapture.CaptureScreenshot(relativePath);
string body =
"{\"ok\":true" +
",\"path\":" + JsonString(absolutePath) +
",\"relativePath\":" + JsonString(relativePath) +
",\"width\":" + width.ToString(CultureInfo.InvariantCulture) +
",\"height\":" + height.ToString(CultureInfo.InvariantCulture) +
",\"camera\":" + JsonString(cameraName) +
",\"mode\":" + JsonString(usedCameraRender ? "camera_render" : "screen_capture") +
"}";
return BridgeResponse.Json(200, body);
}
private static Camera ResolveCaptureCamera()
{
Camera main = Camera.main;
if (main != null && main.isActiveAndEnabled)
return main;
Camera[] cameras = UnityEngine.Object.FindObjectsByType<Camera>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
foreach (Camera camera in cameras)
{
if (camera != null && camera.isActiveAndEnabled)
return camera;
}
return main != null ? main : (cameras.Length > 0 ? cameras[0] : null);
}
private static BridgeResponse GetLogs(Dictionary<string, string> query)
{
int count = GetInt(query, "count", 80, 1, MaxLogs);
List<LogEntry> snapshot;
lock (LogLock)
{
int start = Mathf.Max(0, Logs.Count - count);
snapshot = Logs.GetRange(start, Logs.Count - start);
}
StringBuilder sb = new StringBuilder();
sb.Append("{\"ok\":true,\"logs\":[");
for (int i = 0; i < snapshot.Count; i++)
{
if (i > 0)
sb.Append(',');
LogEntry entry = snapshot[i];
sb.Append('{');
sb.Append("\"index\":").Append(entry.Index.ToString(CultureInfo.InvariantCulture)).Append(',');
sb.Append("\"time\":").Append(JsonString(entry.Time)).Append(',');
sb.Append("\"type\":").Append(JsonString(entry.Type)).Append(',');
sb.Append("\"message\":").Append(JsonString(entry.Message)).Append(',');
sb.Append("\"stackTrace\":").Append(JsonString(entry.StackTrace));
sb.Append('}');
}
sb.Append("]}");
return BridgeResponse.Json(200, sb.ToString());
}
private static BridgeResponse GetSceneRoots()
{
Scene scene = SceneManager.GetActiveScene();
GameObject[] roots = scene.IsValid() ? scene.GetRootGameObjects() : Array.Empty<GameObject>();
StringBuilder sb = new StringBuilder();
sb.Append("{\"ok\":true,\"scene\":").Append(JsonString(scene.IsValid() ? scene.name : string.Empty));
sb.Append(",\"roots\":[");
for (int i = 0; i < roots.Length; i++)
{
if (i > 0)
sb.Append(',');
AppendGameObjectSummary(sb, roots[i]);
}
sb.Append("]}");
return BridgeResponse.Json(200, sb.ToString());
}
private static BridgeResponse GetSceneObjects(Dictionary<string, string> query)
{
string filter = GetString(query, "query", string.Empty);
int limit = GetInt(query, "limit", 120, 1, 500);
string lowerFilter = filter.ToLowerInvariant();
Transform[] transforms = UnityEngine.Object.FindObjectsByType<Transform>(FindObjectsInactive.Include, FindObjectsSortMode.None);
StringBuilder sb = new StringBuilder();
sb.Append("{\"ok\":true,\"objects\":[");
int written = 0;
foreach (Transform transform in transforms)
{
if (transform == null || transform.gameObject == null)
continue;
if (EditorUtility.IsPersistent(transform.gameObject))
continue;
if (!transform.gameObject.scene.IsValid())
continue;
string path = GetHierarchyPath(transform.gameObject);
if (!string.IsNullOrEmpty(lowerFilter) &&
path.ToLowerInvariant().IndexOf(lowerFilter, StringComparison.Ordinal) < 0)
continue;
if (written > 0)
sb.Append(',');
AppendGameObjectSummary(sb, transform.gameObject);
written++;
if (written >= limit)
break;
}
sb.Append("],\"count\":").Append(written.ToString(CultureInfo.InvariantCulture)).Append('}');
return BridgeResponse.Json(200, sb.ToString());
}
private static BridgeResponse GetObjectDetails(Dictionary<string, string> query)
{
string path = GetString(query, "path", string.Empty);
GameObject go = FindGameObjectByPath(path);
if (go == null)
return BridgeResponse.Json(404, "{\"ok\":false,\"error\":\"object_not_found\",\"path\":" + JsonString(path) + "}");
StringBuilder sb = new StringBuilder();
sb.Append("{\"ok\":true,\"object\":");
AppendGameObjectDetails(sb, go);
sb.Append('}');
return BridgeResponse.Json(200, sb.ToString());
}
private static BridgeResponse SetPlayState(bool play, bool pause)
{
EditorApplication.isPlaying = play;
EditorApplication.isPaused = pause;
return GetHealth();
}
private static BridgeResponse SetTransform(string body)
{
string path = ExtractString(body, "path");
if (string.IsNullOrEmpty(path))
return BridgeResponse.Json(400, "{\"ok\":false,\"error\":\"missing_path\"}");
GameObject go = FindGameObjectByPath(path);
if (go == null)
return BridgeResponse.Json(404, "{\"ok\":false,\"error\":\"object_not_found\",\"path\":" + JsonString(path) + "}");
Undo.RecordObject(go.transform, "Codex Bridge Set Transform");
if (TryExtractVector3(body, "position", out Vector3 position))
go.transform.position = position;
if (TryExtractVector3(body, "localPosition", out Vector3 localPosition))
go.transform.localPosition = localPosition;
if (TryExtractVector3(body, "rotationEuler", out Vector3 rotationEuler))
go.transform.eulerAngles = rotationEuler;
if (TryExtractVector3(body, "localRotationEuler", out Vector3 localRotationEuler))
go.transform.localEulerAngles = localRotationEuler;
if (TryExtractVector3(body, "scale", out Vector3 scale))
go.transform.localScale = scale;
EditorUtility.SetDirty(go.transform);
StringBuilder sb = new StringBuilder();
sb.Append("{\"ok\":true,\"object\":");
AppendGameObjectDetails(sb, go);
sb.Append('}');
return BridgeResponse.Json(200, sb.ToString());
}
private static void AppendGameObjectSummary(StringBuilder sb, GameObject go)
{
sb.Append('{');
sb.Append("\"name\":").Append(JsonString(go.name)).Append(',');
sb.Append("\"path\":").Append(JsonString(GetHierarchyPath(go))).Append(',');
sb.Append("\"activeSelf\":").Append(Bool(go.activeSelf)).Append(',');
sb.Append("\"activeInHierarchy\":").Append(Bool(go.activeInHierarchy)).Append(',');
sb.Append("\"scene\":").Append(JsonString(go.scene.IsValid() ? go.scene.name : string.Empty)).Append(',');
sb.Append("\"position\":").Append(VectorJson(go.transform.position)).Append(',');
sb.Append("\"rotationEuler\":").Append(VectorJson(go.transform.eulerAngles)).Append(',');
sb.Append("\"scale\":").Append(VectorJson(go.transform.localScale));
sb.Append('}');
}
private static void AppendGameObjectDetails(StringBuilder sb, GameObject go)
{
AppendGameObjectSummary(sb, go);
sb.Length -= 1;
Component[] components = go.GetComponents<Component>();
sb.Append(",\"components\":[");
for (int i = 0; i < components.Length; i++)
{
if (i > 0)
sb.Append(',');
Component component = components[i];
sb.Append(JsonString(component != null ? component.GetType().FullName : "Missing Script"));
}
sb.Append("]}");
}
private static GameObject FindGameObjectByPath(string path)
{
if (string.IsNullOrEmpty(path))
return null;
string[] parts = path.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 0)
return null;
Scene scene = SceneManager.GetActiveScene();
if (!scene.IsValid())
return null;
foreach (GameObject root in scene.GetRootGameObjects())
{
if (root.name != parts[0])
continue;
Transform current = root.transform;
for (int i = 1; i < parts.Length && current != null; i++)
{
current = FindChildByName(current, parts[i]);
}
if (current != null)
return current.gameObject;
}
return null;
}
private static Transform FindChildByName(Transform parent, string childName)
{
for (int i = 0; i < parent.childCount; i++)
{
Transform child = parent.GetChild(i);
if (child.name == childName)
return child;
}
return null;
}
private static string GetHierarchyPath(GameObject go)
{
Stack<string> parts = new Stack<string>();
Transform current = go.transform;
while (current != null)
{
parts.Push(current.name);
current = current.parent;
}
return string.Join("/", parts.ToArray());
}
private static void OnLogMessageReceived(string condition, string stackTrace, LogType type)
{
lock (LogLock)
{
Logs.Add(new LogEntry
{
Index = ++_logIndex,
Time = DateTime.Now.ToString("HH:mm:ss", CultureInfo.InvariantCulture),
Type = type.ToString(),
Message = condition ?? string.Empty,
StackTrace = stackTrace ?? string.Empty
});
if (Logs.Count > MaxLogs)
Logs.RemoveAt(0);
}
}
private static int GetInt(Dictionary<string, string> query, string key, int fallback, int min, int max)
{
if (!query.TryGetValue(key, out string value) ||
!int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsed))
return fallback;
return Mathf.Clamp(parsed, min, max);
}
private static string GetString(Dictionary<string, string> query, string key, string fallback)
{
return query.TryGetValue(key, out string value) ? value : fallback;
}
private static string ExtractString(string json, string key)
{
if (string.IsNullOrEmpty(json))
return string.Empty;
Match match = Regex.Match(json,
"\"" + Regex.Escape(key) + "\"\\s*:\\s*\"((?:\\\\.|[^\"])*)\"",
RegexOptions.CultureInvariant);
return match.Success ? Regex.Unescape(match.Groups[1].Value) : string.Empty;
}
private static bool TryExtractVector3(string json, string key, out Vector3 value)
{
value = Vector3.zero;
if (string.IsNullOrEmpty(json))
return false;
Match objectMatch = Regex.Match(json,
"\"" + Regex.Escape(key) + "\"\\s*:\\s*\\{(?<body>.*?)\\}",
RegexOptions.Singleline | RegexOptions.CultureInvariant);
if (!objectMatch.Success)
return false;
string objectBody = objectMatch.Groups["body"].Value;
if (!TryExtractFloat(objectBody, "x", out float x) ||
!TryExtractFloat(objectBody, "y", out float y) ||
!TryExtractFloat(objectBody, "z", out float z))
return false;
value = new Vector3(x, y, z);
return true;
}
private static bool TryExtractFloat(string json, string key, out float value)
{
value = 0f;
Match match = Regex.Match(json,
"\"" + Regex.Escape(key) + "\"\\s*:\\s*(?<num>-?\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?)",
RegexOptions.CultureInvariant);
return match.Success &&
float.TryParse(match.Groups["num"].Value, NumberStyles.Float, CultureInfo.InvariantCulture, out value);
}
private static string VectorJson(Vector3 value)
{
return "{\"x\":" + FloatJson(value.x) +
",\"y\":" + FloatJson(value.y) +
",\"z\":" + FloatJson(value.z) + "}";
}
private static string FloatJson(float value)
{
if (float.IsNaN(value) || float.IsInfinity(value))
return "0";
return value.ToString("0.#####", CultureInfo.InvariantCulture);
}
private static string Bool(bool value)
{
return value ? "true" : "false";
}
private static string JsonString(string value)
{
if (value == null)
return "null";
StringBuilder sb = new StringBuilder(value.Length + 2);
sb.Append('"');
foreach (char c in value)
{
switch (c)
{
case '\\':
sb.Append("\\\\");
break;
case '"':
sb.Append("\\\"");
break;
case '\n':
sb.Append("\\n");
break;
case '\r':
sb.Append("\\r");
break;
case '\t':
sb.Append("\\t");
break;
default:
if (c < 32)
sb.Append("\\u").Append(((int)c).ToString("x4", CultureInfo.InvariantCulture));
else
sb.Append(c);
break;
}
}
sb.Append('"');
return sb.ToString();
}
private sealed class BridgeRequest
{
public readonly string Method;
public readonly string Path;
public readonly Dictionary<string, string> Query;
public readonly string Body;
public BridgeRequest(string method, string path, Dictionary<string, string> query, string body)
{
Method = method;
Path = path;
Query = query;
Body = body ?? string.Empty;
}
}
private sealed class BridgeResponse
{
public readonly int StatusCode;
public readonly string ContentType;
public readonly string Body;
private BridgeResponse(int statusCode, string contentType, string body)
{
StatusCode = statusCode;
ContentType = contentType;
Body = body;
}
public static BridgeResponse Json(int statusCode, string body)
{
return new BridgeResponse(statusCode, "application/json", body);
}
}
private sealed class BridgeJob
{
public readonly BridgeRequest Request;
public readonly ManualResetEventSlim Done = new ManualResetEventSlim(false);
public BridgeResponse Response;
public BridgeJob(BridgeRequest request)
{
Request = request;
}
}
private sealed class LogEntry
{
public int Index;
public string Time;
public string Type;
public string Message;
public string StackTrace;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b3aaf23c3d6f42b5b1f68100b9b0f682
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+1 -1
View File
@@ -35,7 +35,7 @@ public static class VRBeatSaberSceneBuilder
var scene = EditorSceneManager.OpenScene(gamePath, OpenSceneMode.Single); var scene = EditorSceneManager.OpenScene(gamePath, OpenSceneMode.Single);
// PlayableManager 제거 (PlayableDirector는 유지) // PlayableManager 제거 (PlayableDirector는 유지)
var pm = Object.FindObjectOfType<PlayableManager>(); var pm = Object.FindFirstObjectByType<PlayableManager>();
if (pm != null) if (pm != null)
Object.DestroyImmediate(pm); Object.DestroyImmediate(pm);
+130 -67
View File
@@ -150,10 +150,6 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 441482e8936e35048a1dffac814e3ef8, type: 3} m_Script: {fileID: 11500000, guid: 441482e8936e35048a1dffac814e3ef8, type: 3}
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
m_Profile: {fileID: 11400000, guid: 8882f7434ef4fcd458f02738f32c7b11, type: 2}
m_StaticLightingSkyUniqueID: 0
m_SkySettings: {fileID: 0}
m_SkySettingsFromProfile: {fileID: 0}
--- !u!4 &72731932 --- !u!4 &72731932
Transform: Transform:
m_ObjectHideFlags: 1 m_ObjectHideFlags: 1
@@ -229,7 +225,7 @@ MonoBehaviour:
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
m_Material: {fileID: 0} m_Material: {fileID: 0}
m_Color: {r: 0.01673192, g: 0.29524884, b: 0.4433962, a: 0.98039216} m_Color: {r: 0.01673192, g: 0.29524884, b: 0.4433962, a: 0.82}
m_RaycastTarget: 1 m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1 m_Maskable: 1
@@ -397,9 +393,9 @@ MonoBehaviour:
m_Transition: 1 m_Transition: 1
m_Colors: m_Colors:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1} m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} m_HighlightedColor: {r: 0.72, g: 0.92, b: 1, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} m_PressedColor: {r: 0.55, g: 0.78, b: 0.9, a: 1}
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} m_SelectedColor: {r: 0.72, g: 0.92, b: 1, a: 1}
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
m_ColorMultiplier: 1 m_ColorMultiplier: 1
m_FadeDuration: 0.1 m_FadeDuration: 0.1
@@ -432,7 +428,7 @@ MonoBehaviour:
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
m_Material: {fileID: 0} m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1} m_Color: {r: 0.06, g: 0.12, b: 0.22, a: 0.72}
m_RaycastTarget: 1 m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1 m_Maskable: 1
@@ -591,13 +587,18 @@ PrefabInstance:
- target: {fileID: 6994737657188836379, guid: d614df01a29ad3e45bb831298dfbad2c, - target: {fileID: 6994737657188836379, guid: d614df01a29ad3e45bb831298dfbad2c,
type: 3} type: 3}
propertyPath: editorPart.handSelected propertyPath: editorPart.handSelected
value: 1 value: 0
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 6994737657188836379, guid: d614df01a29ad3e45bb831298dfbad2c, - target: {fileID: 6994737657188836379, guid: d614df01a29ad3e45bb831298dfbad2c,
type: 3} type: 3}
propertyPath: editorPart.selectedMenu propertyPath: editorPart.selectedMenu
value: 1 value: 1
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 6994737657188836379, guid: d614df01a29ad3e45bb831298dfbad2c,
type: 3}
propertyPath: handSettings.rotationOffset.x
value: 45
objectReference: {fileID: 0}
- target: {fileID: 7214299079173282187, guid: d614df01a29ad3e45bb831298dfbad2c, - target: {fileID: 7214299079173282187, guid: d614df01a29ad3e45bb831298dfbad2c,
type: 3} type: 3}
propertyPath: colorSide propertyPath: colorSide
@@ -616,42 +617,42 @@ PrefabInstance:
- target: {fileID: 7481659171502770090, guid: d614df01a29ad3e45bb831298dfbad2c, - target: {fileID: 7481659171502770090, guid: d614df01a29ad3e45bb831298dfbad2c,
type: 3} type: 3}
propertyPath: m_LocalPosition.x propertyPath: m_LocalPosition.x
value: 0.3567679 value: 0
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 7481659171502770090, guid: d614df01a29ad3e45bb831298dfbad2c, - target: {fileID: 7481659171502770090, guid: d614df01a29ad3e45bb831298dfbad2c,
type: 3} type: 3}
propertyPath: m_LocalPosition.y propertyPath: m_LocalPosition.y
value: -0.4698689 value: 0
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 7481659171502770090, guid: d614df01a29ad3e45bb831298dfbad2c, - target: {fileID: 7481659171502770090, guid: d614df01a29ad3e45bb831298dfbad2c,
type: 3} type: 3}
propertyPath: m_LocalPosition.z propertyPath: m_LocalPosition.z
value: -0.22523999 value: 0
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 7481659171502770090, guid: d614df01a29ad3e45bb831298dfbad2c, - target: {fileID: 7481659171502770090, guid: d614df01a29ad3e45bb831298dfbad2c,
type: 3} type: 3}
propertyPath: m_LocalRotation.w propertyPath: m_LocalRotation.w
value: 0.99958926 value: 0.7071068
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 7481659171502770090, guid: d614df01a29ad3e45bb831298dfbad2c, - target: {fileID: 7481659171502770090, guid: d614df01a29ad3e45bb831298dfbad2c,
type: 3} type: 3}
propertyPath: m_LocalRotation.x propertyPath: m_LocalRotation.x
value: -0 value: 0.7071068
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 7481659171502770090, guid: d614df01a29ad3e45bb831298dfbad2c, - target: {fileID: 7481659171502770090, guid: d614df01a29ad3e45bb831298dfbad2c,
type: 3} type: 3}
propertyPath: m_LocalRotation.y propertyPath: m_LocalRotation.y
value: -0.028659718 value: 0
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 7481659171502770090, guid: d614df01a29ad3e45bb831298dfbad2c, - target: {fileID: 7481659171502770090, guid: d614df01a29ad3e45bb831298dfbad2c,
type: 3} type: 3}
propertyPath: m_LocalRotation.z propertyPath: m_LocalRotation.z
value: -0 value: 0
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 7481659171502770090, guid: d614df01a29ad3e45bb831298dfbad2c, - target: {fileID: 7481659171502770090, guid: d614df01a29ad3e45bb831298dfbad2c,
type: 3} type: 3}
propertyPath: m_LocalEulerAnglesHint.x propertyPath: m_LocalEulerAnglesHint.x
value: 0 value: 90
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 7481659171502770090, guid: d614df01a29ad3e45bb831298dfbad2c, - target: {fileID: 7481659171502770090, guid: d614df01a29ad3e45bb831298dfbad2c,
type: 3} type: 3}
@@ -1256,8 +1257,8 @@ RectTransform:
m_PrefabAsset: {fileID: 0} m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 454873725} m_GameObject: {fileID: 454873725}
m_LocalRotation: {x: -0.007861641, y: 0.97856337, z: 0.038037617, w: 0.20225017} m_LocalRotation: {x: -0.007861641, y: 0.97856337, z: 0.038037617, w: 0.20225017}
m_LocalPosition: {x: 0, y: 0, z: 17.8} m_LocalPosition: {x: 0, y: 0, z: 5}
m_LocalScale: {x: 0.0049999994, y: 0.005, z: 0.005} m_LocalScale: {x: 0.006, y: 0.006, z: 0.006}
m_ConstrainProportionsScale: 0 m_ConstrainProportionsScale: 0
m_Children: m_Children:
- {fileID: 1800899779} - {fileID: 1800899779}
@@ -1267,7 +1268,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0} m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0} m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: -7.5, y: -1.7} m_AnchoredPosition: {x: 0, y: 2.4}
m_SizeDelta: {x: 847.5, y: 1141.086} m_SizeDelta: {x: 847.5, y: 1141.086}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &454873730 --- !u!114 &454873730
@@ -1288,6 +1289,15 @@ MonoBehaviour:
scoreFollowTime: 1 scoreFollowTime: 1
canvasGroup: {fileID: 454873732} canvasGroup: {fileID: 454873732}
onGameOver: {fileID: 11400000, guid: 620f7aac602ad78418fb6c1ca0a6670a, type: 2} onGameOver: {fileID: 11400000, guid: 620f7aac602ad78418fb6c1ca0a6670a, type: 2}
comboLabel: {fileID: 0}
accuracyLabel: {fileID: 0}
judgementLabel: {fileID: 0}
createMissingHudLabels: 1
applyHudPlacement: 0
hudAnchoredPosition: {x: 0, y: 2.4}
perfectWindow: 0.11
greatWindow: 0.2
goodWindow: 0.32
--- !u!114 &454873731 --- !u!114 &454873731
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@@ -2112,9 +2122,9 @@ MonoBehaviour:
m_Transition: 1 m_Transition: 1
m_Colors: m_Colors:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1} m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} m_HighlightedColor: {r: 0.72, g: 0.92, b: 1, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} m_PressedColor: {r: 0.55, g: 0.78, b: 0.9, a: 1}
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} m_SelectedColor: {r: 0.72, g: 0.92, b: 1, a: 1}
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
m_ColorMultiplier: 1 m_ColorMultiplier: 1
m_FadeDuration: 0.1 m_FadeDuration: 0.1
@@ -2147,7 +2157,7 @@ MonoBehaviour:
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
m_Material: {fileID: 0} m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1} m_Color: {r: 0.06, g: 0.12, b: 0.22, a: 0.72}
m_RaycastTarget: 1 m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1 m_Maskable: 1
@@ -2384,7 +2394,7 @@ MonoBehaviour:
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
m_destroyOnLoad: 1 m_destroyOnLoad: 1
currentSDK: 0 currentSDK: 3
gestureConfig: gestureConfig:
minAcelerationThreshold: 15 minAcelerationThreshold: 15
maxAcelerationThreshold: 40 maxAcelerationThreshold: 40
@@ -2656,8 +2666,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0.5} m_AnchorMin: {x: 0, y: 0.5}
m_AnchorMax: {x: 1, y: 0.5} m_AnchorMax: {x: 1, y: 0.5}
m_AnchoredPosition: {x: -1.4000015, y: 2.5099983} m_AnchoredPosition: {x: 0, y: -0.45}
m_SizeDelta: {x: 20.150002, y: 6.41} m_SizeDelta: {x: 18.8, y: 13.2}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!1 &1209841520 --- !u!1 &1209841520
GameObject: GameObject:
@@ -2696,7 +2706,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -16.119999, y: -15.380001} m_AnchoredPosition: {x: -17.400002, y: -15.380001}
m_SizeDelta: {x: 357, y: 157} m_SizeDelta: {x: 357, y: 157}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1209841522 --- !u!114 &1209841522
@@ -2721,9 +2731,9 @@ MonoBehaviour:
m_Transition: 1 m_Transition: 1
m_Colors: m_Colors:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1} m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} m_HighlightedColor: {r: 0.72, g: 0.92, b: 1, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} m_PressedColor: {r: 0.55, g: 0.78, b: 0.9, a: 1}
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} m_SelectedColor: {r: 0.72, g: 0.92, b: 1, a: 1}
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
m_ColorMultiplier: 1 m_ColorMultiplier: 1
m_FadeDuration: 0.1 m_FadeDuration: 0.1
@@ -2756,7 +2766,7 @@ MonoBehaviour:
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
m_Material: {fileID: 0} m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1} m_Color: {r: 0.06, g: 0.12, b: 0.22, a: 0.72}
m_RaycastTarget: 1 m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1 m_Maskable: 1
@@ -2829,7 +2839,6 @@ MonoBehaviour:
playZone: {fileID: 778115775} playZone: {fileID: 778115775}
player: {fileID: 408071456} player: {fileID: 408071456}
settings: {fileID: 11400000, guid: 77da81612feecbe429ec290359d3547e, type: 2} settings: {fileID: 11400000, guid: 77da81612feecbe429ec290359d3547e, type: 2}
onGameOver: {fileID: 11400000, guid: 620f7aac602ad78418fb6c1ca0a6670a, type: 2}
--- !u!4 &1215359035 --- !u!4 &1215359035
Transform: Transform:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@@ -3794,8 +3803,8 @@ MonoBehaviour:
m_faceColor: m_faceColor:
serializedVersion: 2 serializedVersion: 2
rgba: 4294967295 rgba: 4294967295
m_fontSize: 7.1 m_fontSize: 4.4
m_fontSizeBase: 7.1 m_fontSizeBase: 4.4
m_fontWeight: 400 m_fontWeight: 400
m_enableAutoSizing: 0 m_enableAutoSizing: 0
m_fontSizeMin: 18 m_fontSizeMin: 18
@@ -3807,7 +3816,7 @@ MonoBehaviour:
m_characterSpacing: 0 m_characterSpacing: 0
m_characterHorizontalScale: 1 m_characterHorizontalScale: 1
m_wordSpacing: 0 m_wordSpacing: 0
m_lineSpacing: 0 m_lineSpacing: -18
m_lineSpacingMax: 0 m_lineSpacingMax: 0
m_paragraphSpacing: 0 m_paragraphSpacing: 0
m_charWidthMaxAdj: 0 m_charWidthMaxAdj: 0
@@ -4113,7 +4122,7 @@ MonoBehaviour:
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
m_Material: {fileID: 0} m_Material: {fileID: 0}
m_Color: {r: 0.01673192, g: 0.29524884, b: 0.4433962, a: 0.98039216} m_Color: {r: 0.01673192, g: 0.29524884, b: 0.4433962, a: 0.82}
m_RaycastTarget: 1 m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1 m_Maskable: 1
@@ -4380,6 +4389,7 @@ GameObject:
m_Component: m_Component:
- component: {fileID: 1958381893} - component: {fileID: 1958381893}
- component: {fileID: 1958381892} - component: {fileID: 1958381892}
- component: {fileID: 1958381894}
m_Layer: 0 m_Layer: 0
m_Name: SongController m_Name: SongController
m_TagString: Untagged m_TagString: Untagged
@@ -4418,6 +4428,23 @@ Transform:
m_Children: [] m_Children: []
m_Father: {fileID: 0} m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &1958381894
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1958381891}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 3e381cd99de84f67b9f83c19a032dc24, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::Game360VideoBackground
videoClip: {fileID: 32900000, guid: f47e26c36f77476ba803b5158d1b30da, type: 3}
renderTextureSize: 2048
muteVideoAudio: 1
skyboxRotationDegrees: 90
skyboxExposure: 1
--- !u!4 &2043306906 stripped --- !u!4 &2043306906 stripped
Transform: Transform:
m_CorrespondingSourceObject: {fileID: 1002067974212273307, guid: e7173b1ee3369204eb181b376ede2a3e, m_CorrespondingSourceObject: {fileID: 1002067974212273307, guid: e7173b1ee3369204eb181b376ede2a3e,
@@ -4461,7 +4488,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 15.980001, y: -15.380001} m_AnchoredPosition: {x: 17.400002, y: -15.380001}
m_SizeDelta: {x: 357, y: 157} m_SizeDelta: {x: 357, y: 157}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &2094521063 --- !u!114 &2094521063
@@ -4486,9 +4513,9 @@ MonoBehaviour:
m_Transition: 1 m_Transition: 1
m_Colors: m_Colors:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1} m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} m_HighlightedColor: {r: 0.72, g: 0.92, b: 1, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} m_PressedColor: {r: 0.55, g: 0.78, b: 0.9, a: 1}
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} m_SelectedColor: {r: 0.72, g: 0.92, b: 1, a: 1}
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
m_ColorMultiplier: 1 m_ColorMultiplier: 1
m_FadeDuration: 0.1 m_FadeDuration: 0.1
@@ -4521,7 +4548,7 @@ MonoBehaviour:
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
m_Material: {fileID: 0} m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1} m_Color: {r: 0.06, g: 0.12, b: 0.22, a: 0.72}
m_RaycastTarget: 1 m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1 m_Maskable: 1
@@ -4555,9 +4582,10 @@ MonoBehaviour:
m_GameObject: {fileID: 2094521061} m_GameObject: {fileID: 2094521061}
m_Enabled: 1 m_Enabled: 1
m_EditorHideFlags: 0 m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 3f8ab482667b2f44691ffe7131ffbdb7, type: 3} m_Script: {fileID: 11500000, guid: 504eaffe3c185bf469313a589c4026d0, type: 3}
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
button: {fileID: 2094521063}
sceneName: Menu sceneName: Menu
--- !u!1 &2138780048 --- !u!1 &2138780048
GameObject: GameObject:
@@ -4860,6 +4888,11 @@ PrefabInstance:
propertyPath: startOnRightController propertyPath: startOnRightController
value: 1 value: 1
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 934260912312062542, guid: f555cbb0b089fb64ca7c7a5b07f11520,
type: 3}
propertyPath: editorPart.foldoutBasic
value: 1
objectReference: {fileID: 0}
- target: {fileID: 934260912312062542, guid: f555cbb0b089fb64ca7c7a5b07f11520, - target: {fileID: 934260912312062542, guid: f555cbb0b089fb64ca7c7a5b07f11520,
type: 3} type: 3}
propertyPath: editorPart.foldoutInput propertyPath: editorPart.foldoutInput
@@ -4868,13 +4901,23 @@ PrefabInstance:
- target: {fileID: 934260912312062542, guid: f555cbb0b089fb64ca7c7a5b07f11520, - target: {fileID: 934260912312062542, guid: f555cbb0b089fb64ca7c7a5b07f11520,
type: 3} type: 3}
propertyPath: editorPart.handSelected propertyPath: editorPart.handSelected
value: 1 value: 0
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 934260912312062542, guid: f555cbb0b089fb64ca7c7a5b07f11520, - target: {fileID: 934260912312062542, guid: f555cbb0b089fb64ca7c7a5b07f11520,
type: 3} type: 3}
propertyPath: editorPart.selectedMenu propertyPath: editorPart.selectedMenu
value: 1 value: 1
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 934260912312062542, guid: f555cbb0b089fb64ca7c7a5b07f11520,
type: 3}
propertyPath: shareHandInteractionSettings
value: 1
objectReference: {fileID: 0}
- target: {fileID: 934260912312062542, guid: f555cbb0b089fb64ca7c7a5b07f11520,
type: 3}
propertyPath: editorPart.foldoutInteraction
value: 1
objectReference: {fileID: 0}
- target: {fileID: 3968956848465607219, guid: f555cbb0b089fb64ca7c7a5b07f11520, - target: {fileID: 3968956848465607219, guid: f555cbb0b089fb64ca7c7a5b07f11520,
type: 3} type: 3}
propertyPath: m_RootOrder propertyPath: m_RootOrder
@@ -4883,27 +4926,27 @@ PrefabInstance:
- target: {fileID: 3968956848465607219, guid: f555cbb0b089fb64ca7c7a5b07f11520, - target: {fileID: 3968956848465607219, guid: f555cbb0b089fb64ca7c7a5b07f11520,
type: 3} type: 3}
propertyPath: m_LocalPosition.x propertyPath: m_LocalPosition.x
value: 0.36676788 value: 0
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 3968956848465607219, guid: f555cbb0b089fb64ca7c7a5b07f11520, - target: {fileID: 3968956848465607219, guid: f555cbb0b089fb64ca7c7a5b07f11520,
type: 3} type: 3}
propertyPath: m_LocalPosition.y propertyPath: m_LocalPosition.y
value: -0.18886888 value: 0
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 3968956848465607219, guid: f555cbb0b089fb64ca7c7a5b07f11520, - target: {fileID: 3968956848465607219, guid: f555cbb0b089fb64ca7c7a5b07f11520,
type: 3} type: 3}
propertyPath: m_LocalPosition.z propertyPath: m_LocalPosition.z
value: -0.23423982 value: 0
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 3968956848465607219, guid: f555cbb0b089fb64ca7c7a5b07f11520, - target: {fileID: 3968956848465607219, guid: f555cbb0b089fb64ca7c7a5b07f11520,
type: 3} type: 3}
propertyPath: m_LocalRotation.w propertyPath: m_LocalRotation.w
value: 1 value: 0.9238795
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 3968956848465607219, guid: f555cbb0b089fb64ca7c7a5b07f11520, - target: {fileID: 3968956848465607219, guid: f555cbb0b089fb64ca7c7a5b07f11520,
type: 3} type: 3}
propertyPath: m_LocalRotation.x propertyPath: m_LocalRotation.x
value: -0 value: 0.38268343
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 3968956848465607219, guid: f555cbb0b089fb64ca7c7a5b07f11520, - target: {fileID: 3968956848465607219, guid: f555cbb0b089fb64ca7c7a5b07f11520,
type: 3} type: 3}
@@ -4918,7 +4961,7 @@ PrefabInstance:
- target: {fileID: 3968956848465607219, guid: f555cbb0b089fb64ca7c7a5b07f11520, - target: {fileID: 3968956848465607219, guid: f555cbb0b089fb64ca7c7a5b07f11520,
type: 3} type: 3}
propertyPath: m_LocalEulerAnglesHint.x propertyPath: m_LocalEulerAnglesHint.x
value: 0 value: 45
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 3968956848465607219, guid: f555cbb0b089fb64ca7c7a5b07f11520, - target: {fileID: 3968956848465607219, guid: f555cbb0b089fb64ca7c7a5b07f11520,
type: 3} type: 3}
@@ -5093,27 +5136,27 @@ PrefabInstance:
- target: {fileID: 1002067974212273307, guid: e7173b1ee3369204eb181b376ede2a3e, - target: {fileID: 1002067974212273307, guid: e7173b1ee3369204eb181b376ede2a3e,
type: 3} type: 3}
propertyPath: m_LocalPosition.x propertyPath: m_LocalPosition.x
value: 0.36676788 value: 0
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 1002067974212273307, guid: e7173b1ee3369204eb181b376ede2a3e, - target: {fileID: 1002067974212273307, guid: e7173b1ee3369204eb181b376ede2a3e,
type: 3} type: 3}
propertyPath: m_LocalPosition.y propertyPath: m_LocalPosition.y
value: -0.18886888 value: 0
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 1002067974212273307, guid: e7173b1ee3369204eb181b376ede2a3e, - target: {fileID: 1002067974212273307, guid: e7173b1ee3369204eb181b376ede2a3e,
type: 3} type: 3}
propertyPath: m_LocalPosition.z propertyPath: m_LocalPosition.z
value: -0.23423982 value: 0
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 1002067974212273307, guid: e7173b1ee3369204eb181b376ede2a3e, - target: {fileID: 1002067974212273307, guid: e7173b1ee3369204eb181b376ede2a3e,
type: 3} type: 3}
propertyPath: m_LocalRotation.w propertyPath: m_LocalRotation.w
value: 1 value: 0.9238795
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 1002067974212273307, guid: e7173b1ee3369204eb181b376ede2a3e, - target: {fileID: 1002067974212273307, guid: e7173b1ee3369204eb181b376ede2a3e,
type: 3} type: 3}
propertyPath: m_LocalRotation.x propertyPath: m_LocalRotation.x
value: -0 value: 0.38268343
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 1002067974212273307, guid: e7173b1ee3369204eb181b376ede2a3e, - target: {fileID: 1002067974212273307, guid: e7173b1ee3369204eb181b376ede2a3e,
type: 3} type: 3}
@@ -5128,7 +5171,7 @@ PrefabInstance:
- target: {fileID: 1002067974212273307, guid: e7173b1ee3369204eb181b376ede2a3e, - target: {fileID: 1002067974212273307, guid: e7173b1ee3369204eb181b376ede2a3e,
type: 3} type: 3}
propertyPath: m_LocalEulerAnglesHint.x propertyPath: m_LocalEulerAnglesHint.x
value: 0 value: 45
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 1002067974212273307, guid: e7173b1ee3369204eb181b376ede2a3e, - target: {fileID: 1002067974212273307, guid: e7173b1ee3369204eb181b376ede2a3e,
type: 3} type: 3}
@@ -5160,6 +5203,11 @@ PrefabInstance:
propertyPath: startOnRightController propertyPath: startOnRightController
value: 0 value: 0
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 3894408489718918374, guid: e7173b1ee3369204eb181b376ede2a3e,
type: 3}
propertyPath: editorPart.foldoutBasic
value: 1
objectReference: {fileID: 0}
- target: {fileID: 3894408489718918374, guid: e7173b1ee3369204eb181b376ede2a3e, - target: {fileID: 3894408489718918374, guid: e7173b1ee3369204eb181b376ede2a3e,
type: 3} type: 3}
propertyPath: editorPart.foldoutInput propertyPath: editorPart.foldoutInput
@@ -5168,13 +5216,23 @@ PrefabInstance:
- target: {fileID: 3894408489718918374, guid: e7173b1ee3369204eb181b376ede2a3e, - target: {fileID: 3894408489718918374, guid: e7173b1ee3369204eb181b376ede2a3e,
type: 3} type: 3}
propertyPath: editorPart.handSelected propertyPath: editorPart.handSelected
value: 0 value: 1
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 3894408489718918374, guid: e7173b1ee3369204eb181b376ede2a3e, - target: {fileID: 3894408489718918374, guid: e7173b1ee3369204eb181b376ede2a3e,
type: 3} type: 3}
propertyPath: editorPart.selectedMenu propertyPath: editorPart.selectedMenu
value: 1 value: 1
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 3894408489718918374, guid: e7173b1ee3369204eb181b376ede2a3e,
type: 3}
propertyPath: shareHandInteractionSettings
value: 1
objectReference: {fileID: 0}
- target: {fileID: 3894408489718918374, guid: e7173b1ee3369204eb181b376ede2a3e,
type: 3}
propertyPath: editorPart.foldoutInteraction
value: 1
objectReference: {fileID: 0}
- target: {fileID: 8620810952101844687, guid: e7173b1ee3369204eb181b376ede2a3e, - target: {fileID: 8620810952101844687, guid: e7173b1ee3369204eb181b376ede2a3e,
type: 3} type: 3}
propertyPath: m_Name propertyPath: m_Name
@@ -5280,7 +5338,7 @@ PrefabInstance:
- target: {fileID: 6994737657188836379, guid: d614df01a29ad3e45bb831298dfbad2c, - target: {fileID: 6994737657188836379, guid: d614df01a29ad3e45bb831298dfbad2c,
type: 3} type: 3}
propertyPath: editorPart.handSelected propertyPath: editorPart.handSelected
value: 0 value: 1
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 6994737657188836379, guid: d614df01a29ad3e45bb831298dfbad2c, - target: {fileID: 6994737657188836379, guid: d614df01a29ad3e45bb831298dfbad2c,
type: 3} type: 3}
@@ -5292,6 +5350,11 @@ PrefabInstance:
propertyPath: startOnRightcController propertyPath: startOnRightcController
value: 0 value: 0
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 6994737657188836379, guid: d614df01a29ad3e45bb831298dfbad2c,
type: 3}
propertyPath: handSettings.rotationOffset.x
value: 45
objectReference: {fileID: 0}
- target: {fileID: 7214299079173282187, guid: d614df01a29ad3e45bb831298dfbad2c, - target: {fileID: 7214299079173282187, guid: d614df01a29ad3e45bb831298dfbad2c,
type: 3} type: 3}
propertyPath: controller propertyPath: controller
@@ -5305,42 +5368,42 @@ PrefabInstance:
- target: {fileID: 7481659171502770090, guid: d614df01a29ad3e45bb831298dfbad2c, - target: {fileID: 7481659171502770090, guid: d614df01a29ad3e45bb831298dfbad2c,
type: 3} type: 3}
propertyPath: m_LocalPosition.x propertyPath: m_LocalPosition.x
value: 0.3567679 value: 0
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 7481659171502770090, guid: d614df01a29ad3e45bb831298dfbad2c, - target: {fileID: 7481659171502770090, guid: d614df01a29ad3e45bb831298dfbad2c,
type: 3} type: 3}
propertyPath: m_LocalPosition.y propertyPath: m_LocalPosition.y
value: -0.4698689 value: 0
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 7481659171502770090, guid: d614df01a29ad3e45bb831298dfbad2c, - target: {fileID: 7481659171502770090, guid: d614df01a29ad3e45bb831298dfbad2c,
type: 3} type: 3}
propertyPath: m_LocalPosition.z propertyPath: m_LocalPosition.z
value: -0.22523999 value: 0
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 7481659171502770090, guid: d614df01a29ad3e45bb831298dfbad2c, - target: {fileID: 7481659171502770090, guid: d614df01a29ad3e45bb831298dfbad2c,
type: 3} type: 3}
propertyPath: m_LocalRotation.w propertyPath: m_LocalRotation.w
value: 0.99958926 value: 0.7071068
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 7481659171502770090, guid: d614df01a29ad3e45bb831298dfbad2c, - target: {fileID: 7481659171502770090, guid: d614df01a29ad3e45bb831298dfbad2c,
type: 3} type: 3}
propertyPath: m_LocalRotation.x propertyPath: m_LocalRotation.x
value: -0 value: 0.7071068
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 7481659171502770090, guid: d614df01a29ad3e45bb831298dfbad2c, - target: {fileID: 7481659171502770090, guid: d614df01a29ad3e45bb831298dfbad2c,
type: 3} type: 3}
propertyPath: m_LocalRotation.y propertyPath: m_LocalRotation.y
value: -0.028659718 value: 0
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 7481659171502770090, guid: d614df01a29ad3e45bb831298dfbad2c, - target: {fileID: 7481659171502770090, guid: d614df01a29ad3e45bb831298dfbad2c,
type: 3} type: 3}
propertyPath: m_LocalRotation.z propertyPath: m_LocalRotation.z
value: -0 value: 0
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 7481659171502770090, guid: d614df01a29ad3e45bb831298dfbad2c, - target: {fileID: 7481659171502770090, guid: d614df01a29ad3e45bb831298dfbad2c,
type: 3} type: 3}
propertyPath: m_LocalEulerAnglesHint.x propertyPath: m_LocalEulerAnglesHint.x
value: 0 value: 90
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 7481659171502770090, guid: d614df01a29ad3e45bb831298dfbad2c, - target: {fileID: 7481659171502770090, guid: d614df01a29ad3e45bb831298dfbad2c,
type: 3} type: 3}
+17 -17
View File
@@ -412,7 +412,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 12.051, y: -28.918} m_AnchoredPosition: {x: 12.051, y: -30.052994}
m_SizeDelta: {x: 130, y: 8} m_SizeDelta: {x: 130, y: 8}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &108468832 --- !u!114 &108468832
@@ -725,7 +725,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0.00000033474, y: -40.969} m_AnchoredPosition: {x: 0.00000033474, y: -44.1}
m_SizeDelta: {x: 168, y: 0.5} m_SizeDelta: {x: 168, y: 0.5}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &306375770 --- !u!114 &306375770
@@ -920,7 +920,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -61.975, y: -36.665} m_AnchoredPosition: {x: -61.975, y: -37.8}
m_SizeDelta: {x: 18, y: 7} m_SizeDelta: {x: 18, y: 7}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &354046698 --- !u!114 &354046698
@@ -1670,7 +1670,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -13.772, y: -5.6772} m_AnchoredPosition: {x: -13.772, y: -5.2}
m_SizeDelta: {x: 120, y: 9} m_SizeDelta: {x: 120, y: 9}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &528106932 --- !u!114 &528106932
@@ -2131,7 +2131,7 @@ Transform:
m_GameObject: {fileID: 633731941} m_GameObject: {fileID: 633731941}
serializedVersion: 2 serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalPosition: {x: -0.5, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1} m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0 m_ConstrainProportionsScale: 0
m_Children: m_Children:
@@ -2704,7 +2704,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -61.975, y: -28.918} m_AnchoredPosition: {x: -61.975, y: -30.052994}
m_SizeDelta: {x: 18, y: 7} m_SizeDelta: {x: 18, y: 7}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &697160355 --- !u!114 &697160355
@@ -3187,7 +3187,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 24.101, y: 8.095} m_AnchoredPosition: {x: -33.1, y: 8.095}
m_SizeDelta: {x: 88, y: 9} m_SizeDelta: {x: 88, y: 9}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &885348079 --- !u!114 &885348079
@@ -3324,7 +3324,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: -69} m_AnchoredPosition: {x: 0, y: -66}
m_SizeDelta: {x: 170, y: 10} m_SizeDelta: {x: 170, y: 10}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!1 &927688130 --- !u!1 &927688130
@@ -3361,7 +3361,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -61.975, y: -21.171} m_AnchoredPosition: {x: -61.975, y: -22.305994}
m_SizeDelta: {x: 18, y: 7} m_SizeDelta: {x: 18, y: 7}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &927688132 --- !u!114 &927688132
@@ -3637,7 +3637,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 12.051, y: -21.171} m_AnchoredPosition: {x: 12.051, y: -22.305994}
m_SizeDelta: {x: 130, y: 8} m_SizeDelta: {x: 130, y: 8}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &961817605 --- !u!114 &961817605
@@ -5126,7 +5126,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -20, y: -50.2} m_AnchoredPosition: {x: -20, y: -51.5}
m_SizeDelta: {x: 98, y: 10} m_SizeDelta: {x: 98, y: 10}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1328124617 --- !u!114 &1328124617
@@ -5382,7 +5382,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0.00000033474, y: -9.9814} m_AnchoredPosition: {x: 0.00000033474, y: -11.4}
m_SizeDelta: {x: 168, y: 0.5} m_SizeDelta: {x: 168, y: 0.5}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1401498817 --- !u!114 &1401498817
@@ -5953,7 +5953,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 65, y: -50.200005} m_AnchoredPosition: {x: 65, y: -51.500004}
m_SizeDelta: {x: 36, y: 10} m_SizeDelta: {x: 36, y: 10}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1614055316 --- !u!114 &1614055316
@@ -7070,7 +7070,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -41.317, y: 8.095} m_AnchoredPosition: {x: 51.9, y: 8.095}
m_SizeDelta: {x: 44, y: 9} m_SizeDelta: {x: 44, y: 9}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1795570113 --- !u!114 &1795570113
@@ -7191,7 +7191,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 12.051, y: -36.665} m_AnchoredPosition: {x: 12.051, y: -37.799995}
m_SizeDelta: {x: 130, y: 8} m_SizeDelta: {x: 130, y: 8}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1799455459 --- !u!114 &1799455459
@@ -7866,7 +7866,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -57.671, y: -14.285} m_AnchoredPosition: {x: -57.671, y: -15.419995}
m_SizeDelta: {x: 30, y: 6} m_SizeDelta: {x: 30, y: 6}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1924042092 --- !u!114 &1924042092
@@ -8107,7 +8107,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 2379e0d70040c994089638264e6e9934, type: 3} m_Script: {fileID: 11500000, guid: 2379e0d70040c994089638264e6e9934, type: 3}
m_Name: m_Name:
m_EditorClassIdentifier: Assembly-CSharp::NasPublisher m_EditorClassIdentifier: Assembly-CSharp::NasPublisher
nasBaseUrl: 'http://whdwo798.synology.me:5000 ' nasBaseUrl: http://whdwo798.synology.me:5000
nasAccount: beatSaber_app nasAccount: beatSaber_app
nasRootPath: /web/beatsaber nasRootPath: /web/beatsaber
staticBaseUrl: http://whdwo798.synology.me/beatsaber staticBaseUrl: http://whdwo798.synology.me/beatsaber
+3
View File
@@ -36,6 +36,8 @@ public class BeatSageNote
public static class BeatSageConverter public static class BeatSageConverter
{ {
private static readonly bool LogConversions = false;
public static List<NoteData> Convert(string rawJson, float bpm) public static List<NoteData> Convert(string rawJson, float bpm)
{ {
var result = new List<NoteData>(); var result = new List<NoteData>();
@@ -62,6 +64,7 @@ public static class BeatSageConverter
}); });
} }
if (LogConversions)
Debug.Log($"[BeatSageConverter] Converted {result.Count} notes."); Debug.Log($"[BeatSageConverter] Converted {result.Count} notes.");
return result; return result;
} }
+2 -2
View File
@@ -17,7 +17,7 @@ public class DesktopUIMode : MonoBehaviour
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
private static void AutoCreate() private static void AutoCreate()
{ {
if (FindObjectOfType<DesktopUIMode>() != null) return; if (FindFirstObjectByType<DesktopUIMode>() != null) return;
new GameObject("[DesktopUIMode]").AddComponent<DesktopUIMode>(); new GameObject("[DesktopUIMode]").AddComponent<DesktopUIMode>();
} }
@@ -92,7 +92,7 @@ public class DesktopUIMode : MonoBehaviour
if (cam == null) if (cam == null)
foreach (var c in FindObjectsByType<Camera>(FindObjectsSortMode.None)) foreach (var c in FindObjectsByType<Camera>(FindObjectsSortMode.None))
if (c.enabled && c.gameObject.scene.name != "DontDestroyOnLoad") { cam = c; break; } if (c.enabled && c.gameObject.scene.name != "DontDestroyOnLoad") { cam = c; break; }
cam ??= FindObjectOfType<Camera>(); cam ??= FindFirstObjectByType<Camera>();
if (cam == null) return; if (cam == null) return;
foreach (var canvas in FindObjectsByType<Canvas>(FindObjectsSortMode.None)) foreach (var canvas in FindObjectsByType<Canvas>(FindObjectsSortMode.None))
+53 -2
View File
@@ -8,7 +8,8 @@ public class DownloadManager : MonoBehaviour
{ {
[SerializeField] private string baseUrl = "http://whdwo798.synology.me/beatsaber"; [SerializeField] private string baseUrl = "http://whdwo798.synology.me/beatsaber";
private static string CacheRoot => Path.Combine(Application.temporaryCachePath, "beatsaber"); private static string CacheRoot => Path.Combine(Application.persistentDataPath, "beatsaber");
private static string LegacyCacheRoot => Path.Combine(Application.temporaryCachePath, "beatsaber");
// ── Public API ─────────────────────────────────────────── // ── Public API ───────────────────────────────────────────
@@ -38,20 +39,35 @@ public class DownloadManager : MonoBehaviour
Directory.Delete(dir, recursive: true); Directory.Delete(dir, recursive: true);
Debug.Log($"[DownloadManager] 삭제: {songId}"); Debug.Log($"[DownloadManager] 삭제: {songId}");
} }
string legacyDir = LegacySongDir(songId);
if (Directory.Exists(legacyDir))
Directory.Delete(legacyDir, recursive: true);
} }
public void DeleteDifficulty(SongInfo song, string difficulty) public void DeleteDifficulty(SongInfo song, string difficulty)
{ {
TryMigrateLegacySong(song.id);
string path = MapPath(song, difficulty); string path = MapPath(song, difficulty);
if (path != null && File.Exists(path)) if (path != null && File.Exists(path))
File.Delete(path); File.Delete(path);
string songDir = SongDir(song.id);
if (Directory.Exists(songDir) && Directory.GetFileSystemEntries(songDir).Length == 0)
Directory.Delete(songDir);
} }
public bool IsSongDownloaded(string songId) public bool IsSongDownloaded(string songId)
=> File.Exists(AudioPath(songId)); {
TryMigrateLegacySong(songId);
return File.Exists(AudioPath(songId));
}
public bool IsDifficultyDownloaded(SongInfo song, string difficulty) public bool IsDifficultyDownloaded(SongInfo song, string difficulty)
{ {
TryMigrateLegacySong(song.id);
string path = MapPath(song, difficulty); string path = MapPath(song, difficulty);
return path != null && File.Exists(path); return path != null && File.Exists(path);
} }
@@ -73,6 +89,8 @@ public class DownloadManager : MonoBehaviour
private IEnumerator DownloadSongCoroutine(SongInfo song, string difficulty, private IEnumerator DownloadSongCoroutine(SongInfo song, string difficulty,
Action<float> onProgress, Action onComplete, Action<string> onError) Action<float> onProgress, Action onComplete, Action<string> onError)
{ {
TryMigrateLegacySong(song.id);
string songDir = Path.GetFullPath(SongDir(song.id)); string songDir = Path.GetFullPath(SongDir(song.id));
Directory.CreateDirectory(songDir); Directory.CreateDirectory(songDir);
@@ -157,4 +175,37 @@ public class DownloadManager : MonoBehaviour
private static string SongDir(string songId) private static string SongDir(string songId)
=> Path.Combine(CacheRoot, songId); => Path.Combine(CacheRoot, songId);
private static string LegacySongDir(string songId)
=> Path.Combine(LegacyCacheRoot, songId);
private static void TryMigrateLegacySong(string songId)
{
string sourceDir = LegacySongDir(songId);
string targetDir = SongDir(songId);
if (Directory.Exists(targetDir) || !Directory.Exists(sourceDir))
return;
CopyDirectory(sourceDir, targetDir);
Directory.Delete(sourceDir, recursive: true);
Debug.Log($"[DownloadManager] 기존 캐시를 영구 저장소로 이동: {songId}");
}
private static void CopyDirectory(string sourceDir, string targetDir)
{
Directory.CreateDirectory(targetDir);
foreach (string file in Directory.GetFiles(sourceDir))
{
string targetFile = Path.Combine(targetDir, Path.GetFileName(file));
File.Copy(file, targetFile, overwrite: true);
}
foreach (string dir in Directory.GetDirectories(sourceDir))
{
string targetSubDir = Path.Combine(targetDir, Path.GetFileName(dir));
CopyDirectory(dir, targetSubDir);
}
}
} }
+130
View File
@@ -0,0 +1,130 @@
using UnityEngine;
using UnityEngine.Video;
public class Game360VideoBackground : MonoBehaviour
{
[SerializeField] private VideoClip videoClip;
[SerializeField] private int renderTextureSize = 2048;
[SerializeField] private bool muteVideoAudio = true;
[SerializeField, Range(0f, 360f)] private float skyboxRotationDegrees = 0f;
[SerializeField, Range(0f, 8f)] private float skyboxExposure = 1f;
private GameObject videoPlayerObject;
private Material skyboxMaterial;
private Material previousSkybox;
private RenderTexture renderTexture;
private VideoPlayer videoPlayer;
private void Awake()
{
if (videoClip == null)
{
Debug.LogWarning("[Game360VideoBackground] videoClip is not assigned.");
return;
}
CreateSkyboxMaterial();
CreateVideoPlayer();
}
private void OnDestroy()
{
if (videoPlayer != null)
{
videoPlayer.prepareCompleted -= OnVideoPrepared;
videoPlayer.errorReceived -= OnVideoError;
}
if (renderTexture != null)
{
renderTexture.Release();
Destroy(renderTexture);
}
RenderSettings.skybox = previousSkybox;
DynamicGI.UpdateEnvironment();
if (skyboxMaterial != null)
Destroy(skyboxMaterial);
if (videoPlayerObject != null)
Destroy(videoPlayerObject);
}
private void CreateSkyboxMaterial()
{
renderTexture = new RenderTexture(renderTextureSize, renderTextureSize / 2, 0, RenderTextureFormat.ARGB32)
{
name = "Game360VideoRenderTexture",
wrapMode = TextureWrapMode.Clamp,
filterMode = FilterMode.Bilinear,
};
renderTexture.Create();
previousSkybox = RenderSettings.skybox;
skyboxMaterial = new Material(ResolveSkyboxShader())
{
name = "Game360VideoMaterial",
};
skyboxMaterial.SetTexture("_MainTex", renderTexture);
skyboxMaterial.SetFloat("_ImageType", 0f);
skyboxMaterial.SetFloat("_Mapping", 0f);
skyboxMaterial.SetFloat("_Layout", 0f);
ApplySkyboxSettings();
RenderSettings.skybox = skyboxMaterial;
DynamicGI.UpdateEnvironment();
}
private void CreateVideoPlayer()
{
videoPlayerObject = new GameObject("[360 Video Skybox Player]");
videoPlayerObject.transform.SetParent(transform, false);
videoPlayer = videoPlayerObject.AddComponent<VideoPlayer>();
videoPlayer.playOnAwake = false;
videoPlayer.isLooping = true;
videoPlayer.waitForFirstFrame = true;
videoPlayer.renderMode = VideoRenderMode.RenderTexture;
videoPlayer.targetTexture = renderTexture;
videoPlayer.clip = videoClip;
videoPlayer.audioOutputMode = muteVideoAudio
? VideoAudioOutputMode.None
: VideoAudioOutputMode.Direct;
videoPlayer.prepareCompleted += OnVideoPrepared;
videoPlayer.errorReceived += OnVideoError;
videoPlayer.Prepare();
}
private static Shader ResolveSkyboxShader()
{
return Shader.Find("Skybox/Panoramic")
?? Shader.Find("Skybox/6 Sided")
?? Shader.Find("Standard");
}
private void OnVideoPrepared(VideoPlayer source)
{
source.Play();
}
private void OnValidate()
{
ApplySkyboxSettings();
}
private void ApplySkyboxSettings()
{
if (skyboxMaterial == null)
return;
skyboxMaterial.SetFloat("_Exposure", skyboxExposure);
skyboxMaterial.SetFloat("_Rotation", skyboxRotationDegrees);
DynamicGI.UpdateEnvironment();
}
private static void OnVideoError(VideoPlayer source, string message)
{
Debug.LogWarning($"[Game360VideoBackground] VideoPlayer error: {message}");
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3e381cd99de84f67b9f83c19a032dc24
+24
View File
@@ -0,0 +1,24 @@
using UnityEngine;
public static class GlobalSyncSettings
{
private const string AudioOffsetMsKey = "VRBeats.GlobalAudioOffsetMs";
public static float AudioOffsetMs
{
get => PlayerPrefs.GetFloat(AudioOffsetMsKey, 0.0f);
set
{
PlayerPrefs.SetFloat(AudioOffsetMsKey, Mathf.Clamp(value, -300.0f, 300.0f));
PlayerPrefs.Save();
}
}
public static float AudioOffsetSeconds => AudioOffsetMs / 1000.0f;
public static void Reset()
{
PlayerPrefs.DeleteKey(AudioOffsetMsKey);
PlayerPrefs.Save();
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a2e8c518ec2f4a03a6d820774b475ce0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+30 -5
View File
@@ -5,12 +5,13 @@ using UnityEngine;
[RequireComponent(typeof(TMP_Text))] [RequireComponent(typeof(TMP_Text))]
public class MarqueeText : MonoBehaviour public class MarqueeText : MonoBehaviour
{ {
public float speed = 35f; public float speed = 14f;
public float pauseStart = 1.5f; public float pauseStart = 1.8f;
public float pauseEnd = 0.6f; public float pauseEnd = 0.9f;
private TMP_Text _label; private TMP_Text _label;
private RectTransform _rect; private RectTransform _rect;
private Coroutine _scrollRoutine;
private void Awake() private void Awake()
{ {
@@ -20,7 +21,22 @@ public class MarqueeText : MonoBehaviour
private IEnumerator Start() private IEnumerator Start()
{ {
yield return null; // layout 완료 후 실행 yield return null;
Refresh();
}
private void OnDisable()
{
StopScrolling();
}
public void Refresh()
{
if (!isActiveAndEnabled || _label == null || _rect == null || transform.parent == null)
return;
StopScrolling();
SetX(0f);
_label.ForceMeshUpdate(); _label.ForceMeshUpdate();
float textW = _label.preferredWidth; float textW = _label.preferredWidth;
@@ -28,7 +44,7 @@ public class MarqueeText : MonoBehaviour
float dist = textW - containerW; float dist = textW - containerW;
if (dist > 1f) if (dist > 1f)
StartCoroutine(ScrollLoop(dist)); _scrollRoutine = StartCoroutine(ScrollLoop(dist));
} }
private IEnumerator ScrollLoop(float dist) private IEnumerator ScrollLoop(float dist)
@@ -52,4 +68,13 @@ public class MarqueeText : MonoBehaviour
private void SetX(float x) => private void SetX(float x) =>
_rect.anchoredPosition = new Vector2(x, _rect.anchoredPosition.y); _rect.anchoredPosition = new Vector2(x, _rect.anchoredPosition.y);
private void StopScrolling()
{
if (_scrollRoutine == null)
return;
StopCoroutine(_scrollRoutine);
_scrollRoutine = null;
}
} }
+93
View File
@@ -0,0 +1,93 @@
using System.Collections;
using TMPro;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
public class MenuSyncButtonInjector : MonoBehaviour
{
private const string MenuSceneName = "Menu";
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
private static void AutoCreate()
{
if (FindFirstObjectByType<MenuSyncButtonInjector>() != null)
return;
GameObject go = new GameObject("[MenuSyncButtonInjector]");
DontDestroyOnLoad(go);
go.AddComponent<MenuSyncButtonInjector>();
}
private void Awake()
{
SceneManager.sceneLoaded += OnSceneLoaded;
StartCoroutine(InjectAfterFrame());
}
private void OnDestroy()
{
SceneManager.sceneLoaded -= OnSceneLoaded;
}
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
StartCoroutine(InjectAfterFrame());
}
private IEnumerator InjectAfterFrame()
{
yield return null;
if (SceneManager.GetActiveScene().name != MenuSceneName)
yield break;
if (GameObject.Find("SyncButton") != null)
yield break;
Button sourceButton = FindButtonByText("CreateSong");
if (sourceButton == null)
sourceButton = FindButtonByText("음악만들기");
if (sourceButton == null)
yield break;
Button syncButton = Instantiate(sourceButton, sourceButton.transform.parent);
syncButton.gameObject.name = "SyncButton";
foreach (VRBeats.LoadSceneButton loader in syncButton.GetComponents<VRBeats.LoadSceneButton>())
{
loader.enabled = false;
Destroy(loader);
}
syncButton.onClick.RemoveAllListeners();
syncButton.onClick.AddListener(SyncCalibrationOverlay.Open);
RectTransform sourceRect = sourceButton.GetComponent<RectTransform>();
RectTransform syncRect = syncButton.GetComponent<RectTransform>();
syncRect.anchoredPosition = sourceRect.anchoredPosition + new Vector2(0.0f, -22.0f);
TextMeshProUGUI tmp = syncButton.GetComponentInChildren<TextMeshProUGUI>(true);
if (tmp != null)
tmp.text = "SYNC";
Text text = syncButton.GetComponentInChildren<Text>(true);
if (text != null)
text.text = "SYNC";
}
private static Button FindButtonByText(string text)
{
foreach (Button button in FindObjectsByType<Button>(FindObjectsSortMode.None))
{
TextMeshProUGUI tmp = button.GetComponentInChildren<TextMeshProUGUI>(true);
if (tmp != null && tmp.text.Trim() == text)
return button;
Text legacyText = button.GetComponentInChildren<Text>(true);
if (legacyText != null && legacyText.text.Trim() == text)
return button;
}
return null;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: efc5a20a7b4749bfb60d95ac0f0b2180
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+92 -19
View File
@@ -3,6 +3,7 @@ using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text; using System.Text;
using System.Text.RegularExpressions;
using UnityEngine; using UnityEngine;
using UnityEngine.Networking; using UnityEngine.Networking;
@@ -25,23 +26,29 @@ public class NasPublisher : MonoBehaviour
private void LoadConfig() private void LoadConfig()
{ {
string path = Path.Combine(Application.streamingAssetsPath, "nas_config.json"); string path = Path.Combine(Application.streamingAssetsPath, "nas_config.json");
if (!File.Exists(path)) { Debug.LogWarning("[NasPublisher] nas_config.json not found: " + path); return; } if (!File.Exists(path))
{
NormalizeSettings();
return;
}
var cfg = JsonUtility.FromJson<NasConfig>(File.ReadAllText(path)); var cfg = JsonUtility.FromJson<NasConfig>(File.ReadAllText(path));
if (cfg == null) return; if (cfg == null) return;
_password = cfg.password ?? ""; _password = cfg.password ?? "";
if (!string.IsNullOrEmpty(cfg.host)) nasBaseUrl = cfg.host; if (!string.IsNullOrWhiteSpace(cfg.host)) nasBaseUrl = cfg.host.Trim();
if (!string.IsNullOrEmpty(cfg.account)) nasAccount = cfg.account; if (!string.IsNullOrWhiteSpace(cfg.account)) nasAccount = cfg.account.Trim();
if (!string.IsNullOrEmpty(cfg.rootPath)) nasRootPath = cfg.rootPath; if (!string.IsNullOrWhiteSpace(cfg.rootPath)) nasRootPath = cfg.rootPath.Trim();
if (!string.IsNullOrEmpty(cfg.staticUrl)) staticBaseUrl = cfg.staticUrl; if (!string.IsNullOrWhiteSpace(cfg.staticUrl)) staticBaseUrl = cfg.staticUrl.Trim();
NormalizeSettings();
} }
[Serializable] private class NasConfig [Serializable] private class NasConfig
{ {
public string host; public string host = "";
public string account; public string account = "";
public string rootPath; public string rootPath = "";
public string staticUrl; public string staticUrl = "";
public string password; public string password = "";
} }
public IEnumerator Publish( public IEnumerator Publish(
@@ -52,6 +59,8 @@ public class NasPublisher : MonoBehaviour
Action onComplete, Action onComplete,
Action<string> onError) Action<string> onError)
{ {
NormalizeSettings();
bool failed = false; bool failed = false;
void OnErr(string e) { onError?.Invoke(e); failed = true; } void OnErr(string e) { onError?.Invoke(e); failed = true; }
@@ -92,13 +101,32 @@ public class NasPublisher : MonoBehaviour
private IEnumerator Login(Action<string> onError) private IEnumerator Login(Action<string> onError)
{ {
if (string.IsNullOrWhiteSpace(_password))
{
onError?.Invoke("NAS password missing. Create Assets/StreamingAssets/nas_config.json with host, account, rootPath, staticUrl, and password.");
yield break;
}
string url = $"{nasBaseUrl}/webapi/auth.cgi" + string url = $"{nasBaseUrl}/webapi/auth.cgi" +
$"?api=SYNO.API.Auth&version=6&method=login" + $"?api=SYNO.API.Auth&version=6&method=login" +
$"&account={UnityWebRequest.EscapeURL(nasAccount)}" + $"&account={UnityWebRequest.EscapeURL(nasAccount)}" +
$"&passwd={UnityWebRequest.EscapeURL(_password)}" + $"&passwd={UnityWebRequest.EscapeURL(_password)}" +
$"&session=FileStation&format=sid&enable_syno_token=yes"; $"&session=FileStation&format=sid&enable_syno_token=yes";
using var req = UnityWebRequest.Get(url); UnityWebRequest req;
try
{
req = UnityWebRequest.Get(url);
}
catch (UriFormatException e)
{
onError?.Invoke($"DSM login URL invalid: '{nasBaseUrl}' — {e.Message}");
yield break;
}
string resp;
using (req)
{
yield return req.SendWebRequest(); yield return req.SendWebRequest();
if (req.result != UnityWebRequest.Result.Success) if (req.result != UnityWebRequest.Result.Success)
@@ -107,16 +135,19 @@ public class NasPublisher : MonoBehaviour
yield break; yield break;
} }
string resp = req.downloadHandler.text; resp = req.downloadHandler.text;
_sid = ParseJsonString(resp, "sid"); _sid = ParseJsonString(resp, "sid");
_synoToken = ParseJsonString(resp, "synotoken"); _synoToken = ParseJsonString(resp, "synotoken");
}
if (string.IsNullOrEmpty(_sid)) if (string.IsNullOrEmpty(_sid))
onError?.Invoke("DSM sid parse failed — check credentials."); onError?.Invoke($"DSM sid parse failed. Check NAS account/password/permissions. Response: {Shorten(resp)}");
} }
private IEnumerator Logout() private IEnumerator Logout()
{ {
NormalizeSettings();
string url = $"{nasBaseUrl}/webapi/auth.cgi" + string url = $"{nasBaseUrl}/webapi/auth.cgi" +
$"?api=SYNO.API.Auth&version=1&method=logout&session=FileStation&_sid={_sid}"; $"?api=SYNO.API.Auth&version=1&method=logout&session=FileStation&_sid={_sid}";
using var req = UnityWebRequest.Get(url); using var req = UnityWebRequest.Get(url);
@@ -133,6 +164,8 @@ public class NasPublisher : MonoBehaviour
private IEnumerator UploadBytes(byte[] bytes, string fileName, private IEnumerator UploadBytes(byte[] bytes, string fileName,
string nasFolder, Action<string> onError) string nasFolder, Action<string> onError)
{ {
NormalizeSettings();
string uploadUrl = $"{nasBaseUrl}/webapi/entry.cgi" + string uploadUrl = $"{nasBaseUrl}/webapi/entry.cgi" +
$"?api=SYNO.FileStation.Upload&version=2&method=upload" + $"?api=SYNO.FileStation.Upload&version=2&method=upload" +
$"&_sid={UnityWebRequest.EscapeURL(_sid)}"; $"&_sid={UnityWebRequest.EscapeURL(_sid)}";
@@ -179,6 +212,8 @@ public class NasPublisher : MonoBehaviour
private IEnumerator PatchSongsJson(SongInfo newSong, Action<string> onError) private IEnumerator PatchSongsJson(SongInfo newSong, Action<string> onError)
{ {
NormalizeSettings();
SongsList list = null; SongsList list = null;
using (var req = UnityWebRequest.Get($"{staticBaseUrl}/songs.json")) using (var req = UnityWebRequest.Get($"{staticBaseUrl}/songs.json"))
@@ -201,12 +236,16 @@ public class NasPublisher : MonoBehaviour
private static string ParseJsonString(string json, string key) private static string ParseJsonString(string json, string key)
{ {
string search = $"\"{key}\":\""; if (string.IsNullOrEmpty(json) || string.IsNullOrEmpty(key))
int start = json.IndexOf(search, StringComparison.Ordinal); return null;
if (start < 0) return null;
start += search.Length; Match match = Regex.Match(
int end = json.IndexOf('"', start); json,
return end > start ? json.Substring(start, end - start) : null; $"\"{Regex.Escape(key)}\"\\s*:\\s*\"(?<value>(?:\\\\.|[^\"])*)\"");
return match.Success
? Regex.Unescape(match.Groups["value"].Value)
: null;
} }
private static void AssignMapFile(SongInfo song, string diff, string fileName) private static void AssignMapFile(SongInfo song, string diff, string fileName)
@@ -214,4 +253,38 @@ public class NasPublisher : MonoBehaviour
var info = song.difficulties.Get(diff); var info = song.difficulties.Get(diff);
if (info != null) info.mapFile = $"maps/{fileName}"; if (info != null) info.mapFile = $"maps/{fileName}";
} }
private void OnValidate()
{
NormalizeSettings();
}
private void NormalizeSettings()
{
nasBaseUrl = NormalizeBaseUrl(nasBaseUrl);
staticBaseUrl = NormalizeBaseUrl(staticBaseUrl);
nasAccount = nasAccount?.Trim() ?? "";
nasRootPath = NormalizeRootPath(nasRootPath);
}
private static string NormalizeBaseUrl(string value)
{
return (value ?? "").Trim().TrimEnd('/');
}
private static string NormalizeRootPath(string value)
{
value = (value ?? "").Trim().Replace('\\', '/');
if (string.IsNullOrEmpty(value))
return "/";
return value.StartsWith("/") ? value.TrimEnd('/') : "/" + value.TrimEnd('/');
}
private static string Shorten(string value, int maxLength = 240)
{
if (string.IsNullOrEmpty(value) || value.Length <= maxLength)
return value ?? "";
return value.Substring(0, maxLength) + "...";
}
} }
+13
View File
@@ -16,6 +16,19 @@ public class NoteData
public class MapData public class MapData
{ {
public List<NoteData> target; public List<NoteData> target;
public ForcedResultData forcedResult;
}
[Serializable]
public class ForcedResultData
{
public bool enabled;
public int totalNotes;
public int perfect;
public int great;
public int good;
public int miss;
public int maxCombo;
} }
[Serializable] [Serializable]
+91 -12
View File
@@ -13,17 +13,32 @@ public class SongController : MonoBehaviour
[SerializeField] private GameEvent onLevelComplete; [SerializeField] private GameEvent onLevelComplete;
[SerializeField] private TMP_Text countdownText; [SerializeField] private TMP_Text countdownText;
private const float LaneSpacing = 0.42f;
private const float LayerSpacing = 0.34f;
private const float HorizontalCenter = 1.5f;
private const float VerticalCenter = 1f;
private const float VerticalOffset = 0.22f;
private AudioManager _audio; private AudioManager _audio;
private ScoreManager _scoreManager;
private float _clipLength;
private static string CacheRoot => private static string CacheRoot =>
Path.Combine(Application.temporaryCachePath, "beatsaber"); Path.Combine(Application.persistentDataPath, "beatsaber");
private void Start() private void Start()
{ {
_audio = FindObjectOfType<AudioManager>(); _audio = FindFirstObjectByType<AudioManager>();
_scoreManager = FindFirstObjectByType<ScoreManager>();
StartCoroutine(LoadAndPlay()); StartCoroutine(LoadAndPlay());
} }
private void Update()
{
if (_audio != null && _scoreManager != null && _clipLength > 0.0f)
_scoreManager.SetSongProgress(_audio.CurrentTime, _clipLength);
}
private IEnumerator LoadAndPlay() private IEnumerator LoadAndPlay()
{ {
SongInfo song = GameSession.SelectedSong; SongInfo song = GameSession.SelectedSong;
@@ -48,6 +63,7 @@ public class SongController : MonoBehaviour
} }
clip = DownloadHandlerAudioClip.GetContent(req); clip = DownloadHandlerAudioClip.GetContent(req);
} }
_clipLength = clip.length;
// Load and parse map // Load and parse map
DifficultyInfo diffInfo = song.difficulties.Get(diff); DifficultyInfo diffInfo = song.difficulties.Get(diff);
@@ -63,19 +79,49 @@ public class SongController : MonoBehaviour
yield break; yield break;
} }
MapData map = JsonUtility.FromJson<MapData>(File.ReadAllText(mapPath)); MapData map = JsonUtility.FromJson<MapData>(File.ReadAllText(mapPath));
if (map?.target == null) if (map == null)
{ {
Debug.LogError("[SongController] Map parse failed"); Debug.LogError("[SongController] Map parse failed");
yield break; yield break;
} }
map.target.Sort((a, b) => a.time.CompareTo(b.time)); if (map.target == null)
map.target = new List<NoteData>();
if (IsForcedResultMap(map))
{
_scoreManager?.SetTotalNotes(Mathf.Max(0, map.forcedResult.totalNotes));
yield return StartCoroutine(Countdown());
_audio.PlayClip(clip);
yield return new WaitForSeconds(Mathf.Min(Mathf.Max(0.2f, _clipLength), 0.75f));
_scoreManager?.ApplyForcedResult(
map.forcedResult.totalNotes,
map.forcedResult.perfect,
map.forcedResult.great,
map.forcedResult.good,
map.forcedResult.miss,
map.forcedResult.maxCombo);
_scoreManager?.CompleteSong();
onLevelComplete?.Invoke();
yield break;
}
map.target.Sort(CompareNotes);
if (_clipLength <= 0.0f)
{
float lastNoteTime = map.target.Count > 0 ? map.target[map.target.Count - 1].time : 0.0f;
_clipLength = Mathf.Max(song.duration, lastNoteTime + 1.0f);
}
_scoreManager?.SetTotalNotes(map.target.Count);
yield return StartCoroutine(Countdown()); yield return StartCoroutine(Countdown());
_audio.PlayClip(clip); _audio.PlayClip(clip);
StartCoroutine(SpawnRoutine(map.target)); StartCoroutine(SpawnRoutine(map.target));
yield return StartCoroutine(WaitForCompletion(clip.length)); yield return StartCoroutine(WaitForCompletion(_clipLength, map.target));
} }
private IEnumerator Countdown() private IEnumerator Countdown()
@@ -101,7 +147,8 @@ public class SongController : MonoBehaviour
foreach (NoteData note in notes) foreach (NoteData note in notes)
{ {
float spawnAt = Mathf.Max(0f, note.time - travelTime); float adjustedNoteTime = note.time + GlobalSyncSettings.AudioOffsetSeconds;
float spawnAt = Mathf.Max(0f, adjustedNoteTime - travelTime);
yield return new WaitUntil(() => _audio.CurrentTime >= spawnAt); yield return new WaitUntil(() => _audio.CurrentTime >= spawnAt);
SpawnNote(note); SpawnNote(note);
} }
@@ -109,11 +156,11 @@ public class SongController : MonoBehaviour
private void SpawnNote(NoteData note) private void SpawnNote(NoteData note)
{ {
float x = -0.375f + note.position * 0.25f; float x = MapLaneX(note.position);
float y = -0.333f + note.lineLayer * 0.333f; float y = MapLayerY(note.lineLayer);
// 스폰 시점의 실제 남은 시간으로 역산 → 동시 노트가 프레임 차이 나도 같은 타이밍에 도착 // 스폰 시점의 실제 남은 시간으로 역산 → 동시 노트가 프레임 차이 나도 같은 타이밍에 도착
float remaining = note.time - _audio.CurrentTime; float remaining = note.time + GlobalSyncSettings.AudioOffsetSeconds - _audio.CurrentTime;
float travelTime = Mathf.Max(0.05f, remaining); float travelTime = Mathf.Max(0.05f, remaining);
var info = new SpawnEventInfo var info = new SpawnEventInfo
@@ -121,7 +168,7 @@ public class SongController : MonoBehaviour
position = new Vector3(x, y, 0f), position = new Vector3(x, y, 0f),
colorSide = note.colorType == 0 ? ColorSide.Left : ColorSide.Right, colorSide = note.colorType == 0 ? ColorSide.Left : ColorSide.Right,
hitDirection = MapCutDirection(note.cutDirection), hitDirection = MapCutDirection(note.cutDirection),
useSpark = true, useSpark = false,
speed = 2f, speed = 2f,
travelTimeOverride = travelTime, travelTimeOverride = travelTime,
}; };
@@ -129,6 +176,34 @@ public class SongController : MonoBehaviour
VR_BeatManager.instance.Spawn(cubePrefab, info); VR_BeatManager.instance.Spawn(cubePrefab, info);
} }
private static int CompareNotes(NoteData a, NoteData b)
{
int timeCompare = a.time.CompareTo(b.time);
if (timeCompare != 0)
return timeCompare;
int positionCompare = a.position.CompareTo(b.position);
if (positionCompare != 0)
return positionCompare;
return a.lineLayer.CompareTo(b.lineLayer);
}
private static bool IsForcedResultMap(MapData map)
=> map?.forcedResult != null && map.forcedResult.enabled;
private static float MapLaneX(int position)
{
int lane = Mathf.Clamp(position, 0, 3);
return (lane - HorizontalCenter) * LaneSpacing;
}
private static float MapLayerY(int lineLayer)
{
int layer = Mathf.Clamp(lineLayer, 0, 2);
return VerticalOffset + (layer - VerticalCenter) * LayerSpacing;
}
// Beat Saber cutDirection → VRBeats Direction // Beat Saber cutDirection → VRBeats Direction
// BS: 0=Up 1=Down 2=Left 3=Right 4=UpperLeft 5=UpperRight 6=LowerLeft 7=LowerRight 8=Any // BS: 0=Up 1=Down 2=Left 3=Right 4=UpperLeft 5=UpperRight 6=LowerLeft 7=LowerRight 8=Any
private static readonly Direction[] CutDirMap = private static readonly Direction[] CutDirMap =
@@ -147,9 +222,13 @@ public class SongController : MonoBehaviour
private static Direction MapCutDirection(int cut) private static Direction MapCutDirection(int cut)
=> (cut >= 0 && cut < CutDirMap.Length) ? CutDirMap[cut] : Direction.Center; => (cut >= 0 && cut < CutDirMap.Length) ? CutDirMap[cut] : Direction.Center;
private IEnumerator WaitForCompletion(float clipLength) private IEnumerator WaitForCompletion(float clipLength, List<NoteData> notes)
{ {
yield return new WaitUntil(() => _audio.CurrentTime >= clipLength - 0.5f); float lastNoteTime = notes.Count > 0 ? notes[notes.Count - 1].time : 0.0f;
float resultTime = Mathf.Min(clipLength, lastNoteTime + GlobalSyncSettings.AudioOffsetSeconds + 0.35f);
yield return new WaitUntil(() => _audio.CurrentTime >= resultTime);
yield return new WaitForSeconds(0.35f);
_scoreManager?.CompleteSong();
onLevelComplete?.Invoke(); onLevelComplete?.Invoke();
} }
} }
+63
View File
@@ -50,14 +50,27 @@ public class SongCreatorManager : MonoBehaviour
[SerializeField] private BeatSageUploader beatSageUploader; [SerializeField] private BeatSageUploader beatSageUploader;
[SerializeField] private NasPublisher nasPublisher; [SerializeField] private NasPublisher nasPublisher;
private static readonly Color NeonBg = new Color(0.05f, 0.82f, 0.95f, 0.42f);
private static readonly Color DarkButtonBg = new Color(0.09f, 0.22f, 0.27f, 0.66f);
private static readonly Color DisabledBg = new Color(0.06f, 0.12f, 0.15f, 0.48f);
private static readonly Color ButtonText = new Color(0.92f, 1.0f, 1.0f, 1.0f);
private static readonly Color MutedText = new Color(0.66f, 0.80f, 0.84f, 0.76f);
private static readonly Color NeonOutline = new Color(0.25f, 0.96f, 1.0f, 0.42f);
private static string InputPath => private static string InputPath =>
Path.Combine(Application.persistentDataPath, "input"); Path.Combine(Application.persistentDataPath, "input");
private readonly List<string> audioFiles = new(); private readonly List<string> audioFiles = new();
private string _pendingFilePath; private string _pendingFilePath;
private void OnValidate()
{
ApplyButtonStyles();
}
private void Start() private void Start()
{ {
ApplyButtonStyles();
Directory.CreateDirectory(InputPath); Directory.CreateDirectory(InputPath);
if (inputPathHint != null) if (inputPathHint != null)
@@ -258,6 +271,7 @@ public class SongCreatorManager : MonoBehaviour
if (refreshBtn != null) refreshBtn.interactable = value; if (refreshBtn != null) refreshBtn.interactable = value;
if (filePickerBtn != null) filePickerBtn.interactable = value; if (filePickerBtn != null) filePickerBtn.interactable = value;
if (urlDownloadBtn != null) urlDownloadBtn.interactable = value; if (urlDownloadBtn != null) urlDownloadBtn.interactable = value;
ApplyButtonStyles();
} }
private void OnFilePickerClicked() private void OnFilePickerClicked()
@@ -323,6 +337,7 @@ public class SongCreatorManager : MonoBehaviour
{ {
SetAddStatus("Downloading..."); SetAddStatus("Downloading...");
if (urlDownloadBtn != null) urlDownloadBtn.interactable = false; if (urlDownloadBtn != null) urlDownloadBtn.interactable = false;
ApplyButtonStyles();
string fileName; string fileName;
try try
@@ -341,6 +356,7 @@ public class SongCreatorManager : MonoBehaviour
yield return req.SendWebRequest(); yield return req.SendWebRequest();
if (urlDownloadBtn != null) urlDownloadBtn.interactable = true; if (urlDownloadBtn != null) urlDownloadBtn.interactable = true;
ApplyButtonStyles();
if (req.result == UnityWebRequest.Result.Success) if (req.result == UnityWebRequest.Result.Success)
{ {
@@ -358,4 +374,51 @@ public class SongCreatorManager : MonoBehaviour
} }
private void SetAddStatus(string msg) { if (addStatusText != null) addStatusText.text = msg; } private void SetAddStatus(string msg) { if (addStatusText != null) addStatusText.text = msg; }
private void ApplyButtonStyles()
{
ApplyCreatorButtonStyle(generateButton, true);
ApplyCreatorButtonStyle(urlDownloadBtn, true);
ApplyCreatorButtonStyle(refreshBtn, false);
ApplyCreatorButtonStyle(filePickerBtn, false);
ApplyCreatorButtonStyle(backButton, false);
}
private static void ApplyCreatorButtonStyle(Button btn, bool primary)
{
if (btn == null)
return;
Color bg = btn.interactable ? (primary ? NeonBg : DarkButtonBg) : DisabledBg;
if (btn.targetGraphic is Image img)
{
img.color = bg;
img.raycastTarget = true;
}
var colors = btn.colors;
colors.normalColor = bg;
colors.highlightedColor = btn.interactable
? new Color(0.10f, 0.95f, 1.0f, primary ? 0.58f : 0.48f)
: DisabledBg;
colors.pressedColor = btn.interactable
? new Color(0.02f, 0.58f, 0.72f, 0.80f)
: DisabledBg;
colors.selectedColor = colors.highlightedColor;
colors.disabledColor = DisabledBg;
colors.fadeDuration = 0.08f;
btn.colors = colors;
TMP_Text label = btn.GetComponentInChildren<TMP_Text>(true);
if (label != null)
{
label.color = btn.interactable ? ButtonText : MutedText;
label.raycastTarget = false;
}
Outline outline = btn.GetComponent<Outline>() ?? btn.gameObject.AddComponent<Outline>();
outline.enabled = btn.interactable;
outline.effectColor = NeonOutline;
outline.effectDistance = new Vector2(0.0f, -0.28f);
}
} }
+158 -11
View File
@@ -33,13 +33,20 @@ public class SongDetailPanel : MonoBehaviour
[Header("씬 이름")] [Header("씬 이름")]
[SerializeField] private string gameSceneName = "Game"; [SerializeField] private string gameSceneName = "Game";
private static readonly Color SelectedColor = new Color(0.2f, 0.78f, 0.4f); private static readonly Color NeonBg = new Color(0.05f, 0.82f, 0.95f, 0.42f);
private static readonly Color DeselectedImgColor = new Color(1f, 1f, 1f, 0.12f); // original button alpha private static readonly Color DarkButtonBg = new Color(0.09f, 0.22f, 0.27f, 0.66f);
private static readonly Color DisabledBg = new Color(0.06f, 0.12f, 0.15f, 0.48f);
private static readonly Color DangerBg = new Color(0.52f, 0.16f, 0.22f, 0.72f);
private static readonly Color ButtonText = new Color(0.92f, 1.0f, 1.0f, 1.0f);
private static readonly Color MutedText = new Color(0.66f, 0.80f, 0.84f, 0.76f);
private static readonly Color NeonOutline = new Color(0.25f, 0.96f, 1.0f, 0.42f);
private SongInfo currentSong; private SongInfo currentSong;
private string selectedDifficulty; private string selectedDifficulty;
private DownloadManager downloadManager; private DownloadManager downloadManager;
private SongSelectManager selectManager; private SongSelectManager selectManager;
private MarqueeText titleMarquee;
private MarqueeText artistMarquee;
private readonly (string key, Func<SongDetailPanel, Button> btn)[] diffSlots = private readonly (string key, Func<SongDetailPanel, Button> btn)[] diffSlots =
{ {
@@ -49,6 +56,22 @@ public class SongDetailPanel : MonoBehaviour
("expertplus", p => p.btnExpertPlus), ("expertplus", p => p.btnExpertPlus),
}; };
private void Awake()
{
HideDifficultyLabel();
titleMarquee = ConfigureMarqueeText(titleText, 5.0f, 7.2f);
artistMarquee = ConfigureMarqueeText(artistText, 3.4f, 4.4f);
ConfigureOneLineText(infoText, 3.2f, 4.2f, TextAlignmentOptions.MidlineLeft);
ConfigureButtonText(btnNormal, 3.2f, 4.0f);
ConfigureButtonText(btnHard, 3.2f, 4.0f);
ConfigureButtonText(btnExpert, 3.2f, 4.0f);
ConfigureButtonText(btnExpertPlus, 3.0f, 3.8f);
ConfigureButtonText(downloadButton, 3.5f, 4.4f);
ConfigureButtonText(deleteButton, 3.5f, 4.4f);
ConfigureButtonText(playButton, 3.5f, 4.4f);
ConfigureButtonText(closeButton, 5.2f, 6.4f);
}
// ── Public API ─────────────────────────────────────────── // ── Public API ───────────────────────────────────────────
public void Show(SongInfo song, DownloadManager dm, SongSelectManager sm) public void Show(SongInfo song, DownloadManager dm, SongSelectManager sm)
@@ -61,9 +84,11 @@ public class SongDetailPanel : MonoBehaviour
titleText.text = song.title; titleText.text = song.title;
artistText.text = song.artist; artistText.text = song.artist;
infoText.text = song.duration > 0 infoText.text = song.duration > 0
? $"BPM {Mathf.RoundToInt(song.bpm)} | {FormatDuration(song.duration)}" ? $"BPM {Mathf.RoundToInt(song.bpm)} {FormatDuration(song.duration)}"
: $"BPM {Mathf.RoundToInt(song.bpm)}"; : $"BPM {Mathf.RoundToInt(song.bpm)}";
titleMarquee?.Refresh();
artistMarquee?.Refresh();
RefreshUI(); RefreshUI();
} }
@@ -92,8 +117,11 @@ public class SongDetailPanel : MonoBehaviour
downloadButton.gameObject.SetActive(!downloaded); downloadButton.gameObject.SetActive(!downloaded);
deleteButton.gameObject.SetActive(downloaded); deleteButton.gameObject.SetActive(downloaded);
downloadButton.interactable = !downloaded;
deleteButton.interactable = downloaded;
playButton.interactable = downloaded && selectedDifficulty != null; playButton.interactable = downloaded && selectedDifficulty != null;
progressGroup.SetActive(false); progressGroup.SetActive(false);
UpdateActionButtonStyles(downloaded);
downloadButton.onClick.RemoveAllListeners(); downloadButton.onClick.RemoveAllListeners();
downloadButton.onClick.AddListener(OnDownloadClicked); downloadButton.onClick.AddListener(OnDownloadClicked);
@@ -116,6 +144,7 @@ public class SongDetailPanel : MonoBehaviour
selectedDifficulty = difficulty; selectedDifficulty = difficulty;
playButton.interactable = true; playButton.interactable = true;
UpdateDiffColors(); UpdateDiffColors();
UpdateActionButtonStyles(true);
} }
private void UpdateDiffColors() private void UpdateDiffColors()
@@ -125,14 +154,7 @@ public class SongDetailPanel : MonoBehaviour
Button btn = getBtn(this); Button btn = getBtn(this);
bool selected = key == selectedDifficulty; bool selected = key == selectedDifficulty;
if (btn.targetGraphic is Image img) ApplyButtonStyle(btn, selected ? NeonBg : DarkButtonBg, selected, btn.interactable, false);
img.color = selected ? SelectedColor : DeselectedImgColor;
var cb = btn.colors;
cb.normalColor = Color.white;
cb.highlightedColor = selected ? new Color(0.3f, 0.95f, 0.55f) : new Color(1f, 1f, 1f, 0.25f);
cb.pressedColor = selected ? new Color(0.15f, 0.6f, 0.3f) : new Color(1f, 1f, 1f, 0.35f);
btn.colors = cb;
} }
} }
@@ -234,4 +256,129 @@ public class SongDetailPanel : MonoBehaviour
private static string FormatDuration(int seconds) private static string FormatDuration(int seconds)
=> $"{seconds / 60}:{seconds % 60:D2}"; => $"{seconds / 60}:{seconds % 60:D2}";
private void HideDifficultyLabel()
{
Transform label = transform.Find("LblDifficulty");
if (label != null)
label.gameObject.SetActive(false);
}
private void UpdateActionButtonStyles(bool downloaded)
{
ApplyButtonStyle(downloadButton, NeonBg, true, !downloaded, false);
ApplyButtonStyle(deleteButton, DangerBg, true, downloaded, true);
ApplyButtonStyle(playButton, NeonBg, true, playButton.interactable, false);
ApplyButtonStyle(closeButton, DarkButtonBg, false, true, false);
}
private static void ApplyButtonStyle(Button btn, Color activeBg, bool outlined, bool enabled, bool danger)
{
if (btn == null)
return;
Color bg = enabled ? activeBg : DisabledBg;
if (btn.targetGraphic is Image img)
img.color = bg;
var colors = btn.colors;
colors.normalColor = bg;
colors.highlightedColor = enabled
? (danger ? new Color(0.72f, 0.23f, 0.30f, 0.86f) : new Color(0.10f, 0.95f, 1.0f, 0.58f))
: DisabledBg;
colors.pressedColor = enabled
? (danger ? new Color(0.42f, 0.10f, 0.15f, 0.92f) : new Color(0.02f, 0.58f, 0.72f, 0.80f))
: DisabledBg;
colors.selectedColor = colors.highlightedColor;
colors.disabledColor = DisabledBg;
colors.fadeDuration = 0.08f;
btn.colors = colors;
TMP_Text label = btn.GetComponentInChildren<TMP_Text>();
if (label != null)
label.color = enabled ? ButtonText : MutedText;
Outline outline = btn.GetComponent<Outline>() ?? btn.gameObject.AddComponent<Outline>();
outline.enabled = outlined && enabled;
outline.effectColor = danger ? new Color(1.0f, 0.35f, 0.42f, 0.34f) : NeonOutline;
outline.effectDistance = new Vector2(0.0f, -0.28f);
}
private static MarqueeText ConfigureMarqueeText(TMP_Text text, float minSize, float maxSize)
{
if (text == null)
return null;
RectTransform textRect = text.rectTransform;
Transform originalParent = textRect.parent;
int siblingIndex = textRect.GetSiblingIndex();
string maskName = $"{text.name}Mask";
Transform existingMask = originalParent != null ? originalParent.Find(maskName) : null;
RectTransform maskRect;
if (existingMask != null)
{
maskRect = existingMask as RectTransform;
if (textRect.parent != existingMask)
textRect.SetParent(existingMask, false);
}
else
{
var mask = new GameObject(maskName);
mask.transform.SetParent(originalParent, false);
mask.transform.SetSiblingIndex(siblingIndex);
maskRect = mask.AddComponent<RectTransform>();
maskRect.anchorMin = textRect.anchorMin;
maskRect.anchorMax = textRect.anchorMax;
maskRect.pivot = textRect.pivot;
maskRect.anchoredPosition = textRect.anchoredPosition;
maskRect.sizeDelta = textRect.sizeDelta;
maskRect.localRotation = textRect.localRotation;
maskRect.localScale = textRect.localScale;
mask.AddComponent<RectMask2D>();
textRect.SetParent(mask.transform, false);
}
textRect.anchorMin = new Vector2(0f, 0f);
textRect.anchorMax = new Vector2(0f, 1f);
textRect.pivot = new Vector2(0f, 0.5f);
textRect.anchoredPosition = Vector2.zero;
textRect.localRotation = Quaternion.identity;
textRect.localScale = Vector3.one;
textRect.sizeDelta = new Vector2(260.0f, 0f);
ConfigureOneLineText(text, minSize, maxSize, TextAlignmentOptions.MidlineLeft);
text.overflowMode = TextOverflowModes.Overflow;
text.raycastTarget = false;
MarqueeText marquee = text.GetComponent<MarqueeText>() ?? text.gameObject.AddComponent<MarqueeText>();
marquee.speed = 9f;
marquee.pauseStart = 1.25f;
marquee.pauseEnd = 0.8f;
return marquee;
}
private static void ConfigureButtonText(Button btn, float minSize, float maxSize)
{
if (btn == null)
return;
TMP_Text label = btn.GetComponentInChildren<TMP_Text>();
ConfigureOneLineText(label, minSize, maxSize, TextAlignmentOptions.Center);
if (label != null)
label.raycastTarget = false;
}
private static void ConfigureOneLineText(TMP_Text text, float minSize, float maxSize, TextAlignmentOptions alignment)
{
if (text == null)
return;
text.enableAutoSizing = true;
text.fontSizeMin = minSize;
text.fontSizeMax = maxSize;
text.alignment = alignment;
text.overflowMode = TextOverflowModes.Ellipsis;
text.textWrappingMode = TextWrappingModes.NoWrap;
}
} }
+137 -37
View File
@@ -16,8 +16,11 @@ public class SongSelectManager : MonoBehaviour
[SerializeField] private TMP_Text errorText; [SerializeField] private TMP_Text errorText;
private static readonly Color TabActive = new Color(1f, 1f, 1f, 0.45f); private static readonly Color TabActiveBg = new Color(0.05f, 0.82f, 0.95f, 0.42f);
private static readonly Color TabInactive = new Color(1f, 1f, 1f, 0.12f); private static readonly Color TabInactiveBg = new Color(0.09f, 0.22f, 0.27f, 0.66f);
private static readonly Color TabActiveText = new Color(0.92f, 1.0f, 1.0f, 1.0f);
private static readonly Color TabInactiveText = new Color(0.72f, 0.86f, 0.90f, 0.82f);
private static readonly Color TabActiveOutline = new Color(0.25f, 0.96f, 1.0f, 0.55f);
private static string CachePath => private static string CachePath =>
Path.Combine(Application.persistentDataPath, "songs_cache.json"); Path.Combine(Application.persistentDataPath, "songs_cache.json");
@@ -49,17 +52,39 @@ public class SongSelectManager : MonoBehaviour
private void SetTabVisual(bool owned) private void SetTabVisual(bool owned)
{ {
ApplyTabColor(tabAllBtn, owned ? TabInactive : TabActive); ApplyTabStyle(tabAllBtn, !owned);
ApplyTabColor(tabOwnedBtn, owned ? TabActive : TabInactive); ApplyTabStyle(tabOwnedBtn, owned);
} }
private static void ApplyTabColor(Button btn, Color c) private static void ApplyTabStyle(Button btn, bool active)
{ {
if (btn == null)
return;
Color bg = active ? TabActiveBg : TabInactiveBg;
if (btn.targetGraphic is Image img) if (btn.targetGraphic is Image img)
img.color = c; img.color = bg;
var colors = btn.colors; var colors = btn.colors;
colors.normalColor = Color.white; colors.normalColor = bg;
colors.highlightedColor = active
? new Color(0.10f, 0.95f, 1.0f, 0.58f)
: new Color(0.14f, 0.34f, 0.40f, 0.72f);
colors.pressedColor = active
? new Color(0.02f, 0.58f, 0.72f, 0.72f)
: new Color(0.08f, 0.20f, 0.24f, 0.82f);
colors.selectedColor = colors.highlightedColor;
colors.disabledColor = new Color(0.05f, 0.10f, 0.12f, 0.45f);
btn.colors = colors; btn.colors = colors;
TMP_Text label = btn.GetComponentInChildren<TMP_Text>();
if (label != null)
label.color = active ? TabActiveText : TabInactiveText;
Outline outline = btn.GetComponent<Outline>() ?? btn.gameObject.AddComponent<Outline>();
outline.enabled = active;
outline.effectColor = TabActiveOutline;
outline.effectDistance = new Vector2(0.0f, -0.35f);
} }
private void FetchSongs() private void FetchSongs()
@@ -70,9 +95,10 @@ public class SongSelectManager : MonoBehaviour
downloadManager.FetchSongsList( downloadManager.FetchSongsList(
onSuccess: list => onSuccess: list =>
{ {
allSongs = list.songs; allSongs = list.songs ?? new List<SongInfo>();
SaveCache(list); AddLocalForcedRankDummies(allSongs);
SongLibrary.Instance.ValidateWithFileSystem(downloadManager, list.songs); SaveCache(new SongsList { version = list.version, songs = allSongs });
SongLibrary.Instance.ValidateWithFileSystem(downloadManager, allSongs);
loadingOverlay.SetActive(false); loadingOverlay.SetActive(false);
RefreshCards(); RefreshCards();
}, },
@@ -134,14 +160,16 @@ public class SongSelectManager : MonoBehaviour
bc.fadeDuration = 0.1f; bc.fadeDuration = 0.1f;
btn.colors = bc; btn.colors = bc;
float textLeftInset = downloaded ? 12f : 5f;
// Title — RectMask2D 컨테이너 안에서 마퀴 스크롤 // Title — RectMask2D 컨테이너 안에서 마퀴 스크롤
var titleMask = new GameObject("TitleMask"); var titleMask = new GameObject("TitleMask");
titleMask.transform.SetParent(card.transform, false); titleMask.transform.SetParent(card.transform, false);
var tmr = titleMask.AddComponent<RectTransform>(); var tmr = titleMask.AddComponent<RectTransform>();
tmr.anchorMin = new Vector2(0f, 0.5f); tmr.anchorMin = new Vector2(0f, 0.5f);
tmr.anchorMax = new Vector2(1f, 1f); tmr.anchorMax = new Vector2(1f, 1f);
tmr.offsetMin = new Vector2(5f, 0f); tmr.offsetMin = new Vector2(textLeftInset, 0f);
tmr.offsetMax = new Vector2(downloaded ? -20f : -3f, 0f); tmr.offsetMax = new Vector2(-3f, 0f);
titleMask.AddComponent<RectMask2D>(); titleMask.AddComponent<RectMask2D>();
var titleGO = new GameObject("Title"); var titleGO = new GameObject("Title");
@@ -159,48 +187,44 @@ public class SongSelectManager : MonoBehaviour
tTmp.color = Color.white; tTmp.color = Color.white;
tTmp.alignment = TextAlignmentOptions.MidlineLeft; tTmp.alignment = TextAlignmentOptions.MidlineLeft;
tTmp.overflowMode = TextOverflowModes.Overflow; tTmp.overflowMode = TextOverflowModes.Overflow;
tTmp.enableWordWrapping = false; tTmp.textWrappingMode = TextWrappingModes.NoWrap;
titleGO.AddComponent<MarqueeText>(); titleGO.AddComponent<MarqueeText>();
// Artist // Artist
var artistGO = new GameObject("Artist"); var artistGO = new GameObject("Artist");
artistGO.transform.SetParent(card.transform, false); artistGO.transform.SetParent(card.transform, false);
var ar = artistGO.AddComponent<RectTransform>(); var ar = artistGO.AddComponent<RectTransform>();
ar.anchorMin = new Vector2(0f, 0f); ar.anchorMin = new Vector2(0f, 0.04f);
ar.anchorMax = new Vector2(1f, 0.5f); ar.anchorMax = new Vector2(1f, 0.48f);
ar.offsetMin = new Vector2(5f, 1f); ar.offsetMin = new Vector2(textLeftInset, 0f);
ar.offsetMax = new Vector2(-3f, 0f); ar.offsetMax = new Vector2(-3f, 0f);
var aTmp = artistGO.AddComponent<TextMeshProUGUI>(); var aTmp = artistGO.AddComponent<TextMeshProUGUI>();
if (_cardFont != null) aTmp.font = _cardFont; if (_cardFont != null) aTmp.font = _cardFont;
aTmp.text = song.artist; aTmp.text = song.artist;
aTmp.fontSize = 4f; aTmp.fontSize = 4f;
aTmp.enableAutoSizing = true;
aTmp.fontSizeMin = 2.8f;
aTmp.fontSizeMax = 4f;
aTmp.color = new Color(1f, 1f, 1f, 0.6f); aTmp.color = new Color(1f, 1f, 1f, 0.6f);
aTmp.alignment = TextAlignmentOptions.MidlineLeft; aTmp.alignment = TextAlignmentOptions.MidlineLeft;
aTmp.overflowMode = TextOverflowModes.Ellipsis;
aTmp.textWrappingMode = TextWrappingModes.NoWrap;
// Downloaded badge // Downloaded check mark
if (downloaded) if (downloaded)
{ {
var badge = new GameObject("Badge"); var checkGO = new GameObject("OwnedCheck");
badge.transform.SetParent(card.transform, false); checkGO.transform.SetParent(card.transform, false);
var br = badge.AddComponent<RectTransform>(); var cr = checkGO.AddComponent<RectTransform>();
br.anchorMin = new Vector2(1f, 0.5f); cr.anchorMin = new Vector2(0f, 0f);
br.anchorMax = new Vector2(1f, 0.5f); cr.anchorMax = new Vector2(0f, 1f);
br.pivot = new Vector2(1f, 0.5f); cr.pivot = new Vector2(0f, 0.5f);
br.anchoredPosition = new Vector2(-3f, 0f); cr.anchoredPosition = new Vector2(3.0f, 0f);
br.sizeDelta = new Vector2(14f, 5.5f); cr.sizeDelta = new Vector2(6f, 0f);
badge.AddComponent<Image>().color = new Color(0.2f, 0.78f, 0.4f, 0.85f);
var bl = new GameObject("Text"); Color checkColor = new Color(0.36f, 1.0f, 0.58f, 0.95f);
bl.transform.SetParent(badge.transform, false); CreateCheckStroke(checkGO.transform, "ShortStroke", new Vector2(1.8f, 7.1f), new Vector2(1.5f, 0.35f), 42.0f, checkColor);
var blr = bl.AddComponent<RectTransform>(); CreateCheckStroke(checkGO.transform, "LongStroke", new Vector2(3.25f, 7.85f), new Vector2(3.7f, 0.35f), -45.0f, checkColor);
blr.anchorMin = Vector2.zero;
blr.anchorMax = Vector2.one;
blr.offsetMin = blr.offsetMax = Vector2.zero;
var blTmp = bl.AddComponent<TextMeshProUGUI>();
blTmp.text = "OWNED";
blTmp.fontSize = 3.5f;
blTmp.color = Color.white;
blTmp.alignment = TextAlignmentOptions.Center;
} }
SongInfo captured = song; SongInfo captured = song;
@@ -213,6 +237,25 @@ public class SongSelectManager : MonoBehaviour
detailPanel.Show(song, downloadManager, this); detailPanel.Show(song, downloadManager, this);
} }
private static void CreateCheckStroke(Transform parent, string name, Vector2 anchoredPosition,
Vector2 size, float rotationZ, Color color)
{
var stroke = new GameObject(name);
stroke.transform.SetParent(parent, false);
var rect = stroke.AddComponent<RectTransform>();
rect.anchorMin = new Vector2(0f, 0f);
rect.anchorMax = new Vector2(0f, 0f);
rect.pivot = new Vector2(0.5f, 0.5f);
rect.anchoredPosition = anchoredPosition;
rect.sizeDelta = size;
rect.localRotation = Quaternion.Euler(0f, 0f, rotationZ);
var img = stroke.AddComponent<Image>();
img.color = color;
img.raycastTarget = false;
}
private static void SaveCache(SongsList list) private static void SaveCache(SongsList list)
{ {
try { File.WriteAllText(CachePath, JsonUtility.ToJson(list, true)); } try { File.WriteAllText(CachePath, JsonUtility.ToJson(list, true)); }
@@ -225,4 +268,61 @@ public class SongSelectManager : MonoBehaviour
try { return JsonUtility.FromJson<SongsList>(File.ReadAllText(CachePath)); } try { return JsonUtility.FromJson<SongsList>(File.ReadAllText(CachePath)); }
catch { return null; } catch { return null; }
} }
private static void AddLocalForcedRankDummies(List<SongInfo> songs)
{
string root = Path.Combine(Application.persistentDataPath, "beatsaber");
AddLocalForcedRankDummy(songs, root, "dummy_rank_m", "M", 10);
AddLocalForcedRankDummy(songs, root, "dummy_rank_splus", "S+", 100);
AddLocalForcedRankDummy(songs, root, "dummy_rank_s", "S", 100);
AddLocalForcedRankDummy(songs, root, "dummy_rank_a", "A", 100);
AddLocalForcedRankDummy(songs, root, "dummy_rank_b", "B", 100);
AddLocalForcedRankDummy(songs, root, "dummy_rank_c", "C", 100);
AddLocalForcedRankDummy(songs, root, "dummy_rank_d", "D", 100);
AddLocalForcedRankDummy(songs, root, "dummy_rank_f", "F", 100);
AddLocalForcedRankDummy(songs, root, "dummy_rank_splus_border", "S+ 98%", 100);
AddLocalForcedRankDummy(songs, root, "dummy_rank_f_zero", "F 0%", 20);
}
private static void AddLocalForcedRankDummy(List<SongInfo> songs, string root, string id, string title, int noteCount)
{
if (songs.Exists(song => song.id == id))
return;
string songDir = Path.Combine(root, id);
string audioPath = Path.Combine(songDir, $"{id}.mp3");
string mapFile = $"Map_{id}_forced.json";
string mapPath = Path.Combine(songDir, mapFile);
if (!File.Exists(audioPath) || !File.Exists(mapPath))
return;
long audioSize = new FileInfo(audioPath).Length;
long mapSize = new FileInfo(mapPath).Length;
DifficultyInfo info = new DifficultyInfo
{
mapFile = mapFile,
mapSize = mapSize,
noteCount = noteCount
};
songs.Insert(0, new SongInfo
{
id = id,
title = title,
artist = "Forced Rank Dummy",
bpm = 120.0f,
duration = 1,
audioFile = $"dummy/{id}.mp3",
audioSize = audioSize,
coverImage = "",
difficulties = new DifficultyMap
{
normal = info,
hard = info,
expert = info,
expertplus = info
},
addedAt = "2026-05-29"
});
}
} }
+341
View File
@@ -0,0 +1,341 @@
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using UnityEngine.XR;
using XRCommonUsages = UnityEngine.XR.CommonUsages;
using XRInputDevice = UnityEngine.XR.InputDevice;
public class SyncCalibrationOverlay : MonoBehaviour
{
private const float TickInterval = 1.0f;
private const int MaxSamples = 8;
private static Scene pendingReturnScene;
private readonly List<float> samples = new();
private readonly List<(Canvas canvas, bool enabled)> hiddenCanvases = new();
private AudioSource audioSource = null;
private AudioClip tickClip = null;
private RectTransform sweepDot = null;
private RectTransform visualPulse = null;
private TextMeshProUGUI offsetText = null;
private TextMeshProUGUI sampleText = null;
private TextMeshProUGUI guideText = null;
private float lastTickTime = 0.0f;
private float nextTickTime = 0.0f;
private float pendingVisualPulseTime = -1.0f;
private float visualPulseTimer = 0.0f;
private bool previousPrimary = false;
private bool previousSecondary = false;
private Scene returnScene;
private Scene syncScene;
private bool canvasesRestored = false;
public static void Open()
{
if (FindFirstObjectByType<SyncCalibrationOverlay>() != null)
return;
pendingReturnScene = SceneManager.GetActiveScene();
Scene calibrationScene = SceneManager.CreateScene("SyncCalibration");
SceneManager.SetActiveScene(calibrationScene);
GameObject overlay = new GameObject("[SyncCalibrationOverlay]");
overlay.AddComponent<SyncCalibrationOverlay>();
}
private void Awake()
{
syncScene = gameObject.scene;
returnScene = pendingReturnScene;
HideExistingCanvases();
BuildView();
tickClip = CreateTickClip();
audioSource = gameObject.AddComponent<AudioSource>();
audioSource.playOnAwake = false;
audioSource.spatialBlend = 0.0f;
float now = Time.unscaledTime;
lastTickTime = now;
nextTickTime = now + 0.8f;
UpdateTexts();
}
private void OnDestroy()
{
RestoreCanvases();
}
private void Update()
{
float now = Time.unscaledTime;
if (now >= nextTickTime)
{
lastTickTime = nextTickTime;
nextTickTime += TickInterval;
audioSource.PlayOneShot(tickClip);
pendingVisualPulseTime = lastTickTime + GlobalSyncSettings.AudioOffsetSeconds;
}
if (pendingVisualPulseTime > 0.0f && now >= pendingVisualPulseTime)
{
visualPulseTimer = 0.18f;
pendingVisualPulseTime = -1.0f;
}
UpdateMetronomeVisual(now);
HandleInput(now);
}
private void HandleInput(float now)
{
bool primary = GetRightButton(XRCommonUsages.primaryButton) || IsKeyboardPressed(Key.Space);
bool secondary = GetRightButton(XRCommonUsages.secondaryButton) || IsKeyboardPressed(Key.Escape);
if (primary && !previousPrimary)
CaptureSample(now);
if (secondary && !previousSecondary)
Close();
previousPrimary = primary;
previousSecondary = secondary;
}
private void CaptureSample(float now)
{
float nearestTick = Mathf.Abs(now - lastTickTime) <= Mathf.Abs(now - nextTickTime)
? lastTickTime
: nextTickTime;
float offsetMs = Mathf.Clamp((now - nearestTick) * 1000.0f, -300.0f, 300.0f);
samples.Add(offsetMs);
if (samples.Count > MaxSamples)
samples.RemoveAt(0);
float sum = 0.0f;
for (int i = 0; i < samples.Count; i++)
sum += samples[i];
GlobalSyncSettings.AudioOffsetMs = sum / samples.Count;
UpdateTexts();
}
private void AdjustOffset(float deltaMs)
{
GlobalSyncSettings.AudioOffsetMs += deltaMs;
samples.Clear();
UpdateTexts();
}
private void ResetOffset()
{
GlobalSyncSettings.Reset();
samples.Clear();
UpdateTexts();
}
private void Close()
{
RestoreCanvases();
if (returnScene.IsValid() && returnScene.isLoaded)
SceneManager.SetActiveScene(returnScene);
if (syncScene.IsValid() && syncScene.isLoaded)
SceneManager.UnloadSceneAsync(syncScene);
else
Destroy(gameObject);
}
private void BuildView()
{
Camera camera = Camera.main ?? FindFirstObjectByType<Camera>();
GameObject canvasObject = new GameObject("SyncCalibrationCanvas");
canvasObject.transform.SetParent(transform, false);
Canvas canvas = canvasObject.AddComponent<Canvas>();
canvas.renderMode = RenderMode.WorldSpace;
canvas.sortingOrder = 400;
canvas.worldCamera = camera;
canvasObject.AddComponent<GraphicRaycaster>();
RectTransform canvasRect = canvasObject.GetComponent<RectTransform>();
canvasRect.sizeDelta = new Vector2(1080.0f, 660.0f);
canvasObject.transform.localScale = Vector3.one * 0.0028f;
if (camera != null)
{
Transform camTransform = camera.transform;
canvasObject.transform.position = camTransform.position + camTransform.forward * 1.9f;
canvasObject.transform.rotation = Quaternion.LookRotation(canvasObject.transform.position - camTransform.position, camTransform.up);
}
Image background = CreateImage("Panel", canvasRect, new Vector2(0, 0), new Vector2(1020, 620), new Color(0.02f, 0.06f, 0.09f, 0.92f));
background.raycastTarget = false;
TextMeshProUGUI title = CreateText("Title", canvasRect, "SYNC CALIBRATION", new Vector2(0, 260), new Vector2(920, 58), 42, Color.white, TextAlignmentOptions.Center);
title.fontStyle = FontStyles.Bold;
guideText = CreateText("Guide", canvasRect, "", new Vector2(0, 180), new Vector2(920, 92), 26, new Color(0.77f, 0.9f, 1.0f, 1.0f), TextAlignmentOptions.Center);
Image bar = CreateImage("BeatBar", canvasRect, new Vector2(0, 82), new Vector2(760, 10), new Color(0.25f, 0.85f, 1.0f, 0.28f));
bar.raycastTarget = false;
sweepDot = CreateImage("BeatDot", canvasRect, new Vector2(-380, 82), new Vector2(34, 34), new Color(0.35f, 0.95f, 1.0f, 1.0f)).rectTransform;
visualPulse = CreateImage("VisualPulse", canvasRect, new Vector2(0, 82), new Vector2(140, 140), new Color(0.35f, 0.95f, 1.0f, 0.0f)).rectTransform;
offsetText = CreateText("Offset", canvasRect, "", new Vector2(0, 0), new Vector2(720, 74), 54, Color.white, TextAlignmentOptions.Center);
offsetText.fontStyle = FontStyles.Bold;
sampleText = CreateText("Samples", canvasRect, "", new Vector2(0, -72), new Vector2(760, 44), 24, new Color(0.65f, 0.78f, 0.84f, 1.0f), TextAlignmentOptions.Center);
CreateButton(canvasRect, "-10ms", new Vector2(-300, -165), () => AdjustOffset(-10.0f));
CreateButton(canvasRect, "+10ms", new Vector2(-100, -165), () => AdjustOffset(10.0f));
CreateButton(canvasRect, "RESET", new Vector2(100, -165), ResetOffset);
CreateButton(canvasRect, "BACK", new Vector2(300, -165), Close);
CreateText("Footer", canvasRect, "A / Space: capture beat B / Esc: back", new Vector2(0, -270), new Vector2(860, 40), 22, new Color(0.58f, 0.7f, 0.75f, 1.0f), TextAlignmentOptions.Center);
}
private void UpdateMetronomeVisual(float now)
{
float phase = Mathf.InverseLerp(lastTickTime, nextTickTime, now);
if (sweepDot != null)
sweepDot.anchoredPosition = new Vector2(Mathf.Lerp(-380.0f, 380.0f, phase), 82.0f);
if (visualPulse == null)
return;
visualPulseTimer = Mathf.Max(0.0f, visualPulseTimer - Time.unscaledDeltaTime);
float alpha = visualPulseTimer / 0.18f;
visualPulse.sizeDelta = Vector2.one * Mathf.Lerp(190.0f, 80.0f, alpha);
Image image = visualPulse.GetComponent<Image>();
if (image != null)
image.color = new Color(0.35f, 0.95f, 1.0f, alpha * 0.52f);
}
private void UpdateTexts()
{
float offset = GlobalSyncSettings.AudioOffsetMs;
if (offsetText != null)
offsetText.text = $"{offset:+0;-0;0} ms";
if (sampleText != null)
sampleText.text = $"samples {samples.Count}/{MaxSamples} global offset saved";
if (guideText != null)
guideText.text = "Tick 소리가 들리는 순간 A / Space를 누르세요.\n파란 원이 박자와 겹치면 보정이 맞습니다.";
}
private void HideExistingCanvases()
{
hiddenCanvases.Clear();
foreach (Canvas canvas in FindObjectsByType<Canvas>(FindObjectsSortMode.None))
{
hiddenCanvases.Add((canvas, canvas.enabled));
canvas.enabled = false;
}
}
private void RestoreCanvases()
{
if (canvasesRestored)
return;
canvasesRestored = true;
foreach ((Canvas canvas, bool enabled) in hiddenCanvases)
{
if (canvas != null)
canvas.enabled = enabled;
}
}
private static TextMeshProUGUI CreateText(string name, RectTransform parent, string value, Vector2 position, Vector2 size, int fontSize, Color color, TextAlignmentOptions alignment)
{
GameObject go = new GameObject(name);
go.transform.SetParent(parent, false);
RectTransform rect = go.AddComponent<RectTransform>();
rect.anchoredPosition = position;
rect.sizeDelta = size;
TextMeshProUGUI text = go.AddComponent<TextMeshProUGUI>();
text.text = value;
text.fontSize = fontSize;
text.color = color;
text.alignment = alignment;
text.textWrappingMode = TextWrappingModes.Normal;
text.overflowMode = TextOverflowModes.Overflow;
text.raycastTarget = false;
return text;
}
private static Image CreateImage(string name, RectTransform parent, Vector2 position, Vector2 size, Color color)
{
GameObject go = new GameObject(name);
go.transform.SetParent(parent, false);
RectTransform rect = go.AddComponent<RectTransform>();
rect.anchoredPosition = position;
rect.sizeDelta = size;
Image image = go.AddComponent<Image>();
image.color = color;
return image;
}
private static void CreateButton(RectTransform parent, string label, Vector2 position, UnityEngine.Events.UnityAction action)
{
Image image = CreateImage(label + "Button", parent, position, new Vector2(168, 58), new Color(0.07f, 0.18f, 0.24f, 0.96f));
Button button = image.gameObject.AddComponent<Button>();
button.onClick.AddListener(action);
ColorBlock colors = button.colors;
colors.normalColor = new Color(0.07f, 0.18f, 0.24f, 0.96f);
colors.highlightedColor = new Color(0.13f, 0.38f, 0.48f, 1.0f);
colors.pressedColor = new Color(0.08f, 0.72f, 0.85f, 1.0f);
colors.selectedColor = colors.highlightedColor;
button.colors = colors;
TextMeshProUGUI labelText = CreateText(label + "Text", image.rectTransform, label, Vector2.zero, new Vector2(154, 48), 24, Color.white, TextAlignmentOptions.Center);
labelText.fontStyle = FontStyles.Bold;
}
private static bool GetRightButton(InputFeatureUsage<bool> usage)
{
var devices = new List<XRInputDevice>();
InputDevices.GetDevicesWithCharacteristics(InputDeviceCharacteristics.Controller | InputDeviceCharacteristics.Right, devices);
if (devices.Count == 0)
return false;
devices[0].TryGetFeatureValue(usage, out bool pressed);
return pressed;
}
private static bool IsKeyboardPressed(Key key)
{
Keyboard keyboard = Keyboard.current;
return keyboard != null && keyboard[key].isPressed;
}
private static AudioClip CreateTickClip()
{
const int sampleRate = 48000;
const float duration = 0.055f;
int sampleCount = Mathf.CeilToInt(sampleRate * duration);
float[] data = new float[sampleCount];
for (int i = 0; i < sampleCount; i++)
{
float t = (float)i / sampleRate;
float envelope = Mathf.Exp(-t * 62.0f);
float high = Mathf.Sin(2.0f * Mathf.PI * 1760.0f * t);
float click = i < 80 ? 1.0f - (float)i / 80.0f : 0.0f;
data[i] = Mathf.Clamp((high * 0.75f + click * 0.35f) * envelope, -1.0f, 1.0f);
}
AudioClip clip = AudioClip.Create("SyncTick", sampleCount, 1, sampleRate, false);
clip.SetData(data, 0);
return clip;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 90f8d18d467240d8bb84178048a5aa91
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+512
View File
@@ -0,0 +1,512 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using UnityEngine.XR;
namespace VRBeats
{
[RequireComponent(typeof(LineRenderer))]
public class VRPointerController : MonoBehaviour
{
[SerializeField] private bool isRightHand = true;
[SerializeField] private float maxDistance = 50f;
[SerializeField] private bool debugLogging = false;
[SerializeField] private float scrollSpeed = 2.4f;
[SerializeField] private float scrollDeadZone = 0.15f;
[SerializeField] private float dragScrollSpeed = 1.25f;
[SerializeField] private float dragClickThreshold = 0.025f;
private LineRenderer _line;
private bool _prevTrigger;
private Selectable _currentHover;
private ScrollRect _dragScrollRect;
private Selectable _triggerPressSelectable;
private Vector2 _dragStartLocalPoint;
private float _dragStartNormalizedPosition;
private float _dragMaxNormalizedDelta;
private static readonly Color NormalColor = new Color(1f, 1f, 1f, 0.8f);
private static readonly Color HoverColor = new Color(0.3f, 0.8f, 1f, 1f);
private float _deviceLogTimer;
// 버튼별 이전 상태
private bool _prevGrip;
private bool _prevPrimary;
private bool _prevSecondary;
private bool _prevThumbstick;
private void Awake()
{
_line = GetComponent<LineRenderer>();
_line.positionCount = 2;
_line.startWidth = 0.005f;
_line.endWidth = 0.001f;
_line.useWorldSpace = true;
// enabled 상태는 OnEnable/OnDisable이 관리 — Awake에서 건드리지 않음
}
private void Start()
{
// Awake 이후 reflection으로 isRightHand가 설정되므로 Start에서 로그
if (debugLogging)
Debug.Log($"[VRPointer] Start — {gameObject.name} / isRightHand={isRightHand}");
}
private void OnEnable()
{
if (_line != null) _line.enabled = true;
}
private void OnDisable()
{
if (_line != null) _line.enabled = false;
}
private void Update()
{
// 3초마다 연결된 디바이스 목록 출력
if (debugLogging)
{
_deviceLogTimer += Time.deltaTime;
if (_deviceLogTimer >= 3f)
{
_deviceLogTimer = 0f;
LogConnectedDevices();
}
}
bool trigger = GetButton(CommonUsages.triggerButton);
bool grip = GetButton(CommonUsages.gripButton);
bool primary = GetButton(CommonUsages.primaryButton);
bool secondary = GetButton(CommonUsages.secondaryButton);
bool thumbstick = GetButton(CommonUsages.primary2DAxisClick);
bool triggerDown = trigger && !_prevTrigger;
bool triggerUp = !trigger && _prevTrigger;
bool gripDown = grip && !_prevGrip;
bool primaryDown = primary && !_prevPrimary;
bool secondaryDown = secondary && !_prevSecondary;
bool thumbstickDown = thumbstick && !_prevThumbstick;
string hand = isRightHand ? "R" : "L";
if (debugLogging)
{
if (triggerDown) Debug.Log($"[VRPointer:{hand}] 검지 트리거 눌림");
if (gripDown) Debug.Log($"[VRPointer:{hand}] 그립(중지) 눌림");
if (primaryDown) Debug.Log($"[VRPointer:{hand}] {(isRightHand ? "A" : "X")} 버튼 눌림");
if (secondaryDown) Debug.Log($"[VRPointer:{hand}] {(isRightHand ? "B" : "Y")} 버튼 눌림");
if (thumbstickDown) Debug.Log($"[VRPointer:{hand}] 조이스틱 클릭 눌림");
}
Ray ray = new Ray(transform.position, transform.forward);
float selectableHitDist = maxDistance;
float scrollHitDist = maxDistance;
Selectable hit = FindSelectableUnderRay(ray, ref selectableHitDist);
ScrollRect scrollRect = FindScrollRectUnderRay(ray, ref scrollHitDist);
float hitDist = Mathf.Min(selectableHitDist, scrollHitDist);
bool beganScrollDrag = false;
if (triggerDown)
beganScrollDrag = TryBeginScrollDrag(scrollRect, hit, ray);
if (_dragScrollRect != null && trigger)
UpdateScrollDrag(ray);
else if (_dragScrollRect == null)
HandleScroll(scrollRect);
// 호버 변화 로그
if (debugLogging && hit != _currentHover)
{
Debug.Log(hit != null
? $"[VRPointer] HOVER → {hit.gameObject.name}"
: $"[VRPointer] HOVER → (없음)");
}
UpdateHoverState(hit);
if (triggerUp && _dragScrollRect != null)
EndScrollDrag(hand, ray);
// 검지 트리거 또는 A/X 버튼으로 클릭.
// ScrollRect 위의 검지 트리거는 드래그/클릭 판별을 위해 release 시점에 처리한다.
if ((triggerDown && !beganScrollDrag) || primaryDown)
{
if (_currentHover != null)
{
string btn = triggerDown && !beganScrollDrag ? "검지 트리거" : (isRightHand ? "A" : "X");
if (debugLogging)
Debug.Log($"[VRPointer:{hand}] CLICK [{btn}] → {_currentHover.gameObject.name}");
Click(_currentHover);
}
else if (debugLogging)
{
Debug.Log($"[VRPointer:{hand}] CLICK — 레이 아래 버튼 없음 " +
$"pos={transform.position:F2} fwd={transform.forward:F2}");
DebugRaycastAttempt(new Ray(transform.position, transform.forward));
}
}
DrawLine(hitDist);
_prevTrigger = trigger;
_prevGrip = grip;
_prevPrimary = primary;
_prevSecondary = secondary;
_prevThumbstick = thumbstick;
}
private void LogConnectedDevices()
{
var all = new List<InputDevice>();
InputDevices.GetDevices(all);
if (all.Count == 0)
{
Debug.LogWarning($"[VRPointer] 연결된 XR 디바이스 없음 ({gameObject.name})");
return;
}
var chars = InputDeviceCharacteristics.Controller |
(isRightHand ? InputDeviceCharacteristics.Right : InputDeviceCharacteristics.Left);
var matched = new List<InputDevice>();
InputDevices.GetDevicesWithCharacteristics(chars, matched);
Debug.Log($"[VRPointer] 전체 디바이스 {all.Count}개 | " +
$"{(isRightHand ? "" : "")} 컨트롤러 {matched.Count}개 ({gameObject.name})");
}
private void UpdateHoverState(Selectable hit)
{
if (hit == _currentHover) return;
var es = EventSystem.current;
if (_currentHover != null)
ExecuteEvents.Execute(_currentHover.gameObject,
new PointerEventData(es), ExecuteEvents.pointerExitHandler);
_currentHover = hit;
if (_currentHover != null)
ExecuteEvents.Execute(_currentHover.gameObject,
new PointerEventData(es), ExecuteEvents.pointerEnterHandler);
}
private static void Click(Selectable sel)
{
var es = EventSystem.current;
if (es == null)
{
Debug.LogWarning("[VRPointer] EventSystem.current is null — click 실패");
return;
}
var eventData = new PointerEventData(es);
ExecuteEvents.Execute(sel.gameObject, eventData, ExecuteEvents.pointerDownHandler);
ExecuteEvents.Execute(sel.gameObject, eventData, ExecuteEvents.pointerUpHandler);
ExecuteEvents.Execute(sel.gameObject, eventData, ExecuteEvents.pointerClickHandler);
var btn = sel.GetComponent<Button>();
if (btn != null) btn.onClick.Invoke();
}
private void DrawLine(float hitDist)
{
Color c = _currentHover != null ? HoverColor : NormalColor;
_line.startColor = c;
_line.endColor = new Color(c.r, c.g, c.b, 0f);
_line.SetPosition(0, transform.position);
_line.SetPosition(1, transform.position + transform.forward * hitDist);
}
private static void DebugRaycastAttempt(Ray ray)
{
var all = Selectable.allSelectablesArray;
Debug.Log($"[VRPointer:DEBUG] Selectable 총 {all.Length}개 | ray.origin={ray.origin:F2} ray.dir={ray.direction:F2}");
foreach (Selectable sel in all)
{
if (!sel.gameObject.activeInHierarchy) { Debug.Log($" SKIP(비활성) {sel.gameObject.name}"); continue; }
if (!sel.interactable) { Debug.Log($" SKIP(interactable=false) {sel.gameObject.name}"); continue; }
var rt = sel.GetComponent<RectTransform>();
if (rt == null) { Debug.Log($" SKIP(RectTransform없음) {sel.gameObject.name}"); continue; }
Vector3[] c = new Vector3[4];
rt.GetWorldCorners(c);
Vector3 normal = rt.forward;
if (Vector3.Dot(normal, ray.direction) >= 0f) normal = -normal;
Plane plane = new Plane(normal, c[0]);
bool hit = plane.Raycast(ray, out float dist);
if (!hit) { Debug.Log($" MISS(Plane.Raycast=false) {sel.gameObject.name} | rtPos={rt.position:F2} normal={normal:F2}"); continue; }
bool inRect = IsPointInRect(ray.GetPoint(dist), c);
Debug.Log($" {(inRect ? "HIT" : "MISS(InRect=false)")} {sel.gameObject.name} | dist={dist:F2} inRect={inRect}");
}
}
private static Selectable FindSelectableUnderRay(Ray ray, ref float maxDist)
{
Selectable closest = null;
float closestDist = maxDist;
foreach (Selectable sel in Selectable.allSelectablesArray)
{
if (!sel.gameObject.activeInHierarchy || !sel.interactable) continue;
if (!IsOnEnabledCanvas(sel)) continue;
var rt = sel.GetComponent<RectTransform>();
if (rt == null) continue;
Vector3[] corners = new Vector3[4];
rt.GetWorldCorners(corners);
// Plane.Raycast은 normal 쪽(앞면)에서 레이가 출발해야 true 반환.
// rt.forward가 카메라 반대를 향할 수 있으므로 레이 원점 방향으로 노말 보정.
Vector3 normal = rt.forward;
if (Vector3.Dot(normal, ray.direction) >= 0f)
normal = -normal;
Plane plane = new Plane(normal, corners[0]);
if (!plane.Raycast(ray, out float dist)) continue;
if (dist >= closestDist || dist <= 0f) continue;
if (!IsPointInRect(ray.GetPoint(dist), corners)) continue;
closestDist = dist;
closest = sel;
}
maxDist = closestDist;
return closest;
}
private static bool IsPointInRect(Vector3 p, Vector3[] c)
{
Vector3 toP = p - c[0];
Vector3 right = c[3] - c[0];
Vector3 up = c[1] - c[0];
float r = Vector3.Dot(toP, right) / right.sqrMagnitude;
float u = Vector3.Dot(toP, up) / up.sqrMagnitude;
return r >= 0f && r <= 1f && u >= 0f && u <= 1f;
}
private static ScrollRect FindScrollRectUnderRay(Ray ray, ref float maxDist)
{
ScrollRect closest = null;
float closestDist = maxDist;
var all = Object.FindObjectsByType<ScrollRect>(FindObjectsSortMode.None);
foreach (ScrollRect scroll in all)
{
if (!scroll.isActiveAndEnabled) continue;
if (!IsOnEnabledCanvas(scroll)) continue;
RectTransform rt = scroll.viewport != null
? scroll.viewport
: scroll.GetComponent<RectTransform>();
if (rt == null) continue;
Vector3[] corners = new Vector3[4];
rt.GetWorldCorners(corners);
Vector3 normal = rt.forward;
if (Vector3.Dot(normal, ray.direction) >= 0f)
normal = -normal;
Plane plane = new Plane(normal, corners[0]);
if (!plane.Raycast(ray, out float dist)) continue;
if (dist >= closestDist || dist <= 0f) continue;
if (!IsPointInRect(ray.GetPoint(dist), corners)) continue;
closestDist = dist;
closest = scroll;
}
if (closest != null)
maxDist = closestDist;
return closest;
}
private static bool IsOnEnabledCanvas(Component component)
{
Canvas[] canvases = component.GetComponentsInParent<Canvas>(true);
if (canvases.Length == 0)
return true;
for (int i = 0; i < canvases.Length; i++)
{
Canvas canvas = canvases[i];
if (canvas == null)
continue;
if (!canvas.enabled || !canvas.gameObject.activeInHierarchy)
return false;
}
return true;
}
private void HandleScroll(ScrollRect scrollRect)
{
if (!CanScrollVertically(scrollRect))
return;
Vector2 axis = GetAxis(CommonUsages.primary2DAxis);
if (Mathf.Abs(axis.y) < scrollDeadZone)
return;
scrollRect.verticalNormalizedPosition = Mathf.Clamp01(
scrollRect.verticalNormalizedPosition + axis.y * scrollSpeed * Time.deltaTime);
}
private bool TryBeginScrollDrag(ScrollRect scrollRect, Selectable pressSelectable, Ray ray)
{
if (!CanScrollVertically(scrollRect))
return false;
if (!TryGetScrollLocalPoint(scrollRect, ray, out Vector2 localPoint, out _))
return false;
_dragScrollRect = scrollRect;
_triggerPressSelectable = pressSelectable;
_dragStartLocalPoint = localPoint;
_dragStartNormalizedPosition = scrollRect.verticalNormalizedPosition;
_dragMaxNormalizedDelta = 0f;
return true;
}
private void UpdateScrollDrag(Ray ray)
{
if (_dragScrollRect == null)
return;
if (!TryGetScrollLocalPoint(_dragScrollRect, ray, out Vector2 localPoint, out float viewportHeight))
return;
float deltaY = localPoint.y - _dragStartLocalPoint.y;
float normalizedDelta = deltaY / viewportHeight * dragScrollSpeed;
_dragMaxNormalizedDelta = Mathf.Max(_dragMaxNormalizedDelta, Mathf.Abs(normalizedDelta));
_dragScrollRect.verticalNormalizedPosition = Mathf.Clamp01(
_dragStartNormalizedPosition - normalizedDelta);
}
private void EndScrollDrag(string hand, Ray ray)
{
bool shouldClick = _dragMaxNormalizedDelta < dragClickThreshold;
ScrollRect scrollRect = _dragScrollRect;
Selectable pressSelectable = _triggerPressSelectable;
float startNormalizedPosition = _dragStartNormalizedPosition;
ClearScrollDrag();
if (!shouldClick)
return;
if (scrollRect != null)
scrollRect.verticalNormalizedPosition = startNormalizedPosition;
if (pressSelectable != null && pressSelectable.isActiveAndEnabled && pressSelectable.interactable)
{
if (debugLogging)
Debug.Log($"[VRPointer:{hand}] CLICK [검지 트리거] → {pressSelectable.gameObject.name}");
Click(pressSelectable);
}
else if (debugLogging)
{
Debug.Log($"[VRPointer:{hand}] CLICK — 레이 아래 버튼 없음 " +
$"pos={transform.position:F2} fwd={transform.forward:F2}");
DebugRaycastAttempt(ray);
}
}
private void ClearScrollDrag()
{
_dragScrollRect = null;
_triggerPressSelectable = null;
_dragStartLocalPoint = Vector2.zero;
_dragStartNormalizedPosition = 0f;
_dragMaxNormalizedDelta = 0f;
}
private static bool CanScrollVertically(ScrollRect scrollRect)
{
if (scrollRect == null || !scrollRect.vertical)
return false;
RectTransform viewport = scrollRect.viewport != null
? scrollRect.viewport
: scrollRect.GetComponent<RectTransform>();
if (viewport == null || scrollRect.content == null)
return true;
return scrollRect.content.rect.height > viewport.rect.height + 1f;
}
private static bool TryGetScrollLocalPoint(ScrollRect scrollRect, Ray ray, out Vector2 localPoint, out float viewportHeight)
{
localPoint = Vector2.zero;
viewportHeight = 1f;
RectTransform rt = scrollRect.viewport != null
? scrollRect.viewport
: scrollRect.GetComponent<RectTransform>();
if (rt == null)
return false;
Vector3[] corners = new Vector3[4];
rt.GetWorldCorners(corners);
Vector3 normal = rt.forward;
if (Vector3.Dot(normal, ray.direction) >= 0f)
normal = -normal;
Plane plane = new Plane(normal, corners[0]);
if (!plane.Raycast(ray, out float dist) || dist <= 0f)
return false;
Vector3 local = rt.InverseTransformPoint(ray.GetPoint(dist));
localPoint = new Vector2(local.x, local.y);
viewportHeight = Mathf.Max(1f, rt.rect.height);
return true;
}
private bool GetButton(InputFeatureUsage<bool> usage)
{
var chars = InputDeviceCharacteristics.Controller |
(isRightHand
? InputDeviceCharacteristics.Right
: InputDeviceCharacteristics.Left);
var devices = new List<InputDevice>();
InputDevices.GetDevicesWithCharacteristics(chars, devices);
if (devices.Count == 0) return false;
devices[0].TryGetFeatureValue(usage, out bool pressed);
return pressed;
}
private Vector2 GetAxis(InputFeatureUsage<Vector2> usage)
{
var chars = InputDeviceCharacteristics.Controller |
(isRightHand
? InputDeviceCharacteristics.Right
: InputDeviceCharacteristics.Left);
var devices = new List<InputDevice>();
InputDevices.GetDevicesWithCharacteristics(chars, devices);
if (devices.Count == 0) return Vector2.zero;
devices[0].TryGetFeatureValue(usage, out Vector2 axis);
return axis;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: eec74ab726176d74491e9be716a7a609
+114
View File
@@ -0,0 +1,114 @@
using UnityEngine;
using UnityEngine.SceneManagement;
namespace VRBeats
{
// 모든 씬에서 자동 실행.
// Game 씬: VRPointerController를 비활성 상태로 추가 → VR_InteractorController가 게임오버 시 활성화.
// 나머지 씬: 바로 활성 상태로 추가.
public class VRPointerSetup : MonoBehaviour
{
private static VRPointerSetup instance;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
private static void ResetStatics()
{
instance = null;
}
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void AutoInject()
{
if (instance != null)
return;
var go = new GameObject("[VRPointerSetup]");
go.AddComponent<VRPointerSetup>();
}
private void Awake()
{
if (instance != null && instance != this)
{
Destroy(gameObject);
return;
}
instance = this;
DontDestroyOnLoad(gameObject);
}
private void OnEnable()
{
SceneManager.sceneLoaded += OnSceneLoaded;
}
private void OnDisable()
{
SceneManager.sceneLoaded -= OnSceneLoaded;
}
private void Start()
{
SetupActiveScene();
}
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
SetupScene(scene);
}
private static void SetupActiveScene()
{
SetupScene(SceneManager.GetActiveScene());
}
private static void SetupScene(Scene scene)
{
bool isGameScene = scene.name == "Game";
SetupControllers(disabledByDefault: isGameScene);
}
private static void SetupControllers(bool disabledByDefault)
{
foreach (var go in FindObjectsByType<GameObject>(FindObjectsSortMode.None))
{
string name = go.name;
bool isRight = name.Contains("Right");
bool isLeft = name.Contains("Left");
if (!isRight && !isLeft) continue;
if (!name.Contains("Controller") && !name.Contains("Hand")) continue;
if (go.GetComponent<LineRenderer>() == null) continue;
DisableToolkitPointerComponents(go);
if (go.GetComponent<VRPointerController>() != null) continue;
var pointer = go.AddComponent<VRPointerController>();
var field = typeof(VRPointerController)
.GetField("isRightHand",
System.Reflection.BindingFlags.NonPublic |
System.Reflection.BindingFlags.Instance);
field?.SetValue(pointer, isRight);
// Game 씬에서는 게임오버 전까지 비활성
if (disabledByDefault)
pointer.enabled = false;
}
}
private static void DisableToolkitPointerComponents(GameObject go)
{
var rayInteractor = go.GetComponent<UnityEngine.XR.Interaction.Toolkit.Interactors.XRRayInteractor>();
if (rayInteractor != null)
rayInteractor.enabled = false;
var lineVisual = go.GetComponent<UnityEngine.XR.Interaction.Toolkit.Interactors.Visuals.XRInteractorLineVisual>();
if (lineVisual != null)
lineVisual.enabled = false;
}
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 38f89babd4e99734aac47edbc4f87aa3
@@ -44,28 +44,6 @@ MonoBehaviour:
balance: balance:
m_OverrideState: 1 m_OverrideState: 1
m_Value: 0 m_Value: 0
--- !u!114 &-8104416584915340131
MonoBehaviour:
m_ObjectHideFlags: 3
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 0}
m_Name: CopyPasteTestComponent2
m_EditorClassIdentifier: Unity.RenderPipelines.Core.Editor.Tests:UnityEditor.Rendering.Tests:VolumeComponentCopyPasteTests/CopyPasteTestComponent2
active: 1
p1:
m_OverrideState: 1
m_Value: 0
p2:
m_OverrideState: 1
m_Value: 0
p21:
m_OverrideState: 1
m_Value: 0
--- !u!114 &-7750755424749557576 --- !u!114 &-7750755424749557576
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 3 m_ObjectHideFlags: 3
@@ -239,19 +217,6 @@ MonoBehaviour:
maxNits: maxNits:
m_OverrideState: 1 m_OverrideState: 1
m_Value: 1000 m_Value: 1000
--- !u!114 &-5360449096862653589
MonoBehaviour:
m_ObjectHideFlags: 3
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 0}
m_Name: VolumeComponentSupportedEverywhere
m_EditorClassIdentifier: Unity.RenderPipelines.Core.Editor.Tests:UnityEngine.Rendering.Tests:VolumeComponentEditorSupportedOnTests/VolumeComponentSupportedEverywhere
active: 1
--- !u!114 &-5139089513906902183 --- !u!114 &-5139089513906902183
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 3 m_ObjectHideFlags: 3
@@ -408,28 +373,6 @@ MonoBehaviour:
tint: tint:
m_OverrideState: 1 m_OverrideState: 1
m_Value: 0 m_Value: 0
--- !u!114 &-581120513425526550
MonoBehaviour:
m_ObjectHideFlags: 3
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 0}
m_Name: CopyPasteTestComponent3
m_EditorClassIdentifier: Unity.RenderPipelines.Core.Editor.Tests:UnityEditor.Rendering.Tests:VolumeComponentCopyPasteTests/CopyPasteTestComponent3
active: 1
p1:
m_OverrideState: 1
m_Value: 0
p2:
m_OverrideState: 1
m_Value: 0
p31:
m_OverrideState: 1
m_Value: {r: 0, g: 0, b: 0, a: 1}
--- !u!114 &11400000 --- !u!114 &11400000
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@@ -852,19 +795,6 @@ MonoBehaviour:
intensity: intensity:
m_OverrideState: 1 m_OverrideState: 1
m_Value: 0 m_Value: 0
--- !u!114 &6940869943325143175
MonoBehaviour:
m_ObjectHideFlags: 3
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 0}
m_Name: VolumeComponentSupportedOnAnySRP
m_EditorClassIdentifier: Unity.RenderPipelines.Core.Editor.Tests:UnityEngine.Rendering.Tests:VolumeComponentEditorSupportedOnTests/VolumeComponentSupportedOnAnySRP
active: 1
--- !u!114 &7173750748008157695 --- !u!114 &7173750748008157695
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 3 m_ObjectHideFlags: 3
@@ -961,22 +891,3 @@ MonoBehaviour:
blueOutBlueIn: blueOutBlueIn:
m_OverrideState: 1 m_OverrideState: 1
m_Value: 100 m_Value: 100
--- !u!114 &9122958982931076880
MonoBehaviour:
m_ObjectHideFlags: 3
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 0}
m_Name: CopyPasteTestComponent1
m_EditorClassIdentifier: Unity.RenderPipelines.Core.Editor.Tests:UnityEditor.Rendering.Tests:VolumeComponentCopyPasteTests/CopyPasteTestComponent1
active: 1
p1:
m_OverrideState: 1
m_Value: 0
p2:
m_OverrideState: 1
m_Value: 0
@@ -0,0 +1,6 @@
{
"host": "http://192.168.55.3:5000",
"publicHost": "http://whdwo798.synology.me",
"account": "",
"password": ""
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 5f6f642b36f74dc5a0f44793fa605c2e
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
File diff suppressed because one or more lines are too long
@@ -33,7 +33,8 @@ MonoBehaviour:
m_defaultTextMeshProUITextContainerSize: {x: 200, y: 50} m_defaultTextMeshProUITextContainerSize: {x: 200, y: 50}
m_autoSizeTextContainer: 0 m_autoSizeTextContainer: 0
m_IsTextObjectScaleStatic: 0 m_IsTextObjectScaleStatic: 0
m_fallbackFontAssets: [] m_fallbackFontAssets:
- {fileID: 11400000, guid: f6c6fe0f3c5912a43a8a6707e336d2ea, type: 2}
m_matchMaterialPreset: 1 m_matchMaterialPreset: 1
m_HideSubTextObjects: 1 m_HideSubTextObjects: 1
m_defaultSpriteAsset: {fileID: 11400000, guid: c41005c129ba4d66911b75229fd70b45, m_defaultSpriteAsset: {fileID: 11400000, guid: c41005c129ba4d66911b75229fd70b45,
+168
View File
@@ -0,0 +1,168 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 8
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: SkyBox
m_Shader: {fileID: 108, guid: 0000000000000000f000000000000000, type: 0}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords: []
m_InvalidKeywords:
- _MAPPING_LATITUDE_LONGITUDE_LAYOUT
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap: {}
disabledShaderPasses:
- MOTIONVECTORS
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _BackTex:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _BaseMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _BumpMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailAlbedoMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailMask:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailNormalMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DownTex:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _EmissionMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _FrontTex:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _LeftTex:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _MainTex:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _MetallicGlossMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _OcclusionMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _ParallaxMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _RightTex:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _SpecGlossMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _UpTex:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- unity_Lightmaps:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- unity_LightmapsInd:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- unity_ShadowMasks:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _AddPrecomputedVelocity: 0
- _AlphaClip: 0
- _AlphaToMask: 0
- _Blend: 0
- _BlendModePreserveSpecular: 1
- _BumpScale: 1
- _ClearCoatMask: 0
- _ClearCoatSmoothness: 0
- _Cull: 2
- _Cutoff: 0.5
- _DetailAlbedoMapScale: 1
- _DetailNormalMapScale: 1
- _DstBlend: 0
- _DstBlendAlpha: 0
- _EnvironmentReflections: 1
- _Exposure: 1
- _GlossMapScale: 0
- _Glossiness: 0
- _GlossyReflections: 0
- _ImageType: 0
- _Layout: 0
- _Mapping: 1
- _Metallic: 0
- _MirrorOnBack: 0
- _OcclusionStrength: 1
- _Parallax: 0.005
- _QueueOffset: 0
- _ReceiveShadows: 1
- _Rotation: 0
- _Smoothness: 0.5
- _SmoothnessTextureChannel: 0
- _SpecularHighlights: 1
- _SrcBlend: 1
- _SrcBlendAlpha: 1
- _Surface: 0
- _WorkflowMode: 1
- _XRMotionVectorsPass: 1
- _ZWrite: 1
m_Colors:
- _BaseColor: {r: 1, g: 1, b: 1, a: 1}
- _Color: {r: 1, g: 1, b: 1, a: 1}
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
- _SpecColor: {r: 0.2, g: 0.2, b: 0.2, a: 1}
- _Tint: {r: 0.5, g: 0.5, b: 0.5, a: 0.5}
m_BuildTextureStacks: []
m_AllowLocking: 1
--- !u!114 &5645475041611047199
MonoBehaviour:
m_ObjectHideFlags: 11
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
version: 10
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: fecd661b14876064fa838cbb52ca425e
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:
@@ -11,7 +11,7 @@ namespace DamageSystem
OnDamage.AddListener( ApplyImpactForce ); OnDamage.AddListener( ApplyImpactForce );
} }
private void ApplyImpactForce(DamageInfo info, DamageablePart damageable) protected override void ApplyImpactForce(DamageInfo info, DamageablePart damageable)
{ {
//if this AI if no dead it is being controlled by the Animator so dont apply any impact force //if this AI if no dead it is being controlled by the Animator so dont apply any impact force
if (!IsDead) if (!IsDead)
@@ -28,4 +28,3 @@ namespace DamageSystem
} }
} }
} }
@@ -14,7 +14,7 @@ namespace VRSDK.Climbing
{ {
base.Start(); base.Start();
target = FindObjectOfType<ClimbingTarget>(); target = FindFirstObjectByType<ClimbingTarget>();
onGrabStateChange.AddListener( OnGrabStateChangeClimb ); onGrabStateChange.AddListener( OnGrabStateChangeClimb );
} }
@@ -89,4 +89,3 @@ namespace VRSDK.Climbing
} }
@@ -1,5 +1,6 @@
using UnityEngine; using UnityEngine;
using UnityEditor; using UnityEditor;
using UnityEditor.Build;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@@ -193,8 +194,8 @@ namespace VRSDK.EditorCode
public static void ForceRebuild() public static void ForceRebuild()
{ {
string[] rebuildSymbols = { "RebuildToggle1", "RebuildToggle2" }; string[] rebuildSymbols = { "RebuildToggle1", "RebuildToggle2" };
string definesString = PlayerSettings.GetScriptingDefineSymbolsForGroup( NamedBuildTarget buildTarget = NamedBuildTarget.FromBuildTargetGroup(EditorUserBuildSettings.selectedBuildTargetGroup);
EditorUserBuildSettings.selectedBuildTargetGroup ); string definesString = PlayerSettings.GetScriptingDefineSymbols(buildTarget);
var definesStringTemp = definesString; var definesStringTemp = definesString;
if (definesStringTemp.Contains( rebuildSymbols[0] )) if (definesStringTemp.Contains( rebuildSymbols[0] ))
{ {
@@ -208,14 +209,9 @@ namespace VRSDK.EditorCode
{ {
definesStringTemp += ";" + rebuildSymbols[0]; definesStringTemp += ";" + rebuildSymbols[0];
} }
PlayerSettings.SetScriptingDefineSymbolsForGroup( PlayerSettings.SetScriptingDefineSymbols(buildTarget, definesStringTemp);
EditorUserBuildSettings.selectedBuildTargetGroup, PlayerSettings.SetScriptingDefineSymbols(buildTarget, definesString);
definesStringTemp );
PlayerSettings.SetScriptingDefineSymbolsForGroup(
EditorUserBuildSettings.selectedBuildTargetGroup,
definesString );
} }
} }
} }
@@ -15,10 +15,6 @@ namespace VRSDK.EditorCode
private HandVisualizerTool targetScript = null; private HandVisualizerTool targetScript = null;
private static string returnScenePath = null;
private static Scene returnScene;
private static bool inPreviewMode = false;
private void Awake() private void Awake()
{ {
/* /*
@@ -95,4 +91,3 @@ namespace VRSDK.EditorCode
} }
} }
@@ -26,15 +26,15 @@ namespace VRSDK.EditorCode
{ {
Scene originalScene = EditorSceneManager.GetActiveScene(); Scene originalScene = EditorSceneManager.GetActiveScene();
originalScenePath = EditorApplication.currentScene; originalScenePath = originalScene.path;
Scene previewScene = EditorSceneManager.NewScene( NewSceneSetup.DefaultGameObjects, NewSceneMode.Additive ); Scene previewScene = EditorSceneManager.NewScene( NewSceneSetup.DefaultGameObjects, NewSceneMode.Additive );
EditorSceneManager.MoveGameObjectToScene( clone, previewScene ); EditorSceneManager.MoveGameObjectToScene( clone, previewScene );
EditorSceneManager.UnloadSceneAsync( originalScene ); EditorSceneManager.UnloadSceneAsync( originalScene );
previewModeEnable = true; previewModeEnable = true;
inspectedGrabbable = GameObject.FindObjectOfType<VR_Grabbable>(); inspectedGrabbable = GameObject.FindFirstObjectByType<VR_Grabbable>();
activeController = GameObject.FindObjectOfType<VR_Controller>(); activeController = GameObject.FindFirstObjectByType<VR_Controller>();
OverrideGrabAnimation(); OverrideGrabAnimation();
@@ -55,7 +55,7 @@ namespace VRSDK.EditorCode
{ {
handPreviewSave = new HandPreviewSave( inspectedGrabbable ); handPreviewSave = new HandPreviewSave( inspectedGrabbable );
ExitPreviewMode(); ExitPreviewMode();
GameObjectMarker marker = GameObject.FindObjectOfType<GameObjectMarker>(); GameObjectMarker marker = GameObject.FindFirstObjectByType<GameObjectMarker>();
handPreviewSave.LoadInto(marker.GetComponent<VR_Grabbable>()); handPreviewSave.LoadInto(marker.GetComponent<VR_Grabbable>());
GameObject.DestroyImmediate(marker); GameObject.DestroyImmediate(marker);
} }
@@ -131,4 +131,3 @@ namespace VRSDK.EditorCode
} }
@@ -23,7 +23,7 @@ namespace VRSDK
private void Start() private void Start()
{ {
Player player = FindObjectOfType<Player>(); Player player = FindFirstObjectByType<Player>();
gameOverScreenFader = player.GameOverScreenFader; gameOverScreenFader = player.GameOverScreenFader;
} }
@@ -123,4 +123,3 @@ namespace VRSDK
} }
} }
@@ -16,7 +16,7 @@ namespace VRSDK
private void Start() private void Start()
{ {
characterController = FindObjectOfType<VR_CharacterController>(); characterController = FindFirstObjectByType<VR_CharacterController>();
if (characterController != null) if (characterController != null)
{ {
@@ -25,7 +25,7 @@ namespace VRSDK
if (anchorPoint == null) if (anchorPoint == null)
{ {
Player player = FindObjectOfType<Player>(); Player player = FindFirstObjectByType<Player>();
anchorPoint = player.PocketsAnchorPoint; anchorPoint = player.PocketsAnchorPoint;
} }
@@ -36,7 +36,7 @@ namespace VRSDK
private void SetTeleportCallback() private void SetTeleportCallback()
{ {
VR_TeleportHandler teleportHandler = FindObjectOfType<VR_TeleportHandler>(); VR_TeleportHandler teleportHandler = FindFirstObjectByType<VR_TeleportHandler>();
if (teleportHandler != null) if (teleportHandler != null)
{ {
@@ -102,4 +102,3 @@ namespace VRSDK
} }
} }
@@ -22,7 +22,7 @@ namespace Platinio
{ {
//get all the singletones //get all the singletones
T[] singletons = GameObject.FindObjectsOfType( typeof(T) ) as T[]; T[] singletons = FindObjectsByType<T>(FindObjectsSortMode.None);
if(singletons != null) if(singletons != null)
{ {
@@ -18,10 +18,9 @@ namespace VRSDK
{ {
for (int n = 0; n < wallCubePartArray.Length; n++) for (int n = 0; n < wallCubePartArray.Length; n++)
{ {
wallCubePartArray[n].Reset( resetTime ); wallCubePartArray[n].ResetPart( resetTime );
} }
} }
} }
} }
@@ -17,7 +17,7 @@ namespace VRSDK
rb = GetComponent<Rigidbody>(); rb = GetComponent<Rigidbody>();
} }
public void Reset(float t) public void ResetPart(float t)
{ {
rb.isKinematic = true; rb.isKinematic = true;
@@ -64,4 +64,3 @@ namespace VRSDK
} }
} }
@@ -49,7 +49,7 @@ namespace VRSDK
public HandPhysics(HistoryBuffer buffer) public HandPhysics(HistoryBuffer buffer)
{ {
historyBuffer = buffer; historyBuffer = buffer;
characterController = MonoBehaviour.FindObjectOfType<VR_CharacterController>(); characterController = MonoBehaviour.FindFirstObjectByType<VR_CharacterController>();
trackingSpace = VR_Manager.instance.Player.TrackingSpace; trackingSpace = VR_Manager.instance.Player.TrackingSpace;
@@ -148,4 +148,3 @@ namespace VRSDK
} }
} }
@@ -95,17 +95,17 @@ namespace VRSDK
return GetAxis1D(button) > 0.25f; return GetAxis1D(button) > 0.25f;
case VR_InputButton.Primary: case VR_InputButton.Primary:
#if UNITY_XR #if UNITY_XR
thisInputDevice.IsPressed(InputHelpers.Button.PrimaryButton, out value); thisInputDevice.TryGetFeatureValue(CommonUsages.primaryButton, out value);
#endif #endif
break; break;
case VR_InputButton.Secondary: case VR_InputButton.Secondary:
#if UNITY_XR #if UNITY_XR
thisInputDevice.IsPressed(InputHelpers.Button.SecondaryButton, out value); thisInputDevice.TryGetFeatureValue(CommonUsages.secondaryButton, out value);
#endif #endif
break; break;
case VR_InputButton.TumbstickPress: case VR_InputButton.TumbstickPress:
#if UNITY_XR #if UNITY_XR
thisInputDevice.IsPressed(InputHelpers.Button.Primary2DAxisClick, out value); thisInputDevice.TryGetFeatureValue(CommonUsages.primary2DAxisClick, out value);
#endif #endif
break; break;
} }
@@ -158,4 +158,3 @@ namespace VRSDK
} }
} }
@@ -1,16 +1,19 @@
using UnityEngine; using UnityEngine;
using UnityEngine.Serialization;
namespace VRSDK.Locomotion namespace VRSDK.Locomotion
{ {
//this scripts handles aim marker position and rotation //this scripts handles aim marker position and rotation
public class VR_AimMarker : MonoBehaviour public class VR_AimMarker : MonoBehaviour
{ {
[SerializeField] private GameObject marker = null; [SerializeField] private GameObject marker = null;
[SerializeField] private BoxCollider collider = null; [FormerlySerializedAs("collider")]
[SerializeField] private BoxCollider markerCollider = null;
[SerializeField] private float slopeLimit; [SerializeField] private float slopeLimit;
public GameObject Marker { get { return marker; } } public GameObject Marker { get { return marker; } }
public BoxCollider Collider { get { return collider; } } public BoxCollider Collider { get { return markerCollider; } }
public float SlopeLimit { get { return slopeLimit; } } public float SlopeLimit { get { return slopeLimit; } }
private void Awake() private void Awake()
@@ -52,4 +55,3 @@ namespace VRSDK.Locomotion
} }
} }
@@ -88,11 +88,13 @@ namespace VRSDK
[NonSerialized] [NonSerialized]
public float CameraHeight; public float CameraHeight;
#if SDK_OCULUS
/// <summary> /// <summary>
/// This event is raised after the character controller is moved. This is used by the OVRAvatarLocomotion script to keep the avatar transform synchronized /// This event is raised after the character controller is moved. This is used by the OVRAvatarLocomotion script to keep the avatar transform synchronized
/// with the OVRPlayerController. /// with the OVRPlayerController.
/// </summary> /// </summary>
public event Action<Transform> TransformUpdated; public event Action<Transform> TransformUpdated;
#endif
/// <summary> /// <summary>
/// This bool is set to true whenever the player controller has been teleported. It is reset after every frame. Some systems, such as /// This bool is set to true whenever the player controller has been teleported. It is reset after every frame. Some systems, such as
@@ -177,7 +179,9 @@ namespace VRSDK
private float SimulationRate = 60f; private float SimulationRate = 60f;
private float buttonRotation = 0f; private float buttonRotation = 0f;
private bool ReadyToSnapTurn; // Set to true when a snap turn has occurred, code requires one frame of centered thumbstick to enable another snap turn. private bool ReadyToSnapTurn; // Set to true when a snap turn has occurred, code requires one frame of centered thumbstick to enable another snap turn.
#if SDK_OCULUS
private bool playerControllerEnabled = false; private bool playerControllerEnabled = false;
#endif
private float moveInfluence = 0.0f; private float moveInfluence = 0.0f;
public Vector3 lastPosition = Vector3.zero; public Vector3 lastPosition = Vector3.zero;
private Vector3 velocity = Vector3.zero; private Vector3 velocity = Vector3.zero;
@@ -409,8 +413,6 @@ namespace VRSDK
moveRight = Input.GetKey(KeyCode.D) || Input.GetKey(KeyCode.RightArrow); moveRight = Input.GetKey(KeyCode.D) || Input.GetKey(KeyCode.RightArrow);
moveBack = Input.GetKey(KeyCode.S) || Input.GetKey(KeyCode.DownArrow); moveBack = Input.GetKey(KeyCode.S) || Input.GetKey(KeyCode.DownArrow);
#endif #endif
bool dpad_move = false;
MoveScale = 1.0f; MoveScale = 1.0f;
if ((moveForward && moveLeft) || (moveForward && moveRight) || (moveBack && moveLeft) || (moveBack && moveRight)) if ((moveForward && moveLeft) || (moveForward && moveRight) || (moveBack && moveLeft) || (moveBack && moveRight))
@@ -427,6 +429,7 @@ namespace VRSDK
// Run! // Run!
#if !UNITY_XR #if !UNITY_XR
bool dpad_move = false;
if (dpad_move || Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift)) if (dpad_move || Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift))
moveInfluence *= 2.0f; moveInfluence *= 2.0f;
#endif #endif
@@ -713,4 +716,3 @@ namespace VRSDK
} }
} }
@@ -172,7 +172,7 @@ namespace VRSDK
private void FindOrCreate_VR_Manager() private void FindOrCreate_VR_Manager()
{ {
if (FindObjectOfType<VR_Manager>() == null) if (FindFirstObjectByType<VR_Manager>() == null)
{ {
Debug.LogError("you need a VR_Manager active in the scene in order to use VR Shooter Kit"); Debug.LogError("you need a VR_Manager active in the scene in order to use VR Shooter Kit");
} }
@@ -630,4 +630,3 @@ namespace VRSDK
} }
} }
@@ -18,8 +18,6 @@ namespace VRSDK
{ {
private float minAcelerationThreshold = 0.0f; private float minAcelerationThreshold = 0.0f;
private float maxAcelerationThreshold = 0.0f; private float maxAcelerationThreshold = 0.0f;
private int sampleCount = 0;
private VR_Controller controller = null; private VR_Controller controller = null;
private GesturePhase rotationGesturePhase = GesturePhase.Tracking; private GesturePhase rotationGesturePhase = GesturePhase.Tracking;
private Quaternion rotationGesturefromQuaternion = Quaternion.identity; private Quaternion rotationGesturefromQuaternion = Quaternion.identity;
@@ -128,4 +126,3 @@ namespace VRSDK
} }
} }
@@ -55,8 +55,6 @@ namespace VRSDK
protected bool preventDefault = false; protected bool preventDefault = false;
protected float velocityChangeThreshold = 10f; protected float velocityChangeThreshold = 10f;
protected float angularVelocityChangeThreshold = 20f; protected float angularVelocityChangeThreshold = 20f;
private bool previousUseGravityState = false;
private bool previousGravityState = false;
private VR_Controller lastInteractController = null; private VR_Controller lastInteractController = null;
private bool objectWasThrow = false; private bool objectWasThrow = false;
protected bool canUseDropZone = true; protected bool canUseDropZone = true;
@@ -34,6 +34,7 @@ namespace VRSDK
#region PUBLIC #region PUBLIC
public VR_SDK CurrentSDK { get { return currentSDK; } } public VR_SDK CurrentSDK { get { return currentSDK; } }
public ControllerGestureConfig GestureConfig { get { return gestureConfig; } }
public List<VR_Interactable> InteractList { get { return interactList; } } public List<VR_Interactable> InteractList { get { return interactList; } }
public List<VR_Highlight> HighlightList { get { return highlightList; } } public List<VR_Highlight> HighlightList { get { return highlightList; } }
public List<VR_Grabbable> GrabbableList { get { return grabbableList; } } public List<VR_Grabbable> GrabbableList { get { return grabbableList; } }
@@ -43,7 +44,7 @@ namespace VRSDK
{ {
if (player == null) if (player == null)
{ {
player = FindObjectOfType<VR_Player>(); player = FindFirstObjectByType<VR_Player>();
player.Construct(); player.Construct();
} }
@@ -140,4 +141,3 @@ namespace VRSDK
} }
} }
@@ -23,7 +23,7 @@ namespace VRSDK
{ {
get get
{ {
return FindObjectOfType<CharacterController>(); return FindFirstObjectByType<CharacterController>();
} }
} }
@@ -109,4 +109,3 @@ namespace VRSDK
} }
} }
@@ -13,7 +13,6 @@ namespace VRSDK
public float SpeedModifier { get { return speedModifier; } } public float SpeedModifier { get { return speedModifier; } }
public float AngularSpeedModifier { get { return aungularSpeedModifier; } } public float AngularSpeedModifier { get { return aungularSpeedModifier; } }
bool throwed = false;
private void Awake() private void Awake()
{ {
@@ -39,4 +38,3 @@ namespace VRSDK
} }
} }
@@ -76,7 +76,7 @@ MonoBehaviour:
m_PrefabInstance: {fileID: 0} m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0} m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 748387694296687208} m_GameObject: {fileID: 748387694296687208}
m_Enabled: 1 m_Enabled: 0
m_EditorHideFlags: 0 m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 6803edce0201f574f923fd9d10e5b30a, type: 3} m_Script: {fileID: 11500000, guid: 6803edce0201f574f923fd9d10e5b30a, type: 3}
m_Name: m_Name:
@@ -170,7 +170,7 @@ MonoBehaviour:
m_AllowHoveredActivate: 0 m_AllowHoveredActivate: 0
m_TargetPriorityMode: 0 m_TargetPriorityMode: 0
m_HideControllerOnSelect: 0 m_HideControllerOnSelect: 0
m_InputCompatibilityMode: 0 m_InputCompatibilityMode: 2
m_PlayAudioClipOnSelectEntered: 0 m_PlayAudioClipOnSelectEntered: 0
m_AudioClipForOnSelectEntered: {fileID: 0} m_AudioClipForOnSelectEntered: {fileID: 0}
m_PlayAudioClipOnSelectExited: 0 m_PlayAudioClipOnSelectExited: 0
@@ -251,7 +251,7 @@ MonoBehaviour:
m_OccludeARHitsWith2DObjects: 0 m_OccludeARHitsWith2DObjects: 0
m_ScaleMode: 0 m_ScaleMode: 0
m_UIPressInput: m_UIPressInput:
m_InputSourceMode: 2 m_InputSourceMode: 1
m_InputActionPerformed: m_InputActionPerformed:
m_Name: UI Press m_Name: UI Press
m_Type: 1 m_Type: 1
@@ -259,7 +259,15 @@ MonoBehaviour:
m_Id: 0630124c-531b-4795-acfe-690be0b93310 m_Id: 0630124c-531b-4795-acfe-690be0b93310
m_Processors: m_Processors:
m_Interactions: m_Interactions:
m_SingletonActionBindings: [] m_SingletonActionBindings:
- m_Name:
m_Id: a1b2c3d4-1111-2222-3333-444455556666
m_Path: <XRController>{RightHand}/triggerPressed
m_Interactions:
m_Processors:
m_Groups:
m_Action: UI Press
m_Flags: 0
m_Flags: 0 m_Flags: 0
m_InputActionValue: m_InputActionValue:
m_Name: UI Press Value m_Name: UI Press Value
@@ -520,7 +528,7 @@ MonoBehaviour:
m_PrefabInstance: {fileID: 0} m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0} m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 748387694296687208} m_GameObject: {fileID: 748387694296687208}
m_Enabled: 1 m_Enabled: 0
m_EditorHideFlags: 0 m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: e988983f96fe1dd48800bcdfc82f23e9, type: 3} m_Script: {fileID: 11500000, guid: e988983f96fe1dd48800bcdfc82f23e9, type: 3}
m_Name: m_Name:
@@ -752,7 +760,7 @@ MonoBehaviour:
m_PrefabInstance: {fileID: 0} m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0} m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 748387694868252494} m_GameObject: {fileID: 748387694868252494}
m_Enabled: 1 m_Enabled: 0
m_EditorHideFlags: 0 m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 6803edce0201f574f923fd9d10e5b30a, type: 3} m_Script: {fileID: 11500000, guid: 6803edce0201f574f923fd9d10e5b30a, type: 3}
m_Name: m_Name:
@@ -846,7 +854,7 @@ MonoBehaviour:
m_AllowHoveredActivate: 0 m_AllowHoveredActivate: 0
m_TargetPriorityMode: 0 m_TargetPriorityMode: 0
m_HideControllerOnSelect: 0 m_HideControllerOnSelect: 0
m_InputCompatibilityMode: 0 m_InputCompatibilityMode: 2
m_PlayAudioClipOnSelectEntered: 0 m_PlayAudioClipOnSelectEntered: 0
m_AudioClipForOnSelectEntered: {fileID: 0} m_AudioClipForOnSelectEntered: {fileID: 0}
m_PlayAudioClipOnSelectExited: 0 m_PlayAudioClipOnSelectExited: 0
@@ -927,7 +935,7 @@ MonoBehaviour:
m_OccludeARHitsWith2DObjects: 0 m_OccludeARHitsWith2DObjects: 0
m_ScaleMode: 0 m_ScaleMode: 0
m_UIPressInput: m_UIPressInput:
m_InputSourceMode: 2 m_InputSourceMode: 1
m_InputActionPerformed: m_InputActionPerformed:
m_Name: UI Press m_Name: UI Press
m_Type: 1 m_Type: 1
@@ -935,7 +943,15 @@ MonoBehaviour:
m_Id: bc4a4f29-0c8c-48eb-b453-4183b34bc238 m_Id: bc4a4f29-0c8c-48eb-b453-4183b34bc238
m_Processors: m_Processors:
m_Interactions: m_Interactions:
m_SingletonActionBindings: [] m_SingletonActionBindings:
- m_Name:
m_Id: b2c3d4e5-aaaa-bbbb-cccc-ddddeeeeffff
m_Path: <XRController>{LeftHand}/triggerPressed
m_Interactions:
m_Processors:
m_Groups:
m_Action: UI Press
m_Flags: 0
m_Flags: 0 m_Flags: 0
m_InputActionValue: m_InputActionValue:
m_Name: UI Press Value m_Name: UI Press Value
@@ -1196,7 +1212,7 @@ MonoBehaviour:
m_PrefabInstance: {fileID: 0} m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0} m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 748387694868252494} m_GameObject: {fileID: 748387694868252494}
m_Enabled: 1 m_Enabled: 0
m_EditorHideFlags: 0 m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: e988983f96fe1dd48800bcdfc82f23e9, type: 3} m_Script: {fileID: 11500000, guid: e988983f96fe1dd48800bcdfc82f23e9, type: 3}
m_Name: m_Name:
@@ -411,7 +411,7 @@ MonoBehaviour:
m_PrefabInstance: {fileID: 0} m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0} m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8546668446538128435} m_GameObject: {fileID: 8546668446538128435}
m_Enabled: 1 m_Enabled: 0
m_EditorHideFlags: 0 m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: e988983f96fe1dd48800bcdfc82f23e9, type: 3} m_Script: {fileID: 11500000, guid: e988983f96fe1dd48800bcdfc82f23e9, type: 3}
m_Name: m_Name:
@@ -1003,7 +1003,7 @@ MonoBehaviour:
m_PrefabInstance: {fileID: 0} m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0} m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8546668447105775893} m_GameObject: {fileID: 8546668447105775893}
m_Enabled: 1 m_Enabled: 0
m_EditorHideFlags: 0 m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: e988983f96fe1dd48800bcdfc82f23e9, type: 3} m_Script: {fileID: 11500000, guid: e988983f96fe1dd48800bcdfc82f23e9, type: 3}
m_Name: m_Name:
@@ -1295,7 +1295,7 @@ Camera:
m_GameObject: {fileID: 8546668447772986810} m_GameObject: {fileID: 8546668447772986810}
m_Enabled: 1 m_Enabled: 1
serializedVersion: 2 serializedVersion: 2
m_ClearFlags: 2 m_ClearFlags: 1
m_BackGroundColor: {r: 0, g: 0, b: 0, a: 0} m_BackGroundColor: {r: 0, g: 0, b: 0, a: 0}
m_projectionMatrixMode: 1 m_projectionMatrixMode: 1
m_GateFitMode: 2 m_GateFitMode: 2
@@ -342,7 +342,7 @@ MonoBehaviour:
handSettings: handSettings:
interactPoint: {fileID: 3074267110786978836} interactPoint: {fileID: 3074267110786978836}
highlightPoint: {fileID: 3074267110786978836} highlightPoint: {fileID: 3074267110786978836}
rotationOffset: {x: 0, y: 90, z: 25} rotationOffset: {x: 25, y: 0, z: 0}
canInteract: 1 canInteract: 1
rightHandAnimationSettings: rightHandAnimationSettings:
animation: {fileID: 0} animation: {fileID: 0}
@@ -522,7 +522,7 @@ MonoBehaviour:
m_GameObject: {fileID: 5575416034875238503} m_GameObject: {fileID: 5575416034875238503}
m_Enabled: 1 m_Enabled: 1
m_EditorHideFlags: 0 m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: c68346ff56573a1429560a527ad447e0, type: 3} m_Script: {fileID: 11500000, guid: 4de824eda67bd1c4ba4d379a9debd2b3, type: 3}
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
fastCollisionListener: {fileID: 5407220909436794986} fastCollisionListener: {fileID: 5407220909436794986}
@@ -533,6 +533,7 @@ MonoBehaviour:
hitForce: 0 hitForce: 0
maxHitForce: 0 maxHitForce: 0
canDismember: 0 canDismember: 0
colorSide: 1
--- !u!114 &5407220909436794986 --- !u!114 &5407220909436794986
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
+101 -88
View File
@@ -156,8 +156,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -18, y: 19.5} m_AnchoredPosition: {x: -19, y: 19.5}
m_SizeDelta: {x: 30, y: 7} m_SizeDelta: {x: 34, y: 7}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &11933137 --- !u!114 &11933137
MonoBehaviour: MonoBehaviour:
@@ -456,8 +456,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 4} m_AnchoredPosition: {x: 0, y: 4.2}
m_SizeDelta: {x: 50, y: 0.4} m_SizeDelta: {x: 68, y: 0.4}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &97527770 --- !u!114 &97527770
MonoBehaviour: MonoBehaviour:
@@ -652,7 +652,7 @@ MonoBehaviour:
m_spriteAsset: {fileID: 0} m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0 m_tintAllSprites: 0
m_StyleSheet: {fileID: 0} m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: 0 m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0 m_overrideHtmlColors: 0
m_faceColor: m_faceColor:
serializedVersion: 2 serializedVersion: 2
@@ -1267,8 +1267,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 12} m_AnchoredPosition: {x: -16.05, y: 10.8}
m_SizeDelta: {x: 50, y: 6} m_SizeDelta: {x: 42, y: 4.8}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &285384529 --- !u!114 &285384529
MonoBehaviour: MonoBehaviour:
@@ -1290,7 +1290,7 @@ MonoBehaviour:
m_OnCullStateChanged: m_OnCullStateChanged:
m_PersistentCalls: m_PersistentCalls:
m_Calls: [] m_Calls: []
m_text: m_text: Anesthesia
m_isRightToLeft: 0 m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
@@ -1299,7 +1299,7 @@ MonoBehaviour:
m_fontMaterials: [] m_fontMaterials: []
m_fontColor32: m_fontColor32:
serializedVersion: 2 serializedVersion: 2
rgba: 4294967295 rgba: 3439329279
m_fontColor: {r: 1, g: 1, b: 1, a: 0.8} m_fontColor: {r: 1, g: 1, b: 1, a: 0.8}
m_enableVertexGradient: 0 m_enableVertexGradient: 0
m_colorMode: 3 m_colorMode: 3
@@ -1312,7 +1312,7 @@ MonoBehaviour:
m_spriteAsset: {fileID: 0} m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0 m_tintAllSprites: 0
m_StyleSheet: {fileID: 0} m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: 0 m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0 m_overrideHtmlColors: 0
m_faceColor: m_faceColor:
serializedVersion: 2 serializedVersion: 2
@@ -1324,7 +1324,7 @@ MonoBehaviour:
m_fontSizeMin: 18 m_fontSizeMin: 18
m_fontSizeMax: 72 m_fontSizeMax: 72
m_fontStyle: 0 m_fontStyle: 0
m_HorizontalAlignment: 2 m_HorizontalAlignment: 1
m_VerticalAlignment: 512 m_VerticalAlignment: 512
m_textAlignment: 65535 m_textAlignment: 65535
m_characterSpacing: 0 m_characterSpacing: 0
@@ -1405,8 +1405,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -26.6, y: -9.4} m_AnchoredPosition: {x: -44, y: -9.4}
m_SizeDelta: {x: 52.7, y: 49} m_SizeDelta: {x: 56, y: 49}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!1 &313102848 --- !u!1 &313102848
GameObject: GameObject:
@@ -1625,7 +1625,7 @@ RectTransform:
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 0} m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 890, y: 666} m_SizeDelta: {x: 850, y: 800}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &359789290 --- !u!114 &359789290
MonoBehaviour: MonoBehaviour:
@@ -1647,7 +1647,7 @@ MonoBehaviour:
m_OnCullStateChanged: m_OnCullStateChanged:
m_PersistentCalls: m_PersistentCalls:
m_Calls: [] m_Calls: []
m_Sprite: {fileID: 21300000, guid: eb6ca18f47347e94aa2011847ccd1fa9, type: 3} m_Sprite: {fileID: 21300000, guid: 6765d030cfc5fc84e9523fcd9439be89, type: 3}
m_Type: 0 m_Type: 0
m_PreserveAspect: 0 m_PreserveAspect: 0
m_FillCenter: 1 m_FillCenter: 1
@@ -1682,7 +1682,7 @@ GameObject:
m_Icon: {fileID: 0} m_Icon: {fileID: 0}
m_NavMeshLayer: 0 m_NavMeshLayer: 0
m_StaticEditorFlags: 0 m_StaticEditorFlags: 0
m_IsActive: 1 m_IsActive: 0
--- !u!224 &365318615 --- !u!224 &365318615
RectTransform: RectTransform:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@@ -1830,16 +1830,16 @@ RectTransform:
m_GameObject: {fileID: 365636951} m_GameObject: {fileID: 365636951}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 0.9, y: 0.9, z: 0.9} m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 1 m_ConstrainProportionsScale: 0
m_Children: m_Children:
- {fileID: 1536039027} - {fileID: 1536039027}
m_Father: {fileID: 1223157292} m_Father: {fileID: 1223157292}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 12, y: -14} m_AnchoredPosition: {x: 17, y: -9.7}
m_SizeDelta: {x: 22, y: 7} m_SizeDelta: {x: 27, y: 6}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &365636953 --- !u!114 &365636953
MonoBehaviour: MonoBehaviour:
@@ -1957,7 +1957,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0.1, y: -9.4} m_AnchoredPosition: {x: -15, y: -9.4}
m_SizeDelta: {x: 0.5, y: 49} m_SizeDelta: {x: 0.5, y: 49}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &393692317 --- !u!114 &393692317
@@ -2300,16 +2300,16 @@ RectTransform:
m_GameObject: {fileID: 549476134} m_GameObject: {fileID: 549476134}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 0.9, y: 0.9, z: 0.9} m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 1 m_ConstrainProportionsScale: 0
m_Children: m_Children:
- {fileID: 138927897} - {fileID: 138927897}
m_Father: {fileID: 1223157292} m_Father: {fileID: 1223157292}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -12, y: -5} m_AnchoredPosition: {x: -17, y: -2}
m_SizeDelta: {x: 22, y: 7} m_SizeDelta: {x: 27, y: 6}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &549476136 --- !u!114 &549476136
MonoBehaviour: MonoBehaviour:
@@ -2428,7 +2428,7 @@ RectTransform:
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 15.5} m_AnchoredPosition: {x: 0, y: 15.5}
m_SizeDelta: {x: 104, y: 0.5} m_SizeDelta: {x: 144, y: 0.5}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &584186484 --- !u!114 &584186484
MonoBehaviour: MonoBehaviour:
@@ -2534,7 +2534,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0} m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1} m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: -0.54999924, y: 0.040000916} m_AnchoredPosition: {x: -0.55000305, y: 0.040000916}
m_SizeDelta: {x: 1.1, y: 3.12} m_SizeDelta: {x: 1.1, y: 3.12}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &624828134 --- !u!114 &624828134
@@ -2639,8 +2639,8 @@ RectTransform:
m_PrefabAsset: {fileID: 0} m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 661667650} m_GameObject: {fileID: 661667650}
m_LocalRotation: {x: 0, y: 0.38268343, z: 0, w: 0.92387956} m_LocalRotation: {x: 0, y: 0.38268343, z: 0, w: 0.92387956}
m_LocalPosition: {x: 0, y: 0, z: 19.76} m_LocalPosition: {x: 0, y: 0, z: 21.44}
m_LocalScale: {x: 0.25, y: 0.25, z: 1} m_LocalScale: {x: 0.21, y: 0.21, z: 1}
m_ConstrainProportionsScale: 0 m_ConstrainProportionsScale: 0
m_Children: m_Children:
- {fileID: 140294464} - {fileID: 140294464}
@@ -2652,7 +2652,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 45, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 45, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 24.54, y: 4.415} m_AnchoredPosition: {x: 28.85, y: 4.71}
m_SizeDelta: {x: 105.885, y: 71.226} m_SizeDelta: {x: 105.885, y: 71.226}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &661667652 --- !u!114 &661667652
@@ -2786,8 +2786,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 14, y: 19.5} m_AnchoredPosition: {x: 17.5, y: 19.5}
m_SizeDelta: {x: 30, y: 7} m_SizeDelta: {x: 34, y: 7}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &711973508 --- !u!114 &711973508
MonoBehaviour: MonoBehaviour:
@@ -3087,7 +3087,7 @@ MonoBehaviour:
m_spriteAsset: {fileID: 0} m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0 m_tintAllSprites: 0
m_StyleSheet: {fileID: 0} m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: 0 m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0 m_overrideHtmlColors: 0
m_faceColor: m_faceColor:
serializedVersion: 2 serializedVersion: 2
@@ -3224,7 +3224,7 @@ MonoBehaviour:
m_spriteAsset: {fileID: 0} m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0 m_tintAllSprites: 0
m_StyleSheet: {fileID: 0} m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: 0 m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0 m_overrideHtmlColors: 0
m_faceColor: m_faceColor:
serializedVersion: 2 serializedVersion: 2
@@ -3310,16 +3310,16 @@ RectTransform:
m_GameObject: {fileID: 848577108} m_GameObject: {fileID: 848577108}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 0.9, y: 0.9, z: 0.9} m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 1 m_ConstrainProportionsScale: 0
m_Children: m_Children:
- {fileID: 2121890138} - {fileID: 2121890138}
m_Father: {fileID: 1223157292} m_Father: {fileID: 1223157292}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 12, y: -5} m_AnchoredPosition: {x: 17, y: -2}
m_SizeDelta: {x: 22, y: 7} m_SizeDelta: {x: 27, y: 6}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &848577110 --- !u!114 &848577110
MonoBehaviour: MonoBehaviour:
@@ -3437,8 +3437,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 7} m_AnchoredPosition: {x: 20.5, y: 10.8}
m_SizeDelta: {x: 50, y: 5} m_SizeDelta: {x: 28, y: 4.8}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &861505383 --- !u!114 &861505383
MonoBehaviour: MonoBehaviour:
@@ -3460,7 +3460,7 @@ MonoBehaviour:
m_OnCullStateChanged: m_OnCullStateChanged:
m_PersistentCalls: m_PersistentCalls:
m_Calls: [] m_Calls: []
m_text: m_text: BPM 120
m_isRightToLeft: 0 m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
@@ -3469,7 +3469,7 @@ MonoBehaviour:
m_fontMaterials: [] m_fontMaterials: []
m_fontColor32: m_fontColor32:
serializedVersion: 2 serializedVersion: 2
rgba: 4294967295 rgba: 2583691263
m_fontColor: {r: 1, g: 1, b: 1, a: 0.6} m_fontColor: {r: 1, g: 1, b: 1, a: 0.6}
m_enableVertexGradient: 0 m_enableVertexGradient: 0
m_colorMode: 3 m_colorMode: 3
@@ -3482,7 +3482,7 @@ MonoBehaviour:
m_spriteAsset: {fileID: 0} m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0 m_tintAllSprites: 0
m_StyleSheet: {fileID: 0} m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: 0 m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0 m_overrideHtmlColors: 0
m_faceColor: m_faceColor:
serializedVersion: 2 serializedVersion: 2
@@ -3494,7 +3494,7 @@ MonoBehaviour:
m_fontSizeMin: 18 m_fontSizeMin: 18
m_fontSizeMax: 72 m_fontSizeMax: 72
m_fontStyle: 0 m_fontStyle: 0
m_HorizontalAlignment: 2 m_HorizontalAlignment: 1
m_VerticalAlignment: 512 m_VerticalAlignment: 512
m_textAlignment: 65535 m_textAlignment: 65535
m_characterSpacing: 0 m_characterSpacing: 0
@@ -3611,8 +3611,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -3, y: 18.5} m_AnchoredPosition: {x: -5.5, y: 17}
m_SizeDelta: {x: 38, y: 8} m_SizeDelta: {x: 63, y: 7.4}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &921515098 --- !u!114 &921515098
MonoBehaviour: MonoBehaviour:
@@ -3634,7 +3634,7 @@ MonoBehaviour:
m_OnCullStateChanged: m_OnCullStateChanged:
m_PersistentCalls: m_PersistentCalls:
m_Calls: [] m_Calls: []
m_text: --- m_text: Oxlo
m_isRightToLeft: 0 m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
@@ -3656,13 +3656,13 @@ MonoBehaviour:
m_spriteAsset: {fileID: 0} m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0 m_tintAllSprites: 0
m_StyleSheet: {fileID: 0} m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: 0 m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0 m_overrideHtmlColors: 0
m_faceColor: m_faceColor:
serializedVersion: 2 serializedVersion: 2
rgba: 4294967295 rgba: 4294967295
m_fontSize: 6.5 m_fontSize: 7.2
m_fontSizeBase: 6.5 m_fontSizeBase: 7.2
m_fontWeight: 400 m_fontWeight: 400
m_enableAutoSizing: 0 m_enableAutoSizing: 0
m_fontSizeMin: 18 m_fontSizeMin: 18
@@ -3817,16 +3817,16 @@ RectTransform:
m_GameObject: {fileID: 967100893} m_GameObject: {fileID: 967100893}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 0.9, y: 0.9, z: 0.9} m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 1 m_ConstrainProportionsScale: 0
m_Children: m_Children:
- {fileID: 829525444} - {fileID: 829525444}
m_Father: {fileID: 1223157292} m_Father: {fileID: 1223157292}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 16.400002, y: -22.900002} m_AnchoredPosition: {x: 22.5, y: -20.2}
m_SizeDelta: {x: 16, y: 7} m_SizeDelta: {x: 18, y: 6.4}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &967100895 --- !u!114 &967100895
MonoBehaviour: MonoBehaviour:
@@ -4019,8 +4019,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: -18.5} m_AnchoredPosition: {x: 0, y: -16}
m_SizeDelta: {x: 50, y: 0.4} m_SizeDelta: {x: 68, y: 0.4}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1010856770 --- !u!114 &1010856770
MonoBehaviour: MonoBehaviour:
@@ -4095,7 +4095,7 @@ RectTransform:
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 23.5} m_AnchoredPosition: {x: 0, y: 23.5}
m_SizeDelta: {x: 104, y: 0.5} m_SizeDelta: {x: 144, y: 0.5}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1014388332 --- !u!114 &1014388332
MonoBehaviour: MonoBehaviour:
@@ -4145,6 +4145,7 @@ GameObject:
m_Component: m_Component:
- component: {fileID: 1051307065} - component: {fileID: 1051307065}
- component: {fileID: 1051307064} - component: {fileID: 1051307064}
- component: {fileID: 1051307066}
m_Layer: 0 m_Layer: 0
m_Name: VR_Manager m_Name: VR_Manager
m_TagString: Untagged m_TagString: Untagged
@@ -4184,6 +4185,18 @@ Transform:
m_Children: [] m_Children: []
m_Father: {fileID: 622937451} m_Father: {fileID: 622937451}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &1051307066
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1051307063}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 38f89babd4e99734aac47edbc4f87aa3, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::VRBeats.VRPointerSetup
--- !u!4 &1143639918 stripped --- !u!4 &1143639918 stripped
Transform: Transform:
m_CorrespondingSourceObject: {fileID: 748387694978730046, guid: f5d43ad86a21ab1479a999e16359b99b, m_CorrespondingSourceObject: {fileID: 748387694978730046, guid: f5d43ad86a21ab1479a999e16359b99b,
@@ -4242,7 +4255,7 @@ GameObject:
m_Icon: {fileID: 0} m_Icon: {fileID: 0}
m_NavMeshLayer: 0 m_NavMeshLayer: 0
m_StaticEditorFlags: 0 m_StaticEditorFlags: 0
m_IsActive: 0 m_IsActive: 1
--- !u!224 &1223157292 --- !u!224 &1223157292
RectTransform: RectTransform:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@@ -4252,7 +4265,7 @@ RectTransform:
m_GameObject: {fileID: 1223157291} m_GameObject: {fileID: 1223157291}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 0.89393, y: 0.89393, z: 0.89393} m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0 m_ConstrainProportionsScale: 0
m_Children: m_Children:
- {fileID: 1293606945} - {fileID: 1293606945}
@@ -4274,8 +4287,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 26.6, y: -9.4} m_AnchoredPosition: {x: 29, y: -9.4}
m_SizeDelta: {x: 52.7, y: 49} m_SizeDelta: {x: 86, y: 49}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1223157293 --- !u!114 &1223157293
MonoBehaviour: MonoBehaviour:
@@ -4473,7 +4486,7 @@ MonoBehaviour:
m_spriteAsset: {fileID: 0} m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0 m_tintAllSprites: 0
m_StyleSheet: {fileID: 0} m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: 0 m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0 m_overrideHtmlColors: 0
m_faceColor: m_faceColor:
serializedVersion: 2 serializedVersion: 2
@@ -4643,8 +4656,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 21, y: 20.5} m_AnchoredPosition: {x: 37, y: 20.5}
m_SizeDelta: {x: 8, y: 7} m_SizeDelta: {x: 7, y: 6.5}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1293606946 --- !u!114 &1293606946
MonoBehaviour: MonoBehaviour:
@@ -4756,16 +4769,16 @@ RectTransform:
m_GameObject: {fileID: 1388756479} m_GameObject: {fileID: 1388756479}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 0.9, y: 0.9, z: 0.9} m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 1 m_ConstrainProportionsScale: 0
m_Children: m_Children:
- {fileID: 1245764537} - {fileID: 1245764537}
m_Father: {fileID: 1223157292} m_Father: {fileID: 1223157292}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -9.5, y: -22.7} m_AnchoredPosition: {x: -12.5, y: -20.2}
m_SizeDelta: {x: 34, y: 7} m_SizeDelta: {x: 40, y: 6.4}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1388756481 --- !u!114 &1388756481
MonoBehaviour: MonoBehaviour:
@@ -5013,16 +5026,16 @@ RectTransform:
m_GameObject: {fileID: 1436526096} m_GameObject: {fileID: 1436526096}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 0.9, y: 0.9, z: 0.9} m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 1 m_ConstrainProportionsScale: 0
m_Children: m_Children:
- {fileID: 1786385005} - {fileID: 1786385005}
m_Father: {fileID: 1223157292} m_Father: {fileID: 1223157292}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -12, y: -14} m_AnchoredPosition: {x: -17, y: -9.7}
m_SizeDelta: {x: 22, y: 7} m_SizeDelta: {x: 27, y: 6}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1436526098 --- !u!114 &1436526098
MonoBehaviour: MonoBehaviour:
@@ -5145,7 +5158,7 @@ MonoBehaviour:
m_CheckFor3DOcclusion: 0 m_CheckFor3DOcclusion: 0
m_BlockingMask: m_BlockingMask:
serializedVersion: 2 serializedVersion: 2
m_Bits: 2147483647 m_Bits: 4294967295
m_RaycastTriggerInteraction: 1 m_RaycastTriggerInteraction: 1
--- !u!114 &1445586367 --- !u!114 &1445586367
MonoBehaviour: MonoBehaviour:
@@ -5186,7 +5199,7 @@ MonoBehaviour:
m_FallbackScreenDPI: 96 m_FallbackScreenDPI: 96
m_DefaultSpriteDPI: 96 m_DefaultSpriteDPI: 96
m_DynamicPixelsPerUnit: 1 m_DynamicPixelsPerUnit: 1
m_PresetInfoIsWorld: 0 m_PresetInfoIsWorld: 1
--- !u!223 &1445586369 --- !u!223 &1445586369
Canvas: Canvas:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@@ -5228,8 +5241,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0.67, y: 4.46} m_AnchoredPosition: {x: 0.4, y: 4.5}
m_SizeDelta: {x: 105.885, y: 68.223} m_SizeDelta: {x: 148, y: 68.2}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1445586371 --- !u!114 &1445586371
MonoBehaviour: MonoBehaviour:
@@ -5480,7 +5493,7 @@ MonoBehaviour:
m_spriteAsset: {fileID: 0} m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0 m_tintAllSprites: 0
m_StyleSheet: {fileID: 0} m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: 0 m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0 m_overrideHtmlColors: 0
m_faceColor: m_faceColor:
serializedVersion: 2 serializedVersion: 2
@@ -5920,7 +5933,7 @@ RectTransform:
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 28.5} m_AnchoredPosition: {x: 0, y: 28.5}
m_SizeDelta: {x: 100, y: 9} m_SizeDelta: {x: 138, y: 9}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1754869934 --- !u!114 &1754869934
MonoBehaviour: MonoBehaviour:
@@ -6220,7 +6233,7 @@ MonoBehaviour:
m_spriteAsset: {fileID: 0} m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0 m_tintAllSprites: 0
m_StyleSheet: {fileID: 0} m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: 0 m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0 m_overrideHtmlColors: 0
m_faceColor: m_faceColor:
serializedVersion: 2 serializedVersion: 2
@@ -6613,7 +6626,7 @@ MonoBehaviour:
m_spriteAsset: {fileID: 0} m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0 m_tintAllSprites: 0
m_StyleSheet: {fileID: 0} m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: 0 m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0 m_overrideHtmlColors: 0
m_faceColor: m_faceColor:
serializedVersion: 2 serializedVersion: 2
@@ -6707,8 +6720,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -6, y: -21.5} m_AnchoredPosition: {x: -12.5, y: -20.2}
m_SizeDelta: {x: 34, y: 7} m_SizeDelta: {x: 40, y: 6.4}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1888615411 --- !u!114 &1888615411
MonoBehaviour: MonoBehaviour:
@@ -7144,8 +7157,8 @@ RectTransform:
m_PrefabAsset: {fileID: 0} m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1946485404} m_GameObject: {fileID: 1946485404}
m_LocalRotation: {x: 0, y: -0.38268343, z: 0, w: 0.92387956} m_LocalRotation: {x: 0, y: -0.38268343, z: 0, w: 0.92387956}
m_LocalPosition: {x: 0, y: 0, z: 17.9} m_LocalPosition: {x: 0, y: 0, z: 20.96}
m_LocalScale: {x: 0.25, y: 0.25, z: 1} m_LocalScale: {x: 0.21, y: 0.21, z: 1}
m_ConstrainProportionsScale: 0 m_ConstrainProportionsScale: 0
m_Children: m_Children:
- {fileID: 2121492652} - {fileID: 2121492652}
@@ -7154,7 +7167,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: -45, z: 0} m_LocalEulerAnglesHint: {x: 0, y: -45, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -22.2, y: 4.39} m_AnchoredPosition: {x: -27.37, y: 4.39}
m_SizeDelta: {x: 105.89, y: 66.53} m_SizeDelta: {x: 105.89, y: 66.53}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1946485406 --- !u!114 &1946485406
@@ -7446,7 +7459,7 @@ MonoBehaviour:
m_CheckFor3DOcclusion: 0 m_CheckFor3DOcclusion: 0
m_BlockingMask: m_BlockingMask:
serializedVersion: 2 serializedVersion: 2
m_Bits: 2147483647 m_Bits: 4294967295
m_RaycastTriggerInteraction: 1 m_RaycastTriggerInteraction: 1
--- !u!114 &1965927025 --- !u!114 &1965927025
MonoBehaviour: MonoBehaviour:
@@ -7487,7 +7500,7 @@ MonoBehaviour:
m_FallbackScreenDPI: 96 m_FallbackScreenDPI: 96
m_DefaultSpriteDPI: 96 m_DefaultSpriteDPI: 96
m_DynamicPixelsPerUnit: 1 m_DynamicPixelsPerUnit: 1
m_PresetInfoIsWorld: 0 m_PresetInfoIsWorld: 1
--- !u!223 &1965927027 --- !u!223 &1965927027
Canvas: Canvas:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@@ -7714,7 +7727,7 @@ MonoBehaviour:
m_spriteAsset: {fileID: 0} m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0 m_tintAllSprites: 0
m_StyleSheet: {fileID: 0} m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: 0 m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0 m_overrideHtmlColors: 0
m_faceColor: m_faceColor:
serializedVersion: 2 serializedVersion: 2
+30 -4
View File
@@ -11,6 +11,8 @@ namespace VRBeats
[SerializeField] private float fadeOutTime = 4.0f; [SerializeField] private float fadeOutTime = 4.0f;
private AudioSource audioSource = null; private AudioSource audioSource = null;
private double scheduledDspStartTime = -1.0;
private bool hasScheduledClip = false;
private void Start() private void Start()
{ {
@@ -43,13 +45,37 @@ namespace VRBeats
public void PlayClip(AudioClip clip) public void PlayClip(AudioClip clip)
{ {
audioSource.clip = clip; PlayClipScheduled(clip);
audioSource.Play();
} }
public float CurrentTime => audioSource != null ? audioSource.time : 0f; public double PlayClipScheduled(AudioClip clip, double delaySeconds = 0.1)
{
ResetThisComponent();
audioSource.Stop();
audioSource.clip = clip;
audioSource.time = 0.0f;
scheduledDspStartTime = AudioSettings.dspTime + delaySeconds;
hasScheduledClip = true;
audioSource.PlayScheduled(scheduledDspStartTime);
return scheduledDspStartTime;
}
public float CurrentTime
{
get
{
if (audioSource == null)
return 0.0f;
if (hasScheduledClip || scheduledDspStartTime >= 0.0)
return Mathf.Max(0.0f, (float)(AudioSettings.dspTime - scheduledDspStartTime));
return 0.0f;
}
}
} }
} }
@@ -20,6 +20,11 @@ namespace VRBeats
public override void DoDamage(DamageInfo info) public override void DoDamage(DamageInfo info)
{ {
// 방향·속도·색상이 맞지 않으면 자르지 않음
var beatCube = GetComponent<VR_BeatCube>();
if (beatCube != null && !beatCube.IsCutIntentValid(info as BeatDamageInfo))
return;
onCut.Invoke(info); onCut.Invoke(info);
var beatDamageInfo = info as BeatDamageInfo; var beatDamageInfo = info as BeatDamageInfo;
@@ -34,6 +39,11 @@ namespace VRBeats
if (Cut(info.hitPoint, cutDir, insideMaterial)) if (Cut(info.hitPoint, cutDir, insideMaterial))
{ {
Color trailColor = beatCube != null
? VR_BeatManager.instance.GetColorFromColorSide(beatCube.ThisColorSide)
: Color.white;
Vector3 saberUp = beatDamageInfo.hitObject != null ? beatDamageInfo.hitObject.transform.up : cutDir;
SliceTrailEffect.Spawn(info.hitPoint, info.hitDir, saberUp, trailColor);
Destroy(gameObject); Destroy(gameObject);
} }
} }
@@ -0,0 +1,467 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
namespace VRBeats
{
/// <summary>
/// Builds a short world-space ribbon from the whole saber blade instead of
/// attaching a TrailRenderer to the tip. This keeps the afterimage on the
/// blade surface and avoids the "propeller" look from a single end point.
/// </summary>
public class SaberTrailEffect : MonoBehaviour
{
private const int MaxSamples = 18;
private const float TrailLifetime = 0.13f;
private const float MinSwingSpeed = 0.75f;
private const float MinSampleDistance = 0.012f;
private const float BladeBaseT = 0.10f;
private const float BladeTipT = 0.98f;
private readonly List<BladeSample> samples = new List<BladeSample>(MaxSamples);
private readonly List<Vector3> vertices = new List<Vector3>(MaxSamples * 2);
private readonly List<Color> colors = new List<Color>(MaxSamples * 2);
private readonly List<Vector2> uvs = new List<Vector2>(MaxSamples * 2);
private readonly List<int> triangles = new List<int>((MaxSamples - 1) * 12);
private Transform bladeStart;
private Transform bladeEnd;
private GameObject wideObject;
private GameObject coreObject;
private Mesh wideMesh;
private Mesh coreMesh;
private MeshRenderer wideRenderer;
private MeshRenderer coreRenderer;
private Material wideMaterial;
private Material coreMaterial;
private Color trailColor = Color.cyan;
private bool visible = true;
private bool hasLastFrame;
private Vector3 lastBase;
private Vector3 lastTip;
private struct BladeSample
{
public Vector3 basePos;
public Vector3 tipPos;
public float time;
}
private void Awake()
{
RemoveLegacyTrailRenderers();
ResolveBladeAnchors();
EnsureRenderers();
SetColor(trailColor);
SetVisible(visible);
}
private void LateUpdate()
{
if (!visible)
{
Clear();
hasLastFrame = false;
return;
}
if (bladeEnd == null)
{
ResolveBladeAnchors();
}
Vector3 basePos;
Vector3 tipPos;
GetBladeSegment(out basePos, out tipPos);
float now = Time.time;
float speed = 0f;
float moved = 0f;
if (hasLastFrame)
{
float deltaTime = Mathf.Max(Time.deltaTime, 0.0001f);
speed = Mathf.Max(
Vector3.Distance(basePos, lastBase),
Vector3.Distance(tipPos, lastTip)) / deltaTime;
moved = Mathf.Max(
Vector3.Distance(basePos, lastBase),
Vector3.Distance(tipPos, lastTip));
}
if (!hasLastFrame || (speed >= MinSwingSpeed && moved >= MinSampleDistance))
{
AddSample(basePos, tipPos, now);
}
TrimExpiredSamples(now);
BuildRibbon(wideMesh, now, 0f, 1f, trailColor, 0.10f, 0.36f);
Color coreColor = Color.Lerp(Color.white, trailColor, CoreColorWeight(trailColor));
float alphaMultiplier = VisibilityAlphaMultiplier(trailColor);
BuildRibbon(coreMesh, now, 0.42f, 1f, coreColor, 0.14f * alphaMultiplier, 0.48f * alphaMultiplier);
lastBase = basePos;
lastTip = tipPos;
hasLastFrame = true;
}
private void OnDisable()
{
Clear();
hasLastFrame = false;
}
private void OnDestroy()
{
DestroyRuntimeObject(wideObject);
DestroyRuntimeObject(coreObject);
DestroyRuntimeObject(wideMesh);
DestroyRuntimeObject(coreMesh);
DestroyRuntimeObject(wideMaterial);
DestroyRuntimeObject(coreMaterial);
}
public void SetVisible(bool value)
{
visible = value;
if (wideRenderer != null)
{
wideRenderer.enabled = value;
}
if (coreRenderer != null)
{
coreRenderer.enabled = value;
}
if (!value)
{
Clear();
hasLastFrame = false;
}
}
public void SetColor(Color color)
{
trailColor = NormalizeColor(color);
EnsureRenderers();
float alphaMultiplier = VisibilityAlphaMultiplier(trailColor);
ApplyMaterialColor(wideMaterial, trailColor, 0.34f * alphaMultiplier);
Color coreColor = Color.Lerp(Color.white, trailColor, CoreColorWeight(trailColor));
ApplyMaterialColor(coreMaterial, coreColor, 0.50f * alphaMultiplier);
}
private void ResolveBladeAnchors()
{
bladeStart = FindChildRecursive(transform, "Start");
bladeEnd = FindChildRecursive(transform, "End");
if (bladeEnd == null)
{
bladeEnd = transform;
}
if (bladeStart == null)
{
bladeStart = transform;
}
}
private void GetBladeSegment(out Vector3 basePos, out Vector3 tipPos)
{
Vector3 start = bladeStart != null ? bladeStart.position : transform.position;
Vector3 end = bladeEnd != null ? bladeEnd.position : transform.position + transform.forward;
if (Vector3.Distance(start, end) < 0.001f)
{
end = start + transform.forward * 0.8f;
}
basePos = Vector3.Lerp(start, end, BladeBaseT);
tipPos = Vector3.Lerp(start, end, BladeTipT);
}
private void AddSample(Vector3 basePos, Vector3 tipPos, float time)
{
if (samples.Count >= MaxSamples)
{
samples.RemoveAt(0);
}
samples.Add(new BladeSample
{
basePos = basePos,
tipPos = tipPos,
time = time
});
}
private void TrimExpiredSamples(float now)
{
for (int i = samples.Count - 1; i >= 0; i--)
{
if (now - samples[i].time > TrailLifetime)
{
samples.RemoveAt(i);
}
}
}
private void BuildRibbon(Mesh mesh, float now, float innerT, float outerT, Color color, float innerAlpha, float outerAlpha)
{
if (mesh == null)
{
return;
}
vertices.Clear();
colors.Clear();
uvs.Clear();
triangles.Clear();
if (samples.Count < 2)
{
mesh.Clear();
return;
}
for (int i = 0; i < samples.Count; i++)
{
BladeSample sample = samples[i];
float age01 = Mathf.Clamp01((now - sample.time) / TrailLifetime);
float fade = Mathf.Pow(1f - age01, 1.7f);
float along = samples.Count <= 1 ? 0f : i / (float)(samples.Count - 1);
Vector3 inner = Vector3.Lerp(sample.basePos, sample.tipPos, innerT);
Vector3 outer = Vector3.Lerp(sample.basePos, sample.tipPos, outerT);
vertices.Add(inner);
vertices.Add(outer);
colors.Add(WithAlpha(color, fade * innerAlpha));
colors.Add(WithAlpha(color, fade * outerAlpha));
uvs.Add(new Vector2(0f, along));
uvs.Add(new Vector2(1f, along));
}
for (int i = 0; i < samples.Count - 1; i++)
{
int a = i * 2;
int b = a + 1;
int c = a + 2;
int d = a + 3;
triangles.Add(a);
triangles.Add(b);
triangles.Add(c);
triangles.Add(b);
triangles.Add(d);
triangles.Add(c);
triangles.Add(c);
triangles.Add(b);
triangles.Add(a);
triangles.Add(c);
triangles.Add(d);
triangles.Add(b);
}
mesh.Clear();
mesh.SetVertices(vertices);
mesh.SetColors(colors);
mesh.SetUVs(0, uvs);
mesh.SetTriangles(triangles, 0);
mesh.RecalculateBounds();
}
private void EnsureRenderers()
{
if (wideObject == null)
{
wideRenderer = CreateRibbonObject("SaberBladeAfterimage_Wide", out wideObject, out wideMesh, out wideMaterial, false);
}
if (coreObject == null)
{
coreRenderer = CreateRibbonObject("SaberBladeAfterimage_Core", out coreObject, out coreMesh, out coreMaterial, true);
}
}
private MeshRenderer CreateRibbonObject(string objectName, out GameObject ribbonObject, out Mesh mesh, out Material material, bool additive)
{
ribbonObject = new GameObject(objectName);
ribbonObject.hideFlags = HideFlags.DontSave;
ribbonObject.transform.SetPositionAndRotation(Vector3.zero, Quaternion.identity);
ribbonObject.transform.localScale = Vector3.one;
mesh = new Mesh
{
name = objectName + " Mesh"
};
mesh.MarkDynamic();
MeshFilter filter = ribbonObject.AddComponent<MeshFilter>();
filter.sharedMesh = mesh;
MeshRenderer renderer = ribbonObject.AddComponent<MeshRenderer>();
material = CreateTrailMaterial(objectName + " Material", additive);
renderer.sharedMaterial = material;
renderer.shadowCastingMode = ShadowCastingMode.Off;
renderer.receiveShadows = false;
renderer.motionVectorGenerationMode = MotionVectorGenerationMode.ForceNoMotion;
renderer.enabled = visible;
return renderer;
}
private Material CreateTrailMaterial(string materialName, bool additive)
{
Shader shader = Shader.Find("Sprites/Default");
if (shader == null)
{
shader = Shader.Find("Unlit/Transparent");
}
Material material = new Material(shader)
{
name = materialName,
hideFlags = HideFlags.DontSave,
renderQueue = (int)RenderQueue.Transparent
};
if (material.HasProperty("_SrcBlend"))
{
material.SetInt("_SrcBlend", additive ? (int)BlendMode.SrcAlpha : (int)BlendMode.SrcAlpha);
}
if (material.HasProperty("_DstBlend"))
{
material.SetInt("_DstBlend", additive ? (int)BlendMode.One : (int)BlendMode.OneMinusSrcAlpha);
}
if (material.HasProperty("_ZWrite"))
{
material.SetInt("_ZWrite", 0);
}
return material;
}
private void ApplyMaterialColor(Material material, Color color, float alpha)
{
if (material == null)
{
return;
}
Color materialColor = WithAlpha(color, alpha);
if (material.HasProperty("_Color"))
{
material.SetColor("_Color", materialColor);
}
}
private void Clear()
{
samples.Clear();
if (wideMesh != null)
{
wideMesh.Clear();
}
if (coreMesh != null)
{
coreMesh.Clear();
}
}
private void RemoveLegacyTrailRenderers()
{
TrailRenderer[] legacyTrails = GetComponentsInChildren<TrailRenderer>(true);
for (int i = legacyTrails.Length - 1; i >= 0; i--)
{
DestroyRuntimeObject(legacyTrails[i]);
}
}
private static Transform FindChildRecursive(Transform root, string childName)
{
if (root == null)
{
return null;
}
if (root.name == childName)
{
return root;
}
for (int i = 0; i < root.childCount; i++)
{
Transform found = FindChildRecursive(root.GetChild(i), childName);
if (found != null)
{
return found;
}
}
return null;
}
private static Color NormalizeColor(Color color)
{
float max = Mathf.Max(color.r, color.g, color.b);
if (max > 1f)
{
color.r /= max;
color.g /= max;
color.b /= max;
}
color.a = 1f;
return color;
}
private static float CoreColorWeight(Color color)
{
return IsBlueDominant(color) ? 0.78f : 0.45f;
}
private static float VisibilityAlphaMultiplier(Color color)
{
return IsBlueDominant(color) ? 1.35f : 1f;
}
private static bool IsBlueDominant(Color color)
{
return color.b > color.r && color.b >= color.g;
}
private static Color WithAlpha(Color color, float alpha)
{
color.a = Mathf.Clamp01(alpha);
return color;
}
private static void DestroyRuntimeObject(Object target)
{
if (target == null)
{
return;
}
if (Application.isPlaying)
{
Destroy(target);
}
else
{
DestroyImmediate(target);
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0f4ab89ec5474db497559af138ed0a6f
@@ -0,0 +1,152 @@
using System.Collections;
using UnityEngine;
using UnityEngine.Rendering;
namespace VRBeats
{
public class SliceTrailEffect : MonoBehaviour
{
private const float Lifetime = 0.24f;
private const int PointCount = 7;
private LineRenderer glowLine = null;
private LineRenderer coreLine = null;
private Vector3 center = Vector3.zero;
private Vector3 tangent = Vector3.right;
private Vector3 lift = Vector3.up;
private Color effectColor = Color.cyan;
public static void Spawn(Vector3 position, Vector3 hitDir, Vector3 saberUp, Color color)
{
GameObject effectObject = new GameObject("SliceTrailEffect");
SliceTrailEffect effect = effectObject.AddComponent<SliceTrailEffect>();
effect.Construct(position, hitDir, saberUp, color);
}
private void Construct(Vector3 position, Vector3 hitDir, Vector3 saberUp, Color color)
{
center = position;
transform.position = position;
effectColor = NormalizeColor(color);
tangent = saberUp.sqrMagnitude > 0.001f ? saberUp.normalized : Vector3.right;
lift = hitDir.sqrMagnitude > 0.001f ? hitDir.normalized : Vector3.up;
float widthMultiplier = IsBlueDominant(effectColor) ? 1.18f : 1.0f;
glowLine = CreateLine("Glow", 0.16f * widthMultiplier, 0.45f);
coreLine = CreateLine("Core", 0.045f * widthMultiplier, 0.95f);
StartCoroutine(Animate());
}
private LineRenderer CreateLine(string name, float width, float alpha)
{
GameObject lineObject = new GameObject(name);
lineObject.transform.SetParent(transform, false);
LineRenderer line = lineObject.AddComponent<LineRenderer>();
line.positionCount = PointCount;
line.useWorldSpace = true;
line.alignment = LineAlignment.View;
line.textureMode = LineTextureMode.Stretch;
line.numCornerVertices = 8;
line.numCapVertices = 8;
line.shadowCastingMode = ShadowCastingMode.Off;
line.receiveShadows = false;
line.material = CreateMaterial();
line.widthMultiplier = width;
ApplyGradient(line, alpha);
return line;
}
private IEnumerator Animate()
{
float age = 0.0f;
while (age < Lifetime)
{
float t = age / Lifetime;
float length = Mathf.Lerp(0.72f, 1.45f, t);
float bend = Mathf.Lerp(0.12f, 0.34f, t);
float alpha = 1.0f - t;
float alphaMultiplier = VisibilityAlphaMultiplier(effectColor);
UpdateLine(glowLine, length, bend, alpha * 0.45f * alphaMultiplier);
UpdateLine(coreLine, length * 0.88f, bend * 0.55f, alpha * 0.95f * alphaMultiplier);
age += Time.deltaTime;
yield return null;
}
Destroy(gameObject);
}
private void UpdateLine(LineRenderer line, float length, float bend, float alpha)
{
if (line == null)
return;
for (int i = 0; i < PointCount; i++)
{
float normalized = PointCount <= 1 ? 0.0f : (float)i / (PointCount - 1);
float offset = normalized - 0.5f;
float curve = Mathf.Sin(normalized * Mathf.PI) * bend;
line.SetPosition(i, center + tangent * (offset * length) + lift * curve);
}
ApplyGradient(line, alpha);
}
private static Material CreateMaterial()
{
Shader shader = Shader.Find("Sprites/Default");
if (shader == null)
shader = Shader.Find("Unlit/Transparent");
Material material = new Material(shader);
material.name = "Runtime Slice Trail";
return material;
}
private void ApplyGradient(LineRenderer line, float alpha)
{
Color start = new Color(effectColor.r, effectColor.g, effectColor.b, 0.0f);
Color mid = new Color(effectColor.r, effectColor.g, effectColor.b, alpha);
Color end = new Color(effectColor.r, effectColor.g, effectColor.b, 0.0f);
Gradient gradient = new Gradient();
gradient.SetKeys(
new[]
{
new GradientColorKey(start, 0.0f),
new GradientColorKey(mid, 0.5f),
new GradientColorKey(end, 1.0f),
},
new[]
{
new GradientAlphaKey(0.0f, 0.0f),
new GradientAlphaKey(alpha, 0.5f),
new GradientAlphaKey(0.0f, 1.0f),
});
line.colorGradient = gradient;
}
private static Color NormalizeColor(Color color)
{
float max = Mathf.Max(color.r, Mathf.Max(color.g, color.b));
if (max > 1.0f)
color /= max;
color.a = 1.0f;
return color;
}
private static float VisibilityAlphaMultiplier(Color color)
{
return IsBlueDominant(color) ? 1.35f : 1.0f;
}
private static bool IsBlueDominant(Color color)
{
return color.b > color.r && color.b >= color.g;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 41e40f1c9d0c48af9e2ad90cb8c5108c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+13 -4
View File
@@ -7,7 +7,7 @@ namespace VRBeats
public class VR_BeatCube : MonoBehaviour public class VR_BeatCube : MonoBehaviour
{ {
[SerializeField] private float minCutSpeed = 0.5f; [SerializeField] private float minCutSpeed = 0.5f;
[SerializeField] private OnSliceAction sliceAction = null; [SerializeField] private float maxCutAngle = 40f;
[SerializeField] private GameEvent onCorrectSlice = null; [SerializeField] private GameEvent onCorrectSlice = null;
[SerializeField] private GameEvent onIncorrectSlice = null; [SerializeField] private GameEvent onIncorrectSlice = null;
[SerializeField] private GameEvent onPlayerMiss = null; [SerializeField] private GameEvent onPlayerMiss = null;
@@ -56,16 +56,18 @@ namespace VRBeats
//notify to whoever is listening that the player did a correct/incorrect slice //notify to whoever is listening that the player did a correct/incorrect slice
if ( IsCutIntentValid(info as BeatDamageInfo) ) if ( IsCutIntentValid(info as BeatDamageInfo) )
{ {
ScoreManager.ReportSliceTiming(GetTimingErrorSeconds());
onCorrectSlice.Invoke(); onCorrectSlice.Invoke();
} }
else else
{ {
ScoreManager.ReportMiss();
onIncorrectSlice.Invoke(); onIncorrectSlice.Invoke();
} }
} }
private bool IsCutIntentValid(BeatDamageInfo info) public bool IsCutIntentValid(BeatDamageInfo info)
{ {
if (info == null) return false; if (info == null) return false;
@@ -76,7 +78,7 @@ namespace VRBeats
return true; return true;
float cutAngle = Vector2.Angle(transform.up, info.hitDir); float cutAngle = Vector2.Angle(transform.up, info.hitDir);
return info.colorSide == ThisColorSide && cutAngle < 80.0f; return info.colorSide == ThisColorSide && cutAngle < maxCutAngle;
} }
@@ -98,6 +100,7 @@ namespace VRBeats
public void Kill() public void Kill()
{ {
ScoreManager.ReportMiss();
onPlayerMiss.Invoke(); onPlayerMiss.Invoke();
canBeKilled = false; canBeKilled = false;
transform.ScaleTween(Vector3.zero, 2.0f).SetEase(Ease.EaseOutExpo).SetOnComplete( delegate transform.ScaleTween(Vector3.zero, 2.0f).SetEase(Ease.EaseOutExpo).SetOnComplete( delegate
@@ -107,8 +110,14 @@ namespace VRBeats
} ); } );
} }
private float GetTimingErrorSeconds()
{
float speed = Mathf.Max(Mathf.Abs(thisSpawneable.Speed), 0.001f);
float distanceFromPlayer = Mathf.Abs(transform.position.z - player.position.z);
return distanceFromPlayer / speed;
}
} }
} }
@@ -1,6 +1,8 @@
using UnityEngine; using System.Collections;
using UnityEngine;
using Platinio; using Platinio;
using UnityEngine.Playables; using UnityEngine.Playables;
using UnityEngine.SceneManagement;
using VRBeats.ScriptableEvents; using VRBeats.ScriptableEvents;
using VRSDK; using VRSDK;
@@ -11,8 +13,6 @@ namespace VRBeats
[SerializeField] private BoxCollider playZone = null; [SerializeField] private BoxCollider playZone = null;
[SerializeField] private Transform player = null; [SerializeField] private Transform player = null;
[SerializeField] private VR_BeatSettings settings = null; [SerializeField] private VR_BeatSettings settings = null;
[SerializeField] private GameEvent onGameOver = null;
private AudioManager audioManager = null; private AudioManager audioManager = null;
private EnviromentController enviromentController = null; private EnviromentController enviromentController = null;
private PlayableDirector playableDirector = null; private PlayableDirector playableDirector = null;
@@ -24,20 +24,17 @@ namespace VRBeats
public Transform Player { get { return player; } } public Transform Player { get { return player; } }
private int playerConsecutiveMiss = 0;
protected override void Awake() protected override void Awake()
{ {
base.Awake(); base.Awake();
audioManager = FindObjectOfType<AudioManager>(); audioManager = FindFirstObjectByType<AudioManager>();
enviromentController = FindObjectOfType<EnviromentController>(); enviromentController = FindFirstObjectByType<EnviromentController>();
playableDirector = FindObjectOfType<PlayableDirector>(); playableDirector = FindFirstObjectByType<PlayableDirector>();
} }
protected override void Start() protected override void Start()
{ {
base.Start(); base.Start();
playerConsecutiveMiss = 0;
} }
@@ -65,24 +62,22 @@ namespace VRBeats
Vector3 finalPosition = CalculateSpawnPosition( info.position); Vector3 finalPosition = CalculateSpawnPosition( info.position);
Vector3 travelOffset = Vector3.forward * -settings.TargetTravelDistance; Vector3 travelOffset = Vector3.forward * -settings.TargetTravelDistance;
Vector3 spawnPosition = finalPosition - travelOffset; Vector3 spawnPosition = finalPosition - travelOffset;
Spawneable clone = Instantiate( spawneable , spawnPosition , Quaternion.Euler( info.rotation ) );
SetSpeedRelativeToPlayZone(info);
clone.Construct(info);
Vector3 finalScale = clone.transform.localScale;
clone.transform.localScale = Vector3.zero;
float travelTime = info.travelTimeOverride > 0f ? info.travelTimeOverride : settings.TargetTravelTime; float travelTime = info.travelTimeOverride > 0f ? info.travelTimeOverride : settings.TargetTravelTime;
clone.transform.Move(finalPosition, travelTime).SetEase(settings.TargetTravelEase).SetOnComplete(delegate info.speed = settings.TargetTravelDistance / Mathf.Max(0.05f, travelTime);
SetSpeedRelativeToPlayZone(info);
Spawneable clone = Instantiate( spawneable , spawnPosition , Quaternion.Euler( info.rotation ) );
clone.Construct(info);
StartCoroutine(BeginContinuousSpawnNextFrame(clone));
}
private IEnumerator BeginContinuousSpawnNextFrame(Spawneable clone)
{ {
yield return null;
if (clone != null)
clone.OnSpawn(); clone.OnSpawn();
}).SetUpdateMode(Platinio.TweenEngine.UpdateMode.Update);
clone.transform.ScaleTween(finalScale, travelTime).SetEase(settings.TargetTravelEase);
} }
private void SetSpeedRelativeToPlayZone(SpawnEventInfo info) private void SetSpeedRelativeToPlayZone(SpawnEventInfo info)
@@ -127,13 +122,7 @@ namespace VRBeats
public void RestartLevel() public void RestartLevel()
{ {
gameObject.CancelAllTweens(); SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
isGameRunning = true;
audioManager.SetAudioMixerPitch(1.0f);
enviromentController.TurnLightsOn();
playableDirector.time = 0.0f;
playableDirector.Play();
} }
} }
@@ -5,6 +5,8 @@ namespace VRBeats
{ {
public class VR_InteractorController : MonoBehaviour public class VR_InteractorController : MonoBehaviour
{ {
[SerializeField] private bool disableOnAwake = true;
private UnityEngine.XR.Interaction.Toolkit.Interactors.XRRayInteractor rayInteractor = null; private UnityEngine.XR.Interaction.Toolkit.Interactors.XRRayInteractor rayInteractor = null;
private UnityEngine.XR.Interaction.Toolkit.Interactors.Visuals.XRInteractorLineVisual interactorLineVisual = null; private UnityEngine.XR.Interaction.Toolkit.Interactors.Visuals.XRInteractorLineVisual interactorLineVisual = null;
private LineRenderer lineRender = null; private LineRenderer lineRender = null;
@@ -15,22 +17,54 @@ namespace VRBeats
interactorLineVisual = GetComponent<UnityEngine.XR.Interaction.Toolkit.Interactors.Visuals.XRInteractorLineVisual>(); interactorLineVisual = GetComponent<UnityEngine.XR.Interaction.Toolkit.Interactors.Visuals.XRInteractorLineVisual>();
lineRender = GetComponent<LineRenderer>(); lineRender = GetComponent<LineRenderer>();
if (disableOnAwake)
DisableXRRayInteractorComponents(); DisableXRRayInteractorComponents();
} }
public void DisableXRRayInteractorComponents() public void DisableXRRayInteractorComponents()
{ {
if (rayInteractor != null)
rayInteractor.enabled = false; rayInteractor.enabled = false;
if (interactorLineVisual != null)
interactorLineVisual.enabled = false; interactorLineVisual.enabled = false;
if (lineRender != null)
lineRender.enabled = false; lineRender.enabled = false;
// VRPointerController 위치가 컨트롤러/레이 구조에 따라 달라질 수 있어서 계층 전체에서 찾는다.
var pointer = FindPointerController();
if (pointer != null)
pointer.enabled = false;
} }
public void EnableXRRayInteractorComponents() public void EnableXRRayInteractorComponents()
{ {
rayInteractor.enabled = true; if (rayInteractor != null)
interactorLineVisual.enabled = true; rayInteractor.enabled = false;
if (interactorLineVisual != null)
interactorLineVisual.enabled = false;
if (lineRender != null)
lineRender.enabled = true; lineRender.enabled = true;
var pointer = FindPointerController();
if (pointer != null)
pointer.enabled = true;
}
private VRPointerController FindPointerController()
{
var pointer = GetComponent<VRPointerController>();
if (pointer != null)
return pointer;
pointer = GetComponentInParent<VRPointerController>();
if (pointer != null)
return pointer;
pointer = GetComponentInChildren<VRPointerController>(true);
if (pointer != null)
return pointer;
return transform.root.GetComponentInChildren<VRPointerController>(true);
} }
} }
@@ -15,10 +15,14 @@ namespace VRBeats
private VR_Grabbable grabbable = null; private VR_Grabbable grabbable = null;
private ColorSide colorSide = ColorSide.Left; private ColorSide colorSide = ColorSide.Left;
private MeshRenderer[] renderArray = null; private MeshRenderer[] renderArray = null;
private SaberTrailEffect trailEffect = null;
private void Awake() private void Awake()
{ {
renderArray = transform.GetComponentsInChildren<MeshRenderer>(); renderArray = transform.GetComponentsInChildren<MeshRenderer>();
trailEffect = GetComponent<SaberTrailEffect>();
if (trailEffect == null)
trailEffect = gameObject.AddComponent<SaberTrailEffect>();
grabbable = GetComponent<VR_Grabbable>(); grabbable = GetComponent<VR_Grabbable>();
grabbable.OnGrabStateChange.AddListener(OnGrabStateChange); grabbable.OnGrabStateChange.AddListener(OnGrabStateChange);
@@ -44,6 +48,9 @@ namespace VRBeats
{ {
SetMaterialBindings(materialBindingArray[n], c); SetMaterialBindings(materialBindingArray[n], c);
} }
if (trailEffect != null)
trailEffect.SetColor(c);
} }
private void SetMaterialBindings(MaterialBindings matBindings, Color c) private void SetMaterialBindings(MaterialBindings matBindings, Color c)
@@ -55,11 +62,15 @@ namespace VRBeats
public void MakeVisible() public void MakeVisible()
{ {
SetRenderArrayEnableValue(true); SetRenderArrayEnableValue(true);
if (trailEffect != null)
trailEffect.SetVisible(true);
} }
public void MakeInvisible() public void MakeInvisible()
{ {
SetRenderArrayEnableValue(false); SetRenderArrayEnableValue(false);
if (trailEffect != null)
trailEffect.SetVisible(false);
} }
private void SetRenderArrayEnableValue(bool value) private void SetRenderArrayEnableValue(bool value)
@@ -13,24 +13,42 @@ namespace VRBeats
private void Start() private void Start()
{ {
if (colorSide == ColorSide.Left) controller = VR_Manager.instance.Player.LeftController; ResolveController();
if (colorSide == ColorSide.Right) controller = VR_Manager.instance.Player.RightController;
} }
protected override DamageInfo CreateDamageInfo(Vector3 hitPoint) protected override DamageInfo CreateDamageInfo(Vector3 hitPoint)
{ {
ResolveController();
var damageInfo = base.CreateDamageInfo(hitPoint); var damageInfo = base.CreateDamageInfo(hitPoint);
BeatDamageInfo beatDamageInfo = new BeatDamageInfo(damageInfo); BeatDamageInfo beatDamageInfo = new BeatDamageInfo(damageInfo);
Vector3 controllerVelocity = controller.Velocity; Vector3 controllerVelocity = controller != null ? controller.Velocity : Vector3.zero;
beatDamageInfo.hitForce = Mathf.Min((controllerVelocity * hitForce).magnitude, maxHitForce); beatDamageInfo.hitForce = Mathf.Min((controllerVelocity * hitForce).magnitude, maxHitForce);
beatDamageInfo.hitObject = gameObject; beatDamageInfo.hitObject = gameObject;
beatDamageInfo.colorSide = colorSide; beatDamageInfo.colorSide = colorSide;
beatDamageInfo.velocity = controller.Velocity.magnitude; beatDamageInfo.velocity = controllerVelocity.magnitude;
return beatDamageInfo; return beatDamageInfo;
} }
private void ResolveController()
{
VR_Grabbable grabbable = GetComponent<VR_Grabbable>();
if (grabbable != null && grabbable.GrabController != null)
{
controller = grabbable.GrabController;
colorSide = controller.ControllerType == VR_ControllerType.Right ? ColorSide.Right : ColorSide.Left;
return;
}
if (VR_Manager.instance == null || VR_Manager.instance.Player == null)
return;
controller = colorSide == ColorSide.Left
? VR_Manager.instance.Player.LeftController
: VR_Manager.instance.Player.RightController;
}
} }
} }
@@ -18,7 +18,8 @@ namespace VRBeats
private void Rotate() private void Rotate()
{ {
float rotation = Random.Range(-maxRotation , maxRotation); float direction = Random.value < 0.5f ? -1.0f : 1.0f;
float rotation = Random.Range(minRotation, maxRotation) * direction;
transform.RotateTween( Vector3.forward , rotation , animTime).SetEase(ease).SetOnComplete( Rotate ); transform.RotateTween( Vector3.forward , rotation , animTime).SetEase(ease).SetOnComplete( Rotate );
} }
+7 -1
View File
@@ -14,8 +14,9 @@ namespace VRBeats
public void Construct(Color c) public void Construct(Color c)
{ {
float visibilityMultiplier = IsBlueDominant(c) ? 1.6f : 1.0f;
materialBindings.SetUseEmmisiveIntensity(false); materialBindings.SetUseEmmisiveIntensity(false);
materialBindings.SetEmmisiveColor(c * glowEffect); materialBindings.SetEmmisiveColor(c * glowEffect * visibilityMultiplier);
PlayAnimation(); PlayAnimation();
} }
@@ -35,6 +36,11 @@ namespace VRBeats
}).SetOwner(gameObject); ; }).SetOwner(gameObject); ;
} }
private static bool IsBlueDominant(Color color)
{
return color.b > color.r && color.b >= color.g;
}
} }
} }
@@ -16,7 +16,7 @@ namespace VRBeats
} }
else else
{ {
VolumeSingleton[] instancesArray = FindObjectsOfType<VolumeSingleton>(); VolumeSingleton[] instancesArray = FindObjectsByType<VolumeSingleton>(FindObjectsSortMode.None);
foreach (var volumeSingleton in instancesArray) foreach (var volumeSingleton in instancesArray)
{ {
@@ -30,4 +30,3 @@ namespace VRBeats
} }
} }
@@ -101,7 +101,7 @@ namespace Platinio.UI
Canvas canvas = null; Canvas canvas = null;
//just return the first encounter canvas //just return the first encounter canvas
canvas = GameObject.FindObjectOfType<Canvas>(); canvas = GameObject.FindFirstObjectByType<Canvas>();
if (canvas != null) if (canvas != null)
{ {
@@ -144,4 +144,3 @@ namespace Platinio.UI
} }
} }
@@ -32,7 +32,7 @@ namespace VRBeats
public Vector3 rotation = Vector3.zero; public Vector3 rotation = Vector3.zero;
public float speed = 2.0f; public float speed = 2.0f;
public int speedMultiplier = 1; public int speedMultiplier = 1;
// 0 이면 Settings.TargetTravelTime 사용, 양수면 해당 시간로 이동 // 0이면 Settings.TargetTravelTime 사용, 양수면 해당 시간 동안 일정 속도로 이동
public float travelTimeOverride = 0f; public float travelTimeOverride = 0f;
} }
} }
@@ -13,7 +13,7 @@ namespace VRBeats
private void Awake() private void Awake()
{ {
enviromentController = FindObjectOfType<EnviromentController>(); enviromentController = FindFirstObjectByType<EnviromentController>();
} }
private void Update() private void Update()
+299 -20
View File
@@ -1,5 +1,4 @@
using UnityEngine; using UnityEngine;
using Platinio.TweenEngine;
using TMPro; using TMPro;
namespace VRBeats namespace VRBeats
@@ -7,55 +6,335 @@ namespace VRBeats
public class FinalScoreLabel : MonoBehaviour public class FinalScoreLabel : MonoBehaviour
{ {
[SerializeField] private TextMeshProUGUI scoreText = null; [SerializeField] private TextMeshProUGUI scoreText = null;
[SerializeField] private float scoreFadeTime = 10.0f;
[SerializeField] private int length = 10; [SerializeField] private int length = 10;
private string initialValue = ""; private string initialValue = "";
private ScoreManager scoreManager = null; private ScoreManager scoreManager = null;
private GameObject resultRoot = null;
private TextMeshProUGUI rankBackText = null;
private TextMeshProUGUI rankShadowText = null;
private TextMeshProUGUI rankDepthText = null;
private TextMeshProUGUI rankRimText = null;
private TextMeshProUGUI rankMainText = null;
private TextMeshProUGUI rankHighlightText = null;
private TextMeshProUGUI resultScoreText = null;
private TextMeshProUGUI resultAccuracyText = null;
private TextMeshProUGUI resultComboText = null;
private CanvasGroup scoreHudCanvasGroup = null;
private void Awake() private void Awake()
{ {
for (int n = 0; n < length; n++) for (int n = 0; n < length; n++)
{
initialValue += "0"; initialValue += "0";
}
scoreManager = FindObjectOfType<ScoreManager>();
scoreManager = FindFirstObjectByType<ScoreManager>();
if (scoreManager != null)
scoreHudCanvasGroup = scoreManager.GetComponent<CanvasGroup>() ??
scoreManager.gameObject.AddComponent<CanvasGroup>();
ApplyPopupTextStyle();
BuildResultLayout();
} }
public void ShowScore() public void ShowScore()
{ {
PlatinioTween.instance.ValueTween( 0.0f , scoreManager.CurrentScore , scoreFadeTime).SetOnUpdateFloat(delegate (float v) if (scoreText == null || scoreManager == null)
return;
SetTitleActive(false);
SetScoreHudVisible(false);
gameObject.CancelAllTweens();
if (resultRoot != null)
{ {
SetScore( (int)v ); scoreText.gameObject.SetActive(false);
}); PopulateResultLayout();
resultRoot.SetActive(true);
}
else
{
scoreText.text = scoreManager.BuildResultSummary(length);
}
} }
public void ResetValues() public void ResetValues()
{ {
gameObject.CancelAllTweens(); gameObject.CancelAllTweens();
if (resultRoot != null)
resultRoot.SetActive(false);
if (scoreText != null)
scoreText.gameObject.SetActive(true);
SetTitleActive(true);
SetScoreHudVisible(true);
ApplyPopupTextStyle();
if (scoreText != null)
scoreText.text = initialValue; scoreText.text = initialValue;
} }
private void PopulateResultLayout()
private void SetScore(int score)
{ {
if (this.scoreText == null) if (scoreManager == null ||
rankBackText == null ||
rankShadowText == null ||
rankDepthText == null ||
rankRimText == null ||
rankMainText == null ||
rankHighlightText == null ||
resultScoreText == null ||
resultAccuracyText == null ||
resultComboText == null)
return; return;
string scoreText = score.ToString(); string rank = scoreManager.Rank;
int addLength = Mathf.Max( length - scoreText.Length , 0); Color mainColor = HexToColor(scoreManager.RankColorHex);
string addZeros = ""; Color depthColor = HexToColor(GetRankDepthColorHex(rank));
for (int n = 0; n < addLength; n++) Color rimColor = HexToColor(GetRankRimColorHex(rank));
rankBackText.text = rank;
rankShadowText.text = rank;
rankDepthText.text = rank;
rankDepthText.color = depthColor;
rankRimText.text = rank;
rankMainText.text = rank;
rankHighlightText.text = rank;
ApplyMetalRankColors(mainColor, depthColor, rimColor);
resultScoreText.text =
$"<size=56%><color=#A0C8FF>SCORE</color></size>\n{scoreManager.CurrentScore:N0}";
resultAccuracyText.text =
$"<color=#A0C8FF>ACCURACY</color> {scoreManager.AccuracyPercent:0.0}%";
resultComboText.text = $"MAX COMBO {scoreManager.MaxCombo}";
}
private void SetTitleActive(bool active)
{ {
addZeros += "0"; Transform titleObj = scoreText != null
? scoreText.rectTransform.parent?.Find("Title")
: null;
if (titleObj != null)
titleObj.gameObject.SetActive(active);
} }
this.scoreText.text = addZeros + scoreText; private void ApplyPopupTextStyle()
{
if (scoreText == null)
return;
RectTransform rect = scoreText.rectTransform;
rect.anchorMin = new Vector2(0.5f, 0.5f);
rect.anchorMax = new Vector2(0.5f, 0.5f);
rect.anchoredPosition = new Vector2(0.0f, 0.0f);
rect.sizeDelta = new Vector2(620.0f, 250.0f);
scoreText.enableAutoSizing = true;
scoreText.fontSizeMin = 1.2f;
scoreText.fontSizeMax = 5.5f;
scoreText.alignment = TextAlignmentOptions.MidlineLeft;
scoreText.overflowMode = TextOverflowModes.Truncate;
scoreText.textWrappingMode = TextWrappingModes.NoWrap;
scoreText.lineSpacing = -10.0f;
scoreText.color = Color.white;
scoreText.richText = true;
} }
private void SetScoreHudVisible(bool visible)
{
if (scoreHudCanvasGroup == null)
return;
scoreHudCanvasGroup.alpha = visible ? 1.0f : 0.0f;
scoreHudCanvasGroup.interactable = false;
scoreHudCanvasGroup.blocksRaycasts = false;
}
private void BuildResultLayout()
{
if (scoreText == null)
return;
Transform parent = scoreText.rectTransform.parent;
if (parent == null)
return;
GameObject root = new GameObject("ResultLayoutRoot");
root.transform.SetParent(parent, false);
RectTransform rootRect = root.AddComponent<RectTransform>();
rootRect.anchorMin = new Vector2(0.5f, 0.5f);
rootRect.anchorMax = new Vector2(0.5f, 0.5f);
rootRect.anchoredPosition = new Vector2(5.0f, 3.1f);
rootRect.sizeDelta = new Vector2(82.0f, 34.0f);
root.SetActive(false);
resultRoot = root;
// Panel-local coordinates are small world-canvas units, not screen pixels.
rankBackText = MakeTmpLabel(root.transform, "RankBackText",
new Vector2(-20.9f, -1.1f), new Vector2(37.0f, 27.0f), 16.0f,
new Color(0.0f, 0.0f, 0.0f, 0.48f), TextAlignmentOptions.Midline);
rankShadowText = MakeTmpLabel(root.transform, "RankShadowText",
new Vector2(-21.35f, -0.55f), new Vector2(37.0f, 27.0f), 16.0f,
new Color(0.0f, 0.06f, 0.14f, 0.82f), TextAlignmentOptions.Midline);
rankDepthText = MakeTmpLabel(root.transform, "RankDepthText",
new Vector2(-21.8f, -0.1f), new Vector2(37.0f, 27.0f), 16.0f,
Color.white, TextAlignmentOptions.Midline);
rankRimText = MakeTmpLabel(root.transform, "RankRimText",
new Vector2(-22.15f, 0.22f), new Vector2(37.0f, 27.0f), 16.0f,
Color.white, TextAlignmentOptions.Midline);
rankMainText = MakeTmpLabel(root.transform, "RankMainText",
new Vector2(-22.45f, 0.5f), new Vector2(37.0f, 27.0f), 16.0f,
Color.white, TextAlignmentOptions.Midline);
rankHighlightText = MakeTmpLabel(root.transform, "RankHighlightText",
new Vector2(-22.85f, 1.0f), new Vector2(37.0f, 27.0f), 16.0f,
Color.white, TextAlignmentOptions.Midline);
resultScoreText = MakeTmpLabel(root.transform, "ResultScoreText",
new Vector2(16.8f, 7.4f), new Vector2(43.0f, 10.8f), 5.35f,
Color.white, TextAlignmentOptions.MidlineLeft);
resultAccuracyText = MakeTmpLabel(root.transform, "ResultAccuracyText",
new Vector2(16.8f, -1.2f), new Vector2(43.0f, 5.4f), 3.05f,
new Color(0.84f, 0.97f, 1.0f, 0.9f), TextAlignmentOptions.MidlineLeft);
resultComboText = MakeTmpLabel(root.transform, "ResultComboText",
new Vector2(16.8f, -6.8f), new Vector2(43.0f, 5.4f), 2.95f,
new Color(0.84f, 0.97f, 1.0f, 1.0f), TextAlignmentOptions.MidlineLeft);
ConfigureRankLayer(rankBackText, new Color(0.0f, 0.0f, 0.0f, 0.72f), 0.34f);
ConfigureRankLayer(rankShadowText, new Color(0.0f, 0.0f, 0.0f, 0.74f), 0.2f);
ConfigureRankLayer(rankDepthText, new Color(0.0f, 0.0f, 0.0f, 0.55f), 0.13f);
ConfigureRankLayer(rankRimText, new Color(1.0f, 1.0f, 1.0f, 0.82f), 0.08f);
ConfigureRankLayer(rankMainText, new Color(0.0f, 0.16f, 0.28f, 0.7f), 0.06f);
ConfigureRankLayer(rankHighlightText, new Color(1.0f, 1.0f, 1.0f, 0.35f), 0.02f);
}
private TextMeshProUGUI MakeTmpLabel(Transform parent, string name,
Vector2 pos, Vector2 size, float fontSize, Color color, TextAlignmentOptions align)
{
GameObject go = new GameObject(name);
go.transform.SetParent(parent, false);
RectTransform rect = go.AddComponent<RectTransform>();
rect.anchorMin = new Vector2(0.5f, 0.5f);
rect.anchorMax = new Vector2(0.5f, 0.5f);
rect.anchoredPosition = pos;
rect.sizeDelta = size;
TextMeshProUGUI tmp = go.AddComponent<TextMeshProUGUI>();
tmp.fontSize = fontSize;
tmp.enableAutoSizing = true;
tmp.fontSizeMin = fontSize * 0.6f;
tmp.fontSizeMax = fontSize;
tmp.color = color;
tmp.alignment = align;
tmp.overflowMode = TextOverflowModes.Truncate;
tmp.textWrappingMode = TextWrappingModes.NoWrap;
tmp.lineSpacing = -4.0f;
tmp.raycastTarget = false;
if (scoreText != null && scoreText.font != null)
{
tmp.font = scoreText.font;
tmp.fontSharedMaterial = scoreText.fontSharedMaterial;
}
return tmp;
}
private static void ConfigureRankLayer(TextMeshProUGUI tmp, Color outlineColor, float outlineWidth)
{
if (tmp == null)
return;
tmp.fontSizeMin = tmp.fontSize * 0.75f;
tmp.outlineColor = outlineColor;
tmp.outlineWidth = outlineWidth;
tmp.characterSpacing = -1.0f;
}
private void ApplyMetalRankColors(Color mainColor, Color depthColor, Color rimColor)
{
Color darkMetal = new Color(0.02f, 0.08f, 0.14f, 0.82f);
Color steel = new Color(0.70f, 0.95f, 1.0f, 1.0f);
Color whiteHot = new Color(1.0f, 1.0f, 1.0f, 1.0f);
Color lowerMain = Color.Lerp(mainColor, depthColor, 0.55f);
SetSolidRankLayer(rankBackText, new Color(0.0f, 0.0f, 0.0f, 0.50f));
SetSolidRankLayer(rankShadowText, darkMetal);
SetRankGradient(rankDepthText,
Color.Lerp(depthColor, steel, 0.22f),
depthColor,
new Color(0.0f, 0.12f, 0.22f, 0.95f),
new Color(0.0f, 0.05f, 0.10f, 0.95f));
SetRankGradient(rankRimText,
whiteHot,
rimColor,
Color.Lerp(rimColor, mainColor, 0.35f),
Color.Lerp(mainColor, depthColor, 0.45f));
SetRankGradient(rankMainText,
whiteHot,
Color.Lerp(whiteHot, rimColor, 0.45f),
Color.Lerp(mainColor, steel, 0.18f),
lowerMain);
SetRankGradient(rankHighlightText,
new Color(1.0f, 1.0f, 1.0f, 0.42f),
new Color(0.92f, 1.0f, 1.0f, 0.28f),
new Color(1.0f, 1.0f, 1.0f, 0.08f),
new Color(1.0f, 1.0f, 1.0f, 0.02f));
}
private static void SetSolidRankLayer(TextMeshProUGUI tmp, Color color)
{
if (tmp == null)
return;
tmp.enableVertexGradient = false;
tmp.color = color;
}
private static void SetRankGradient(TextMeshProUGUI tmp,
Color topLeft, Color topRight, Color bottomLeft, Color bottomRight)
{
if (tmp == null)
return;
tmp.enableVertexGradient = true;
tmp.color = Color.white;
tmp.colorGradient = new VertexGradient(topLeft, topRight, bottomLeft, bottomRight);
}
private static string GetRankDepthColorHex(string rank)
{
switch (rank)
{
case "M": return "#7EEBFF";
case "S+": return "#116BFF";
case "S": return "#B56A16";
case "A": return "#5CAA30";
case "B": return "#B89E20";
case "C": return "#C05A10";
case "D": return "#B02040";
default: return "#606870";
}
}
private static string GetRankRimColorHex(string rank)
{
switch (rank)
{
case "M": return "#FFFFFF";
case "S+": return "#E8FFFF";
case "S": return "#FFF4B8";
case "A": return "#F1FFD8";
case "B": return "#FFF5B8";
case "C": return "#FFE0B8";
case "D": return "#FFD5D5";
default: return "#E8F0F8";
}
}
private static Color HexToColor(string hex)
{
if (ColorUtility.TryParseHtmlString(hex, out Color color))
return color;
return Color.white;
}
} }
} }
+549 -85
View File
@@ -1,4 +1,4 @@
using UnityEngine; using UnityEngine;
using UnityEngine.UI; using UnityEngine.UI;
using Platinio.TweenEngine; using Platinio.TweenEngine;
using VRBeats.ScriptableEvents; using VRBeats.ScriptableEvents;
@@ -7,6 +7,14 @@ namespace VRBeats
{ {
public class ScoreManager : MonoBehaviour public class ScoreManager : MonoBehaviour
{ {
private enum BeatJudgement
{
Perfect,
Great,
Good,
Miss
}
[SerializeField] private Text multiplierLabel = null; [SerializeField] private Text multiplierLabel = null;
[SerializeField] private Text scoreLabel = null; [SerializeField] private Text scoreLabel = null;
[SerializeField] private Image multiplierLoader = null; [SerializeField] private Image multiplierLoader = null;
@@ -14,61 +22,221 @@ namespace VRBeats
[SerializeField] private CanvasGroup canvasGroup = null; [SerializeField] private CanvasGroup canvasGroup = null;
[SerializeField] private GameEvent onGameOver = null; [SerializeField] private GameEvent onGameOver = null;
[Header("DJMAX Style Score")]
[SerializeField] private Text comboLabel = null;
[SerializeField] private Text accuracyLabel = null;
[SerializeField] private Text judgementLabel = null;
[SerializeField] private bool createMissingHudLabels = true;
[SerializeField] private bool applyHudPlacement = true;
[SerializeField] private Vector2 hudAnchoredPosition = new Vector2(0.0f, 1.65f);
[SerializeField] private float perfectWindow = 0.11f;
[SerializeField] private float greatWindow = 0.20f;
[SerializeField] private float goodWindow = 0.32f;
private int maxMultiplier = 0; private int maxMultiplier = 0;
private int scorePerHit = 0; private const int MaxCourseScore = 1000000;
private int currentScore = 0; private const float ProgressBarWidth = 150.0f;
private int currentMultiplier = 0; private float currentMultiplier = 1.0f;
private int toNextMultiplierIncrease = 2;
private int acumulateCorrectSlices = 0;
private int acumulateErrors = 0; private int acumulateErrors = 0;
private int errorLimit = 0; private int errorLimit = 0;
private int totalNoteCount = 0;
private int judgedNoteCount = 0;
private int currentCombo = 0;
private int maxCombo = 0;
private int perfectCount = 0;
private int greatCount = 0;
private int goodCount = 0;
private int missCount = 0;
private int earnedAccuracyPoints = 0;
private float visualScore = 0.0f; private float visualScore = 0.0f;
private int scoreTweenID = -1; private int scoreTweenID = -1;
private int loaderTweenID = -1; private int loaderTweenID = -1;
private BeatJudgement lastJudgement = BeatJudgement.Perfect;
private float judgementTimer = 0.0f;
private Text progressLabel = null;
private Text rankLabel = null;
private Image progressBarBackground = null;
private Image progressBarFill = null;
private Vector3 comboBaseScale = Vector3.one;
private float songCurrentTime = 0.0f;
private float songDuration = 0.0f;
private bool resultFinalized = false;
private bool destroyed = false; private bool destroyed = false;
private static bool hasPendingSliceTiming = false;
private static float pendingSliceTiming = 0.0f;
private static Font hudFont = null;
private Image ringBackground = null;
public int CurrentScore public int CurrentScore
{ {
get get
{ {
return currentScore; float accuracyRatio = AccuracyPercent / 100.0f;
float comboRatio = totalNoteCount > 0
? maxCombo / (float)totalNoteCount
: 0.0f;
return Mathf.RoundToInt(800000.0f * accuracyRatio + 200000.0f * comboRatio);
} }
} }
public float AccuracyPercent
{
get
{
int denominatorNotes = totalNoteCount > 0 ? totalNoteCount : judgedNoteCount;
if (denominatorNotes <= 0)
return 100.0f;
return (float)earnedAccuracyPoints / (denominatorNotes * 1000) * 100.0f;
}
}
public string Rank
{
get
{
if (CurrentScore >= MaxCourseScore) return "M";
float accuracy = AccuracyPercent;
if (accuracy >= 98.0f) return "S+";
if (accuracy >= 95.0f) return "S";
if (accuracy >= 90.0f) return "A";
if (accuracy >= 80.0f) return "B";
if (accuracy >= 70.0f) return "C";
if (accuracy >= 60.0f) return "D";
return "F";
}
}
public int MaxCombo => maxCombo;
public string RankColorHex => GetRankColorHex();
private void Awake() private void Awake()
{ {
maxMultiplier = VR_BeatManager.instance.GameSettings.MaxMultiplier; maxMultiplier = VR_BeatManager.instance.GameSettings.MaxMultiplier;
scorePerHit = VR_BeatManager.instance.GameSettings.ScorePerHit;
errorLimit = VR_BeatManager.instance.GameSettings.ErrorLimit; errorLimit = VR_BeatManager.instance.GameSettings.ErrorLimit;
if (multiplierLoader != null)
multiplierLoader.fillAmount = 0.0f; multiplierLoader.fillAmount = 0.0f;
canvasGroup ??= GetComponent<CanvasGroup>() ?? gameObject.AddComponent<CanvasGroup>();
PrepareHud();
}
public static void ReportSliceTiming(float timingErrorSeconds)
{
pendingSliceTiming = timingErrorSeconds;
hasPendingSliceTiming = true;
}
public static void ReportMiss()
{
hasPendingSliceTiming = false;
pendingSliceTiming = 0.0f;
}
public void SetTotalNotes(int noteCount)
{
totalNoteCount = Mathf.Max(0, noteCount);
resultFinalized = false;
UpdateScoreTween();
}
public void ApplyForcedResult(int noteCount, int perfect, int great, int good, int miss, int forcedMaxCombo)
{
totalNoteCount = Mathf.Max(0, noteCount);
perfectCount = Mathf.Max(0, perfect);
greatCount = Mathf.Max(0, great);
goodCount = Mathf.Max(0, good);
missCount = Mathf.Max(0, miss);
judgedNoteCount = perfectCount + greatCount + goodCount + missCount;
earnedAccuracyPoints = perfectCount * 1000 + greatCount * 900 + goodCount * 700;
maxCombo = Mathf.Clamp(forcedMaxCombo, 0, Mathf.Max(totalNoteCount, judgedNoteCount));
currentCombo = maxCombo;
currentMultiplier = missCount > 0 ? 1.0f : GetComboMultiplier(currentCombo);
lastJudgement = missCount > 0 ? BeatJudgement.Miss : BeatJudgement.Perfect;
judgementTimer = 0.45f;
resultFinalized = false;
UpdateScoreTween();
UpdateMultiplierLoaderValue();
}
public void CompleteSong()
{
if (resultFinalized)
return;
resultFinalized = true;
int missedUnjudgedNotes = Mathf.Max(0, totalNoteCount - judgedNoteCount);
if (missedUnjudgedNotes > 0)
{
missCount += missedUnjudgedNotes;
judgedNoteCount += missedUnjudgedNotes;
currentCombo = 0;
currentMultiplier = 1.0f;
}
UpdateScoreTween();
}
public void SetSongProgress(float currentTime, float duration)
{
songDuration = Mathf.Max(0.0f, duration);
songCurrentTime = songDuration > 0.0f
? Mathf.Clamp(currentTime, 0.0f, songDuration)
: Mathf.Max(0.0f, currentTime);
} }
public void OnGameOver() public void OnGameOver()
{ {
gameObject.CancelAllTweens(); gameObject.CancelAllTweens();
canvasGroup.Fade(0.0f, 0.5f).SetEase(Ease.EaseOutExpo).SetOwner(gameObject); if (canvasGroup != null)
{
canvasGroup.alpha = 0.0f;
canvasGroup.interactable = false;
canvasGroup.blocksRaycasts = false;
}
SetSaberVisibility(false);
} }
public void OnGameRestart() public void OnGameRestart()
{ {
ResetThisComponent(); ResetThisComponent();
gameObject.CancelAllTweens(); gameObject.CancelAllTweens();
canvasGroup.Fade(1.0f, 0.5f).SetEase(Ease.EaseOutExpo).SetOwner(gameObject); if (canvasGroup != null)
{
canvasGroup.alpha = 1.0f;
canvasGroup.interactable = true;
canvasGroup.blocksRaycasts = false;
}
SetSaberVisibility(true);
} }
public void ResetThisComponent() public void ResetThisComponent()
{ {
currentMultiplier = 0; currentMultiplier = 1.0f;
currentScore = 0;
acumulateCorrectSlices = 0;
visualScore = 0; visualScore = 0;
acumulateErrors = 0; acumulateErrors = 0;
toNextMultiplierIncrease = 2; judgedNoteCount = 0;
currentCombo = 0;
maxCombo = 0;
perfectCount = 0;
greatCount = 0;
goodCount = 0;
missCount = 0;
earnedAccuracyPoints = 0;
judgementTimer = 0.0f;
resultFinalized = false;
hasPendingSliceTiming = false;
pendingSliceTiming = 0.0f;
if (multiplierLoader != null)
multiplierLoader.fillAmount = 0.0f;
} }
private void Update() private void Update()
{ {
UpdateUI(); UpdateUI();
@@ -79,46 +247,13 @@ namespace VRBeats
if (destroyed) if (destroyed)
return; return;
acumulateErrors = 0; BeatJudgement judgement = ConsumeJudgement();
acumulateCorrectSlices++; RegisterJudgement(judgement);
currentScore += scorePerHit + (scorePerHit * currentMultiplier);
CancelTweenById(scoreTweenID); acumulateErrors = judgement == BeatJudgement.Miss ? acumulateErrors + 1 : 0;
scoreTweenID = PlatinioTween.instance.ValueTween(visualScore, currentScore, scoreFollowTime).SetEase(Ease.EaseOutExpo).SetOnUpdateFloat(delegate (float value)
{
visualScore = value;
}).ID;
UpdateScoreTween();
UpdateMultiplierLoaderValue(); UpdateMultiplierLoaderValue();
if (acumulateCorrectSlices >= toNextMultiplierIncrease)
{
IncreaseMultiplier();
}
}
private void CancelTweenById(int id)
{
if(id != -1)
PlatinioTween.instance.CancelTween(id);
}
private void UpdateMultiplierLoaderValue()
{
if (destroyed)
return;
float multiplierLoaderValue = (float)acumulateCorrectSlices / (float)toNextMultiplierIncrease;
CancelTweenById(loaderTweenID);
loaderTweenID = PlatinioTween.instance.ValueTween(multiplierLoader.fillAmount, multiplierLoaderValue, 1.0f).SetEase(Ease.EaseOutExpo).SetOnUpdateFloat(delegate (float value)
{
if(multiplierLoader != null)
multiplierLoader.fillAmount = value;
}).SetOwner(multiplierLoader.gameObject).ID;
} }
public void OnIncorrectSlice() public void OnIncorrectSlice()
@@ -126,18 +261,57 @@ namespace VRBeats
if (destroyed) if (destroyed)
return; return;
RegisterJudgement(BeatJudgement.Miss);
acumulateErrors++; acumulateErrors++;
acumulateCorrectSlices = 0; currentMultiplier = 1.0f;
currentMultiplier = 0;
toNextMultiplierIncrease = 2;
UpdateScoreTween();
UpdateMultiplierLoaderValue(); UpdateMultiplierLoaderValue();
if (acumulateErrors > errorLimit) if (acumulateErrors > errorLimit)
{
onGameOver.Invoke(); onGameOver.Invoke();
} }
public string BuildResultSummary(int minScoreLength)
{
string score = CurrentScore.ToString("N0");
return $"<line-height=76%><size=300%><color={GetRankColorHex()}>{Rank}</color></size>" +
$"<pos=255><voffset=0.48em><size=92%>{score}</size></voffset>\n" +
$"<pos=255><size=72%><color=#D7F7FF>MAX COMBO {maxCombo}</color></size>";
}
private void CancelTweenById(int id)
{
if (id != -1)
PlatinioTween.instance.CancelTween(id);
}
private void UpdateMultiplierLoaderValue()
{
if (destroyed || multiplierLoader == null)
return;
float multiplierLoaderValue = GetComboTierProgress();
CancelTweenById(loaderTweenID);
loaderTweenID = PlatinioTween.instance.ValueTween(multiplierLoader.fillAmount, multiplierLoaderValue, 1.0f)
.SetEase(Ease.EaseOutExpo)
.SetOnUpdateFloat(delegate (float value)
{
if (multiplierLoader != null)
multiplierLoader.fillAmount = value;
})
.SetOwner(multiplierLoader.gameObject)
.ID;
}
private void UpdateScoreTween()
{
CancelTweenById(scoreTweenID);
scoreTweenID = PlatinioTween.instance.ValueTween(visualScore, CurrentScore, scoreFollowTime)
.SetEase(Ease.EaseOutExpo)
.SetOnUpdateFloat(delegate (float value) { visualScore = value; })
.ID;
} }
private void UpdateUI() private void UpdateUI()
@@ -145,41 +319,331 @@ namespace VRBeats
if (destroyed) if (destroyed)
return; return;
multiplierLabel.text = currentMultiplier.ToString(); if (multiplierLabel != null)
scoreLabel.text = Mathf.CeilToInt( visualScore ).ToString(); multiplierLabel.text = $"x{Mathf.RoundToInt(currentMultiplier)}";
} if (scoreLabel != null)
scoreLabel.text = $"{Mathf.CeilToInt(visualScore):N0}";
if (comboLabel != null)
comboLabel.text = currentCombo > 0
? $"<size=18><color=#E6F8FF>COMBO</color></size>\n<size=42>{currentCombo}</size>"
: "<size=18><color=#E6F8FF>COMBO</color></size>\n<size=42>0</size>";
if (accuracyLabel != null)
accuracyLabel.text = $"{AccuracyPercent:0.0}%";
if (rankLabel != null)
rankLabel.text = $"<color={GetRankColorHex()}>{Rank}</color>";
if (progressLabel != null)
progressLabel.text = songDuration > 0.0f
? $"{FormatTime(songCurrentTime)} / {FormatTime(songDuration)}"
: "";
if (progressBarFill != null)
SetProgressBarFill(songDuration > 0.0f
? Mathf.Clamp01(songCurrentTime / songDuration)
: 0.0f);
private void IncreaseMultiplier() if (judgementLabel == null)
{
if (destroyed)
return; return;
acumulateCorrectSlices = 0; judgementTimer -= Time.deltaTime;
currentMultiplier = Mathf.Min( currentMultiplier + 1 , maxMultiplier ); judgementLabel.text = judgementTimer > 0.0f ? GetJudgementText(lastJudgement) : "";
toNextMultiplierIncrease = (currentMultiplier + 1) * 2; judgementLabel.color = GetJudgementColor(lastJudgement);
PlatinioTween.instance.CancelTween(multiplierLoader.gameObject);
PlatinioTween.instance.ValueTween(multiplierLoader.fillAmount, 1.0f, 1.0f).SetEase(Ease.EaseOutExpo).SetOnUpdateFloat(delegate (float value)
{
if(multiplierLoader != null)
multiplierLoader.fillAmount = value;
}).SetOwner(multiplierLoader.gameObject).SetOnComplete( delegate
{
if (multiplierLabel != null)
{
PlatinioTween.instance.ValueTween(multiplierLoader.fillAmount, 0.0f, 0.5f).SetEase(Ease.EaseOutExpo).SetOnUpdateFloat(delegate (float value)
{
if (multiplierLoader != null)
multiplierLoader.fillAmount = value;
}).SetOwner(multiplierLoader.gameObject);
} }
} ); private BeatJudgement ConsumeJudgement()
{
if (!hasPendingSliceTiming)
return BeatJudgement.Perfect;
float timing = pendingSliceTiming;
hasPendingSliceTiming = false;
pendingSliceTiming = 0.0f;
if (timing <= perfectWindow) return BeatJudgement.Perfect;
if (timing <= greatWindow) return BeatJudgement.Great;
if (timing <= goodWindow) return BeatJudgement.Good;
return BeatJudgement.Good;
} }
private void RegisterJudgement(BeatJudgement judgement)
{
lastJudgement = judgement;
judgementTimer = 0.45f;
judgedNoteCount++;
if (judgement == BeatJudgement.Perfect)
{
perfectCount++;
earnedAccuracyPoints += 1000;
currentCombo++;
}
else if (judgement == BeatJudgement.Great)
{
greatCount++;
earnedAccuracyPoints += 900;
currentCombo++;
}
else if (judgement == BeatJudgement.Good)
{
goodCount++;
earnedAccuracyPoints += 700;
currentCombo++;
}
else
{
missCount++;
currentCombo = 0;
} }
maxCombo = Mathf.Max(maxCombo, currentCombo);
currentMultiplier = judgement == BeatJudgement.Miss
? 1.0f
: Mathf.Min(GetComboMultiplier(currentCombo), Mathf.Max(1.0f, maxMultiplier));
PulseComboLabel(judgement);
}
private static float GetComboMultiplier(int combo)
{
if (combo >= 50) return 1.5f;
if (combo >= 30) return 1.35f;
if (combo >= 15) return 1.2f;
if (combo >= 5) return 1.1f;
return 1.0f;
}
private static string GetJudgementText(BeatJudgement judgement)
{
switch (judgement)
{
case BeatJudgement.Perfect: return "PERFECT";
case BeatJudgement.Great: return "GREAT";
case BeatJudgement.Good: return "GOOD";
default: return "BREAK";
}
}
private static Color GetJudgementColor(BeatJudgement judgement)
{
switch (judgement)
{
case BeatJudgement.Perfect: return new Color(0.25f, 0.95f, 1.0f, 1.0f);
case BeatJudgement.Great: return new Color(0.58f, 1.0f, 0.45f, 1.0f);
case BeatJudgement.Good: return new Color(1.0f, 0.8f, 0.35f, 1.0f);
default: return new Color(1.0f, 0.25f, 0.45f, 1.0f);
}
}
private float GetComboTierProgress()
{
int lower = 0;
int upper = 5;
if (currentCombo >= 50) return 1.0f;
if (currentCombo >= 30) { lower = 30; upper = 50; }
else if (currentCombo >= 15) { lower = 15; upper = 30; }
else if (currentCombo >= 5) { lower = 5; upper = 15; }
return Mathf.InverseLerp(lower, upper, currentCombo);
}
private void PrepareHud()
{
RectTransform rect = transform as RectTransform;
if (applyHudPlacement && rect != null)
rect.anchoredPosition = hudAnchoredPosition;
comboLabel = comboLabel != null ? comboLabel : FindHudText("Combo");
accuracyLabel = accuracyLabel != null ? accuracyLabel : FindHudText("Accuracy");
rankLabel = rankLabel != null ? rankLabel : FindHudText("Rank");
judgementLabel = judgementLabel != null ? judgementLabel : FindHudText("Judgement");
progressLabel = progressLabel != null ? progressLabel : FindHudText("SongProgress");
progressBarBackground = progressBarBackground != null ? progressBarBackground : FindHudImage("SongProgressBarBackground");
progressBarFill = progressBarFill != null ? progressBarFill : FindHudImage("SongProgressBarFill");
if (!createMissingHudLabels)
return;
comboLabel ??= CreateHudText("Combo");
accuracyLabel ??= CreateHudText("Accuracy");
rankLabel ??= CreateHudText("Rank");
judgementLabel ??= CreateHudText("Judgement");
progressLabel ??= CreateHudText("SongProgress");
progressBarBackground ??= CreateHudImage("SongProgressBarBackground");
progressBarFill ??= CreateHudImage("SongProgressBarFill");
ringBackground = ringBackground != null ? ringBackground : FindHudImage("MultiplierRingBg");
ringBackground ??= CreateHudImage("MultiplierRingBg");
ConfigureText(comboLabel, new Vector2(-335.0f, 92.0f), new Vector2(190.0f, 118.0f), 34, Color.white, TextAnchor.MiddleCenter);
ConfigureText(scoreLabel, new Vector2(-335.0f, 10.0f), new Vector2(190.0f, 42.0f), 22, Color.white, TextAnchor.MiddleCenter);
ConfigureText(accuracyLabel, new Vector2(-335.0f, -34.0f), new Vector2(190.0f, 32.0f), 18, new Color(0.84f, 0.94f, 1.0f, 1.0f), TextAnchor.MiddleCenter);
ConfigureText(rankLabel, new Vector2(-335.0f, -92.0f), new Vector2(190.0f, 72.0f), 48, Color.white, TextAnchor.MiddleCenter);
ConfigureText(judgementLabel, new Vector2(0.0f, 118.0f), new Vector2(280.0f, 56.0f), 28, new Color(0.25f, 0.95f, 1.0f, 1.0f), TextAnchor.MiddleCenter);
ConfigureText(multiplierLabel, new Vector2(335.0f, 38.0f), new Vector2(118.0f, 76.0f), 34, Color.white, TextAnchor.MiddleCenter);
ConfigureText(progressLabel, new Vector2(335.0f, -75.0f), new Vector2(180.0f, 30.0f), 17, Color.white, TextAnchor.MiddleCenter);
ConfigureImage(multiplierLoader, new Vector2(335.0f, 38.0f), new Vector2(112.0f, 112.0f), new Color(1.0f, 1.0f, 1.0f, 0.78f));
ConfigureImage(ringBackground, new Vector2(335.0f, 38.0f), new Vector2(112.0f, 112.0f), new Color(1.0f, 1.0f, 1.0f, 0.15f));
if (ringBackground != null && multiplierLoader != null)
{
ringBackground.sprite = multiplierLoader.sprite;
ringBackground.type = Image.Type.Simple;
ringBackground.transform.SetSiblingIndex(multiplierLoader.transform.GetSiblingIndex());
}
ConfigureImage(progressBarBackground, new Vector2(335.0f, -48.0f), new Vector2(ProgressBarWidth, 5.0f), new Color(1.0f, 1.0f, 1.0f, 0.22f));
ConfigureImage(progressBarFill, new Vector2(335.0f - ProgressBarWidth * 0.5f, -48.0f), new Vector2(ProgressBarWidth, 5.0f), Color.white);
if (progressBarFill != null)
{
RectTransform fillRect = progressBarFill.rectTransform;
fillRect.pivot = new Vector2(0.0f, 0.5f);
SetProgressBarFill(0.0f);
}
comboBaseScale = comboLabel != null ? comboLabel.transform.localScale : Vector3.one;
}
private Text CreateHudText(string name)
{
GameObject textObject = new GameObject(name);
textObject.layer = gameObject.layer;
textObject.transform.SetParent(transform, false);
RectTransform rect = textObject.AddComponent<RectTransform>();
textObject.AddComponent<CanvasRenderer>();
Text text = textObject.AddComponent<Text>();
return text;
}
private Image CreateHudImage(string name)
{
GameObject imageObject = new GameObject(name);
imageObject.layer = gameObject.layer;
imageObject.transform.SetParent(transform, false);
imageObject.AddComponent<CanvasRenderer>();
return imageObject.AddComponent<Image>();
}
private static void ConfigureText(Text text, Vector2 anchoredPosition, Vector2 size, int fontSize, Color color, TextAnchor alignment)
{
if (text == null)
return;
if (text.font == null)
text.font = HudFont;
RectTransform rect = text.rectTransform;
rect.anchorMin = new Vector2(0.5f, 0.5f);
rect.anchorMax = new Vector2(0.5f, 0.5f);
rect.anchoredPosition = anchoredPosition;
rect.sizeDelta = size;
text.fontSize = fontSize;
text.color = color;
text.alignment = alignment;
text.horizontalOverflow = HorizontalWrapMode.Overflow;
text.verticalOverflow = VerticalWrapMode.Overflow;
text.raycastTarget = false;
text.supportRichText = true;
text.lineSpacing = 0.86f;
Shadow shadow = text.GetComponent<Shadow>() ?? text.gameObject.AddComponent<Shadow>();
shadow.effectColor = new Color(0.0f, 0.0f, 0.0f, 0.72f);
shadow.effectDistance = new Vector2(3.0f, -3.0f);
}
private static void ConfigureImage(Image image, Vector2 anchoredPosition, Vector2 size)
{
ConfigureImage(image, anchoredPosition, size, new Color(1.0f, 1.0f, 1.0f, 0.85f));
}
private static void ConfigureImage(Image image, Vector2 anchoredPosition, Vector2 size, Color color)
{
if (image == null)
return;
RectTransform rect = image.rectTransform;
rect.anchorMin = new Vector2(0.5f, 0.5f);
rect.anchorMax = new Vector2(0.5f, 0.5f);
rect.anchoredPosition = anchoredPosition;
rect.sizeDelta = size;
image.color = color;
image.raycastTarget = false;
}
private void SetProgressBarFill(float progress)
{
if (progressBarFill == null)
return;
RectTransform rect = progressBarFill.rectTransform;
rect.sizeDelta = new Vector2(ProgressBarWidth * Mathf.Clamp01(progress), rect.sizeDelta.y);
}
private static Font HudFont
{
get
{
if (hudFont == null)
hudFont = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
return hudFont;
}
}
private Text FindHudText(string objectName)
{
Transform child = transform.Find(objectName);
return child != null ? child.GetComponent<Text>() : null;
}
private Image FindHudImage(string objectName)
{
Transform child = transform.Find(objectName);
return child != null ? child.GetComponent<Image>() : null;
}
private string GetRankColorHex()
{
switch (Rank)
{
case "M": return "#E8B7FF";
case "S+": return "#41F2FF";
case "S": return "#FFD95C";
case "A": return "#B9FF72";
case "B": return "#FFE06A";
case "C": return "#FFB15C";
case "D": return "#FF7C7C";
default: return "#A9B7C0";
}
}
private void PulseComboLabel(BeatJudgement judgement)
{
if (comboLabel == null || judgement == BeatJudgement.Miss)
return;
comboLabel.gameObject.CancelAllTweens();
comboLabel.transform.localScale = comboBaseScale * 1.08f;
comboLabel.transform.ScaleTween(comboBaseScale, 0.16f).SetEase(Ease.EaseOutExpo).SetOwner(comboLabel.gameObject);
}
private static string FormatTime(float seconds)
{
int wholeSeconds = Mathf.Max(0, Mathf.FloorToInt(seconds));
int minutes = wholeSeconds / 60;
int remainingSeconds = wholeSeconds % 60;
return $"{minutes}:{remainingSeconds:00}";
}
private static void SetSaberVisibility(bool visible)
{
VR_Saber[] sabers = FindObjectsByType<VR_Saber>(FindObjectsSortMode.None);
for (int i = 0; i < sabers.Length; i++)
{
if (sabers[i] == null)
continue;
if (visible)
sabers[i].MakeVisible();
else
sabers[i].MakeInvisible();
}
}
}
} }
+3 -3
View File
@@ -12,11 +12,11 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 7aabf7bc54d695644952b5c737f1c915, type: 3} m_Script: {fileID: 11500000, guid: 7aabf7bc54d695644952b5c737f1c915, type: 3}
m_Name: Settings m_Name: Settings
m_EditorClassIdentifier: m_EditorClassIdentifier:
rightColor: {r: 0, g: 0.6002884, b: 1, a: 1} rightColor: {r: 0.03, g: 0.32, b: 1, a: 1}
leftColor: {r: 1, g: 0, b: 0, a: 1} leftColor: {r: 1, g: 0, b: 0, a: 1}
glowIntensity: 100 glowIntensity: 40
targetTravelDistance: 40 targetTravelDistance: 40
targetTravelTime: 1.8 targetTravelTime: 3.2
targetTravelEase: 19 targetTravelEase: 19
errorLimit: 7 errorLimit: 7
scorePerHit: 50 scorePerHit: 50
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 6fb30ae126e70f441a51ad0652c5e708
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
Binary file not shown.
+18
View File
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: b1b477339527b404585e10e42a15d653
VideoClipImporter:
externalObjects: {}
serializedVersion: 3
frameRange: 0
startFrame: -1
endFrame: -1
colorSpace: 0
deinterlace: 0
encodeAlpha: 0
flipVertical: 0
flipHorizontal: 0
importAudio: 1
targetSettings: {}
userData:
assetBundleName:
assetBundleVariant:
Binary file not shown.
+18
View File
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: f47e26c36f77476ba803b5158d1b30da
VideoClipImporter:
externalObjects: {}
serializedVersion: 3
frameRange: 0
startFrame: -1
endFrame: -1
colorSpace: 0
deinterlace: 0
encodeAlpha: 0
flipVertical: 0
flipHorizontal: 0
importAudio: 1
targetSettings: {}
userData:
assetBundleName:
assetBundleVariant:
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

+143
View File
@@ -0,0 +1,143 @@
fileFormatVersion: 2
guid: 6765d030cfc5fc84e9523fcd9439be89
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 0
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 0
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: iOS
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:
+108
View File
@@ -0,0 +1,108 @@
# Claude Review Request: Game Scene HUD Polish
## Status
This proposal has mostly been implemented and superseded by the current UI-quality pass.
Implemented since this proposal:
- ScoreCanvas was moved closer to the player.
- Left/right HUD structure was created.
- Multiplier ring background and song progress bar were added.
- Song current time / total time is displayed.
- Result screen polish has become the current priority.
Remaining HUD follow-up:
- Verify HUD readability in Play Mode.
- Consider migrating HUD labels from legacy `UI.Text` to TMP after the result screen is stable.
- Add rank/multiplier micro animations only after static layout is approved.
## Goal
Match the Game scene HUD closer to the provided Beat Saber-style reference:
- Left side: combo, score, accuracy, current rank
- Right side: multiplier ring and remaining/elapsed song time
- Keep the center lane clear for notes and sabers
- Use thin, readable white/cyan text with minimal visual noise
## Current Scene Findings
Unity bridge reports active scene:
- Scene: `Game`
- Playing: `true`
- Main HUD root: `_UI/ScoreCanvas`
- Existing HUD items:
- `_UI/ScoreCanvas/Combo`
- `_UI/ScoreCanvas/Score`
- `_UI/ScoreCanvas/Accuracy`
- `_UI/ScoreCanvas/Rank`
- `_UI/ScoreCanvas/Multiplier`
- `_UI/ScoreCanvas/Image`
- `_UI/ScoreCanvas/SongProgress`
- `_UI/ScoreCanvas/Judgement`
Current `ScoreCanvas` world placement:
- Position: approximately `(5.8, 2.4, 17.8)`
- Rotation: approximately `(354.8, 18.7, 0)`
- Scale: `(0.005, 0.005, 0.005)`
## Proposed Direction
Prefer scene/UI layout polish first. Avoid gameplay logic changes.
1. Split HUD into stable left and right visual groups under `_UI/ScoreCanvas`.
2. Left group layout:
- `COMBO`
- combo number, larger
- score
- accuracy percent
- rank, larger
3. Right group layout:
- circular multiplier ring
- multiplier value centered, e.g. `x4`
- small time bar below
- elapsed / total or remaining time text
4. Move `Judgement` away from persistent HUD, likely near center-top or temporarily shown and faded.
5. Keep HUD outside the note highway and saber swing area.
## Suggested Implementation Options
### Option A: Scene-only first
Adjust Game scene RectTransforms / world canvas placement and existing child positions without changing code.
Pros:
- Lowest risk
- Fast to evaluate visually in Unity
- Matches user request to polish Game scene first
Cons:
- Existing `ScoreManager.PrepareHud()` may overwrite some RectTransform values at runtime if `applyHudPlacement` or label setup runs.
Estimated score: 78/100 unless we confirm code does not overwrite the scene layout.
### Option B: Minimal code-supported layout
Make `ScoreManager` expose a stable Beat Saber HUD layout preset and only adjust positions/sizes of existing labels.
Pros:
- Runtime layout stays consistent
- Existing labels and score data remain unchanged
- Low gameplay risk
Cons:
- Requires code edit, so must pass the 80+ review gate.
Estimated score: 86/100 if scoped only to HUD placement/style and verified in Play mode.
## Codex Recommendation
Start with Option A in Unity scene if possible. If `ScoreManager.PrepareHud()` overwrites the layout during play, move to Option B with a narrow code change.
Codex preliminary score for Option B: 86/100.
Claude Code: please review this proposal and either approve with score >= 80 or suggest revisions before code changes proceed.
+26
View File
@@ -0,0 +1,26 @@
# Collaboration Rules
When changing code in this project, Codex and Claude Code should use a mutual review gate before proceeding.
## Code Change Gate
1. Before editing code, state the intended change and expected impact.
2. Share feedback between Codex and Claude Code when both are active.
3. Score the proposed change out of 100 based on:
- Correctness
- Scope control
- Risk to current Unity scenes and runtime behavior
- Maintainability
- Test or verification plan
4. Proceed only when the agreed score is 80 or higher.
5. If the score is below 80, revise the approach first.
## Current User Preference
Current quality focus:
1. Polish the result screen UI first.
2. Then polish the in-game HUD.
3. Then consider song select UI.
Do not change code unless the review gate above is satisfied.
+183 -385
View File
@@ -1,434 +1,232 @@
# VR Beat Saber 프로젝트 인수인계 문서 # VR Beat Saber Handoff
## 개요 ## Project Status
Meta Quest용 VR Beat Saber 클론. Beat Sage API로 노래를 자동 채보하고, Synology NAS에 업로드/다운로드한다. - Unity: `6000.3.12f1`
이 문서는 **기존 프로젝트(구버전)를 VRBeatsKit 기반 새 프로젝트로 이전**하기 위한 인수인계 자료다. - Branch: `master`
- Remote: `origin = https://whdwo798.synology.me/whdwo798/BeatSaber.git`
- Latest pushed commit before this document update: `fb59fc3 feat: polish result screen UI`
- Current focus: raise overall UI quality, starting from the result screen, then HUD, then song select.
--- ## Collaboration Rule
## 기존 프로젝트 소스 코드 - Code changes should go through a Codex and Claude Code mutual review gate.
- A proposal should score at least 80/100 before implementation.
- Review criteria:
- Correctness
- Scope control
- Unity scene/runtime risk
- Maintainability
- Verification plan
**기존 프로젝트 전체 파일은 아래 git 저장소에서 가져온다.** See `COLLABORATION_RULES.md`.
``` ## Unity / MCP Bridge
https://whdwo798.synology.me/whdwo798/BeatSaber.git
- `Assets/Editor/UnityCodexBridgeServer.cs`
- Bridge now falls back across ports `19744-19748` when the default port is busy.
- `tools/unity-mcp-server/index.mjs`
- MCP server probes the same fallback port range when `UNITY_BRIDGE_URL` is not explicitly set.
- This was added because Unity reported that the bridge socket address was already in use.
## Game Scene HUD
### ScoreCanvas Placement
`Assets/Scenes/Game.unity`
- `ScoreCanvas` was moved closer to the player:
- Z: `17.8 -> 5`
- Scale: `0.005 -> 0.006`
- `applyHudPlacement`: `true -> false`
- This prevents HUD elements from collapsing visually near the center of the tunnel.
### Runtime HUD
`Assets/VRBeatsKit/Scripts/UI/ScoreManager.cs`
- Left HUD:
- Combo
- Score
- Accuracy
- Current rank
- Right HUD:
- Multiplier value
- Multiplier ring and faint ring background
- Song progress bar
- Current time / total time
- Dynamic legacy `Text` labels lazily load `LegacyRuntime.ttf` to avoid missing-font rendering issues.
- The progress bar fills from `0 -> 1` as the song progresses.
## Timing / Completion
`Assets/VRBeatsKit/Scripts/Core/AudioManager.cs`
- Song time now uses scheduled DSP time instead of reading `AudioSource.time`.
- This avoids the Unity warning about requesting `time` from an audio source whose resource is not a clip.
`Assets/Script/SongController.cs`
- Song duration falls back to metadata or last note time when `clip.length` is zero.
- Completion waits until the last note / known duration instead of ending early because of a bad clip length.
- Note layer Y positions were raised so low notes are easier to slice:
```text
lineLayer 0: -0.12
lineLayer 1: +0.22
lineLayer 2: +0.56
``` ```
```bash ## Scoring / Rank Rules
git clone https://whdwo798.synology.me/whdwo798/BeatSaber.git
`Assets/VRBeatsKit/Scripts/UI/ScoreManager.cs`
### Score Formula
```text
CurrentScore = 800000 * accuracyRatio + 200000 * comboRatio
``` ```
> 단, 이 저장소는 구버전 프로젝트다. 새 프로젝트는 VRBeatsKit을 기반으로 재구성하며, - Max score: `1,000,000`
> 위 저장소의 `Assets/Script/` 등 핵심 스크립트를 참고/이식하는 용도로 사용한다. - Accuracy contribution: `800,000`
- Max combo contribution: `200,000`
--- ### Judgement Points
## Git 설정 (새 프로젝트) ```text
Perfect = 1000
새 Unity 프로젝트를 생성한 뒤 **가장 먼저** git을 초기화하고 파일을 커밋해야 한다. Great = 900
Claude Code는 대화 시작 시 `git status` / `git log`를 자동으로 읽어 컨텍스트를 파악한다. Good = 700
커밋이 없으면 Claude가 변경 이력을 추적할 수 없다. Miss = 0
### 초기화 순서
```bash
# 새 프로젝트 루트에서
git init
git remote add origin <GitHub 저장소 URL>
``` ```
### .gitignore ### Timing Windows
기존 프로젝트의 `.gitignore`를 복사하면 된다. 핵심 규칙: ```text
Perfect <= 0.11s
```gitignore Great <= 0.20s
# Unity 표준 Good <= 0.32s
/Library/
/Temp/
/Obj/
/Build/
/Builds/
/Logs/
/UserSettings/
# NAS 비밀번호 — 절대 커밋 금지
/Assets/StreamingAssets/nas_config.json
/Assets/StreamingAssets/nas_config.json.meta
``` ```
### 첫 커밋 ### Combo Multiplier
파일 복사 완료 후: The multiplier is intentionally beginner-friendly:
```bash ```text
git add . 0-4 combo x1.0
git commit -m "init: VRBeatsKit 기반 프로젝트 초기 설정" 5-14 combo x1.1
15-29 combo x1.2
30-49 combo x1.35
50+ combo x1.5
``` ```
이후 기능 단위로 커밋하면 Claude가 `git log`로 작업 이력을 파악한다. ### Rank Thresholds
--- - `M`: final score `>= 1,000,000`
- Other ranks use `AccuracyPercent`:
## 새 프로젝트 구성 방법 ```text
S+ >= 98%
### 전제 조건 S >= 95%
A >= 90%
1. Unity Hub에서 **새 URP 3D 프로젝트** 생성 (기존 프로젝트와 동일 Unity 버전) B >= 80%
2. Asset Store에서 **VRBeatsKit** 임포트 C >= 70%
3. Package Manager에서 아래 패키지 설치: D >= 60%
- XR Interaction Toolkit (3.x) F < 60%
- XR Hands
- OpenXR Plugin
- TextMeshPro
- Unity Input System
### 복사할 파일 (기존 프로젝트 → 새 프로젝트)
아래 폴더/파일을 `Assets/` 아래에 그대로 복사한다.
```
Assets/Script/ ← 아래 "복사 제외" 목록 참고
Assets/Editor/VRBeatSaberSceneBuilder.cs
Assets/StreamingAssets/ ← nas_config.json 포함 (절대 git 커밋 금지)
Assets/Fonts/NanumGothic SDF.asset 및 관련 파일
Assets/360Music/
Assets/Audio/ ← HitSound.wav, MissSound.wav
Assets/Prefab/ ← RED.prefab, BLUE.prefab (추후 VRBeatsKit 큐브로 교체)
``` ```
### 복사 제외 (VRBeatsKit으로 대체) ### Rank Colors
``` ```text
Assets/Script/Saber.cs → VRBeatsKit VR_Saber.cs 사용 M #E8B7FF prism / jewel tone
Assets/Script/Cube.cs → VRBeatsKit VR_BeatCube.cs 사용 S+ #41F2FF cyan neon
S #FFD95C gold
A #B9FF72
B #FFE06A
C #FFB15C
D #FF7C7C
F #A9B7C0
``` ```
--- ## Result Screen
## 전체 씬 구성 `Assets/VRBeatsKit/Scripts/UI/FinalScoreLabel.cs`
``` The result screen is now the first major UI-quality target.
Intro → SongSelect → Game
SongSelect → SongCreator → (NAS 업로드) → SongSelect Current result layout is generated at runtime:
```text
ResultLayoutRoot
RankShadowText
RankDepthText
RankMainText
ResultScoreText
ResultAccuracyText
ResultComboText
``` ```
| 씬 | 역할 | Implemented:
|---|---|
| Intro | 로고 → SongSelect 자동 전환 |
| SongSelect | NAS에서 songs.json 로드, 곡 목록 표시, 다운로드/플레이 |
| Game | 음악 재생 + 큐브 스폰 + 점수/HP + 결과 화면 |
| SongCreator | 음악 파일 선택 → Beat Sage API 채보 → NAS 업로드 |
| MapEditorScene | 맵 에디터 (선택적) |
--- - Existing `Title` is hidden while result content is shown.
- Existing serialized `scoreText` is hidden and used as a font/material source.
- Rank uses layered TMP text for a more dimensional badge.
- Result info now shows:
- `SCORE`
- final score
- `ACCURACY`
- max combo
- Result popup and restart/back buttons were tinted toward a darker translucent cyan UI style.
## 전체 데이터 흐름 Known follow-up:
``` - `tmp.lineSpacing = -8.0f` is aggressive and may need visual tuning if the `SCORE` label overlaps the score value.
[SongCreator] - Button styling is improved by color, but does not yet have a dedicated neon border/glow treatment.
사용자: 음악 파일 선택 (로컬 파일 또는 URL) - The scene appears to contain both result and game-over popup/button sets; verify that shared color changes are intentional.
→ BeatSageUploader: Beat Sage API 채보 요청
POST https://beatsage.com/create
→ GET /heartbeat/{id} 폴링
→ GET /download/{id} → .zip (Normal.dat, Hard.dat, Expert.dat, ExpertPlus.dat)
→ BeatSageConverter: .dat → NoteData 변환
→ NasPublisher: Synology NAS 업로드
songs.json 갱신: /web/beatsaber/songs.json
맵 JSON: /web/beatsaber/maps/Map_{id}_{diff}.json
오디오: /web/beatsaber/music/{id}.mp3
[SongSelect] ## Visual Effects
→ DownloadManager: NAS에서 songs.json 로드
→ 사용자 곡 선택 → GameSession.SelectedSong, GameSession.SelectedDifficulty 설정
→ 다운로드: {id}.mp3 + Map_{id}_{diff}.json → Application.temporaryCachePath/beatsaber/{id}/
[Game] Blue visibility improvements were made in:
→ Spawner.InitGame(): 캐시에서 오디오/맵 로드
→ VRBeatsKit AudioManager AudioSource에 클립 세팅
→ 카운트다운 3→2→1→GO
→ 매 프레임: audioSource.time 기준으로 VR_BeatManager.Spawn() 호출
→ ScoreManager: 히트/미스 집계 → HP → 결과 화면
```
--- - `Assets/VRBeatsKit/Settings/Settings.asset`
- `Assets/VRBeatsKit/Scripts/Core/SaberTrailEffect.cs`
- `Assets/VRBeatsKit/Scripts/Core/SliceTrailEffect.cs`
- `Assets/VRBeatsKit/Scripts/Other/Spark.cs`
## 주요 스크립트 역할 Goal:
### 복사하는 스크립트 (수정 없음) - Keep red strong.
- Make blue saber/trail/spark readable against the bright cyan tunnel.
| 파일 | 역할 | ## UI Quality Direction
|---|---|
| `GameSession.cs` | static 컨테이너 — 씬 간 선택 곡/난이도 전달 |
| `NoteData.cs` | DTO — NoteData, MapData, SongInfo, DifficultyMap 등 |
| `BeatSageConverter.cs` | Beat Sage .dat 형식 → NoteData 변환 |
| `BeatSageUploader.cs` | Beat Sage API 연동 (POST/GET). `LastMetadata` 프로퍼티에 info.dat 파싱 결과 저장. |
| `NasPublisher.cs` | Synology DSM 7.2 API 업로드 |
| `DownloadManager.cs` | NAS → 로컬 캐시 다운로드 |
| `SongLibrary.cs` | 다운로드 상태 추적 (persistentDataPath) |
| `SongSelectManager.cs` | 곡 목록 UI |
| `SongDetailPanel.cs` | 곡 상세 / 다운로드 / 플레이 버튼 |
| `SongCreatorManager.cs` | 크리에이터 UI, 파일 선택, URL 다운로드. title/BPM 수동 입력 불필요 — info.dat에서 자동 추출. 난이도는 항상 4개 전부 생성. |
| `IntroManager.cs` | 인트로 → SongSelect 자동 전환 |
| `DesktopUIMode.cs` | 에디터에서 XR 없이 테스트할 때 UI Raycaster 교체 |
| `ScoreManager.cs` | 싱글턴 — Score/Combo/MaxCombo/Multiplier/HP |
| `ScoreHUD.cs` | 점수/콤보 TMP + HP 슬라이더 |
| `ResultsPanel.cs` | 결과 화면 (CLEAR/FAILED, 랭크 S~D) |
| `SaberGlow.cs` | 검 끝 포인트 라이트 제어 |
| `SaberSkinSelector.cs` | 검 외형 선택 (PlayerPrefs 저장) |
| `CacheManager.cs` | 캐시 정리 유틸 |
### 새로 작성이 필요한 스크립트 Current agreed direction:
| 파일 | 내용 | 1. Finish result screen polish first.
|---|---| 2. Then polish in-game HUD.
| `Spawner.cs` | **VRBeatsKit 통합 버전** — 아래 상세 참고 | 3. Then consider HUD migration from legacy `UI.Text` to TMP for sharper VR rendering.
4. Then redesign song select cards/details.
--- Design principles:
## Spawner.cs 새 버전 작성 방법 (핵심) - Thin readable text.
- Dark translucent panels, not heavy opaque boxes.
- White/cyan baseline UI with rank color accents.
- Large values, small labels.
- Avoid clutter in the note highway.
- Add micro animations only after the static layout is readable.
기존 Spawner는 `cubePrefabs[]`(GameObject)를 Instantiate했다. ## Verification Notes
새 버전은 `VR_BeatManager.instance.Spawn(Spawneable, SpawnEventInfo)`를 호출한다.
### 변경 포인트 - `git diff --check` passed for the latest result UI changes.
- `dotnet build beatSaber.slnx --no-incremental` is not reliable for this Unity project in the current environment because regular .NET build cannot resolve many Unity/TMP/UI/XR assemblies.
- Prefer Unity Editor compile and Play Mode for final verification.
**1. AudioSource 획득 방식 변경** ## Recommended Next Checks
```csharp
// 기존: [SerializeField] AudioSource audioSource;
// 신규: VRBeatsKit AudioManager에서 가져옴
void Awake()
{
var am = FindObjectOfType<VRBeats.AudioManager>();
if (am != null) audioSource = am.GetComponent<AudioSource>();
}
```
**2. 큐브 프리팹 타입 변경** 1. Play the Game scene and finish a song.
```csharp 2. Check result screen layout for `M`, `S+`, `S`, and `F`.
// 기존: public GameObject[] cubePrefabs; (RED=0, BLUE=1) 3. Verify score, accuracy, and max combo do not overlap.
// 신규: 4. Confirm Restart and Back buttons remain clickable.
public VRBeats.Spawneable redCubePrefab; // colorType == 0 5. Check whether result popup and game-over popup color changes should both stay.
public VRBeats.Spawneable blueCubePrefab; // colorType == 1 6. If result screen is stable, move to HUD polish and TMP migration discussion.
```
**3. SpawnNote() 전면 교체**
```csharp
// Beat Saber cutDirection(0~8) → VRBeats Direction 매핑
private static readonly VRBeats.Direction[] DirMap =
{
VRBeats.Direction.Up, // 0
VRBeats.Direction.Down, // 1
VRBeats.Direction.Left, // 2
VRBeats.Direction.Right, // 3
VRBeats.Direction.UpperLeft, // 4
VRBeats.Direction.UpperRight, // 5
VRBeats.Direction.LowerLeft, // 6
VRBeats.Direction.LowerRight, // 7
VRBeats.Direction.Center // 8 (dot)
};
private void SpawnNote(NoteData data)
{
if (VRBeats.VR_BeatManager.instance == null) return;
VRBeats.Spawneable prefab = data.colorType == 0 ? redCubePrefab : blueCubePrefab;
if (prefab == null) return;
var info = new VRBeats.SpawnEventInfo
{
colorSide = data.colorType == 0 ? VRBeats.ColorSide.Right : VRBeats.ColorSide.Left,
hitDirection = DirMap[Mathf.Clamp(data.cutDirection, 0, 8)],
// position: 0~3열 → -0.5~0.5, 0~2행 → -0.5~0.5
position = new Vector3(
(data.position / 3.0f) - 0.5f,
(data.lineLayer / 2.0f) - 0.5f,
0f),
speed = noteSpeed,
useSpark = true
};
VRBeats.VR_BeatManager.instance.Spawn(prefab, info);
}
```
**4. spawnPoints[] 제거** — VR_BeatManager가 PlayZone 기반으로 위치 계산하므로 불필요.
**5. 나머지 로직은 기존과 동일** — InitGame(), CountDown(), LoadAudioClip(), ShowResults() 등
---
## ScoreManager 충돌 없음
- 우리 `ScoreManager.cs` → 전역 네임스페이스
- VRBeatsKit `Scripts/UI/ScoreManager.cs``namespace VRBeats`
- 두 파일이 공존 가능. 우리 ScoreManager를 그대로 사용한다.
---
## NAS 설정
| 항목 | 값 |
|---|---|
| DSM API (내부) | `http://192.168.55.3:5000` |
| DSM API (외부) | `http://whdwo798.synology.me` |
| 정적 파일 서버 | `http://whdwo798.synology.me/beatsaber` |
| NAS 루트 경로 | `/web/beatsaber` |
| 비밀번호 저장 위치 | `Assets/StreamingAssets/nas_config.json` |
**보안 규칙**: `nas_config.json`은 절대 git에 커밋하지 않는다. `.gitignore`에 추가 필수.
```json
{
"host": "http://192.168.55.3:5000",
"publicHost": "http://whdwo798.synology.me",
"account": "계정명",
"password": "비밀번호"
}
```
---
## NAS 파일 구조
```
/web/beatsaber/
├── songs.json ← 전체 곡 목록
├── maps/
│ └── Map_{id}_{difficulty}.json ← 난이도별 맵 (NoteData 배열)
└── music/
└── {id}.mp3 ← 오디오 파일
```
### songs.json 형식
```json
{
"version": "1.0",
"songs": [
{
"id": "uuid",
"title": "곡 제목",
"artist": "아티스트",
"bpm": 120.0,
"duration": 180,
"audioFile": "music/uuid.mp3",
"audioSize": 1234567,
"coverImage": "",
"noteJumpSpeed": 10.0,
"difficulties": {
"normal": { "mapFile": "maps/Map_uuid_normal.json", "mapSize": 0, "noteCount": 0 },
"hard": { "mapFile": "maps/Map_uuid_hard.json", "mapSize": 0, "noteCount": 0 },
"expert": { "mapFile": "maps/Map_uuid_expert.json", "mapSize": 0, "noteCount": 0 },
"expertplus": { "mapFile": "maps/Map_uuid_expertplus.json", "mapSize": 0, "noteCount": 0 }
},
"addedAt": "2026-05-21T00:00:00Z"
}
]
}
```
### 맵 JSON 형식 (Map_{id}_{diff}.json)
```json
{
"target": [
{ "time": 1.23, "position": 1, "lineLayer": 1, "colorType": 0, "cutDirection": 1 }
]
}
```
---
## Beat Sage API
- **Base URL**: `https://beatsage.com`
- **흐름**: `POST /create``GET /heartbeat/{id}` 폴링 (status: "DONE") → `GET /download/{id}` (.zip)
- **지원 난이도**: Normal, Hard, Expert, ExpertPlus
- **zip 내 파일명**: `Normal.dat`, `Hard.dat`, `Expert.dat`, `ExpertPlus.dat`, `info.dat`
- **인증 불필요** (퍼블릭 API)
- **입력 방식 2가지**: `audio_file`(로컬 파일 업로드) 또는 `audio_url`(직접 URL 전달, Beat Sage 서버에서 다운로드)
- **info.dat 활용**: `_beatsPerMinute`(자동 감지), `_songName`, `_songAuthorName` 추출 → `BeatSageUploader.LastMetadata`에 저장. SongCreatorManager에서 이 값을 우선 사용하고 UI 입력이 있으면 override.
---
## ScoreManager 명세
```csharp
public class ScoreManager : MonoBehaviour
{
public static ScoreManager Instance;
public int Score;
public int Combo;
public int MaxCombo;
public int Multiplier; // 1/2/4/8 — 4콤보마다 증가
public int HP; // 기본 100, 미스 시 -10, 0이면 게임오버
public const int MaxHP = 100;
public float HitRate; // notesHit / noteCount (0~1)
public event Action<int, int, int> OnScoreChanged; // score, combo, multiplier
public event Action<int> OnHPChanged;
public event Action OnGameOver;
public void SetNoteCount(int count);
public void RegisterHit();
public void RegisterMiss();
}
```
### 랭크 기준 (ResultsPanel)
| 랭크 | HitRate |
|---|---|
| S | 95% 이상 |
| A | 80% 이상 |
| B | 65% 이상 |
| C | 50% 이상 |
| D | 50% 미만 |
---
## VRBeatsKit 주요 클래스 요약
| 클래스 | 역할 |
|---|---|
| `VR_BeatManager` | 싱글턴 — 큐브 스폰, 색상 설정, GameOver |
| `VR_BeatCube` | 큐브 이동 + 히트/미스 판정 |
| `VR_BeatCubeSpawneable` | 큐브 스폰 설정 (화살표/점, ColorSide) |
| `VR_Saber` | 세이버 슬라이싱 (EzySlice 기반) |
| `SpawnEventInfo` | 스폰 파라미터 (hitDirection, colorSide, position, speed) |
| `AudioManager` | AudioSource + AudioMixer 래퍼 |
| `VR_BeatSettings` | ScriptableObject — 색상, 속도, 멀티플라이어 한도 등 |
### SpawnEventInfo 구조
```csharp
public class SpawnEventInfo {
public Direction hitDirection; // UpperLeft=0,Up=1,UpperRight=2,Left=3,Center=4,Right=5,LowerLeft=6,Down=7,LowerRight=8
public ColorSide colorSide; // Left, Right
public bool useSpark;
public Vector3 position; // -0.5~0.5 정규화 (PlayZone 기준)
public Vector3 rotation;
public float speed;
public int speedMultiplier;
}
```
---
## 로컬 캐시 경로
```
Application.temporaryCachePath/beatsaber/{songId}/
├── {songId}.mp3
├── Map_{songId}_normal.json
├── Map_{songId}_hard.json
├── Map_{songId}_expert.json
└── Map_{songId}_expertplus.json
```
---
## 알려진 주의사항
1. **Game 씬 직접 Play 금지**: `GameSession.SelectedSong == null` → SongSelect로 튕김. 반드시 SongSelect 씬부터 시작.
2. **NAS 업로드**: 수동 multipart body (UploadHandlerRaw) 사용. Unity 기본 multipart는 DSM에서 401 오류.
3. **AudioType.MPEG**: MP3 로딩 시 `UnityWebRequestMultimedia.GetAudioClip(uri, AudioType.MPEG)` 사용.
4. **Unity `??` 연산자**: Unity Object에 `??` 쓰면 fake-null을 못 잡음. 반드시 `if (x == null)` 또는 `TryGetComponent` 사용.
5. **씬 Build Settings 등록 필수**: Intro(0), SongSelect(1), Game(2), SongCreator(3), MapEditorScene(4)
+1
View File
@@ -1,5 +1,6 @@
{ {
"dependencies": { "dependencies": {
"com.unity.2d.sprite": "1.0.0",
"com.unity.ai.navigation": "2.0.11", "com.unity.ai.navigation": "2.0.11",
"com.unity.collab-proxy": "2.11.4", "com.unity.collab-proxy": "2.11.4",
"com.unity.ide.rider": "3.0.39", "com.unity.ide.rider": "3.0.39",
+6
View File
@@ -1,5 +1,11 @@
{ {
"dependencies": { "dependencies": {
"com.unity.2d.sprite": {
"version": "1.0.0",
"depth": 0,
"source": "builtin",
"dependencies": {}
},
"com.unity.ai.navigation": { "com.unity.ai.navigation": {
"version": "2.0.11", "version": "2.0.11",
"depth": 0, "depth": 0,
+65
View File
@@ -0,0 +1,65 @@
# Unity MCP Bridge
The project now has a local Unity Editor bridge plus a Node MCP server.
## Flow
```text
Codex MCP tool
-> tools/unity-mcp-server/index.mjs
-> http://127.0.0.1:19744
-> Assets/Editor/UnityCodexBridgeServer.cs
-> Unity Editor scene, logs, and camera capture
```
The Unity bridge binds only to `127.0.0.1`.
## Unity menu
```text
Tools > Codex Bridge > Start Server
Tools > Codex Bridge > Stop Server
Tools > Codex Bridge > Auto Start
Tools > Codex Bridge > Capture Game View
```
Auto Start is enabled by default.
## MCP setup
Install Node dependencies:
```powershell
cd tools\unity-mcp-server
npm install
```
Add this to Codex MCP config and restart Codex/VS Code:
```toml
[mcp_servers.vrbeats_unity]
command = "node"
args = ["C:\\Users\\User-40\\Desktop\\unity\\work\\BeatSabar\\VRBeatSaber\\tools\\unity-mcp-server\\index.mjs"]
startup_timeout_sec = 30
[mcp_servers.vrbeats_unity.env]
UNITY_BRIDGE_URL = "http://127.0.0.1:19744"
```
## Useful bridge URLs
These can be tested in a browser or with PowerShell while Unity is open:
```text
http://127.0.0.1:19744/health
http://127.0.0.1:19744/logs?count=50
http://127.0.0.1:19744/scene/roots
http://127.0.0.1:19744/scene/objects?query=Canvas&limit=50
http://127.0.0.1:19744/capture?width=1280&height=720
```
Screenshots are saved to:
```text
Captures/latest.png
```
+197 -19
View File
@@ -64,6 +64,8 @@ h4{font-size:12px;font-weight:700;color:var(--mu);text-transform:uppercase;lette
<button class="tab-btn" onclick="show('detail')">SongDetailPanel.cs</button> <button class="tab-btn" onclick="show('detail')">SongDetailPanel.cs</button>
<button class="tab-btn" onclick="show('selectmgr')">SongSelectManager.cs</button> <button class="tab-btn" onclick="show('selectmgr')">SongSelectManager.cs</button>
<button class="tab-btn" onclick="show('marquee')">MarqueeText.cs</button> <button class="tab-btn" onclick="show('marquee')">MarqueeText.cs</button>
<button class="tab-btn" onclick="show('vrpointer')">VRPointerController.cs</button>
<button class="tab-btn" onclick="show('vrptrsetup')">VRPointerSetup.cs</button>
<h2>유틸</h2> <h2>유틸</h2>
<button class="tab-btn" onclick="show('songlibrary')">SongLibrary.cs</button> <button class="tab-btn" onclick="show('songlibrary')">SongLibrary.cs</button>
<button class="tab-btn" onclick="show('desktop')">DesktopUIMode.cs</button> <button class="tab-btn" onclick="show('desktop')">DesktopUIMode.cs</button>
@@ -1002,6 +1004,13 @@ h4{font-size:12px;font-weight:700;color:var(--mu);text-transform:uppercase;lette
[<span class="ty">SerializeField</span>] <span class="kw">private</span> <span class="ty">GameEvent</span> onLevelComplete; [<span class="ty">SerializeField</span>] <span class="kw">private</span> <span class="ty">GameEvent</span> onLevelComplete;
[<span class="ty">SerializeField</span>] <span class="kw">private</span> <span class="ty">TMP_Text</span> countdownText; [<span class="ty">SerializeField</span>] <span class="kw">private</span> <span class="ty">TMP_Text</span> countdownText;
<span class="ann">// Beat Saber 4열/3행 좌표를 VRBeatsKit PlayZone 상대 좌표로 매핑하는 값</span>
<span class="ann">// 기존 0.25 간격은 큐브 실제 폭보다 좁아 인접 라인이 가로로 겹쳤다</span>
<span class="kw">private const float</span> LaneSpacing = <span class="nm">0.42f</span>;
<span class="kw">private const float</span> LayerSpacing = <span class="nm">0.38f</span>;
<span class="kw">private const float</span> HorizontalCenter = <span class="nm">1.5f</span>;
<span class="kw">private const float</span> VerticalCenter = <span class="nm">1f</span>;
<span class="kw">private</span> <span class="ty">AudioManager</span> _audio; <span class="ann">// VRBeatsKit AudioManager: 실제 AudioSource 래핑</span> <span class="kw">private</span> <span class="ty">AudioManager</span> _audio; <span class="ann">// VRBeatsKit AudioManager: 실제 AudioSource 래핑</span>
<span class="ann">// static 프로퍼티: CacheRoot는 DownloadManager와 완전히 동일한 경로를 반환해야 한다</span> <span class="ann">// static 프로퍼티: CacheRoot는 DownloadManager와 완전히 동일한 경로를 반환해야 한다</span>
@@ -1009,8 +1018,8 @@ h4{font-size:12px;font-weight:700;color:var(--mu);text-transform:uppercase;lette
<span class="kw">private void</span> <span class="fn">Start</span>() <span class="kw">private void</span> <span class="fn">Start</span>()
{ {
<span class="ann">// FindObjectOfType: 씬 전체에서 AudioManager를 찾음 (느리지만 Start에서 1번만 호출)</span> <span class="ann">// FindFirstObjectByType: Unity 6 기준 FindObjectOfType 대체 API</span>
_audio = <span class="fn">FindObjectOfType</span>&lt;<span class="ty">AudioManager</span>&gt;(); _audio = <span class="fn">FindFirstObjectByType</span>&lt;<span class="ty">AudioManager</span>&gt;();
<span class="fn">StartCoroutine</span>(<span class="fn">LoadAndPlay</span>()); <span class="fn">StartCoroutine</span>(<span class="fn">LoadAndPlay</span>());
} }
@@ -1047,8 +1056,8 @@ h4{font-size:12px;font-weight:700;color:var(--mu);text-transform:uppercase;lette
<span class="ann">// File.ReadAllText + JsonUtility.FromJson = 동기 파싱. 파일이 작으므로 문제없음</span> <span class="ann">// File.ReadAllText + JsonUtility.FromJson = 동기 파싱. 파일이 작으므로 문제없음</span>
<span class="ty">MapData</span> map = JsonUtility.<span class="fn">FromJson</span>&lt;<span class="ty">MapData</span>&gt;(File.<span class="fn">ReadAllText</span>(mapPath)); <span class="ty">MapData</span> map = JsonUtility.<span class="fn">FromJson</span>&lt;<span class="ty">MapData</span>&gt;(File.<span class="fn">ReadAllText</span>(mapPath));
<span class="kw">if</span> (map?.target == <span class="kw">null</span>) { Debug.<span class="fn">LogError</span>(<span class="st">"Map parse failed"</span>); <span class="kw">yield break</span>; } <span class="kw">if</span> (map?.target == <span class="kw">null</span>) { Debug.<span class="fn">LogError</span>(<span class="st">"Map parse failed"</span>); <span class="kw">yield break</span>; }
<span class="ann">// time 기준으로 오름차순 정렬 → SpawnRoutine이 앞에서부터 순서대로 처리</span> <span class="ann">// time → position → lineLayer 순 정렬. 같은 시간대 노트도 처리 순서가 안정적이다</span>
map.target.<span class="fn">Sort</span>((a, b) =&gt; a.time.<span class="fn">CompareTo</span>(b.time)); map.target.<span class="fn">Sort</span>(<span class="fn">CompareNotes</span>);
<span class="ann">// ── 카운트다운 → 음악 시작 → 스폰 루프 ──────────────────</span> <span class="ann">// ── 카운트다운 → 음악 시작 → 스폰 루프 ──────────────────</span>
<span class="kw">yield return</span> <span class="fn">StartCoroutine</span>(<span class="fn">Countdown</span>()); <span class="kw">yield return</span> <span class="fn">StartCoroutine</span>(<span class="fn">Countdown</span>());
@@ -1096,11 +1105,10 @@ h4{font-size:12px;font-weight:700;color:var(--mu);text-transform:uppercase;lette
<span class="kw">private void</span> <span class="fn">SpawnNote</span>(<span class="ty">NoteData</span> note) <span class="kw">private void</span> <span class="fn">SpawnNote</span>(<span class="ty">NoteData</span> note)
{ {
<span class="ann">// Beat Saber 4열 그리드 → 월드 X 좌표 선형 매핑</span> <span class="ann">// Beat Saber 4열/3행 그리드 → VRBeatsKit 상대 좌표</span>
<span class="ann">// 열0=-0.375, 열1=-0.125, 열2=0.125, 열3=0.375 (중앙 기준 대칭)</span> <span class="ann">// 현재 X: -0.63, -0.21, 0.21, 0.63. 인접 큐브의 가로 겹침을 피하기 위한 폭</span>
<span class="kw">float</span> x = <span class="nm">-0.375f</span> + note.position * <span class="nm">0.25f</span>; <span class="kw">float</span> x = <span class="fn">MapLaneX</span>(note.position);
<span class="ann">// 3행 그리드 → 월드 Y 좌표. 행0=-0.333(아래), 행1=0(중간), 행2=0.333(위)</span> <span class="kw">float</span> y = <span class="fn">MapLayerY</span>(note.lineLayer);
<span class="kw">float</span> y = <span class="nm">-0.333f</span> + note.lineLayer * <span class="nm">0.333f</span>;
<span class="ann">// ★ 핵심: travelTimeOverride 계산</span> <span class="ann">// ★ 핵심: travelTimeOverride 계산</span>
<span class="ann">// 문제: foreach로 동시 노트 2개를 처리하면 1프레임(~16ms) 차이가 남</span> <span class="ann">// 문제: foreach로 동시 노트 2개를 처리하면 1프레임(~16ms) 차이가 남</span>
@@ -1125,6 +1133,28 @@ h4{font-size:12px;font-weight:700;color:var(--mu);text-transform:uppercase;lette
<span class="ty">VR_BeatManager</span>.instance.<span class="fn">Spawn</span>(cubePrefab, info); <span class="ty">VR_BeatManager</span>.instance.<span class="fn">Spawn</span>(cubePrefab, info);
} }
<span class="ann">// 같은 time을 가진 노트가 많아도 정렬 결과가 매번 같도록 비교 기준을 명시</span>
<span class="kw">private static int</span> <span class="fn">CompareNotes</span>(<span class="ty">NoteData</span> a, <span class="ty">NoteData</span> b)
{
<span class="kw">int</span> timeCompare = a.time.<span class="fn">CompareTo</span>(b.time);
<span class="kw">if</span> (timeCompare != <span class="nm">0</span>) <span class="kw">return</span> timeCompare;
<span class="kw">int</span> positionCompare = a.position.<span class="fn">CompareTo</span>(b.position);
<span class="kw">if</span> (positionCompare != <span class="nm">0</span>) <span class="kw">return</span> positionCompare;
<span class="kw">return</span> a.lineLayer.<span class="fn">CompareTo</span>(b.lineLayer);
}
<span class="kw">private static float</span> <span class="fn">MapLaneX</span>(<span class="kw">int</span> position)
{
<span class="kw">int</span> lane = Mathf.<span class="fn">Clamp</span>(position, <span class="nm">0</span>, <span class="nm">3</span>);
<span class="kw">return</span> (lane - HorizontalCenter) * LaneSpacing;
}
<span class="kw">private static float</span> <span class="fn">MapLayerY</span>(<span class="kw">int</span> lineLayer)
{
<span class="kw">int</span> layer = Mathf.<span class="fn">Clamp</span>(lineLayer, <span class="nm">0</span>, <span class="nm">2</span>);
<span class="kw">return</span> (layer - VerticalCenter) * LayerSpacing;
}
<span class="ann">// ── cutDirection 조회 테이블 ──────────────────────────────</span> <span class="ann">// ── cutDirection 조회 테이블 ──────────────────────────────</span>
<span class="ann">// Beat Saber 숫자(0-8) → VRBeatsKit Direction enum</span> <span class="ann">// Beat Saber 숫자(0-8) → VRBeatsKit Direction enum</span>
<span class="ann">// if-else 8개 대신 배열 인덱스로 O(1) 변환. static readonly = 앱 수명 동안 1번만 할당</span> <span class="ann">// if-else 8개 대신 배열 인덱스로 O(1) 변환. static readonly = 앱 수명 동안 1번만 할당</span>
@@ -1160,7 +1190,7 @@ h4{font-size:12px;font-weight:700;color:var(--mu);text-transform:uppercase;lette
<div id="p-audiomgr" class="panel"> <div id="p-audiomgr" class="panel">
<div class="file-header"> <div class="file-header">
<h1>AudioManager.cs <span style="font-size:14px;font-weight:400;color:var(--mu)">(VRBeatsKit)</span></h1> <h1>AudioManager.cs <span style="font-size:14px;font-weight:400;color:var(--mu)">(VRBeatsKit)</span></h1>
<p>VRBeatsKit 내장 오디오 관리자. 우리 코드에서 <code>PlayClip()</code> <code>CurrentTime</code>만 추가했다.</p> <p>VRBeatsKit 내장 오디오 관리자. <code>PlayScheduled()</code> <code>AudioSettings.dspTime</code> 기준 시간을 사용해 음악/노트 싱크 흔들림을 줄인다.</p>
</div> </div>
<div class="cw"><div class="ch"><span>VRBeatsKit/Scripts/Core/AudioManager.cs</span></div><pre> <div class="cw"><div class="ch"><span>VRBeatsKit/Scripts/Core/AudioManager.cs</span></div><pre>
<span class="kw">namespace</span> <span class="ty">VRBeats</span> <span class="kw">namespace</span> <span class="ty">VRBeats</span>
@@ -1174,6 +1204,8 @@ h4{font-size:12px;font-weight:700;color:var(--mu);text-transform:uppercase;lette
[<span class="ty">SerializeField</span>] <span class="kw">private float</span> fadeOutTime = <span class="nm">4.0f</span>; <span class="ann">// 피치 페이드 시간</span> [<span class="ty">SerializeField</span>] <span class="kw">private float</span> fadeOutTime = <span class="nm">4.0f</span>; <span class="ann">// 피치 페이드 시간</span>
<span class="kw">private</span> <span class="ty">AudioSource</span> audioSource = <span class="kw">null</span>; <span class="kw">private</span> <span class="ty">AudioSource</span> audioSource = <span class="kw">null</span>;
<span class="kw">private double</span> scheduledDspStartTime = -<span class="nm">1.0</span>;
<span class="kw">private bool</span> hasScheduledClip = <span class="kw">false</span>;
<span class="kw">private void</span> <span class="fn">Start</span>() <span class="kw">private void</span> <span class="fn">Start</span>()
{ {
@@ -1212,17 +1244,40 @@ h4{font-size:12px;font-weight:700;color:var(--mu);text-transform:uppercase;lette
<span class="kw">public void</span> <span class="fn">SetAudioMixerPitch</span>(<span class="kw">float</span> value) <span class="kw">public void</span> <span class="fn">SetAudioMixerPitch</span>(<span class="kw">float</span> value)
=&gt; audioSource.outputAudioMixerGroup.audioMixer.<span class="fn">SetFloat</span>(<span class="st">"Pitch"</span>, value); =&gt; audioSource.outputAudioMixerGroup.audioMixer.<span class="fn">SetFloat</span>(<span class="st">"Pitch"</span>, value);
<span class="ann">// ★ 우리가 추가한 메서드 ─────────────────────────────────</span>
<span class="ann">// SongController.LoadAndPlay()에서 로드된 AudioClip을 재생할 때 호출</span> <span class="ann">// SongController.LoadAndPlay()에서 로드된 AudioClip을 재생할 때 호출</span>
<span class="ann">// 내부적으로 예약 재생을 사용해 프레임 상태와 무관한 DSP 기준 시작 시간을 만든다</span>
<span class="kw">public void</span> <span class="fn">PlayClip</span>(<span class="ty">AudioClip</span> clip) <span class="kw">public void</span> <span class="fn">PlayClip</span>(<span class="ty">AudioClip</span> clip)
{ {
audioSource.clip = clip; <span class="ann">// 재생할 클립 교체</span> <span class="fn">PlayClipScheduled</span>(clip);
audioSource.<span class="fn">Play</span>(); <span class="ann">// 즉시 재생 시작</span>
} }
<span class="ann">// 현재 재생 위치(초). SpawnRoutine의 WaitUntil 조건에서 매 프레임 읽힌</span> <span class="ann">// AudioSource.Play()는 호출 프레임과 오디오 스레드 타이밍에 따라 시작 오차가 생길 수 있</span>
<span class="ann">// audioSource가 null이면 0 반환 (씬 초기화 중 안전)</span> <span class="ann">// PlayScheduled는 DSP 시간에 맞춰 재생 예약하므로 노트 스폰 기준을 더 안정적으로 잡을 수 있다</span>
<span class="kw">public float</span> CurrentTime =&gt; audioSource != <span class="kw">null</span> ? audioSource.time : <span class="nm">0f</span>; <span class="kw">public double</span> <span class="fn">PlayClipScheduled</span>(<span class="ty">AudioClip</span> clip, <span class="kw">double</span> delaySeconds = <span class="nm">0.1</span>)
{
<span class="fn">ResetThisComponent</span>();
audioSource.<span class="fn">Stop</span>();
audioSource.clip = clip;
audioSource.time = <span class="nm">0.0f</span>;
scheduledDspStartTime = <span class="ty">AudioSettings</span>.dspTime + delaySeconds;
hasScheduledClip = <span class="kw">true</span>;
audioSource.<span class="fn">PlayScheduled</span>(scheduledDspStartTime);
<span class="kw">return</span> scheduledDspStartTime;
}
<span class="ann">// 현재 재생 위치(초). 예약 재생 중이면 AudioSource.time 대신 DSP 시간 차이를 사용한다</span>
<span class="ann">// 예약 시작 전에는 음수일 수 있으나 SpawnRoutine의 spawnAt은 0 이상이라 조기 스폰되지 않는다</span>
<span class="kw">public float</span> CurrentTime
{
<span class="kw">get</span>
{
<span class="kw">if</span> (audioSource == <span class="kw">null</span>) <span class="kw">return</span> <span class="nm">0.0f</span>;
<span class="kw">if</span> (hasScheduledClip) <span class="kw">return</span> (<span class="kw">float</span>)(<span class="ty">AudioSettings</span>.dspTime - scheduledDspStartTime);
<span class="kw">return</span> audioSource.time;
}
}
} }
} }
</pre></div> </pre></div>
@@ -1777,7 +1832,7 @@ h4{font-size:12px;font-weight:700;color:var(--mu);text-transform:uppercase;lette
<span class="kw">if</span> (_cardFont != <span class="kw">null</span>) tTmp.font = _cardFont; <span class="kw">if</span> (_cardFont != <span class="kw">null</span>) tTmp.font = _cardFont;
tTmp.text = song.title; tTmp.fontSize = <span class="nm">5f</span>; tTmp.color = <span class="ty">Color</span>.white; tTmp.text = song.title; tTmp.fontSize = <span class="nm">5f</span>; tTmp.color = <span class="ty">Color</span>.white;
tTmp.overflowMode = <span class="ty">TextOverflowModes</span>.Overflow; <span class="ann">// 영역 넘어도 잘리지 않음 (RectMask2D가 처리)</span> tTmp.overflowMode = <span class="ty">TextOverflowModes</span>.Overflow; <span class="ann">// 영역 넘어도 잘리지 않음 (RectMask2D가 처리)</span>
tTmp.enableWordWrapping = <span class="kw">false</span>; <span class="ann">// 줄바꿈 금지 → 한 줄로</span> tTmp.textWrappingMode = <span class="ty">TextWrappingModes</span>.NoWrap; <span class="ann">// Unity 6/TMP 최신 API. 줄바꿈 금지</span>
titleGO.<span class="fn">AddComponent</span>&lt;<span class="ty">MarqueeText</span>&gt;(); <span class="ann">// 텍스트가 컨테이너보다 길면 자동 스크롤</span> titleGO.<span class="fn">AddComponent</span>&lt;<span class="ty">MarqueeText</span>&gt;(); <span class="ann">// 텍스트가 컨테이너보다 길면 자동 스크롤</span>
<span class="ann">// ★ 클로저 버그 방지: foreach 변수 song을 captured에 복사</span> <span class="ann">// ★ 클로저 버그 방지: foreach 변수 song을 captured에 복사</span>
@@ -1988,6 +2043,129 @@ h4{font-size:12px;font-weight:700;color:var(--mu);text-transform:uppercase;lette
</pre></div> </pre></div>
</div> </div>
<!-- ══════════════════════ VRPointerController.cs ══════════════════════ -->
<div id="p-vrpointer" class="panel">
<div class="file-header">
<h1>VRPointerController.cs</h1>
<p>VR 컨트롤러에서 직접 World Space UI 버튼을 hover/click 처리하는 런타임 포인터. 게임오버 Back/Retry와 SongCreator UI 클릭 안정화용.</p>
</div>
<div class="box box-g"><div class="lbl">핵심 의도</div><p>XR UI 모듈의 씬별 설정에 의존하지 않고, 컨트롤러 forward 레이와 Unity UI <code>Selectable</code>을 직접 교차 검사한다. 클릭은 <code>ExecuteEvents</code><code>Button.onClick.Invoke()</code>를 함께 호출한다.</p></div>
<div class="cw"><div class="ch"><span>VRPointerController.cs</span></div><pre>
[<span class="ty">RequireComponent</span>(<span class="kw">typeof</span>(<span class="ty">LineRenderer</span>))]
<span class="kw">public class</span> <span class="ty">VRPointerController</span> : <span class="ty">MonoBehaviour</span>
{
[<span class="ty">SerializeField</span>] <span class="kw">private bool</span> isRightHand = <span class="kw">true</span>;
[<span class="ty">SerializeField</span>] <span class="kw">private float</span> maxDistance = <span class="nm">50f</span>;
<span class="kw">private</span> <span class="ty">LineRenderer</span> _line;
<span class="kw">private</span> <span class="ty">Selectable</span> _currentHover;
<span class="kw">private bool</span> _prevTrigger, _prevPrimary;
<span class="kw">private void</span> <span class="fn">Awake</span>()
{
_line = <span class="fn">GetComponent</span>&lt;<span class="ty">LineRenderer</span>&gt;();
_line.positionCount = <span class="nm">2</span>;
_line.startWidth = <span class="nm">0.005f</span>;
_line.endWidth = <span class="nm">0.001f</span>;
_line.useWorldSpace = <span class="kw">true</span>;
}
<span class="kw">private void</span> <span class="fn">Update</span>()
{
<span class="kw">bool</span> trigger = <span class="fn">GetButton</span>(<span class="ty">CommonUsages</span>.triggerButton);
<span class="kw">bool</span> primary = <span class="fn">GetButton</span>(<span class="ty">CommonUsages</span>.primaryButton);
<span class="kw">bool</span> triggerDown = trigger &amp;&amp; !_prevTrigger;
<span class="kw">bool</span> primaryDown = primary &amp;&amp; !_prevPrimary;
_prevTrigger = trigger;
_prevPrimary = primary;
<span class="kw">var</span> ray = <span class="kw">new</span> <span class="ty">Ray</span>(transform.position, transform.forward);
<span class="kw">float</span> hitDist = maxDistance;
<span class="ty">Selectable</span> hit = <span class="fn">FindSelectableUnderRay</span>(ray, <span class="kw">ref</span> hitDist);
<span class="fn">UpdateHoverState</span>(hit);
<span class="ann">// 검지 트리거 또는 A/X 버튼으로 현재 hover된 Selectable 클릭</span>
<span class="kw">if</span> ((triggerDown || primaryDown) &amp;&amp; _currentHover != <span class="kw">null</span>)
<span class="fn">Click</span>(_currentHover);
<span class="fn">DrawLine</span>(hitDist);
}
<span class="kw">private static void</span> <span class="fn">Click</span>(<span class="ty">Selectable</span> sel)
{
<span class="kw">var</span> es = <span class="ty">EventSystem</span>.current;
<span class="kw">if</span> (es == <span class="kw">null</span>) <span class="kw">return</span>;
<span class="kw">var</span> eventData = <span class="kw">new</span> <span class="ty">PointerEventData</span>(es);
<span class="ty">ExecuteEvents</span>.<span class="fn">Execute</span>(sel.gameObject, eventData, <span class="ty">ExecuteEvents</span>.pointerDownHandler);
<span class="ty">ExecuteEvents</span>.<span class="fn">Execute</span>(sel.gameObject, eventData, <span class="ty">ExecuteEvents</span>.pointerUpHandler);
<span class="ty">ExecuteEvents</span>.<span class="fn">Execute</span>(sel.gameObject, eventData, <span class="ty">ExecuteEvents</span>.pointerClickHandler);
<span class="ann">// 일부 Button은 ExecuteEvents만으로 onClick까지 가지 않는 경우가 있어 명시 호출</span>
<span class="kw">var</span> btn = sel.<span class="fn">GetComponent</span>&lt;<span class="ty">Button</span>&gt;();
<span class="kw">if</span> (btn != <span class="kw">null</span>) btn.onClick.<span class="fn">Invoke</span>();
}
<span class="ann">// Selectable의 RectTransform 월드 코너와 레이/평면 교차를 직접 계산</span>
<span class="kw">private static</span> <span class="ty">Selectable</span> <span class="fn">FindSelectableUnderRay</span>(<span class="ty">Ray</span> ray, <span class="kw">ref float</span> maxDist) { ... }
<span class="kw">private bool</span> <span class="fn">GetButton</span>(<span class="ty">InputFeatureUsage</span>&lt;<span class="kw">bool</span>&gt; usage) { ... }
}
</pre></div>
</div>
<!-- ══════════════════════ VRPointerSetup.cs ══════════════════════ -->
<div id="p-vrptrsetup" class="panel">
<div class="file-header">
<h1>VRPointerSetup.cs</h1>
<p>모든 씬에서 자동 실행되는 포인터 주입기. 컨트롤러/손 오브젝트를 찾아 <code>VRPointerController</code>를 붙인다.</p>
</div>
<div class="cw"><div class="ch"><span>VRPointerSetup.cs</span></div><pre>
<span class="kw">public class</span> <span class="ty">VRPointerSetup</span> : <span class="ty">MonoBehaviour</span>
{
<span class="kw">private static</span> <span class="ty">VRPointerSetup</span> instance;
[<span class="ty">RuntimeInitializeOnLoadMethod</span>(<span class="ty">RuntimeInitializeLoadType</span>.BeforeSceneLoad)]
<span class="kw">private static void</span> <span class="fn">AutoInject</span>()
{
<span class="kw">if</span> (instance != <span class="kw">null</span>) <span class="kw">return</span>;
<span class="kw">new</span> <span class="ty">GameObject</span>(<span class="st">"[VRPointerSetup]"</span>).<span class="fn">AddComponent</span>&lt;<span class="ty">VRPointerSetup</span>&gt;();
}
<span class="kw">private void</span> <span class="fn">Awake</span>()
{
<span class="kw">if</span> (instance != <span class="kw">null</span> &amp;&amp; instance != <span class="kw">this</span>) { <span class="fn">Destroy</span>(gameObject); <span class="kw">return</span>; }
instance = <span class="kw">this</span>;
<span class="ty">DontDestroyOnLoad</span>(gameObject);
}
<span class="kw">private void</span> <span class="fn">OnEnable</span>() =&gt; <span class="ty">SceneManager</span>.sceneLoaded += <span class="fn">OnSceneLoaded</span>;
<span class="kw">private void</span> <span class="fn">OnDisable</span>() =&gt; <span class="ty">SceneManager</span>.sceneLoaded -= <span class="fn">OnSceneLoaded</span>;
<span class="kw">private static void</span> <span class="fn">SetupScene</span>(<span class="ty">Scene</span> scene)
{
<span class="kw">bool</span> isGameScene = scene.name == <span class="st">"Game"</span>;
<span class="fn">SetupControllers</span>(disabledByDefault: isGameScene);
}
<span class="ann">// Game 씬에서는 게임오버 전까지 포인터 비활성. VR_InteractorController가 GameOver 때 켠다</span>
<span class="kw">private static void</span> <span class="fn">SetupControllers</span>(<span class="kw">bool</span> disabledByDefault)
{
<span class="kw">foreach</span> (<span class="kw">var</span> go <span class="kw">in</span> <span class="fn">FindObjectsByType</span>&lt;<span class="ty">GameObject</span>&gt;(<span class="ty">FindObjectsSortMode</span>.None))
{
<span class="kw">bool</span> isRight = go.name.<span class="fn">Contains</span>(<span class="st">"Right"</span>);
<span class="kw">bool</span> isLeft = go.name.<span class="fn">Contains</span>(<span class="st">"Left"</span>);
<span class="kw">if</span> (!isRight &amp;&amp; !isLeft) <span class="kw">continue</span>;
<span class="kw">if</span> (go.<span class="fn">GetComponent</span>&lt;<span class="ty">LineRenderer</span>&gt;() == <span class="kw">null</span>) <span class="kw">continue</span>;
<span class="kw">if</span> (go.<span class="fn">GetComponent</span>&lt;<span class="ty">VRPointerController</span>&gt;() != <span class="kw">null</span>) <span class="kw">continue</span>;
<span class="kw">var</span> pointer = go.<span class="fn">AddComponent</span>&lt;<span class="ty">VRPointerController</span>&gt;();
<span class="kw">if</span> (disabledByDefault) pointer.enabled = <span class="kw">false</span>;
}
}
}
</pre></div>
</div>
<!-- ══════════════════════ DesktopUIMode.cs ══════════════════════ --> <!-- ══════════════════════ DesktopUIMode.cs ══════════════════════ -->
<div id="p-desktop" class="panel"> <div id="p-desktop" class="panel">
<div class="file-header"> <div class="file-header">
@@ -2008,7 +2186,7 @@ h4{font-size:12px;font-weight:700;color:var(--mu);text-transform:uppercase;lette
[<span class="ty">RuntimeInitializeOnLoadMethod</span>(<span class="ty">RuntimeInitializeLoadType</span>.AfterSceneLoad)] [<span class="ty">RuntimeInitializeOnLoadMethod</span>(<span class="ty">RuntimeInitializeLoadType</span>.AfterSceneLoad)]
<span class="kw">private static void</span> <span class="fn">AutoCreate</span>() <span class="kw">private static void</span> <span class="fn">AutoCreate</span>()
{ {
<span class="kw">if</span> (<span class="fn">FindObjectOfType</span>&lt;<span class="ty">DesktopUIMode</span>&gt;() != <span class="kw">null</span>) <span class="kw">return</span>; <span class="ann">// 이미 있으면 스킵</span> <span class="kw">if</span> (<span class="fn">FindFirstObjectByType</span>&lt;<span class="ty">DesktopUIMode</span>&gt;() != <span class="kw">null</span>) <span class="kw">return</span>; <span class="ann">// 이미 있으면 스킵</span>
<span class="kw">new</span> <span class="ty">GameObject</span>(<span class="st">"[DesktopUIMode]"</span>).<span class="fn">AddComponent</span>&lt;<span class="ty">DesktopUIMode</span>&gt;(); <span class="kw">new</span> <span class="ty">GameObject</span>(<span class="st">"[DesktopUIMode]"</span>).<span class="fn">AddComponent</span>&lt;<span class="ty">DesktopUIMode</span>&gt;();
} }
@@ -2073,7 +2251,7 @@ h4{font-size:12px;font-weight:700;color:var(--mu);text-transform:uppercase;lette
<span class="kw">if</span> (cam == <span class="kw">null</span>) <span class="ann">// main 카메라 없으면 활성 카메라 중 첫 번째</span> <span class="kw">if</span> (cam == <span class="kw">null</span>) <span class="ann">// main 카메라 없으면 활성 카메라 중 첫 번째</span>
<span class="kw">foreach</span> (<span class="kw">var</span> c <span class="kw">in</span> <span class="fn">FindObjectsByType</span>&lt;<span class="ty">Camera</span>&gt;(<span class="ty">FindObjectsSortMode</span>.None)) <span class="kw">foreach</span> (<span class="kw">var</span> c <span class="kw">in</span> <span class="fn">FindObjectsByType</span>&lt;<span class="ty">Camera</span>&gt;(<span class="ty">FindObjectsSortMode</span>.None))
<span class="kw">if</span> (c.enabled &amp;&amp; c.gameObject.scene.name != <span class="st">"DontDestroyOnLoad"</span>) { cam = c; <span class="kw">break</span>; } <span class="kw">if</span> (c.enabled &amp;&amp; c.gameObject.scene.name != <span class="st">"DontDestroyOnLoad"</span>) { cam = c; <span class="kw">break</span>; }
cam ??= <span class="fn">FindObjectOfType</span>&lt;<span class="ty">Camera</span>&gt;(); <span class="ann">// 최후의 수단</span> cam ??= <span class="fn">FindFirstObjectByType</span>&lt;<span class="ty">Camera</span>&gt;(); <span class="ann">// 최후의 수단</span>
<span class="kw">if</span> (cam == <span class="kw">null</span>) <span class="kw">return</span>; <span class="kw">if</span> (cam == <span class="kw">null</span>) <span class="kw">return</span>;
<span class="kw">foreach</span> (<span class="kw">var</span> canvas <span class="kw">in</span> <span class="fn">FindObjectsByType</span>&lt;<span class="ty">Canvas</span>&gt;(<span class="ty">FindObjectsSortMode</span>.None)) <span class="kw">foreach</span> (<span class="kw">var</span> canvas <span class="kw">in</span> <span class="fn">FindObjectsByType</span>&lt;<span class="ty">Canvas</span>&gt;(<span class="ty">FindObjectsSortMode</span>.None))
+145 -14
View File
@@ -316,6 +316,7 @@
<a href="#naspublisher">NasPublisher.cs</a> <a href="#naspublisher">NasPublisher.cs</a>
<a href="#downloadmanager">DownloadManager.cs</a> <a href="#downloadmanager">DownloadManager.cs</a>
<a href="#songcontroller">SongController.cs</a> <a href="#songcontroller">SongController.cs</a>
<a href="#vrpointer">VR UI 포인터</a>
<div class="section-label">UI</div> <div class="section-label">UI</div>
<a href="#songselectmanager">SongSelectManager.cs</a> <a href="#songselectmanager">SongSelectManager.cs</a>
@@ -333,13 +334,14 @@
<!-- 헤더 --> <!-- 헤더 -->
<div class="page-header"> <div class="page-header">
<h1>VR Beat Saber — 코드 리뷰</h1> <h1>VR Beat Saber — 코드 리뷰</h1>
<p>Unity + Meta Quest 기반 커스텀 비트 세이버 클론 | 학습용 코드 리뷰 문서</p> <p>Unity 6000.3.12f1 + Meta Quest 기반 커스텀 비트 세이버 클론 | 최신 코드 리뷰 문서</p>
<div style="margin-top:12px"> <div style="margin-top:12px">
<span class="badge badge-blue">Unity 2022+</span> <span class="badge badge-blue">Unity 6000.3.12f1</span>
<span class="badge badge-green">C#</span> <span class="badge badge-green">C#</span>
<span class="badge badge-purple">VRBeatsKit</span> <span class="badge badge-purple">VRBeatsKit</span>
<span class="badge badge-yellow">Beat Sage API</span> <span class="badge badge-yellow">Beat Sage API</span>
<span class="badge badge-red">Synology NAS</span> <span class="badge badge-red">Synology NAS</span>
<span class="badge badge-green">Build: 경고 0 / 오류 0</span>
</div> </div>
</div> </div>
@@ -359,7 +361,7 @@
</div> </div>
<div class="card"> <div class="card">
<h4>Game 씬</h4> <h4>Game 씬</h4>
<p>캐시된 MP3 + 맵 JSON 로드 → 카운트다운 → 큐브 스폰 → 스코어 집계.</p> <p>캐시된 MP3 + 맵 JSON 로드 → DSP 기준 오디오 재생 → 큐브 스폰 → 게임오버/결과 UI.</p>
</div> </div>
</div> </div>
</section> </section>
@@ -416,11 +418,13 @@ Application.temporaryCachePath/beatsaber/
<h3>스크립트 의존 관계</h3> <h3>스크립트 의존 관계</h3>
<table> <table>
<tr><th>스크립트</th><th>의존 대상</th><th>의존 방식</th></tr> <tr><th>스크립트</th><th>의존 대상</th><th>의존 방식</th></tr>
<tr><td>SongController</td><td>GameSession, AudioManager, VR_BeatManager</td><td>static / FindObjectOfType / singleton</td></tr> <tr><td>SongController</td><td>GameSession, AudioManager, VR_BeatManager</td><td>static / FindFirstObjectByType / singleton</td></tr>
<tr><td>SongSelectManager</td><td>DownloadManager, SongDetailPanel, SongLibrary</td><td>SerializeField / singleton</td></tr> <tr><td>SongSelectManager</td><td>DownloadManager, SongDetailPanel, SongLibrary</td><td>SerializeField / singleton</td></tr>
<tr><td>NasPublisher</td><td>BeatSageConverter</td><td>static class 직접 호출</td></tr> <tr><td>NasPublisher</td><td>BeatSageConverter</td><td>static class 직접 호출</td></tr>
<tr><td>BeatSageUploader</td><td>BeatSageConverter, NoteData</td><td>static class 직접 호출</td></tr> <tr><td>BeatSageUploader</td><td>BeatSageConverter, NoteData</td><td>static class 직접 호출</td></tr>
<tr><td>DownloadManager</td><td>NoteData (SongInfo)</td><td>파라미터</td></tr> <tr><td>DownloadManager</td><td>NoteData (SongInfo)</td><td>파라미터</td></tr>
<tr><td>VRPointerSetup</td><td>VRPointerController, SceneManager</td><td>RuntimeInitializeOnLoadMethod / sceneLoaded</td></tr>
<tr><td>VRPointerController</td><td>Selectable, EventSystem, XR InputDevice</td><td>직접 Ray/Rect 교차 + ExecuteEvents</td></tr>
</table> </table>
</section> </section>
@@ -752,6 +756,38 @@ list ??= <span class="kw">new</span> <span class="ty">SongsList</span> { version
}</pre> }</pre>
</div> </div>
<h3>오디오 싱크 — DSP 기준 예약 재생</h3>
<div class="code-wrapper">
<div class="code-header"><span class="code-filename">AudioManager.cs — PlayClipScheduled()</span></div>
<pre><span class="kw">public double</span> <span class="fn">PlayClipScheduled</span>(<span class="ty">AudioClip</span> clip, <span class="kw">double</span> delaySeconds = <span class="num">0.1</span>)
{
audioSource.<span class="fn">Stop</span>();
audioSource.clip = clip;
audioSource.time = <span class="num">0.0f</span>;
scheduledDspStartTime = <span class="ty">AudioSettings</span>.dspTime + delaySeconds;
hasScheduledClip = <span class="kw">true</span>;
audioSource.<span class="fn">PlayScheduled</span>(scheduledDspStartTime);
<span class="kw">return</span> scheduledDspStartTime;
}
<span class="kw">public float</span> CurrentTime
{
<span class="kw">get</span>
{
<span class="kw">if</span> (hasScheduledClip)
<span class="kw">return</span> (<span class="kw">float</span>)(<span class="ty">AudioSettings</span>.dspTime - scheduledDspStartTime);
<span class="kw">return</span> audioSource.time;
}
}</pre>
</div>
<div class="point point-green">
<div class="label">개선 완료</div>
<p><code>AudioSource.Play()</code> 대신 <code>PlayScheduled()</code>를 사용해 음악 시작 시점을 DSP 시간에 고정했다. 노트 스폰 기준도 <code>AudioSettings.dspTime</code> 차이로 계산하므로 프레임 상태에 따른 싱크 흔들림이 줄었다.</p>
</div>
<div class="point point-blue"> <div class="point point-blue">
<div class="label">학습 포인트 — 타이밍 보정 기법</div> <div class="label">학습 포인트 — 타이밍 보정 기법</div>
<p> <p>
@@ -784,9 +820,70 @@ list ??= <span class="kw">new</span> <span class="ty">SongsList</span> { version
<h3>위치 계산</h3> <h3>위치 계산</h3>
<div class="code-wrapper"> <div class="code-wrapper">
<div class="code-header"><span class="code-filename">SongController.cs</span></div> <div class="code-header"><span class="code-filename">SongController.cs</span></div>
<pre><span class="cmt">// Beat Saber 그리드 → 월드 좌표 선형 매핑</span> <pre><span class="kw">private const float</span> LaneSpacing = <span class="num">0.42f</span>;
<span class="ty">float</span> x = <span class="num">-0.375f</span> + note.position * <span class="num">0.25f</span>; <span class="cmt">// 열 0→-0.375, 1→-0.125, 2→0.125, 3→0.375</span> <span class="kw">private const float</span> LayerSpacing = <span class="num">0.38f</span>;
<span class="ty">float</span> y = <span class="num">-0.333f</span> + note.lineLayer * <span class="num">0.333f</span>; <span class="cmt">// 행 0→-0.333, 1→0, 2→0.333</span></pre> <span class="kw">private const float</span> HorizontalCenter = <span class="num">1.5f</span>;
<span class="kw">private const float</span> VerticalCenter = <span class="num">1f</span>;
<span class="kw">private static float</span> <span class="fn">MapLaneX</span>(<span class="ty">int</span> position)
{
<span class="ty">int</span> lane = Mathf.<span class="fn">Clamp</span>(position, <span class="num">0</span>, <span class="num">3</span>);
<span class="kw">return</span> (lane - HorizontalCenter) * LaneSpacing;
}
<span class="kw">private static float</span> <span class="fn">MapLayerY</span>(<span class="ty">int</span> lineLayer)
{
<span class="ty">int</span> layer = Mathf.<span class="fn">Clamp</span>(lineLayer, <span class="num">0</span>, <span class="num">2</span>);
<span class="kw">return</span> (layer - VerticalCenter) * LayerSpacing;
}</pre>
</div>
<div class="point point-green">
<div class="label">개선 완료 — 가로 겹침</div>
<p>기존 라인 간격은 <code>0.25</code>였고 큐브 실제 폭은 약 <code>0.36</code>이라 인접 라인이 겹칠 수밖에 없었다. 현재 X 좌표는 대략 <code>-0.63, -0.21, 0.21, 0.63</code>으로 벌어져 가로 겹침을 피한다.</p>
</div>
</section>
<!-- ───────────────────────────────── VR Pointer ── -->
<section id="vrpointer">
<h2>VRPointerController / VRPointerSetup — VR UI 클릭 안정화</h2>
<p>게임오버 Back/Retry와 SongCreator UI 버튼이 컨트롤러로 클릭되지 않던 문제를 해결하기 위해 추가된 런타임 포인터 시스템이다.</p>
<h3>구조</h3>
<div class="flow">
<div class="flow-box">VRPointerSetup<br><small style="color:var(--muted)">BeforeSceneLoad 자동 생성</small></div>
<span class="flow-arrow"></span>
<div class="flow-box">SceneManager.sceneLoaded</div>
<span class="flow-arrow"></span>
<div class="flow-box">Controller/Hand + LineRenderer 탐색</div>
<span class="flow-arrow"></span>
<div class="flow-box highlight">VRPointerController 주입</div>
</div>
<div class="code-wrapper">
<div class="code-header"><span class="code-filename">VRPointerController.cs — 클릭 처리</span></div>
<pre><span class="kw">private static void</span> <span class="fn">Click</span>(<span class="ty">Selectable</span> sel)
{
<span class="kw">var</span> es = <span class="ty">EventSystem</span>.current;
<span class="kw">var</span> eventData = <span class="kw">new</span> <span class="ty">PointerEventData</span>(es);
<span class="ty">ExecuteEvents</span>.<span class="fn">Execute</span>(sel.gameObject, eventData, <span class="ty">ExecuteEvents</span>.pointerDownHandler);
<span class="ty">ExecuteEvents</span>.<span class="fn">Execute</span>(sel.gameObject, eventData, <span class="ty">ExecuteEvents</span>.pointerUpHandler);
<span class="ty">ExecuteEvents</span>.<span class="fn">Execute</span>(sel.gameObject, eventData, <span class="ty">ExecuteEvents</span>.pointerClickHandler);
<span class="kw">var</span> btn = sel.<span class="fn">GetComponent</span>&lt;<span class="ty">Button</span>&gt;();
<span class="kw">if</span> (btn != <span class="kw">null</span>) btn.onClick.<span class="fn">Invoke</span>();
}</pre>
</div>
<div class="point point-blue">
<div class="label">평가</div>
<p>XR UI 모듈 설정에 의존하지 않고 Selectable을 직접 Ray/Rect 교차 검사하는 방식이라 씬별 Raycaster 설정 차이에 강하다. 단, 커스텀 Selectable이나 비사각형 UI가 늘어나면 GraphicRaycaster 기반으로 재검토할 수 있다.</p>
</div>
<div class="point point-yellow">
<div class="label">실기 확인 필요</div>
<p>Game 씬에서는 포인터가 기본 비활성이고 게임오버 시 <code>VR_InteractorController</code>를 통해 활성화된다. Quest에서 Back/Retry, SongCreator 생성 버튼, 곡 선택 카드 클릭은 직접 확인해야 한다.</p>
</div> </div>
</section> </section>
@@ -875,6 +972,16 @@ btn.onClick.<span class="fn">AddListener</span>(() => <span class="fn">OnCardCli
<td>SongController.CutDirMap, BeatSageUploader.DiffNames</td> <td>SongController.CutDirMap, BeatSageUploader.DiffNames</td>
<td><p>static readonly 배열/딕셔너리로 switch-case를 대체. GC 없음.</p></td> <td><p>static readonly 배열/딕셔너리로 switch-case를 대체. GC 없음.</p></td>
</tr> </tr>
<tr>
<td>Deterministic Sort</td>
<td>SongController.CompareNotes()</td>
<td><p>time이 같은 노트도 position, lineLayer 순으로 정렬해 스폰 처리 순서를 안정화.</p></td>
</tr>
<tr>
<td>Runtime Injection</td>
<td>VRPointerSetup</td>
<td><p>씬에 직접 배치하지 않고 런타임에 컨트롤러 오브젝트를 찾아 포인터 컴포넌트를 주입.</p></td>
</tr>
<tr> <tr>
<td>Upsert</td> <td>Upsert</td>
<td>NasPublisher.PatchSongsJson()</td> <td>NasPublisher.PatchSongsJson()</td>
@@ -938,7 +1045,26 @@ btn.onClick.<span class="fn">AddListener</span>(() => <span class="fn">OnCardCli
<span class="kw">using</span> (<span class="kw">var</span> req = <span class="ty">UnityWebRequest</span>.<span class="fn">Get</span>(url)) { ... }</pre> <span class="kw">using</span> (<span class="kw">var</span> req = <span class="ty">UnityWebRequest</span>.<span class="fn">Get</span>(url)) { ... }</pre>
</div> </div>
<h3>5. Unity ?? 연산자 주의사항</h3> <h3>5. Unity 6 API 전환</h3>
<div class="code-wrapper">
<div class="code-header"><span class="code-filename">deprecated API 정리</span></div>
<pre><span class="cmt">// 이전</span>
FindObjectOfType&lt;<span class="ty">AudioManager</span>&gt;();
FindObjectsOfType&lt;<span class="ty">Canvas</span>&gt;();
tTmp.enableWordWrapping = <span class="kw">false</span>;
<span class="cmt">// 현재</span>
FindFirstObjectByType&lt;<span class="ty">AudioManager</span>&gt;();
FindObjectsByType&lt;<span class="ty">Canvas</span>&gt;(<span class="ty">FindObjectsSortMode</span>.None);
tTmp.textWrappingMode = <span class="ty">TextWrappingModes</span>.NoWrap;</pre>
</div>
<div class="point point-green">
<div class="label">현재 상태</div>
<p><code>dotnet build VRBeatSaber.slnx --no-incremental</code> 기준 경고 0개, 오류 0개다. VRBeatsKit 내부 deprecated API와 미사용 필드 경고까지 정리되어 있다.</p>
</div>
<h3>6. Unity ?? 연산자 주의사항</h3>
<div class="code-wrapper"> <div class="code-wrapper">
<div class="code-header"><span class="code-filename">SaberGlow.cs 버그 사례</span></div> <div class="code-header"><span class="code-filename">SaberGlow.cs 버그 사례</span></div>
<pre><span class="cmt">// 버그: Unity 오브젝트에 ?? 연산자 사용 → Destroyed 오브젝트를 null로 인식 못함</span> <pre><span class="cmt">// 버그: Unity 오브젝트에 ?? 연산자 사용 → Destroyed 오브젝트를 null로 인식 못함</span>
@@ -1010,12 +1136,17 @@ btn.onClick.<span class="fn">AddListener</span>(() => <span class="fn">OnCardCli
<li class="done">SongCreator 씬 — Beat Sage API + NAS 업로드 파이프라인</li> <li class="done">SongCreator 씬 — Beat Sage API + NAS 업로드 파이프라인</li>
<li class="done">SongSelect 씬 — 카드 목록 + 다운로드 + 플레이</li> <li class="done">SongSelect 씬 — 카드 목록 + 다운로드 + 플레이</li>
<li class="done">Game 씬 — SongController, 카운트다운, 큐브 스폰</li> <li class="done">Game 씬 — SongController, 카운트다운, 큐브 스폰</li>
<li class="done">travelTimeOverride — 동시 노트 보정</li> <li class="done">travelTimeOverride — 동시 노트 도착 타이밍 보정</li>
<li class="done">Git remote 설정 (Synology NAS)</li> <li class="done">AudioManager — DSP 기준 PlayScheduled 싱크 개선</li>
<li class="todo">Game 씬 ScoreManager / ScoreHUD 연결</li> <li class="done">VRPointerController/Setup — VR UI hover/click 처리</li>
<li class="todo">Game 씬 ResultsPanel 연결 (CLEAR/FAILED, 랭크)</li> <li class="done">GameOver Back/Retry 버튼 스크립트 참조 복구</li>
<li class="todo">VR 기기 실제 플레이 테스트</li> <li class="done">큐브 가로 간격 보정 — 인접 라인 겹침 방지</li>
<li class="todo">targetTravelTime 1.8 플레이 후 미세 조정</li> <li class="done">C# 빌드 경고 0개 정리</li>
<li class="done">Git remote 설정 및 master/main 최신화</li>
<li class="todo">Quest 실기에서 GameOver Back/Retry 클릭 확인</li>
<li class="todo">Quest 실기에서 SongCreator UI 클릭 확인</li>
<li class="todo">큐브 간격, 세이버 각도, targetTravelTime 1.8 체감 조정</li>
<li class="todo">SongCreator 생성 직후 첫 재생 싱크/로드 로그 추가 검증</li>
</ul> </ul>
</section> </section>
+55
View File
@@ -0,0 +1,55 @@
# VRBeats Unity MCP Server
This MCP server connects Codex to the Unity Editor bridge in this project.
## Unity side
Open the project in Unity. The bridge auto-starts on domain reload and listens only on:
```text
http://127.0.0.1:19744
```
Manual controls are available in Unity:
```text
Tools > Codex Bridge > Start Server
Tools > Codex Bridge > Stop Server
Tools > Codex Bridge > Auto Start
Tools > Codex Bridge > Capture Game View
```
## Install
Run once from this folder:
```powershell
npm install
```
## Codex MCP config
Add a server entry like this to the Codex MCP config, then restart Codex/VS Code so the tool list refreshes:
```toml
[mcp_servers.vrbeats_unity]
command = "node"
args = ["C:\\Users\\User-40\\Desktop\\unity\\work\\BeatSabar\\VRBeatSaber\\tools\\unity-mcp-server\\index.mjs"]
startup_timeout_sec = 30
[mcp_servers.vrbeats_unity.env]
UNITY_BRIDGE_URL = "http://127.0.0.1:19744"
```
## Exposed tools
- `unity_health`
- `unity_capture_game_view`
- `unity_get_console_logs`
- `unity_list_scene_roots`
- `unity_list_scene_objects`
- `unity_get_object`
- `unity_set_transform`
- `unity_set_play_state`
`unity_capture_game_view` writes `Captures/latest.png` in the project root.
@@ -0,0 +1,7 @@
[mcp_servers.vrbeats_unity]
command = "node"
args = ["C:\\Users\\User-40\\Desktop\\unity\\work\\BeatSabar\\VRBeatSaber\\tools\\unity-mcp-server\\index.mjs"]
startup_timeout_sec = 30
[mcp_servers.vrbeats_unity.env]
UNITY_BRIDGE_URL = "http://127.0.0.1:19744"
+207
View File
@@ -0,0 +1,207 @@
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const EXPLICIT_UNITY_BRIDGE_URL = process.env.UNITY_BRIDGE_URL;
let unityBridgeUrl = (EXPLICIT_UNITY_BRIDGE_URL || "http://127.0.0.1:19744").replace(/\/$/, "");
const server = new McpServer({
name: "vrbeats-unity",
version: "0.1.0",
});
function textResult(value) {
const text = typeof value === "string" ? value : JSON.stringify(value, null, 2);
return {
content: [
{
type: "text",
text,
},
],
};
}
async function callUnity(path, options = {}) {
const response = await fetchUnity(path, options);
const rawText = await response.text();
let parsed;
try {
parsed = rawText.length > 0 ? JSON.parse(rawText) : {};
} catch {
parsed = { ok: false, rawText };
}
if (!response.ok) {
const detail = typeof parsed === "object" ? JSON.stringify(parsed, null, 2) : rawText;
throw new Error(`Unity bridge HTTP ${response.status}: ${detail}`);
}
return parsed;
}
async function fetchUnity(path, options = {}) {
const urls = [unityBridgeUrl];
if (!EXPLICIT_UNITY_BRIDGE_URL) {
for (let port = 19744; port <= 19748; port += 1) {
const url = `http://127.0.0.1:${port}`;
if (!urls.includes(url)) {
urls.push(url);
}
}
}
let lastError;
for (const url of urls) {
try {
const response = await fetch(`${url}${path}`, {
method: options.method || "GET",
headers: {
"Content-Type": "application/json",
},
body: options.body === undefined ? undefined : JSON.stringify(options.body),
});
unityBridgeUrl = url;
return response;
} catch (error) {
lastError = error;
}
}
throw lastError;
}
function queryString(params) {
const query = new URLSearchParams();
for (const [key, value] of Object.entries(params)) {
if (value !== undefined && value !== null && value !== "") {
query.set(key, String(value));
}
}
const value = query.toString();
return value.length > 0 ? `?${value}` : "";
}
const Vector3Schema = z.object({
x: z.number(),
y: z.number(),
z: z.number(),
});
server.registerTool(
"unity_health",
{
title: "Unity Bridge Health",
description: "Check whether the Unity Editor bridge is reachable and report play state.",
inputSchema: {},
},
async () => textResult(await callUnity("/health")),
);
server.registerTool(
"unity_capture_game_view",
{
title: "Capture Unity Game View",
description: "Capture the active Unity camera to Captures/latest.png and return the local file path.",
inputSchema: {
width: z.number().int().min(320).max(4096).default(1280),
height: z.number().int().min(180).max(4096).default(720),
},
},
async ({ width = 1280, height = 720 }) => {
const result = await callUnity(`/capture${queryString({ width, height })}`);
return textResult(result);
},
);
server.registerTool(
"unity_get_console_logs",
{
title: "Get Unity Console Logs",
description: "Read recent logs captured by the Unity Editor bridge.",
inputSchema: {
count: z.number().int().min(1).max(250).default(80),
},
},
async ({ count = 80 }) => textResult(await callUnity(`/logs${queryString({ count })}`)),
);
server.registerTool(
"unity_list_scene_roots",
{
title: "List Unity Scene Roots",
description: "List root GameObjects in the active Unity scene.",
inputSchema: {},
},
async () => textResult(await callUnity("/scene/roots")),
);
server.registerTool(
"unity_list_scene_objects",
{
title: "List Unity Scene Objects",
description: "List GameObjects in the active Unity scene, optionally filtered by hierarchy path text.",
inputSchema: {
query: z.string().optional().describe("Case-insensitive path filter."),
limit: z.number().int().min(1).max(500).default(120),
},
},
async ({ query = "", limit = 120 }) =>
textResult(await callUnity(`/scene/objects${queryString({ query, limit })}`)),
);
server.registerTool(
"unity_get_object",
{
title: "Get Unity Object",
description: "Read transform and component information for a GameObject by hierarchy path.",
inputSchema: {
path: z.string().min(1).describe("Hierarchy path, for example Canvas/Panel/Button."),
},
},
async ({ path }) => textResult(await callUnity(`/object${queryString({ path })}`)),
);
server.registerTool(
"unity_set_transform",
{
title: "Set Unity Object Transform",
description: "Set world/local position, rotation, or local scale for a GameObject by hierarchy path.",
inputSchema: {
path: z.string().min(1),
position: Vector3Schema.optional(),
localPosition: Vector3Schema.optional(),
rotationEuler: Vector3Schema.optional(),
localRotationEuler: Vector3Schema.optional(),
scale: Vector3Schema.optional(),
},
},
async (input) => textResult(await callUnity("/transform", { method: "POST", body: input })),
);
server.registerTool(
"unity_set_play_state",
{
title: "Set Unity Play State",
description: "Start, pause, resume, or stop Unity Play Mode.",
inputSchema: {
state: z.enum(["play", "pause", "resume", "stop"]),
},
},
async ({ state }) => {
const endpoint = state === "stop" ? "/stop" : state === "pause" ? "/pause" : "/play";
return textResult(await callUnity(endpoint));
},
);
const transport = new StdioServerTransport();
await server.connect(transport);
File diff suppressed because it is too large Load Diff
+19
View File
@@ -0,0 +1,19 @@
{
"name": "vrbeats-unity-mcp-server",
"version": "0.1.0",
"private": true,
"type": "module",
"bin": {
"vrbeats-unity-mcp": "./index.mjs"
},
"engines": {
"node": ">=18"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.1",
"zod": "^3.25.0"
},
"scripts": {
"start": "node ./index.mjs"
}
}