This commit is contained in:
Michal Pikulski
2025-10-07 12:57:23 +02:00
48 changed files with 3062 additions and 254 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

View File

@@ -0,0 +1,195 @@
fileFormatVersion: 2
guid: 44a64b7a80921694790236bab7765357
TextureImporter:
internalIDToNameTable:
- first:
213: -8897872742393391051
second: TennisBall_0
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 2
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: iOS
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: WebGL
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: WindowsStoreApps
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites:
- serializedVersion: 2
name: TennisBall_0
rect:
serializedVersion: 2
x: 21
y: 29
width: 219
height: 190
alignment: 0
pivot: {x: 0, y: 0}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: -1
bones: []
spriteID: 538d20d32e7648480800000000000000
internalID: -8897872742393391051
vertices: []
indices:
edges: []
weights: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable:
TennisBall_0: -8897872742393391051
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,127 @@
using UnityEngine;
using UnityEditor;
namespace Interactions
{
[CustomEditor(typeof(Interactable))]
public class InteractableEditor : UnityEditor.Editor
{
SerializedProperty isOneTimeProp;
SerializedProperty cooldownProp;
SerializedProperty characterToInteractProp;
SerializedProperty interactionStartedProp;
SerializedProperty interactionInterruptedProp;
SerializedProperty characterArrivedProp;
SerializedProperty interactionCompleteProp;
private void OnEnable()
{
isOneTimeProp = serializedObject.FindProperty("isOneTime");
cooldownProp = serializedObject.FindProperty("cooldown");
characterToInteractProp = serializedObject.FindProperty("characterToInteract");
interactionStartedProp = serializedObject.FindProperty("interactionStarted");
interactionInterruptedProp = serializedObject.FindProperty("interactionInterrupted");
characterArrivedProp = serializedObject.FindProperty("characterArrived");
interactionCompleteProp = serializedObject.FindProperty("interactionComplete");
}
public override void OnInspectorGUI()
{
serializedObject.Update();
EditorGUILayout.LabelField("Interaction Settings", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(isOneTimeProp);
EditorGUILayout.PropertyField(cooldownProp);
EditorGUILayout.PropertyField(characterToInteractProp);
// Add the buttons for creating move targets
EditorGUILayout.Space(10);
EditorGUILayout.LabelField("Character Move Targets", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Add Trafalgar Target"))
{
CreateMoveTarget(CharacterToInteract.Trafalgar);
}
if (GUILayout.Button("Add Pulver Target"))
{
CreateMoveTarget(CharacterToInteract.Pulver);
}
EditorGUILayout.EndHorizontal();
// Add a button for creating a "Both" target
if (GUILayout.Button("Add Both Characters Target"))
{
CreateMoveTarget(CharacterToInteract.Both);
}
// Display character target counts
Interactable interactable = (Interactable)target;
CharacterMoveToTarget[] moveTargets = interactable.GetComponentsInChildren<CharacterMoveToTarget>();
int trafalgarTargets = 0;
int pulverTargets = 0;
int bothTargets = 0;
foreach (var target in moveTargets)
{
if (target.characterType == CharacterToInteract.Trafalgar)
trafalgarTargets++;
else if (target.characterType == CharacterToInteract.Pulver)
pulverTargets++;
else if (target.characterType == CharacterToInteract.Both)
bothTargets++;
}
EditorGUILayout.LabelField($"Trafalgar Targets: {trafalgarTargets}, Pulver Targets: {pulverTargets}, Both Targets: {bothTargets}");
if (trafalgarTargets > 1 || pulverTargets > 1 || bothTargets > 1 ||
(bothTargets > 0 && (trafalgarTargets > 0 || pulverTargets > 0)))
{
EditorGUILayout.HelpBox("Warning: Multiple move targets found that may conflict. Priority order: Both > Character-specific targets.", MessageType.Warning);
}
EditorGUILayout.Space(10);
EditorGUILayout.LabelField("Interaction Events", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(interactionStartedProp);
EditorGUILayout.PropertyField(interactionInterruptedProp);
EditorGUILayout.PropertyField(characterArrivedProp);
EditorGUILayout.PropertyField(interactionCompleteProp);
serializedObject.ApplyModifiedProperties();
}
private void CreateMoveTarget(CharacterToInteract characterType)
{
Interactable interactable = (Interactable)target;
// Create a new GameObject
GameObject targetObj = new GameObject($"{characterType}MoveTarget");
// Set parent
targetObj.transform.SetParent(interactable.transform);
targetObj.transform.localPosition = Vector3.zero; // Start at the same position as the interactable
// Add CharacterMoveToTarget component
CharacterMoveToTarget moveTarget = targetObj.AddComponent<CharacterMoveToTarget>();
moveTarget.characterType = characterType;
// Position it based on character type (offset for better visibility)
switch (characterType)
{
case CharacterToInteract.Trafalgar:
moveTarget.positionOffset = new Vector3(1.0f, 0, 0);
break;
case CharacterToInteract.Pulver:
moveTarget.positionOffset = new Vector3(0, 0, 1.0f);
break;
case CharacterToInteract.Both:
moveTarget.positionOffset = new Vector3(0.7f, 0, 0.7f);
break;
}
// Select the newly created object
Selection.activeGameObject = targetObj;
Undo.RegisterCreatedObjectUndo(targetObj, $"Create {characterType} Move Target");
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e2011e96b1b84886825d0509dd0a5cee
timeCreated: 1759744192

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);
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6ab56ee48f1445529a62d30c1985f059
timeCreated: 1759746824

View File

@@ -0,0 +1,48 @@
using UnityEngine;
using UnityEditor;
using Interactions;
using System;
[CustomPropertyDrawer(typeof(InteractionTimelineAction.TimelineEventMapping))]
public class TimelineEventMappingDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
// Use default property drawer, but initialize values if needed
InitializeDefaultValues(property);
EditorGUI.PropertyField(position, property, label, true);
}
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
return EditorGUI.GetPropertyHeight(property, label, true);
}
// Called when property is created to initialize default values
private void InitializeDefaultValues(SerializedProperty property)
{
// Check if this is a new/empty property that needs initialization
SerializedProperty timelinesArray = property.FindPropertyRelative("timelines");
if (timelinesArray != null && timelinesArray.arraySize == 0)
{
// This appears to be a new property, so initialize default values
// Initialize timelines array with one empty element
timelinesArray.ClearArray();
timelinesArray.InsertArrayElementAtIndex(0);
timelinesArray.GetArrayElementAtIndex(0).objectReferenceValue = null;
// Set default binding values
property.FindPropertyRelative("bindPlayerCharacter").boolValue = true;
property.FindPropertyRelative("bindPulverCharacter").boolValue = true;
property.FindPropertyRelative("playerTrackName").stringValue = "Player";
property.FindPropertyRelative("pulverTrackName").stringValue = "Pulver";
property.FindPropertyRelative("timeoutSeconds").floatValue = 30f;
property.FindPropertyRelative("loopLast").boolValue = false;
property.FindPropertyRelative("loopAll").boolValue = false;
// Make sure to apply modifications
property.serializedObject.ApplyModifiedProperties();
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c5a227e33ab14a0f86bf391016c57b00
timeCreated: 1759756346

View File

@@ -0,0 +1,394 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &-7584736085941489071
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d21dcc2386d650c4597f3633c75a1f98, type: 3}
m_Name: Animation Track
m_EditorClassIdentifier: Unity.Timeline::UnityEngine.Timeline.AnimationTrack
m_Version: 3
m_AnimClip: {fileID: 0}
m_Locked: 0
m_Muted: 0
m_CustomPlayableFullTypename:
m_Curves: {fileID: 0}
m_Parent: {fileID: 11400000}
m_Children: []
m_Clips:
- m_Version: 1
m_Start: 0.06666666666666667
m_ClipIn: 0
m_Asset: {fileID: -2070454477998879764}
m_Duration: 1.0333333333333334
m_TimeScale: 1
m_ParentTrack: {fileID: -7584736085941489071}
m_EaseInDuration: 0
m_EaseOutDuration: 0
m_BlendInDuration: -1
m_BlendOutDuration: -1
m_MixInCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
m_MixOutCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
m_BlendInCurveMode: 0
m_BlendOutCurveMode: 0
m_ExposedParameterNames: []
m_AnimationCurves: {fileID: 0}
m_Recordable: 0
m_PostExtrapolationMode: 1
m_PreExtrapolationMode: 1
m_PostExtrapolationTime: Infinity
m_PreExtrapolationTime: 0.06666666666666667
m_DisplayName: BalltreeHit
m_Markers:
m_Objects: []
m_InfiniteClipPreExtrapolation: 0
m_InfiniteClipPostExtrapolation: 0
m_InfiniteClipOffsetPosition: {x: 0, y: 0, z: 0}
m_InfiniteClipOffsetEulerAngles: {x: 0, y: 0, z: 0}
m_InfiniteClipTimeOffset: 0
m_InfiniteClipRemoveOffset: 0
m_InfiniteClipApplyFootIK: 1
mInfiniteClipLoop: 0
m_MatchTargetFields: 63
m_Position: {x: 0, y: 0, z: 0}
m_EulerAngles: {x: 0, y: 0, z: 0}
m_AvatarMask: {fileID: 0}
m_ApplyAvatarMask: 1
m_TrackOffset: 0
m_InfiniteClip: {fileID: 0}
m_OpenClipOffsetRotation: {x: 0, y: 0, z: 0, w: 1}
m_Rotation: {x: 0, y: 0, z: 0, w: 1}
m_ApplyOffsets: 0
--- !u!114 &-4664548104421960294
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fde0d25a170598d46a0b9dc16b4527a5, type: 3}
m_Name: ActivationPlayableAsset
m_EditorClassIdentifier: Unity.Timeline::UnityEngine.Timeline.ActivationPlayableAsset
--- !u!114 &-2395336864975438248
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d21dcc2386d650c4597f3633c75a1f98, type: 3}
m_Name: Animation Track (1)
m_EditorClassIdentifier: Unity.Timeline::UnityEngine.Timeline.AnimationTrack
m_Version: 3
m_AnimClip: {fileID: 0}
m_Locked: 0
m_Muted: 0
m_CustomPlayableFullTypename:
m_Curves: {fileID: 0}
m_Parent: {fileID: 11400000}
m_Children: []
m_Clips:
- m_Version: 1
m_Start: 0
m_ClipIn: 0
m_Asset: {fileID: 3115908604919352715}
m_Duration: 0.8333333333333334
m_TimeScale: 1
m_ParentTrack: {fileID: -2395336864975438248}
m_EaseInDuration: 0
m_EaseOutDuration: 0
m_BlendInDuration: -1
m_BlendOutDuration: -1
m_MixInCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
m_MixOutCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
m_BlendInCurveMode: 0
m_BlendOutCurveMode: 0
m_ExposedParameterNames: []
m_AnimationCurves: {fileID: 0}
m_Recordable: 0
m_PostExtrapolationMode: 1
m_PreExtrapolationMode: 1
m_PostExtrapolationTime: Infinity
m_PreExtrapolationTime: 0
m_DisplayName: Pulver_Cucumbatacc
m_Markers:
m_Objects: []
m_InfiniteClipPreExtrapolation: 0
m_InfiniteClipPostExtrapolation: 0
m_InfiniteClipOffsetPosition: {x: 0, y: 0, z: 0}
m_InfiniteClipOffsetEulerAngles: {x: 0, y: 0, z: 0}
m_InfiniteClipTimeOffset: 0
m_InfiniteClipRemoveOffset: 0
m_InfiniteClipApplyFootIK: 1
mInfiniteClipLoop: 0
m_MatchTargetFields: 63
m_Position: {x: 0, y: 0, z: 0}
m_EulerAngles: {x: 0, y: 0, z: 0}
m_AvatarMask: {fileID: 0}
m_ApplyAvatarMask: 1
m_TrackOffset: 0
m_InfiniteClip: {fileID: 0}
m_OpenClipOffsetRotation: {x: 0, y: 0, z: 0, w: 1}
m_Rotation: {x: 0, y: 0, z: 0, w: 1}
m_ApplyOffsets: 0
--- !u!114 &-2070454477998879764
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 030f85c3f73729f4f976f66ffb23b875, type: 3}
m_Name: AnimationPlayableAsset
m_EditorClassIdentifier: Unity.Timeline::UnityEngine.Timeline.AnimationPlayableAsset
m_Clip: {fileID: 7400000, guid: c77ba8b4bbb8013478339a542995d25b, type: 2}
m_Position: {x: 0, y: 0, z: 0}
m_EulerAngles: {x: 0, y: 0, z: 0}
m_UseTrackMatchFields: 1
m_MatchTargetFields: 63
m_RemoveStartOffset: 1
m_ApplyFootIK: 1
m_Loop: 0
m_Version: 1
m_Rotation: {x: 0, y: 0, z: 0, w: 1}
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: bfda56da833e2384a9677cd3c976a436, type: 3}
m_Name: PulverCucumberSmack1
m_EditorClassIdentifier: Unity.Timeline::UnityEngine.Timeline.TimelineAsset
m_Version: 0
m_Tracks:
- {fileID: -7584736085941489071}
- {fileID: -2395336864975438248}
- {fileID: 3942302933360259000}
m_FixedDuration: 0
m_EditorSettings:
m_Framerate: 60
m_ScenePreview: 1
m_DurationMode: 0
m_MarkerTrack: {fileID: 0}
--- !u!114 &3115908604919352715
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 030f85c3f73729f4f976f66ffb23b875, type: 3}
m_Name: AnimationPlayableAsset
m_EditorClassIdentifier: Unity.Timeline::UnityEngine.Timeline.AnimationPlayableAsset
m_Clip: {fileID: 7400000, guid: 09d7dd4e84cbed54bb4ca4e63ad0c6fa, type: 2}
m_Position: {x: 0, y: 0, z: 0}
m_EulerAngles: {x: 0, y: 0, z: 0}
m_UseTrackMatchFields: 1
m_MatchTargetFields: 63
m_RemoveStartOffset: 1
m_ApplyFootIK: 1
m_Loop: 0
m_Version: 1
m_Rotation: {x: 0, y: 0, z: 0, w: 1}
--- !u!114 &3942302933360259000
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 21bf7f712d84d26478ebe6a299f21738, type: 3}
m_Name: Activation Track
m_EditorClassIdentifier: Unity.Timeline::UnityEngine.Timeline.ActivationTrack
m_Version: 3
m_AnimClip: {fileID: 0}
m_Locked: 0
m_Muted: 0
m_CustomPlayableFullTypename:
m_Curves: {fileID: 0}
m_Parent: {fileID: 11400000}
m_Children: []
m_Clips:
- m_Version: 1
m_Start: 1.0166666666666666
m_ClipIn: 0
m_Asset: {fileID: -4664548104421960294}
m_Duration: 0.8999999999999999
m_TimeScale: 1
m_ParentTrack: {fileID: 3942302933360259000}
m_EaseInDuration: 0
m_EaseOutDuration: 0
m_BlendInDuration: 0
m_BlendOutDuration: 0
m_MixInCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
m_MixOutCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
m_BlendInCurveMode: 0
m_BlendOutCurveMode: 0
m_ExposedParameterNames: []
m_AnimationCurves: {fileID: 0}
m_Recordable: 0
m_PostExtrapolationMode: 0
m_PreExtrapolationMode: 0
m_PostExtrapolationTime: 0
m_PreExtrapolationTime: 0
m_DisplayName: Active
m_Markers:
m_Objects: []
m_PostPlaybackState: 3

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1791fd5a24a3142418ed441a2a25b374
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,197 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &-7584736085941489071
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d21dcc2386d650c4597f3633c75a1f98, type: 3}
m_Name: Animation Track
m_EditorClassIdentifier: Unity.Timeline::UnityEngine.Timeline.AnimationTrack
m_Version: 3
m_AnimClip: {fileID: 0}
m_Locked: 0
m_Muted: 0
m_CustomPlayableFullTypename:
m_Curves: {fileID: 0}
m_Parent: {fileID: 11400000}
m_Children: []
m_Clips: []
m_Markers:
m_Objects: []
m_InfiniteClipPreExtrapolation: 0
m_InfiniteClipPostExtrapolation: 0
m_InfiniteClipOffsetPosition: {x: 0, y: 0, z: 0}
m_InfiniteClipOffsetEulerAngles: {x: 0, y: 0, z: 0}
m_InfiniteClipTimeOffset: 0
m_InfiniteClipRemoveOffset: 0
m_InfiniteClipApplyFootIK: 1
mInfiniteClipLoop: 0
m_MatchTargetFields: 63
m_Position: {x: 0, y: 0, z: 0}
m_EulerAngles: {x: 0, y: 0, z: 0}
m_AvatarMask: {fileID: 0}
m_ApplyAvatarMask: 1
m_TrackOffset: 0
m_InfiniteClip: {fileID: 0}
m_OpenClipOffsetRotation: {x: 0, y: 0, z: 0, w: 1}
m_Rotation: {x: 0, y: 0, z: 0, w: 1}
m_ApplyOffsets: 0
--- !u!114 &-2395336864975438248
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d21dcc2386d650c4597f3633c75a1f98, type: 3}
m_Name: Animation Track (1)
m_EditorClassIdentifier: Unity.Timeline::UnityEngine.Timeline.AnimationTrack
m_Version: 3
m_AnimClip: {fileID: 0}
m_Locked: 0
m_Muted: 0
m_CustomPlayableFullTypename:
m_Curves: {fileID: 0}
m_Parent: {fileID: 11400000}
m_Children: []
m_Clips:
- m_Version: 1
m_Start: 0
m_ClipIn: 0
m_Asset: {fileID: 3115908604919352715}
m_Duration: 0.8333333333333334
m_TimeScale: 1
m_ParentTrack: {fileID: -2395336864975438248}
m_EaseInDuration: 0
m_EaseOutDuration: 0
m_BlendInDuration: -1
m_BlendOutDuration: -1
m_MixInCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
m_MixOutCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
m_BlendInCurveMode: 0
m_BlendOutCurveMode: 0
m_ExposedParameterNames: []
m_AnimationCurves: {fileID: 0}
m_Recordable: 0
m_PostExtrapolationMode: 1
m_PreExtrapolationMode: 1
m_PostExtrapolationTime: Infinity
m_PreExtrapolationTime: 0
m_DisplayName: Pulver_Cucumbatacc
m_Markers:
m_Objects: []
m_InfiniteClipPreExtrapolation: 0
m_InfiniteClipPostExtrapolation: 0
m_InfiniteClipOffsetPosition: {x: 0, y: 0, z: 0}
m_InfiniteClipOffsetEulerAngles: {x: 0, y: 0, z: 0}
m_InfiniteClipTimeOffset: 0
m_InfiniteClipRemoveOffset: 0
m_InfiniteClipApplyFootIK: 1
mInfiniteClipLoop: 0
m_MatchTargetFields: 63
m_Position: {x: 0, y: 0, z: 0}
m_EulerAngles: {x: 0, y: 0, z: 0}
m_AvatarMask: {fileID: 0}
m_ApplyAvatarMask: 1
m_TrackOffset: 0
m_InfiniteClip: {fileID: 0}
m_OpenClipOffsetRotation: {x: 0, y: 0, z: 0, w: 1}
m_Rotation: {x: 0, y: 0, z: 0, w: 1}
m_ApplyOffsets: 0
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: bfda56da833e2384a9677cd3c976a436, type: 3}
m_Name: PulverCucumberSmack_empty
m_EditorClassIdentifier: Unity.Timeline::UnityEngine.Timeline.TimelineAsset
m_Version: 0
m_Tracks:
- {fileID: -7584736085941489071}
- {fileID: -2395336864975438248}
m_FixedDuration: 0
m_EditorSettings:
m_Framerate: 60
m_ScenePreview: 1
m_DurationMode: 0
m_MarkerTrack: {fileID: 0}
--- !u!114 &3115908604919352715
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 030f85c3f73729f4f976f66ffb23b875, type: 3}
m_Name: AnimationPlayableAsset
m_EditorClassIdentifier: Unity.Timeline::UnityEngine.Timeline.AnimationPlayableAsset
m_Clip: {fileID: 7400000, guid: 09d7dd4e84cbed54bb4ca4e63ad0c6fa, type: 2}
m_Position: {x: 0, y: 0, z: 0}
m_EulerAngles: {x: 0, y: 0, z: 0}
m_UseTrackMatchFields: 1
m_MatchTargetFields: 63
m_RemoveStartOffset: 1
m_ApplyFootIK: 1
m_Loop: 0
m_Version: 1
m_Rotation: {x: 0, y: 0, z: 0, w: 1}

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ee609df51f47bd541a23d5425e289e30
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -313,6 +313,14 @@ PrefabInstance:
propertyPath: m_Layer
value: 7
objectReference: {fileID: 0}
- target: {fileID: 6109476811019011833, guid: 361ccc9ef82acef4784b24b72013d971, type: 3}
propertyPath: m_Size.x
value: 3.8700001
objectReference: {fileID: 0}
- target: {fileID: 6109476811019011833, guid: 361ccc9ef82acef4784b24b72013d971, type: 3}
propertyPath: m_Size.y
value: 7.55122
objectReference: {fileID: 0}
- target: {fileID: 6109476811019011833, guid: 361ccc9ef82acef4784b24b72013d971, type: 3}
propertyPath: m_SortingOrder
value: 1

