Working state for minigameobstacles

This commit is contained in:
2025-09-17 16:10:18 +02:00
committed by Michal Pikulski
parent 2ec5c3d855
commit 50070651c5
26 changed files with 4573 additions and 19 deletions

2
.gitignore vendored
View File

@@ -99,6 +99,8 @@ InitTestScene*.unity*
# Auto-generated scenes by play mode tests # Auto-generated scenes by play mode tests
/[Aa]ssets/[Ii]nit[Tt]est[Ss]cene*.unity* /[Aa]ssets/[Ii]nit[Tt]est[Ss]cene*.unity*
.idea/.idea.AppleHillsProduction/.idea/indexLayout.xml
.vscode/extensions.json .vscode/extensions.json
.vscode/launch.json .vscode/launch.json
.vscode/settings.json .vscode/settings.json
.idea/.idea.AppleHillsProduction/.idea/indexLayout.xml

View File

@@ -43,6 +43,9 @@ namespace Editor.Utilities
[Tooltip("Color used for previewing colliders in the scene view")] [Tooltip("Color used for previewing colliders in the scene view")]
private Color previewColor = new Color(0.2f, 1f, 0.3f, 0.5f); private Color previewColor = new Color(0.2f, 1f, 0.3f, 0.5f);
[Tooltip("Layer to assign to GameObjects when colliders are generated")]
private int targetLayer = 0;
private List<Mesh> previewMeshes = new List<Mesh>(); private List<Mesh> previewMeshes = new List<Mesh>();
[MenuItem("Tools/Sprite Collider Generator")] [MenuItem("Tools/Sprite Collider Generator")]
@@ -51,8 +54,29 @@ namespace Editor.Utilities
GetWindow<SpriteColliderGenerator>("Sprite Collider Generator"); GetWindow<SpriteColliderGenerator>("Sprite Collider Generator");
} }
private void OnEnable()
{
// Subscribe to scene change events to clear invalid object references
UnityEditor.SceneManagement.EditorSceneManager.sceneOpened += OnSceneOpened;
UnityEditor.SceneManagement.EditorSceneManager.sceneClosed += OnSceneClosed;
// Also subscribe to playmode changes as they can invalidate references
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
// Subscribe to prefab stage changes (Unity 2018.3+)
UnityEditor.SceneManagement.PrefabStage.prefabStageOpened += OnPrefabStageOpened;
UnityEditor.SceneManagement.PrefabStage.prefabStageClosing += OnPrefabStageClosing;
}
private void OnDisable() private void OnDisable()
{ {
// Unsubscribe from events
UnityEditor.SceneManagement.EditorSceneManager.sceneOpened -= OnSceneOpened;
UnityEditor.SceneManagement.EditorSceneManager.sceneClosed -= OnSceneClosed;
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
UnityEditor.SceneManagement.PrefabStage.prefabStageOpened -= OnPrefabStageOpened;
UnityEditor.SceneManagement.PrefabStage.prefabStageClosing -= OnPrefabStageClosing;
// Clean up any preview meshes when window is closed // Clean up any preview meshes when window is closed
foreach (var mesh in previewMeshes) foreach (var mesh in previewMeshes)
{ {
@@ -64,6 +88,52 @@ namespace Editor.Utilities
previewMeshes.Clear(); previewMeshes.Clear();
} }
private void OnSceneOpened(UnityEngine.SceneManagement.Scene scene, UnityEditor.SceneManagement.OpenSceneMode mode)
{
// Clear selected objects when a scene is opened
ClearInvalidReferences();
}
private void OnSceneClosed(UnityEngine.SceneManagement.Scene scene)
{
// Clear selected objects when a scene is closed
ClearInvalidReferences();
}
private void OnPlayModeStateChanged(PlayModeStateChange state)
{
// Clear references when entering/exiting play mode as they become invalid
if (state == PlayModeStateChange.ExitingEditMode || state == PlayModeStateChange.ExitingPlayMode)
{
ClearInvalidReferences();
}
}
private void OnPrefabStageOpened(UnityEditor.SceneManagement.PrefabStage stage)
{
// Clear selected objects when entering a prefab stage
ClearInvalidReferences();
}
private void OnPrefabStageClosing(UnityEditor.SceneManagement.PrefabStage stage)
{
// Clear selected objects when exiting a prefab stage
ClearInvalidReferences();
}
/// <summary>
/// Clears invalid GameObject references from the selected objects list
/// </summary>
private void ClearInvalidReferences()
{
if (selectedObjects.Count > 0)
{
selectedObjects.Clear();
ClearPreviews(); // Also clear any preview meshes
Repaint(); // Refresh the window UI
}
}
private void OnGUI() private void OnGUI()
{ {
EditorGUILayout.BeginVertical(); EditorGUILayout.BeginVertical();
@@ -146,6 +216,12 @@ namespace Editor.Utilities
new GUIContent("Generate Trigger Colliders", "When enabled, creates trigger colliders instead of solid colliders."), new GUIContent("Generate Trigger Colliders", "When enabled, creates trigger colliders instead of solid colliders."),
generateTriggerColliders); generateTriggerColliders);
// Layer selection
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField(new GUIContent("Target Layer:", "Layer to assign to GameObjects when colliders are generated. Leave as 'Nothing' to keep current layer."), GUILayout.Width(180));
targetLayer = EditorGUILayout.LayerField(targetLayer);
EditorGUILayout.EndHorizontal();
// Offset option // Offset option
offsetFromCenter = EditorGUILayout.Toggle( offsetFromCenter = EditorGUILayout.Toggle(
new GUIContent("Offset From Center", "When enabled, allows scaling the collider outward or inward from the sprite center."), new GUIContent("Offset From Center", "When enabled, allows scaling the collider outward or inward from the sprite center."),
@@ -445,9 +521,25 @@ namespace Editor.Utilities
polygonCollider.SetPath(i, paths[i]); polygonCollider.SetPath(i, paths[i]);
} }
// Set the layer on the GameObject if a specific layer is selected
SetTargetLayer(targetObject);
return true; return true;
} }
/// <summary>
/// Sets the target layer on the GameObject if a layer is selected
/// </summary>
/// <param name="targetObject">The GameObject to set the layer on</param>
private void SetTargetLayer(GameObject targetObject)
{
if (targetLayer != 0)
{
Undo.RecordObject(targetObject, "Set GameObject Layer");
targetObject.layer = targetLayer;
}
}
private List<Vector2[]> GetSpritePaths(Sprite sprite, float tolerance) private List<Vector2[]> GetSpritePaths(Sprite sprite, float tolerance)
{ {
List<Vector2[]> result = new List<Vector2[]>(); List<Vector2[]> result = new List<Vector2[]>();

View File

@@ -10,8 +10,8 @@ GameObject:
m_Component: m_Component:
- component: {fileID: 7111145574660306503} - component: {fileID: 7111145574660306503}
- component: {fileID: 3889795708575321074} - component: {fileID: 3889795708575321074}
- component: {fileID: 7249681423942450184} - component: {fileID: 8688779957837852254}
m_Layer: 0 m_Layer: 6
m_Name: Left_Tile2_0 m_Name: Left_Tile2_0
m_TagString: Untagged m_TagString: Untagged
m_Icon: {fileID: 0} m_Icon: {fileID: 0}
@@ -88,7 +88,7 @@ SpriteRenderer:
m_WasSpriteAssigned: 1 m_WasSpriteAssigned: 1
m_MaskInteraction: 0 m_MaskInteraction: 0
m_SpriteSortPoint: 0 m_SpriteSortPoint: 0
--- !u!60 &7249681423942450184 --- !u!60 &8688779957837852254
PolygonCollider2D: PolygonCollider2D:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0} m_CorrespondingSourceObject: {fileID: 0}
@@ -155,8 +155,8 @@ GameObject:
m_Component: m_Component:
- component: {fileID: 1003080013996268193} - component: {fileID: 1003080013996268193}
- component: {fileID: 4856205316150460481} - component: {fileID: 4856205316150460481}
- component: {fileID: 2843103852598642252} - component: {fileID: 8615330168962481848}
m_Layer: 0 m_Layer: 6
m_Name: Right_Tile1_0 m_Name: Right_Tile1_0
m_TagString: Untagged m_TagString: Untagged
m_Icon: {fileID: 0} m_Icon: {fileID: 0}
@@ -233,7 +233,7 @@ SpriteRenderer:
m_WasSpriteAssigned: 1 m_WasSpriteAssigned: 1
m_MaskInteraction: 0 m_MaskInteraction: 0
m_SpriteSortPoint: 0 m_SpriteSortPoint: 0
--- !u!60 &2843103852598642252 --- !u!60 &8615330168962481848
PolygonCollider2D: PolygonCollider2D:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0} m_CorrespondingSourceObject: {fileID: 0}
@@ -282,8 +282,6 @@ PolygonCollider2D:
- - {x: -0.48499998, y: 2.5} - - {x: -0.48499998, y: 2.5}
- {x: -0.49499997, y: 2.19} - {x: -0.49499997, y: 2.19}
- {x: 0.035, y: 1.66} - {x: 0.035, y: 1.66}
- {x: 0.035, y: 1.42}
- {x: -0.24499999, y: 1.15}
- {x: -0.285, y: 0.90999997} - {x: -0.285, y: 0.90999997}
- {x: -0.13499999, y: 0.45999998} - {x: -0.13499999, y: 0.45999998}
- {x: -1.115, y: -0.03} - {x: -1.115, y: -0.03}

View File

