Unity Timeline Interaction System Integration (#17)

- Added InteractionTimelineAction component for timeline-driven interactions
- Implemented custom editor for timeline event mapping
- Updated interaction event flow to support timeline actions
- Enhanced character move target configuration
- Improved inspector UI for interactable components
- Added technical documentation for interaction system
- Refactored interaction action base classes for extensibility
- Fixed issues with character binding in timelines

Co-authored-by: Michal Pikulski <michal@foolhardyhorizons.com>
Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com>
Reviewed-on: #17
This commit is contained in:
2025-10-07 10:57:11 +00:00
parent c46036dce6
commit 10992b43cc
48 changed files with 3062 additions and 254 deletions

View File

@@ -0,0 +1,184 @@
using UnityEngine;
using UnityEditor;
using UnityEditor.Playables;
using UnityEngine.Playables;
namespace Interactions
{
[CustomEditor(typeof(InteractionTimelineAction))]
public class InteractionTimelineActionEditor : UnityEditor.Editor
{
private SerializedProperty respondToEventsProp;
private SerializedProperty pauseInteractionFlowProp;
private SerializedProperty playableDirectorProp;
private SerializedProperty timelineMappingsProp;
private void OnEnable()
{
respondToEventsProp = serializedObject.FindProperty("respondToEvents");
pauseInteractionFlowProp = serializedObject.FindProperty("pauseInteractionFlow");
playableDirectorProp = serializedObject.FindProperty("playableDirector");
timelineMappingsProp = serializedObject.FindProperty("timelineMappings");
}
public override void OnInspectorGUI()
{
serializedObject.Update();
// Basic properties from the base class
EditorGUILayout.LabelField("Basic Settings", EditorStyles.boldLabel);
// Show the pause interaction flow property
EditorGUILayout.PropertyField(pauseInteractionFlowProp, new GUIContent("Pause Interaction Flow",
"If true, the interaction will wait for the timeline to complete before proceeding"));
// Show the respondToEvents list
EditorGUILayout.PropertyField(respondToEventsProp, new GUIContent("Respond To Events",
"Select which interaction events this timeline action should respond to"), true);
// Show the playable director reference
EditorGUILayout.PropertyField(playableDirectorProp, new GUIContent("Playable Director",
"The director component that will play the timeline. Auto-assigned if not specified."));
// Show the timeline mappings
EditorGUILayout.Space(10);
EditorGUILayout.LabelField("Timeline Mappings", EditorStyles.boldLabel);
if (timelineMappingsProp.arraySize == 0)
{
EditorGUILayout.HelpBox("No timeline mappings added yet. Add one to assign timeline assets to specific interaction events.", MessageType.Info);
}
EditorGUILayout.PropertyField(timelineMappingsProp, new GUIContent("Timeline Mappings"), true);
// Add buttons for quickly adding timeline mappings for common events
EditorGUILayout.Space(10);
EditorGUILayout.LabelField("Quick Add Mappings", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Add Player Arrival"))
{
AddTimelineMapping(InteractionEventType.PlayerArrived);
}
if (GUILayout.Button("Add Interacting Character"))
{
AddTimelineMapping(InteractionEventType.InteractingCharacterArrived);
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Add Interaction Started"))
{
AddTimelineMapping(InteractionEventType.InteractionStarted);
}
if (GUILayout.Button("Add Interaction Complete"))
{
AddTimelineMapping(InteractionEventType.InteractionComplete);
}
EditorGUILayout.EndHorizontal();
// Check for configuration issues
InteractionTimelineAction timelineAction = (InteractionTimelineAction)target;
ValidateConfiguration(timelineAction);
serializedObject.ApplyModifiedProperties();
}
private void AddTimelineMapping(InteractionEventType eventType)
{
int index = timelineMappingsProp.arraySize;
timelineMappingsProp.InsertArrayElementAtIndex(index);
SerializedProperty newElement = timelineMappingsProp.GetArrayElementAtIndex(index);
// Set default values
newElement.FindPropertyRelative("eventType").enumValueIndex = (int)eventType;
// Create a default array with one empty slot for the timeline
SerializedProperty timelinesArray = newElement.FindPropertyRelative("timelines");
timelinesArray.ClearArray();
timelinesArray.InsertArrayElementAtIndex(0);
timelinesArray.GetArrayElementAtIndex(0).objectReferenceValue = null;
// Set default binding values
newElement.FindPropertyRelative("bindPlayerCharacter").boolValue = false;
newElement.FindPropertyRelative("bindPulverCharacter").boolValue = false;
newElement.FindPropertyRelative("playerTrackName").stringValue = "Player";
newElement.FindPropertyRelative("pulverTrackName").stringValue = "Pulver";
newElement.FindPropertyRelative("timeoutSeconds").floatValue = 30f;
newElement.FindPropertyRelative("loopLast").boolValue = false;
newElement.FindPropertyRelative("loopAll").boolValue = false;
// Also add the event to the respondToEvents list if it's not already there
bool found = false;
for (int i = 0; i < respondToEventsProp.arraySize; i++)
{
if (respondToEventsProp.GetArrayElementAtIndex(i).enumValueIndex == (int)eventType)
{
found = true;
break;
}
}
if (!found)
{
int responseIndex = respondToEventsProp.arraySize;
respondToEventsProp.InsertArrayElementAtIndex(responseIndex);
respondToEventsProp.GetArrayElementAtIndex(responseIndex).enumValueIndex = (int)eventType;
}
}
private void ValidateConfiguration(InteractionTimelineAction timelineAction)
{
// Check if we have a PlayableDirector component
PlayableDirector director = timelineAction.GetComponent<PlayableDirector>();
if (director == null)
{
EditorGUILayout.HelpBox("This GameObject is missing a PlayableDirector component, which is required for timeline playback.", MessageType.Error);
}
// Check if we have mappings but no events to respond to
if (timelineMappingsProp.arraySize > 0 && respondToEventsProp.arraySize == 0)
{
EditorGUILayout.HelpBox("You have timeline mappings but no events to respond to. Add events to the 'Respond To Events' list.", MessageType.Warning);
}
// Check if we have events to respond to but no mappings for them
bool hasUnmappedEvents = false;
for (int i = 0; i < respondToEventsProp.arraySize; i++)
{
InteractionEventType eventType = (InteractionEventType)respondToEventsProp.GetArrayElementAtIndex(i).enumValueIndex;
bool found = false;
for (int j = 0; j < timelineMappingsProp.arraySize; j++)
{
SerializedProperty mappingProp = timelineMappingsProp.GetArrayElementAtIndex(j);
InteractionEventType mappingEventType = (InteractionEventType)mappingProp.FindPropertyRelative("eventType").enumValueIndex;
if (mappingEventType == eventType)
{
found = true;
// Check if the mapping has timelines assigned
SerializedProperty timelinesArray = mappingProp.FindPropertyRelative("timelines");
if (timelinesArray.arraySize == 0 || timelinesArray.GetArrayElementAtIndex(0).objectReferenceValue == null)
{
EditorGUILayout.HelpBox($"The mapping for {eventType} has no timeline assets assigned.", MessageType.Warning);
}
break;
}
}
if (!found)
{
hasUnmappedEvents = true;
break;
}
}
if (hasUnmappedEvents)
{
EditorGUILayout.HelpBox("Some events in 'Respond To Events' have no timeline mapping.", MessageType.Warning);
}
}
}
}