Adding behaviour to a layer with data
This guide will focus on adding functional behavior to a layer using persisted data. It will provide the structure for a headless approach to layer properties, enabling custom layer behavior without exposing settings to the end user.
Properties in this platform are modular, making it possible to attach reusable “property sections” to a layer rather than defining specific fields. This modular approach allows data persistence when saving and reloading projects, with Netherlands3D managing persistence automatically.
This guide builds on the previous steps for creating a simple layer type. Here, we'll add properties that are stored within the layer’s data but without displaying them in the properties panel UI.
About the examples
The code examples are meant to show a simple scenario where you have a layer with 2 cubes and where we add behaviour for users to change the colour randomly by clicking on a cube, and where that colour is persisted between sessions. It is expected that for your own use-case you can adapt these examples to suit your situation.
Step 1: Create a Controller Script with ILayerWithPropertyData
Begin by creating a MonoBehaviour
script to encapsulate behaviour for your layer. This behaviour script will handle
the interactions with your property data. To start, implement the ILayerWithPropertyData
interface in the script.
This interface includes a LoadProperties
method, which we’ll define in a later step. For now, leave the
LoadProperties
method empty, focusing on setting up the controller structure.
The code:
using System;
using System.Collections.Generic;
using System.Linq;
using Netherlands3D.Twin.Layers;
using Netherlands3D.Twin.Layers.Properties;
using UnityEngine;
[RequireComponent(typeof(LayerGameObject))]
public class TwoCubesColorChangingBehaviour : MonoBehaviour, ILayerWithPropertyData
{
private LayerPropertyData propertyData;
public LayerPropertyData PropertyData => propertyData;
public void LoadProperties(List<LayerPropertyData> properties)
{
}
// implement the on click behaviour that will randomly change the
// color on one of the cubes; this is omitted for brevity
}
In subsequent code samples some parts will be omitted for brevity -such as the import statements-.
Step 2: Define a Property Data Class
Next, create a dedicated class to represent the data fields you want to persist. This class should extend the
LayerPropertyData
superclass, which provides a foundation for layer properties. By extending LayerPropertyData
, this
class becomes capable of being serialized and deserialized by JSON.net, which
Netherlands3D uses to store and retrieve data within .nl3d
project files.
The data fields in this class should only represent state, with no embedded business logic, ensuring data integrity and predictability.
The code:
using System.Runtime.Serialization;
using Newtonsoft.Json;
using UnityEngine.Events;
public class TwoCubesColorPropertyData : LayerPropertyData
{
// We will be adding fields in the next step
}
Step 3: Add Fields to the Property Data Class
Now, add the fields to the property data class that you want to persist. For serialization and future compatibility, use
a DataContract
annotation on the class. This annotation should include a Namespace
attribute, typically a URI
associated with your organization, to uniquely identify the origin of the class. In addition, a Name
attribute provides
a clear, recognizable identifier for this data type.
When defining fields, it is encouraged to use a UnityEvent
alongside a C# property to trigger an event whenever a
property changes. This approach will allow the behaviour script to detect changes in state through events, making the
class versatile and responsive.
The code:
...
[DataContract(Namespace = "https://example.org/schemas/my-nl3d-project", Name = "TwoCubesColor")]
public class TwoCubesColorPropertyData : LayerPropertyData
{
[DataMember] private Color cube1Color = Color.white;
[DataMember] private Color cube2Color = Color.red;
[JsonIgnore] public readonly UnityEvent<Color> Cube1ColorChanged = new();
[JsonIgnore] public readonly UnityEvent<Color> Cube2ColorChanged = new();
[JsonIgnore]
public float Cube1Color
{
get => cube1Color;
set
{
cube1Color = value;
Cube1ColorChanged.Invoke(cube1Color);
}
}
[JsonIgnore]
public float Cube2Color
{
get => cube2Color;
set
{
cube2Color = value;
Cube2ColorChanged.Invoke(cube2Color);
}
}
}
Step 4: Wiring the Controller Script to the Property Data
With your property data class in place, return to the controller script and establish the connection between it and your
properties. Start by creating a private field in the controller script, using the property data class you defined in
Step 2. Next, set the controller's PropertyData
getter to retrieve data from this field. Finally, implement the
LoadProperties
method by populating it with the necessary code to retrieve the correct LayerPropertyData
object from
the layer at load time.
Each controller can contain only one LayerPropertyData instance.
This ensures the controller is dedicated to a single cohesive set of properties and promotes modularity, allowing the controller to be reused across different layers without modification. For more information, see the explanation section on layers and properties.
The code:
[RequireComponent(typeof(LayerGameObject))]
public class TwoCubesColorChangingBehaviour : MonoBehaviour, ILayerWithPropertyData
{
// Note this change -> We have replaced the generic LayerPropertyData with TwoCubesColorPropertyData
private TwoCubesColorPropertyData propertyData;
public LayerPropertyData PropertyData => propertyData;
private void Start()
{
// Initialise the behaviour with properties from the propertyData - this will trigger upon loading a new project
OnCube1ColorChanged(propertyData.Cube1Color);
OnCube2ColorChanged(propertyData.Cube2Color);
}
public void LoadProperties(List<LayerPropertyData> properties)
{
// Find the property data for this behaviour in the list of properties belonging to its parent layer
var properties = properties.OfType<TwoCubesColorPropertyData>().FirstOrDefault();
// if we found something, use that
if (properties != null)
{
propertyData = properties;
}
// if nothing is provided, or propertyData is otherwise null; make sure we have a default
propertyData ??= new();
// Add the listeners to allow this behaviour to respond to change in its state
propertyData.Cube1ColorChanged.AddListener(OnCube1ColorChanged);
propertyData.Cube2ColorChanged.AddListener(OnCube2ColorChanged);
}
private void OnCube1ColorChanged(Color colour)
{
// Implement the logic to apply this color to cube 1
}
private void OnCube2ColorChanged(Color colour)
{
// Implement the logic to apply this color to cube 2
}
private void OnDestroy()
{
propertyData.Cube1ColorChanged.RemoveListener(OnCube1ColorChanged);
propertyData.Cube2ColorChanged.RemoveListener(OnCube2ColorChanged);
}
}
Step 5: Add the New Behavior to the Layer Prefab
To integrate your new persistent behavior with the layer, attach the behaviour script to the prefab for your layer. By adding the behaviour script, you enable the layer to utilize the persisted properties, allowing it to execute specific behaviours based on saved data.