@@ -10,7 +10,8 @@ GameObject:
m_Component: m_Component:
- component: {fileID: 7111145574660306503} - component: {fileID: 7111145574660306503}
- component: {fileID: 3889795708575321074} - component: {fileID: 3889795708575321074}
m_Layer: 0 - component: {fileID: 8698932925238153222}
m_Layer: 6
m_Name: Left_Tile2_0 m_Name: Left_Tile2_0
m_TagString: Untagged m_TagString: Untagged
m_Icon: {fileID: 0} m_Icon: {fileID: 0}
@@ -87,6 +88,63 @@ SpriteRenderer:
m_WasSpriteAssigned: 1 m_WasSpriteAssigned: 1
m_MaskInteraction: 0 m_MaskInteraction: 0
m_SpriteSortPoint: 0 m_SpriteSortPoint: 0
--- !u!60 &8698932925238153222
PolygonCollider2D:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 864595161669782950}
m_Enabled: 1
serializedVersion: 3
m_Density: 1
m_Material: {fileID: 0}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
m_ForceSendLayers:
serializedVersion: 2
m_Bits: 4294967295
m_ForceReceiveLayers:
serializedVersion: 2
m_Bits: 4294967295
m_ContactCaptureLayers:
serializedVersion: 2
m_Bits: 4294967295
m_CallbackLayers:
serializedVersion: 2
m_Bits: 4294967295
m_IsTrigger: 0
m_UsedByEffector: 0
m_CompositeOperation: 0
m_CompositeOrder: 0
m_Offset: {x: 0, y: 0}
m_SpriteTilingProperty:
border: {x: 0, y: 0, z: 0, w: 0}
pivot: {x: 0.5, y: 0.5}
oldSize: {x: 2.37, y: 5}
newSize: {x: 2.37, y: 5}
adaptiveTilingThreshold: 0.5
drawMode: 0
adaptiveTiling: 0
m_AutoTiling: 0
m_Points:
m_Paths:
- - {x: 0.575, y: -1.5}
- {x: 0.185, y: -0.63}
- {x: 1.145, y: 0.85999995}
- {x: 1.185, y: 1.41}
- {x: 0.835, y: 1.8499999}
- {x: 0.635, y: 2.5}
- {x: -1.185, y: 2.5}
- {x: -1.185, y: -2.5}
- {x: 0.635, y: -2.5}
- {x: 0.625, y: -1.64}
m_UseDelaunayMesh: 0
--- !u!1 &2171518497100337372 --- !u!1 &2171518497100337372
GameObject: GameObject:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@@ -97,7 +155,8 @@ GameObject:
m_Component: m_Component:
- component: {fileID: 1003080013996268193} - component: {fileID: 1003080013996268193}
- component: {fileID: 4856205316150460481} - component: {fileID: 4856205316150460481}
m_Layer: 0 - component: {fileID: 1368284191430742655}
m_Layer: 6
m_Name: Right_Tile1_0 m_Name: Right_Tile1_0
m_TagString: Untagged m_TagString: Untagged
m_Icon: {fileID: 0} m_Icon: {fileID: 0}
@@ -174,6 +233,67 @@ SpriteRenderer:
m_WasSpriteAssigned: 1 m_WasSpriteAssigned: 1
m_MaskInteraction: 0 m_MaskInteraction: 0
m_SpriteSortPoint: 0 m_SpriteSortPoint: 0
--- !u!60 &1368284191430742655
PolygonCollider2D:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2171518497100337372}
m_Enabled: 1
serializedVersion: 3
m_Density: 1
m_Material: {fileID: 0}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
m_ForceSendLayers:
serializedVersion: 2
m_Bits: 4294967295
m_ForceReceiveLayers:
serializedVersion: 2
m_Bits: 4294967295
m_ContactCaptureLayers:
serializedVersion: 2
m_Bits: 4294967295
m_CallbackLayers:
serializedVersion: 2
m_Bits: 4294967295
m_IsTrigger: 0
m_UsedByEffector: 0
m_CompositeOperation: 0
m_CompositeOrder: 0
m_Offset: {x: 0, y: 0}
m_SpriteTilingProperty:
border: {x: 0, y: 0, z: 0, w: 0}
pivot: {x: 0.5, y: 0.5}
oldSize: {x: 2.65, y: 5}
newSize: {x: 2.65, y: 5}
adaptiveTilingThreshold: 0.5
drawMode: 0
adaptiveTiling: 0
m_AutoTiling: 0
m_Points:
m_Paths:
- - {x: -0.48499998, y: 2.5}
- {x: -0.49499997, y: 2.19}
- {x: 0.035, y: 1.66}
- {x: -0.285, y: 0.90999997}
- {x: -0.13499999, y: 0.45999998}
- {x: -1.115, y: -0.03}
- {x: -1.3249999, y: -0.35}
- {x: -1.2049999, y: -0.84999996}
- {x: -0.36499998, y: -1.0699999}
- {x: -0.585, y: -1.27}
- {x: -0.625, y: -1.65}
- {x: -0.48499998, y: -2.5}
- {x: 1.3249999, y: -2.5}
- {x: 1.3249999, y: 2.5}
m_UseDelaunayMesh: 0
--- !u!1 &2956826569642009690 --- !u!1 &2956826569642009690
GameObject: GameObject:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0

View File

@@ -10,7 +10,8 @@ GameObject:
m_Component: m_Component:
- component: {fileID: 7111145574660306503} - component: {fileID: 7111145574660306503}
- component: {fileID: 3889795708575321074} - component: {fileID: 3889795708575321074}
m_Layer: 0 - component: {fileID: 6919687589473331973}
m_Layer: 6
m_Name: Left_Tile2_0 m_Name: Left_Tile2_0
m_TagString: Untagged m_TagString: Untagged
m_Icon: {fileID: 0} m_Icon: {fileID: 0}
@@ -87,6 +88,72 @@ SpriteRenderer:
m_WasSpriteAssigned: 1 m_WasSpriteAssigned: 1
m_MaskInteraction: 0 m_MaskInteraction: 0
m_SpriteSortPoint: 0 m_SpriteSortPoint: 0
--- !u!60 &6919687589473331973
PolygonCollider2D:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 864595161669782950}
m_Enabled: 1
serializedVersion: 3
m_Density: 1
m_Material: {fileID: 0}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
m_ForceSendLayers:
serializedVersion: 2
m_Bits: 4294967295
m_ForceReceiveLayers:
serializedVersion: 2
m_Bits: 4294967295
m_ContactCaptureLayers:
serializedVersion: 2
m_Bits: 4294967295
m_CallbackLayers:
serializedVersion: 2
m_Bits: 4294967295
m_IsTrigger: 0
m_UsedByEffector: 0
m_CompositeOperation: 0
m_CompositeOrder: 0
m_Offset: {x: 0, y: 0}
m_SpriteTilingProperty:
border: {x: 0, y: 0, z: 0, w: 0}
pivot: {x: 0.5, y: 0.5}
oldSize: {x: 2.65, y: 5}
newSize: {x: 2.37, y: 5}
adaptiveTilingThreshold: 0.5
drawMode: 0
adaptiveTiling: 0
m_AutoTiling: 0
m_Points:
m_Paths:
- - {x: 0.635, y: -1.78}
- {x: 0.585, y: -1.27}
- {x: 0.36499998, y: -1.14}
- {x: 0.415, y: -1.05}
- {x: 1.2149999, y: -0.85999995}
- {x: 1.3249999, y: -0.7}
- {x: 1.3249999, y: -0.35}
- {x: 1.145, y: -0.049999997}
- {x: 0.13499999, y: 0.45999998}
- {x: 0.275, y: 0.87}
- {x: 0.265, y: 1.0699999}
- {x: -0.044999998, y: 1.41}
- {x: -0.035, y: 1.66}
- {x: 0.48499998, y: 2.2}
- {x: 0.49499997, y: 2.48}
- {x: -1.3249999, y: 2.5}
- {x: -1.3249999, y: -2.5}
- {x: 0.505, y: -2.5}
- {x: 0.555, y: -2.12}
m_UseDelaunayMesh: 0
--- !u!1 &2171518497100337372 --- !u!1 &2171518497100337372
GameObject: GameObject:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@@ -97,7 +164,8 @@ GameObject:
m_Component: m_Component:
- component: {fileID: 1003080013996268193} - component: {fileID: 1003080013996268193}
- component: {fileID: 4856205316150460481} - component: {fileID: 4856205316150460481}
m_Layer: 0 - component: {fileID: 1911291775322313535}
m_Layer: 6
m_Name: Right_Tile1_0 m_Name: Right_Tile1_0
m_TagString: Untagged m_TagString: Untagged
m_Icon: {fileID: 0} m_Icon: {fileID: 0}
@@ -174,6 +242,65 @@ SpriteRenderer:
m_WasSpriteAssigned: 1 m_WasSpriteAssigned: 1
m_MaskInteraction: 0 m_MaskInteraction: 0
m_SpriteSortPoint: 0 m_SpriteSortPoint: 0
--- !u!60 &1911291775322313535
PolygonCollider2D:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2171518497100337372}
m_Enabled: 1
serializedVersion: 3
m_Density: 1
m_Material: {fileID: 0}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
m_ForceSendLayers:
serializedVersion: 2
m_Bits: 4294967295
m_ForceReceiveLayers:
serializedVersion: 2
m_Bits: 4294967295
m_ContactCaptureLayers:
serializedVersion: 2
m_Bits: 4294967295
m_CallbackLayers:
serializedVersion: 2
m_Bits: 4294967295
m_IsTrigger: 0
m_UsedByEffector: 0
m_CompositeOperation: 0
m_CompositeOrder: 0
m_Offset: {x: 0, y: 0}
m_SpriteTilingProperty:
border: {x: 0, y: 0, z: 0, w: 0}
pivot: {x: 0.5, y: 0.5}
oldSize: {x: 2.37, y: 5}
newSize: {x: 2.65, y: 5}
adaptiveTilingThreshold: 0.5
drawMode: 0
adaptiveTiling: 0
m_AutoTiling: 0
m_Points:
m_Paths:
- - {x: -0.635, y: 2.5}
- {x: -0.835, y: 1.8499999}
- {x: -1.185, y: 1.42}
- {x: -1.185, y: 1.06}
- {x: -1.055, y: 0.69}
- {x: -0.185, y: -0.65999997}
- {x: -0.175, y: -0.78999996}
- {x: -0.585, y: -1.49}
- {x: -0.675, y: -2.08}
- {x: -0.635, y: -2.5}
- {x: 1.185, y: -2.5}
- {x: 1.185, y: 2.5}
m_UseDelaunayMesh: 0
--- !u!1 &2956826569642009690 --- !u!1 &2956826569642009690
GameObject: GameObject:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0

View File

