| name | add-sdf-element |
| description | Extend the SDF parse → import → implement pipeline with a new element type. Use when: adding support for a new SDF XML element, extending world/model import, creating new Helper MonoBehaviours. |
Add a New SDF Element
Procedure for adding support for a new SDF XML element through the full pipeline: Parser → Importer → Implementer → Helper.
When to Use
- Adding a new SDF element type (e.g.,
<heightmap>, <population>, <atmosphere>)
- Extending an existing element with new child elements
- Creating a new category of scene object that needs runtime state tracking
Architecture
SDFormat package (Parser) → Import.Loader (Importer) → Implement.* (Implementer)
↓ ↓ ↓
C# domain class Unity GameObjects created Components/colliders/renderers
↓
Helper.* MonoBehaviour (runtime state)
Procedure
1. Extend the SDFormat Parser Package
The parser lives in the external com.lge-ros2.sdformat Unity package. Add or extend a domain class:
namespace SDFormat
{
public class MyElement
{
public string Name { get; set; }
public Math.Pose3d RawPose { get; set; }
public string PoseRelativeTo { get; set; }
}
}
Ensure the parser populates this class from the corresponding XML element. If the element is a child of <model>, <link>, <world>, etc., add a collection property to the parent:
public IReadOnlyList<MyElement> MyElements => _myElements;
2. Add Virtual Method in Import.Base.Common
Edit Assets/Scripts/Tools/SDF/Import/Import.Base.Common.cs:
protected virtual object ImportMyElement(in MyElement element, in object parentObject)
{
PrintNotImported(MethodBase.GetCurrentMethod().Name, element.Name);
return null;
}
If the element needs post-processing (after children are created):
protected virtual void AfterImportMyElement(in MyElement element, in object targetObject)
{
PrintNotImported(MethodBase.GetCurrentMethod().Name, element.Name);
}
3. Add Iteration in Import.Base
Edit Assets/Scripts/Tools/SDF/Import/Import.Base.cs — add a batch import method:
private void ImportMyElements(IReadOnlyList<MyElement> items, in object parentObject)
{
foreach (var item in items)
{
var createdObject = ImportMyElement(item, parentObject);
}
}
Wire it into the appropriate parent import method (e.g., in ImportLinks(), ImportModels(), or Start()).
4. Create Loader Override
Create Assets/Scripts/Tools/SDF/Import/Import.MyElement.cs:
using UE = UnityEngine;
namespace SDFormat
{
using Implement;
namespace Import
{
public partial class Loader : Base
{
protected override object ImportMyElement(in MyElement element, in object parentObject)
{
var targetObject = parentObject as UE.GameObject;
var newObject = targetObject.CreateMyElement(element);
return newObject;
}
}
}
}
5. Create Implement Extension Methods
Create Assets/Scripts/Tools/SDF/Implement/Implement.MyElement.cs:
using UE = UnityEngine;
namespace SDFormat
{
namespace Implement
{
public static class MyElement
{
public static UE.GameObject CreateMyElement(
this UE.GameObject targetObject, in SDFormat.MyElement element)
{
var newObject = new UE.GameObject(element.Name);
newObject.tag = "MyTag";
newObject.transform.SetParent(targetObject.transform, false);
if (element.RawPose != null)
{
var (pos, rot) = element.RawPose.ToUnity();
newObject.transform.localPosition = pos;
newObject.transform.localRotation = rot;
}
var helper = newObject.AddComponent<Helper.MyElement>();
helper.Pose = element.RawPose;
helper.PoseRelativeTo = element.PoseRelativeTo;
return newObject;
}
}
}
}
6. Create Helper MonoBehaviour (If Needed)
Create Assets/Scripts/Tools/SDF/Helper/MyElement.cs:
using UE = UnityEngine;
namespace SDFormat
{
namespace Helper
{
public class MyElement : Base
{
[UE.Header("SDF Properties")]
public string myProperty;
new protected void Awake()
{
base.Awake();
}
new protected void Start()
{
base.Start();
}
}
}
}
7. Wire Into Orchestration
Depending on when the element needs to be processed, wire it into the orchestration flow in Import.Base.cs:
Immediate processing (during parent import):
ImportMyElements(model.MyElements, newModelObject);
Deferred processing (needs other elements to exist first, like joints):
protected void StoreMyElements(IReadOnlyList<MyElement> items, in object parentObject)
{
foreach (var item in items)
{
_myElementObjectList.Add(item, parentObject);
}
}
private void ImportDeferredMyElements()
{
foreach (var item in _myElementObjectList)
{
ImportMyElement(item.Key, item.Value);
}
}
The orchestration order in Base.Start() is:
ImportWorld → ImportModels (creates GameObjects)
- Deferred joints (needs both parent/child links)
- Deferred grippers
ImportActors
SpecifyPose (applies all poses)
- Deferred plugins (needs full articulation hierarchy)
Key Patterns
- Coordinate conversion: Always use
SDF2Unity methods (ToUnity(), Position(), Rotation()) — never manually swap axes
- Partial class:
Import.Loader is a partial class split across files. Each new element gets its own Import.*.cs file
- Extension methods: Implementers are static extension methods on
GameObject in the SDFormat.Implement namespace
- Helper.Base inheritance: Always call
base.Awake() and base.Start() — they handle pose control and root model discovery
- Tags: Use existing Unity tags (
Model, Link, Visual, Collision, Sensor, Light, Actor, Marker, Props, Geometry, Road). Adding new tags requires editing ProjectSettings/TagManager.asset
- ArticulationBody disabled: During import,
ArticulationBody components are disabled. SpecifyPose() re-enables them after all poses are applied
Checklist