Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c335995a9a | |||
| 72dad1ce4c | |||
| fb59fc36f7 | |||
| b46ccddbdb | |||
| c4330aa544 | |||
| 03105a4f85 | |||
| ee34d79a66 |
+16
@@ -20,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
|
||||||
|
|||||||
@@ -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:
|
||||||
+108
-42
@@ -225,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
|
||||||
@@ -393,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
|
||||||
@@ -428,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
|
||||||
@@ -587,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
|
||||||
@@ -627,12 +632,12 @@ PrefabInstance:
|
|||||||
- target: {fileID: 7481659171502770090, guid: d614df01a29ad3e45bb831298dfbad2c,
|
- target: {fileID: 7481659171502770090, guid: d614df01a29ad3e45bb831298dfbad2c,
|
||||||
type: 3}
|
type: 3}
|
||||||
propertyPath: m_LocalRotation.w
|
propertyPath: m_LocalRotation.w
|
||||||
value: 0.9239
|
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.3827
|
value: 0.7071068
|
||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: 7481659171502770090, guid: d614df01a29ad3e45bb831298dfbad2c,
|
- target: {fileID: 7481659171502770090, guid: d614df01a29ad3e45bb831298dfbad2c,
|
||||||
type: 3}
|
type: 3}
|
||||||
@@ -647,7 +652,7 @@ PrefabInstance:
|
|||||||
- target: {fileID: 7481659171502770090, guid: d614df01a29ad3e45bb831298dfbad2c,
|
- target: {fileID: 7481659171502770090, guid: d614df01a29ad3e45bb831298dfbad2c,
|
||||||
type: 3}
|
type: 3}
|
||||||
propertyPath: m_LocalEulerAnglesHint.x
|
propertyPath: m_LocalEulerAnglesHint.x
|
||||||
value: 45
|
value: 90
|
||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: 7481659171502770090, guid: d614df01a29ad3e45bb831298dfbad2c,
|
- target: {fileID: 7481659171502770090, guid: d614df01a29ad3e45bb831298dfbad2c,
|
||||||
type: 3}
|
type: 3}
|
||||||
@@ -1252,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}
|
||||||
@@ -1263,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
|
||||||
@@ -1284,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
|
||||||
@@ -2108,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
|
||||||
@@ -2143,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
|
||||||
@@ -2380,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
|
||||||
@@ -2652,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:
|
||||||
@@ -2692,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
|
||||||
@@ -2717,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
|
||||||
@@ -2752,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
|
||||||
@@ -2825,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
|
||||||
@@ -3790,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
|
||||||
@@ -3803,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
|
||||||
@@ -4109,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
|
||||||
@@ -4376,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
|
||||||
@@ -4414,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,
|
||||||
@@ -4457,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
|
||||||
@@ -4482,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
|
||||||
@@ -4517,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
|
||||||
@@ -4857,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
|
||||||
@@ -4865,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
|
||||||
@@ -5157,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
|
||||||
@@ -5165,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
|
||||||
@@ -5277,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}
|
||||||
@@ -5289,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
|
||||||
@@ -5317,27 +5383,27 @@ PrefabInstance:
|
|||||||
- 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}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a2e8c518ec2f4a03a6d820774b475ce0
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -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,14 +26,20 @@ 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
|
||||||
@@ -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) + "...";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -14,21 +14,31 @@ public class SongController : MonoBehaviour
|
|||||||
[SerializeField] private TMP_Text countdownText;
|
[SerializeField] private TMP_Text countdownText;
|
||||||
|
|
||||||
private const float LaneSpacing = 0.42f;
|
private const float LaneSpacing = 0.42f;
|
||||||
private const float LayerSpacing = 0.38f;
|
private const float LayerSpacing = 0.34f;
|
||||||
private const float HorizontalCenter = 1.5f;
|
private const float HorizontalCenter = 1.5f;
|
||||||
private const float VerticalCenter = 1f;
|
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 = FindFirstObjectByType<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;
|
||||||
@@ -53,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);
|
||||||
@@ -68,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;
|
||||||
}
|
}
|
||||||
|
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);
|
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()
|
||||||
@@ -106,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);
|
||||||
}
|
}
|
||||||
@@ -118,7 +160,7 @@ public class SongController : MonoBehaviour
|
|||||||
float y = MapLayerY(note.lineLayer);
|
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
|
||||||
@@ -126,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,
|
||||||
};
|
};
|
||||||
@@ -147,6 +189,9 @@ public class SongController : MonoBehaviour
|
|||||||
return a.lineLayer.CompareTo(b.lineLayer);
|
return a.lineLayer.CompareTo(b.lineLayer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsForcedResultMap(MapData map)
|
||||||
|
=> map?.forcedResult != null && map.forcedResult.enabled;
|
||||||
|
|
||||||
private static float MapLaneX(int position)
|
private static float MapLaneX(int position)
|
||||||
{
|
{
|
||||||
int lane = Mathf.Clamp(position, 0, 3);
|
int lane = Mathf.Clamp(position, 0, 3);
|
||||||
@@ -156,7 +201,7 @@ public class SongController : MonoBehaviour
|
|||||||
private static float MapLayerY(int lineLayer)
|
private static float MapLayerY(int lineLayer)
|
||||||
{
|
{
|
||||||
int layer = Mathf.Clamp(lineLayer, 0, 2);
|
int layer = Mathf.Clamp(lineLayer, 0, 2);
|
||||||
return (layer - VerticalCenter) * LayerSpacing;
|
return VerticalOffset + (layer - VerticalCenter) * LayerSpacing;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Beat Saber cutDirection → VRBeats Direction
|
// Beat Saber cutDirection → VRBeats Direction
|
||||||
@@ -177,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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
@@ -166,41 +194,37 @@ public class SongSelectManager : MonoBehaviour
|
|||||||
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"
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -11,10 +11,20 @@ namespace VRBeats
|
|||||||
{
|
{
|
||||||
[SerializeField] private bool isRightHand = true;
|
[SerializeField] private bool isRightHand = true;
|
||||||
[SerializeField] private float maxDistance = 50f;
|
[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 LineRenderer _line;
|
||||||
private bool _prevTrigger;
|
private bool _prevTrigger;
|
||||||
private Selectable _currentHover;
|
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 NormalColor = new Color(1f, 1f, 1f, 0.8f);
|
||||||
private static readonly Color HoverColor = new Color(0.3f, 0.8f, 1f, 1f);
|
private static readonly Color HoverColor = new Color(0.3f, 0.8f, 1f, 1f);
|
||||||
@@ -40,6 +50,7 @@ namespace VRBeats
|
|||||||
private void Start()
|
private void Start()
|
||||||
{
|
{
|
||||||
// Awake 이후 reflection으로 isRightHand가 설정되므로 Start에서 로그
|
// Awake 이후 reflection으로 isRightHand가 설정되므로 Start에서 로그
|
||||||
|
if (debugLogging)
|
||||||
Debug.Log($"[VRPointer] Start — {gameObject.name} / isRightHand={isRightHand}");
|
Debug.Log($"[VRPointer] Start — {gameObject.name} / isRightHand={isRightHand}");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,12 +67,15 @@ namespace VRBeats
|
|||||||
private void Update()
|
private void Update()
|
||||||
{
|
{
|
||||||
// 3초마다 연결된 디바이스 목록 출력
|
// 3초마다 연결된 디바이스 목록 출력
|
||||||
|
if (debugLogging)
|
||||||
|
{
|
||||||
_deviceLogTimer += Time.deltaTime;
|
_deviceLogTimer += Time.deltaTime;
|
||||||
if (_deviceLogTimer >= 3f)
|
if (_deviceLogTimer >= 3f)
|
||||||
{
|
{
|
||||||
_deviceLogTimer = 0f;
|
_deviceLogTimer = 0f;
|
||||||
LogConnectedDevices();
|
LogConnectedDevices();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool trigger = GetButton(CommonUsages.triggerButton);
|
bool trigger = GetButton(CommonUsages.triggerButton);
|
||||||
bool grip = GetButton(CommonUsages.gripButton);
|
bool grip = GetButton(CommonUsages.gripButton);
|
||||||
@@ -70,31 +84,41 @@ namespace VRBeats
|
|||||||
bool thumbstick = GetButton(CommonUsages.primary2DAxisClick);
|
bool thumbstick = GetButton(CommonUsages.primary2DAxisClick);
|
||||||
|
|
||||||
bool triggerDown = trigger && !_prevTrigger;
|
bool triggerDown = trigger && !_prevTrigger;
|
||||||
|
bool triggerUp = !trigger && _prevTrigger;
|
||||||
bool gripDown = grip && !_prevGrip;
|
bool gripDown = grip && !_prevGrip;
|
||||||
bool primaryDown = primary && !_prevPrimary;
|
bool primaryDown = primary && !_prevPrimary;
|
||||||
bool secondaryDown = secondary && !_prevSecondary;
|
bool secondaryDown = secondary && !_prevSecondary;
|
||||||
bool thumbstickDown = thumbstick && !_prevThumbstick;
|
bool thumbstickDown = thumbstick && !_prevThumbstick;
|
||||||
|
|
||||||
_prevTrigger = trigger;
|
|
||||||
_prevGrip = grip;
|
|
||||||
_prevPrimary = primary;
|
|
||||||
_prevSecondary = secondary;
|
|
||||||
_prevThumbstick = thumbstick;
|
|
||||||
|
|
||||||
string hand = isRightHand ? "R" : "L";
|
string hand = isRightHand ? "R" : "L";
|
||||||
|
if (debugLogging)
|
||||||
|
{
|
||||||
if (triggerDown) Debug.Log($"[VRPointer:{hand}] 검지 트리거 눌림");
|
if (triggerDown) Debug.Log($"[VRPointer:{hand}] 검지 트리거 눌림");
|
||||||
if (gripDown) Debug.Log($"[VRPointer:{hand}] 그립(중지) 눌림");
|
if (gripDown) Debug.Log($"[VRPointer:{hand}] 그립(중지) 눌림");
|
||||||
if (primaryDown) Debug.Log($"[VRPointer:{hand}] {(isRightHand ? "A" : "X")} 버튼 눌림");
|
if (primaryDown) Debug.Log($"[VRPointer:{hand}] {(isRightHand ? "A" : "X")} 버튼 눌림");
|
||||||
if (secondaryDown) Debug.Log($"[VRPointer:{hand}] {(isRightHand ? "B" : "Y")} 버튼 눌림");
|
if (secondaryDown) Debug.Log($"[VRPointer:{hand}] {(isRightHand ? "B" : "Y")} 버튼 눌림");
|
||||||
if (thumbstickDown)Debug.Log($"[VRPointer:{hand}] 조이스틱 클릭 눌림");
|
if (thumbstickDown) Debug.Log($"[VRPointer:{hand}] 조이스틱 클릭 눌림");
|
||||||
|
}
|
||||||
|
|
||||||
Ray ray = new Ray(transform.position, transform.forward);
|
Ray ray = new Ray(transform.position, transform.forward);
|
||||||
float hitDist = maxDistance;
|
float selectableHitDist = maxDistance;
|
||||||
|
float scrollHitDist = maxDistance;
|
||||||
|
|
||||||
Selectable hit = FindSelectableUnderRay(ray, ref hitDist);
|
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 (hit != _currentHover)
|
if (debugLogging && hit != _currentHover)
|
||||||
{
|
{
|
||||||
Debug.Log(hit != null
|
Debug.Log(hit != null
|
||||||
? $"[VRPointer] HOVER → {hit.gameObject.name}"
|
? $"[VRPointer] HOVER → {hit.gameObject.name}"
|
||||||
@@ -103,16 +127,21 @@ namespace VRBeats
|
|||||||
|
|
||||||
UpdateHoverState(hit);
|
UpdateHoverState(hit);
|
||||||
|
|
||||||
// 검지 트리거 또는 A/X 버튼으로 클릭
|
if (triggerUp && _dragScrollRect != null)
|
||||||
if (triggerDown || primaryDown)
|
EndScrollDrag(hand, ray);
|
||||||
|
|
||||||
|
// 검지 트리거 또는 A/X 버튼으로 클릭.
|
||||||
|
// ScrollRect 위의 검지 트리거는 드래그/클릭 판별을 위해 release 시점에 처리한다.
|
||||||
|
if ((triggerDown && !beganScrollDrag) || primaryDown)
|
||||||
{
|
{
|
||||||
if (_currentHover != null)
|
if (_currentHover != null)
|
||||||
{
|
{
|
||||||
string btn = triggerDown ? "검지 트리거" : (isRightHand ? "A" : "X");
|
string btn = triggerDown && !beganScrollDrag ? "검지 트리거" : (isRightHand ? "A" : "X");
|
||||||
|
if (debugLogging)
|
||||||
Debug.Log($"[VRPointer:{hand}] CLICK [{btn}] → {_currentHover.gameObject.name}");
|
Debug.Log($"[VRPointer:{hand}] CLICK [{btn}] → {_currentHover.gameObject.name}");
|
||||||
Click(_currentHover);
|
Click(_currentHover);
|
||||||
}
|
}
|
||||||
else
|
else if (debugLogging)
|
||||||
{
|
{
|
||||||
Debug.Log($"[VRPointer:{hand}] CLICK — 레이 아래 버튼 없음 " +
|
Debug.Log($"[VRPointer:{hand}] CLICK — 레이 아래 버튼 없음 " +
|
||||||
$"pos={transform.position:F2} fwd={transform.forward:F2}");
|
$"pos={transform.position:F2} fwd={transform.forward:F2}");
|
||||||
@@ -121,6 +150,12 @@ namespace VRBeats
|
|||||||
}
|
}
|
||||||
|
|
||||||
DrawLine(hitDist);
|
DrawLine(hitDist);
|
||||||
|
|
||||||
|
_prevTrigger = trigger;
|
||||||
|
_prevGrip = grip;
|
||||||
|
_prevPrimary = primary;
|
||||||
|
_prevSecondary = secondary;
|
||||||
|
_prevThumbstick = thumbstick;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LogConnectedDevices()
|
private void LogConnectedDevices()
|
||||||
@@ -222,6 +257,7 @@ namespace VRBeats
|
|||||||
foreach (Selectable sel in Selectable.allSelectablesArray)
|
foreach (Selectable sel in Selectable.allSelectablesArray)
|
||||||
{
|
{
|
||||||
if (!sel.gameObject.activeInHierarchy || !sel.interactable) continue;
|
if (!sel.gameObject.activeInHierarchy || !sel.interactable) continue;
|
||||||
|
if (!IsOnEnabledCanvas(sel)) continue;
|
||||||
|
|
||||||
var rt = sel.GetComponent<RectTransform>();
|
var rt = sel.GetComponent<RectTransform>();
|
||||||
if (rt == null) continue;
|
if (rt == null) continue;
|
||||||
@@ -260,6 +296,189 @@ namespace VRBeats
|
|||||||
return r >= 0f && r <= 1f && u >= 0f && u <= 1f;
|
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)
|
private bool GetButton(InputFeatureUsage<bool> usage)
|
||||||
{
|
{
|
||||||
var chars = InputDeviceCharacteristics.Controller |
|
var chars = InputDeviceCharacteristics.Controller |
|
||||||
@@ -274,5 +493,20 @@ namespace VRBeats
|
|||||||
devices[0].TryGetFeatureValue(usage, out bool pressed);
|
devices[0].TryGetFeatureValue(usage, out bool pressed);
|
||||||
return 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,9 @@ namespace VRBeats
|
|||||||
if (!isRight && !isLeft) continue;
|
if (!isRight && !isLeft) continue;
|
||||||
if (!name.Contains("Controller") && !name.Contains("Hand")) continue;
|
if (!name.Contains("Controller") && !name.Contains("Hand")) continue;
|
||||||
if (go.GetComponent<LineRenderer>() == null) continue;
|
if (go.GetComponent<LineRenderer>() == null) continue;
|
||||||
|
|
||||||
|
DisableToolkitPointerComponents(go);
|
||||||
|
|
||||||
if (go.GetComponent<VRPointerController>() != null) continue;
|
if (go.GetComponent<VRPointerController>() != null) continue;
|
||||||
|
|
||||||
var pointer = go.AddComponent<VRPointerController>();
|
var pointer = go.AddComponent<VRPointerController>();
|
||||||
@@ -94,8 +97,18 @@ namespace VRBeats
|
|||||||
if (disabledByDefault)
|
if (disabledByDefault)
|
||||||
pointer.enabled = false;
|
pointer.enabled = false;
|
||||||
|
|
||||||
Debug.Log($"[VRPointerSetup] {(isRight ? "Right" : "Left")} pointer 추가: {go.name} (enabled={!disabledByDefault})");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -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
|
||||||
@@ -528,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:
|
||||||
@@ -760,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:
|
||||||
@@ -854,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
|
||||||
@@ -1212,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
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -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:
|
||||||
@@ -4255,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
|
||||||
@@ -4265,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}
|
||||||
@@ -4287,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:
|
||||||
@@ -4486,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
|
||||||
@@ -4656,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:
|
||||||
@@ -4769,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:
|
||||||
@@ -5026,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:
|
||||||
@@ -5241,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:
|
||||||
@@ -5493,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
|
||||||
@@ -5933,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:
|
||||||
@@ -6233,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
|
||||||
@@ -6626,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
|
||||||
@@ -6720,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:
|
||||||
@@ -7157,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}
|
||||||
@@ -7167,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
|
||||||
@@ -7727,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
|
||||||
|
|||||||
@@ -69,10 +69,10 @@ namespace VRBeats
|
|||||||
if (audioSource == null)
|
if (audioSource == null)
|
||||||
return 0.0f;
|
return 0.0f;
|
||||||
|
|
||||||
if (hasScheduledClip)
|
if (hasScheduledClip || scheduledDspStartTime >= 0.0)
|
||||||
return (float)(AudioSettings.dspTime - scheduledDspStartTime);
|
return Mathf.Max(0.0f, (float)(AudioSettings.dspTime - scheduledDspStartTime));
|
||||||
|
|
||||||
return audioSource.time;
|
return 0.0f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,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:
|
||||||
@@ -56,10 +56,12 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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,6 +110,13 @@ 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;
|
||||||
|
|
||||||
@@ -60,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)
|
||||||
@@ -122,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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,9 +39,9 @@ namespace VRBeats
|
|||||||
public void EnableXRRayInteractorComponents()
|
public void EnableXRRayInteractorComponents()
|
||||||
{
|
{
|
||||||
if (rayInteractor != null)
|
if (rayInteractor != null)
|
||||||
rayInteractor.enabled = true;
|
rayInteractor.enabled = false;
|
||||||
if (interactorLineVisual != null)
|
if (interactorLineVisual != null)
|
||||||
interactorLineVisual.enabled = true;
|
interactorLineVisual.enabled = false;
|
||||||
if (lineRender != null)
|
if (lineRender != null)
|
||||||
lineRender.enabled = true;
|
lineRender.enabled = 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using Platinio.TweenEngine;
|
|
||||||
using TMPro;
|
using TMPro;
|
||||||
|
|
||||||
namespace VRBeats
|
namespace VRBeats
|
||||||
@@ -7,54 +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 = FindFirstObjectByType<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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Binary file not shown.
@@ -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:
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
+176
-445
@@ -1,501 +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
|
||||||
|
|
||||||
## 현재 상태 (2026-05-26)
|
- 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
|
||||||
|
|
||||||
현재 저장소는 VRBeatsKit 기반 프로젝트 위에 커스텀 곡 선택/생성/NAS 연동 흐름을 붙인 상태다.
|
See `COLLABORATION_RULES.md`.
|
||||||
|
|
||||||
- Unity 버전: `6000.3.12f1`
|
## Unity / MCP Bridge
|
||||||
- 현재 브랜치: `main`
|
|
||||||
- 원격 저장소: `origin` = `https://whdwo798.synology.me/whdwo798/BeatSaber.git`
|
|
||||||
- 최근 푸시 커밋: `182d2c9 fix: stabilize VR UI and song playback`
|
|
||||||
- `dotnet build VRBeatSaber.slnx --no-incremental` 결과: 오류 0개, 경고 0개
|
|
||||||
- 현재 워킹트리에는 큐브 간격 보정과 경고 제거 작업이 커밋 전 변경으로 남아 있다.
|
|
||||||
|
|
||||||
### 실제 씬 구성
|
- `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.
|
||||||
|
|
||||||
현재 Build Settings는 아래 순서다.
|
## Game Scene HUD
|
||||||
|
|
||||||
1. `Assets/VRBeatsKit/Scenes/Menu.unity`
|
### ScoreCanvas Placement
|
||||||
2. `Assets/VRBeatsKit/Scenes/BoxingStyle.unity`
|
|
||||||
3. `Assets/Scenes/SongCreator.unity`
|
|
||||||
4. `Assets/VRBeatsKit/Scenes/SaberStyle.unity`
|
|
||||||
5. `Assets/Scenes/Game.unity`
|
|
||||||
|
|
||||||
문서 아래쪽에 남아 있는 `Intro -> SongSelect -> Game -> SongCreator` 흐름은 목표 설계에 가깝다. 현재 실제 진입점은 VRBeatsKit `Menu.unity`이며, 그 안의 `SongSelect` 패널이 커스텀 곡 선택 UI 역할을 한다.
|
`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
|
```text
|
||||||
Menu.unity / SongSelect
|
lineLayer 0: -0.12
|
||||||
-> DownloadManager가 NAS 정적 서버의 songs.json 로드
|
lineLayer 1: +0.22
|
||||||
-> SongDetailPanel에서 곡/난이도 다운로드
|
lineLayer 2: +0.56
|
||||||
-> GameSession.SelectedSong / SelectedDifficulty 설정
|
|
||||||
-> Game.unity 로드
|
|
||||||
|
|
||||||
Game.unity
|
|
||||||
-> SongController가 temporaryCachePath의 mp3 + map json 로드
|
|
||||||
-> VRBeats.AudioManager로 음악 재생
|
|
||||||
-> 오디오 시간 기준으로 VR_BeatManager.Spawn() 호출
|
|
||||||
-> VR_BeatCube / Cuttable / DamageSaber가 색상, 방향, 속도 판정
|
|
||||||
|
|
||||||
SongCreator.unity
|
|
||||||
-> SongCreatorManager가 로컬 mp3 또는 직접 mp3 URL 입력
|
|
||||||
-> BeatSageUploader가 Beat Sage 요청/폴링/ZIP 다운로드
|
|
||||||
-> BeatSageConverter가 .dat를 NoteData로 변환
|
|
||||||
-> NasPublisher가 mp3, map json, songs.json을 Synology NAS에 업로드
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 최근 반영된 변경
|
## Scoring / Rank Rules
|
||||||
|
|
||||||
- `Assets/Script/SongController.cs`
|
`Assets/VRBeatsKit/Scripts/UI/ScoreManager.cs`
|
||||||
- Beat Saber 4라인 좌표 매핑을 넓혀 인접 큐브가 가로로 겹치는 문제를 줄였다.
|
|
||||||
- 기존 라인 x 좌표는 대략 `-0.375, -0.125, 0.125, 0.375`였고, 현재는 `-0.63, -0.21, 0.21, 0.63`이다.
|
|
||||||
- 맵 노트 정렬을 `time -> position -> lineLayer` 순서로 바꿔 같은 시간대 노트 처리 순서를 안정화했다.
|
|
||||||
- 전체 C# 경고 제거
|
|
||||||
- `FindObjectOfType`, `FindObjectsOfType`, `InputHelpers`, `TMP_Text.enableWordWrapping`, `EditorApplication.currentScene`, 구버전 `PlayerSettings` API를 최신 API로 교체했다.
|
|
||||||
- 미사용 필드/변수, 상속 멤버 숨김, Unity 메시지 시그니처 경고를 정리했다.
|
|
||||||
- 최종 확인 빌드: `dotnet build VRBeatSaber.slnx --no-incremental` = 경고 0개, 오류 0개.
|
|
||||||
- `Assets/Script/VRPointerController.cs`, `VRPointerSetup.cs` 추가
|
|
||||||
- VR 컨트롤러 레이로 Unity UI 버튼을 직접 hover/click 처리한다.
|
|
||||||
- `Game` 씬에서는 게임오버 전까지 비활성화하고, 메뉴 계열 씬에서는 활성화한다.
|
|
||||||
- `Assets/Script/VRPointerSetup.cs`
|
|
||||||
- `DontDestroyOnLoad` 싱글턴으로 변경되어 `Menu -> SongCreator -> Game` 같은 씬 전환 후에도 포인터를 다시 주입한다.
|
|
||||||
- `SceneManager.sceneLoaded`마다 현재 씬 컨트롤러를 검사한다.
|
|
||||||
- `Assets/VRBeatsKit/Scripts/Core/VR_InteractorController.cs`
|
|
||||||
- XR Ray Interactor enable/disable 시 `VRPointerController`도 함께 제어한다.
|
|
||||||
- 컨트롤러 구조 차이를 고려해 현재 오브젝트, 부모, 자식, 루트 하위에서 `VRPointerController`를 찾는다.
|
|
||||||
- `Assets/VRBeatsKit/Scripts/Core/AudioManager.cs`
|
|
||||||
- `AudioSource.Play()` 대신 `PlayScheduled()`를 사용하고, `AudioSettings.dspTime` 기준으로 `CurrentTime`을 계산한다.
|
|
||||||
- MP3 재생 시작 시점과 노트 스폰 기준 시간이 프레임 상태에 따라 흔들리는 문제를 줄이기 위한 변경이다.
|
|
||||||
- `Assets/VRBeatsKit/Scripts/Core/VR_BeatCube.cs`
|
|
||||||
- `IsCutIntentValid()`를 public으로 변경하고 `maxCutAngle`을 추가했다.
|
|
||||||
- `Assets/VRBeatsKit/Scripts/Core/Cuttable.cs`
|
|
||||||
- 색상/방향/속도가 틀린 큐브는 절단 시각 효과도 발생하지 않도록 막았다.
|
|
||||||
- `Assets/Scenes/Game.unity`
|
|
||||||
- `SongController`가 큐브 프리팹, `OnLevelComplete`, 카운트다운 텍스트와 연결되어 있다.
|
|
||||||
- `GameOverPopup`의 Back 버튼에 깨진 스크립트 참조가 있어 `LoadSceneButton`으로 복구했다.
|
|
||||||
- 현재 사용 중인 좌/우 세이버 루트 회전을 `X 45도`로 보정해 컨트롤러에서 너무 수직으로 서는 문제를 줄였다.
|
|
||||||
- `Assets/VRBeatsKit/Scenes/Menu.unity`
|
|
||||||
- `SongSelectManager`, `DownloadManager`, `SongDetailPanel`, `SongLibrary`가 연결되어 있다.
|
|
||||||
- `VRPointerSetup`이 `VR_Manager`에 추가되어 있다.
|
|
||||||
- `Assets/img/360.mp4`, `Assets/img/beatSaber.png`
|
|
||||||
- 메뉴/비주얼용 에셋으로 추가됨.
|
|
||||||
- `.gitignore`
|
|
||||||
- `*.csproj.user` 제외 추가.
|
|
||||||
- `.gitattributes`
|
|
||||||
- `*.mp4 binary` 추가.
|
|
||||||
|
|
||||||
### 현재 주의사항
|
### Score Formula
|
||||||
|
|
||||||
1. `Assets/StreamingAssets/nas_config.json`은 현재 저장소에 없다. NAS 업로드를 테스트하려면 로컬에 직접 만들어야 하며, 절대 커밋하지 않는다.
|
```text
|
||||||
2. `SongCreator.unity`의 직렬화된 `nasBaseUrl` 값에 끝 공백이 들어가 있다: `http://whdwo798.synology.me:5000 `. 런타임에서 `nas_config.json`으로 덮어쓰지 않으면 로그인 URL 문제가 날 수 있다.
|
CurrentScore = 800000 * accuracyRatio + 200000 * comboRatio
|
||||||
3. `SongCreatorManager`는 난이도 토글 필드를 갖고 있지만 현재 로직은 4개 난이도(`normal`, `hard`, `expert`, `expertplus`)를 항상 전부 생성한다.
|
|
||||||
4. `manualEditorButton`은 씬에서 미연결이고 코드에서도 사용하지 않는다.
|
|
||||||
5. `Assets/img/360.mp4`는 약 192MB다. 현재 일반 Git으로 푸시되어 있으므로, 원격 정책이 바뀌면 Git LFS 전환을 검토해야 한다.
|
|
||||||
6. 큐브 간격은 수치상 겹침을 피하도록 넓혔지만, 실제 Quest 착용 테스트에서 손 위치/판정 거리/시야 피로도를 확인해야 한다.
|
|
||||||
7. SongCreator에서 생성 직후 첫 재생이 곡에 따라 늦거나 싱크가 흔들리는 체감이 있었다. 게임 씬 오디오 기준은 `AudioSettings.dspTime`으로 개선했지만, 생성/다운로드/첫 로드 전체 파이프라인은 추가 로그 검증이 필요하다.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 기존 프로젝트 소스 코드
|
|
||||||
|
|
||||||
**기존 프로젝트 전체 파일은 아래 git 저장소에서 가져온다.**
|
|
||||||
|
|
||||||
```
|
|
||||||
https://whdwo798.synology.me/whdwo798/BeatSaber.git
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
- Max score: `1,000,000`
|
||||||
git clone https://whdwo798.synology.me/whdwo798/BeatSaber.git
|
- Accuracy contribution: `800,000`
|
||||||
|
- Max combo contribution: `200,000`
|
||||||
|
|
||||||
|
### Judgement Points
|
||||||
|
|
||||||
|
```text
|
||||||
|
Perfect = 1000
|
||||||
|
Great = 900
|
||||||
|
Good = 700
|
||||||
|
Miss = 0
|
||||||
```
|
```
|
||||||
|
|
||||||
> 단, 이 저장소는 구버전 프로젝트다. 새 프로젝트는 VRBeatsKit을 기반으로 재구성하며,
|
### Timing Windows
|
||||||
> 위 저장소의 `Assets/Script/` 등 핵심 스크립트를 참고/이식하는 용도로 사용한다.
|
|
||||||
|
|
||||||
---
|
```text
|
||||||
|
Perfect <= 0.11s
|
||||||
## Git 설정 (새 프로젝트)
|
Great <= 0.20s
|
||||||
|
Good <= 0.32s
|
||||||
새 Unity 프로젝트를 생성한 뒤 **가장 먼저** git을 초기화하고 파일을 커밋해야 한다.
|
|
||||||
Claude Code는 대화 시작 시 `git status` / `git log`를 자동으로 읽어 컨텍스트를 파악한다.
|
|
||||||
커밋이 없으면 Claude가 변경 이력을 추적할 수 없다.
|
|
||||||
|
|
||||||
### 초기화 순서
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 새 프로젝트 루트에서
|
|
||||||
git init
|
|
||||||
git remote add origin <GitHub 저장소 URL>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### .gitignore
|
### Combo Multiplier
|
||||||
|
|
||||||
기존 프로젝트의 `.gitignore`를 복사하면 된다. 핵심 규칙:
|
The multiplier is intentionally beginner-friendly:
|
||||||
|
|
||||||
```gitignore
|
```text
|
||||||
# Unity 표준
|
0-4 combo x1.0
|
||||||
/Library/
|
5-14 combo x1.1
|
||||||
/Temp/
|
15-29 combo x1.2
|
||||||
/Obj/
|
30-49 combo x1.35
|
||||||
/Build/
|
50+ combo x1.5
|
||||||
/Builds/
|
|
||||||
/Logs/
|
|
||||||
/UserSettings/
|
|
||||||
|
|
||||||
# NAS 비밀번호 — 절대 커밋 금지
|
|
||||||
/Assets/StreamingAssets/nas_config.json
|
|
||||||
/Assets/StreamingAssets/nas_config.json.meta
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 첫 커밋
|
### Rank Thresholds
|
||||||
|
|
||||||
파일 복사 완료 후:
|
- `M`: final score `>= 1,000,000`
|
||||||
|
- Other ranks use `AccuracyPercent`:
|
||||||
|
|
||||||
```bash
|
```text
|
||||||
git add .
|
S+ >= 98%
|
||||||
git commit -m "init: VRBeatsKit 기반 프로젝트 초기 설정"
|
S >= 95%
|
||||||
|
A >= 90%
|
||||||
|
B >= 80%
|
||||||
|
C >= 70%
|
||||||
|
D >= 60%
|
||||||
|
F < 60%
|
||||||
```
|
```
|
||||||
|
|
||||||
이후 기능 단위로 커밋하면 Claude가 `git log`로 작업 이력을 파악한다.
|
### Rank Colors
|
||||||
|
|
||||||
---
|
```text
|
||||||
|
M #E8B7FF prism / jewel tone
|
||||||
## 새 프로젝트 구성 방법
|
S+ #41F2FF cyan neon
|
||||||
|
S #FFD95C gold
|
||||||
### 전제 조건
|
A #B9FF72
|
||||||
|
B #FFE06A
|
||||||
1. Unity Hub에서 **새 URP 3D 프로젝트** 생성 (기존 프로젝트와 동일 Unity 버전)
|
C #FFB15C
|
||||||
2. Asset Store에서 **VRBeatsKit** 임포트
|
D #FF7C7C
|
||||||
3. Package Manager에서 아래 패키지 설치:
|
F #A9B7C0
|
||||||
- XR Interaction Toolkit (3.x)
|
|
||||||
- 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으로 대체)
|
## Result Screen
|
||||||
|
|
||||||
```
|
`Assets/VRBeatsKit/Scripts/UI/FinalScoreLabel.cs`
|
||||||
Assets/Script/Saber.cs → VRBeatsKit VR_Saber.cs 사용
|
|
||||||
Assets/Script/Cube.cs → VRBeatsKit VR_BeatCube.cs 사용
|
The result screen is now the first major UI-quality target.
|
||||||
|
|
||||||
|
Current result layout is generated at runtime:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ResultLayoutRoot
|
||||||
|
RankShadowText
|
||||||
|
RankDepthText
|
||||||
|
RankMainText
|
||||||
|
ResultScoreText
|
||||||
|
ResultAccuracyText
|
||||||
|
ResultComboText
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
Implemented:
|
||||||
|
|
||||||
## 전체 씬 구성
|
- 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:
|
||||||
Intro → SongSelect → Game
|
|
||||||
SongSelect → SongCreator → (NAS 업로드) → SongSelect
|
|
||||||
```
|
|
||||||
|
|
||||||
| 씬 | 역할 |
|
- `tmp.lineSpacing = -8.0f` is aggressive and may need visual tuning if the `SCORE` label overlaps the score value.
|
||||||
|---|---|
|
- Button styling is improved by color, but does not yet have a dedicated neon border/glow treatment.
|
||||||
| Intro | 로고 → SongSelect 자동 전환 |
|
- The scene appears to contain both result and game-over popup/button sets; verify that shared color changes are intentional.
|
||||||
| SongSelect | NAS에서 songs.json 로드, 곡 목록 표시, 다운로드/플레이 |
|
|
||||||
| Game | 음악 재생 + 큐브 스폰 + 점수/HP + 결과 화면 |
|
|
||||||
| SongCreator | 음악 파일 선택 → Beat Sage API 채보 → NAS 업로드 |
|
|
||||||
| MapEditorScene | 맵 에디터 (선택적) |
|
|
||||||
|
|
||||||
---
|
## Visual Effects
|
||||||
|
|
||||||
## 전체 데이터 흐름
|
Blue visibility improvements were made in:
|
||||||
|
|
||||||
```
|
- `Assets/VRBeatsKit/Settings/Settings.asset`
|
||||||
[SongCreator]
|
- `Assets/VRBeatsKit/Scripts/Core/SaberTrailEffect.cs`
|
||||||
사용자: 음악 파일 선택 (로컬 파일 또는 URL)
|
- `Assets/VRBeatsKit/Scripts/Core/SliceTrailEffect.cs`
|
||||||
→ BeatSageUploader: Beat Sage API 채보 요청
|
- `Assets/VRBeatsKit/Scripts/Other/Spark.cs`
|
||||||
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]
|
Goal:
|
||||||
→ DownloadManager: NAS에서 songs.json 로드
|
|
||||||
→ 사용자 곡 선택 → GameSession.SelectedSong, GameSession.SelectedDifficulty 설정
|
|
||||||
→ 다운로드: {id}.mp3 + Map_{id}_{diff}.json → Application.temporaryCachePath/beatsaber/{id}/
|
|
||||||
|
|
||||||
[Game]
|
- Keep red strong.
|
||||||
→ Spawner.InitGame(): 캐시에서 오디오/맵 로드
|
- Make blue saber/trail/spark readable against the bright cyan tunnel.
|
||||||
→ VRBeatsKit AudioManager AudioSource에 클립 세팅
|
|
||||||
→ 카운트다운 3→2→1→GO
|
|
||||||
→ 매 프레임: audioSource.time 기준으로 VR_BeatManager.Spawn() 호출
|
|
||||||
→ ScoreManager: 히트/미스 집계 → HP → 결과 화면
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
## UI Quality Direction
|
||||||
|
|
||||||
## 주요 스크립트 역할
|
Current agreed direction:
|
||||||
|
|
||||||
### 복사하는 스크립트 (수정 없음)
|
1. Finish result screen polish first.
|
||||||
|
2. Then polish in-game HUD.
|
||||||
|
3. Then consider HUD migration from legacy `UI.Text` to TMP for sharper VR rendering.
|
||||||
|
4. Then redesign song select cards/details.
|
||||||
|
|
||||||
| 파일 | 역할 |
|
Design principles:
|
||||||
|---|---|
|
|
||||||
| `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개 전부 생성. |
|
|
||||||
| `SongController.cs` | Game 씬 실행부. 캐시된 mp3/map json을 로드하고 VRBeatsKit `VR_BeatManager.Spawn()`으로 노트를 스폰. |
|
|
||||||
| `DesktopUIMode.cs` | 에디터에서 XR 없이 테스트할 때 UI Raycaster 교체 |
|
|
||||||
| `VRPointerController.cs` | VR 컨트롤러 레이로 UI hover/click 처리. 디버그 로그 포함. |
|
|
||||||
| `VRPointerSetup.cs` | 씬 로드 후 손/컨트롤러 오브젝트에 `VRPointerController` 자동 주입. |
|
|
||||||
| `XRSimulatorLoader.cs` | 에디터/PC 테스트용 XR Interaction Simulator 프리팹 주입. |
|
|
||||||
|
|
||||||
### 현재 미이식/미확인 스크립트
|
- 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.
|
||||||
|
|
||||||
| 파일 | 내용 |
|
## Verification Notes
|
||||||
|---|---|
|
|
||||||
| `IntroManager.cs` | 현재 저장소에 없음. 인트로 씬 흐름을 살릴 경우 작성/이식 필요. |
|
|
||||||
| `ScoreManager.cs`, `ScoreHUD.cs`, `ResultsPanel.cs` | 전역 네임스페이스 커스텀 점수 UI는 현재 저장소에 없음. 현재는 VRBeatsKit `VRBeats.ScoreManager`와 이벤트 자산을 사용. |
|
|
||||||
| `SaberGlow.cs`, `SaberSkinSelector.cs`, `CacheManager.cs` | 현재 저장소에 없음. 필요 시 기존 프로젝트에서 이식. |
|
|
||||||
|
|
||||||
---
|
- `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.
|
||||||
|
|
||||||
## Game 실행부 현재 구현
|
## Recommended Next Checks
|
||||||
|
|
||||||
기존 인수인계 문서에는 `Spawner.cs`를 새로 작성하라고 되어 있었지만, 현재 저장소에서는 별도 `Spawner.cs` 대신 `Assets/Script/SongController.cs`가 그 역할을 수행한다.
|
1. Play the Game scene and finish a song.
|
||||||
|
2. Check result screen layout for `M`, `S+`, `S`, and `F`.
|
||||||
### `SongController.cs` 핵심
|
3. Verify score, accuracy, and max combo do not overlap.
|
||||||
|
4. Confirm Restart and Back buttons remain clickable.
|
||||||
```csharp
|
5. Check whether result popup and game-over popup color changes should both stay.
|
||||||
private IEnumerator LoadAndPlay()
|
6. If result screen is stable, move to HUD polish and TMP migration discussion.
|
||||||
{
|
|
||||||
SongInfo song = GameSession.SelectedSong;
|
|
||||||
string diff = GameSession.SelectedDifficulty;
|
|
||||||
// mp3와 map json을 Application.temporaryCachePath/beatsaber/{songId}/ 에서 로드
|
|
||||||
// 카운트다운 후 VRBeats.AudioManager.PlayClip(clip)
|
|
||||||
// SpawnRoutine(map.target) 실행
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SpawnNote(NoteData note)
|
|
||||||
{
|
|
||||||
float x = MapLaneX(note.position);
|
|
||||||
float y = MapLayerY(note.lineLayer);
|
|
||||||
|
|
||||||
var info = new SpawnEventInfo
|
|
||||||
{
|
|
||||||
position = new Vector3(x, y, 0f),
|
|
||||||
colorSide = note.colorType == 0 ? ColorSide.Left : ColorSide.Right,
|
|
||||||
hitDirection = MapCutDirection(note.cutDirection),
|
|
||||||
useSpark = true,
|
|
||||||
speed = 2f,
|
|
||||||
travelTimeOverride = note.time - _audio.CurrentTime,
|
|
||||||
};
|
|
||||||
|
|
||||||
VR_BeatManager.instance.Spawn(cubePrefab, info);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`travelTimeOverride`는 동시 노트가 프레임 차이로 스폰되어도 같은 타이밍에 도착하도록 `VR_BeatManager`에 추가된 값이다.
|
|
||||||
|
|
||||||
현재 라인 매핑은 `LaneSpacing = 0.42f`, `LayerSpacing = 0.38f`를 사용한다. 이는 VRBeatsKit 큐브 콜라이더의 실제 폭이 기존 라인 간격보다 커서 인접 라인이 겹치던 문제를 피하기 위한 값이다.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ScoreManager 충돌 없음
|
|
||||||
|
|
||||||
- 예전 설계: 전역 네임스페이스 커스텀 `ScoreManager.cs`
|
|
||||||
- 현재 저장소: `Assets/VRBeatsKit/Scripts/UI/ScoreManager.cs`의 `VRBeats.ScoreManager` 사용
|
|
||||||
- 전역 커스텀 `ScoreManager.cs`를 다시 이식하면 네임스페이스가 달라 공존은 가능하지만, 이벤트 연결과 UI 패널을 별도로 구성해야 한다.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 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`이면 `SongController`가 오류를 로그로 남기고 진행하지 않는다. 곡 플레이는 `Menu.unity`의 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 현재 상태**: 현재 등록 순서는 `Menu`, `BoxingStyle`, `SongCreator`, `SaberStyle`, `Game`이다. 예전 목표 설계의 `Intro`, `SongSelect`, `MapEditorScene`은 현재 Build Settings에 없다.
|
|
||||||
6. **경고 0 상태 유지**: 패키지 내부까지 경고를 제거해 둔 상태라, 새 SDK/API를 추가할 때 `dotnet build VRBeatSaber.slnx --no-incremental`로 경고 재발 여부를 확인한다.
|
|
||||||
7. **VR 실기 테스트 필수 항목**: 게임오버 Back/Retry 클릭, SongCreator UI 클릭, 큐브 가로 간격, 큐브 도착 싱크, 세이버 각도는 Quest에서 직접 확인해야 한다.
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
```
|
||||||
@@ -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><<span class="ty">AudioManager</span>>();
|
_audio = <span class="fn">FindFirstObjectByType</span><<span class="ty">AudioManager</span>>();
|
||||||
<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><<span class="ty">MapData</span>>(File.<span class="fn">ReadAllText</span>(mapPath));
|
<span class="ty">MapData</span> map = JsonUtility.<span class="fn">FromJson</span><<span class="ty">MapData</span>>(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) => 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)
|
||||||
=> audioSource.outputAudioMixerGroup.audioMixer.<span class="fn">SetFloat</span>(<span class="st">"Pitch"</span>, value);
|
=> 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 => 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><<span class="ty">MarqueeText</span>>(); <span class="ann">// 텍스트가 컨테이너보다 길면 자동 스크롤</span>
|
titleGO.<span class="fn">AddComponent</span><<span class="ty">MarqueeText</span>>(); <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><<span class="ty">LineRenderer</span>>();
|
||||||
|
_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 && !_prevTrigger;
|
||||||
|
<span class="kw">bool</span> primaryDown = primary && !_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) && _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><<span class="ty">Button</span>>();
|
||||||
|
<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><<span class="kw">bool</span>> 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><<span class="ty">VRPointerSetup</span>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
<span class="kw">private void</span> <span class="fn">Awake</span>()
|
||||||
|
{
|
||||||
|
<span class="kw">if</span> (instance != <span class="kw">null</span> && 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>() => <span class="ty">SceneManager</span>.sceneLoaded += <span class="fn">OnSceneLoaded</span>;
|
||||||
|
<span class="kw">private void</span> <span class="fn">OnDisable</span>() => <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><<span class="ty">GameObject</span>>(<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 && !isLeft) <span class="kw">continue</span>;
|
||||||
|
<span class="kw">if</span> (go.<span class="fn">GetComponent</span><<span class="ty">LineRenderer</span>>() == <span class="kw">null</span>) <span class="kw">continue</span>;
|
||||||
|
<span class="kw">if</span> (go.<span class="fn">GetComponent</span><<span class="ty">VRPointerController</span>>() != <span class="kw">null</span>) <span class="kw">continue</span>;
|
||||||
|
|
||||||
|
<span class="kw">var</span> pointer = go.<span class="fn">AddComponent</span><<span class="ty">VRPointerController</span>>();
|
||||||
|
<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><<span class="ty">DesktopUIMode</span>>() != <span class="kw">null</span>) <span class="kw">return</span>; <span class="ann">// 이미 있으면 스킵</span>
|
<span class="kw">if</span> (<span class="fn">FindFirstObjectByType</span><<span class="ty">DesktopUIMode</span>>() != <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><<span class="ty">DesktopUIMode</span>>();
|
<span class="kw">new</span> <span class="ty">GameObject</span>(<span class="st">"[DesktopUIMode]"</span>).<span class="fn">AddComponent</span><<span class="ty">DesktopUIMode</span>>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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><<span class="ty">Camera</span>>(<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><<span class="ty">Camera</span>>(<span class="ty">FindObjectsSortMode</span>.None))
|
||||||
<span class="kw">if</span> (c.enabled && c.gameObject.scene.name != <span class="st">"DontDestroyOnLoad"</span>) { cam = c; <span class="kw">break</span>; }
|
<span class="kw">if</span> (c.enabled && c.gameObject.scene.name != <span class="st">"DontDestroyOnLoad"</span>) { cam = c; <span class="kw">break</span>; }
|
||||||
cam ??= <span class="fn">FindObjectOfType</span><<span class="ty">Camera</span>>(); <span class="ann">// 최후의 수단</span>
|
cam ??= <span class="fn">FindFirstObjectByType</span><<span class="ty">Camera</span>>(); <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><<span class="ty">Canvas</span>>(<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><<span class="ty">Canvas</span>>(<span class="ty">FindObjectsSortMode</span>.None))
|
||||||
@@ -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><<span class="ty">Button</span>>();
|
||||||
|
<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<<span class="ty">AudioManager</span>>();
|
||||||
|
FindObjectsOfType<<span class="ty">Canvas</span>>();
|
||||||
|
tTmp.enableWordWrapping = <span class="kw">false</span>;
|
||||||
|
|
||||||
|
<span class="cmt">// 현재</span>
|
||||||
|
FindFirstObjectByType<<span class="ty">AudioManager</span>>();
|
||||||
|
FindObjectsByType<<span class="ty">Canvas</span>>(<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>
|
||||||
|
|
||||||
@@ -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"
|
||||||
@@ -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);
|
||||||
Generated
+1162
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user