diff --git a/.gitignore b/.gitignore index 1a756e4c..666a9d05 100644 --- a/.gitignore +++ b/.gitignore @@ -88,3 +88,4 @@ Assets/Digger/ Assets/Samples/ Assets/Silantro/ Assets/Runtime/StraightFour/3rd-party/ +.worldkit/ diff --git a/Assets/Runtime/Handlers/JavascriptHandler/APIs/Input/Scripts/Input.cs b/Assets/Runtime/Handlers/JavascriptHandler/APIs/Input/Scripts/Input.cs index fbad2562..0691d8d2 100644 --- a/Assets/Runtime/Handlers/JavascriptHandler/APIs/Input/Scripts/Input.cs +++ b/Assets/Runtime/Handlers/JavascriptHandler/APIs/Input/Scripts/Input.cs @@ -627,6 +627,13 @@ public static bool AddRigFollower(BaseEntity entityToFollowRig) WebVerseRuntime.Instance.vrRig.rigFollowers.Add(entityToFollowRig.internalEntity); } } + + // The rig is now the sole writer of this entity's position via UpdateFollowers. + // Suppress the entity's own FixedUpdate motion to prevent two-writer ghosting. + if (entityToFollowRig.internalEntity is StraightFour.Entity.CharacterEntity ce) + { + ce.externalPositionControl = true; + } return true; } @@ -714,6 +721,12 @@ public static bool RemoveRigFollower(BaseEntity entityToFollowRig) WebVerseRuntime.Instance.vrRig.rigFollowers.Remove(entityToFollowRig.internalEntity); } } + + // Restore entity-owned FixedUpdate motion now that the rig no longer drives position. + if (entityToFollowRig.internalEntity is StraightFour.Entity.CharacterEntity ce) + { + ce.externalPositionControl = false; + } return true; } diff --git a/Assets/Runtime/Handlers/JavascriptHandler/APIs/WorldBrowserUtilities/Scripts/World.cs b/Assets/Runtime/Handlers/JavascriptHandler/APIs/WorldBrowserUtilities/Scripts/World.cs index 584e2cea..9b539da0 100644 --- a/Assets/Runtime/Handlers/JavascriptHandler/APIs/WorldBrowserUtilities/Scripts/World.cs +++ b/Assets/Runtime/Handlers/JavascriptHandler/APIs/WorldBrowserUtilities/Scripts/World.cs @@ -28,6 +28,15 @@ public static string GetQueryParam(string key) return WebVerseRuntime.Instance.straightFour.GetParam(key); } + /// + /// Get the URL of the currently loaded World or Web Page. + /// + /// The URL of the current World or Web Page, or null if none has been loaded. + public static string GetWorldURL() + { + return WebVerseRuntime.Instance.currentURL; + } + /// /// Get the current World Load State. /// @@ -59,6 +68,19 @@ public static string GetWorldLoadState() /// /// The URL of the World to load. public static void LoadWorld(string url) + { + LoadWorld(url, null); + } + + /// + /// Load a World from a URL, along with a script to run in the same JINT engine as the world's + /// own scripts. + /// + /// The URL of the World to load. + /// Either inline JavaScript logic, or a URI ending in ".js" pointing + /// to a script resource. The script is prepended to the world's script list and runs first. + /// Only supported for VEML worlds; ignored for x3d and glTF worlds. + public static void LoadWorld(string url, string requireScript) { WebVerseRuntime.Instance.LoadWorld(url, new System.Action((name) => { @@ -68,7 +90,33 @@ public static void LoadWorld(string url) multibar.ToggleMultibar(); multibar.ToggleMultibar(); } - })); + }), requireScript); + } + + /// + /// Dry-run validation of a World's VEML without switching to it. Downloads and parses the + /// VEML, downloads (but does not execute) referenced scripts, and HEAD-requests referenced + /// asset URIs. Reports the result via the JS callback. Does not unload the active world, + /// mutate runtime state, or touch the JINT engine. + /// + /// The URL of the World to test. + /// Name of a JS function to invoke when the test completes. The + /// function is called with three arguments: (success: bool, errorMessage: string|null, title: + /// string|null). On success, errorMessage is null. On failure, errorMessage is a + /// newline-separated list of issues. title is the parsed metadata.title when the document + /// parsed, otherwise null. + public static void TestLoadWorld(string url, string onTestComplete) + { + WebVerseRuntime.Instance.TestLoadWorld(url, + new System.Action((success, errorMessage, title) => + { + if (string.IsNullOrEmpty(onTestComplete)) + { + return; + } + WebVerseRuntime.Instance.javascriptHandler.CallWithParams( + onTestComplete, new object[] { success, errorMessage, title }); + })); } /// diff --git a/Assets/Runtime/Handlers/JavascriptHandler/Tests/WorldAPITests.cs b/Assets/Runtime/Handlers/JavascriptHandler/Tests/WorldAPITests.cs new file mode 100644 index 00000000..bd7bc5fe --- /dev/null +++ b/Assets/Runtime/Handlers/JavascriptHandler/Tests/WorldAPITests.cs @@ -0,0 +1,259 @@ +// Copyright (c) 2019-2026 Five Squared Interactive. All rights reserved. + +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; +using FiveSQD.WebVerse.Runtime; +using FiveSQD.WebVerse.LocalStorage; +using System.IO; +using System.Reflection; +using System.Text.RegularExpressions; +using WorldAPI = FiveSQD.WebVerse.Handlers.Javascript.APIs.Utilities.World; + +/// +/// Unit tests for the World JavaScript API. +/// +public class WorldAPITests +{ + private WebVerseRuntime runtime; + private GameObject runtimeGO; + private string testDirectory; + + [OneTimeSetUp] + public void OneTimeSetUp() + { + LogAssert.ignoreFailingMessages = true; + } + + [SetUp] + public void SetUp() + { + LogAssert.ignoreFailingMessages = true; + + runtimeGO = new GameObject("runtime"); + runtime = runtimeGO.AddComponent(); + + runtime.highlightMaterial = new Material(Shader.Find("Standard")); + runtime.skyMaterial = new Material(Shader.Find("Standard")); + runtime.characterControllerPrefab = new GameObject("DummyCharacterController"); + runtime.inputEntityPrefab = new GameObject("DummyInputEntity"); + runtime.voxelPrefab = new GameObject("DummyVoxel"); + runtime.webVerseWebViewPrefab = new GameObject("DummyWebView"); + + testDirectory = Path.Combine(Path.GetTempPath(), "WorldAPITests"); + runtime.Initialize(LocalStorageManager.LocalStorageMode.Cache, 128, 128, 128, testDirectory); + } + + [TearDown] + public void TearDown() + { + WebVerseRuntime.Instance = null; + if (runtime != null && Directory.Exists(testDirectory)) + { + Directory.Delete(testDirectory, true); + } + if (runtimeGO != null) + { + Object.DestroyImmediate(runtimeGO); + } + } + + private static void SetCurrentURL(WebVerseRuntime runtime, string url) + { + PropertyInfo prop = typeof(WebVerseRuntime).GetProperty( + "currentURL", BindingFlags.Public | BindingFlags.Instance); + Assert.NotNull(prop, "currentURL property must exist on WebVerseRuntime."); + prop.SetValue(runtime, url); + } + + [Test] + public void GetWorldURL_BeforeAnyLoad_ReturnsNull() + { + Assert.IsNull(WorldAPI.GetWorldURL()); + } + + [Test] + public void GetWorldURL_AfterCurrentURLSet_ReturnsThatURL() + { + const string url = "https://example.test/world.veml"; + SetCurrentURL(runtime, url); + + Assert.AreEqual(url, WorldAPI.GetWorldURL()); + } + + [Test] + public void GetWorldURL_ReflectsLatestAssignment() + { + SetCurrentURL(runtime, "https://example.test/first.veml"); + SetCurrentURL(runtime, "https://example.test/second.veml"); + + Assert.AreEqual("https://example.test/second.veml", WorldAPI.GetWorldURL()); + } + + [Test] + public void LoadWorld_SetsCurrentURL() + { + const string url = "https://example.test/load-world.veml"; + + // The full load pipeline pulls in handlers and HTTP that aren't wired + // up in this test context, so downstream work may throw. We only care + // that the currentURL assignment at the top of LoadWorld ran. + try { runtime.LoadWorld(url, null); } catch { } + + Assert.AreEqual(url, WorldAPI.GetWorldURL()); + } + + [Test] + public void LoadWebPage_SetsCurrentURL() + { + const string url = "https://example.test/page.html"; + + // The placeholder WebView in this test context isn't fully set up, so LoadURL on it + // intentionally errors. We only care that currentURL was assigned at the top of LoadWebPage + // before the WebView call ran. + LogAssert.Expect(LogType.Error, "[WebVerseWebView->LoadURL] WebVerse WebView not set up."); + try { runtime.LoadWebPage(url, null); } catch { } + + Assert.AreEqual(url, WorldAPI.GetWorldURL()); + } + + [Test] + public void LoadWorld_WithInlineRequireScript_AcceptsArgument() + { + const string url = "https://example.test/with-require.veml"; + + // The new overload should accept an inline JS body without throwing at the + // signature/dispatch boundary. Downstream load machinery may still throw. + try { runtime.LoadWorld(url, null, "var __requireSentinel = 1;"); } catch { } + + Assert.AreEqual(url, WorldAPI.GetWorldURL()); + } + + [Test] + public void LoadWorld_WithURIRequireScript_AcceptsArgument() + { + const string url = "https://example.test/with-require-uri.veml"; + + try { runtime.LoadWorld(url, null, "init.js"); } catch { } + + Assert.AreEqual(url, WorldAPI.GetWorldURL()); + } + + [Test] + public void LoadWorld_DefaultRequireScript_BackwardCompatible() + { + // The single-onLoaded overload signature still works (default param = null). + const string url = "https://example.test/no-require.veml"; + + try { runtime.LoadWorld(url, null); } catch { } + + Assert.AreEqual(url, WorldAPI.GetWorldURL()); + } + + [Test] + public void JSAPI_LoadWorld_OneArg_Compiles() + { + // Verifies the World.LoadWorld(url) overload remains callable. + try { WorldAPI.LoadWorld("https://example.test/one-arg.veml"); } catch { } + } + + [Test] + public void JSAPI_LoadWorld_TwoArg_Compiles() + { + // Verifies the new World.LoadWorld(url, requireScript) overload is callable. + try + { + WorldAPI.LoadWorld("https://example.test/two-arg.veml", "var __sentinel = 1;"); + } + catch { } + } + + [Test] + public void TestLoadWorld_DoesNotMutateCurrentURL() + { + // Pre-condition: simulate a previously loaded world. + const string sentinel = "https://example.test/already-loaded.veml"; + SetCurrentURL(runtime, sentinel); + + try + { + runtime.TestLoadWorld("https://example.test/test-target.veml", + (success, errorMessage, title) => { }); + } + catch { } + + // The contract: TestLoadWorld must not overwrite currentURL even if the network + // call later fails or hangs. + Assert.AreEqual(sentinel, WorldAPI.GetWorldURL()); + } + + [Test] + public void TestLoadWorld_RejectsNonVEMLExtensions() + { + bool callbackFired = false; + bool reportedSuccess = true; + string reportedError = null; + + try + { + runtime.TestLoadWorld("https://example.test/world.glb", + (success, errorMessage, title) => + { + callbackFired = true; + reportedSuccess = success; + reportedError = errorMessage; + }); + } + catch { } + + Assert.IsTrue(callbackFired, "Callback should fire synchronously for non-VEML extensions."); + Assert.IsFalse(reportedSuccess); + StringAssert.Contains("VEML", reportedError); + } + + [Test] + public void TestLoadWorld_X3DAlsoRejected() + { + bool callbackFired = false; + bool reportedSuccess = true; + + try + { + runtime.TestLoadWorld("https://example.test/world.x3d", + (success, errorMessage, title) => + { + callbackFired = true; + reportedSuccess = success; + }); + } + catch { } + + Assert.IsTrue(callbackFired); + Assert.IsFalse(reportedSuccess); + } + + [Test] + public void JSAPI_TestLoadWorld_Compiles() + { + // Verifies the JS-facing World.TestLoadWorld(url, callbackName) signature is callable. + try + { + WorldAPI.TestLoadWorld("https://example.test/world.veml", "onTestComplete"); + } + catch { } + } + + [Test] + public void JSAPI_TestLoadWorld_NullCallback_DoesNotThrow() + { + // A null/empty callback name should be tolerated (just no JS invocation). + Assert.DoesNotThrow(() => + { + try + { + WorldAPI.TestLoadWorld("https://example.test/world.glb", null); + } + catch { } + }); + } +} diff --git a/Assets/Runtime/Handlers/JavascriptHandler/Tests/WorldAPITests.cs.meta b/Assets/Runtime/Handlers/JavascriptHandler/Tests/WorldAPITests.cs.meta new file mode 100644 index 00000000..0c6c48e8 --- /dev/null +++ b/Assets/Runtime/Handlers/JavascriptHandler/Tests/WorldAPITests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3ebf932f86bb43babcd6706210a81af1 diff --git a/Assets/Runtime/Handlers/VEMLHandler/Scripts/VEMLHandler.cs b/Assets/Runtime/Handlers/VEMLHandler/Scripts/VEMLHandler.cs index ec60d65f..097839ad 100644 --- a/Assets/Runtime/Handlers/VEMLHandler/Scripts/VEMLHandler.cs +++ b/Assets/Runtime/Handlers/VEMLHandler/Scripts/VEMLHandler.cs @@ -122,7 +122,7 @@ public void GetWorldName(string resourceURI, Action onComplete) /// URI of the world file. /// Action to invoke upon completion of world loading. /// Provides a success/fail indication. - public void LoadVEMLDocumentIntoWorld(string resourceURI, Action onComplete) + public void LoadVEMLDocumentIntoWorld(string resourceURI, Action onComplete, string requireScript = null) { Action onDownloaded = () => { @@ -136,12 +136,286 @@ public void LoadVEMLDocumentIntoWorld(string resourceURI, Action onComplet } else { - StartCoroutine(ApplyVEMLDocument(veml, Path.GetDirectoryName(resourceURI), onComplete)); + StartCoroutine(ApplyVEMLDocument(veml, Path.GetDirectoryName(resourceURI), onComplete, requireScript)); } }; DownloadVEML(resourceURI, onDownloaded); } + /// + /// Dry-run validation of a VEML document. Downloads and parses the VEML, downloads (but does + /// not execute) referenced scripts, and HEAD-requests referenced asset URIs (meshes, textures, + /// audio, images, html links, panorama, sky textures, terrain-layer textures). Does not touch + /// the active world, runtime state, currentURL, or the JINT engine. + /// + /// URI of the VEML document to test. + /// Invoked with (success, errorMessage, title). errorMessage is + /// null on success; on failure it is a newline-separated list of issues. title is the parsed + /// metadata.title if the document parsed, otherwise null. + public void TestVEMLDocument(string resourceURI, Action onTestComplete) + { + Action onDownloaded = () => + { + Schema.V3_0.veml veml; + try + { + veml = LoadVEML(Path.Combine(runtime.fileHandler.fileDirectory, + FileHandler.ToFileURI(resourceURI))); + } + catch (Exception ex) + { + onTestComplete.Invoke(false, + "Failed to parse VEML at " + resourceURI + ": " + ex.Message, null); + return; + } + + if (veml == null) + { + onTestComplete.Invoke(false, + "Not a valid VEML document: " + resourceURI, null); + return; + } + + StartCoroutine(TestApplyVEMLDocument(veml, + Path.GetDirectoryName(resourceURI), onTestComplete)); + }; + DownloadVEML(resourceURI, onDownloaded); + } + + /// + /// Dry-run coroutine for a parsed VEML document. See TestVEMLDocument for the contract. + /// + private IEnumerator TestApplyVEMLDocument(Schema.V3_0.veml veml, string baseURI, + Action onTestComplete) + { + string formattedBaseURI = VEMLUtilities.FormatURI(baseURI); + List errors = new List(); + string title = null; + + // Metadata structural validation. + if (veml.metadata == null) + { + errors.Add("Missing required field: metadata."); + } + else + { + title = veml.metadata.title; + if (string.IsNullOrEmpty(title)) + { + errors.Add("Missing required field: metadata.title."); + } + } + + // Download all script URIs (we never execute them). Inline scripts are skipped — nothing + // to check at the URI level. + Dictionary scriptResults = new Dictionary(); + if (veml.metadata != null && veml.metadata.script != null) + { + foreach (string script in veml.metadata.script) + { + if (string.IsNullOrEmpty(script) || !script.EndsWith(".js")) + { + continue; + } + string fullURI = VEMLUtilities.FullyQualifyURI(script, formattedBaseURI); + if (scriptResults.ContainsKey(fullURI)) + { + continue; + } + scriptResults[fullURI] = null; + string capturedURI = fullURI; + LoadScriptResourceAsString(fullURI, new Action((content) => + { + scriptResults[capturedURI] = content ?? ""; + })); + } + } + + // Walk entity tree, collecting asset URIs to HEAD-check. + List assetURIs = new List(); + if (veml.environment != null && veml.environment.entity != null) + { + foreach (Schema.V3_0.entity ent in veml.environment.entity) + { + CollectEntityAssetURIs(ent, formattedBaseURI, assetURIs); + } + } + + // Background-level asset URIs (panorama image, lite procedural sky textures). + if (veml.environment != null && veml.environment.background != null) + { + Schema.V3_0.background bg = veml.environment.background; + if (bg.ItemElementName == Schema.V3_0.ItemChoiceType.panorama + && bg.Item is string panoramaURI && !string.IsNullOrEmpty(panoramaURI)) + { + assetURIs.Add(VEMLUtilities.FullyQualifyURI(panoramaURI, formattedBaseURI)); + } + else if (bg.ItemElementName == Schema.V3_0.ItemChoiceType.liteproceduralsky + && bg.Item is Schema.V3_0.liteproceduralsky lps) + { + if (!string.IsNullOrEmpty(lps.startextureuri)) + { + assetURIs.Add(VEMLUtilities.FullyQualifyURI(lps.startextureuri, formattedBaseURI)); + } + if (!string.IsNullOrEmpty(lps.cloudstextureuri)) + { + assetURIs.Add(VEMLUtilities.FullyQualifyURI(lps.cloudstextureuri, formattedBaseURI)); + } + } + } + + // HEAD-request each unique asset URI. 0 = pending, -1 = HEAD not supported in this build. + Dictionary headResults = new Dictionary(); + foreach (string uri in assetURIs) + { + if (string.IsNullOrEmpty(uri) || headResults.ContainsKey(uri)) + { + continue; + } + headResults[uri] = 0; +#if USE_WEBINTERFACE + string capturedURI = uri; + Action, byte[]> onHeadResponse = + new Action, byte[]>((code, headers, data) => + { + headResults[capturedURI] = code == 0 ? -2 : code; + }); + HTTPRequest headReq = new HTTPRequest(uri, HTTPRequest.HTTPMethod.Head, onHeadResponse); + headReq.Send(); +#else + headResults[uri] = -1; +#endif + } + + // Wait for all scripts and HEAD-requests to settle, or for timeout. + float elapsed = 0f; + while (elapsed < timeout) + { + bool allDone = true; + foreach (KeyValuePair kv in scriptResults) + { + if (kv.Value == null) { allDone = false; break; } + } + if (allDone) + { + foreach (KeyValuePair kv in headResults) + { + if (kv.Value == 0) { allDone = false; break; } + } + } + if (allDone) break; + yield return new WaitForSeconds(0.25f); + elapsed += 0.25f; + } + + // Aggregate failures. + foreach (KeyValuePair kv in scriptResults) + { + if (kv.Value == null) + { + errors.Add("Script load timeout: " + kv.Key); + } + else if (kv.Value.Length == 0) + { + errors.Add("Script empty or unreachable: " + kv.Key); + } + } + foreach (KeyValuePair kv in headResults) + { + if (kv.Value == 0) + { + errors.Add("Asset URI request timeout: " + kv.Key); + } + else if (kv.Value == -1) + { + // Build lacks USE_WEBINTERFACE; we couldn't verify. Don't flag as error. + } + else if (kv.Value == -2) + { + errors.Add("Asset URI request failed (network/no response): " + kv.Key); + } + else if (kv.Value > 399) + { + errors.Add("Asset URI returned " + kv.Value + ": " + kv.Key); + } + } + + bool success = errors.Count == 0; + string errorMessage = success ? null : string.Join("\n", errors); + onTestComplete.Invoke(success, errorMessage, title); + } + + /// + /// Recursively collect asset URIs referenced by an entity and its descendants. NOTE: when new + /// entity types are added to the V3.0 schema with new URI fields, extend this method to pick + /// them up — otherwise TestVEMLDocument's asset checks will silently miss them. + /// + private void CollectEntityAssetURIs(Schema.V3_0.entity ent, string baseURI, List output) + { + if (ent == null) return; + + // Mesh-based entity types each carry a meshresource[] array. + string[] meshResources = null; + if (ent is Schema.V3_0.mesh m) meshResources = m.meshresource; + else if (ent is Schema.V3_0.airplane a) meshResources = a.meshresource; + else if (ent is Schema.V3_0.automobile au) meshResources = au.meshresource; + else if (ent is Schema.V3_0.character c) meshResources = c.meshresource; + + if (meshResources != null) + { + foreach (string mr in meshResources) + { + if (!string.IsNullOrEmpty(mr)) + { + output.Add(VEMLUtilities.FullyQualifyURI(mr, baseURI)); + } + } + } + + if (ent is Schema.V3_0.image img && !string.IsNullOrEmpty(img.imagefile)) + { + output.Add(VEMLUtilities.FullyQualifyURI(img.imagefile, baseURI)); + } + if (ent is Schema.V3_0.audio aud && !string.IsNullOrEmpty(aud.audiofile)) + { + output.Add(VEMLUtilities.FullyQualifyURI(aud.audiofile, baseURI)); + } + if (ent is Schema.V3_0.html h && !string.IsNullOrEmpty(h.url)) + { + output.Add(VEMLUtilities.FullyQualifyURI(h.url, baseURI)); + } + + // Terrain-layer textures. + if (ent is Schema.V3_0.terrain t && t.layer != null) + { + foreach (Schema.V3_0.terrainlayer layer in t.layer) + { + if (layer == null) continue; + if (!string.IsNullOrEmpty(layer.diffusetexture)) + { + output.Add(VEMLUtilities.FullyQualifyURI(layer.diffusetexture, baseURI)); + } + if (!string.IsNullOrEmpty(layer.normaltexture)) + { + output.Add(VEMLUtilities.FullyQualifyURI(layer.normaltexture, baseURI)); + } + if (!string.IsNullOrEmpty(layer.masktexture)) + { + output.Add(VEMLUtilities.FullyQualifyURI(layer.masktexture, baseURI)); + } + } + } + + // Recurse into children (the schema names this field "entity1" because "entity" is taken). + if (ent.entity1 != null) + { + foreach (Schema.V3_0.entity child in ent.entity1) + { + CollectEntityAssetURIs(child, baseURI, output); + } + } + } + /// /// Download a VEML document. /// @@ -608,7 +882,7 @@ private void FinishVEMLDownload(string uri, int responseCode, byte[] rawData) /// Base URI of the VEML document. /// Action to invoke upon completion of world loading. /// Provides a success/fail indication. - private IEnumerator ApplyVEMLDocument(Schema.V3_0.veml vemlDocument, string baseURI, Action onComplete) + private IEnumerator ApplyVEMLDocument(Schema.V3_0.veml vemlDocument, string baseURI, Action onComplete, string requireScript = null) { string formattedBaseURI = VEMLUtilities.FormatURI(baseURI); @@ -621,7 +895,7 @@ private IEnumerator ApplyVEMLDocument(Schema.V3_0.veml vemlDocument, string base scriptsDoneProcessing = true; }); - if (ProcessMetadata(vemlDocument, baseURI, onScriptsProcessed) == false) + if (ProcessMetadata(vemlDocument, baseURI, onScriptsProcessed, requireScript) == false) { Logging.LogWarning("[VEMLHandler->ApplyVEMLDocument] Error processing metadata."); onComplete.Invoke(false); @@ -698,7 +972,7 @@ private IEnumerator ApplyVEMLDocument(Schema.V3_0.veml vemlDocument, string base /// Action to invoke when scripts are processed. Provides an array of /// strings containing the script contents. /// Whether or not the operation succeeded. - private bool ProcessMetadata(Schema.V3_0.veml vemlDocument, string baseURI, Action onScriptsProcessed) + private bool ProcessMetadata(Schema.V3_0.veml vemlDocument, string baseURI, Action onScriptsProcessed, string requireScript = null) { string formattedBaseURI = VEMLUtilities.FormatURI(baseURI); @@ -726,7 +1000,7 @@ private bool ProcessMetadata(Schema.V3_0.veml vemlDocument, string baseURI, Acti return false; } - StartCoroutine(ProcessScripts(vemlDocument, baseURI, onScriptsProcessed)); + StartCoroutine(ProcessScripts(vemlDocument, baseURI, onScriptsProcessed, requireScript)); if (ProcessInputEvents(vemlDocument, baseURI) == false) { @@ -822,12 +1096,33 @@ private bool ProcessEnvironment(Schema.V3_0.veml vemlDocument, string baseURI) /// Base URI of the VEML document. /// Action to invoke when scripts are processed. Provides an array of /// strings containing the script contents. - private IEnumerator ProcessScripts(Schema.V3_0.veml vemlDocument, string baseURI, Action onProcessed) + private IEnumerator ProcessScripts(Schema.V3_0.veml vemlDocument, string baseURI, Action onProcessed, string requireScript = null) { string formattedBaseURI = VEMLUtilities.FormatURI(baseURI); Dictionary scriptsToRun = new Dictionary(); + // Set up requireScript (loaded ahead of VEML's own scripts so it runs first). + string resolvedRequireScript = null; + bool requireScriptLoaded = string.IsNullOrEmpty(requireScript); + if (!string.IsNullOrEmpty(requireScript)) + { + if (requireScript.EndsWith(".js")) + { + LoadScriptResourceAsString(VEMLUtilities.FullyQualifyURI(requireScript, formattedBaseURI), + new Action((scr) => + { + resolvedRequireScript = scr; + requireScriptLoaded = true; + })); + } + else + { + resolvedRequireScript = requireScript; + requireScriptLoaded = true; + } + } + // Set up scripts. if (vemlDocument.metadata.script != null) { @@ -858,20 +1153,30 @@ private IEnumerator ProcessScripts(Schema.V3_0.veml vemlDocument, string baseURI bool allLoaded = true; do { - allLoaded = true; - foreach (string script in scriptsToRun.Values) + allLoaded = requireScriptLoaded; + if (allLoaded) { - if (script == null) + foreach (string script in scriptsToRun.Values) { - allLoaded = false; - yield return new WaitForSeconds(0.25f); - elapsedTime += 0.25f; - break; + if (script == null) + { + allLoaded = false; + break; + } } } + if (!allLoaded) + { + yield return new WaitForSeconds(0.25f); + elapsedTime += 0.25f; + } } while (allLoaded == false && elapsedTime < timeout); List scripts = new List(); + if (!string.IsNullOrEmpty(resolvedRequireScript)) + { + scripts.Add(resolvedRequireScript); + } foreach (string script in scriptsToRun.Values) { scripts.Add(script); diff --git a/Assets/Runtime/Runtime/Scripts/WebVerseRuntime.cs b/Assets/Runtime/Runtime/Scripts/WebVerseRuntime.cs index c0371e3d..58dbd6f8 100644 --- a/Assets/Runtime/Runtime/Scripts/WebVerseRuntime.cs +++ b/Assets/Runtime/Runtime/Scripts/WebVerseRuntime.cs @@ -312,6 +312,14 @@ public struct RuntimeSettings [Tooltip("Material to use for highlighting.")] public Material highlightMaterial; + /// + /// Material to use for the placement preview of entities (the "ghost" mesh shown while + /// the user is positioning an entity). If left unassigned, preview meshes render with + /// Unity's missing-material pink. + /// + [Tooltip("Material to use for entity placement previews.")] + public Material previewMaterial; + /// /// Material to use for the sky. /// @@ -690,7 +698,11 @@ public void LoadURL(string url, Action onLoaded = null) /// /// URL containing the world to load. /// Action to perform on load. Provides string containing loaded world name. - public void LoadWorld(string url, Action onLoaded) + /// Optional. Either inline JavaScript logic or a URI ending in ".js" + /// pointing to a script resource. The script is prepended to the world's script list and runs in + /// the same JINT engine as the world's own scripts. Only honored for VEML worlds; a warning is + /// logged and the script is ignored for x3d and glTF worlds. + public void LoadWorld(string url, Action onLoaded, string requireScript = null) { if (straightFour == null) { @@ -698,6 +710,8 @@ public void LoadWorld(string url, Action onLoaded) return; } + currentURL = url; + if (StraightFour.StraightFour.ActiveWorld != null) { UnloadWorld(); @@ -711,6 +725,14 @@ public void LoadWorld(string url, Action onLoaded) queryParams = url.Substring(url.IndexOf('?') + 1); } + if (!string.IsNullOrEmpty(requireScript) + && (baseURL.EndsWith(".x3d") || baseURL.EndsWith(".x3db") || baseURL.EndsWith(".x3dv") + || baseURL.EndsWith(".glb") || baseURL.EndsWith(".gltf"))) + { + Logging.LogWarning("[WebVerseRuntime->LoadWorld] requireScript is only supported for " + + "VEML worlds; ignoring for " + baseURL); + } + if (baseURL.EndsWith(".x3d") || baseURL.EndsWith(".x3db") || baseURL.EndsWith(".x3dv")) { x3dHandler.GetX3DTitle(baseURL, (title) => @@ -828,13 +850,48 @@ public void LoadWorld(string url, Action onLoaded) enableDefault = Logging.GetConfiguration().enableDefault }; StraightFour.StraightFour.LoadWorld(title, queryParams); - vemlHandler.LoadVEMLDocumentIntoWorld(baseURL, onLoadComplete); + vemlHandler.LoadVEMLDocumentIntoWorld(baseURL, onLoadComplete, requireScript); }; vemlHandler.GetWorldName(baseURL, onFound); } } + /// + /// Dry-run validation of a world's VEML document without switching to it. Downloads and + /// parses the VEML, downloads (but does not execute) referenced scripts, and HEAD-requests + /// referenced asset URIs. Does not unload the active world, mutate currentURL, change runtime + /// state, or touch the JINT engine. + /// + /// URL of the VEML world to test. + /// Invoked with (success, errorMessage, title). errorMessage is + /// null on success; on failure it is a newline-separated list of issues. title is the parsed + /// metadata.title if the document parsed, otherwise null. + public void TestLoadWorld(string url, Action onTestComplete) + { + if (vemlHandler == null) + { + onTestComplete.Invoke(false, "VEML handler not initialized.", null); + return; + } + + string baseURL = url; + if (url.Contains("?")) + { + baseURL = url.Substring(0, url.IndexOf('?')); + } + + if (baseURL.EndsWith(".x3d") || baseURL.EndsWith(".x3db") || baseURL.EndsWith(".x3dv") + || baseURL.EndsWith(".glb") || baseURL.EndsWith(".gltf")) + { + onTestComplete.Invoke(false, + "TestLoadWorld currently supports VEML worlds only.", null); + return; + } + + vemlHandler.TestVEMLDocument(baseURL, onTestComplete); + } + /// /// Unload a world. /// @@ -922,6 +979,7 @@ public void UnloadWorld() /// Action to perform on load. Provides string indicating web page. public void LoadWebPage(string url, Action onLoaded) { + currentURL = url; state = RuntimeState.WebPage; webverseWebView.Show(); webverseWebView.LoadURL(url); @@ -1009,6 +1067,7 @@ private void InitializeComponents(LocalStorageManager.LocalStorageMode storageMo } straightFour.airplaneEntityPrefab = airplaneEntityPrefab; straightFour.highlightMaterial = highlightMaterial; + straightFour.previewMaterial = previewMaterial; straightFour.skyMaterial = skyMaterial; straightFour.liteProceduralSkyMaterial = liteProceduralSkyMaterial; straightFour.liteProceduralSkyObject = liteProceduralSkyObject; diff --git a/Assets/Runtime/StraightFour/Entity/Airplane/Scripts/AirplaneEntity.cs b/Assets/Runtime/StraightFour/Entity/Airplane/Scripts/AirplaneEntity.cs index e14ad7f8..dc781925 100644 --- a/Assets/Runtime/StraightFour/Entity/Airplane/Scripts/AirplaneEntity.cs +++ b/Assets/Runtime/StraightFour/Entity/Airplane/Scripts/AirplaneEntity.cs @@ -690,9 +690,11 @@ private void MakePlacing() gameObject.SetActive(true); rbody.isKinematic = true; + // Disable colliders during placement so the placement raycast passes through the + // preview to hit world geometry. See MeshEntity.MakePlacing for context. foreach (MeshCollider meshCollider in meshColliders) { - meshCollider.enabled = true; + meshCollider.enabled = false; } interactionState = InteractionState.Placing; } @@ -770,17 +772,16 @@ private void SetUpPreviewObject() DestroyImmediate(entity); } - Collider collider = previewObject.GetComponent(); - if (collider) + // Remove ALL colliders on the preview (root + descendants). See MeshEntity for context. + foreach (Collider c in previewObject.GetComponentsInChildren(true)) { - Destroy(collider); + DestroyImmediate(c); } - Rigidbody rbody = previewObject.GetComponent(); - if (rbody) + foreach (Rigidbody rb in previewObject.GetComponentsInChildren(true)) { - Destroy(rbody); - } + DestroyImmediate(rb); + } foreach (MeshRenderer rend in previewObject.GetComponentsInChildren()) { diff --git a/Assets/Runtime/StraightFour/Entity/Automobile/Scripts/AutomobileEntity.cs b/Assets/Runtime/StraightFour/Entity/Automobile/Scripts/AutomobileEntity.cs index b4f875be..0cb15b8a 100644 --- a/Assets/Runtime/StraightFour/Entity/Automobile/Scripts/AutomobileEntity.cs +++ b/Assets/Runtime/StraightFour/Entity/Automobile/Scripts/AutomobileEntity.cs @@ -809,9 +809,11 @@ private void MakePlacing() gameObject.SetActive(true); rbody.isKinematic = true; + // Disable colliders during placement so the placement raycast passes through the + // preview to hit world geometry. See MeshEntity.MakePlacing for context. foreach (MeshCollider meshCollider in meshColliders) { - meshCollider.enabled = true; + meshCollider.enabled = false; } interactionState = InteractionState.Placing; } @@ -889,17 +891,16 @@ private void SetUpPreviewObject() DestroyImmediate(entity); } - Collider collider = previewObject.GetComponent(); - if (collider) + // Remove ALL colliders on the preview (root + descendants). See MeshEntity for context. + foreach (Collider c in previewObject.GetComponentsInChildren(true)) { - Destroy(collider); + DestroyImmediate(c); } - Rigidbody rbody = previewObject.GetComponent(); - if (rbody) + foreach (Rigidbody rb in previewObject.GetComponentsInChildren(true)) { - Destroy(rbody); - } + DestroyImmediate(rb); + } foreach (MeshRenderer rend in previewObject.GetComponentsInChildren()) { diff --git a/Assets/Runtime/StraightFour/Entity/Base/Scripts/BaseEntity.cs b/Assets/Runtime/StraightFour/Entity/Base/Scripts/BaseEntity.cs index f8d0b0d8..10e49ea1 100644 --- a/Assets/Runtime/StraightFour/Entity/Base/Scripts/BaseEntity.cs +++ b/Assets/Runtime/StraightFour/Entity/Base/Scripts/BaseEntity.cs @@ -94,6 +94,11 @@ public enum InteractionState { Hidden, Static, Physical, Placing } /// private List seats = new List(); + /// + /// Animation names tracked by this entity's animation control methods. + /// + private HashSet definedAnimations = new HashSet(); + /// /// Interaction state of the entity. /// @@ -731,8 +736,10 @@ public virtual bool PlayAnimation(string animationName) AnimationClip clip = animation.GetClip(animationName); if (clip != null) { + definedAnimations.Add(animationName); animation[animationName].weight = 0.1f; animation.Play(animationName); + return true; } } } @@ -764,6 +771,29 @@ public virtual bool StopAnimation(string animationName) return false; } + /// + /// Stop all animations. + /// + /// Whether or not any animations were found. + public virtual bool StopAllAnimations() + { + if (definedAnimations.Count > 0) + { + bool foundAnimation = false; + foreach (string animationName in definedAnimations) + { + if (StopAnimation(animationName)) + { + foundAnimation = true; + } + } + + return foundAnimation; + } + + return false; + } + /// /// Set the speed of an animation. /// diff --git a/Assets/Runtime/StraightFour/Entity/Character/Scripts/CharacterEntity.cs b/Assets/Runtime/StraightFour/Entity/Character/Scripts/CharacterEntity.cs index 11d2d2eb..93538c58 100644 --- a/Assets/Runtime/StraightFour/Entity/Character/Scripts/CharacterEntity.cs +++ b/Assets/Runtime/StraightFour/Entity/Character/Scripts/CharacterEntity.cs @@ -132,11 +132,27 @@ public override string entityTag private GameObject highlightCube; /// - /// The current applied velocity. + /// The current horizontal displacement applied this tick (x and z). Resets after each + /// FixedUpdate. y is unused; vertical motion lives in verticalVelocity. /// private Vector3 currentVelocity = Vector3.zero; /// + /// Vertical velocity in meters/second. Integrated by gravity each FixedUpdate; displacement + /// is verticalVelocity * Time.deltaTime applied via CharacterController.Move. Jump() and the + /// y component of Move(amount) treat this as velocity (m/s), not per-frame displacement. + /// + private float verticalVelocity = 0f; + + /// + /// When true, FixedUpdate skips gravity, grounding, and Move() entirely. Some other system + /// (typically the VR rig writing position from rigOrigin every Update via UpdateFollowers) + /// is the sole writer of this entity's position, so the entity must not also write or the + /// two writers fight and the avatar flickers between positions. Wired via + /// Input.AddRigFollower / RemoveRigFollower. + /// + public bool externalPositionControl = false; + /// Avatar animation manager for this character. /// private AvatarAnimationManager _avatarAnimationManager; @@ -368,7 +384,8 @@ public bool Move(Vector3 amount, bool synchronize = true) return false; } - currentVelocity = new Vector3(currentVelocity.x + amount.x, currentVelocity.y + amount.y, currentVelocity.z + amount.z); + currentVelocity.x += amount.x; + currentVelocity.z += amount.z; characterController.Move(amount); if (synchronizer != null && synchronize == true) @@ -402,7 +419,7 @@ public bool Jump(float amount, bool discardIfFalling = true, bool synchronize = if (IsOnSurface() || !discardIfFalling) { - currentVelocity.y += amount; + verticalVelocity += amount; } if (synchronizer != null && synchronize == true) @@ -431,7 +448,16 @@ public bool IsOnSurface() return false; } - return Physics.Raycast(transform.position - new Vector3(0, characterController.height / 2, 0), Vector3.down, 0.25f); + // CharacterController.isGrounded is the canonical signal; it's updated each Move() with + // the controller's internal slope/penetration logic and accounts for skinWidth. + if (characterController.isGrounded) return true; + + // Fallback raycast for the pre-first-Move case and as a small-gap safety net. + // Distance is skinWidth + a small margin so we catch the "just barely above floor" + // state without falsely reporting grounded at large gaps. + float rayDistance = characterController.skinWidth + 0.1f; + return Physics.Raycast(transform.position - new Vector3(0, characterController.height / 2f, 0), + Vector3.down, rayDistance); } public bool IsAboveGround() @@ -1205,6 +1231,19 @@ private void MakePhysical() } gameObject.SetActive(true); + // Keep Rigidbody kinematic in Physical state — character motion is owned by + // CharacterController.Move() in FixedUpdate. A non-kinematic Rigidbody on the same + // GameObject causes per-frame position fighting (visible as the avatar/label rendering + // at two flickering positions). Defends against SetMotion(Moving) flipping this. + if (rigidBody != null) + { + rigidBody.isKinematic = true; + } + // Leave capsuleCollider disabled — the CharacterController is the canonical collider + // for character motion. Enabling the CapsuleCollider on the same GameObject made the + // character settle ~0.5 m above the floor (the CapsuleCollider's center.y) because the + // two colliders fight during depenetration. MakeHidden and MakeStatic both disable it + // for the same reason; Physical should match. interactionState = InteractionState.Physical; } @@ -1268,38 +1307,45 @@ private void SetUpHighlightVolume() highlightCube.SetActive(false); } - private float timeToWaitForUpdate = 0.025f; - private float timeWaitedForUpdate = 0; private int stepToRaise = 1; private int maxStepToRaise = 1024; void FixedUpdate() { - timeWaitedForUpdate += Time.deltaTime; - if (timeWaitedForUpdate >= timeToWaitForUpdate) - { - timeWaitedForUpdate = 0; - } - else + // No throttling here — FixedUpdate already runs at the fixed physics rate (50 Hz by + // default), which is the correct cadence for gravity integration. The previous throttle + // (0.025s) caused FixedUpdate to fire only every other tick at fixedDeltaTime=0.02s but + // still used Time.deltaTime=0.02s in the integration math — producing gravity at half + // the intended rate. + if (characterController == null) { + //LogSystem.LogError("[CharacterEntity->Update] No character controller for character entity."); return; } - if (characterController == null) + // If some other system (typically the VR rig via UpdateFollowers) is writing this + // entity's position each frame, don't add a second writer here — they would fight and + // the avatar would flicker between the two writers' positions. + if (externalPositionControl) { - //LogSystem.LogError("[CharacterEntity->Update] No character controller for character entity."); return; } - if (IsOnSurface() && currentVelocity.y < 0) + // Reset vertical velocity when grounded and falling — prevents gravity from compounding + // while on a surface, and zeroes any tiny residual downward velocity from prior ticks. + if (IsOnSurface() && verticalVelocity < 0) { - currentVelocity.y = 0f; + verticalVelocity = 0f; } if (rigidBody.useGravity) { - currentVelocity.y += -9.81f * Time.deltaTime; // TODO: Magic number, tie into larger gravity system. + verticalVelocity += -9.81f * Time.deltaTime; // TODO: tie into larger gravity system. } - characterController.Move(currentVelocity); + + // currentVelocity.x/z are per-frame displacement (legacy semantics callers depend on). + // verticalVelocity is in m/s — multiply by dt to convert to displacement-this-tick. + characterController.Move(new Vector3( + currentVelocity.x, verticalVelocity * Time.deltaTime, currentVelocity.z)); currentVelocity.x = currentVelocity.z = 0; if (fixHeight) diff --git a/Assets/Runtime/StraightFour/Entity/Mesh/Scripts/MeshEntity.cs b/Assets/Runtime/StraightFour/Entity/Mesh/Scripts/MeshEntity.cs index 2e252175..700c02de 100644 --- a/Assets/Runtime/StraightFour/Entity/Mesh/Scripts/MeshEntity.cs +++ b/Assets/Runtime/StraightFour/Entity/Mesh/Scripts/MeshEntity.cs @@ -377,6 +377,7 @@ public override void Initialize(System.Guid idToSet) MakeHidden(); SetUpHighlightVolume(); + StopAllAnimations(); } /// @@ -637,11 +638,15 @@ private void MakePlacing() gameObject.SetActive(true); rigidBody.isKinematic = true; + // Disable colliders during placement so the placement raycast (camera/pointer) passes + // through the preview to hit world geometry. With colliders on, the entity blocks its + // own placement raycast, producing erratic positioning. Make* methods that exit Placing + // (Static/Physical) re-enable colliders normally. foreach (MeshCollider meshCollider in meshColliders) { - meshCollider.enabled = true; + meshCollider.enabled = false; } - boxCollider.enabled = true; + boxCollider.enabled = false; interactionState = InteractionState.Placing; } @@ -718,16 +723,18 @@ private void SetUpPreviewObject() DestroyImmediate(entity); } - Collider collider = previewObject.GetComponent(); - if (collider) + // Remove ALL colliders on the preview (root + descendants). The previous code only + // removed one Collider from the root, so entities with multiple colliders or any + // colliders on child GameObjects left the preview raycastable. + foreach (Collider c in previewObject.GetComponentsInChildren(true)) { - Destroy(collider); + DestroyImmediate(c); } - Rigidbody rbody = previewObject.GetComponent(); - if (rbody) + // Remove Rigidbodies from root + descendants for consistency. + foreach (Rigidbody rb in previewObject.GetComponentsInChildren(true)) { - Destroy(rbody); + DestroyImmediate(rb); } // Strip CollisionEmitter from preview clone to prevent ghost events diff --git a/Assets/Runtime/StraightFour/Entity/Water/Scripts/WaterBlockerEntity.cs b/Assets/Runtime/StraightFour/Entity/Water/Scripts/WaterBlockerEntity.cs index 5dce8c8a..f2465716 100644 --- a/Assets/Runtime/StraightFour/Entity/Water/Scripts/WaterBlockerEntity.cs +++ b/Assets/Runtime/StraightFour/Entity/Water/Scripts/WaterBlockerEntity.cs @@ -579,7 +579,9 @@ private void MakePlacing() gameObject.SetActive(true); rigidBody.isKinematic = true; - boxCollider.enabled = true; + // Disable colliders during placement so the placement raycast passes through the + // preview to hit world geometry. See MeshEntity.MakePlacing for context. + boxCollider.enabled = false; interactionState = InteractionState.Placing; } @@ -656,17 +658,16 @@ private void SetUpPreviewObject() DestroyImmediate(entity); } - Collider collider = previewObject.GetComponent(); - if (collider) + // Remove ALL colliders on the preview (root + descendants). See MeshEntity for context. + foreach (Collider c in previewObject.GetComponentsInChildren(true)) { - Destroy(collider); + DestroyImmediate(c); } - Rigidbody rbody = previewObject.GetComponent(); - if (rbody) + foreach (Rigidbody rb in previewObject.GetComponentsInChildren(true)) { - Destroy(rbody); - } + DestroyImmediate(rb); + } foreach (MeshRenderer rend in previewObject.GetComponentsInChildren()) { diff --git a/Assets/Runtime/StraightFour/Entity/Water/Scripts/WaterBodyEntity.cs b/Assets/Runtime/StraightFour/Entity/Water/Scripts/WaterBodyEntity.cs index 00a70b2c..87f2f4ca 100644 --- a/Assets/Runtime/StraightFour/Entity/Water/Scripts/WaterBodyEntity.cs +++ b/Assets/Runtime/StraightFour/Entity/Water/Scripts/WaterBodyEntity.cs @@ -689,7 +689,9 @@ private void MakePlacing() gameObject.SetActive(true); rigidBody.isKinematic = true; - meshCollider.enabled = true; + // Disable colliders during placement so the placement raycast passes through the + // preview to hit world geometry. See MeshEntity.MakePlacing for context. + meshCollider.enabled = false; interactionState = InteractionState.Placing; } @@ -766,17 +768,16 @@ private void SetUpPreviewObject() DestroyImmediate(entity); } - Collider collider = previewObject.GetComponent(); - if (collider) + // Remove ALL colliders on the preview (root + descendants). See MeshEntity for context. + foreach (Collider c in previewObject.GetComponentsInChildren(true)) { - Destroy(collider); + DestroyImmediate(c); } - Rigidbody rbody = previewObject.GetComponent(); - if (rbody) + foreach (Rigidbody rb in previewObject.GetComponentsInChildren(true)) { - Destroy(rbody); - } + DestroyImmediate(rb); + } foreach (MeshRenderer rend in previewObject.GetComponentsInChildren()) { diff --git a/Assets/Runtime/StraightFour/Testing/EntityTests/CharacterEntityGroundingTests.cs b/Assets/Runtime/StraightFour/Testing/EntityTests/CharacterEntityGroundingTests.cs new file mode 100644 index 00000000..74c4d4a6 --- /dev/null +++ b/Assets/Runtime/StraightFour/Testing/EntityTests/CharacterEntityGroundingTests.cs @@ -0,0 +1,368 @@ +// Copyright (c) 2019-2026 Five Squared Interactive. All rights reserved. + +using System; +using System.Collections; +using System.Collections.Generic; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; +using FiveSQD.StraightFour; +using FiveSQD.StraightFour.Entity; +using UnityEditor; + +/// +/// PlayMode tests that exercise CharacterEntity grounding and gravity behavior to validate four +/// hypotheses about a VR floating/oscillating bug: +/// (1) IsOnSurface raycast (0.25 units) is too short to reliably detect contact. +/// (2) CharacterController.isGrounded is never consulted. +/// (3) Pure-vertical Move() calls behave differently from mixed-axis ones. +/// (4) currentVelocity is treated inconsistently as both velocity (m/s) and per-tick displacement. +/// +/// Each test is self-contained: it builds a floor (or no floor for free-fall) and a CharacterEntity, +/// then asserts on grounded/position state after a known physics interval. The tests are tuned so +/// they fail on the current code path and pass with the proposed fixes (longer raycast, isGrounded +/// fallback, units fix, etc.) — so they double as a regression gate. +/// +public class CharacterEntityGroundingTests +{ + private GameObject weGO; + private StraightFour straightFour; + private GameObject cameraGO; + private GameObject floorGO; + + /// + /// Helper: compute the transform.y needed to place the controller's foot at the given world Y. + /// CharacterController.center stays at Unity default (0,0,0), so foot = transform.y - height/2. + /// + private static float TransformYForFoot(CharacterEntity ce, float footY) + { + CharacterController cc = ce.GetComponent(); + return footY + cc.height / 2f - cc.center.y; + } + + [OneTimeSetUp] + public void OneTimeSetUp() + { + LogAssert.ignoreFailingMessages = true; + } + + [SetUp] + public void SetUp() + { + LogAssert.ignoreFailingMessages = true; + } + + [TearDown] + public void TearDown() + { + try + { + if (StraightFour.ActiveWorld != null) + { + StraightFour.UnloadWorld(); + } + } + catch (Exception) + { + // CameraManager may already be destroyed. + } + if (floorGO != null) { UnityEngine.Object.DestroyImmediate(floorGO); floorGO = null; } + if (cameraGO != null) { UnityEngine.Object.DestroyImmediate(cameraGO); cameraGO = null; } + } + + /// + /// Builds a StraightFour world, a flat floor at y=0 (top surface), and spawns a CharacterEntity + /// at the provided spawnPosition. fixHeight is disabled so the rescue-warp doesn't mask grounding + /// bugs. The returned CharacterEntity is fully initialized. + /// + private IEnumerator BuildSceneAndCharacter(Vector3 spawnPosition, bool buildFloor, + Action callback) + { + LogAssert.ignoreFailingMessages = true; + + cameraGO = new GameObject("TestCamera"); + Camera camera = cameraGO.AddComponent(); + camera.transform.position = new Vector3(0, 0, -10); + cameraGO.tag = "MainCamera"; + + if (buildFloor) + { + // A wide flat box collider with its top surface at y=0. + floorGO = GameObject.CreatePrimitive(PrimitiveType.Cube); + floorGO.name = "TestFloor"; + floorGO.transform.position = new Vector3(0, -0.5f, 0); + floorGO.transform.localScale = new Vector3(20f, 1f, 20f); + } + + weGO = new GameObject("WE"); + straightFour = weGO.AddComponent(); + straightFour.characterControllerPrefab = AssetDatabase.LoadAssetAtPath( + "Assets/Runtime/StraightFour/Entity/Character/Prefabs/UserAvatar.prefab"); + straightFour.skyMaterial = AssetDatabase.LoadAssetAtPath( + "Assets/Runtime/StraightFour/Environment/Materials/Skybox.mat"); + yield return null; + StraightFour.LoadWorld("grounding-test"); + + bool loaded = false; + Guid charId = StraightFour.ActiveWorld.entityManager.LoadCharacterEntity( + null, null, Vector3.zero, Quaternion.identity, new Vector3(0, 2, 0), + spawnPosition, Quaternion.identity, Vector3.one, + tag: "test-char", + onLoaded: () => { loaded = true; }); + + float elapsed = 0f; + while (!loaded && elapsed < 5f) + { + yield return new WaitForSeconds(0.1f); + elapsed += 0.1f; + } + + CharacterEntity ce = StraightFour.ActiveWorld.entityManager.FindEntity(charId) as CharacterEntity; + Assert.IsNotNull(ce, "CharacterEntity failed to load within 5s."); + + // Initialize() leaves the character in Hidden state (gameObject.SetActive(false)) so + // FixedUpdate doesn't run. Switch to Physical to match the live VR scenario. + ce.SetInteractionState(BaseEntity.InteractionState.Physical); + ce.fixHeight = false; // Disable rescue warp so we observe raw grounding behavior. + + // Re-apply spawn position after going Physical, in case prior state changed transform. + ce.SetPosition(spawnPosition, local: true, synchronize: false); + + // Give one fixed tick for the controller to register. + yield return new WaitForFixedUpdate(); + + callback(ce); + } + + /// + /// Helper: project the character's foot Y from transform.position and CharacterController height. + /// + private static float FootY(CharacterEntity ce) + { + CharacterController cc = ce.GetComponent(); + return ce.transform.position.y - cc.height / 2f + cc.center.y; + } + + // --------------------------------------------------------------------------------------------- + // Suspect 1 + 4: After dropping from 3m, the character should settle with foot on the floor. + // --------------------------------------------------------------------------------------------- + + [UnityTest] + public IEnumerator CharacterEntity_Drops_FromThreeMeters_SettlesOnFloor() + { + CharacterEntity ce = null; + yield return BuildSceneAndCharacter(new Vector3(0, 3f, 0), buildFloor: true, c => ce = c); + + // Let physics settle. Free-fall from 3m takes ~0.78s; allow generous margin. + yield return new WaitForSeconds(3f); + + float foot = FootY(ce); + Assert.That(foot, Is.EqualTo(0f).Within(0.10f), + $"Character did not settle on floor. Foot y = {foot:F3}, expected ~0. " + + $"Transform y = {ce.transform.position.y:F3}. Suspect 1 (short raycast), " + + $"Suspect 4 (units), or a Rigidbody fighting CharacterController."); + } + + // --------------------------------------------------------------------------------------------- + // Suspect 1: After landing, the character should hold position. Bobbing > a few mm is the bug. + // --------------------------------------------------------------------------------------------- + + [UnityTest] + public IEnumerator CharacterEntity_AfterLanding_DoesNotOscillate() + { + CharacterEntity ce = null; + yield return BuildSceneAndCharacter(new Vector3(0, 3f, 0), buildFloor: true, c => ce = c); + + // Wait for initial settle. + yield return new WaitForSeconds(3f); + + // Sample foot y over ~1s of physics. With CharacterEntity throttled to ~25 Hz updates, + // we sample 40 frames to cover one second comfortably. + List samples = new List(); + for (int i = 0; i < 50; i++) + { + samples.Add(FootY(ce)); + yield return new WaitForFixedUpdate(); + } + + float min = float.MaxValue, max = float.MinValue; + foreach (float y in samples) { if (y < min) min = y; if (y > max) max = y; } + float swing = max - min; + + Assert.Less(swing, 0.02f, + $"Character oscillated by {swing:F4} m after settling (min={min:F4} max={max:F4}). " + + "Suspect 1 (short raycast) or Rigidbody-vs-CharacterController fight."); + } + + // --------------------------------------------------------------------------------------------- + // Suspect 1 direct: IsOnSurface() must return true when the foot is touching the floor. + // --------------------------------------------------------------------------------------------- + + [UnityTest] + public IEnumerator CharacterEntity_IsOnSurface_TrueAtSmallGapAboveFloor() + { + CharacterEntity ce = null; + yield return BuildSceneAndCharacter(new Vector3(0, 5f, 0), buildFloor: true, c => ce = c); + + // Disable gravity so the position holds while we measure. + Rigidbody rb = ce.GetComponent(); + if (rb != null) rb.useGravity = false; + + CharacterController cc = ce.GetComponent(); + float targetY = TransformYForFoot(ce, 0.05f); + + // CharacterController suppresses direct transform.position writes during simulation in + // some Unity versions. Canonical workaround: disable, teleport, re-enable. + cc.enabled = false; + ce.transform.position = new Vector3(0, targetY, 0); + cc.enabled = true; + + yield return new WaitForFixedUpdate(); + yield return new WaitForFixedUpdate(); + + Assert.IsTrue(ce.IsOnSurface(), + $"IsOnSurface returned false when foot was 0.05 m above floor. " + + $"Foot y={FootY(ce):F4}, transform y={ce.transform.position.y:F4}, " + + $"cc.isGrounded={cc.isGrounded}."); + } + + /// + /// IsOnSurface SHOULD return false when the gap is well beyond the raycast distance — this + /// documents current behavior and pairs with the previous test as a boundary regression. + /// + [UnityTest] + public IEnumerator CharacterEntity_IsOnSurface_FalseAtLargeGapAboveFloor() + { + CharacterEntity ce = null; + yield return BuildSceneAndCharacter(new Vector3(0, 5f, 0), buildFloor: true, c => ce = c); + + Rigidbody rb = ce.GetComponent(); + if (rb != null) rb.useGravity = false; + + CharacterController cc = ce.GetComponent(); + float targetY = TransformYForFoot(ce, 1.5f); + + cc.enabled = false; + ce.transform.position = new Vector3(0, targetY, 0); + cc.enabled = true; + + yield return new WaitForFixedUpdate(); + + Assert.IsFalse(ce.IsOnSurface(), + $"IsOnSurface returned true at a 1.5 m gap. Foot y={FootY(ce):F4}, " + + $"transform y={ce.transform.position.y:F4}, cc.isGrounded={cc.isGrounded}. " + + "Either the raycast is too long, or it's hitting something other than the floor."); + } + + // --------------------------------------------------------------------------------------------- + // Free-fall integration should approximate s = 0.5 * g * t^2. + // + // Analytic free-fall over t=0.3s: dy = 0.5 * 9.81 * 0.09 ≈ 0.44 m. + // Discrete sum at 50 Hz fixed-step: ≈ 0.47 m. + // Pre-fix bugs to catch: + // - Unit-confusion (velocity stored as displacement): dy ≈ 5–7 m (way over). + // - FixedUpdate-throttle gating with mismatched dt: dy ≈ 0.11 m (half-rate, way under). + // --------------------------------------------------------------------------------------------- + + [UnityTest] + public IEnumerator CharacterEntity_FreeFall_RoughlyMatchesGravity() + { + CharacterEntity ce = null; + // No floor. Spawn high so the character has room to fall. + yield return BuildSceneAndCharacter(new Vector3(0, 100f, 0), buildFloor: false, c => ce = c); + + // Let one tick pass to ensure the character is initialized in a normal state. + yield return new WaitForFixedUpdate(); + float startY = ce.transform.position.y; + + yield return new WaitForSeconds(0.3f); + float endY = ce.transform.position.y; + float dropped = startY - endY; + + Assert.Less(dropped, 0.8f, + $"Character fell {dropped:F3} m in 0.3 s. Analytic free-fall is ~0.44 m; >0.8 m suggests " + + "the units bug (currentVelocity used as displacement per tick)."); + Assert.Greater(dropped, 0.25f, + $"Character only fell {dropped:F3} m in 0.3 s — significantly less than the expected " + + "~0.44 m of free-fall. Suggests gravity is being applied at half rate (FixedUpdate " + + "throttle gate using Time.deltaTime instead of the throttle interval), or gravity is " + + "otherwise weakened."); + } + + // --------------------------------------------------------------------------------------------- + // Suspect 2 + 3: CharacterController.isGrounded should be consulted in grounding decisions, and + // pure-vertical Move() may behave worse than mixed-axis. This test compares behavior of an + // idle character vs one that's been "tickled" with a tiny horizontal perturbation each frame. + // If the perturbation produces visibly different (less bouncy) settling, Suspect 3 is real. + // --------------------------------------------------------------------------------------------- + + [UnityTest] + public IEnumerator CharacterEntity_PureVerticalSettle_NoWorseThan_MixedAxisSettle() + { + CharacterEntity ce = null; + yield return BuildSceneAndCharacter(new Vector3(0, 3f, 0), buildFloor: true, c => ce = c); + + // Phase A: idle settle for 3s, measure final y. + yield return new WaitForSeconds(3f); + float idleFootY = FootY(ce); + + // Reset position to drop again, this time with tiny horizontal Move()s during fall. + ce.SetPosition(new Vector3(0, 3f, 0), local: true, synchronize: false); + yield return new WaitForFixedUpdate(); + + for (int i = 0; i < 120; i++) + { + // Tiny non-zero horizontal nudge — invokes the CharacterController's mixed-axis solver. + ce.Move(new Vector3(0.0001f, 0, 0), synchronize: false); + yield return new WaitForFixedUpdate(); + } + float nudgedFootY = FootY(ce); + + // Both should settle near the floor (y=0). If the nudged version is closer to the floor than + // the idle version by more than 5 cm, Suspect 3 is confirmed. + float diff = Mathf.Abs(idleFootY - nudgedFootY); + Assert.Less(diff, 0.05f, + $"Idle-settle foot y = {idleFootY:F4}, nudged-settle foot y = {nudgedFootY:F4}, " + + $"diff = {diff:F4} m. A large diff means the CharacterController.Move() solver " + + "behaves differently for pure-vertical vs mixed-axis input (Suspect 3) — fix C from " + + "the diagnosis would address this."); + } + + // --------------------------------------------------------------------------------------------- + // Diagnostic dump: not a pass/fail test, but exercises the path and logs the key signals every + // tick so a human can read the console after a run. Useful first-look during investigation. + // --------------------------------------------------------------------------------------------- + + [UnityTest] + [Explicit("Diagnostic only — prints per-tick grounding state. Run manually when investigating.")] + public IEnumerator CharacterEntity_LogsGroundingStateEachTick() + { + CharacterEntity ce = null; + yield return BuildSceneAndCharacter(new Vector3(0, 3f, 0), buildFloor: true, c => ce = c); + + CharacterController cc = ce.GetComponent(); + Rigidbody rb = ce.GetComponent(); + + for (int i = 0; i < 80; i++) + { + Vector3 foot = ce.transform.position - new Vector3(0, cc.height / 2f, 0); + bool onSurface = ce.IsOnSurface(); + bool ccGrounded = cc.isGrounded; + + float gap = -1f; + if (Physics.Raycast(foot, Vector3.down, out RaycastHit hit, 10f, + ~0, QueryTriggerInteraction.Ignore)) + { + gap = hit.distance; + } + + Debug.Log($"[t={Time.time:F3}] footY={foot.y:F4} gap={gap:F4} " + + $"IsOnSurface={onSurface} cc.isGrounded={ccGrounded} " + + $"rb.kinematic={rb?.isKinematic} rb.useGravity={rb?.useGravity}"); + + yield return new WaitForFixedUpdate(); + } + + Assert.Pass("Diagnostic log complete — inspect Console output."); + } +} diff --git a/Assets/Runtime/StraightFour/Testing/EntityTests/CharacterEntityGroundingTests.cs.meta b/Assets/Runtime/StraightFour/Testing/EntityTests/CharacterEntityGroundingTests.cs.meta new file mode 100644 index 00000000..a032713f --- /dev/null +++ b/Assets/Runtime/StraightFour/Testing/EntityTests/CharacterEntityGroundingTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a56f70a55ee044ed9648d2baff9ae115 diff --git a/Assets/Runtime/TopLevel/Scenes/MobileRuntime.unity b/Assets/Runtime/TopLevel/Scenes/MobileRuntime.unity index 12b23ecc..e0cc0668 100644 --- a/Assets/Runtime/TopLevel/Scenes/MobileRuntime.unity +++ b/Assets/Runtime/TopLevel/Scenes/MobileRuntime.unity @@ -119,6 +119,108 @@ NavMeshSettings: debug: m_Flags: 0 m_NavMeshData: {fileID: 0} +--- !u!1001 &45003657 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + serializedVersion: 3 + m_TransformParent: {fileID: 0} + m_Modifications: + - target: {fileID: 4011543431373168956, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} + propertyPath: m_Pivot.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4011543431373168956, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} + propertyPath: m_Pivot.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4011543431373168956, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} + propertyPath: m_AnchorMax.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4011543431373168956, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} + propertyPath: m_AnchorMax.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4011543431373168956, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} + propertyPath: m_AnchorMin.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4011543431373168956, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} + propertyPath: m_AnchorMin.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4011543431373168956, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} + propertyPath: m_SizeDelta.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4011543431373168956, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} + propertyPath: m_SizeDelta.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4011543431373168956, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} + propertyPath: m_LocalPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4011543431373168956, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4011543431373168956, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} + propertyPath: m_LocalPosition.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4011543431373168956, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 4011543431373168956, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} + propertyPath: m_LocalRotation.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4011543431373168956, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} + propertyPath: m_LocalRotation.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4011543431373168956, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} + propertyPath: m_LocalRotation.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4011543431373168956, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} + propertyPath: m_AnchoredPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4011543431373168956, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} + propertyPath: m_AnchoredPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4011543431373168956, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4011543431373168956, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4011543431373168956, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4071536258802031107, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} + propertyPath: m_Name + value: TabUIWebViewPrefab + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: [] + m_AddedComponents: [] + m_SourcePrefab: {fileID: 100100000, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} +--- !u!1 &45003658 stripped +GameObject: + m_CorrespondingSourceObject: {fileID: 4071536258802031107, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} + m_PrefabInstance: {fileID: 45003657} + m_PrefabAsset: {fileID: 0} --- !u!1 &167713673 GameObject: m_ObjectHideFlags: 0 @@ -2167,6 +2269,7 @@ MonoBehaviour: stateSettings: {fileID: 11400000, guid: 3404c49a4170ad94e87f0e8545b86758, type: 2} airplaneEntityPrefab: {fileID: 2546332423327978143, guid: 9f84e1b9b912d9b4db6c879e27fa1ff8, type: 3} highlightMaterial: {fileID: 2100000, guid: fb72f213bae4057419539525b597f16d, type: 2} + previewMaterial: {fileID: 0} skyMaterial: {fileID: 2100000, guid: 0aade801f5d375245b8bd2843b11a686, type: 2} liteProceduralSkyMaterial: {fileID: 2100000, guid: acf78ab0ce284f148a485a21087ea77a, type: 2} liteProceduralSkyObject: {fileID: 1137534123} @@ -2821,6 +2924,763 @@ Transform: m_LocalPosition: {x: 554, y: 499.95, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 1988647508222235451} + - {fileID: 181125647174249131} + - {fileID: 4287565822571781507} + - {fileID: 5118400725549204037} + - {fileID: 2165298448928398224} + - {fileID: 6574365494872069928} + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &1898133186 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1898133184} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 781ffc189cb8d1a4181bf4e946009b29, type: 3} + m_Name: + m_EditorClassIdentifier: + runtime: {fileID: 472491403} + tabUIWebViewPrefab: {fileID: 45003658} + vrParent: {fileID: 0} + vrCamera: {fileID: 0} + maxTabs: 10 + maxSnapshotMemoryMB: 100 + homeUrl: +--- !u!114 &62904377632733555 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7258881038783247875} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: e70d2552bcc865047bc902f1a2461f87, type: 3} + m_Name: + m_EditorClassIdentifier: + tooltip: {fileID: 4872086524637674466} + tooltipText: Click to go forward + hoverActivationThreshold: 1 +--- !u!224 &181125647174249131 stripped +RectTransform: + m_CorrespondingSourceObject: {fileID: 4585713414978531267, guid: 1dc8289dee9c3e04bb253d553a6ea5d0, type: 3} + m_PrefabInstance: {fileID: 6453882070439004907} + m_PrefabAsset: {fileID: 0} +--- !u!1 &374595054142335279 stripped +GameObject: + m_CorrespondingSourceObject: {fileID: 4323634567748746375, guid: 6243bcac2f86a7a429b19a4b7364f8b1, type: 3} + m_PrefabInstance: {fileID: 4498976061784427153} + m_PrefabAsset: {fileID: 0} +--- !u!222 &897725327113868448 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7258881038783247875} + m_CullTransparentMesh: 1 +--- !u!1 &1018654042979147675 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1840187026903600140} + - component: {fileID: 1668215394590156806} + - component: {fileID: 2584217623066612233} + - component: {fileID: 4872086524637674466} + m_Layer: 5 + m_Name: Tooltip + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 0 +--- !u!114 &1060188679371143449 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6315992069626536027} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_text: + m_isRightToLeft: 0 + m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} + m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} + m_fontSharedMaterials: [] + m_fontMaterial: {fileID: 0} + m_fontMaterials: [] + m_fontColor32: + serializedVersion: 2 + rgba: 4278190080 + m_fontColor: {r: 0, g: 0, b: 0, a: 1} + m_enableVertexGradient: 0 + m_colorMode: 3 + m_fontColorGradient: + topLeft: {r: 1, g: 1, b: 1, a: 1} + topRight: {r: 1, g: 1, b: 1, a: 1} + bottomLeft: {r: 1, g: 1, b: 1, a: 1} + bottomRight: {r: 1, g: 1, b: 1, a: 1} + m_fontColorGradientPreset: {fileID: 0} + m_spriteAsset: {fileID: 0} + m_tintAllSprites: 0 + m_StyleSheet: {fileID: 0} + m_TextStyleHashCode: -1183493901 + m_overrideHtmlColors: 0 + m_faceColor: + serializedVersion: 2 + rgba: 4294967295 + m_fontSize: 20 + m_fontSizeBase: 20 + m_fontWeight: 400 + m_enableAutoSizing: 0 + m_fontSizeMin: 18 + m_fontSizeMax: 72 + m_fontStyle: 0 + m_HorizontalAlignment: 2 + m_VerticalAlignment: 512 + m_textAlignment: 65535 + m_characterSpacing: 0 + m_wordSpacing: 0 + m_lineSpacing: 0 + m_lineSpacingMax: 0 + m_paragraphSpacing: 0 + m_charWidthMaxAdj: 0 + m_TextWrappingMode: 1 + m_wordWrappingRatios: 0.4 + m_overflowMode: 0 + m_linkedTextComponent: {fileID: 0} + parentLinkedComponent: {fileID: 0} + m_enableKerning: 1 + m_ActiveFontFeatures: 00000000 + m_enableExtraPadding: 0 + checkPaddingRequired: 0 + m_isRichText: 1 + m_EmojiFallbackSupport: 1 + m_parseCtrlCharacters: 1 + m_isOrthographic: 1 + m_isCullingEnabled: 0 + m_horizontalMapping: 0 + m_verticalMapping: 0 + m_uvLineOffset: 0 + m_geometrySortingOrder: 0 + m_IsTextObjectScaleStatic: 0 + m_VertexBufferAutoSizeReduction: 0 + m_useMaxVisibleDescender: 1 + m_pageToDisplay: 1 + m_margin: {x: 2.6341553, y: 0, z: -23.213348, w: 0} + m_isUsingLegacyAnimationComponent: 0 + m_isVolumetricText: 0 + m_hasFontAssetChanged: 0 + m_baseMaterial: {fileID: 0} + m_maskOffset: {x: 0, y: 0, z: 0, w: 0} +--- !u!222 &1668215394590156806 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1018654042979147675} + m_CullTransparentMesh: 1 +--- !u!224 &1840187026903600140 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1018654042979147675} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 3195220051027805464} + m_Father: {fileID: 5353811237432420247} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 111, y: -80.25} + m_SizeDelta: {x: 179.19391, y: 31.594498} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &1870516355556978866 stripped +GameObject: + m_CorrespondingSourceObject: {fileID: 8744682661361095537, guid: 4efdc47942ba3ed4388eb90053f5988b, type: 3} + m_PrefabInstance: {fileID: 7716761825581739910} + m_PrefabAsset: {fileID: 0} +--- !u!224 &1988647508222235451 stripped +RectTransform: + m_CorrespondingSourceObject: {fileID: 7765979136907116940, guid: e1752a319f294da48aa4572fd5f4faf7, type: 3} + m_PrefabInstance: {fileID: 6089151490756196357} + m_PrefabAsset: {fileID: 0} +--- !u!222 &2061081047798151252 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6315992069626536027} + m_CullTransparentMesh: 1 +--- !u!224 &2165298448928398224 stripped +RectTransform: + m_CorrespondingSourceObject: {fileID: 2298980698609654716, guid: e90a727d63cb3e64cb131b7d39297a0f, type: 3} + m_PrefabInstance: {fileID: 2698370743161617208} + m_PrefabAsset: {fileID: 0} +--- !u!1001 &2249156467525330175 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + serializedVersion: 3 + m_TransformParent: {fileID: 1898133185} + m_Modifications: + - target: {fileID: 1742075923792624124, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchorMax.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 1742075923792624124, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchorMin.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 1742075923792624124, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchoredPosition.x + value: 170.75 + objectReference: {fileID: 0} + - target: {fileID: 1742075923792624124, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchoredPosition.y + value: -474.7727 + objectReference: {fileID: 0} + - target: {fileID: 2328989087641193109, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_Pivot.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2328989087641193109, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_Pivot.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2328989087641193109, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchorMax.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2328989087641193109, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchorMax.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2328989087641193109, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchorMin.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2328989087641193109, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchorMin.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2328989087641193109, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_SizeDelta.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2328989087641193109, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_SizeDelta.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2328989087641193109, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_LocalPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2328989087641193109, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2328989087641193109, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_LocalPosition.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2328989087641193109, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 2328989087641193109, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_LocalRotation.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2328989087641193109, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_LocalRotation.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2328989087641193109, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_LocalRotation.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2328989087641193109, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchoredPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2328989087641193109, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchoredPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2328989087641193109, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2328989087641193109, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2328989087641193109, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2702427302246568075, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchorMax.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 2702427302246568075, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchorMin.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 2702427302246568075, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchoredPosition.x + value: 170.75 + objectReference: {fileID: 0} + - target: {fileID: 2702427302246568075, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchoredPosition.y + value: -849.5908 + objectReference: {fileID: 0} + - target: {fileID: 3084871783138012815, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchorMax.x + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 3084871783138012815, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchorMax.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 3487255676057137607, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchorMax.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 3487255676057137607, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_SizeDelta.y + value: -17 + objectReference: {fileID: 0} + - target: {fileID: 3771538046396679732, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchorMax.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 3771538046396679732, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchorMin.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 3771538046396679732, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchoredPosition.x + value: 170.75 + objectReference: {fileID: 0} + - target: {fileID: 3771538046396679732, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchoredPosition.y + value: -549.72723 + objectReference: {fileID: 0} + - target: {fileID: 4127085878270801471, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: desktopMode + value: + objectReference: {fileID: 0} + - target: {fileID: 4127085878270801471, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: nativeSettings + value: + objectReference: {fileID: 472491408} + - target: {fileID: 4966645532661389859, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchorMax.x + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 4966645532661389859, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchorMax.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 4966645532661389859, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchorMin.y + value: 0.77110785 + objectReference: {fileID: 0} + - target: {fileID: 5086805301692129715, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchorMax.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 5086805301692129715, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchorMin.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 5086805301692129715, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchoredPosition.x + value: 170.75 + objectReference: {fileID: 0} + - target: {fileID: 5086805301692129715, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchoredPosition.y + value: -224.90909 + objectReference: {fileID: 0} + - target: {fileID: 5184664611584485851, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchorMax.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 5184664611584485851, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchorMin.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 5184664611584485851, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchoredPosition.x + value: 170.75 + objectReference: {fileID: 0} + - target: {fileID: 5184664611584485851, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchoredPosition.y + value: -25 + objectReference: {fileID: 0} + - target: {fileID: 5825053534868217129, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchorMax.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 5825053534868217129, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchorMin.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 5825053534868217129, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchoredPosition.x + value: 170.75 + objectReference: {fileID: 0} + - target: {fileID: 5825053534868217129, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchoredPosition.y + value: -399.81818 + objectReference: {fileID: 0} + - target: {fileID: 6398496623086018798, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchorMax.x + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 6398496623086018798, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchorMax.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 6398496623086018798, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_SizeDelta.x + value: -17 + objectReference: {fileID: 0} + - target: {fileID: 6398496623086018798, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_SizeDelta.y + value: -17 + objectReference: {fileID: 0} + - target: {fileID: 6762958295356658006, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchorMax.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 6762958295356658006, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchorMin.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 6762958295356658006, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchoredPosition.x + value: 170.75 + objectReference: {fileID: 0} + - target: {fileID: 6762958295356658006, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchoredPosition.y + value: -949.54535 + objectReference: {fileID: 0} + - target: {fileID: 7171043448034504270, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchorMax.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 7171043448034504270, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchorMin.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 7171043448034504270, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchoredPosition.x + value: 170.75 + objectReference: {fileID: 0} + - target: {fileID: 7171043448034504270, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchoredPosition.y + value: -324.86365 + objectReference: {fileID: 0} + - target: {fileID: 8084162891842064009, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchorMax.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 8084162891842064009, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchorMin.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 8084162891842064009, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchoredPosition.x + value: 170.75 + objectReference: {fileID: 0} + - target: {fileID: 8084162891842064009, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchoredPosition.y + value: -749.6363 + objectReference: {fileID: 0} + - target: {fileID: 8112418018014962849, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchorMax.x + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 8112418018014962849, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_SizeDelta.x + value: -17 + objectReference: {fileID: 0} + - target: {fileID: 8822790655069677569, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_Name + value: Settings-Mobile + objectReference: {fileID: 0} + - target: {fileID: 8822790655069677569, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_IsActive + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 8858450273282345298, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchorMax.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 8858450273282345298, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchorMin.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 8858450273282345298, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchoredPosition.x + value: 170.75 + objectReference: {fileID: 0} + - target: {fileID: 8858450273282345298, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchoredPosition.y + value: -124.954544 + objectReference: {fileID: 0} + - target: {fileID: 9190234847787547429, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchorMax.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 9190234847787547429, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchorMin.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 9190234847787547429, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchoredPosition.x + value: 170.75 + objectReference: {fileID: 0} + - target: {fileID: 9190234847787547429, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} + propertyPath: m_AnchoredPosition.y + value: -649.68176 + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: [] + m_AddedComponents: [] + m_SourcePrefab: {fileID: 100100000, guid: 976f4ee8fbeb3ea499c5717ac9c8acf4, type: 3} +--- !u!114 &2584217623066612233 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1018654042979147675} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.7019608} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10907, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1001 &2698370743161617208 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + serializedVersion: 3 + m_TransformParent: {fileID: 1898133185} + m_Modifications: + - target: {fileID: 2298980698609654716, guid: e90a727d63cb3e64cb131b7d39297a0f, type: 3} + propertyPath: m_Pivot.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2298980698609654716, guid: e90a727d63cb3e64cb131b7d39297a0f, type: 3} + propertyPath: m_Pivot.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2298980698609654716, guid: e90a727d63cb3e64cb131b7d39297a0f, type: 3} + propertyPath: m_AnchorMax.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2298980698609654716, guid: e90a727d63cb3e64cb131b7d39297a0f, type: 3} + propertyPath: m_AnchorMax.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2298980698609654716, guid: e90a727d63cb3e64cb131b7d39297a0f, type: 3} + propertyPath: m_AnchorMin.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2298980698609654716, guid: e90a727d63cb3e64cb131b7d39297a0f, type: 3} + propertyPath: m_AnchorMin.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2298980698609654716, guid: e90a727d63cb3e64cb131b7d39297a0f, type: 3} + propertyPath: m_SizeDelta.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2298980698609654716, guid: e90a727d63cb3e64cb131b7d39297a0f, type: 3} + propertyPath: m_SizeDelta.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2298980698609654716, guid: e90a727d63cb3e64cb131b7d39297a0f, type: 3} + propertyPath: m_LocalPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2298980698609654716, guid: e90a727d63cb3e64cb131b7d39297a0f, type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2298980698609654716, guid: e90a727d63cb3e64cb131b7d39297a0f, type: 3} + propertyPath: m_LocalPosition.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2298980698609654716, guid: e90a727d63cb3e64cb131b7d39297a0f, type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 2298980698609654716, guid: e90a727d63cb3e64cb131b7d39297a0f, type: 3} + propertyPath: m_LocalRotation.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2298980698609654716, guid: e90a727d63cb3e64cb131b7d39297a0f, type: 3} + propertyPath: m_LocalRotation.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2298980698609654716, guid: e90a727d63cb3e64cb131b7d39297a0f, type: 3} + propertyPath: m_LocalRotation.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2298980698609654716, guid: e90a727d63cb3e64cb131b7d39297a0f, type: 3} + propertyPath: m_AnchoredPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2298980698609654716, guid: e90a727d63cb3e64cb131b7d39297a0f, type: 3} + propertyPath: m_AnchoredPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2298980698609654716, guid: e90a727d63cb3e64cb131b7d39297a0f, type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2298980698609654716, guid: e90a727d63cb3e64cb131b7d39297a0f, type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2298980698609654716, guid: e90a727d63cb3e64cb131b7d39297a0f, type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3387419331718106927, guid: e90a727d63cb3e64cb131b7d39297a0f, type: 3} + propertyPath: m_AnchorMax.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 3387419331718106927, guid: e90a727d63cb3e64cb131b7d39297a0f, type: 3} + propertyPath: m_AnchorMin.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 3387419331718106927, guid: e90a727d63cb3e64cb131b7d39297a0f, type: 3} + propertyPath: m_AnchoredPosition.x + value: 254.25 + objectReference: {fileID: 0} + - target: {fileID: 4373880454811058208, guid: e90a727d63cb3e64cb131b7d39297a0f, type: 3} + propertyPath: m_AnchorMax.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 4373880454811058208, guid: e90a727d63cb3e64cb131b7d39297a0f, type: 3} + propertyPath: m_AnchorMin.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 4373880454811058208, guid: e90a727d63cb3e64cb131b7d39297a0f, type: 3} + propertyPath: m_AnchoredPosition.x + value: 184.25 + objectReference: {fileID: 0} + - target: {fileID: 4373880454811058208, guid: e90a727d63cb3e64cb131b7d39297a0f, type: 3} + propertyPath: m_AnchoredPosition.y + value: -76.87501 + objectReference: {fileID: 0} + - target: {fileID: 5531828587333347769, guid: e90a727d63cb3e64cb131b7d39297a0f, type: 3} + propertyPath: m_AnchorMax.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 5531828587333347769, guid: e90a727d63cb3e64cb131b7d39297a0f, type: 3} + propertyPath: m_AnchorMin.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 5531828587333347769, guid: e90a727d63cb3e64cb131b7d39297a0f, type: 3} + propertyPath: m_AnchoredPosition.x + value: 184.25 + objectReference: {fileID: 0} + - target: {fileID: 5531828587333347769, guid: e90a727d63cb3e64cb131b7d39297a0f, type: 3} + propertyPath: m_AnchoredPosition.y + value: -180.62503 + objectReference: {fileID: 0} + - target: {fileID: 8438332059091148627, guid: e90a727d63cb3e64cb131b7d39297a0f, type: 3} + propertyPath: m_Name + value: ExitMenu-Mobile + objectReference: {fileID: 0} + - target: {fileID: 8438332059091148627, guid: e90a727d63cb3e64cb131b7d39297a0f, type: 3} + propertyPath: m_IsActive + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 8517978974965674228, guid: e90a727d63cb3e64cb131b7d39297a0f, type: 3} + propertyPath: m_AnchorMax.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 8517978974965674228, guid: e90a727d63cb3e64cb131b7d39297a0f, type: 3} + propertyPath: m_AnchorMin.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 8517978974965674228, guid: e90a727d63cb3e64cb131b7d39297a0f, type: 3} + propertyPath: m_AnchoredPosition.x + value: 84.75 + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: [] + m_AddedComponents: [] + m_SourcePrefab: {fileID: 100100000, guid: e90a727d63cb3e64cb131b7d39297a0f, type: 3} +--- !u!224 &3195220051027805464 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6315992069626536027} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} @@ -2855,3 +3715,4 @@ SceneRoots: - {fileID: 839982477} - {fileID: 1137534127} - {fileID: 167713676} + - {fileID: 45003657} diff --git a/Assets/XR/Settings/OpenXR Package Settings.asset b/Assets/XR/Settings/OpenXR Package Settings.asset index c84999bb..8a930c6f 100644 --- a/Assets/XR/Settings/OpenXR Package Settings.asset +++ b/Assets/XR/Settings/OpenXR Package Settings.asset @@ -33,6 +33,7 @@ MonoBehaviour: m_Name: MetaXRFeature Android m_EditorClassIdentifier: m_enabled: 1 + k__BackingField: 0 nameUi: Meta XR Feature version: 0.0.1 featureIdInternal: com.meta.openxr.feature.metaxr @@ -82,7 +83,6 @@ MonoBehaviour: m_Name: Standalone m_EditorClassIdentifier: features: - - {fileID: -7441576640521792224} - {fileID: -4251221511889710573} - {fileID: -8283934257360705991} - {fileID: -3043467695985465025} @@ -95,20 +95,6 @@ MonoBehaviour: - {fileID: -4233259048951607741} - {fileID: -2467792937570659600} - {fileID: -3907765551054486986} - - {fileID: -7296088344262875955} - - {fileID: 6770610698521954227} - - {fileID: -6477775478286594775} - - {fileID: -7222906391444432655} - - {fileID: 6575429890421189379} - - {fileID: 5913630344976663486} - - {fileID: -7313330577548468968} - - {fileID: 1158942647253497871} - - {fileID: 8492585916147005112} - - {fileID: 7295433065535495109} - - {fileID: 714606220454857056} - - {fileID: -3744789857744238503} - - {fileID: 1690885375729658985} - - {fileID: 9123467854060196379} - {fileID: -6322854048430258810} - {fileID: -2804446222640820088} - {fileID: 7243270749764318523} @@ -238,6 +224,7 @@ MonoBehaviour: m_Name: OpenXRLifeCycleFeature Android m_EditorClassIdentifier: m_enabled: 1 + k__BackingField: 0 nameUi: version: 0.1.0 featureIdInternal: MetaOpenXR-OpenXRLifeCycle @@ -278,6 +265,7 @@ MonoBehaviour: m_Name: OpenXRLifeCycleFeature Standalone m_EditorClassIdentifier: m_enabled: 0 + k__BackingField: 0 nameUi: version: 0.1.0 featureIdInternal: MetaOpenXR-OpenXRLifeCycle @@ -298,6 +286,7 @@ MonoBehaviour: m_Name: AROcclusionFeature Standalone m_EditorClassIdentifier: m_enabled: 0 + k__BackingField: 0 nameUi: 'Meta Quest: Occlusion' version: 0.1.0 featureIdInternal: com.unity.openxr.feature.arfoundation-meta-occlusion @@ -339,6 +328,7 @@ MonoBehaviour: m_Name: ARAnchorFeature Standalone m_EditorClassIdentifier: m_enabled: 0 + k__BackingField: 0 nameUi: 'Meta Quest: Anchors' version: 0.1.0 featureIdInternal: com.unity.openxr.feature.arfoundation-meta-anchor @@ -359,6 +349,7 @@ MonoBehaviour: m_Name: ARCameraFeature Standalone m_EditorClassIdentifier: m_enabled: 0 + k__BackingField: 0 nameUi: 'Meta Quest: Camera (Passthrough)' version: 0.1.0 featureIdInternal: com.unity.openxr.feature.arfoundation-meta-camera @@ -379,6 +370,7 @@ MonoBehaviour: m_Name: ARBoundingBoxFeature Standalone m_EditorClassIdentifier: m_enabled: 0 + k__BackingField: 0 nameUi: 'Meta Quest: Bounding Boxes' version: 0.1.0 featureIdInternal: com.unity.openxr.feature.arfoundation-meta-bounding-boxes @@ -523,6 +515,7 @@ MonoBehaviour: m_Name: BoundaryVisibilityFeature Android m_EditorClassIdentifier: m_enabled: 1 + k__BackingField: 0 nameUi: 'Meta Quest: Boundary Visibility' version: 0.1.0 featureIdInternal: com.unity.openxr.feature.meta-boundary-visibility @@ -530,6 +523,7 @@ MonoBehaviour: company: Unity Technologies priority: 0 required: 0 + k__BackingField: 0 --- !u!114 &-5415651528169704530 MonoBehaviour: m_ObjectHideFlags: 0 @@ -565,6 +559,7 @@ MonoBehaviour: m_Name: ARRaycastFeature Android m_EditorClassIdentifier: m_enabled: 1 + k__BackingField: 0 nameUi: 'Meta Quest: Raycasts' version: 0.1.0 featureIdInternal: com.unity.openxr.feature.arfoundation-meta-raycast @@ -585,6 +580,7 @@ MonoBehaviour: m_Name: DisplayUtilitiesFeature Android m_EditorClassIdentifier: m_enabled: 1 + k__BackingField: 0 nameUi: 'Meta Quest: Display Utilities' version: 0.1.0 featureIdInternal: com.unity.openxr.feature.meta-display-utilities @@ -625,6 +621,7 @@ MonoBehaviour: m_Name: MetaXREyeTrackedFoveationFeature Android m_EditorClassIdentifier: m_enabled: 0 + k__BackingField: 0 nameUi: Meta XR Eye Tracked Foveation version: 0.0.1 featureIdInternal: com.meta.openxr.feature.eyetrackedfoveation @@ -646,6 +643,7 @@ MonoBehaviour: m_Name: ARSessionFeature Android m_EditorClassIdentifier: m_enabled: 1 + k__BackingField: 0 nameUi: 'Meta Quest: Session' version: 0.1.0 featureIdInternal: com.unity.openxr.feature.arfoundation-meta-session @@ -726,6 +724,7 @@ MonoBehaviour: m_Name: ARCameraFeature Android m_EditorClassIdentifier: m_enabled: 1 + k__BackingField: 0 nameUi: 'Meta Quest: Camera (Passthrough)' version: 0.1.0 featureIdInternal: com.unity.openxr.feature.arfoundation-meta-camera @@ -766,6 +765,7 @@ MonoBehaviour: m_Name: MetaXRFeature Standalone m_EditorClassIdentifier: m_enabled: 1 + k__BackingField: 0 nameUi: Meta XR Feature version: 0.0.1 featureIdInternal: com.meta.openxr.feature.metaxr @@ -978,6 +978,7 @@ MonoBehaviour: m_Name: MetaXRSubsampledLayout Android m_EditorClassIdentifier: m_enabled: 0 + k__BackingField: 0 nameUi: Meta XR Subsampled Layout version: 0.0.1 featureIdInternal: com.meta.openxr.feature.subsampledLayout @@ -1018,6 +1019,7 @@ MonoBehaviour: m_Name: ARBoundingBoxFeature Android m_EditorClassIdentifier: m_enabled: 1 + k__BackingField: 0 nameUi: 'Meta Quest: Bounding Boxes' version: 0.1.0 featureIdInternal: com.unity.openxr.feature.arfoundation-meta-bounding-boxes @@ -1095,6 +1097,7 @@ MonoBehaviour: m_Name: MetaXREyeTrackedFoveationFeature Standalone m_EditorClassIdentifier: m_enabled: 0 + k__BackingField: 0 nameUi: Meta XR Eye Tracked Foveation version: 0.0.1 featureIdInternal: com.meta.openxr.feature.eyetrackedfoveation @@ -1116,6 +1119,7 @@ MonoBehaviour: m_Name: ARAnchorFeature Android m_EditorClassIdentifier: m_enabled: 1 + k__BackingField: 0 nameUi: 'Meta Quest: Anchors' version: 0.1.0 featureIdInternal: com.unity.openxr.feature.arfoundation-meta-anchor @@ -1136,6 +1140,7 @@ MonoBehaviour: m_Name: ARPlaneFeature Standalone m_EditorClassIdentifier: m_enabled: 0 + k__BackingField: 0 nameUi: 'Meta Quest: Planes' version: 0.1.0 featureIdInternal: com.unity.openxr.feature.arfoundation-meta-plane @@ -1156,6 +1161,7 @@ MonoBehaviour: m_Name: MetaXRFoveationFeature Standalone m_EditorClassIdentifier: m_enabled: 1 + k__BackingField: 0 nameUi: Meta XR Foveation version: 1.0.0 featureIdInternal: com.meta.openxr.feature.foveation @@ -1176,6 +1182,7 @@ MonoBehaviour: m_Name: ARMeshFeature Android m_EditorClassIdentifier: m_enabled: 1 + k__BackingField: 0 nameUi: 'Meta Quest: Meshing' version: 0.1.0 featureIdInternal: com.unity.openxr.feature.arfoundation-meta-mesh @@ -1344,6 +1351,7 @@ MonoBehaviour: m_Name: AROcclusionFeature Android m_EditorClassIdentifier: m_enabled: 1 + k__BackingField: 0 nameUi: 'Meta Quest: Occlusion' version: 0.1.0 featureIdInternal: com.unity.openxr.feature.arfoundation-meta-occlusion @@ -1405,6 +1413,7 @@ MonoBehaviour: m_Name: ARPlaneFeature Android m_EditorClassIdentifier: m_enabled: 1 + k__BackingField: 0 nameUi: 'Meta Quest: Planes' version: 0.1.0 featureIdInternal: com.unity.openxr.feature.arfoundation-meta-plane @@ -1425,6 +1434,7 @@ MonoBehaviour: m_Name: MetaXRFoveationFeature Android m_EditorClassIdentifier: m_enabled: 0 + k__BackingField: 0 nameUi: Meta XR Foveation version: 1.0.0 featureIdInternal: com.meta.openxr.feature.foveation @@ -1445,6 +1455,7 @@ MonoBehaviour: m_Name: ARMeshFeature Standalone m_EditorClassIdentifier: m_enabled: 0 + k__BackingField: 0 nameUi: 'Meta Quest: Meshing' version: 0.1.0 featureIdInternal: com.unity.openxr.feature.arfoundation-meta-mesh @@ -1466,6 +1477,7 @@ MonoBehaviour: m_Name: DisplayUtilitiesFeature Standalone m_EditorClassIdentifier: m_enabled: 0 + k__BackingField: 0 nameUi: 'Meta Quest: Display Utilities' version: 0.1.0 featureIdInternal: com.unity.openxr.feature.meta-display-utilities @@ -1486,6 +1498,7 @@ MonoBehaviour: m_Name: BoundaryVisibilityFeature Standalone m_EditorClassIdentifier: m_enabled: 0 + k__BackingField: 0 nameUi: 'Meta Quest: Boundary Visibility' version: 0.1.0 featureIdInternal: com.unity.openxr.feature.meta-boundary-visibility @@ -1493,6 +1506,7 @@ MonoBehaviour: company: Unity Technologies priority: 0 required: 0 + k__BackingField: 0 --- !u!114 &7146165264086680457 MonoBehaviour: m_ObjectHideFlags: 0 @@ -1560,6 +1574,7 @@ MonoBehaviour: m_Name: ARSessionFeature Standalone m_EditorClassIdentifier: m_enabled: 0 + k__BackingField: 0 nameUi: 'Meta Quest: Session' version: 0.1.0 featureIdInternal: com.unity.openxr.feature.arfoundation-meta-session @@ -1600,7 +1615,6 @@ MonoBehaviour: m_Name: Android m_EditorClassIdentifier: features: - - {fileID: -7633581328313503459} - {fileID: 4674697878492794949} - {fileID: 7146165264086680457} - {fileID: -2340439152450337852} @@ -1612,21 +1626,6 @@ MonoBehaviour: - {fileID: 4436760230863042719} - {fileID: -5909640409290506278} - {fileID: -4594511983156627525} - - {fileID: 1085581369992130134} - - {fileID: -5643199017387485149} - - {fileID: -803836931037024053} - - {fileID: -4136287910980941023} - - {fileID: -4844709515900620156} - - {fileID: 2486025960671627241} - - {fileID: 4588223029664240611} - - {fileID: 5230357031813526908} - - {fileID: -5273211353462809153} - - {fileID: -4389619210768067104} - - {fileID: -4539300325892458710} - - {fileID: -8917394965726186233} - - {fileID: 5562653971502655839} - - {fileID: 8005351155306599530} - - {fileID: -1428233723045200180} - {fileID: 8316444451361848473} - {fileID: -2379692420766146042} - {fileID: -2367121910693502146} @@ -1658,6 +1657,7 @@ MonoBehaviour: m_Name: MetaXRSpaceWarp Android m_EditorClassIdentifier: m_enabled: 0 + k__BackingField: 0 nameUi: Meta XR Space Warp version: 1.0.0 featureIdInternal: com.meta.openxr.feature.spacewarp @@ -1698,6 +1698,7 @@ MonoBehaviour: m_Name: ARRaycastFeature Standalone m_EditorClassIdentifier: m_enabled: 0 + k__BackingField: 0 nameUi: 'Meta Quest: Raycasts' version: 0.1.0 featureIdInternal: com.unity.openxr.feature.arfoundation-meta-raycast @@ -1718,6 +1719,7 @@ MonoBehaviour: m_Name: MetaXRSubsampledLayout Standalone m_EditorClassIdentifier: m_enabled: 0 + k__BackingField: 0 nameUi: Meta XR Subsampled Layout version: 0.0.1 featureIdInternal: com.meta.openxr.feature.subsampledLayout