View File

@@ -136,7 +136,7 @@ MonoBehaviour:
radius: 2
height: 2
canMove: 1
maxSpeed: 15
maxSpeed: 30
gravity: {x: 0, y: 0, z: 0}
groundMask:
serializedVersion: 2
@@ -236,6 +236,8 @@ SpriteRenderer:
m_RayTracingAccelStructBuildFlagsOverride: 0
m_RayTracingAccelStructBuildFlags: 1
m_SmallMeshCulling: 1
m_ForceMeshLod: -1
m_MeshLodSelectionBias: 0
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
@@ -257,6 +259,7 @@ SpriteRenderer:
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 2

View File

@@ -1,5 +1,51 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &1646387898454772943
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 7371967679236352629}
- component: {fileID: 2280359367130513804}
m_Layer: 0
m_Name: PulverMoveTarget
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &7371967679236352629
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1646387898454772943}
serializedVersion: 2
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: -4.4615383, y: 0.61538696, z: 0}
m_LocalScale: {x: 0.7692308, y: 0.7692308, z: 0.7692308}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 4937390562043858043}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &2280359367130513804
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1646387898454772943}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 2bcf343b3ef74f0fb3c64be6fd2893b6, type: 3}
m_Name:
m_EditorClassIdentifier: AppleHillsScripts::Interactions.CharacterMoveToTarget
characterType: 2
positionOffset: {x: 0, y: 0, z: 1}
--- !u!1 &3591802784221671576
GameObject:
m_ObjectHideFlags: 0
@@ -94,6 +140,11 @@ GameObject:
- component: {fileID: 4937390562043858043}
- component: {fileID: 2720557426779044373}
- component: {fileID: 8897661028274890141}
- component: {fileID: 8437452310832126615}
- component: {fileID: 492578671844741631}
- component: {fileID: 8984729148657672365}
- component: {fileID: 1569498917964935965}
- component: {fileID: 3871210969445384207}
m_Layer: 10
m_Name: BallTree
m_TagString: Untagged
@@ -115,6 +166,7 @@ Transform:
m_ConstrainProportionsScale: 1
m_Children:
- {fileID: 6631072601870453588}
- {fileID: 7371967679236352629}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!212 &2720557426779044373
@@ -137,6 +189,8 @@ SpriteRenderer:
m_RayTracingAccelStructBuildFlagsOverride: 0
m_RayTracingAccelStructBuildFlags: 1
m_SmallMeshCulling: 1
m_ForceMeshLod: -1
m_MeshLodSelectionBias: 0
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
@@ -158,6 +212,7 @@ SpriteRenderer:
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 1
@@ -218,3 +273,115 @@ BoxCollider2D:
m_AutoTiling: 0
m_Size: {x: 3.437712, y: 7.532383}
m_EdgeRadius: 0
--- !u!114 &8437452310832126615
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7379304988657006554}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 73d6494a73174ffabc6a7d3089d51e73, type: 3}
m_Name:
m_EditorClassIdentifier:
isOneTime: 0
cooldown: -1
characterToInteract: 2
interactionStarted:
m_PersistentCalls:
m_Calls: []
interactionInterrupted:
m_PersistentCalls:
m_Calls: []
characterArrived:
m_PersistentCalls:
m_Calls: []
interactionComplete:
m_PersistentCalls:
m_Calls: []
--- !u!114 &492578671844741631
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7379304988657006554}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 1101f6c4eb04423b89dc78dc7c9f1aae, type: 3}
m_Name:
m_EditorClassIdentifier:
stepData: {fileID: 11400000, guid: 8ac614a698631554ab8ac39aed04a189, type: 2}
puzzleIndicator: {fileID: 0}
drawPromptRangeGizmo: 1
--- !u!114 &8984729148657672365
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7379304988657006554}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 42e77a0c97604b6eb7674e58726c831a, type: 3}
m_Name:
m_EditorClassIdentifier: AppleHillsScripts::Interactions.InteractionTimelineAction
respondToEvents: 04000000
pauseInteractionFlow: 1
playableDirector: {fileID: 0}
timelineMappings:
- eventType: 4
timelines: []
bindPlayerCharacter: 0
bindPulverCharacter: 0
playerTrackName:
pulverTrackName:
timeoutSeconds: 30
loopLast: 0
loopAll: 0
--- !u!320 &1569498917964935965
PlayableDirector:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7379304988657006554}
m_Enabled: 1
serializedVersion: 3
m_PlayableAsset: {fileID: 11400000, guid: dd9566026364e814a8dad109e6c365ca, type: 2}
m_InitialState: 0
m_WrapMode: 2
m_DirectorUpdateMode: 1
m_InitialTime: 0
m_SceneBindings:
- key: {fileID: -7584736085941489071, guid: dd9566026364e814a8dad109e6c365ca, type: 2}
value: {fileID: 3871210969445384207}
- key: {fileID: -2395336864975438248, guid: dd9566026364e814a8dad109e6c365ca, type: 2}
value: {fileID: 0}
- key: {fileID: -7231857257271738743, guid: dd9566026364e814a8dad109e6c365ca, type: 2}
value: {fileID: 0}
m_ExposedReferences:
m_References: []
--- !u!95 &3871210969445384207
Animator:
serializedVersion: 7
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7379304988657006554}
m_Enabled: 1
m_Avatar: {fileID: 0}
m_Controller: {fileID: 0}
m_CullingMode: 0
m_UpdateMode: 0
m_ApplyRootMotion: 0
m_LinearVelocityBlending: 0
m_StabilizeFeet: 0
m_AnimatePhysics: 0
m_WarningMessage:
m_HasTransformHierarchy: 1
m_AllowConstantClipSamplingOptimization: 1
m_KeepAnimatorStateOnDisable: 0
m_WriteDefaultValuesOnDisable: 0

View File

