Creating Custom Track and Clip Types
This guide walks through creating a new track/clip pair in C++. We use TransformTrack/TransformClip as a reference and build a hypothetical ColorTrack/ColorClip that animates a node's color property.
Prerequisites: Familiarity with the Timeline architecture and the built-in tracks.
Step 1: Define the Keyframe Struct
If your clip uses keyframes, define a struct with at least a mTime field. Add whatever data each keyframe carries.
// ColorClip.h
#include "Timeline/TimelineTypes.h"
#include <glm/glm.hpp>
struct ColorKeyframe
{
float mTime = 0.0f;
glm::vec4 mColor = glm::vec4(1.0f);
InterpMode mInterpMode = InterpMode::Linear;
};
Step 2: Create the Clip Header
Inherit from TimelineClip and use the DECLARE_CLIP macro. Override the keyframe API if your clip uses keyframes.
// ColorClip.h
#pragma once
#include "Timeline/TimelineClip.h"
#include <vector>
struct ColorKeyframe;
class ColorClip : public TimelineClip
{
public:
DECLARE_CLIP(ColorClip, TimelineClip);
ColorClip();
virtual void SaveStream(Stream& stream) override;
virtual void LoadStream(Stream& stream, uint32_t version) override;
virtual void GatherProperties(std::vector<Property>& outProps) override;
// Keyframe support
virtual bool SupportsKeyframes() const override { return true; }
virtual uint32_t GetNumKeyframes() const override;
virtual float GetKeyframeTime(uint32_t index) const override;
virtual void AddKeyframeAtTime(float localTime, Node* targetNode) override;
virtual void SetKeyframeTime(uint32_t index, float time) override;
virtual void RemoveKeyframe(uint32_t index) override;
// Custom accessors
const ColorKeyframe& GetKeyframe(uint32_t index) const;
void AddKeyframe(const ColorKeyframe& kf);
// Evaluation
glm::vec4 EvaluateAtLocalTime(float localTime) const;
private:
std::vector<ColorKeyframe> mKeyframes;
};
Step 3: Implement the Clip
Use FORCE_LINK_DEF and DEFINE_CLIP at the top of the cpp file. Implement serialization, keyframe management, and evaluation.
// ColorClip.cpp
#include "ColorClip.h"
#include "ObjectRef.h"
FORCE_LINK_DEF(ColorClip);
DEFINE_CLIP(ColorClip);
ColorClip::ColorClip()
{
}
void ColorClip::SaveStream(Stream& stream)
{
TimelineClip::SaveStream(stream); // Always call base first
stream.WriteUint32((uint32_t)mKeyframes.size());
for (const auto& kf : mKeyframes)
{
stream.WriteFloat(kf.mTime);
stream.WriteVec4(kf.mColor);
stream.WriteUint8((uint8_t)kf.mInterpMode);
}
}
void ColorClip::LoadStream(Stream& stream, uint32_t version)
{
TimelineClip::LoadStream(stream, version); // Always call base first
uint32_t count = stream.ReadUint32();
mKeyframes.resize(count);
for (uint32_t i = 0; i < count; ++i)
{
mKeyframes[i].mTime = stream.ReadFloat();
mKeyframes[i].mColor = stream.ReadVec4();
mKeyframes[i].mInterpMode = (InterpMode)stream.ReadUint8();
}
}
void ColorClip::GatherProperties(std::vector<Property>& outProps)
{
TimelineClip::GatherProperties(outProps);
// Add any clip-level properties visible in the editor here
}
uint32_t ColorClip::GetNumKeyframes() const
{
return (uint32_t)mKeyframes.size();
}
float ColorClip::GetKeyframeTime(uint32_t index) const
{
return mKeyframes[index].mTime;
}
void ColorClip::AddKeyframeAtTime(float localTime, Node* targetNode)
{
ColorKeyframe kf;
kf.mTime = localTime;
kf.mColor = glm::vec4(1.0f); // Default white; could sample from targetNode
AddKeyframe(kf);
}
void ColorClip::SetKeyframeTime(uint32_t index, float time)
{
mKeyframes[index].mTime = time;
}
void ColorClip::RemoveKeyframe(uint32_t index)
{
mKeyframes.erase(mKeyframes.begin() + index);
}
const ColorKeyframe& ColorClip::GetKeyframe(uint32_t index) const
{
return mKeyframes[index];
}
void ColorClip::AddKeyframe(const ColorKeyframe& kf)
{
// Insert in sorted order by time
auto it = mKeyframes.begin();
while (it != mKeyframes.end() && it->mTime < kf.mTime)
{
++it;
}
mKeyframes.insert(it, kf);
}
glm::vec4 ColorClip::EvaluateAtLocalTime(float localTime) const
{
if (mKeyframes.empty())
return glm::vec4(1.0f);
if (mKeyframes.size() == 1 || localTime <= mKeyframes[0].mTime)
return mKeyframes[0].mColor;
if (localTime >= mKeyframes.back().mTime)
return mKeyframes.back().mColor;
// Find surrounding keyframes
uint32_t i = 0;
for (; i < mKeyframes.size() - 1; ++i)
{
if (localTime < mKeyframes[i + 1].mTime)
break;
}
const ColorKeyframe& kf0 = mKeyframes[i];
const ColorKeyframe& kf1 = mKeyframes[i + 1];
if (kf0.mInterpMode == InterpMode::Step)
return kf0.mColor;
float t = (localTime - kf0.mTime) / (kf1.mTime - kf0.mTime);
return glm::mix(kf0.mColor, kf1.mColor, t);
}
Step 4: Create the Track Header
Inherit from TimelineTrack and use the DECLARE_TRACK macro.
// ColorTrack.h
#pragma once
#include "Timeline/TimelineTrack.h"
class ColorTrack : public TimelineTrack
{
public:
DECLARE_TRACK(ColorTrack, TimelineTrack);
ColorTrack();
virtual void Evaluate(float time, Node* target, TimelineInstance* inst) override;
virtual void Reset(Node* target, TimelineInstance* inst) override;
virtual const char* GetTrackTypeName() const override { return "Color"; }
virtual glm::vec4 GetTrackColor() const override;
virtual TypeId GetDefaultClipType() const override;
};
Step 5: Implement the Track
// ColorTrack.cpp
#include "ColorTrack.h"
#include "ColorClip.h"
#include "Timeline/TimelineInstance.h"
#include "Nodes/Node3D.h"
#include "ObjectRef.h"
FORCE_LINK_DEF(ColorTrack);
DEFINE_TRACK(ColorTrack);
ColorTrack::ColorTrack()
{
}
void ColorTrack::Evaluate(float time, Node* target, TimelineInstance* inst)
{
if (target == nullptr)
return;
for (uint32_t i = 0; i < GetNumClips(); ++i)
{
TimelineClip* baseClip = GetClip(i);
if (baseClip->ContainsTime(time))
{
ColorClip* clip = static_cast<ColorClip*>(baseClip);
float localTime = clip->GetLocalTime(time);
glm::vec4 color = clip->EvaluateAtLocalTime(localTime);
// Apply the color to the target node.
// This depends on your node API -- adjust as needed.
// Example: target->SetColor(color);
break;
}
}
}
void ColorTrack::Reset(Node* target, TimelineInstance* inst)
{
// Restore the node's original color from the pre-play snapshot.
// Implementation depends on what state you captured.
}
glm::vec4 ColorTrack::GetTrackColor() const
{
return glm::vec4(0.9f, 0.4f, 0.4f, 1.0f); // Reddish tint in the editor
}
TypeId ColorTrack::GetDefaultClipType() const
{
return ColorClip::GetStaticType();
}
Step 6: Add to the Visual Studio Project
Add the new .h and .cpp files to Engine/Engine.vcxproj:
<!-- In an <ItemGroup> with other ClCompile entries -->
<ClCompile Include="Source\Engine\Timeline\Tracks\ColorTrack.cpp" />
<ClCompile Include="Source\Engine\Timeline\Tracks\ColorClip.cpp" />
<!-- In an <ItemGroup> with other ClInclude entries -->
<ClInclude Include="Source\Engine\Timeline\Tracks\ColorTrack.h" />
<ClInclude Include="Source\Engine\Timeline\Tracks\ColorClip.h" />
Add to Engine/Engine.vcxproj.filters:
<!-- ClCompile filter entries -->
<ClCompile Include="Source\Engine\Timeline\Tracks\ColorTrack.cpp">
<Filter>Source Files\Engine\Timeline\Tracks</Filter>
</ClCompile>
<ClCompile Include="Source\Engine\Timeline\Tracks\ColorClip.cpp">
<Filter>Source Files\Engine\Timeline\Tracks</Filter>
</ClCompile>
<!-- ClInclude filter entries -->
<ClInclude Include="Source\Engine\Timeline\Tracks\ColorTrack.h">
<Filter>Header Files\Engine\Timeline\Tracks</Filter>
</ClInclude>
<ClInclude Include="Source\Engine\Timeline\Tracks\ColorClip.h">
<Filter>Header Files\Engine\Timeline\Tracks</Filter>
</ClInclude>
If the filter groups do not already exist, add them with a unique GUID:
<Filter Include="Source Files\Engine\Timeline\Tracks">
<UniqueIdentifier>{GENERATE-A-NEW-GUID}</UniqueIdentifier>
</Filter>
Summary Checklist
- Keyframe struct -- Define with
mTime+ your data fields + optionalmInterpMode. - Clip header -- Inherit
TimelineClip, useDECLARE_CLIP, override keyframe methods. - Clip implementation -- Use
FORCE_LINK_DEF+DEFINE_CLIP, implement serialization and evaluation. - Track header -- Inherit
TimelineTrack, useDECLARE_TRACK, overrideEvaluate/Reset/metadata. - Track implementation -- Use
FORCE_LINK_DEF+DEFINE_TRACK, iterate clips and apply to node. - vcxproj -- Add all
.hand.cppfiles to the project and filters.
Reference: Existing Track Implementations
| Track | File Path | Good Reference For |
|---|---|---|
| TransformTrack | Timeline/Tracks/TransformTrack.cpp |
Keyframed evaluation with slerp |
| ScriptValueTrack | Timeline/Tracks/ScriptValueTrack.cpp |
Property-name-based evaluation, Datum types |
| FunctionCallTrack | Timeline/Tracks/FunctionCallTrack.cpp |
Fire-once keyframe pattern |
| ActivateTrack | Timeline/Tracks/ActivateTrack.cpp |
Simple non-keyframed clip logic |
| AnimationTrack | Timeline/Tracks/AnimationTrack.cpp |
Asset-driven evaluation, wrap modes |
| AudioTrack | Timeline/Tracks/AudioTrack.cpp |
Fade envelopes, end-mode handling |