diff --git a/Assets/StraightFour/Testing/WorldTests/WorldTests.cs b/Assets/StraightFour/Testing/WorldTests/WorldTests.cs index 19f3198..d0ee8a1 100644 --- a/Assets/StraightFour/Testing/WorldTests/WorldTests.cs +++ b/Assets/StraightFour/Testing/WorldTests/WorldTests.cs @@ -55,4 +55,93 @@ public IEnumerator WorldTests_World() Assert.IsTrue(world.cameraManager == null); Assert.IsTrue(world.materialManager == null); } + + [UnityTest] + public IEnumerator WorldTests_WorldOffsetUpdate() + { + // Initialize World Engine and Load World. + GameObject WEGO = new GameObject(); + StraightFour we = WEGO.AddComponent(); + we.skyMaterial = AssetDatabase.LoadAssetAtPath("Assets/StraightFour/Environment/Materials/Skybox.mat"); + we.liteProceduralSkyMaterial = AssetDatabase.LoadAssetAtPath("Assets/StraightFour/Environment/Materials/LiteProceduralSkybox.mat"); + yield return null; + StraightFour.LoadWorld("test"); + + // Create a character entity + GameObject characterGO = new GameObject("TestCharacter"); + CharacterEntity character = characterGO.AddComponent(); + character.Initialize(System.Guid.NewGuid(), null, Vector3.zero, Quaternion.identity, Vector3.zero); + character.SetInteractionState(FiveSQD.StraightFour.Entity.BaseEntity.InteractionState.Physical); + + // Set the tracked character + StraightFour.ActiveWorld.SetTrackedCharacterEntity(character); + StraightFour.ActiveWorld.enableAutoWorldOffsetUpdate = true; + StraightFour.ActiveWorld.worldOffsetUpdateThreshold = 100f; + + // Verify initial offset is zero + Assert.AreEqual(Vector3.zero, StraightFour.ActiveWorld.worldOffset); + + // Move character beyond threshold (logical position) + character.SetPosition(new Vector3(150, 0, 0), false, false); + + // Unity position should be (150, 0, 0) with offset (0, 0, 0) + Assert.AreEqual(new Vector3(150, 0, 0), character.transform.position); + + // Wait a frame for Update to run + yield return null; + + // Verify world offset was updated to recenter Unity position at origin + // New offset should be (0, 0, 0) - (150, 0, 0) = (-150, 0, 0) + Vector3 expectedOffset = new Vector3(-150, 0, 0); + Assert.AreEqual(expectedOffset, StraightFour.ActiveWorld.worldOffset); + + // Character should now be at Unity origin while maintaining logical position + Assert.AreEqual(Vector3.zero, character.transform.position); + Assert.AreEqual(new Vector3(150, 0, 0), character.GetPosition(false)); + + // Move character to logical (160, 0, 10) - still close to Unity origin + character.SetPosition(new Vector3(160, 0, 10), false, false); + + // Unity position should be (160, 0, 10) + (-150, 0, 0) = (10, 0, 10) + Assert.AreEqual(new Vector3(10, 0, 10), character.transform.position); + + // Wait a frame for Update to run + yield return null; + + // Offset should remain the same since Unity distance from origin is small + Assert.AreEqual(expectedOffset, StraightFour.ActiveWorld.worldOffset); + + // Move character far again to logical (300, 0, 0) + character.SetPosition(new Vector3(300, 0, 0), false, false); + + // Unity position should be (300, 0, 0) + (-150, 0, 0) = (150, 0, 0) + Assert.AreEqual(new Vector3(150, 0, 0), character.transform.position); + + // Wait a frame for Update to run + yield return null; + + // Verify offset updated again: new offset = (-150, 0, 0) - (150, 0, 0) = (-300, 0, 0) + expectedOffset = new Vector3(-300, 0, 0); + Assert.AreEqual(expectedOffset, StraightFour.ActiveWorld.worldOffset); + + // Character should be back at Unity origin + Assert.AreEqual(Vector3.zero, character.transform.position); + Assert.AreEqual(new Vector3(300, 0, 0), character.GetPosition(false)); + + // Test disabling auto-update + StraightFour.ActiveWorld.enableAutoWorldOffsetUpdate = false; + character.SetPosition(new Vector3(500, 0, 0), false, false); + + // Unity position should be (500, 0, 0) + (-300, 0, 0) = (200, 0, 0) + Assert.AreEqual(new Vector3(200, 0, 0), character.transform.position); + + // Wait a frame for Update to run + yield return null; + + // Offset should NOT have changed + Assert.AreEqual(new Vector3(-300, 0, 0), StraightFour.ActiveWorld.worldOffset); + + // Character should still be at Unity (200, 0, 0) + Assert.AreEqual(new Vector3(200, 0, 0), character.transform.position); + } } \ No newline at end of file diff --git a/Assets/StraightFour/World.cs b/Assets/StraightFour/World.cs index 9088901..1191518 100644 --- a/Assets/StraightFour/World.cs +++ b/Assets/StraightFour/World.cs @@ -223,6 +223,23 @@ public Vector3 worldOffset private Vector3 _worldOffset; + /// + /// The character entity being tracked for world offset updates. + /// + private CharacterEntity trackedCharacterEntity; + + /// + /// The distance threshold from origin that triggers a world offset update. + /// + [Tooltip("The distance threshold from origin that triggers a world offset update.")] + public float worldOffsetUpdateThreshold = 1000f; + + /// + /// Whether or not to enable automatic world offset updates. + /// + [Tooltip("Whether or not to enable automatic world offset updates.")] + public bool enableAutoWorldOffsetUpdate = true; + /// /// The GameObject for the mesh manager. /// @@ -371,6 +388,48 @@ public void Initialize(WorldInfo worldInfo) siteName = worldInfo.siteName; } + /// + /// Set the character entity to track for automatic world offset updates. + /// + /// The character entity to track. + public void SetTrackedCharacterEntity(CharacterEntity entity) + { + trackedCharacterEntity = entity; + } + + /// + /// Update method to check tracked character distance and update world offset if needed. + /// + void Update() + { + if (!enableAutoWorldOffsetUpdate || trackedCharacterEntity == null) + { + return; + } + + // Get the Unity position of the tracked character entity + Vector3 unityPosition = trackedCharacterEntity.transform.position; + + // Calculate distance from Unity origin (0, 0, 0) + float distanceFromOrigin = unityPosition.magnitude; + + // If beyond threshold, update world offset to recenter around character + if (distanceFromOrigin > worldOffsetUpdateThreshold) + { + // Calculate new offset to recenter the Unity origin at the character + // We want Unity position to be (0, 0, 0) while preserving logical position + // Unity = Logical + offset + // Target: Unity' = (0, 0, 0), Logical' = Logical (preserved by setter) + // We have: Logical = Unity - offset + // So: Unity' = Logical + offset' = (Unity - offset) + offset' = (0, 0, 0) + // Therefore: offset' = offset - Unity + Vector3 newOffset = new Vector3(worldOffset.x - unityPosition.x, worldOffset.y, worldOffset.z - unityPosition.z); + + // Update the world offset + worldOffset = newOffset; + } + } + /// /// Unload the world. /// diff --git a/docs/configuration/README.md b/docs/configuration/README.md index f700037..6942287 100644 --- a/docs/configuration/README.md +++ b/docs/configuration/README.md @@ -230,6 +230,58 @@ WorldInfo worldInfo = new WorldInfo bool success = StraightFour.LoadWorld("MyWorld", worldInfo); ``` +### World Offset Auto-Update + +The World class supports automatic world offset updates to maintain floating-point precision as characters move far from the Unity origin. This feature helps prevent precision-related issues and ensures smooth gameplay regardless of how far entities travel in the world. + +#### Configuration + +```csharp +// Enable automatic world offset updates +StraightFour.ActiveWorld.enableAutoWorldOffsetUpdate = true; + +// Set the distance threshold (in Unity units) that triggers an offset update +StraightFour.ActiveWorld.worldOffsetUpdateThreshold = 1000f; + +// Register a character entity to track +CharacterEntity playerCharacter = // ... your player character +StraightFour.ActiveWorld.SetTrackedCharacterEntity(playerCharacter); +``` + +#### How It Works + +1. **Distance Monitoring**: The World's Update() method continuously monitors the tracked character's Unity position (transform.position) +2. **Threshold Check**: When the character's distance from the Unity origin (0, 0, 0) exceeds the threshold, an update is triggered +3. **Offset Update**: The world offset is adjusted to recenter the Unity coordinate system at the character's location +4. **Position Preservation**: All entities' logical (world) positions are preserved while their Unity positions are shifted to stay near the origin + +**Example:** +- Character moves to Unity position (1500, 0, 0) with current offset (0, 0, 0) +- Distance 1500 exceeds threshold of 1000, triggering an update +- New offset is set to (-1500, 0, 0) +- Character's Unity position becomes (0, 0, 0) while maintaining logical position (1500, 0, 0) + +#### Benefits + +- **Improved Precision**: Keeps Unity transform positions close to origin, avoiding floating-point precision loss +- **Seamless Experience**: Characters can travel unlimited distances without precision degradation +- **Automatic**: No manual intervention needed once configured +- **Configurable**: Threshold can be adjusted based on world scale and precision requirements + +#### Default Values + +```csharp +public float worldOffsetUpdateThreshold = 1000f; // Unity units +public bool enableAutoWorldOffsetUpdate = true; // Enabled by default +``` + +#### Notes + +- Only horizontal (X, Z) axes are recentered; Y-axis offset remains unchanged +- The feature can be disabled by setting `enableAutoWorldOffsetUpdate = false` +- The tracked character entity can be changed at runtime using `SetTrackedCharacterEntity()` +- If no character is tracked, automatic updates will not occur + ## Unity Project Configuration ### Package Dependencies