@@ -447648,18 +447648,6 @@ MonoBehaviour:
stepData: {fileID: 11400000, guid: 8ac614a698631554ab8ac39aed04a189, type: 2}
puzzleIndicator: {fileID: 0}
drawPromptRangeGizmo: 1
--- !u!114 &1182494941
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1182494929}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 833a4ccef651449e973e623d9107bef5, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!95 &1182494942
Animator:
serializedVersion: 7
@@ -447670,7 +447658,7 @@ Animator:
m_GameObject: {fileID: 1182494929}
m_Enabled: 1
m_Avatar: {fileID: 0}
m_Controller: {fileID: 9100000, guid: 4587ce13b65b5154c853fe4bddbd6247, type: 2}
m_Controller: {fileID: 0}
m_CullingMode: 0
m_UpdateMode: 0
m_ApplyRootMotion: 0
@@ -449505,6 +449493,14 @@ PrefabInstance:
propertyPath: m_Name
value: FootballBall
objectReference: {fileID: 0}
- target: {fileID: 3606875748053192296, guid: 30285f2632211504484661965ed61c57, type: 3}
propertyPath: m_IsActive
value: 0
objectReference: {fileID: 0}
- target: {fileID: 4386785291364665203, guid: 30285f2632211504484661965ed61c57, type: 3}
propertyPath: characterToInteract
value: 2
objectReference: {fileID: 0}
- target: {fileID: 4419731015739629793, guid: 30285f2632211504484661965ed61c57, type: 3}
propertyPath: m_Sprite
value:
@@ -449515,7 +449511,7 @@ PrefabInstance:
objectReference: {fileID: 0}
- target: {fileID: 5572894649736340512, guid: 30285f2632211504484661965ed61c57, type: 3}
propertyPath: m_LocalPosition.y
value: 92.757
value: 92.79
objectReference: {fileID: 0}
- target: {fileID: 5572894649736340512, guid: 30285f2632211504484661965ed61c57, type: 3}
propertyPath: m_LocalPosition.z
@@ -449560,8 +449556,38 @@ PrefabInstance:
m_RemovedComponents: []
m_RemovedGameObjects: []
m_AddedGameObjects: []
m_AddedComponents: []
m_AddedComponents:
- targetCorrespondingSourceObject: {fileID: 3606875748053192296, guid: 30285f2632211504484661965ed61c57, type: 3}
insertIndex: -1
addedObject: {fileID: 1295249128}
m_SourcePrefab: {fileID: 100100000, guid: 30285f2632211504484661965ed61c57, type: 3}
--- !u!1 &1295249127 stripped
GameObject:
m_CorrespondingSourceObject: {fileID: 3606875748053192296, guid: 30285f2632211504484661965ed61c57, type: 3}
m_PrefabInstance: {fileID: 1295249126}
m_PrefabAsset: {fileID: 0}
--- !u!95 &1295249128
Animator:
serializedVersion: 7
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1295249127}
m_Enabled: 1
m_Avatar: {fileID: 0}
m_Controller: {fileID: 0}
m_CullingMode: 0
m_UpdateMode: 0
m_ApplyRootMotion: 0
m_LinearVelocityBlending: 0
m_StabilizeFeet: 0
m_AnimatePhysics: 0
m_WarningMessage:
m_HasTransformHierarchy: 1
m_AllowConstantClipSamplingOptimization: 1
m_KeepAnimatorStateOnDisable: 0
m_WriteDefaultValuesOnDisable: 0
--- !u!1001 &1313372821
PrefabInstance:
m_ObjectHideFlags: 0
@@ -450508,11 +450534,11 @@ PrefabInstance:
objectReference: {fileID: 0}
- target: {fileID: 2264394306674147778, guid: 8ac0210dbf9d7754e9526d6d5c214f49, type: 3}
propertyPath: m_LocalPosition.x
value: -40.250267
value: -36.1
objectReference: {fileID: 0}
- target: {fileID: 2264394306674147778, guid: 8ac0210dbf9d7754e9526d6d5c214f49, type: 3}
propertyPath: m_LocalPosition.y
value: -44.838055
value: -41.5
objectReference: {fileID: 0}
- target: {fileID: 2264394306674147778, guid: 8ac0210dbf9d7754e9526d6d5c214f49, type: 3}
propertyPath: m_LocalPosition.z
@@ -450555,6 +450581,11 @@ PrefabInstance:
m_AddedGameObjects: []
m_AddedComponents: []
m_SourcePrefab: {fileID: 100100000, guid: 8ac0210dbf9d7754e9526d6d5c214f49, type: 3}
--- !u!95 &1363194739 stripped
Animator:
m_CorrespondingSourceObject: {fileID: 1798693240065965692, guid: 8ac0210dbf9d7754e9526d6d5c214f49, type: 3}
m_PrefabInstance: {fileID: 1363194738}
m_PrefabAsset: {fileID: 0}
--- !u!4 &1370055784 stripped
Transform:
m_CorrespondingSourceObject: {fileID: 7815739457642955130, guid: f645a67c7970b124cacb6450fefdebad, type: 3}
@@ -461610,6 +461641,10 @@ PrefabInstance:
serializedVersion: 3
m_TransformParent: {fileID: 1682591185}
m_Modifications:
- target: {fileID: 1597866798502552092, guid: 0b255c6ea64a74240a8db4d9e8f820be, type: 3}
propertyPath: characterToInteract
value: 2
objectReference: {fileID: 0}
- target: {fileID: 1784002662241348359, guid: 0b255c6ea64a74240a8db4d9e8f820be, type: 3}
propertyPath: m_Name
value: Nails
@@ -463989,6 +464024,10 @@ PrefabInstance:
serializedVersion: 3
m_TransformParent: {fileID: 0}
m_Modifications:
- target: {fileID: 2491802807835645092, guid: 3346526f3046f424196615241a307104, type: 3}
propertyPath: characterToInteract
value: 2
objectReference: {fileID: 0}
- target: {fileID: 2531688711670838070, guid: 3346526f3046f424196615241a307104, type: 3}
propertyPath: m_Sprite
value:
@@ -464041,6 +464080,10 @@ PrefabInstance:
propertyPath: isOneTime
value: 1
objectReference: {fileID: 0}
- target: {fileID: 4289780218821574471, guid: 3346526f3046f424196615241a307104, type: 3}
propertyPath: characterToInteract
value: 2
objectReference: {fileID: 0}
- target: {fileID: 5236930998804014616, guid: 3346526f3046f424196615241a307104, type: 3}
propertyPath: m_Layer
value: 10
@@ -464188,6 +464231,10 @@ PrefabInstance:
propertyPath: isOneTime
value: 1
objectReference: {fileID: 0}
- target: {fileID: 519585874127847016, guid: df01157608cce6447b7ccde0bfa290e1, type: 3}
propertyPath: characterToInteract
value: 2
objectReference: {fileID: 0}
- target: {fileID: 1638886621542193701, guid: df01157608cce6447b7ccde0bfa290e1, type: 3}
propertyPath: characterArrived.m_PersistentCalls.m_Calls.Array.data[0].m_Mode
value: 2
@@ -464574,6 +464621,10 @@ PrefabInstance:
propertyPath: isOneTime
value: 1
objectReference: {fileID: 0}
- target: {fileID: 2767794910448825193, guid: 1fda7fccaa5fbd04695f4c98d29bcbe0, type: 3}
propertyPath: characterToInteract
value: 1
objectReference: {fileID: 0}
- target: {fileID: 2767794910448825193, guid: 1fda7fccaa5fbd04695f4c98d29bcbe0, type: 3}
propertyPath: characterArrived.m_PersistentCalls.m_Calls.Array.size
value: 2
@@ -465069,6 +465120,58 @@ PrefabInstance:
serializedVersion: 3
m_TransformParent: {fileID: 0}
m_Modifications:
- target: {fileID: 1569498917964935965, guid: c36b48a324dcaef4cb5ee0f8ca57f0d6, type: 3}
propertyPath: m_PlayableAsset
value:
objectReference: {fileID: 11400000, guid: ee609df51f47bd541a23d5425e289e30, type: 2}
- target: {fileID: 1569498917964935965, guid: c36b48a324dcaef4cb5ee0f8ca57f0d6, type: 3}
propertyPath: m_SceneBindings.Array.size
value: 8
objectReference: {fileID: 0}
- target: {fileID: 1569498917964935965, guid: c36b48a324dcaef4cb5ee0f8ca57f0d6, type: 3}
propertyPath: m_SceneBindings.Array.data[3].key
value:
objectReference: {fileID: -2395336864975438248, guid: 1791fd5a24a3142418ed441a2a25b374, type: 2}
- target: {fileID: 1569498917964935965, guid: c36b48a324dcaef4cb5ee0f8ca57f0d6, type: 3}
propertyPath: m_SceneBindings.Array.data[4].key
value:
objectReference: {fileID: -7584736085941489071, guid: 1791fd5a24a3142418ed441a2a25b374, type: 2}
- target: {fileID: 1569498917964935965, guid: c36b48a324dcaef4cb5ee0f8ca57f0d6, type: 3}
propertyPath: m_SceneBindings.Array.data[5].key
value:
objectReference: {fileID: 3942302933360259000, guid: 1791fd5a24a3142418ed441a2a25b374, type: 2}
- target: {fileID: 1569498917964935965, guid: c36b48a324dcaef4cb5ee0f8ca57f0d6, type: 3}
propertyPath: m_SceneBindings.Array.data[6].key
value:
objectReference: {fileID: -7584736085941489071, guid: ee609df51f47bd541a23d5425e289e30, type: 2}
- target: {fileID: 1569498917964935965, guid: c36b48a324dcaef4cb5ee0f8ca57f0d6, type: 3}
propertyPath: m_SceneBindings.Array.data[7].key
value:
objectReference: {fileID: -2395336864975438248, guid: ee609df51f47bd541a23d5425e289e30, type: 2}
- target: {fileID: 1569498917964935965, guid: c36b48a324dcaef4cb5ee0f8ca57f0d6, type: 3}
propertyPath: m_SceneBindings.Array.data[0].value
value:
objectReference: {fileID: 0}
- target: {fileID: 1569498917964935965, guid: c36b48a324dcaef4cb5ee0f8ca57f0d6, type: 3}
propertyPath: m_SceneBindings.Array.data[3].value
value:
objectReference: {fileID: 1363194739}
- target: {fileID: 1569498917964935965, guid: c36b48a324dcaef4cb5ee0f8ca57f0d6, type: 3}
propertyPath: m_SceneBindings.Array.data[4].value
value:
objectReference: {fileID: 1182494942}
- target: {fileID: 1569498917964935965, guid: c36b48a324dcaef4cb5ee0f8ca57f0d6, type: 3}
propertyPath: m_SceneBindings.Array.data[5].value
value:
objectReference: {fileID: 1295249127}
- target: {fileID: 1569498917964935965, guid: c36b48a324dcaef4cb5ee0f8ca57f0d6, type: 3}
propertyPath: m_SceneBindings.Array.data[6].value
value:
objectReference: {fileID: 1182494942}
- target: {fileID: 1569498917964935965, guid: c36b48a324dcaef4cb5ee0f8ca57f0d6, type: 3}
propertyPath: m_SceneBindings.Array.data[7].value
value:
objectReference: {fileID: 1363194739}
- target: {fileID: 2720557426779044373, guid: c36b48a324dcaef4cb5ee0f8ca57f0d6, type: 3}
propertyPath: m_Sprite
value:
@@ -465133,6 +465236,14 @@ PrefabInstance:
propertyPath: m_LocalPosition.y
value: -0.16
objectReference: {fileID: 0}
- target: {fileID: 7371967679236352629, guid: c36b48a324dcaef4cb5ee0f8ca57f0d6, type: 3}
propertyPath: m_LocalPosition.x
value: -1.606
objectReference: {fileID: 0}
- target: {fileID: 7371967679236352629, guid: c36b48a324dcaef4cb5ee0f8ca57f0d6, type: 3}
propertyPath: m_LocalPosition.y
value: -0
objectReference: {fileID: 0}
- target: {fileID: 7379304988657006554, guid: c36b48a324dcaef4cb5ee0f8ca57f0d6, type: 3}
propertyPath: m_Name
value: BallTree
@@ -465201,6 +465312,82 @@ PrefabInstance:
propertyPath: m_SpriteTilingProperty.oldSize.y
value: 9.060193
objectReference: {fileID: 0}
- target: {fileID: 8984729148657672365, guid: c36b48a324dcaef4cb5ee0f8ca57f0d6, type: 3}
propertyPath: playableDirector
value:
objectReference: {fileID: 7530821580781571561}
- target: {fileID: 8984729148657672365, guid: c36b48a324dcaef4cb5ee0f8ca57f0d6, type: 3}
propertyPath: timelineMappings.Array.size
value: 1
objectReference: {fileID: 0}
- target: {fileID: 8984729148657672365, guid: c36b48a324dcaef4cb5ee0f8ca57f0d6, type: 3}
propertyPath: 'respondToEvents.Array.data[0]'
value: 2
objectReference: {fileID: 0}
- target: {fileID: 8984729148657672365, guid: c36b48a324dcaef4cb5ee0f8ca57f0d6, type: 3}
propertyPath: timelineMappings.Array.data[0].loopLast
value: 1
objectReference: {fileID: 0}
- target: {fileID: 8984729148657672365, guid: c36b48a324dcaef4cb5ee0f8ca57f0d6, type: 3}
propertyPath: timelineMappings.Array.data[0].eventType
value: 2
objectReference: {fileID: 0}
- target: {fileID: 8984729148657672365, guid: c36b48a324dcaef4cb5ee0f8ca57f0d6, type: 3}
propertyPath: timelineMappings.Array.data[1].eventType
value: 4
objectReference: {fileID: 0}
- target: {fileID: 8984729148657672365, guid: c36b48a324dcaef4cb5ee0f8ca57f0d6, type: 3}
propertyPath: timelineMappings.Array.data[1].timeoutSeconds
value: 30
objectReference: {fileID: 0}
- target: {fileID: 8984729148657672365, guid: c36b48a324dcaef4cb5ee0f8ca57f0d6, type: 3}
propertyPath: timelineMappings.Array.data[0].playerTrackName
value: Player
objectReference: {fileID: 0}
- target: {fileID: 8984729148657672365, guid: c36b48a324dcaef4cb5ee0f8ca57f0d6, type: 3}
propertyPath: timelineMappings.Array.data[0].pulverTrackName
value: Pulver
objectReference: {fileID: 0}
- target: {fileID: 8984729148657672365, guid: c36b48a324dcaef4cb5ee0f8ca57f0d6, type: 3}
propertyPath: timelineMappings.Array.data[1].playerTrackName
value: Player
objectReference: {fileID: 0}
- target: {fileID: 8984729148657672365, guid: c36b48a324dcaef4cb5ee0f8ca57f0d6, type: 3}
propertyPath: timelineMappings.Array.data[1].pulverTrackName
value: Pulver
objectReference: {fileID: 0}
- target: {fileID: 8984729148657672365, guid: c36b48a324dcaef4cb5ee0f8ca57f0d6, type: 3}
propertyPath: timelineMappings.Array.data[0].bindPlayerCharacter
value: 0
objectReference: {fileID: 0}
- target: {fileID: 8984729148657672365, guid: c36b48a324dcaef4cb5ee0f8ca57f0d6, type: 3}
propertyPath: timelineMappings.Array.data[0].bindPulverCharacter
value: 0
objectReference: {fileID: 0}
- target: {fileID: 8984729148657672365, guid: c36b48a324dcaef4cb5ee0f8ca57f0d6, type: 3}
propertyPath: timelineMappings.Array.data[1].bindPlayerCharacter
value: 1
objectReference: {fileID: 0}
- target: {fileID: 8984729148657672365, guid: c36b48a324dcaef4cb5ee0f8ca57f0d6, type: 3}
propertyPath: timelineMappings.Array.data[1].bindPulverCharacter
value: 1
objectReference: {fileID: 0}
- target: {fileID: 8984729148657672365, guid: c36b48a324dcaef4cb5ee0f8ca57f0d6, type: 3}
propertyPath: timelineMappings.Array.data[0].timelines.Array.size
value: 2
objectReference: {fileID: 0}
- target: {fileID: 8984729148657672365, guid: c36b48a324dcaef4cb5ee0f8ca57f0d6, type: 3}
propertyPath: timelineMappings.Array.data[1].timelines.Array.size
value: 1
objectReference: {fileID: 0}
- target: {fileID: 8984729148657672365, guid: c36b48a324dcaef4cb5ee0f8ca57f0d6, type: 3}
propertyPath: 'timelineMappings.Array.data[0].timelines.Array.data[0]'
value:
objectReference: {fileID: 11400000, guid: 1791fd5a24a3142418ed441a2a25b374, type: 2}
- target: {fileID: 8984729148657672365, guid: c36b48a324dcaef4cb5ee0f8ca57f0d6, type: 3}
propertyPath: 'timelineMappings.Array.data[0].timelines.Array.data[1]'
value:
objectReference: {fileID: 11400000, guid: ee609df51f47bd541a23d5425e289e30, type: 2}
m_RemovedComponents: []
m_RemovedGameObjects: []
m_AddedGameObjects: []
@@ -465213,11 +465400,28 @@ PrefabInstance:
addedObject: {fileID: 1182494937}
- targetCorrespondingSourceObject: {fileID: 7379304988657006554, guid: c36b48a324dcaef4cb5ee0f8ca57f0d6, type: 3}
insertIndex: -1
addedObject: {fileID: 1182494941}
addedObject: {fileID: 1182494942}
- targetCorrespondingSourceObject: {fileID: 7379304988657006554, guid: c36b48a324dcaef4cb5ee0f8ca57f0d6, type: 3}
insertIndex: -1
addedObject: {fileID: 1182494942}
addedObject: {fileID: 7530821580781571568}
m_SourcePrefab: {fileID: 100100000, guid: c36b48a324dcaef4cb5ee0f8ca57f0d6, type: 3}
--- !u!320 &7530821580781571561 stripped
PlayableDirector:
m_CorrespondingSourceObject: {fileID: 1569498917964935965, guid: c36b48a324dcaef4cb5ee0f8ca57f0d6, type: 3}
m_PrefabInstance: {fileID: 7530821580781571560}
m_PrefabAsset: {fileID: 0}
--- !u!114 &7530821580781571568
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1182494929}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 833a4ccef651449e973e623d9107bef5, type: 3}
m_Name:
m_EditorClassIdentifier: AppleHillsScripts::OneClickInteraction
--- !u!1001 &7535757761066548300
PrefabInstance:
m_ObjectHideFlags: 0
@@ -465535,6 +465739,10 @@ PrefabInstance:
propertyPath: isOneTime
value: 1
objectReference: {fileID: 0}
- target: {fileID: 8818689886719637838, guid: 3144c6bbac26fbd49a1608152821cc5f, type: 3}
propertyPath: characterToInteract
value: 2
objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedGameObjects: []
m_AddedGameObjects: []

