diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs index 4cf6d8f..d7134ed 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs @@ -1,4 +1,5 @@ using UnityEngine; +using UnityEngine.Rendering; #nullable enable @@ -46,6 +47,7 @@ namespace GameCult.Eve.UnityScene return false; if (!string.IsNullOrWhiteSpace(activeWorld.CameraRig) && + activeWorld.CameraRig != "planar.top-down-follow.v1" && activeWorld.CameraRig != "arpg.orbital-follow.v1" && activeWorld.CameraRig != "third-person-orbit") { @@ -53,7 +55,10 @@ namespace GameCult.Eve.UnityScene } var camera = cameraTransform != null ? cameraTransform : transform; - var player = FindPlayer(resolvedHost, activeWorld.PlayerEntityId); + var targetEntityId = string.IsNullOrWhiteSpace(activeWorld.CameraTargetEntityId) + ? activeWorld.PlayerEntityId + : activeWorld.CameraTargetEntityId; + var player = FindEntity(resolvedHost, targetEntityId); if (player == null) return false; @@ -69,10 +74,12 @@ namespace GameCult.Eve.UnityScene } cameraComponent.cullingMask = cullingMask; } + if (activeWorld.CameraRig == "planar.top-down-follow.v1") + return ApplyPlanarTopDown(activeWorld, camera, cameraComponent, player.position, deltaTime); + var bounds = CalculateVisualBounds( player, - cameraComponent != null ? cameraComponent.cullingMask : -1, - FindSelectedTarget(resolvedHost)); + cameraComponent != null ? cameraComponent.cullingMask : -1); var target = bounds?.center ?? player.position; var radius = bounds?.extents.magnitude ?? 0f; var verticalFov = cameraComponent != null ? cameraComponent.fieldOfView : 60f; @@ -92,12 +99,45 @@ namespace GameCult.Eve.UnityScene return true; } - private static Bounds? CalculateVisualBounds(Transform player, int cullingMask, Transform? selectedTarget) + private static bool ApplyPlanarTopDown( + EveUnityPlayableWorldProjection world, + Transform camera, + Camera? cameraComponent, + Vector3 target, + float deltaTime) + { + var resolvedDistance = Mathf.Max(0.1f, world.CameraDistance); + var verticalFov = Mathf.Clamp(world.CameraVerticalFieldOfViewDegrees, 1f, 179f); + if (cameraComponent != null) + cameraComponent.fieldOfView = verticalFov; + var aspect = cameraComponent != null ? cameraComponent.aspect : 16f / 9f; + var verticalHalfExtent = resolvedDistance * Mathf.Tan(verticalFov * 0.5f * Mathf.Deg2Rad); + var horizontalHalfExtent = verticalHalfExtent * Mathf.Max(0.01f, aspect); + var screenOffset = new Vector3( + (Mathf.Clamp01(world.CameraTargetScreenX) - 0.5f) * 2f * horizontalHalfExtent, + 0f, + (Mathf.Clamp01(world.CameraTargetScreenY) - 0.5f) * 2f * verticalHalfExtent); + var desiredPosition = target + (Vector3.up * resolvedDistance) - screenOffset; + var damping = Mathf.Max(0f, world.CameraPositionDamping); + var t = deltaTime <= 0f || damping <= 0f + ? 1f + : 1f - Mathf.Exp(-damping * deltaTime); + camera.position = Vector3.Lerp(camera.position, desiredPosition, t); + camera.rotation = Quaternion.LookRotation(Vector3.down, Vector3.forward); + + RenderSettings.ambientMode = AmbientMode.Flat; + RenderSettings.ambientLight = new Color( + Mathf.Max(0f, world.AmbientLightR * world.AmbientLightIntensity), + Mathf.Max(0f, world.AmbientLightG * world.AmbientLightIntensity), + Mathf.Max(0f, world.AmbientLightB * world.AmbientLightIntensity), + 1f); + return true; + } + + private static Bounds? CalculateVisualBounds(Transform player, int cullingMask) { Bounds? bounds = null; - foreach (var root in selectedTarget == null - ? new[] { player } - : new[] { player, selectedTarget! }) + foreach (var root in new[] { player }) { foreach (var visual in root.GetComponentsInChildren(includeInactive: false)) { @@ -122,17 +162,6 @@ namespace GameCult.Eve.UnityScene return bounds; } - private static Transform? FindSelectedTarget(EveUnityPlayableWorldClientHost host) - { - var combat = EveUnityCombatPresentation.Find(host.ActiveProjection); - if (combat == null || string.IsNullOrWhiteSpace(combat.SelectedTargetEntityId)) - return null; - if (host.PresentedEntities?.CurrentGeneration != null && - host.PresentedEntities.TryGetByEntityId(combat.SelectedTargetEntityId, out var presented)) - return presented.Transform; - return null; - } - private void LateUpdate() { if (driveInLateUpdate) @@ -148,7 +177,7 @@ namespace GameCult.Eve.UnityScene return host; } - private static Transform? FindPlayer( + private static Transform? FindEntity( EveUnityPlayableWorldClientHost host, string playerEntityId) { diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs index c6d5c88..5dd3f2e 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs @@ -126,7 +126,15 @@ namespace GameCult.Eve.UnityScene worldRoot.GetProp("entityViewSchema"), worldRoot.GetProp("zoneRenderPointerId"), worldRoot.GetProp("zoneRenderSchema"), - ParseStringList(worldRoot.GetProp("excludedRenderChannels"))); + ParseStringList(worldRoot.GetProp("excludedRenderChannels")), + worldRoot.GetProp("cameraTargetEntityId"), + ParseFloat(worldRoot.GetProp("cameraDistance"), 0f), + ParseFloat(worldRoot.GetProp("cameraVerticalFieldOfViewDegrees"), 0f), + ParseFloat(worldRoot.GetProp("cameraTargetScreenX"), 0.5f), + ParseFloat(worldRoot.GetProp("cameraTargetScreenY"), 0.5f), + ParseFloat(worldRoot.GetProp("cameraPositionDamping"), 0f), + ParseVector3(worldRoot.GetProp("ambientLightColor")), + ParseFloat(worldRoot.GetProp("ambientLightIntensity"), 1f)); } private static EveUnityPlayableWorldEntity BuildPlayableEntity(EveSurfaceComponent component) @@ -337,7 +345,15 @@ namespace GameCult.Eve.UnityScene string entityViewSchema = "", string zoneRenderPointerId = "", string zoneRenderSchema = "", - IReadOnlyList? excludedRenderChannels = null) + IReadOnlyList? excludedRenderChannels = null, + string cameraTargetEntityId = "", + float cameraDistance = 0f, + float cameraVerticalFieldOfViewDegrees = 0f, + float cameraTargetScreenX = 0.5f, + float cameraTargetScreenY = 0.5f, + float cameraPositionDamping = 0f, + (float r, float g, float b) ambientLightColor = default, + float ambientLightIntensity = 1f) { WorldRootId = worldRootId ?? ""; StatePointerId = statePointerId ?? ""; @@ -356,6 +372,16 @@ namespace GameCult.Eve.UnityScene ZoneRenderPointerId = zoneRenderPointerId ?? ""; ZoneRenderSchema = zoneRenderSchema ?? ""; ExcludedRenderChannels = excludedRenderChannels ?? Array.Empty(); + CameraTargetEntityId = cameraTargetEntityId ?? ""; + CameraDistance = cameraDistance; + CameraVerticalFieldOfViewDegrees = cameraVerticalFieldOfViewDegrees; + CameraTargetScreenX = cameraTargetScreenX; + CameraTargetScreenY = cameraTargetScreenY; + CameraPositionDamping = cameraPositionDamping; + AmbientLightR = ambientLightColor.r; + AmbientLightG = ambientLightColor.g; + AmbientLightB = ambientLightColor.b; + AmbientLightIntensity = ambientLightIntensity; } public string WorldRootId { get; } @@ -376,6 +402,26 @@ namespace GameCult.Eve.UnityScene public string CameraRig { get; } + public string CameraTargetEntityId { get; } + + public float CameraDistance { get; } + + public float CameraVerticalFieldOfViewDegrees { get; } + + public float CameraTargetScreenX { get; } + + public float CameraTargetScreenY { get; } + + public float CameraPositionDamping { get; } + + public float AmbientLightR { get; } + + public float AmbientLightG { get; } + + public float AmbientLightB { get; } + + public float AmbientLightIntensity { get; } + public string ViewId { get; } public string PlayerEntityId { get; } diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs index 4fe1fe4..53b90cf 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -398,7 +398,17 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(projection.PlayableWorld.ZoneRenderPointerId, Is.EqualTo("cultmesh://aetheria/world/zone-render")); Assert.That(projection.PlayableWorld.AssetManifest, Is.EqualTo("cultmesh://aetheria/assets/manifest")); Assert.That(projection.PlayableWorld.InputProfile, Is.EqualTo("arpg-third-person")); - Assert.That(projection.PlayableWorld.CameraRig, Is.EqualTo("third-person-orbit")); + Assert.That(projection.PlayableWorld.CameraRig, Is.EqualTo("planar.top-down-follow.v1")); + Assert.That(projection.PlayableWorld.CameraTargetEntityId, Is.EqualTo("player-vanguard")); + Assert.That(projection.PlayableWorld.CameraDistance, Is.EqualTo(150f)); + Assert.That(projection.PlayableWorld.CameraVerticalFieldOfViewDegrees, Is.EqualTo(60f)); + Assert.That(projection.PlayableWorld.CameraTargetScreenX, Is.EqualTo(0.9f)); + Assert.That(projection.PlayableWorld.CameraTargetScreenY, Is.EqualTo(0.55f)); + Assert.That(projection.PlayableWorld.CameraPositionDamping, Is.EqualTo(5f)); + Assert.That(projection.PlayableWorld.AmbientLightR, Is.EqualTo(0.2f)); + Assert.That(projection.PlayableWorld.AmbientLightG, Is.EqualTo(0.2f)); + Assert.That(projection.PlayableWorld.AmbientLightB, Is.EqualTo(0.2f)); + Assert.That(projection.PlayableWorld.AmbientLightIntensity, Is.EqualTo(1f)); Assert.That(projection.PlayableWorld.ExcludedRenderChannels, Is.EqualTo(new[] { "map" })); Assert.That(projection.PlayableWorld.PlayerEntityId, Is.EqualTo("player-vanguard")); Assert.That(projection.PlayableWorld.MovementCommand, Is.EqualTo("aetheria.daemon.move_intent")); @@ -1027,6 +1037,8 @@ namespace GameCult.Eve.UnityScene.Tests var hostObject = new GameObject("generic-eve-client"); var cameraObject = new GameObject("generic-eve-camera"); hostObject.SetActive(false); + var ambientMode = RenderSettings.ambientMode; + var ambientLight = RenderSettings.ambientLight; try { @@ -1051,6 +1063,7 @@ namespace GameCult.Eve.UnityScene.Tests rig.CameraTransform = cameraObject.transform; rig.RenderPolicySource = new FixedRenderChannelPolicy("map", 14); var camera = cameraObject.AddComponent(); + camera.aspect = 16f / 9f; var player = rootObject.GetComponentInChildren(); Assert.That(player, Is.Not.Null); @@ -1061,14 +1074,25 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(rig.ApplyRig(0f), Is.True); Assert.That(camera.cullingMask & (1 << 14), Is.Zero); - Assert.That(cameraObject.transform.position.y, Is.GreaterThan(0f)); + Assert.That(camera.fieldOfView, Is.EqualTo(60f).Within(0.001f)); + Assert.That(cameraObject.transform.position.y - player.transform.position.y, Is.EqualTo(150f).Within(0.001f)); + var viewport = camera.WorldToViewportPoint(player.transform.position); + Assert.That(viewport.x, Is.EqualTo(0.9f).Within(0.001f)); + Assert.That(viewport.y, Is.EqualTo(0.55f).Within(0.001f)); + Assert.That(RenderSettings.ambientMode, Is.EqualTo(UnityEngine.Rendering.AmbientMode.Flat)); + Assert.That(RenderSettings.ambientLight.r, Is.EqualTo(0.2f).Within(0.001f)); + + var beforeFollow = cameraObject.transform.position; + player.transform.position += Vector3.right * 10f; + Assert.That(rig.ApplyRig(0.1f), Is.True); Assert.That( - Vector3.Distance(cameraObject.transform.position, player.transform.position), - Is.LessThan(50f), - "An excluded map glyph changed pilot-camera framing."); + cameraObject.transform.position.x - beforeFollow.x, + Is.EqualTo(10f * (1f - Mathf.Exp(-0.5f))).Within(0.001f)); } finally { + RenderSettings.ambientMode = ambientMode; + RenderSettings.ambientLight = ambientLight; UnityEngine.Object.DestroyImmediate(cameraObject); UnityEngine.Object.DestroyImmediate(hostObject); UnityEngine.Object.DestroyImmediate(rootObject); @@ -1122,7 +1146,7 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(firstPresentation.WorldRootId, Is.EqualTo("aetheria.daemon.game.playable")); Assert.That(firstPresentation.PlayerEntityId, Is.EqualTo("player-vanguard")); Assert.That(firstPresentation.InputProfile, Is.EqualTo("arpg-third-person")); - Assert.That(firstPresentation.CameraRig, Is.EqualTo("third-person-orbit")); + Assert.That(firstPresentation.CameraRig, Is.EqualTo("planar.top-down-follow.v1")); Assert.That(firstPresentation.UpsertedEntities, Is.EqualTo(3)); Assert.That(firstPresentation.RemovedEntities, Is.EqualTo(0)); Assert.That(firstPresentation.ActiveEntities, Is.EqualTo(3)); @@ -1678,7 +1702,15 @@ namespace GameCult.Eve.UnityScene.Tests ["zoneRenderSchema"] = "gamecult.aetheria.zone_render.v1", ["assetManifest"] = "cultmesh://aetheria/assets/manifest", ["inputProfile"] = "arpg-third-person", - ["cameraRig"] = "third-person-orbit", + ["cameraRig"] = "planar.top-down-follow.v1", + ["cameraTargetEntityId"] = "player-vanguard", + ["cameraDistance"] = "150", + ["cameraVerticalFieldOfViewDegrees"] = "60", + ["cameraTargetScreenX"] = "0.9", + ["cameraTargetScreenY"] = "0.55", + ["cameraPositionDamping"] = "5", + ["ambientLightColor"] = "0.2,0.2,0.2", + ["ambientLightIntensity"] = "1", ["excludedRenderChannels"] = "map", ["playerEntityId"] = "player-vanguard", ["movementCommand"] = "aetheria.daemon.move_intent", diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index bb3058d..3589d71 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.21", + "version": "0.3.22", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": {