@@ -10,7 +10,8 @@ GameObject:
m_Component: m_Component:
- component: {fileID: 7111145574660306503} - component: {fileID: 7111145574660306503}
- component: {fileID: 3889795708575321074} - component: {fileID: 3889795708575321074}
m_Layer: 0 - component: {fileID: 3320970211105392876}
m_Layer: 6
m_Name: Left_Tile2_0 m_Name: Left_Tile2_0
m_TagString: Untagged m_TagString: Untagged
m_Icon: {fileID: 0} m_Icon: {fileID: 0}
@@ -87,6 +88,72 @@ SpriteRenderer:
m_WasSpriteAssigned: 1 m_WasSpriteAssigned: 1
m_MaskInteraction: 0 m_MaskInteraction: 0
m_SpriteSortPoint: 0 m_SpriteSortPoint: 0
--- !u!60 &3320970211105392876
PolygonCollider2D:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 864595161669782950}
m_Enabled: 1
serializedVersion: 3
m_Density: 1
m_Material: {fileID: 0}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
m_ForceSendLayers:
serializedVersion: 2
m_Bits: 4294967295
m_ForceReceiveLayers:
serializedVersion: 2
m_Bits: 4294967295
m_ContactCaptureLayers:
serializedVersion: 2
m_Bits: 4294967295
m_CallbackLayers:
serializedVersion: 2
m_Bits: 4294967295
m_IsTrigger: 0
m_UsedByEffector: 0
m_CompositeOperation: 0
m_CompositeOrder: 0
m_Offset: {x: 0, y: 0}
m_SpriteTilingProperty:
border: {x: 0, y: 0, z: 0, w: 0}
pivot: {x: 0.5, y: 0.5}
oldSize: {x: 2.65, y: 5}
newSize: {x: 2.37, y: 5}
adaptiveTilingThreshold: 0.5
drawMode: 0
adaptiveTiling: 0
m_AutoTiling: 0
m_Points:
m_Paths:
- - {x: 0.635, y: -1.78}
- {x: 0.585, y: -1.27}
- {x: 0.36499998, y: -1.14}
- {x: 0.415, y: -1.05}
- {x: 1.2149999, y: -0.85999995}
- {x: 1.3249999, y: -0.7}
- {x: 1.3249999, y: -0.35}
- {x: 1.145, y: -0.049999997}
- {x: 0.13499999, y: 0.45999998}
- {x: 0.275, y: 0.87}
- {x: 0.265, y: 1.0699999}
- {x: -0.044999998, y: 1.41}
- {x: -0.035, y: 1.66}
- {x: 0.48499998, y: 2.2}
- {x: 0.49499997, y: 2.48}
- {x: -1.3249999, y: 2.5}
- {x: -1.3249999, y: -2.5}
- {x: 0.505, y: -2.5}
- {x: 0.555, y: -2.12}
m_UseDelaunayMesh: 0
--- !u!1 &2171518497100337372 --- !u!1 &2171518497100337372
GameObject: GameObject:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@@ -97,7 +164,8 @@ GameObject:
m_Component: m_Component:
- component: {fileID: 1003080013996268193} - component: {fileID: 1003080013996268193}
- component: {fileID: 4856205316150460481} - component: {fileID: 4856205316150460481}
m_Layer: 0 - component: {fileID: 3931598674086383704}
m_Layer: 6
m_Name: Right_Tile1_0 m_Name: Right_Tile1_0
m_TagString: Untagged m_TagString: Untagged
m_Icon: {fileID: 0} m_Icon: {fileID: 0}
@@ -174,6 +242,65 @@ SpriteRenderer:
m_WasSpriteAssigned: 1 m_WasSpriteAssigned: 1
m_MaskInteraction: 0 m_MaskInteraction: 0
m_SpriteSortPoint: 0 m_SpriteSortPoint: 0
--- !u!60 &3931598674086383704
PolygonCollider2D:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2171518497100337372}
m_Enabled: 1
serializedVersion: 3
m_Density: 1
m_Material: {fileID: 0}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
m_ForceSendLayers:
serializedVersion: 2
m_Bits: 4294967295
m_ForceReceiveLayers:
serializedVersion: 2
m_Bits: 4294967295
m_ContactCaptureLayers:
serializedVersion: 2
m_Bits: 4294967295
m_CallbackLayers:
serializedVersion: 2
m_Bits: 4294967295
m_IsTrigger: 0
m_UsedByEffector: 0
m_CompositeOperation: 0
m_CompositeOrder: 0
m_Offset: {x: 0, y: 0}
m_SpriteTilingProperty:
border: {x: 0, y: 0, z: 0, w: 0}
pivot: {x: 0.5, y: 0.5}
oldSize: {x: 2.37, y: 5}
newSize: {x: 2.65, y: 5}
adaptiveTilingThreshold: 0.5
drawMode: 0
adaptiveTiling: 0
m_AutoTiling: 0
m_Points:
m_Paths:
- - {x: -0.635, y: 2.5}
- {x: -0.835, y: 1.8499999}
- {x: -1.185, y: 1.42}
- {x: -1.185, y: 1.06}
- {x: -1.055, y: 0.69}
- {x: -0.185, y: -0.65999997}
- {x: -0.175, y: -0.78999996}
- {x: -0.585, y: -1.49}
- {x: -0.675, y: -2.08}
- {x: -0.635, y: -2.5}
- {x: 1.185, y: -2.5}
- {x: 1.185, y: 2.5}
m_UseDelaunayMesh: 0
--- !u!1 &2956826569642009690 --- !u!1 &2956826569642009690
GameObject: GameObject:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0

View File

@@ -10,7 +10,8 @@ GameObject:
m_Component: m_Component:
- component: {fileID: 7111145574660306503} - component: {fileID: 7111145574660306503}
- component: {fileID: 3889795708575321074} - component: {fileID: 3889795708575321074}
m_Layer: 0 - component: {fileID: 1709932086434338450}
m_Layer: 6
m_Name: Left_Tile2_0 m_Name: Left_Tile2_0
m_TagString: Untagged m_TagString: Untagged
m_Icon: {fileID: 0} m_Icon: {fileID: 0}
@@ -87,6 +88,72 @@ SpriteRenderer:
m_WasSpriteAssigned: 1 m_WasSpriteAssigned: 1
m_MaskInteraction: 0 m_MaskInteraction: 0
m_SpriteSortPoint: 0 m_SpriteSortPoint: 0
--- !u!60 &1709932086434338450
PolygonCollider2D:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 864595161669782950}
m_Enabled: 1
serializedVersion: 3
m_Density: 1
m_Material: {fileID: 0}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
m_ForceSendLayers:
serializedVersion: 2
m_Bits: 4294967295
m_ForceReceiveLayers:
serializedVersion: 2
m_Bits: 4294967295
m_ContactCaptureLayers:
serializedVersion: 2
m_Bits: 4294967295
m_CallbackLayers:
serializedVersion: 2
m_Bits: 4294967295
m_IsTrigger: 0
m_UsedByEffector: 0
m_CompositeOperation: 0
m_CompositeOrder: 0
m_Offset: {x: 0, y: 0}
m_SpriteTilingProperty:
border: {x: 0, y: 0, z: 0, w: 0}
pivot: {x: 0.5, y: 0.5}
oldSize: {x: 2.65, y: 5}
newSize: {x: 2.37, y: 5}
adaptiveTilingThreshold: 0.5
drawMode: 0
adaptiveTiling: 0
m_AutoTiling: 0
m_Points:
m_Paths:
- - {x: 0.635, y: -1.78}
- {x: 0.585, y: -1.27}
- {x: 0.36499998, y: -1.14}
- {x: 0.415, y: -1.05}
- {x: 1.2149999, y: -0.85999995}
- {x: 1.3249999, y: -0.7}
- {x: 1.3249999, y: -0.35}
- {x: 1.145, y: -0.049999997}
- {x: 0.13499999, y: 0.45999998}
- {x: 0.275, y: 0.87}
- {x: 0.265, y: 1.0699999}
- {x: -0.044999998, y: 1.41}
- {x: -0.035, y: 1.66}
- {x: 0.48499998, y: 2.2}
- {x: 0.49499997, y: 2.48}
- {x: -1.3249999, y: 2.5}
- {x: -1.3249999, y: -2.5}
- {x: 0.505, y: -2.5}
- {x: 0.555, y: -2.12}
m_UseDelaunayMesh: 0
--- !u!1 &2171518497100337372 --- !u!1 &2171518497100337372
GameObject: GameObject:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@@ -97,7 +164,8 @@ GameObject:
m_Component: m_Component:
- component: {fileID: 1003080013996268193} - component: {fileID: 1003080013996268193}
- component: {fileID: 4856205316150460481} - component: {fileID: 4856205316150460481}
m_Layer: 0 - component: {fileID: 9036823987933958098}
m_Layer: 6
m_Name: Right_Tile1_0 m_Name: Right_Tile1_0
m_TagString: Untagged m_TagString: Untagged
m_Icon: {fileID: 0} m_Icon: {fileID: 0}
@@ -174,6 +242,70 @@ SpriteRenderer:
m_WasSpriteAssigned: 1 m_WasSpriteAssigned: 1
m_MaskInteraction: 0 m_MaskInteraction: 0
m_SpriteSortPoint: 0 m_SpriteSortPoint: 0
--- !u!60 &9036823987933958098
PolygonCollider2D:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2171518497100337372}
m_Enabled: 1
serializedVersion: 3
m_Density: 1
m_Material: {fileID: 0}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
m_ForceSendLayers:
serializedVersion: 2
m_Bits: 4294967295
m_ForceReceiveLayers:
serializedVersion: 2
m_Bits: 4294967295
m_ContactCaptureLayers:
serializedVersion: 2
m_Bits: 4294967295
m_CallbackLayers:
serializedVersion: 2
m_Bits: 4294967295
m_IsTrigger: 0
m_UsedByEffector: 0
m_CompositeOperation: 0
m_CompositeOrder: 0
m_Offset: {x: 0, y: 0}
m_SpriteTilingProperty:
border: {x: 0, y: 0, z: 0, w: 0}
pivot: {x: 0.5, y: 0.5}
oldSize: {x: 2.65, y: 5}
newSize: {x: 2.65, y: 5}
adaptiveTilingThreshold: 0.5
drawMode: 0
adaptiveTiling: 0
m_AutoTiling: 0
m_Points:
m_Paths:
- - {x: -0.48499998, y: 2.5}
- {x: -0.49499997, y: 2.19}
- {x: 0.035, y: 1.66}
- {x: 0.035, y: 1.42}
- {x: -0.24499999, y: 1.15}
- {x: -0.285, y: 0.90999997}
- {x: -0.13499999, y: 0.45999998}
- {x: -1.115, y: -0.03}
- {x: -1.3249999, y: -0.35}
- {x: -1.3249999, y: -0.71}
- {x: -1.2049999, y: -0.84999996}
- {x: -0.36499998, y: -1.0699999}
- {x: -0.585, y: -1.27}
- {x: -0.625, y: -1.65}
- {x: -0.48499998, y: -2.5}
- {x: 1.3249999, y: -2.5}
- {x: 1.3249999, y: 2.5}
m_UseDelaunayMesh: 0
--- !u!1 &2956826569642009690 --- !u!1 &2956826569642009690
GameObject: GameObject:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0

View File

