Scene Integration In DroneRage
Updated: Nov 4, 2024
DroneRage has drone ships coming above from the seat, from behind the walls, into the hole of the player’s ceiling. The app needs to know where these physical walls are. Both the enemies and the lasers interact with the walls and any other scene objects in the physical environment. So, if you shoot your desk or if the enemy shoots your desk, you see the bullet hole decals. Or if one of the drones crashes over a desk, it will fall and hit the desk.
This logic uses Discover’s integration of the Scene API. The Scene API is used to spawn Photon network objects, and then everything is handled completely in the Discover level, like tracking and synchronizing the objects.
All these objects are also already set up at the Discover level to do all things necessary in all the full game experiences contained within Discover. Theoretically, you could swap these objects out depending on the app selection. For example, you don’t need the Interaction SDK’s RayInteractibles in DroneRage, because you are not pointing at the drones, but Discover has all these as a mega object because it’s easier than creating and de-spawning objects.
Adding scene to DroneRage
The SceneElement class can be found in /Assets/Discover/Scripts/SceneElement.cs and SceneElementsManager in /Assets/Discover/Scripts/SceneElementsManager.cs.
To enable DroneRage access to Scene elements, the objects first need to be registered with a Scene Elements Manager in the Awake function of the SceneElement class:
private void Awake()
{
m_highlightObject.SetActive(false);
SceneElementsManager.Instance.RegisterElement(this);
}
Let’s take a look at Unity Editor to better understand how this Scene API integration works. This is the DiscoverAppController prefab.
MRUK uses the Scene API from the Unity Editor’s perspective. Here objects are set up and will be spawned related to the type of anchor.
When a ceiling object is detected by the Scene API, it spawns the CeilingPhotonInstantiator object. This uses the Photon Instantiator component which is part of Discover and instantiates this network object.
The reason for that is that it instantiates network objects through Photon Fusion, rather than using Object.Instantiate. Then, there is the SceneCeilingPhoton class which is a Photon Fusion net object.
This has the Scene Element component. This is a Discover class, which means the Scene Element Manager tracks it automatically. Since it’s a network object, the colocated player automatically spawns that object because it’s a Photon Fusion network object. This is not special in terms of Scene API or colocation. Rather, it’s like any other network GameObject.
In the SceneElementsManager class, in /Assets/Discover/Scripts/SceneElementsManager.cs, there is this line:
public IEnumerable<SceneElement> GetElementsByLabel(string label) =>
Instance.SceneElements.Where(e => e.ContainsLabel(label));
That label is a string (such as “CEILING”, and so on). You can see, for example, in class EnvironmentSwapper of DroneRage in /Discover/DroneRage/Scripts/Scene/EnvironmentSwapper.cs how it changes the materials of the walls:
private async void SwapWalls(bool toAlt)
{
await UniTask.WaitUntil(() => SceneElementsManager.Instance.AreAllElementsSpawned());
foreach (var wall in SceneElementsManager.Instance.GetElementsByLabel(MRUKAnchor.SceneLabels.WALL_FACE))
{
if (toAlt)
{
m_originalWallMaterials ??= wall.Renderer.sharedMaterials;
wall.Renderer.sharedMaterials = m_altWallMaterials;
}
else
{
wall.Renderer.sharedMaterials = m_originalWallMaterials;
}
}
}
By using SceneElementsManager.Instance.GetElementsByLabel(MRUKAnchor.SceneLabels.WALL_FACE)), it gets elements by label (WALL_FACE) and then swaps on that wall.
Note: CEILING is a string and not an enum. This is because it’s part of the Scene API, which is frequently updated. Having that as a string helps with backwards compatibility.
A similar flow is followed for the ceiling swap:
private async void SwapCeiling(bool toAlt)
{
await UniTask.WaitUntil(() => SceneElementsManager.Instance.AreAllElementsSpawned());
var ceiling = SceneElementsManager.Instance.
GetElementsByLabel(MRUKAnchor.SceneLabels.CEILING).
FirstOrDefault()?.
Renderer;
if (ceiling == null)
return;
var block = new MaterialPropertyBlock();
block.SetFloat(s_visibility, toAlt ? 0 : 1);
ceiling.SetPropertyBlock(block);
}
In the Spawner class, as defined in /Assets/Discover/DroneRage/Scripts/Enemies/Spawner.cs, enemies are initially spawned outside the room, behind the wall. To do that, there is a need to first figure out the extent of the wall.
This is how the room extent is calculated:
private void CalculateRoomExtents()
{
var anchors = SceneElementsManager.Instance.GetElementsByLabel(MRUKAnchor.SceneLabels.WALL_FACE).ToList();
if (anchors.Any())
{
m_roomMinExtent = anchors[0].transform.position;
m_roomMaxExtent = anchors[0].transform.position;
m_roomSize = Vector3.zero;
}
foreach (var anchor in anchors)
{
var anchorTransform = anchor.transform;
var position = anchorTransform.position;
var scale = anchorTransform.lossyScale;
var right = anchorTransform.right * scale.x * 0.5f;
var up = anchorTransform.up * scale.y * 0.5f;
var wallPoints = new Vector3[]
{
position - right - up,
position - right + up,
position + right - up,
position + right + up,
};
foreach (var wp in wallPoints)
{
Debug.Log($"wall point {wp}");
m_roomMinExtent = Vector3.Min(m_roomMinExtent, wp);
m_roomMaxExtent = Vector3.Max(m_roomMaxExtent, wp);
m_roomSize.y = Mathf.Max(m_roomSize.y, transform.lossyScale.y);
}
}
m_roomSize = m_roomMaxExtent - m_roomMinExtent;
Debug.Log("Room Size: " + m_roomSize + " Room Min Extents: " + m_roomMinExtent + " Room Max Extents: " + m_roomMaxExtent);
}
The first gets the list of all wall faces with the following line:
var anchors = SceneElementsManager.Instance.GetElementsByLabel(MRUKAnchor.SceneLabels.WALL_FACE).ToList();
Then, it creates a min and max extent.
m_roomMinExtent = anchors[0].transform.position;
m_roomMaxExtent = anchors[0].transform.position;
This essentially gets a bounding box that represents the room and stores that. The following logic finds a point that is outside that bounding box which represents the user’s real-life room.
public Vector3 GetClosestSpawnPoint(Vector3 position)
{
var spawnOffset = position - 0.5f * (m_roomMinExtent + m_roomMaxExtent);
spawnOffset.y = 0f;
spawnOffset = spawnOffset.normalized;
if (spawnOffset.sqrMagnitude == 0f)
{
spawnOffset = Vector3.forward;
}
return 2f * (RoomSize.magnitude + 1f) * spawnOffset + new Vector3(0f, m_roomMaxExtent.y - 1f, 0f);
}
The Scene API is used to spawn all these Photon network objects, and then everything is handled completely in the Discover level, like tracking and synchronizing these objects.