Exporting data from Unity to After Effects can be really helpful to speed up the post process. A recent project required rendering a movie out from a Unity timeline recording, and then creating a HUD inside After Effects that reflected what was happening in the prerendered movie. In this instance, a character collected coins and the HUD in After Effects had to display the amount collected. This could be animated by hand, but using the scripts below help speed up the process.
Unity Data to After Effects
Information
In Unity:
- Create an empty game object in the Unity level, and name it something like ‘Data Recorder‘.
- Create a new script using the code below, and add it to the game object created above.
- In the Inspector, drag the game object with the Playable Director into the ‘Director‘ slot.
- In the field below, set the correct FPS.
- Inside the script that you want to record data, insert the code below to export information to a JSON file. There are a few options, examples below:
- Record an absolute value: DataRecorder.Instance.RecordState( “Player Health”, 100f ); // Sets health exactly to 100
- Record an increase of 1: DataRecorder.Instance.ModifyState( “Coin Count”, 1f ); // Increase “Coin Count” by 1
- Record a decrease of 15: DataRecorder.Instance.ModifyState( “Coin Count”, -15f ); // Decrease”Coin Count” by 15
- Set recording path and setting in Window > General > Recorder > Recorder Window
- Hit ‘Start Record‘ to record the movie sequence.
Create the Import Script:
- Create a new text file and paste the After Effects script below.
- Save the file to your preferred location; the filename and path won’t impact how the script runs.
In After Effects:
- Create a new layer and select it.
- Select Effect > Expression Control > Slider Control
- Double click the name of the new slider control and rename it. It needs to match the name of the variable defined in Unity. For example, use ‘Coin Count‘ if this is the Unity code: DataRecorder.Instance.ModifyState( “Coin Count”, 1f
- Repeat this step as necessary, creating a Slider Control for each variable exported from Unity.
- Select File > Scripts > Run Script File…
- Select the jsx file created earlier.
- Select the json data file that Unity saved out when the movie was recorded. It will be inside the project’s ‘Assets‘ folder.
- The script will import the data and create keyframes for the slider(s), with the recorded values at the recorded frame numbers.
Unity Export Script
After Effects Import Script
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.Playables;
{
public int frame;
public double time;
public List<VariablePair> states = new();
} [System.Serializable] public class VariablePair
{
public string key;
public float value;
} [System.Serializable] public class UniversalDataLog
{
public List<StateSnapshot> events = new();
}
public class DataRecorder : MonoBehaviour
{
[RuntimeInitializeOnLoadMethod( RuntimeInitializeLoadType.SubsystemRegistration )]
private static void Init() => Instance = null;
public static DataRecorder Instance { get; private set; }
[SerializeField] private PlayableDirector director;[SerializeField] private float frameRate = 30f;
private UniversalDataLog dataLog = new();
private Dictionary<string, float> currentStates = new();
private void Awake()
{
if ( Instance == null ) Instance = this;
else Destroy( gameObject );
}
/// <summary>
/// Sets a variable to an exact, specific value.
/// </summary>
public void RecordState( string variableName, float absoluteValue )
{
currentStates[ variableName ] = absoluteValue;
TakeSnapshot();
}
/// <summary>
/// Modifies a variable by an offset amount (positive or negative).
/// </summary>
public void ModifyState( string variableName, float amount )
{
if ( !currentStates.ContainsKey( variableName ) )
{
currentStates[ variableName ] = 0f;
}
currentStates[ variableName ] += amount;
TakeSnapshot();
}
/// <summary>
/// Internal helper to handle the frame math and save the snapshot data.
/// </summary>
private void TakeSnapshot()
{
double t = director != null ? director.time : Time.time;
int frame = Mathf.FloorToInt( (float) ( t * frameRate ) );
StateSnapshot snapshot = new StateSnapshot
{
frame = frame,
time = t
};
foreach ( var kvp in currentStates )
{
snapshot.states.Add( new VariablePair { key = kvp.Key, value = kvp.Value } );
}
dataLog.events.Add( snapshot );
}
public void Save()
{
if ( dataLog.events.Count == 0 ) return;
string json = JsonUtility.ToJson( dataLog, true );
string path = Path.Combine( Application.dataPath, “UniversalHUDData.json” );
File.WriteAllText( path, json );
Debug.Log( $”Universal data successfully saved to: {path}” );
}
private void OnDisable() => Save();
}
(function() {
// 1. Find the active composition
var comp = app.project.activeItem;
if (!comp || !(comp instanceof CompItem)) {
alert(“Please select an active composition.”);
return;
}
// 2. Find the target layer
var layer = comp.layer(“HUD_Controller”);
if (!layer) {
alert(“Could not find a layer named ‘HUD_Controller’.”);
return;
}
// 3. Prompt user to select the universal JSON file
var jsonFile = File.openDialog(“Select the UniversalHUDData.json file”, “*.json”);
if (!jsonFile) return;
if (jsonFile.open(“r”)) {
var content = jsonFile.read();
jsonFile.close();
// Parse the JSON data safely using eval (standard for ExtendScript)
var data;
try {
data = eval(“(” + content + “)”);
} catch (e) {
alert(“Failed to parse JSON file.”);
return;
}
if (!data || !data.events) {
alert(“Invalid data structure.”);
return;
}
// 4. Set the keyframes across matching sliders
app.beginUndoGroup(“Import Universal Unity Data”);
var events = data.events;
var compFrameRate = comp.frameRate;
// Keep track of which sliders we have cleared so we don’t wipe them repeatedly
var clearedSliders = {};
for (var i = 0; i < events.length; i++) {
var ev = events[i];
// Convert Unity frame to AE time based on AE composition framerate
var aeTime = ev.frame / compFrameRate;
// Loop through all states recorded in this specific frame snapshot
for (var j = 0; j < ev.states.length; j++) {
var stateName = ev.states[j].key;
var stateValue = ev.states[j].value;
// Try to find a matching slider on the HUD layer
var targetSlider = null;
try {
targetSlider = layer.effect(stateName)(“Slider”);
} catch(err) {
// Slider doesn’t exist on the layer, skip it safely
continue;
}
if (targetSlider) {
// Wipe pre-existing keys for this slider just once upon first encounter
if (!clearedSliders[stateName]) {
while (targetSlider.numKeys > 0) {
targetSlider.removeKey(1);
}
// Add an initial 0 keyframe if the first event starts later in the timeline
if (aeTime > 0) {
targetSlider.setValueAtTime(0, 0);
}
clearedSliders[stateName] = true;
}
// Apply the keyframe value
targetSlider.setValueAtTime(aeTime, stateValue);
// Set keyframe interpolation to Hold so counts jump instantly
var keyIndex = targetSlider.nearestKeyIndex(aeTime);
targetSlider.setInterpolationTypeAtKey(keyIndex, KeyframeInterpolationType.HOLD);
}
}
}
app.endUndoGroup();
alert(“Successfully imported universal data across matching sliders!”);
}
})();