@@ -10,7 +10,8 @@ GameObject:
m_Component: m_Component:
- component: {fileID: 7111145574660306503} - component: {fileID: 7111145574660306503}
- component: {fileID: 3889795708575321074} - component: {fileID: 3889795708575321074}
m_Layer: 0 - component: {fileID: 6967660003360049346}
m_Layer: 6
m_Name: Left_Tile2_0 m_Name: Left_Tile2_0
m_TagString: Untagged m_TagString: Untagged
m_Icon: {fileID: 0} m_Icon: {fileID: 0}
@@ -87,6 +88,72 @@ SpriteRenderer:
m_WasSpriteAssigned: 1 m_WasSpriteAssigned: 1
m_MaskInteraction: 0 m_MaskInteraction: 0
m_SpriteSortPoint: 0 m_SpriteSortPoint: 0
--- !u!60 &6967660003360049346
PolygonCollider2D:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 864595161669782950}
m_Enabled: 1
serializedVersion: 3
m_Density: 1
m_Material: {fileID: 0}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
m_ForceSendLayers:
serializedVersion: 2
m_Bits: 4294967295
m_ForceReceiveLayers:
serializedVersion: 2
m_Bits: 4294967295
m_ContactCaptureLayers:
serializedVersion: 2
m_Bits: 4294967295
m_CallbackLayers:
serializedVersion: 2
m_Bits: 4294967295
m_IsTrigger: 0
m_UsedByEffector: 0
m_CompositeOperation: 0
m_CompositeOrder: 0
m_Offset: {x: 0, y: 0}
m_SpriteTilingProperty:
border: {x: 0, y: 0, z: 0, w: 0}
pivot: {x: 0.5, y: 0.5}
oldSize: {x: 2.65, y: 5}
newSize: {x: 2.37, y: 5}
adaptiveTilingThreshold: 0.5
drawMode: 0
adaptiveTiling: 0
m_AutoTiling: 0
m_Points:
m_Paths:
- - {x: 0.635, y: -1.78}
- {x: 0.585, y: -1.27}
- {x: 0.36499998, y: -1.14}
- {x: 0.415, y: -1.05}
- {x: 1.2149999, y: -0.85999995}
- {x: 1.3249999, y: -0.7}
- {x: 1.3249999, y: -0.35}
- {x: 1.145, y: -0.049999997}
- {x: 0.13499999, y: 0.45999998}
- {x: 0.275, y: 0.87}
- {x: 0.265, y: 1.0699999}
- {x: -0.044999998, y: 1.41}
- {x: -0.035, y: 1.66}
- {x: 0.48499998, y: 2.2}
- {x: 0.49499997, y: 2.48}
- {x: -1.3249999, y: 2.5}
- {x: -1.3249999, y: -2.5}
- {x: 0.505, y: -2.5}
- {x: 0.555, y: -2.12}
m_UseDelaunayMesh: 0
--- !u!1 &2171518497100337372 --- !u!1 &2171518497100337372
GameObject: GameObject:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@@ -97,7 +164,8 @@ GameObject:
m_Component: m_Component:
- component: {fileID: 1003080013996268193} - component: {fileID: 1003080013996268193}
- component: {fileID: 4856205316150460481} - component: {fileID: 4856205316150460481}
m_Layer: 0 - component: {fileID: 5503039050943823342}
m_Layer: 6
m_Name: Right_Tile1_0 m_Name: Right_Tile1_0
m_TagString: Untagged m_TagString: Untagged
m_Icon: {fileID: 0} m_Icon: {fileID: 0}
@@ -174,6 +242,70 @@ SpriteRenderer:
m_WasSpriteAssigned: 1 m_WasSpriteAssigned: 1
m_MaskInteraction: 0 m_MaskInteraction: 0
m_SpriteSortPoint: 0 m_SpriteSortPoint: 0
--- !u!60 &5503039050943823342
PolygonCollider2D:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2171518497100337372}
m_Enabled: 1
serializedVersion: 3
m_Density: 1
m_Material: {fileID: 0}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
m_ForceSendLayers:
serializedVersion: 2
m_Bits: 4294967295
m_ForceReceiveLayers:
serializedVersion: 2
m_Bits: 4294967295
m_ContactCaptureLayers:
serializedVersion: 2
m_Bits: 4294967295
m_CallbackLayers:
serializedVersion: 2
m_Bits: 4294967295
m_IsTrigger: 0
m_UsedByEffector: 0
m_CompositeOperation: 0
m_CompositeOrder: 0
m_Offset: {x: 0, y: 0}
m_SpriteTilingProperty:
border: {x: 0, y: 0, z: 0, w: 0}
pivot: {x: 0.5, y: 0.5}
oldSize: {x: 2.65, y: 5}
newSize: {x: 2.65, y: 5}
adaptiveTilingThreshold: 0.5
drawMode: 0
adaptiveTiling: 0
m_AutoTiling: 0
m_Points:
m_Paths:
- - {x: -0.48499998, y: 2.5}
- {x: -0.49499997, y: 2.19}
- {x: 0.035, y: 1.66}
- {x: 0.035, y: 1.42}
- {x: -0.24499999, y: 1.15}
- {x: -0.285, y: 0.90999997}
- {x: -0.13499999, y: 0.45999998}
- {x: -1.115, y: -0.03}
- {x: -1.3249999, y: -0.35}
- {x: -1.3249999, y: -0.71}
- {x: -1.2049999, y: -0.84999996}
- {x: -0.36499998, y: -1.0699999}
- {x: -0.585, y: -1.27}
- {x: -0.625, y: -1.65}
- {x: -0.48499998, y: -2.5}
- {x: 1.3249999, y: -2.5}
- {x: 1.3249999, y: 2.5}
m_UseDelaunayMesh: 0
--- !u!1 &2956826569642009690 --- !u!1 &2956826569642009690
GameObject: GameObject:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,233 @@
using UnityEngine;
using Pooling;
namespace Minigames.DivingForPictures
{
/// <summary>
/// Complete floating obstacle component that handles data, movement, collision detection, and pooling.
/// Obstacles move upward toward the surface and detect collisions with the player.
/// </summary>
public class FloatingObstacle : MonoBehaviour, IPoolable
{
[Header("Obstacle Properties")]
[Tooltip("Index of the prefab this obstacle was created from")]
[SerializeField] private int prefabIndex;
[Tooltip("Damage this obstacle deals to the player")]
[SerializeField] private float damage = 1f;
[Tooltip("Movement speed of this obstacle")]
[SerializeField] private float moveSpeed = 2f;
[Header("Movement")]
[Tooltip("Whether this obstacle moves (can be disabled for static obstacles)")]
[SerializeField] private bool enableMovement = true;
[Header("Collision Detection")]
[Tooltip("Layer mask for player detection - should match Player layer")]
[SerializeField] private LayerMask playerLayerMask = 1 << 7; // Player layer
[Tooltip("How often to check for collisions (in seconds)")]
[SerializeField] private float collisionCheckInterval = 0.1f;
[Header("References")]
[Tooltip("Reference to the spawner that created this obstacle")]
[SerializeField] private ObstacleSpawner spawner;
// Public properties
public int PrefabIndex
{
get => prefabIndex;
set => prefabIndex = value;
}
public float Damage
{
get => damage;
set => damage = value;
}
public float MoveSpeed
{
get => moveSpeed;
set => moveSpeed = value;
}
public bool HasDealtDamage => _hasDealtDamage;
// Private fields
private Collider2D _collider;
private float _collisionCheckTimer;
private bool _hasDealtDamage;
private Camera _mainCamera;
private float _screenTop;
private void Awake()
{
_collider = GetComponent<Collider2D>();
if (_collider == null)
{
_collider = GetComponentInChildren<Collider2D>();
}
if (_collider == null)
{
Debug.LogError($"[FloatingObstacle] No Collider2D found on {gameObject.name}!");
}
_mainCamera = Camera.main;
}
private void Update()
{
if (enableMovement)
{
HandleMovement();
}
HandleCollisionDetection();
CheckIfOffScreen();
}
/// <summary>
/// Moves the obstacle upward based on its speed
/// </summary>
private void HandleMovement()
{
transform.position += Vector3.up * (moveSpeed * Time.deltaTime);
}
/// <summary>
/// Checks for collisions with the player at regular intervals
/// </summary>
private void HandleCollisionDetection()
{
if (_hasDealtDamage || _collider == null) return;
_collisionCheckTimer -= Time.deltaTime;
if (_collisionCheckTimer <= 0f)
{
_collisionCheckTimer = collisionCheckInterval;
CheckForPlayerCollision();
}
}
/// <summary>
/// Checks if this obstacle is colliding with the player
/// </summary>
private void CheckForPlayerCollision()
{
Collider2D[] overlapping = new Collider2D[5];
ContactFilter2D filter = new ContactFilter2D();
filter.SetLayerMask(playerLayerMask);
filter.useTriggers = true;
int count = _collider.Overlap(filter, overlapping);
if (count > 0 && !_hasDealtDamage)
{
// Found collision with player
OnPlayerCollision(overlapping[0]);
}
}
/// <summary>
/// Called when this obstacle collides with the player
/// </summary>
/// <param name="playerCollider">The player's collider</param>
private void OnPlayerCollision(Collider2D playerCollider)
{
_hasDealtDamage = true;
// Trigger damage through events (following the existing pattern)
Debug.Log($"[FloatingObstacle] Obstacle dealt {damage} damage to player");
// Broadcast damage event using the static method
PlayerCollisionBehavior.TriggerDamageStart();
// Continue moving upward - don't destroy or stop the obstacle
Debug.Log($"[FloatingObstacle] Obstacle {gameObject.name} hit player and continues moving");
}
/// <summary>
/// Checks if the obstacle has moved off-screen and should be despawned
/// </summary>
private void CheckIfOffScreen()
{
if (_mainCamera == null) return;
// Calculate screen top if not cached
if (_screenTop == 0f)
{
Vector3 topWorldPoint = _mainCamera.ViewportToWorldPoint(new Vector3(0.5f, 1f, _mainCamera.nearClipPlane));
_screenTop = topWorldPoint.y;
}
// Check if obstacle is above screen
if (transform.position.y > _screenTop + 2f) // Extra buffer for safety
{
ReturnToPool();
}
}
/// <summary>
/// Returns this obstacle to the spawner's pool
/// </summary>
private void ReturnToPool()
{
if (spawner != null)
{
spawner.ReturnObstacleToPool(gameObject, prefabIndex);
}
else
{
Debug.LogWarning($"[FloatingObstacle] Cannot return {gameObject.name} to pool - missing spawner reference");
Destroy(gameObject);
}
}
/// <summary>
/// Sets the spawner reference for this obstacle
/// </summary>
/// <param name="obstacleSpawner">The spawner that created this obstacle</param>
public void SetSpawner(ObstacleSpawner obstacleSpawner)
{
spawner = obstacleSpawner;
}
/// <summary>
/// Called when the obstacle is retrieved from the pool
/// </summary>
public void OnSpawn()
{
_hasDealtDamage = false;
_collisionCheckTimer = 0f;
_screenTop = 0f; // Reset cached screen bounds
// Ensure the obstacle is active and visible
gameObject.SetActive(true);
Debug.Log($"[FloatingObstacle] Obstacle {gameObject.name} spawned");
}
/// <summary>
/// Called when the obstacle is returned to the pool
/// </summary>
public void OnDespawn()
{
_hasDealtDamage = false;
_collisionCheckTimer = 0f;
Debug.Log($"[FloatingObstacle] Obstacle {gameObject.name} despawned");
}
/// <summary>
/// Public method to manually trigger return to pool (for external systems)
/// </summary>
public void ForceReturnToPool()
{
ReturnToPool();
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 32718083aef44be2a4318681fcdf5b2e
timeCreated: 1758117709

View File

@@ -0,0 +1,44 @@
using UnityEngine;
using Pooling;
namespace Minigames.DivingForPictures
{
/// <summary>
/// Manages a pool of floating obstacle objects to reduce garbage collection overhead.
/// Optimized for handling a large number of different obstacle prefab types.
/// </summary>
public class ObstaclePool : MultiPrefabPool<FloatingObstacle>
{
/// <summary>
/// Returns an obstacle to the pool
/// </summary>
/// <param name="obstacle">The obstacle to return to the pool</param>
/// <param name="prefabIndex">The index of the prefab this obstacle was created from</param>
public void ReturnObstacle(GameObject obstacle, int prefabIndex)
{
if (obstacle != null)
{
FloatingObstacle obstacleComponent = obstacle.GetComponent<FloatingObstacle>();
if (obstacleComponent != null)
{
Return(obstacleComponent, prefabIndex);
}
else
{
Debug.LogWarning($"Attempted to return a GameObject without a FloatingObstacle component: {obstacle.name}");
Destroy(obstacle);
}
}
}
/// <summary>
/// Gets an obstacle from the pool, or creates a new one if the pool is empty
/// </summary>
/// <returns>An obstacle instance ready to use</returns>
public GameObject GetObstacle(int prefabIndex)
{
FloatingObstacle obstacleComponent = Get(prefabIndex);
return obstacleComponent.gameObject;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a53ba79246a94dc4a71d2fb0d7214cfb
timeCreated: 1758116804

View File

@@ -0,0 +1,452 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using Pooling;
namespace Minigames.DivingForPictures
{
/// <summary>
/// Spawns and manages mobile obstacles for the diving minigame.
/// Uses object pooling and validates spawn positions to avoid colliding with tiles.
/// </summary>
public class ObstacleSpawner : MonoBehaviour
{
[Header("Obstacle Prefabs")]
[Tooltip("List of possible obstacle prefabs to spawn")]
[SerializeField] private List<GameObject> obstaclePrefabs;
[Header("Spawn Settings")]
[Tooltip("Time interval between spawn attempts (in seconds)")]
[SerializeField] private float spawnInterval = 2f;
[Tooltip("Random variation in spawn timing (+/- seconds)")]
[SerializeField] private float spawnIntervalVariation = 0.5f;
[Tooltip("Maximum number of spawn position attempts before skipping")]
[SerializeField] private int maxSpawnAttempts = 10;
[Tooltip("Radius around spawn point to check for tile collisions")]
[SerializeField] private float spawnCollisionRadius = 1f;
[Header("Spawn Position")]
[Tooltip("How far below screen to spawn obstacles")]
[SerializeField] private float spawnDistanceBelowScreen = 2f;
[Tooltip("Horizontal spawn range (distance from center)")]
[SerializeField] private float spawnRangeX = 8f;
[Header("Obstacle Properties Randomization")]
[Tooltip("Minimum movement speed for spawned obstacles")]
[SerializeField] private float minMoveSpeed = 1f;
[Tooltip("Maximum movement speed for spawned obstacles")]
[SerializeField] private float maxMoveSpeed = 4f;
[Tooltip("Minimum damage dealt by obstacles")]
[SerializeField] private float minDamage = 0.5f;
[Tooltip("Maximum damage dealt by obstacles")]
[SerializeField] private float maxDamage = 2f;
[Header("Object Pooling")]
[Tooltip("Whether to use object pooling for obstacles")]
[SerializeField] private bool useObjectPooling = true;
[Tooltip("Maximum objects per prefab type in pool")]
[SerializeField] private int maxPerPrefabPoolSize = 3;
[Tooltip("Total maximum pool size across all prefab types")]
[SerializeField] private int totalMaxPoolSize = 15;
[Header("Layer Settings")]
[Tooltip("Layer mask for tile collision detection - should match WorldObstacle layer")]
[SerializeField] private LayerMask tileLayerMask = 1 << 6; // WorldObstacle layer
[Header("Events")]
[Tooltip("Called when an obstacle is spawned")]
public UnityEvent<GameObject> onObstacleSpawned;
[Tooltip("Called when an obstacle is returned to pool")]
public UnityEvent<GameObject> onObstacleDestroyed;
// Private fields
private ObstaclePool _obstaclePool;
private Camera _mainCamera;
private float _screenBottom;
private Coroutine _spawnCoroutine;
private readonly List<GameObject> _activeObstacles = new List<GameObject>();
private void Awake()
{
_mainCamera = Camera.main;
// Validate obstacle prefabs
ValidateObstaclePrefabs();
if (useObjectPooling)
{
InitializeObjectPool();
}
}
private void Start()
{
CalculateScreenBounds();
StartSpawning();
}
private void OnDestroy()
{
StopSpawning();
}
/// <summary>
/// Validates that all prefabs have required components
/// </summary>
private void ValidateObstaclePrefabs()
{
for (int i = 0; i < obstaclePrefabs.Count; i++)
{
if (obstaclePrefabs[i] == null) continue;
// Check if the prefab has a FloatingObstacle component
if (obstaclePrefabs[i].GetComponent<FloatingObstacle>() == null)
{
Debug.LogWarning($"Obstacle prefab {obstaclePrefabs[i].name} does not have a FloatingObstacle component. Adding one automatically.");
obstaclePrefabs[i].AddComponent<FloatingObstacle>();
}
// Ensure the prefab is on the correct layer
if (obstaclePrefabs[i].layer != 11) // QuarryObstacle layer
{
Debug.LogWarning($"Obstacle prefab {obstaclePrefabs[i].name} is not on QuarryObstacle layer (11). Setting layer automatically.");
SetLayerRecursively(obstaclePrefabs[i], 11);
}
}
}
/// <summary>
/// Sets the layer of a GameObject and all its children
/// </summary>
private void SetLayerRecursively(GameObject obj, int layer)
{
obj.layer = layer;
foreach (Transform child in obj.transform)
{
SetLayerRecursively(child.gameObject, layer);
}
}
/// <summary>
/// Initialize the object pool system
/// </summary>
private void InitializeObjectPool()
{
GameObject poolGO = new GameObject("ObstaclePool");
poolGO.transform.SetParent(transform);
_obstaclePool = poolGO.AddComponent<ObstaclePool>();
// Set up pool configuration
_obstaclePool.maxPerPrefabPoolSize = maxPerPrefabPoolSize;
_obstaclePool.totalMaxPoolSize = totalMaxPoolSize;
// Convert GameObject list to FloatingObstacle list
List<FloatingObstacle> prefabObstacles = new List<FloatingObstacle>(obstaclePrefabs.Count);
foreach (var prefab in obstaclePrefabs)
{
if (prefab != null)
{
FloatingObstacle obstacleComponent = prefab.GetComponent<FloatingObstacle>();
if (obstacleComponent != null)
{
prefabObstacles.Add(obstacleComponent);
}
else
{
Debug.LogError($"Obstacle prefab {prefab.name} is missing a FloatingObstacle component!");
}
}
}
// Initialize the pool
_obstaclePool.Initialize(prefabObstacles);
// Periodically trim the pool
InvokeRepeating(nameof(TrimExcessPooledObstacles), 15f, 30f);
}
/// <summary>
/// Calculate screen bounds in world space
/// </summary>
private void CalculateScreenBounds()
{
if (_mainCamera == null)
{
_mainCamera = Camera.main;
if (_mainCamera == null)
{
Debug.LogError("[ObstacleSpawner] No main camera found!");
return;
}
}
Vector3 bottomWorldPoint = _mainCamera.ViewportToWorldPoint(new Vector3(0.5f, 0f, _mainCamera.nearClipPlane));
_screenBottom = bottomWorldPoint.y;
}
/// <summary>
/// Starts the obstacle spawning coroutine
/// </summary>
public void StartSpawning()
{
if (_spawnCoroutine == null)
{
_spawnCoroutine = StartCoroutine(SpawnObstaclesCoroutine());
Debug.Log("[ObstacleSpawner] Started spawning obstacles");
}
}
/// <summary>
/// Stops the obstacle spawning coroutine
/// </summary>
public void StopSpawning()
{
if (_spawnCoroutine != null)
{
StopCoroutine(_spawnCoroutine);
_spawnCoroutine = null;
Debug.Log("[ObstacleSpawner] Stopped spawning obstacles");
}
}
/// <summary>
/// Main spawning coroutine that runs continuously
/// </summary>
private IEnumerator SpawnObstaclesCoroutine()
{
while (true)
{
// Calculate next spawn time with variation
float nextSpawnTime = spawnInterval + Random.Range(-spawnIntervalVariation, spawnIntervalVariation);
nextSpawnTime = Mathf.Max(0.1f, nextSpawnTime); // Ensure minimum interval
yield return new WaitForSeconds(nextSpawnTime);
// Attempt to spawn an obstacle
TrySpawnObstacle();
}
}
/// <summary>
/// Attempts to spawn an obstacle at a valid position
/// </summary>
private void TrySpawnObstacle()
{
if (obstaclePrefabs == null || obstaclePrefabs.Count == 0)
{
Debug.LogWarning("[ObstacleSpawner] No obstacle prefabs available for spawning!");
return;
}
Vector3 spawnPosition;
bool foundValidPosition = false;
// Try to find a valid spawn position
for (int attempts = 0; attempts < maxSpawnAttempts; attempts++)
{
spawnPosition = GetRandomSpawnPosition();
if (IsValidSpawnPosition(spawnPosition))
{
SpawnObstacleAt(spawnPosition);
foundValidPosition = true;
break;
}
}
if (!foundValidPosition)
{
Debug.Log($"[ObstacleSpawner] Could not find valid spawn position after {maxSpawnAttempts} attempts");
}
}
/// <summary>
/// Gets a random spawn position below the screen
/// </summary>
private Vector3 GetRandomSpawnPosition()
{
float randomX = Random.Range(-spawnRangeX, spawnRangeX);
float spawnY = _screenBottom - spawnDistanceBelowScreen;
return new Vector3(randomX, spawnY, 0f);
}
/// <summary>
/// Checks if a spawn position is valid (not colliding with tiles)
/// </summary>
private bool IsValidSpawnPosition(Vector3 position)
{
// Use OverlapCircle to check for collisions with tiles
Collider2D collision = Physics2D.OverlapCircle(position, spawnCollisionRadius, tileLayerMask);
return collision == null;
}
/// <summary>
/// Spawns an obstacle at the specified position
/// </summary>
private void SpawnObstacleAt(Vector3 position)
{
// Select random prefab
int prefabIndex = Random.Range(0, obstaclePrefabs.Count);
GameObject prefab = obstaclePrefabs[prefabIndex];
if (prefab == null)
{
Debug.LogError($"[ObstacleSpawner] Obstacle prefab at index {prefabIndex} is null!");
return;
}
GameObject obstacle;
// Spawn using pool or instantiate directly
if (useObjectPooling && _obstaclePool != null)
{
obstacle = _obstaclePool.GetObstacle(prefabIndex);
if (obstacle == null)
{
Debug.LogError("[ObstacleSpawner] Failed to get obstacle from pool!");
return;
}
obstacle.transform.position = position;
obstacle.transform.rotation = prefab.transform.rotation;
obstacle.transform.SetParent(transform);
}
else
{
obstacle = Instantiate(prefab, position, prefab.transform.rotation, transform);
}
// Configure the obstacle
ConfigureObstacle(obstacle, prefabIndex);
// Track active obstacles
_activeObstacles.Add(obstacle);
// Invoke events
onObstacleSpawned?.Invoke(obstacle);
Debug.Log($"[ObstacleSpawner] Spawned obstacle {obstacle.name} at {position}");
}
/// <summary>
/// Configures an obstacle with randomized properties
/// </summary>
private void ConfigureObstacle(GameObject obstacle, int prefabIndex)
{
FloatingObstacle obstacleComponent = obstacle.GetComponent<FloatingObstacle>();
if (obstacleComponent != null)
{
// Set prefab index
obstacleComponent.PrefabIndex = prefabIndex;
// Randomize properties
obstacleComponent.MoveSpeed = Random.Range(minMoveSpeed, maxMoveSpeed);
obstacleComponent.Damage = Random.Range(minDamage, maxDamage);
// Set spawner reference (since FloatingObstacle has this built-in now)
obstacleComponent.SetSpawner(this);
}
}
/// <summary>
/// Returns an obstacle to the pool (called by FloatingObstacle)
/// </summary>
public void ReturnObstacleToPool(GameObject obstacle, int prefabIndex)
{
if (obstacle == null) return;
// Remove from active list
_activeObstacles.Remove(obstacle);
// Invoke events
onObstacleDestroyed?.Invoke(obstacle);
// Return to pool or destroy
if (useObjectPooling && _obstaclePool != null)
{
_obstaclePool.ReturnObstacle(obstacle, prefabIndex);
}
else
{
Destroy(obstacle);
}
}
/// <summary>
/// Called periodically to trim excess pooled obstacles
/// </summary>
private void TrimExcessPooledObstacles()
{
if (_obstaclePool != null)
{
_obstaclePool.TrimExcess();
}
}
/// <summary>
/// Public method to change spawn interval at runtime
/// </summary>
public void SetSpawnInterval(float interval)
{
spawnInterval = interval;
}
/// <summary>
/// Public method to change spawn range at runtime
/// </summary>
public void SetSpawnRange(float range)
{
spawnRangeX = range;
}
/// <summary>
/// Public method to set speed range at runtime
/// </summary>
public void SetSpeedRange(float min, float max)
{
minMoveSpeed = min;
maxMoveSpeed = max;
}
/// <summary>
/// Public method to set damage range at runtime
/// </summary>
public void SetDamageRange(float min, float max)
{
minDamage = min;
maxDamage = max;
}
/// <summary>
/// Gets the count of currently active obstacles
/// </summary>
public int ActiveObstacleCount => _activeObstacles.Count;
#if UNITY_EDITOR
private void OnDrawGizmosSelected()
{
// Draw spawn area
Gizmos.color = Color.yellow;
Vector3 center = new Vector3(0f, _screenBottom - spawnDistanceBelowScreen, 0f);
Vector3 size = new Vector3(spawnRangeX * 2f, 1f, 1f);
Gizmos.DrawWireCube(center, size);
// Draw collision radius at spawn point
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(center, spawnCollisionRadius);
}
#endif
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 49ec62157fd945fab730193e9ea0bff7
timeCreated: 1758116903

View File

@@ -0,0 +1,242 @@
using UnityEngine;
using System.Collections;
namespace Minigames.DivingForPictures
{
/// <summary>
/// Handles visual feedback for player damage by making the sprite renderer blink red.
/// Automatically subscribes to damage events from PlayerCollisionBehavior components.
/// </summary>
public class PlayerBlinkBehavior : MonoBehaviour
{
[Header("Blink Settings")]
[Tooltip("Color to blink to when taking damage (typically red for damage indication)")]
[SerializeField] private Color damageBlinkColor = Color.red;
[Tooltip("How fast to blink between normal and damage colors (seconds between color changes)")]
[SerializeField] private float blinkRate = 0.15f;
[Tooltip("Alpha value for the damage color (0 = transparent, 1 = opaque)")]
[Range(0f, 1f)]
[SerializeField] private float damageColorAlpha = 0.7f;
[Header("References")]
[Tooltip("The SpriteRenderer to apply blink effects to (auto-assigned if empty)")]
[SerializeField] private SpriteRenderer targetSpriteRenderer;
private bool _isBlinking;
private bool _isShowingDamageColor;
private Coroutine _blinkCoroutine;
private Color _originalColor; // Missing field declaration
private void Awake()
{
// Auto-assign sprite renderer if not set
if (targetSpriteRenderer == null)
{
targetSpriteRenderer = GetComponent<SpriteRenderer>();
if (targetSpriteRenderer == null)
{
targetSpriteRenderer = GetComponentInChildren<SpriteRenderer>();
if (targetSpriteRenderer != null)
{
Debug.Log($"[PlayerBlinkBehavior] Found SpriteRenderer on child object: {targetSpriteRenderer.gameObject.name}");
}
}
}
if (targetSpriteRenderer == null)
{
Debug.LogError("[PlayerBlinkBehavior] No SpriteRenderer found on this GameObject or its children!");
return;
}
// Store original color
_originalColor = targetSpriteRenderer.color;
// Ensure damage color has the correct alpha
damageBlinkColor.a = damageColorAlpha;
}
private void OnEnable()
{
// Subscribe to damage events
PlayerCollisionBehavior.OnDamageStart += StartBlinking;
PlayerCollisionBehavior.OnDamageEnd += StopBlinking;
}
private void OnDisable()
{
// Unsubscribe from damage events
PlayerCollisionBehavior.OnDamageStart -= StartBlinking;
PlayerCollisionBehavior.OnDamageEnd -= StopBlinking;
// Stop any ongoing blink effect
if (_blinkCoroutine != null)
{
StopCoroutine(_blinkCoroutine);
_blinkCoroutine = null;
}
// Restore original color
RestoreOriginalColor();
}
/// <summary>
/// Starts the red blinking effect when damage begins
/// </summary>
private void StartBlinking()
{
if (targetSpriteRenderer == null) return;
Debug.Log("[PlayerBlinkBehavior] Starting damage blink effect");
// Stop any existing blink coroutine
if (_blinkCoroutine != null)
{
StopCoroutine(_blinkCoroutine);
}
_isBlinking = true;
_isShowingDamageColor = false;
// Start the blink coroutine
_blinkCoroutine = StartCoroutine(BlinkCoroutine());
}
/// <summary>
/// Stops the blinking effect when damage ends
/// </summary>
private void StopBlinking()
{
Debug.Log("[PlayerBlinkBehavior] Stopping damage blink effect");
_isBlinking = false;
// Stop the blink coroutine
if (_blinkCoroutine != null)
{
StopCoroutine(_blinkCoroutine);
_blinkCoroutine = null;
}
// Restore original color
RestoreOriginalColor();
}
/// <summary>
/// Coroutine that handles the blinking animation
/// </summary>
private IEnumerator BlinkCoroutine()
{
while (_isBlinking && targetSpriteRenderer != null)
{
// Toggle between original and damage colors
if (_isShowingDamageColor)
{
targetSpriteRenderer.color = _originalColor;
_isShowingDamageColor = false;
}
else
{
targetSpriteRenderer.color = damageBlinkColor;
_isShowingDamageColor = true;
}
// Wait for blink interval
yield return new WaitForSeconds(blinkRate);
}
}
/// <summary>
/// Restores the sprite renderer to its original color
/// </summary>
private void RestoreOriginalColor()
{
if (targetSpriteRenderer != null)
{
targetSpriteRenderer.color = _originalColor;
_isShowingDamageColor = false;
}
}
/// <summary>
/// Updates the original color reference (useful if sprite color changes during gameplay)
/// </summary>
public void UpdateOriginalColor()
{
if (targetSpriteRenderer != null && !_isBlinking)
{
_originalColor = targetSpriteRenderer.color;
}
}
/// <summary>
/// Public method to change blink color at runtime
/// </summary>
public void SetDamageBlinkColor(Color newColor)
{
damageBlinkColor = newColor;
damageBlinkColor.a = damageColorAlpha;
}
/// <summary>
/// Public method to change blink rate at runtime
/// </summary>
public void SetBlinkRate(float rate)
{
blinkRate = rate;
}
/// <summary>
/// Check if currently blinking
/// </summary>
public bool IsBlinking => _isBlinking;
/// <summary>
/// Manually trigger blink effect (useful for testing or other damage sources)
/// </summary>
public void TriggerBlink(float duration)
{
if (_blinkCoroutine != null)
{
StopCoroutine(_blinkCoroutine);
}
StartCoroutine(ManualBlinkCoroutine(duration));
}
/// <summary>
/// Coroutine for manually triggered blink effects
/// </summary>
private IEnumerator ManualBlinkCoroutine(float duration)
{
_isBlinking = true;
_isShowingDamageColor = false;
float elapsed = 0f;
while (elapsed < duration && targetSpriteRenderer != null)
{
// Toggle between original and damage colors
if (_isShowingDamageColor)
{
targetSpriteRenderer.color = _originalColor;
_isShowingDamageColor = false;
}
else
{
targetSpriteRenderer.color = damageBlinkColor;
_isShowingDamageColor = true;
}
yield return new WaitForSeconds(blinkRate);
elapsed += blinkRate;
}
// Ensure we end with original color
RestoreOriginalColor();
_isBlinking = false;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d8ea29cc80524de8affe17b930cd75c1
timeCreated: 1758114229

View File

@@ -0,0 +1,234 @@
using UnityEngine;
namespace Minigames.DivingForPictures
{
/// <summary>
/// Collision behavior that bumps the player toward the center of the trench.
/// Provides two modes: impulse (force-based push) or smooth movement to center.
/// </summary>
public class PlayerBumpCollisionBehavior : PlayerCollisionBehavior
{
[Header("Bump Settings")]
[Tooltip("Type of bump response: Impulse pushes with force, SmoothToCenter moves directly to center")]
[SerializeField] private BumpMode bumpMode = BumpMode.Impulse;
[Tooltip("Force strength for impulse bumps - higher values push further toward center")]
[SerializeField] private float bumpForce = 5.0f;
[Tooltip("Speed for smooth movement to center (units per second)")]
[SerializeField] private float smoothMoveSpeed = 8.0f;
[Tooltip("Animation curve controlling bump movement over time")]
[SerializeField] private AnimationCurve bumpCurve = new AnimationCurve(new Keyframe(0f, 0f, 0f, 2f), new Keyframe(1f, 1f, 0f, 0f));
[Tooltip("Whether to block player input during bump movement")]
[SerializeField] private bool blockInputDuringBump = true;
public enum BumpMode
{
Impulse, // Force-based push toward center (distance depends on force)
SmoothToCenter // Smooth movement to center with configurable speed
}
private bool _isBumping;
private float _bumpTimer;
private float _bumpStartX;
private float _bumpTargetX;
private float _bumpDuration;
private bool _bumpInputBlocked; // Tracks bump-specific input blocking
protected override void Update()
{
base.Update();
// Handle bump movement
if (_isBumping)
{
_bumpTimer -= Time.deltaTime;
if (_bumpTimer <= 0f)
{
// Bump finished
_isBumping = false;
if (_bumpInputBlocked)
{
RestoreBumpInput();
}
// Ensure we end exactly at target
if (playerCharacter != null)
{
Vector3 currentPos = playerCharacter.transform.position;
playerCharacter.transform.position = new Vector3(_bumpTargetX, currentPos.y, currentPos.z);
}
}
else
{
// Apply bump movement
float progress = 1f - (_bumpTimer / _bumpDuration);
float curveValue = bumpCurve.Evaluate(progress);
float currentX = Mathf.Lerp(_bumpStartX, _bumpTargetX, curveValue);
// Apply the position to the player character
if (playerCharacter != null)
{
Vector3 currentPos = playerCharacter.transform.position;
playerCharacter.transform.position = new Vector3(currentX, currentPos.y, currentPos.z);
}
}
}
}
protected override void HandleCollisionResponse(Collider2D obstacle)
{
switch (bumpMode)
{
case BumpMode.Impulse:
StartImpulseBump();
break;
case BumpMode.SmoothToCenter:
StartSmoothMoveToCenter();
break;
}
Debug.Log($"[PlayerBumpCollisionBehavior] Collision handled with {bumpMode} mode");
}
/// <summary>
/// Starts an impulse bump toward the center with force-based distance
/// </summary>
private void StartImpulseBump()
{
if (playerCharacter == null) return;
float currentX = playerCharacter.transform.position.x;
// Calculate bump distance based on force and current position
float directionToCenter = currentX > 0 ? -1f : 1f; // Direction toward center
// Calculate target position - closer to center based on bump force
float bumpDistance = bumpForce * 0.2f; // Scale factor for distance
float targetX = currentX + (directionToCenter * bumpDistance);
// Clamp to center if we would overshoot
if ((currentX > 0 && targetX < 0) || (currentX < 0 && targetX > 0))
{
targetX = 0f;
}
// Set bump parameters
_bumpStartX = currentX;
_bumpTargetX = targetX;
_bumpDuration = 0.5f; // Fixed duration for impulse
StartBump();
Debug.Log($"[PlayerBumpCollisionBehavior] Starting impulse bump from X={_bumpStartX} to X={_bumpTargetX} (force={bumpForce})");
}
/// <summary>
/// Starts smooth movement to the center
/// </summary>
private void StartSmoothMoveToCenter()
{
if (playerCharacter == null) return;
float currentX = playerCharacter.transform.position.x;
float distanceToCenter = Mathf.Abs(currentX);
// Set bump parameters
_bumpStartX = currentX;
_bumpTargetX = 0f; // Always move to center
_bumpDuration = distanceToCenter / smoothMoveSpeed; // Duration based on distance and speed
StartBump();
Debug.Log($"[PlayerBumpCollisionBehavior] Starting smooth move to center from X={_bumpStartX} (speed={smoothMoveSpeed}, duration={_bumpDuration:F2}s)");
}
/// <summary>
/// Common bump initialization
/// </summary>
private void StartBump()
{
_isBumping = true;
_bumpTimer = _bumpDuration;
// Block player input if enabled (use bump-specific blocking)
if (blockInputDuringBump && playerController != null && playerController.enabled)
{
playerController.enabled = false;
_bumpInputBlocked = true;
Debug.Log("[PlayerBumpCollisionBehavior] Player input blocked during bump");
}
}
/// <summary>
/// Restores player input after bump movement
/// </summary>
private void RestoreBumpInput()
{
if (_bumpInputBlocked && playerController != null)
{
playerController.enabled = true;
_bumpInputBlocked = false;
// Update the controller's target position to current position to prevent snapping
UpdateControllerTarget();
Debug.Log("[PlayerBumpCollisionBehavior] Player input restored after bump");
}
}
protected override void OnImmunityEnd()
{
base.OnImmunityEnd();
// Stop any ongoing bump if immunity ends
if (_isBumping)
{
_isBumping = false;
if (_bumpInputBlocked)
{
RestoreBumpInput();
}
}
}
/// <summary>
/// Public method to change bump mode at runtime
/// </summary>
public void SetBumpMode(BumpMode mode)
{
bumpMode = mode;
}
/// <summary>
/// Public method to change bump force at runtime
/// </summary>
public void SetBumpForce(float force)
{
bumpForce = force;
}
/// <summary>
/// Public method to change smooth move speed at runtime
/// </summary>
public void SetSmoothMoveSpeed(float speed)
{
smoothMoveSpeed = speed;
}
/// <summary>
/// Check if player is currently being bumped
/// </summary>
public bool IsBumping => _isBumping;
/// <summary>
/// Check if input is currently blocked by bump
/// </summary>
public bool IsBumpInputBlocked => _bumpInputBlocked;
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8222f0e3aeeb4fc4975aaead6cf7afbe
timeCreated: 1758109619

View File

@@ -0,0 +1,238 @@
using UnityEngine;
using System;
namespace Minigames.DivingForPictures
{
/// <summary>
/// Base class for handling player collisions with world obstacles.
/// Detects collisions between Player layer (7) and WorldObstacle layer (6).
/// </summary>
public abstract class PlayerCollisionBehavior : MonoBehaviour
{
[Header("Collision Settings")]
[Tooltip("Duration in seconds of damage immunity after being hit")]
[SerializeField] protected float damageImmunityDuration = 1.0f;
[Tooltip("Layer mask for obstacle detection - should match WorldObstacle layer")]
[SerializeField] protected LayerMask obstacleLayerMask = 1 << 6; // WorldObstacle layer
[Header("Input Blocking")]
[Tooltip("Whether to block player input during damage immunity period")]
[SerializeField] protected bool blockInputDuringImmunity;
[Header("References")]
[Tooltip("The player character GameObject (auto-assigned if empty)")]
[SerializeField] protected GameObject playerCharacter;
[Tooltip("Reference to the PlayerController component (auto-assigned if empty)")]
[SerializeField] protected PlayerController playerController;
// Events for damage state changes
public static event Action OnDamageStart;
public static event Action OnDamageEnd;
/// <summary>
/// Public static method to trigger damage start event from external classes
/// </summary>
public static void TriggerDamageStart()
{
OnDamageStart?.Invoke();
}
/// <summary>
/// Public static method to trigger damage end event from external classes
/// </summary>
public static void TriggerDamageEnd()
{
OnDamageEnd?.Invoke();
}
protected bool isImmune;
protected float immunityTimer;
protected Collider2D playerCollider;
protected bool wasInputBlocked;
protected virtual void Awake()
{
// Auto-assign if not set in inspector
if (playerCharacter == null)
playerCharacter = gameObject;
if (playerController == null)
playerController = GetComponent<PlayerController>();
// Look for collider on this GameObject first, then in children
playerCollider = GetComponent<Collider2D>();
if (playerCollider == null)
{
playerCollider = GetComponentInChildren<Collider2D>();
if (playerCollider != null)
{
Debug.Log($"[{GetType().Name}] Found collider on child object: {playerCollider.gameObject.name}");
}
}
if (playerCollider == null)
{
Debug.LogError($"[{GetType().Name}] No Collider2D found on this GameObject or its children!");
}
}
protected virtual void Update()
{
// Handle immunity timer
if (isImmune)
{
immunityTimer -= Time.deltaTime;
if (immunityTimer <= 0f)
{
isImmune = false;
OnImmunityEnd();
}
}
// Check for collisions if not immune
if (!isImmune && playerCollider != null)
{
CheckForCollisions();
}
}
/// <summary>
/// Checks for collisions with obstacle layer objects
/// </summary>
protected virtual void CheckForCollisions()
{
// Get all colliders overlapping with the player
Collider2D[] overlapping = new Collider2D[10];
ContactFilter2D filter = new ContactFilter2D();
filter.SetLayerMask(obstacleLayerMask);
filter.useTriggers = true;
int count = playerCollider.Overlap(filter, overlapping);
if (count > 0)
{
// Found collision, trigger response
OnCollisionDetected(overlapping[0]);
}
}
/// <summary>
/// Called when a collision with an obstacle is detected
/// </summary>
/// <param name="obstacle">The obstacle collider that was hit</param>
protected virtual void OnCollisionDetected(Collider2D obstacle)
{
if (isImmune) return;
// Start immunity period
isImmune = true;
immunityTimer = damageImmunityDuration;
// Call the specific collision response
HandleCollisionResponse(obstacle);
// Notify about immunity start
OnImmunityStart();
}
/// <summary>
/// Override this method to implement specific collision response behavior
/// </summary>
/// <param name="obstacle">The obstacle that was collided with</param>
protected abstract void HandleCollisionResponse(Collider2D obstacle);
/// <summary>
/// Called when damage immunity starts
/// </summary>
protected virtual void OnImmunityStart()
{
Debug.Log($"[{GetType().Name}] Damage immunity started for {damageImmunityDuration} seconds");
// Block input if specified
if (blockInputDuringImmunity)
{
BlockPlayerInput();
}
// Broadcast damage start event
OnDamageStart?.Invoke();
}
/// <summary>
/// Called when damage immunity ends
/// </summary>
protected virtual void OnImmunityEnd()
{
Debug.Log($"[{GetType().Name}] Damage immunity ended");
// Restore input if it was blocked
if (wasInputBlocked)
{
RestorePlayerInput();
}
// Broadcast damage end event
OnDamageEnd?.Invoke();
}
/// <summary>
/// Restores player input after immunity
/// </summary>
protected virtual void RestorePlayerInput()
{
if (playerController != null && wasInputBlocked)
{
playerController.enabled = true;
wasInputBlocked = false;
// Update the controller's target position to current position to prevent snapping
UpdateControllerTarget();
Debug.Log($"[{GetType().Name}] Player input restored after immunity");
}
}
/// <summary>
/// Blocks player input during immunity
/// </summary>
protected virtual void BlockPlayerInput()
{
if (playerController != null && playerController.enabled)
{
playerController.enabled = false;
wasInputBlocked = true;
Debug.Log($"[{GetType().Name}] Player input blocked during immunity");
}
}
/// <summary>
/// Updates the PlayerController's internal target to match current position
/// </summary>
protected virtual void UpdateControllerTarget()
{
if (playerController != null && playerCharacter != null)
{
// Use reflection to update the private _targetFingerX field
var targetField = typeof(PlayerController)
.GetField("_targetFingerX", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (targetField != null)
{
targetField.SetValue(playerController, playerCharacter.transform.position.x);
}
}
}
/// <summary>
/// Public property to check if player is currently immune
/// </summary>
public bool IsImmune => isImmune;
/// <summary>
/// Remaining immunity time
/// </summary>
public float RemainingImmunityTime => immunityTimer;
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e6c959bca2e24e72bf22e92439580d79
timeCreated: 1758109598

View File

@@ -0,0 +1,102 @@
using UnityEngine;
namespace Minigames.DivingForPictures
{
/// <summary>
/// Collision behavior that handles damage from mobile obstacles.
/// Unlike bump collisions, this only deals damage without physical response.
/// Detects collisions between Player layer (7) and QuarryObstacle layer (11).
/// </summary>
public class PlayerDamageCollisionBehavior : PlayerCollisionBehavior
{
[Header("Damage Settings")]
[Tooltip("Base damage amount dealt by obstacles")]
[SerializeField] private float baseDamage = 1f;
[Tooltip("Whether to use the obstacle's individual damage value or the base damage")]
[SerializeField] private bool useObstacleDamageValue = true;
protected override void Awake()
{
base.Awake();
// Override the obstacle layer mask to target QuarryObstacle layer (11)
obstacleLayerMask = 1 << 11; // QuarryObstacle layer
}
protected override void HandleCollisionResponse(Collider2D obstacle)
{
float damageAmount = baseDamage;
// Try to get damage from the obstacle component if enabled
if (useObstacleDamageValue)
{
FloatingObstacle obstacleComponent = obstacle.GetComponent<FloatingObstacle>();
if (obstacleComponent != null)
{
damageAmount = obstacleComponent.Damage;
}
}
// Apply damage (this could be extended to integrate with a health system)
ApplyDamage(damageAmount);
Debug.Log($"[PlayerDamageCollisionBehavior] Player took {damageAmount} damage from obstacle {obstacle.gameObject.name}");
}
/// <summary>
/// Applies damage to the player
/// Override this method to integrate with your health/damage system
/// </summary>
/// <param name="damage">Amount of damage to apply</param>
protected virtual void ApplyDamage(float damage)
{
// For now, just log the damage
// In a full implementation, this would reduce player health, trigger UI updates, etc.
Debug.Log($"[PlayerDamageCollisionBehavior] Applied {damage} damage to player");
// TODO: Integrate with health system when available
// Example: playerHealth.TakeDamage(damage);
}
/// <summary>
/// Override to prevent input blocking during damage immunity
/// Since obstacles pass through the player, we don't want to block input
/// </summary>
protected override void OnImmunityStart()
{
Debug.Log($"[PlayerDamageCollisionBehavior] Damage immunity started for {damageImmunityDuration} seconds");
// Don't block input for obstacle damage - let player keep moving
// Only broadcast the damage event
TriggerDamageStart();
}
/// <summary>
/// Override to handle immunity end without input restoration
/// </summary>
protected override void OnImmunityEnd()
{
Debug.Log($"[PlayerDamageCollisionBehavior] Damage immunity ended");
// Broadcast damage end event
TriggerDamageEnd();
}
/// <summary>
/// Public method to set base damage at runtime
/// </summary>
public void SetBaseDamage(float damage)
{
baseDamage = damage;
}
/// <summary>
/// Public method to toggle between base damage and obstacle-specific damage
/// </summary>
public void SetUseObstacleDamage(bool useObstacleDamage)
{
useObstacleDamageValue = useObstacleDamage;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c9c18dbd013d42ae8c221e6205e4d49c
timeCreated: 1758116850

View File

@@ -0,0 +1,180 @@
using UnityEngine;
namespace Minigames.DivingForPictures
{
/// <summary>
/// Collision behavior that grants temporary immunity and allows the player to phase through obstacles.
/// During immunity, the player can pass through all obstacles without further collision detection.
/// </summary>
public class PlayerPhaseCollisionBehavior : PlayerCollisionBehavior
{
[Header("Phase Settings")]
[Tooltip("Whether to disable the player's collider during immunity to allow phasing through obstacles")]
[SerializeField] private bool disableColliderDuringImmunity = true;
[Tooltip("How fast the player sprite blinks during phase mode (seconds between blinks)")]
[SerializeField] private float visualFeedbackBlinkRate = 0.1f;
private SpriteRenderer _spriteRenderer;
private bool _originalColliderState;
private bool _isBlinking;
private float _blinkTimer;
private Color _originalColor;
private float _originalAlpha;
protected override void Awake()
{
base.Awake();
// Get sprite renderer from player character
if (playerCharacter != null)
{
_spriteRenderer = playerCharacter.GetComponent<SpriteRenderer>();
}
if (_spriteRenderer != null)
{
_originalColor = _spriteRenderer.color;
_originalAlpha = _originalColor.a;
}
if (playerCollider != null)
{
_originalColliderState = playerCollider.enabled;
}
}
protected override void Update()
{
base.Update();
// Handle visual feedback blinking during immunity
if (_isBlinking && _spriteRenderer != null)
{
_blinkTimer -= Time.deltaTime;
if (_blinkTimer <= 0f)
{
// Toggle visibility
Color currentColor = _spriteRenderer.color;
currentColor.a = currentColor.a > 0.5f ? 0.3f : _originalAlpha;
_spriteRenderer.color = currentColor;
_blinkTimer = visualFeedbackBlinkRate;
}
}
}
protected override void CheckForCollisions()
{
// Override to skip collision detection entirely during immunity if collider is disabled
if (isImmune && disableColliderDuringImmunity)
{
return; // Skip collision detection completely
}
base.CheckForCollisions();
}
protected override void HandleCollisionResponse(Collider2D obstacle)
{
Debug.Log("[PlayerPhaseCollisionBehavior] Collision detected - entering phase mode");
// No immediate physical response - just start immunity period
// The immunity will be handled by the base class
}
protected override void OnImmunityStart()
{
base.OnImmunityStart();
// Disable collider to allow phasing through obstacles
if (disableColliderDuringImmunity && playerCollider != null)
{
playerCollider.enabled = false;
Debug.Log("[PlayerPhaseCollisionBehavior] Collider disabled - entering phase mode");
}
// Start visual feedback
StartVisualFeedback();
}
protected override void OnImmunityEnd()
{
base.OnImmunityEnd();
// Re-enable collider
if (playerCollider != null)
{
playerCollider.enabled = _originalColliderState;
Debug.Log("[PlayerPhaseCollisionBehavior] Collider re-enabled - exiting phase mode");
}
// Stop visual feedback
StopVisualFeedback();
}
/// <summary>
/// Starts the visual feedback to indicate immunity/phase mode
/// </summary>
private void StartVisualFeedback()
{
if (_spriteRenderer != null)
{
_isBlinking = true;
_blinkTimer = 0f;
// Start with reduced opacity
Color immunityColor = _originalColor;
immunityColor.a = 0.3f;
_spriteRenderer.color = immunityColor;
}
}
/// <summary>
/// Stops the visual feedback and restores original appearance
/// </summary>
private void StopVisualFeedback()
{
_isBlinking = false;
if (_spriteRenderer != null)
{
// Restore original color and alpha
_spriteRenderer.color = _originalColor;
}
}
/// <summary>
/// Public method to toggle collider behavior during immunity
/// </summary>
public void SetColliderDisabling(bool disable)
{
disableColliderDuringImmunity = disable;
}
/// <summary>
/// Check if player is currently in phase mode
/// </summary>
public bool IsPhasing => isImmune && disableColliderDuringImmunity;
/// <summary>
/// Manually trigger phase mode (useful for testing or special abilities)
/// </summary>
public void TriggerPhaseMode(float duration = -1f)
{
if (duration > 0f)
{
isImmune = true;
immunityTimer = duration;
OnImmunityStart();
}
else
{
isImmune = true;
immunityTimer = damageImmunityDuration;
OnImmunityStart();
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d849a517ce3a41249ae9f37d2722cefa
timeCreated: 1758109641

View File

@@ -18,7 +18,7 @@ TagManager:
- Pulver - Pulver
- WorldBoundary - WorldBoundary
- Interactable - Interactable
- - QuarryObstacle
- -
- -
- -