View File

@@ -0,0 +1,58 @@
using UnityEngine;
namespace Interactions
{
/// <summary>
/// Defines a target position for character movement during interaction.
/// Attach this to an interactable object's child to specify where
/// characters should move during interaction rather than using the default calculations.
/// </summary>
public class CharacterMoveToTarget : MonoBehaviour
{
[Tooltip("Which character this target position is for")]
public CharacterToInteract characterType = CharacterToInteract.Pulver;
[Tooltip("Optional offset from this transform's position")]
public Vector3 positionOffset = Vector3.zero;
/// <summary>
/// Get the target position for this character to move to
/// </summary>
public Vector3 GetTargetPosition()
{
return transform.position + positionOffset;
}
#if UNITY_EDITOR
private void OnDrawGizmos()
{
// Draw a different colored sphere based on which character this target is for
switch (characterType)
{
case CharacterToInteract.Trafalgar:
Gizmos.color = new Color(0f, 0.5f, 1f, 0.8f); // Blue for player
break;
case CharacterToInteract.Pulver:
Gizmos.color = new Color(1f, 0.5f, 0f, 0.8f); // Orange for follower
break;
case CharacterToInteract.Both:
Gizmos.color = new Color(0.7f, 0f, 0.7f, 0.8f); // Purple for both
break;
default:
Gizmos.color = new Color(0.5f, 0.5f, 0.5f, 0.8f); // Gray for none
break;
}
Vector3 targetPos = GetTargetPosition();
Gizmos.DrawSphere(targetPos, 0.2f);
// Draw a line from the parent interactable to this target
Interactable parentInteractable = GetComponentInParent<Interactable>();
if (parentInteractable != null)
{
Gizmos.DrawLine(parentInteractable.transform.position, targetPos);
}
}
#endif
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2bcf343b3ef74f0fb3c64be6fd2893b6
timeCreated: 1759744130

View File

@@ -1,22 +1,27 @@
using Input;
using UnityEngine;
using System;
using System.Collections.Generic;
using UnityEngine.Events;
using System.Threading.Tasks;
namespace Interactions
{
public enum CharacterToInteract
{
None,
Trafalgar,
Pulver
Pulver,
Both
}
/// <summary>
/// Represents an interactable object that can respond to tap input events.
/// </summary>
public class Interactable : MonoBehaviour, ITouchInputConsumer
{
[Header("Interaction Settings")]
public bool isOneTime = false;
public bool isOneTime;
public float cooldown = -1f;
public CharacterToInteract characterToInteract = CharacterToInteract.Pulver;
@@ -30,8 +35,11 @@ namespace Interactions
private bool _interactionInProgress;
private PlayerTouchController _playerRef;
private FollowerController _followerController;
private bool _isActive = true;
private InteractionEventType _currentEventType;
// Action component system
private List<InteractionActionBase> _registeredActions = new List<InteractionActionBase>();
private void Awake()
{
@@ -39,6 +47,52 @@ namespace Interactions
interactionComplete.AddListener(OnInteractionComplete);
}
/// <summary>
/// Register an action component with this interactable
/// </summary>
public void RegisterAction(InteractionActionBase action)
{
if (!_registeredActions.Contains(action))
{
_registeredActions.Add(action);
}
}
/// <summary>
/// Unregister an action component from this interactable
/// </summary>
public void UnregisterAction(InteractionActionBase action)
{
_registeredActions.Remove(action);
}
/// <summary>
/// Dispatch an interaction event to all registered actions and await their completion
/// </summary>
private async Task DispatchEventAsync(InteractionEventType eventType)
{
_currentEventType = eventType;
// Collect all tasks from actions that want to respond
List<Task<bool>> tasks = new List<Task<bool>>();
foreach (var action in _registeredActions)
{
Task<bool> task = action.OnInteractionEvent(eventType, _playerRef, _followerController);
if (task != null)
{
tasks.Add(task);
}
}
if (tasks.Count > 0)
{
// Wait for all tasks to complete
await Task.WhenAll(tasks);
}
// If no tasks were added, the method will complete immediately (no need for await)
}
/// <summary>
/// Handles tap input. Triggers interaction logic.
/// </summary>
@@ -50,11 +104,12 @@ namespace Interactions
return;
}
Debug.Log($"[Interactable] OnTap at {worldPosition} on {gameObject.name}");
// Broadcast interaction started event
TryInteract();
// Start the interaction process asynchronously
_ = TryInteractAsync();
}
public void TryInteract()
private async Task TryInteractAsync()
{
_interactionInProgress = true;
@@ -63,68 +118,302 @@ namespace Interactions
interactionStarted?.Invoke(_playerRef, _followerController);
// Dispatch the InteractionStarted event to action components
await DispatchEventAsync(InteractionEventType.InteractionStarted);
// After all InteractionStarted actions complete, proceed to player movement
await StartPlayerMovementAsync();
}
private async Task StartPlayerMovementAsync()
{
if (_playerRef == null)
{
Debug.Log($"[Interactable] Player character could not be found. Aborting interaction.");
interactionInterrupted.Invoke();
await DispatchEventAsync(InteractionEventType.InteractionInterrupted);
return;
}
// Compute closest point on the interaction radius
Vector3 interactablePos = transform.position;
Vector3 playerPos = _playerRef.transform.position;
float stopDistance = characterToInteract == CharacterToInteract.Pulver
? GameManager.Instance.PlayerStopDistance
: GameManager.Instance.PlayerStopDistanceDirectInteraction;
Vector3 toPlayer = (playerPos - interactablePos).normalized;
Vector3 stopPoint = interactablePos + toPlayer * stopDistance;
// If characterToInteract is None, immediately trigger the characterArrived event
if (characterToInteract == CharacterToInteract.None)
{
await BroadcastCharacterArrivedAsync();
return;
}
// Unsubscribe previous to avoid duplicate calls
_playerRef.OnArrivedAtTarget -= OnPlayerArrived;
_playerRef.OnMoveToCancelled -= OnPlayerMoveCancelled;
_playerRef.OnArrivedAtTarget += OnPlayerArrived;
_playerRef.OnMoveToCancelled += OnPlayerMoveCancelled;
_playerRef.MoveToAndNotify(stopPoint);
// Check for a CharacterMoveToTarget component for Trafalgar (player) or Both
Vector3 stopPoint;
bool customTargetFound = false;
CharacterMoveToTarget[] moveTargets = GetComponentsInChildren<CharacterMoveToTarget>();
foreach (var target in moveTargets)
{
// Target is valid if it matches Trafalgar specifically or is set to Both
if (target.characterType == CharacterToInteract.Trafalgar || target.characterType == CharacterToInteract.Both)
{
stopPoint = target.GetTargetPosition();
customTargetFound = true;
// We need to wait for the player to arrive, so use a TaskCompletionSource
var tcs = new TaskCompletionSource<bool>();
// Use local functions instead of circular lambda references
void OnPlayerArrivedLocal()
{
// First remove both event handlers to prevent memory leaks
if (_playerRef != null)
{
_playerRef.OnArrivedAtTarget -= OnPlayerArrivedLocal;
_playerRef.OnMoveToCancelled -= OnPlayerMoveCancelledLocal;
}
// Then continue with the interaction flow
OnPlayerArrivedAsync().ContinueWith(_ => tcs.TrySetResult(true));
}
void OnPlayerMoveCancelledLocal()
{
// First remove both event handlers to prevent memory leaks
if (_playerRef != null)
{
_playerRef.OnArrivedAtTarget -= OnPlayerArrivedLocal;
_playerRef.OnMoveToCancelled -= OnPlayerMoveCancelledLocal;
}
// Then handle the cancellation
OnPlayerMoveCancelledAsync().ContinueWith(_ => tcs.TrySetResult(false));
}
// Unsubscribe previous handlers (if any)
_playerRef.OnArrivedAtTarget -= OnPlayerArrived;
_playerRef.OnMoveToCancelled -= OnPlayerMoveCancelled;
// Subscribe our new handlers
_playerRef.OnArrivedAtTarget += OnPlayerArrivedLocal;
_playerRef.OnMoveToCancelled += OnPlayerMoveCancelledLocal;
// Start the player movement
_playerRef.MoveToAndNotify(stopPoint);
// Await player arrival
await tcs.Task;
return;
}
}
// If no custom target was found, use the default behavior
if (!customTargetFound)
{
// Compute closest point on the interaction radius
Vector3 interactablePos = transform.position;
Vector3 playerPos = _playerRef.transform.position;
float stopDistance = characterToInteract == CharacterToInteract.Pulver
? GameManager.Instance.PlayerStopDistance
: GameManager.Instance.PlayerStopDistanceDirectInteraction;
Vector3 toPlayer = (playerPos - interactablePos).normalized;
stopPoint = interactablePos + toPlayer * stopDistance;
// We need to wait for the player to arrive, so use a TaskCompletionSource
var tcs = new TaskCompletionSource<bool>();
// Use local functions instead of circular lambda references
void OnPlayerArrivedLocal()
{
// First remove both event handlers to prevent memory leaks
if (_playerRef != null)
{
_playerRef.OnArrivedAtTarget -= OnPlayerArrivedLocal;
_playerRef.OnMoveToCancelled -= OnPlayerMoveCancelledLocal;
}
// Then continue with the interaction flow
OnPlayerArrivedAsync().ContinueWith(_ => tcs.TrySetResult(true));
}
void OnPlayerMoveCancelledLocal()
{
// First remove both event handlers to prevent memory leaks
if (_playerRef != null)
{
_playerRef.OnArrivedAtTarget -= OnPlayerArrivedLocal;
_playerRef.OnMoveToCancelled -= OnPlayerMoveCancelledLocal;
}
// Then handle the cancellation
OnPlayerMoveCancelledAsync().ContinueWith(_ => tcs.TrySetResult(false));
}
// Unsubscribe previous handlers (if any)
_playerRef.OnArrivedAtTarget -= OnPlayerArrived;
_playerRef.OnMoveToCancelled -= OnPlayerMoveCancelled;
// Subscribe our new handlers
_playerRef.OnArrivedAtTarget += OnPlayerArrivedLocal;
_playerRef.OnMoveToCancelled += OnPlayerMoveCancelledLocal;
// Start the player movement
_playerRef.MoveToAndNotify(stopPoint);
// Await player arrival
await tcs.Task;
}
}
private void OnPlayerMoveCancelled()
private async Task OnPlayerMoveCancelledAsync()
{
_interactionInProgress = false;
interactionInterrupted?.Invoke();
await DispatchEventAsync(InteractionEventType.InteractionInterrupted);
}
private void OnPlayerArrived()
private async Task OnPlayerArrivedAsync()
{
if (!_interactionInProgress)
return;
// Unsubscribe to avoid memory leaks
_playerRef.OnArrivedAtTarget -= OnPlayerArrived;
// Dispatch PlayerArrived event
await DispatchEventAsync(InteractionEventType.PlayerArrived);
// After all PlayerArrived actions complete, proceed to character interaction
await HandleCharacterInteractionAsync();
}
private async Task HandleCharacterInteractionAsync()
{
if (characterToInteract == CharacterToInteract.Pulver)
{
_followerController.OnPickupArrived -= OnFollowerArrived;
_followerController.OnPickupArrived += OnFollowerArrived;
_followerController.GoToPointAndReturn(transform.position, _playerRef.transform);
// We need to wait for the follower to arrive, so use a TaskCompletionSource
var tcs = new TaskCompletionSource<bool>();
// Create a proper local function for the event handler
void OnFollowerArrivedLocal()
{
// First remove the event handler to prevent memory leaks
if (_followerController != null)
{
_followerController.OnPickupArrived -= OnFollowerArrivedLocal;
}
// Then continue with the interaction flow
OnFollowerArrivedAsync().ContinueWith(_ => tcs.TrySetResult(true));
}
// Register our new local function handler
_followerController.OnPickupArrived += OnFollowerArrivedLocal;
// Check for a CharacterMoveToTarget component for Pulver or Both
Vector3 targetPosition = transform.position;
CharacterMoveToTarget[] moveTargets = GetComponentsInChildren<CharacterMoveToTarget>();
foreach (var target in moveTargets)
{
if (target.characterType == CharacterToInteract.Pulver || target.characterType == CharacterToInteract.Both)
{
targetPosition = target.GetTargetPosition();
break;
}
}
// Use the new GoToPoint method instead of GoToPointAndReturn
_followerController.GoToPoint(targetPosition);
// Await follower arrival
await tcs.Task;
}
else if (characterToInteract == CharacterToInteract.Trafalgar)
{
BroadcastCharacterArrived();
await BroadcastCharacterArrivedAsync();
}
else if (characterToInteract == CharacterToInteract.Both)
{
// We need to wait for the follower to arrive, so use a TaskCompletionSource
var tcs = new TaskCompletionSource<bool>();
// Create a proper local function for the event handler
void OnFollowerArrivedLocal()
{
// First remove the event handler to prevent memory leaks
if (_followerController != null)
{
_followerController.OnPickupArrived -= OnFollowerArrivedLocal;
}
// Then continue with the interaction flow
OnFollowerArrivedAsync().ContinueWith(_ => tcs.TrySetResult(true));
}
// Register our new local function handler
_followerController.OnPickupArrived += OnFollowerArrivedLocal;
// Check for a CharacterMoveToTarget component for Pulver or Both
Vector3 targetPosition = transform.position;
CharacterMoveToTarget[] moveTargets = GetComponentsInChildren<CharacterMoveToTarget>();
foreach (var target in moveTargets)
{
if (target.characterType == CharacterToInteract.Pulver || target.characterType == CharacterToInteract.Both)
{
targetPosition = target.GetTargetPosition();
break;
}
}
// Use the new GoToPoint method instead of GoToPointAndReturn
_followerController.GoToPoint(targetPosition);
// Await follower arrival
await tcs.Task;
}
}
private void OnFollowerArrived()
private async Task OnFollowerArrivedAsync()
{
if (!_interactionInProgress)
return;
// Unsubscribe to avoid memory leaks
_followerController.OnPickupArrived -= OnFollowerArrived;
// Dispatch InteractingCharacterArrived event and WAIT for all actions to complete
// This ensures we wait for any timeline animations to finish before proceeding
Debug.Log("[Interactable] Follower arrived, dispatching InteractingCharacterArrived event and waiting for completion");
await DispatchEventAsync(InteractionEventType.InteractingCharacterArrived);
Debug.Log("[Interactable] All InteractingCharacterArrived actions completed, proceeding with interaction");
BroadcastCharacterArrived();
// Check if we have any components that might have paused the interaction flow
bool hasTimelineActions = false;
foreach (var action in _registeredActions)
{
if (action is InteractionTimelineAction timelineAction &&
timelineAction.respondToEvents.Contains(InteractionEventType.InteractingCharacterArrived) &&
timelineAction.pauseInteractionFlow)
{
hasTimelineActions = true;
break;
}
}
// Tell the follower to return to the player
if (_followerController != null && _playerRef != null)
{
_followerController.ReturnToPlayer(_playerRef.transform);
}
// After all InteractingCharacterArrived actions complete, proceed to character arrived
await BroadcastCharacterArrivedAsync();
}
private void BroadcastCharacterArrived()
// Legacy non-async method to maintain compatibility with existing code
private void OnPlayerArrived()
{
// This is now just a wrapper for the async version
_ = OnPlayerArrivedAsync();
}
// Legacy non-async method to maintain compatibility with existing code
private void OnPlayerMoveCancelled()
{
// This is now just a wrapper for the async version
_ = OnPlayerMoveCancelledAsync();
}
private async Task BroadcastCharacterArrivedAsync()
{
// Check for ObjectiveStepBehaviour and lock state
var step = GetComponent<PuzzleS.ObjectiveStepBehaviour>();
@@ -138,16 +427,24 @@ namespace Interactions
_followerController = null;
return;
}
// Dispatch CharacterArrived event
// await DispatchEventAsync(InteractionEventType.InteractingCharacterArrived);
// Broadcast appropriate event
characterArrived?.Invoke();
// Reset variables for next time
_interactionInProgress = false;
_playerRef = null;
_followerController = null;
}
private void OnInteractionComplete(bool success)
private async void OnInteractionComplete(bool success)
{
// Dispatch InteractionComplete event
await DispatchEventAsync(InteractionEventType.InteractionComplete);
if (success)
{
if (isOneTime)

View File

@@ -0,0 +1,91 @@
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
using System.Threading.Tasks;
using Input;
namespace Interactions
{
/// <summary>
/// Base class for all interaction action components
/// These components respond to interaction events and can control the interaction flow
/// </summary>
public abstract class InteractionActionBase : MonoBehaviour
{
[Tooltip("Which interaction events this action should respond to")]
public List<InteractionEventType> respondToEvents = new List<InteractionEventType>();
[Tooltip("Whether the interaction flow should wait for this action to complete")]
public bool pauseInteractionFlow = true;
protected Interactable parentInteractable;
protected virtual void Awake()
{
// Get the parent interactable component
parentInteractable = GetComponentInParent<Interactable>();
if (parentInteractable == null)
{
Debug.LogError($"[{GetType().Name}] Cannot find parent Interactable component!");
enabled = false;
return;
}
}
protected virtual void OnEnable()
{
if (parentInteractable != null)
{
parentInteractable.RegisterAction(this);
}
}
protected virtual void OnDisable()
{
if (parentInteractable != null)
{
parentInteractable.UnregisterAction(this);
}
}
/// <summary>
/// Called when an interaction event occurs that this action is registered for
/// </summary>
/// <param name="eventType">The type of event that occurred</param>
/// <returns>A task that completes when the action is finished, or null if action won't execute</returns>
public Task<bool> OnInteractionEvent(InteractionEventType eventType, PlayerTouchController player, FollowerController follower)
{
if (respondToEvents.Contains(eventType) && ShouldExecute(eventType, player, follower))
{
if (pauseInteractionFlow)
{
return ExecuteAsync(eventType, player, follower);
}
else
{
// If we don't need to pause the flow, execute in the background
// and return a completed task
_ = ExecuteAsync(eventType, player, follower);
return Task.FromResult(false);
}
}
return null;
}
/// <summary>
/// Execute the action for the given event asynchronously
/// </summary>
protected abstract Task<bool> ExecuteAsync(InteractionEventType eventType, PlayerTouchController player, FollowerController follower);
/// <summary>
/// Called to determine if this action should execute for the given event
/// Override this to add additional conditions for execution
/// </summary>
protected virtual bool ShouldExecute(InteractionEventType eventType, PlayerTouchController player, FollowerController follower)
{
return true;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5cf351d32dac4169a9db20609727a70f
timeCreated: 1759746705

View File

@@ -0,0 +1,16 @@
using System;
namespace Interactions
{
/// <summary>
/// Defines the different types of events that can occur during an interaction
/// </summary>
public enum InteractionEventType
{
InteractionStarted,
PlayerArrived,
InteractingCharacterArrived,
InteractionComplete,
InteractionInterrupted
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 78684d31bd4d4636834a494c7cb74f48
timeCreated: 1759746690

View File

@@ -0,0 +1,258 @@
using System;
using UnityEngine;
using UnityEngine.Playables;
using System.Linq;
using System.Threading.Tasks;
using Input;
namespace Interactions
{
/// <summary>
/// Component that plays timeline animations in response to interaction events
/// </summary>
[RequireComponent(typeof(PlayableDirector))]
public class InteractionTimelineAction : InteractionActionBase
{
[System.Serializable]
public class TimelineEventMapping
{
public InteractionEventType eventType;
public PlayableAsset[] timelines;
[Tooltip("Whether to bind the player character to the track named 'Player'")]
public bool bindPlayerCharacter = false;
[Tooltip("Whether to bind the follower character to the track named 'Pulver'")]
public bool bindPulverCharacter = false;
[Tooltip("Custom track name for player character binding")]
public string playerTrackName = "Player";
[Tooltip("Custom track name for follower character binding")]
public string pulverTrackName = "Pulver";
[Tooltip("Time in seconds before the timeline is automatically completed (safety feature)")]
public float timeoutSeconds = 30f;
[Tooltip("Whether to loop the last timeline in the sequence")]
public bool loopLast = false;
[Tooltip("Whether to loop through all timelines in the sequence")]
public bool loopAll = false;
// Helper property to check if we have valid timelines
public bool HasValidTimelines => timelines != null && timelines.Length > 0 && timelines[0] != null;
}
[Header("Timeline Configuration")] [SerializeField]
private PlayableDirector playableDirector;
[SerializeField] private TimelineEventMapping[] timelineMappings;
private TaskCompletionSource<bool> _currentPlaybackTCS;
private int _currentTimelineIndex = 0;
private TimelineEventMapping _currentMapping = null;
protected override void Awake()
{
base.Awake();
if (playableDirector == null)
{
playableDirector = GetComponent<PlayableDirector>();
}
if (playableDirector == null)
{
Debug.LogError("[InteractionTimelineAction] PlayableDirector component is missing!");
enabled = false;
return;
}
// Subscribe to the director's stopped event
playableDirector.stopped += OnPlayableDirectorStopped;
}
private void OnDestroy()
{
if (playableDirector != null)
{
playableDirector.stopped -= OnPlayableDirectorStopped;
}
}
protected override async Task<bool> ExecuteAsync(InteractionEventType eventType, PlayerTouchController player,
FollowerController follower)
{
// Find the timeline for this event type
TimelineEventMapping mapping = Array.Find(timelineMappings, m => m.eventType == eventType);
if (mapping == null || !mapping.HasValidTimelines)
{
// No timeline configured for this event
return true;
}
_currentMapping = mapping;
// _currentTimelineIndex = 0;
return await PlayTimelineSequence(player, follower);
}
private async Task<bool> PlayTimelineSequence(PlayerTouchController player, FollowerController follower)
{
if (_currentMapping == null || !_currentMapping.HasValidTimelines)
{
return true;
}
follower.DropHeldItemAt(follower.transform.position);
// Play the current timeline in the sequence
bool result = await PlaySingleTimeline(_currentMapping.timelines[_currentTimelineIndex], _currentMapping, player, follower);
// Return false if the playback failed
if (!result)
{
return false;
}
// Increment the timeline index for next playback
_currentTimelineIndex++;
// Check if we've reached the end of the sequence
if (_currentTimelineIndex >= _currentMapping.timelines.Length)
{
// If loop all is enabled, start over
if (_currentMapping.loopAll)
{
_currentTimelineIndex = 0;
// Don't continue automatically, wait for next interaction
return true;
}
// If loop last is enabled, replay the last timeline
else if (_currentMapping.loopLast)
{
_currentTimelineIndex = _currentMapping.timelines.Length - 1;
// Don't continue automatically, wait for next interaction
return true;
}
// Otherwise, we're done with the sequence
else
{
_currentTimelineIndex = 0;
_currentMapping = null;
return true;
}
}
// If we have more timelines in the sequence, we're done for now
// Next interaction will pick up where we left off
return true;
}
private async Task<bool> PlaySingleTimeline(PlayableAsset timelineAsset, TimelineEventMapping mapping,
PlayerTouchController player, FollowerController follower)
{
if (timelineAsset == null)
{
Debug.LogWarning("[InteractionTimelineAction] Timeline asset is null");
return true; // Return true to continue the interaction flow
}
// Set the timeline asset
playableDirector.playableAsset = timelineAsset;
// Bind characters if needed
if (mapping.bindPlayerCharacter && player != null)
{
try
{
var trackOutput = playableDirector.playableAsset.outputs.FirstOrDefault(o => o.streamName == mapping.playerTrackName);
if (trackOutput.sourceObject != null)
{
playableDirector.SetGenericBinding(trackOutput.sourceObject, player.gameObject);
}
else
{
Debug.LogWarning($"[InteractionTimelineAction] Could not find track named '{mapping.playerTrackName}' for player binding");
}
}
catch (Exception ex)
{
Debug.LogError($"[InteractionTimelineAction] Error binding player to timeline: {ex.Message}");
}
}
if (mapping.bindPulverCharacter && follower != null)
{
try
{
var trackOutput = playableDirector.playableAsset.outputs.FirstOrDefault(o => o.streamName == mapping.pulverTrackName);
if (trackOutput.sourceObject != null)
{
playableDirector.SetGenericBinding(trackOutput.sourceObject, follower.gameObject);
}
else
{
Debug.LogWarning($"[InteractionTimelineAction] Could not find track named '{mapping.pulverTrackName}' for follower binding");
}
}
catch (Exception ex)
{
Debug.LogError($"[InteractionTimelineAction] Error binding follower to timeline: {ex.Message}");
}
}
// Create a task completion source to await the timeline completion
_currentPlaybackTCS = new TaskCompletionSource<bool>();
// Register for the stopped event if not already registered
playableDirector.stopped -= OnPlayableDirectorStopped;
playableDirector.stopped += OnPlayableDirectorStopped;
// Log the timeline playback
Debug.Log($"[InteractionTimelineAction] Playing timeline {timelineAsset.name} for event {mapping.eventType}");
// Play the timeline
playableDirector.Play();
// Start a timeout coroutine for safety using the mapping's timeout
StartCoroutine(TimeoutCoroutine(mapping.timeoutSeconds));
// Await the timeline completion (will be signaled by the OnPlayableDirectorStopped callback)
bool result = await _currentPlaybackTCS.Task;
// Log completion
Debug.Log($"[InteractionTimelineAction] Timeline {timelineAsset.name} playback completed with result: {result}");
// Clear the task completion source
_currentPlaybackTCS = null;
return result;
}
private void OnPlayableDirectorStopped(PlayableDirector director)
{
if (director != playableDirector || _currentPlaybackTCS == null)
return;
Debug.Log($"[InteractionTimelineAction] PlayableDirector stopped. Signaling completion.");
// Signal completion when the director stops
_currentPlaybackTCS.TrySetResult(true);
}
private System.Collections.IEnumerator TimeoutCoroutine(float timeoutDuration)
{
yield return new WaitForSeconds(timeoutDuration);
// If the TCS still exists after timeout, complete it with failure
if (_currentPlaybackTCS != null)
{
Debug.LogWarning($"[InteractionTimelineAction] Timeline playback timed out after {timeoutDuration} seconds");
_currentPlaybackTCS.TrySetResult(false);
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 42e77a0c97604b6eb7674e58726c831a
timeCreated: 1759746720

View File

@@ -277,25 +277,22 @@ public class FollowerController: MonoBehaviour
}
}
// Command follower to go to a specific point (pathfinding mode)
/// <summary>
/// Command follower to go to a specific point (pathfinding mode).
/// Make the follower move to a specific point only. Will not automatically return.
/// </summary>
/// <param name="worldPosition">The world position to move to.</param>
public void GoToPoint(Vector2 worldPosition)
/// <param name="targetPosition">The position to move to.</param>
public void GoToPoint(Vector2 targetPosition)
{
_isManualFollowing = false;
if (_pickupCoroutine != null)
StopCoroutine(_pickupCoroutine);
if (_aiPath != null)
{
_aiPath.enabled = true;
_aiPath.maxSpeed = _followerMaxSpeed;
_aiPath.destination = new Vector3(worldPosition.x, worldPosition.y, 0);
}
_pickupCoroutine = StartCoroutine(GoToPointSequence(targetPosition));
}
// Command follower to go to a specific point and return to player
/// <summary>
/// Command follower to go to a specific point and return to player.
/// Command follower to go to a specific point and return to player after a brief delay.
/// Legacy method that combines GoToPoint and ReturnToPlayer for backward compatibility.
/// </summary>
/// <param name="itemPosition">The position of the item to pick up.</param>
/// <param name="playerTransform">The transform of the player.</param>
@@ -308,6 +305,19 @@ public class FollowerController: MonoBehaviour
_pickupCoroutine = StartCoroutine(PickupSequence(itemPosition, playerTransform));
}
/// <summary>
/// Make the follower return to the player after it has reached a point.
/// </summary>
/// <param name="playerTransform">The transform of the player to return to.</param>
public void ReturnToPlayer(Transform playerTransform)
{
if (_pickupCoroutine != null)
StopCoroutine(_pickupCoroutine);
if (_aiPath != null)
_aiPath.maxSpeed = _followerMaxSpeed;
_pickupCoroutine = StartCoroutine(ReturnToPlayerSequence(playerTransform));
}
private System.Collections.IEnumerator PickupSequence(Vector2 itemPosition, Transform playerTransform)
{
_isManualFollowing = false;
@@ -349,6 +359,63 @@ public class FollowerController: MonoBehaviour
_aiPath.enabled = false;
_pickupCoroutine = null;
}
private System.Collections.IEnumerator GoToPointSequence(Vector2 targetPosition)
{
_isManualFollowing = false;
_isReturningToPlayer = false;
if (_aiPath != null)
{
_aiPath.enabled = true;
_aiPath.maxSpeed = _followerMaxSpeed;
_aiPath.destination = new Vector3(targetPosition.x, targetPosition.y, 0);
}
// Wait until follower reaches target
while (Vector2.Distance(new Vector2(transform.position.x, transform.position.y),
new Vector2(targetPosition.x, targetPosition.y)) > GameManager.Instance.StopThreshold)
{
yield return null;
}
// Signal arrival
OnPickupArrived?.Invoke();
_pickupCoroutine = null;
}
private System.Collections.IEnumerator ReturnToPlayerSequence(Transform playerTransform)
{
if (_aiPath != null && playerTransform != null)
{
_aiPath.maxSpeed = _followerMaxSpeed;
_aiPath.destination = playerTransform.position;
}
_isReturningToPlayer = true;
// Wait until follower returns to player
while (playerTransform != null &&
Vector2.Distance(new Vector2(transform.position.x, transform.position.y),
new Vector2(playerTransform.position.x, playerTransform.position.y)) > GameManager.Instance.StopThreshold)
{
yield return null;
}
_isReturningToPlayer = false;
OnPickupReturned?.Invoke();
// Reset follower speed to normal after pickup
_followerMaxSpeed = _defaultFollowerMaxSpeed;
if (_aiPath != null)
_aiPath.maxSpeed = _followerMaxSpeed;
_isManualFollowing = true;
if (_aiPath != null)
_aiPath.enabled = false;
_pickupCoroutine = null;
}
#endregion Movement
#region ItemInteractions

View File

@@ -205,16 +205,36 @@ namespace PuzzleS
}
/// <summary>
/// Unlocks all initial steps (those with no dependencies).
/// Unlocks all initial steps (those with no dependencies) and any steps whose dependencies are already met.
/// </summary>
private void UnlockInitialSteps()
{
// First, unlock all steps with no dependencies (initial steps)
var initialSteps = PuzzleGraphUtility.FindInitialSteps(_runtimeDependencies);
foreach (var step in initialSteps)
{
Debug.Log($"[Puzzles] Initial step unlocked: {step.stepId}");
UnlockStep(step);
}
// Keep trying to unlock steps as long as we're making progress
bool madeProgress;
do
{
madeProgress = false;
// Check all steps that haven't been unlocked yet
foreach (var step in _runtimeDependencies.Keys.Where(s => !_unlockedSteps.Contains(s)))
{
// Check if all dependencies have been completed
if (AreRuntimeDependenciesMet(step))
{
Debug.Log($"[Puzzles] Chain step unlocked: {step.stepId}");
UnlockStep(step);
madeProgress = true;
}
}
} while (madeProgress);
}
/// <summary>

257
README.md
View File

@@ -1,211 +1,92 @@
# Apple Hills Dialogue System
# Apple Hills
This document provides an overview of the dialogue system used in Apple Hills, intended primarily for designers working with the dialogue creation tools.
Apple Hills is a Unity-based adventure game featuring interactive puzzle mechanics, dialogue systems, and item interactions.
## Overview
## Project Overview
The Apple Hills dialogue system is a node-based dialogue management system that allows for interactive conversations with NPCs. The system currently supports linear, condition-guarded dialogue paths that can respond to various game conditions such as puzzle completion, item pickups, and item combinations. While the architecture was designed to facilitate branching dialogue in future extensions, the current implementation follows a linear progression through nodes.
Apple Hills provides a unique gaming experience with:
## Dialogue Structure
- Node-based dialogue system for interactive NPC conversations
- Puzzle mechanics with condition-based progression
- Item pickup, combination, and interaction systems
- Custom Universal Render Pipeline setup
The dialogue system uses a graph-based structure with different types of nodes that control the flow of conversation. Each dialogue is represented as a graph containing multiple nodes connected through defined paths.
## Repository Structure
### Core Components
```
AppleHills/
├── Assets/ # Unity asset files
│ ├── Art/ # Visual assets, models, textures
│ ├── Dialogue/ # Dialogue system and dialogue data
│ ├── Scripts/ # C# code for game systems
│ │ ├── Core/ # Core managers and services
│ │ ├── Dialogue/ # Dialogue system implementation
│ │ ├── Input/ # Input handling systems
│ │ ├── Interactions/ # Object interaction systems
│ │ ├── Movement/ # Character movement controllers
│ │ ├── PuzzleS/ # Puzzle mechanics and systems
│ │ └── UI/ # User interface components
│ ├── Scenes/ # Game scenes and levels
│ ├── Prefabs/ # Reusable game objects
│ └── ... # Other asset folders
├── Packages/ # Unity package dependencies
├── ProjectSettings/ # Unity project settings
└── docs/ # Project documentation
├── media/ # Images and other media for documentation
└── dialogue_readme.md # Detailed documentation about the dialogue system
```
1. **RuntimeDialogueGraph**: The container for all dialogue nodes that make up a conversation
2. **DialogueComponent**: Attached to game objects to manage dialogue state and progression
3. **SpeechBubble**: Handles the visual presentation of dialogue text
## Code Structure
## QuickStart Guide
### Scripts Organization
Setting up a dialogue interaction in your scene is straightforward:
The game's codebase is organized into several key modules:
### 1. Component Setup
| Module | Purpose |
|--------|---------|
| Animation | Animation controllers and state machines |
| Bootstrap | Game initialization and scene loading |
| Core | Core game managers and services |
| Dialogue | Dialogue tree implementation and text handling |
| Input | Player input processing and mapping |
| Interactions | Interactive object behaviors and triggers |
| Movement | Character controllers and navigation |
| Pooling | Object pooling for performance optimization |
| PuzzleS | Puzzle mechanics, conditions, and validators |
| Settings | Configurable game parameters and constants |
| UI | User interface elements and controllers |
| Utilities | Helper classes and extension methods |
1. **Place the Dialogue Component**:
- Add the `DialogueComponent` to any game object that has an `Interactable` component
- The `Interactable` component handles player proximity and interaction triggers
- Make sure the interactable is properly configured with appropriate interaction radius
### Core Systems Highlight
2. **Add DialogueCanvas**:
- Add the "DialogueCanvas" prefab from the project assets as a child of your object
- Position the speech bubble appropriately above or near the interactable object
- The speech bubble should be clearly visible but not obstruct important scene elements
- You can adjust the scale and position to fit your specific character or object
The `Assets/Scripts/Core` directory contains critical managers that orchestrate the game's systems:
3. **Assign Dialogue Graph**:
- Create a `RuntimeDialogueGraph` scriptable object (Right Click > Create > Dialogue Graph)
- Set up your dialogue nodes in the graph (see Node Types for details)
- Assign the created graph to the `DialogueComponent` on your object
- Make sure to set the entry node ID in the dialogue graph
#### Key Managers
### 2. Testing Your Dialogue
- **GameManager**: Central hub managing game state, scene transitions, and system coordination
- **ItemManager**: Handles inventory system, item pickup, combination, and usage logic
- **SceneManagerService**: Controls scene loading, unloading, and transitions
1. Enter play mode and approach the interactable object
2. When the component has any lines to serve, the speech bubble should display the prompt ("...")
3. Interact with the object to advance through dialogue lines
4. Test any conditional nodes by completing their requirements
5. Verify that the dialogue progresses as expected
#### Settings Framework
### 3. Common Issues
The `Core/Settings` system provides a robust configuration framework:
- **No speech bubble appears**: Check that the DialogueCanvas is properly added as a child and is active
- **Dialogue doesn't advance**: Ensure the node connections (in/out) are properly set in the dialogue graph
- **Condition not triggering**: Verify that the condition IDs (puzzle step, item, etc.) match exactly with your game systems
- **ServiceLocator**: Dependency injection system for accessing game services
- **SettingsProvider**: Central access point for game configuration values
- **BaseSettings/BaseDeveloperSettings**: Foundation for creating configurable parameters
- **InteractionSettings**: Configuration for player-object interactions
- **MovementModeTypes**: Movement parameters for different locomotion modes
## Node Types
## Documentation
The dialogue system supports several node types, each serving a specific purpose in the conversation flow:
Detailed documentation about specific systems can be found in the `docs` folder:
### 1. Dialogue Node
- [Dialogue System Documentation](docs/dialogue_readme.md)
Simple dialogue nodes display text to the player. They can contain multiple lines that are shown sequentially when the player interacts with the NPC.
## Development
**Key features:**
- Multiple dialogue lines displayed in sequence
- Optional looping through lines
- Automatic progression to the next node when all lines are exhausted
The project is structured using standard Unity practices. Key components:
### 2. WaitOnPuzzleStep Node
This node pauses dialogue progression until a specific puzzle step has been completed by the player.
**Key features:**
- Automatically advances when the specified puzzle step is completed
- Displays dialogue while waiting for the condition to be met
- Visual prompt appears when the condition is met, indicating available dialogue
### 3. WaitOnPickup Node
This node waits until the player has picked up a specific item before advancing the dialogue.
**Key features:**
- Automatically advances when the player picks up the specified item
- Shows dialogue while waiting for the item pickup
- Visual prompt appears when the item is picked up, indicating available dialogue
### 4. WaitOnSlot Node
This node requires the player to place a specific item in a designated slot before the dialogue can progress.
**Key features:**
- Supports different dialogue lines for different slot states:
- Default lines when no item is slotted
- Incorrect item lines when the wrong item is placed
- Forbidden item lines when a specifically disallowed item is placed
- Visual prompt appears when the correct item is slotted, indicating available dialogue
### 5. WaitOnCombination Node
This node waits for the player to create a specific item through the combination system.
**Key features:**
- Automatically advances when the player creates the specified item through combination
- Shows dialogue while waiting for the item combination
- Visual prompt appears when the item is created, indicating available dialogue
### 6. End Node
Terminates the dialogue sequence.
**Key features:**
- Marks the dialogue as completed
- No further interaction available until the dialogue is restarted
## Dialogue Flow
1. **Dialogue Initialization**
- When a dialogue is started (often through character interaction), the system begins at the entry node
- Each node's dialogue lines are displayed in sequence as the player interacts
2. **Interaction Mechanism**
- Dialogue advances when the player interacts with the NPC
- Each interaction displays the next line of dialogue
- When all lines in a node are displayed, the system moves to the next node (unless waiting on a condition)
3. **Conditional Progress**
- When the dialogue reaches a conditional node (like WaitOnPuzzleStep), it waits for the condition to be met
- Once the condition is satisfied, the speech bubble shows a prompt
- The next interaction after the condition is met advances to the next node
4. **Visual Indicators**
- Speech bubbles show ellipses ("...") as a prompt when dialogue is available
- The dialogue text can be displayed instantly or with a typewriter effect
- The speech bubble hides when no dialogue is available
## Designer Tips
1. **Node Organization**
- Start every dialogue graph with a standard Dialogue node as the entry point
- End every dialogue path with an End node to properly terminate the conversation
- Use conditional nodes strategically to create gameplay-driven dialogue experiences
2. **Dialogue Writing**
- Keep individual dialogue lines concise for better readability
- Consider using the looping option for nodes when you want to repeat information
- For WaitOnSlot nodes, write unique dialogue for incorrect/forbidden items to provide clear feedback
3. **Flow Control**
- Ensure all nodes (except End nodes) have a valid next node specified
- Test dialogue paths to verify all conditions can be met during gameplay
- Consider using multiple dialogue lines within a single node rather than creating separate nodes for sequential lines
4. **Best Practices**
- Name your nodes descriptively in the editor for easier management
- Group related dialogue sequences into separate dialogue graphs
- Use the speaker name field to clearly identify who is speaking
## Technical Details
### Public Events and APIs
The dialogue system exposes several events that can be used by other systems:
#### DialogueComponent
- **Events**:
- `OnDialogueChanged`: Triggered when the dialogue text changes
- **Properties**:
- `IsActive`: Indicates whether the dialogue is currently active
- `IsCompleted`: Indicates whether the dialogue has reached an End node
- `CurrentSpeakerName`: Returns the name of the current speaker
- **Public Methods**:
- `StartDialogue()`: Initiates the dialogue from the beginning
- `GetCurrentDialogueLine()`: Retrieves the current dialogue line text
- `HasAnyLines()`: Checks if the dialogue component has any lines available
- `SetDialogueGraph(RuntimeDialogueGraph)`: Sets the dialogue graph for the component
#### SpeechBubble
- **Public Methods**:
- `Show()`: Makes the speech bubble visible
- `Hide()`: Hides the speech bubble
- `Toggle()`: Toggles the visibility of the speech bubble
- `SetText(string)`: Sets the text displayed in the speech bubble
- `DisplayDialogueLine(string, bool)`: Displays a dialogue line and handles prompt visibility
- `UpdatePromptVisibility(bool)`: Updates the speech bubble to show a prompt or hide based on dialogue availability
- `SetDisplayMode(TextDisplayMode)`: Changes how text is displayed (instant or typewriter)
- `SkipTypewriter()`: Immediately displays the full text, skipping the typewriter effect
- `SetTypewriterSpeed(float)`: Sets the speed of the typewriter effect
### Integration with Other Systems
The dialogue system integrates with several other game systems:
1. **Puzzle System**: Monitors puzzle completion events to advance WaitOnPuzzleStep nodes
2. **Item System**: Tracks item pickups, combinations, and slot interactions to advance respective node types
3. **Interaction System**: Responds to player interaction with the NPC to progress through dialogue lines
### Technical Workflow
1. Create a RuntimeDialogueGraph asset in the Unity editor
2. Add nodes and connections using the dialogue editor
3. Assign the graph to a DialogueComponent on an NPC GameObject
4. Ensure a SpeechBubble component is available (as a child object or referenced)
5. Set up any necessary puzzle steps, items, or slots that the dialogue will reference
## Summary
The Apple Hills dialogue system provides a powerful and flexible way to create interactive conversations that respond to player actions and game state. By using different node types and conditions, designers can craft engaging dialogues that feel natural and responsive within the game world.
- Universal Render Pipeline for consistent visuals
- Input System for configurable controls
- Addressable Assets for asset management

277
docs/dialogue_readme.md Normal file
View File

@@ -0,0 +1,277 @@
# Apple Hills Dialogue System
This document provides an overview of the dialogue system used in Apple Hills, intended primarily for designers (Damian) working with the dialogue creation tools.
## Overview
The Apple Hills dialogue system is a node-based dialogue management system that allows for interactive conversations with NPCs. The system currently supports linear, condition-guarded dialogue paths that can respond to various game conditions such as puzzle completion, item pickups, and item combinations. While the architecture was designed to facilitate branching dialogue in future extensions, the current implementation follows a linear progression through nodes.
## Dialogue Structure
The dialogue system uses a graph-based structure with different types of nodes that control the flow of conversation. Each dialogue is represented as a graph containing multiple nodes connected through defined paths.
![Dialogue Graph Example](media/dialogue_graph_example.png)
### Core Components
1. **RuntimeDialogueGraph**: The container for all dialogue nodes that make up a conversation
- Defined in `Assets/Scripts/Dialogue/RuntimeDialogueGraph.cs`
- Contains the entry point node ID and a list of all dialogue nodes
- Holds the speaker name that appears in dialogue UI
2. **DialogueComponent**: Attached to game objects to manage dialogue state and progression
- Defined in `Assets/Scripts/Dialogue/DialogueComponent.cs`
- Manages the current state of dialogue (active node, line index)
- Responds to game events like puzzle completion, item pickup, etc.
- Controls dialogue advancement through player interaction
3. **SpeechBubble**: Handles the visual presentation of dialogue text
- Defined in `Assets/Scripts/Dialogue/SpeechBubble.cs`
- Manages the dialogue UI elements and text display
- Implements typewriter effects and visual prompts
## QuickStart Guide
Setting up a dialogue interaction in your scene is straightforward:
### 1. Component Setup
1. **Place the Dialogue Component**:
- Add the `DialogueComponent` to any game object that has an `Interactable` component
- The `Interactable` component handles player proximity and interaction triggers
- Make sure the interactable is properly configured with appropriate interaction radius
![Dialogue Component Inspector](media/dialogue_component_inspector.png)
2. **Add DialogueCanvas**:
- Add the "DialogueCanvas" prefab from the project assets as a child of your object
- Position the speech bubble appropriately above or near the interactable object
- The speech bubble should be clearly visible but not obstruct important scene elements
- You can adjust the scale and position to fit your specific character or object
![Speech Bubble Setup](media/speech_bubble_setup.png)
3. **Assign Dialogue Graph**:
- Create a `RuntimeDialogueGraph` scriptable object (Right Click > Create > Dialogue Graph)
- Set up your dialogue nodes in the graph (see Node Types for details)
- Assign the created graph to the `DialogueComponent` on your object
- Make sure to set the entry node ID in the dialogue graph
![Creating Dialogue Graph](media/create_dialogue_graph.png)
### 2. Testing Your Dialogue
1. Enter play mode and approach the interactable object
2. When the component has any lines to serve, the speech bubble should display the prompt ("...")
3. Interact with the object to advance through dialogue lines
4. Test any conditional nodes by completing their requirements
5. Verify that the dialogue progresses as expected
![Testing Dialogue Flow](media/dialogue_testing_flow.png)
### 3. Common Issues
- **No speech bubble appears**: Check that the DialogueCanvas is properly added as a child and is active
- **Dialogue doesn't advance**: Ensure the node connections (in/out) are properly set in the dialogue graph
- **Condition not triggering**: Verify that the condition IDs (puzzle step, item, etc.) match exactly with your game systems
## Node Types
The dialogue system supports several node types, each serving a specific purpose in the conversation flow:
### 1. Dialogue Node
Simple dialogue nodes display text to the player. They can contain multiple lines that are shown sequentially when the player interacts with the NPC.
**Key features:**
- Multiple dialogue lines displayed in sequence
- Optional looping through lines
- Automatic progression to the next node when all lines are exhausted
![Dialogue Node Example](media/dialogue_node_example.png)
**Implementation details:**
- Defined as `RuntimeDialogueNodeType.Dialogue` in `RuntimeDialogueGraph.cs`
- Uses `dialogueLines` list to store sequential lines of text
- The `loopThroughLines` boolean controls whether the dialogue returns to the first line after reaching the end
### 2. WaitOnPuzzleStep Node
This node pauses dialogue progression until a specific puzzle step has been completed by the player.
**Key features:**
- Automatically advances when the specified puzzle step is completed
- Displays dialogue while waiting for the condition to be met
- Visual prompt appears when the condition is met, indicating available dialogue
![WaitOnPuzzleStep Node Example](media/wait_on_puzzle_node.png)
**Implementation details:**
- Defined as `RuntimeDialogueNodeType.WaitOnPuzzleStep` in `RuntimeDialogueGraph.cs`
- Links to `PuzzleManager.OnStepCompleted` event through the `puzzleStepID` property
- The `DialogueComponent` listens for puzzle completion events through `OnAnyPuzzleStepCompleted` method
### 3. WaitOnPickup Node
This node waits until the player has picked up a specific item before advancing the dialogue.
**Key features:**
- Automatically advances when the player picks up the specified item
- Shows dialogue while waiting for the item pickup
- Visual prompt appears when the item is picked up, indicating available dialogue
![WaitOnPickup Node Example](media/wait_on_pickup_node.png)
**Implementation details:**
- Defined as `RuntimeDialogueNodeType.WaitOnPickup` in `RuntimeDialogueGraph.cs`
- Links to `ItemManager.OnItemPickedUp` event through the `pickupItemID` property
- The `DialogueComponent` listens for item pickup events through `OnAnyItemPickedUp` method
### 4. WaitOnSlot Node
This node requires the player to place a specific item in a designated slot before the dialogue can progress.
**Key features:**
- Supports different dialogue lines for different slot states:
- Default lines when no item is slotted
- Incorrect item lines when the wrong item is placed
- Forbidden item lines when a specifically disallowed item is placed
- Visual prompt appears when the correct item is slotted, indicating available dialogue
![WaitOnSlot Node Example](media/wait_on_slot_node.png)
**Implementation details:**
- Defined as `RuntimeDialogueNodeType.WaitOnSlot` in `RuntimeDialogueGraph.cs`
- Uses multiple events from `ItemManager` including:
- `OnCorrectItemSlotted` - Triggered when the matching `slotItemID` is placed
- `OnIncorrectItemSlotted` - For displaying incorrect item dialogue
- `OnForbiddenItemSlotted` - For displaying forbidden item dialogue
- `OnItemSlotCleared` - For resetting to default dialogue
### 5. WaitOnCombination Node
This node waits for the player to create a specific item through the combination system.
**Key features:**
- Automatically advances when the player creates the specified item through combination
- Shows dialogue while waiting for the item combination
- Visual prompt appears when the item is created, indicating available dialogue
![WaitOnCombination Node Example](media/wait_on_combination_node.png)
**Implementation details:**
- Defined as `RuntimeDialogueNodeType.WaitOnCombination` in `RuntimeDialogueGraph.cs`
- Links to `ItemManager.OnItemsCombined` event through the `combinationResultItemID` property
- The `DialogueComponent` listens for item combination events through `OnAnyItemsCombined` method
### 6. End Node
Terminates the dialogue sequence.
**Key features:**
- Marks the dialogue as completed
- No further interaction available until the dialogue is restarted
![End Node Example](media/end_node.png)
**Implementation details:**
- Defined as `RuntimeDialogueNodeType.End` in `RuntimeDialogueGraph.cs`
- When reached, sets the `IsCompleted` flag on the `DialogueComponent`
- No next node connection is required for this node type
## Dialogue Editor
The dialogue editor is a custom Unity tool that allows for visual creation and editing of dialogue graphs.
![Dialogue Editor Interface](media/dialogue_editor_interface.png)
### Key Editor Features
- **Visual Node Editing**: Add and connect nodes in a visual graph
- **Node Type Selection**: Choose from the six supported node types
- **Dialogue Text Entry**: Add multiple lines of dialogue for each node
- **Condition Setup**: Specify condition IDs for conditional nodes
- **Node Connections**: Create the flow between dialogue nodes
### Editor Workflow
1. **Create New Graph**: Right-click in Project view and select Create > Dialogue Graph
2. **Open Editor**: Double-click the created asset to open the dialogue editor
3. **Add Nodes**: Right-click in the editor and select Add Node > [Node Type]
4. **Configure Nodes**: Enter dialogue text and set conditions as needed
5. **Connect Nodes**: Drag from output ports to input ports to create connections
6. **Set Entry Node**: Mark one node as the entry point for the dialogue
7. **Save**: Save your dialogue graph when finished
## Designer Tips
1. **Node Organization**
- Start every dialogue graph with a standard Dialogue node as the entry point
- End every dialogue path with an End node to properly terminate the conversation
- Use conditional nodes strategically to create gameplay-driven dialogue experiences
2. **Dialogue Writing**
- Keep individual dialogue lines concise for better readability
- Consider using the looping option for nodes when you want to repeat information
- For WaitOnSlot nodes, write unique dialogue for incorrect/forbidden items to provide clear feedback
3. **Flow Control**
- Ensure all nodes (except End nodes) have a valid next node specified
- Test dialogue paths to verify all conditions can be met during gameplay
- Consider using multiple dialogue lines within a single node rather than creating separate nodes for sequential lines
4. **Best Practices**
- Name your nodes descriptively in the editor for easier management
- Group related dialogue sequences into separate dialogue graphs
- Use the speaker name field to clearly identify who is speaking
## Technical Details
### Public Events and APIs
The dialogue system exposes several events that can be used by other systems:
#### DialogueComponent
- **Events**:
- `OnDialogueChanged`: Triggered when the dialogue text changes
- **Properties**:
- `IsActive`: Indicates whether the dialogue is currently active
- `IsCompleted`: Indicates whether the dialogue has reached an End node
- `CurrentSpeakerName`: Returns the name of the current speaker
- **Public Methods**:
- `StartDialogue()`: Initiates the dialogue from the beginning
- `GetCurrentDialogueLine()`: Retrieves the current dialogue line text
- `HasAnyLines()`: Checks if the dialogue component has any lines available
- `SetDialogueGraph(RuntimeDialogueGraph)`: Sets the dialogue graph for the component
#### SpeechBubble
- **Public Methods**:
- `Show()`: Makes the speech bubble visible
- `Hide()`: Hides the speech bubble
- `Toggle()`: Toggles the visibility of the speech bubble
- `SetText(string)`: Sets the text displayed in the speech bubble
- `DisplayDialogueLine(string, bool)`: Displays a dialogue line and handles prompt visibility
- `UpdatePromptVisibility(bool)`: Updates the speech bubble to show a prompt or hide based on dialogue availability
- `SetDisplayMode(TextDisplayMode)`: Changes how text is displayed (instant or typewriter)
- `SkipTypewriter()`: Immediately displays the full text, skipping the typewriter effect
- `SetTypewriterSpeed(float)`: Sets the speed of the typewriter effect
### Integration with Other Systems
The dialogue system integrates with several other game systems:
1. **Puzzle System**: Monitors puzzle completion events to advance WaitOnPuzzleStep nodes
2. **Item System**: Tracks item pickups, combinations, and slot interactions to advance respective node types
3. **Interaction System**: Responds to player interaction with the NPC to progress through dialogue lines
### Technical Workflow
1. Create a RuntimeDialogueGraph asset in the Unity editor
2. Add nodes and connections using the dialogue editor
3. Assign the graph to a DialogueComponent on an NPC GameObject
4. Ensure a SpeechBubble component is available (as a child object or referenced)
5. Set up any necessary puzzle steps, items, or slots that the dialogue will reference

View File

@@ -0,0 +1,275 @@
# Apple Hills Interaction System
This document provides a comprehensive overview of the interaction system in Apple Hills, detailing how interactions are structured, configured, and extended with custom actions.
## Overview
The Apple Hills interaction system allows players to interact with objects in the game world. It supports character movement to interaction points, timed and conditional interactions, and complex behaviors through a component-based architecture. The system is particularly powerful when combined with the Timeline feature for creating cinematic sequences during interactions.
## Core Components
The interaction system consists of several key components that work together:
### Interactable
The `Interactable` component is the foundation of the interaction system. It:
- Handles player input (tapping/clicking)
- Manages which character(s) should interact (Trafalgar, Pulver, or both)
- Coordinates character movement to interaction points
- Dispatches events during the interaction lifecycle
- Manages one-time interactions and cooldowns
![Interactable Inspector](media/interactable_inspector.png)
### CharacterMoveToTarget
The `CharacterMoveToTarget` component defines positions where characters should move when interacting:
- Can be configured for specific characters (Trafalgar, Pulver, or both)
- Supports position offsets for fine-tuning
- Provides visual gizmos in the editor for easy positioning
- Multiple targets can be set up for complex interactions
![Character Move Target Inspector](media/character_move_target_inspector.png)
### Interaction Actions
Actions are components that respond to interaction events and execute custom behavior:
- Derive from the abstract `InteractionActionBase` class
- Can be attached to interactable objects
- Multiple actions can be added to a single interactable
- Actions can optionally block the interaction flow until completion
The inspector for all interaction action components shows the key parameters from InteractionActionBase, with custom configuration options for specific action types:
![InteractionTimelineAction Inspector](media/interaction_timeline_action_inspector.png)
### Interaction Requirements
Requirements are components that determine whether an interaction can occur:
- Derive from the abstract `InteractionRequirementBase` class
- Can prevent interactions based on custom conditions
- Multiple requirements can be added to a single interactable
- Used for creating conditional interactions (e.g., requiring an item)
## Interaction Event Flow
Interactions follow a defined event flow:
1. **InteractionStarted**: Triggered when the player initiates an interaction
2. **PlayerArrived**: Triggered when the player character reaches the interaction point
3. **InteractingCharacterArrived**: Triggered when the interacting character (often Pulver) reaches the interaction point
4. **InteractionComplete**: Triggered when the interaction is completed
5. **InteractionInterrupted**: Triggered if the interaction is interrupted before completion
## InteractionActionBase
The `InteractionActionBase` is the abstract base class for all interaction actions. It provides the framework for creating custom behaviors that respond to interaction events.
### Key Features
- **Event Filtering**: Actions can choose which interaction events to respond to
- **Flow Control**: Actions can optionally pause the interaction flow until completion
- **Asynchronous Execution**: Actions use `async/await` pattern for time-consuming operations
- **Character References**: Actions receive references to both player and follower characters
### Implementation
```csharp
public abstract class InteractionActionBase : MonoBehaviour
{
// Which events this action should respond to
public List<InteractionEventType> respondToEvents;
// Whether to pause the interaction flow during execution
public bool pauseInteractionFlow;
// The main execution method that must be implemented by derived classes
protected abstract Task<bool> ExecuteAsync(
InteractionEventType eventType,
PlayerTouchController player,
FollowerController follower);
// Optional method for adding execution conditions
protected virtual bool ShouldExecute(
InteractionEventType eventType,
PlayerTouchController player,
FollowerController follower)
{
return true;
}
}
```
## InteractionTimelineAction
The `InteractionTimelineAction` is a powerful action that plays Unity Timeline sequences during interactions. It enables cinematic sequences, character animations, camera movements, and more.
### Key Features
- **Multiple Timeline Support**: Can play different timelines for different interaction events
- **Timeline Sequences**: Can play multiple timelines in sequence for a single event
- **Character Binding**: Automatically binds player and follower characters to timeline tracks
- **Flow Control**: Waits for timeline completion before continuing interaction flow
- **Timeout Safety**: Includes a configurable timeout to prevent interactions from getting stuck
- **Looping Options**: Supports looping all timelines or just the last timeline in a sequence
### Timeline Event Mapping
Each mapping connects an interaction event to one or more timeline assets:
```csharp
public class TimelineEventMapping
{
// The event that triggers this timeline
public InteractionEventType eventType;
// The timeline assets to play (in sequence)
public PlayableAsset[] timelines;
// Character binding options
public bool bindPlayerCharacter;
public bool bindPulverCharacter;
public string playerTrackName = "Player";
public string pulverTrackName = "Pulver";
// Playback options
public float timeoutSeconds = 30f;
public bool loopLast = false;
public bool loopAll = false;
}
```
### Custom Editor
The `InteractionTimelineAction` includes a custom editor that makes it easy to configure:
- Quick buttons to add mappings for common events
- Character binding options
- Timeline sequence configuration
- Validation warnings for misconfigured timelines
![Timeline Mapping Editor](media/timeline_mapping_editor.png)
### Implementation Pattern
For a cinematic interaction with timelines:
1. Add an `Interactable` component to your object
2. Add an `InteractionTimelineAction` component to the same object
3. Set up character move targets if needed
4. Create timeline assets for each interaction phase
5. Configure the timeline mappings in the inspector
6. Test the interaction in play mode
### Timeline Configuration
When setting up a timeline for interaction, you'll need to create a Timeline asset and configure it in Unity's Timeline editor:
![Timeline Editor](media/timeline_editor.png)
## Working with the Interactable Editor
The `Interactable` component includes a custom editor that enhances the workflow:
### Character Move Target Creation
The editor provides buttons to easily create character move targets:
- "Add Trafalgar Target" - Creates a move target for the player character
- "Add Pulver Target" - Creates a move target for the follower character
- "Add Both Characters Target" - Creates a move target for both characters
![Interactable Custom Editor](media/interactable_inspector.png)
### Target Visualization
The editor displays the number of targets for each character type and warns about potential conflicts:
```
Trafalgar Targets: 1, Pulver Targets: 1, Both Targets: 0
```
If multiple conflicting targets are detected, a warning is displayed to help prevent unexpected behavior.
## Best Practices
### Target Positioning
- Place character targets with appropriate spacing to prevent characters from overlapping
- Consider the character's facing direction (targets automatically make characters face the interactable)
- Use the position offset for fine-tuning without moving the actual target GameObject
![Target Positioning In Scene](media/target_positioning_scene.png)
### Timeline Design
- Keep timelines modular and focused on specific actions
- Use signals to trigger game events from timelines
- Consider using director notification tracks for advanced timeline integration
- Test timelines with actual characters to ensure animations blend correctly
### Action Combinations
- Combine multiple actions for complex behaviors (e.g., dialogue + timeline)
- Order actions in the Inspector to control execution priority
- Use the `pauseInteractionFlow` option strategically to control sequence flow
## Technical Reference
### InteractionEventType
```csharp
public enum InteractionEventType
{
InteractionStarted, // When interaction is first triggered
PlayerArrived, // When player arrives at interaction point
InteractingCharacterArrived, // When interacting character arrives
InteractionComplete, // When interaction is successfully completed
InteractionInterrupted // When interaction is interrupted
}
```
### CharacterToInteract
```csharp
public enum CharacterToInteract
{
None, // No character interactions
Trafalgar, // Player character only
Pulver, // Follower character only
Both // Both characters
}
```
## Troubleshooting
### Common Issues
- **Characters not moving to targets**: Ensure targets have the correct CharacterToInteract type set
- **Timeline not playing**: Check that the PlayableDirector has a reference to the timeline asset
- **Characters not appearing in timeline**: Verify the track names match the binding configuration
- **Interaction getting stuck**: Make sure timelines have a reasonable timeout value set
- **Multiple timelines playing simultaneously**: Check that the event mappings are correctly configured
## Setup Example
Here's how a typical interaction setup might look in the Inspector:
1. Interactable component with appropriate settings
2. Character move targets positioned in the scene
3. InteractionTimelineAction component configured with timeline mappings
4. PlayableDirector component referencing timeline assets
## Advanced Topics
### Timeline Integration with Dialogue
Timeline actions can be synchronized with dialogue using:
- Animation tracks to trigger dialogue displays
- Signal tracks to advance dialogue
- Custom markers to synchronize dialogue with character animations
### Interaction Sequences
Complex interaction sequences can be created by:
- Using multiple interactables in sequence
- Enabling/disabling interactables based on game state
- Using timelines to guide the player through sequential interactions

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

BIN
docs/media/end_node.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB