Big stupid refactor of spawning almost done
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using AppleHillsCamera;
|
using AppleHillsCamera;
|
||||||
|
using Minigames.BirdPooper;
|
||||||
|
|
||||||
namespace Editor
|
namespace Editor
|
||||||
{
|
{
|
||||||
@@ -16,7 +17,7 @@ namespace Editor
|
|||||||
EdgeAnchor edgeAnchor = (EdgeAnchor)target;
|
EdgeAnchor edgeAnchor = (EdgeAnchor)target;
|
||||||
|
|
||||||
// Check if there's an Obstacle component on the same GameObject
|
// Check if there's an Obstacle component on the same GameObject
|
||||||
var obstacle = edgeAnchor.GetComponent<Minigames.BirdPooper.Obstacle>();
|
var obstacle = edgeAnchor.GetComponent<Obstacle>();
|
||||||
|
|
||||||
if (obstacle != null)
|
if (obstacle != null)
|
||||||
{
|
{
|
||||||
|
|||||||
8
Assets/Editor/Minigames.meta
Normal file
8
Assets/Editor/Minigames.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5d8af6b4c7a999a4aa2180ec5422baf3
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Assets/Editor/Minigames/Airplane.meta
Normal file
8
Assets/Editor/Minigames/Airplane.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 259f822b327e0d740ae0542eb24e1d0a
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
96
Assets/Editor/Minigames/Airplane/AirplaneSettingsEditor.cs
Normal file
96
Assets/Editor/Minigames/Airplane/AirplaneSettingsEditor.cs
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
using Minigames.Airplane.Data;
|
||||||
|
using Minigames.Airplane.Settings;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Editor.Minigames.Airplane
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Custom editor for AirplaneSettings that conditionally shows default obstacle positioning fields.
|
||||||
|
/// Integrates with SettingsEditorWindow.
|
||||||
|
/// </summary>
|
||||||
|
[CustomEditor(typeof(AirplaneSettings))]
|
||||||
|
public class AirplaneSettingsEditor : UnityEditor.Editor
|
||||||
|
{
|
||||||
|
public override void OnInspectorGUI()
|
||||||
|
{
|
||||||
|
serializedObject.Update();
|
||||||
|
|
||||||
|
// Draw all properties except the default obstacle positioning section
|
||||||
|
SerializedProperty iterator = serializedObject.GetIterator();
|
||||||
|
bool enterChildren = true;
|
||||||
|
|
||||||
|
while (iterator.NextVisible(enterChildren))
|
||||||
|
{
|
||||||
|
enterChildren = false;
|
||||||
|
|
||||||
|
// Skip script field
|
||||||
|
if (iterator.propertyPath == "m_Script")
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Handle Default Obstacle Positioning section specially
|
||||||
|
if (iterator.propertyPath == "defaultObstaclePositionMode")
|
||||||
|
{
|
||||||
|
DrawDefaultObstaclePositioningSection();
|
||||||
|
|
||||||
|
// Skip the related fields as we'll draw them conditionally
|
||||||
|
iterator.NextVisible(false); // Skip defaultObstacleRandomYMin
|
||||||
|
iterator.NextVisible(false); // Skip defaultObstacleRandomYMax
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUILayout.PropertyField(iterator, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply changes and mark dirty for SettingsEditorWindow integration
|
||||||
|
if (serializedObject.ApplyModifiedProperties())
|
||||||
|
{
|
||||||
|
EditorUtility.SetDirty(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawDefaultObstaclePositioningSection()
|
||||||
|
{
|
||||||
|
var modeProperty = serializedObject.FindProperty("defaultObstaclePositionMode");
|
||||||
|
var randomYMinProperty = serializedObject.FindProperty("defaultObstacleRandomYMin");
|
||||||
|
var randomYMaxProperty = serializedObject.FindProperty("defaultObstacleRandomYMax");
|
||||||
|
|
||||||
|
EditorGUILayout.Space();
|
||||||
|
EditorGUILayout.PropertyField(modeProperty, new GUIContent("Default Position Mode"));
|
||||||
|
|
||||||
|
SpawnPositionMode currentMode = (SpawnPositionMode)modeProperty.enumValueIndex;
|
||||||
|
|
||||||
|
EditorGUILayout.Space(5);
|
||||||
|
|
||||||
|
switch (currentMode)
|
||||||
|
{
|
||||||
|
case SpawnPositionMode.SnapToGround:
|
||||||
|
EditorGUILayout.HelpBox("Obstacles will raycast to find ground and snap to it. If raycast fails, Fallback Y Position (configured in Ground Snapping section above) will be used.", MessageType.Info);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SpawnPositionMode.SpecifiedY:
|
||||||
|
EditorGUILayout.HelpBox("Obstacles will spawn at Fallback Y Position (configured in Ground Snapping section above).", MessageType.Info);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SpawnPositionMode.RandomRange:
|
||||||
|
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
||||||
|
EditorGUILayout.LabelField("Random Y Range", EditorStyles.miniBoldLabel);
|
||||||
|
EditorGUILayout.PropertyField(randomYMinProperty, new GUIContent("Min Y"));
|
||||||
|
EditorGUILayout.PropertyField(randomYMaxProperty, new GUIContent("Max Y"));
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
if (randomYMinProperty.floatValue > randomYMaxProperty.floatValue)
|
||||||
|
{
|
||||||
|
EditorGUILayout.HelpBox("Min Y should be less than Max Y!", MessageType.Warning);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
EditorGUILayout.HelpBox($"Obstacles will spawn at random Y between {randomYMinProperty.floatValue:F2} and {randomYMaxProperty.floatValue:F2}.", MessageType.Info);
|
||||||
|
}
|
||||||
|
EditorGUILayout.EndVertical();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: aa9b104969324b8584672126a4277d37
|
||||||
|
timeCreated: 1765990746
|
||||||
128
Assets/Editor/Minigames/Airplane/BaseDistanceSpawnerEditor.cs
Normal file
128
Assets/Editor/Minigames/Airplane/BaseDistanceSpawnerEditor.cs
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
using Minigames.Airplane.Core.Spawning;
|
||||||
|
using Minigames.Airplane.Data;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Editor.Minigames.Airplane
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Custom editor for BaseDistanceSpawner that conditionally shows/hides spawn parameters
|
||||||
|
/// based on the selected SpawnPoolMode.
|
||||||
|
/// </summary>
|
||||||
|
[CustomEditor(typeof(BaseDistanceSpawner), true)]
|
||||||
|
public class BaseDistanceSpawnerEditor : UnityEditor.Editor
|
||||||
|
{
|
||||||
|
private SerializedProperty _poolModeProperty;
|
||||||
|
private SerializedProperty _poolsProperty;
|
||||||
|
private SerializedProperty _globalMinDistanceProperty;
|
||||||
|
private SerializedProperty _globalMaxDistanceProperty;
|
||||||
|
private SerializedProperty _recencyPenaltyProperty;
|
||||||
|
private SerializedProperty _groundLayerProperty;
|
||||||
|
private SerializedProperty _maxGroundRaycastProperty;
|
||||||
|
private SerializedProperty _defaultObjectYOffsetProperty;
|
||||||
|
private SerializedProperty _showDebugLogsProperty;
|
||||||
|
|
||||||
|
protected virtual void OnEnable()
|
||||||
|
{
|
||||||
|
_poolModeProperty = serializedObject.FindProperty("poolMode");
|
||||||
|
_poolsProperty = serializedObject.FindProperty("pools");
|
||||||
|
_globalMinDistanceProperty = serializedObject.FindProperty("globalMinDistance");
|
||||||
|
_globalMaxDistanceProperty = serializedObject.FindProperty("globalMaxDistance");
|
||||||
|
_recencyPenaltyProperty = serializedObject.FindProperty("recencyPenaltyDuration");
|
||||||
|
_groundLayerProperty = serializedObject.FindProperty("groundLayer");
|
||||||
|
_maxGroundRaycastProperty = serializedObject.FindProperty("maxGroundRaycastDistance");
|
||||||
|
_defaultObjectYOffsetProperty = serializedObject.FindProperty("defaultObjectYOffset");
|
||||||
|
_showDebugLogsProperty = serializedObject.FindProperty("showDebugLogs");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnInspectorGUI()
|
||||||
|
{
|
||||||
|
serializedObject.Update();
|
||||||
|
|
||||||
|
EditorGUILayout.LabelField("Distance-Based Spawner", EditorStyles.boldLabel);
|
||||||
|
EditorGUILayout.HelpBox("Spawns objects at calculated X positions based on distance from plane. Containers are configured in AirplaneSpawnManager.", MessageType.Info);
|
||||||
|
EditorGUILayout.Space();
|
||||||
|
|
||||||
|
// Pool Configuration
|
||||||
|
EditorGUILayout.LabelField("Pool Configuration", EditorStyles.boldLabel);
|
||||||
|
EditorGUILayout.PropertyField(_poolModeProperty);
|
||||||
|
|
||||||
|
SpawnPoolMode currentMode = (SpawnPoolMode)_poolModeProperty.enumValueIndex;
|
||||||
|
|
||||||
|
// Show global parameters only in Together mode
|
||||||
|
if (currentMode == SpawnPoolMode.Together)
|
||||||
|
{
|
||||||
|
EditorGUILayout.Space();
|
||||||
|
EditorGUILayout.LabelField("Global Spawn Parameters", EditorStyles.boldLabel);
|
||||||
|
EditorGUILayout.PropertyField(_globalMinDistanceProperty);
|
||||||
|
EditorGUILayout.PropertyField(_globalMaxDistanceProperty);
|
||||||
|
|
||||||
|
EditorGUILayout.HelpBox("Together Mode: All pools use global spawn distances. Per-pool overrides are ignored.", MessageType.Info);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
EditorGUILayout.HelpBox("Exclusive Mode: Each pool spawns independently. Configure per-pool spawn distances below.", MessageType.Info);
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUILayout.Space();
|
||||||
|
|
||||||
|
// Pools array with custom display
|
||||||
|
EditorGUILayout.PropertyField(_poolsProperty, true);
|
||||||
|
|
||||||
|
// Show per-pool parameter hints
|
||||||
|
if (_poolsProperty.isExpanded && _poolsProperty.arraySize > 0)
|
||||||
|
{
|
||||||
|
EditorGUI.indentLevel++;
|
||||||
|
for (int i = 0; i < _poolsProperty.arraySize; i++)
|
||||||
|
{
|
||||||
|
var poolProperty = _poolsProperty.GetArrayElementAtIndex(i);
|
||||||
|
var overrideMinProperty = poolProperty.FindPropertyRelative("overrideMinDistance");
|
||||||
|
var overrideMaxProperty = poolProperty.FindPropertyRelative("overrideMaxDistance");
|
||||||
|
|
||||||
|
if (currentMode == SpawnPoolMode.Together)
|
||||||
|
{
|
||||||
|
// Grey out per-pool overrides in Together mode
|
||||||
|
if (overrideMinProperty.floatValue > 0 || overrideMaxProperty.floatValue > 0)
|
||||||
|
{
|
||||||
|
EditorGUILayout.HelpBox($"Pool {i}: Per-pool overrides ignored in Together mode", MessageType.Warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Show active overrides in Exclusive mode
|
||||||
|
if (overrideMinProperty.floatValue <= 0 && overrideMaxProperty.floatValue <= 0)
|
||||||
|
{
|
||||||
|
EditorGUILayout.HelpBox($"Pool {i}: Using global distances (set overrides > 0 to customize)", MessageType.Info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EditorGUI.indentLevel--;
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUILayout.Space();
|
||||||
|
|
||||||
|
// Object Positioning
|
||||||
|
EditorGUILayout.LabelField("Object Positioning", EditorStyles.boldLabel);
|
||||||
|
EditorGUILayout.PropertyField(_groundLayerProperty);
|
||||||
|
EditorGUILayout.PropertyField(_maxGroundRaycastProperty);
|
||||||
|
EditorGUILayout.PropertyField(_defaultObjectYOffsetProperty);
|
||||||
|
EditorGUILayout.HelpBox("Prefabs can use PrefabSpawnEntryComponent to specify Y positioning: SnapToGround, SpecifiedY, or RandomRange", MessageType.Info);
|
||||||
|
|
||||||
|
EditorGUILayout.Space();
|
||||||
|
|
||||||
|
// Recency Tracking
|
||||||
|
EditorGUILayout.LabelField("Recency Tracking", EditorStyles.boldLabel);
|
||||||
|
EditorGUILayout.PropertyField(_recencyPenaltyProperty);
|
||||||
|
EditorGUILayout.HelpBox("Recently spawned prefabs receive lower weight for diversity", MessageType.Info);
|
||||||
|
|
||||||
|
EditorGUILayout.Space();
|
||||||
|
|
||||||
|
// Debug
|
||||||
|
EditorGUILayout.LabelField("Debug", EditorStyles.boldLabel);
|
||||||
|
EditorGUILayout.PropertyField(_showDebugLogsProperty);
|
||||||
|
|
||||||
|
serializedObject.ApplyModifiedProperties();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 32aded175b00b8a4bae4a95861db8123
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
using Minigames.Airplane.Core.Spawning;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Editor.Minigames.Airplane
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Simplified custom editor for GroundDistanceSpawner.
|
||||||
|
/// Shows only ground-relevant fields. Ground Y and interval are in AirplaneSettings.
|
||||||
|
/// </summary>
|
||||||
|
[CustomEditor(typeof(GroundDistanceSpawner))]
|
||||||
|
public class GroundDistanceSpawnerEditor : UnityEditor.Editor
|
||||||
|
{
|
||||||
|
private SerializedProperty _poolsProperty;
|
||||||
|
|
||||||
|
private void OnEnable()
|
||||||
|
{
|
||||||
|
_poolsProperty = serializedObject.FindProperty("pools");
|
||||||
|
|
||||||
|
// Ensure exactly 1 pool exists
|
||||||
|
if (_poolsProperty.arraySize != 1)
|
||||||
|
{
|
||||||
|
_poolsProperty.arraySize = 1;
|
||||||
|
serializedObject.ApplyModifiedProperties();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnInspectorGUI()
|
||||||
|
{
|
||||||
|
serializedObject.Update();
|
||||||
|
|
||||||
|
// Ensure exactly 1 pool
|
||||||
|
if (_poolsProperty.arraySize != 1)
|
||||||
|
{
|
||||||
|
_poolsProperty.arraySize = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw single pool
|
||||||
|
EditorGUILayout.LabelField("Ground Tiles (Fixed: 1 pool)", EditorStyles.boldLabel);
|
||||||
|
|
||||||
|
var poolElement = _poolsProperty.GetArrayElementAtIndex(0);
|
||||||
|
var prefabsProperty = poolElement.FindPropertyRelative("prefabs");
|
||||||
|
|
||||||
|
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
||||||
|
|
||||||
|
EditorGUILayout.PropertyField(prefabsProperty, new GUIContent("Prefabs"), true);
|
||||||
|
|
||||||
|
if (prefabsProperty.arraySize == 0)
|
||||||
|
{
|
||||||
|
EditorGUILayout.HelpBox("Add at least one ground tile prefab", MessageType.Warning);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
EditorGUILayout.LabelField($"Prefabs: {prefabsProperty.arraySize}", EditorStyles.miniLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUILayout.EndVertical();
|
||||||
|
|
||||||
|
serializedObject.ApplyModifiedProperties();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 31225636ea0743fdbfe13fbb0c5f46af
|
||||||
|
timeCreated: 1765987013
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
using Minigames.Airplane.Core.Spawning;
|
||||||
|
using Minigames.Airplane.Data;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Editor.Minigames.Airplane
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Custom editor for ObstacleDistanceSpawner.
|
||||||
|
/// Enforces exactly 2 pools (Positive/Negative) with custom labels.
|
||||||
|
/// All spawn parameters configured in AirplaneSettings.
|
||||||
|
/// </summary>
|
||||||
|
[CustomEditor(typeof(ObstacleDistanceSpawner))]
|
||||||
|
public class ObstacleDistanceSpawnerEditor : UnityEditor.Editor
|
||||||
|
{
|
||||||
|
private SerializedProperty _poolModeProperty;
|
||||||
|
private SerializedProperty _poolsProperty;
|
||||||
|
|
||||||
|
private readonly string[] _poolNames = { "Positive Obstacles", "Negative Obstacles" };
|
||||||
|
private readonly string[] _poolDescriptions =
|
||||||
|
{
|
||||||
|
"Obstacles that benefit the player",
|
||||||
|
"Obstacles that hinder the player"
|
||||||
|
};
|
||||||
|
|
||||||
|
private bool[] _poolFoldouts = new bool[2];
|
||||||
|
|
||||||
|
private void OnEnable()
|
||||||
|
{
|
||||||
|
_poolModeProperty = serializedObject.FindProperty("poolMode");
|
||||||
|
_poolsProperty = serializedObject.FindProperty("pools");
|
||||||
|
|
||||||
|
// Ensure exactly 2 pools exist
|
||||||
|
EnsureTwoPools();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureTwoPools()
|
||||||
|
{
|
||||||
|
if (_poolsProperty != null && _poolsProperty.arraySize != 2)
|
||||||
|
{
|
||||||
|
_poolsProperty.arraySize = 2;
|
||||||
|
|
||||||
|
// Initialize pool descriptions
|
||||||
|
for (int i = 0; i < 2; i++)
|
||||||
|
{
|
||||||
|
var poolElement = _poolsProperty.GetArrayElementAtIndex(i);
|
||||||
|
var descProperty = poolElement.FindPropertyRelative("description");
|
||||||
|
if (descProperty != null && string.IsNullOrEmpty(descProperty.stringValue))
|
||||||
|
{
|
||||||
|
descProperty.stringValue = _poolNames[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
serializedObject.ApplyModifiedProperties();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnInspectorGUI()
|
||||||
|
{
|
||||||
|
serializedObject.Update();
|
||||||
|
|
||||||
|
EditorGUILayout.HelpBox("Note: Positive/Negative ratio, unlock times, spawn distances, recency tracking, positioning, and debug settings are configured globally in AirplaneSettings. Containers are configured in AirplaneSpawnManager.", MessageType.Info);
|
||||||
|
EditorGUILayout.Space();
|
||||||
|
|
||||||
|
if (_poolModeProperty != null)
|
||||||
|
{
|
||||||
|
EditorGUILayout.PropertyField(_poolModeProperty);
|
||||||
|
|
||||||
|
SpawnPoolMode currentMode = (SpawnPoolMode)_poolModeProperty.enumValueIndex;
|
||||||
|
|
||||||
|
if (currentMode == SpawnPoolMode.Together)
|
||||||
|
{
|
||||||
|
EditorGUILayout.HelpBox("Together Mode: Both pools share a single spawn stream using global distances from settings.", MessageType.Info);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
EditorGUILayout.HelpBox("Exclusive Mode: Each pool spawns independently. Use per-pool overrides or global distances from settings.", MessageType.Info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUILayout.Space();
|
||||||
|
|
||||||
|
// Obstacle Pools (exactly 2, custom display)
|
||||||
|
EditorGUILayout.LabelField("Obstacle Pools (Fixed: 2 pools)", EditorStyles.boldLabel);
|
||||||
|
|
||||||
|
EnsureTwoPools();
|
||||||
|
|
||||||
|
if (_poolsProperty != null && _poolsProperty.arraySize == 2)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 2; i++)
|
||||||
|
{
|
||||||
|
var poolElement = _poolsProperty.GetArrayElementAtIndex(i);
|
||||||
|
var prefabsProperty = poolElement.FindPropertyRelative("prefabs");
|
||||||
|
var descriptionProperty = poolElement.FindPropertyRelative("description");
|
||||||
|
|
||||||
|
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
||||||
|
|
||||||
|
// Foldout with custom label
|
||||||
|
_poolFoldouts[i] = EditorGUILayout.Foldout(_poolFoldouts[i], $"{_poolNames[i]} (Pool {i})", true, EditorStyles.foldoutHeader);
|
||||||
|
|
||||||
|
if (_poolFoldouts[i])
|
||||||
|
{
|
||||||
|
EditorGUI.indentLevel++;
|
||||||
|
|
||||||
|
EditorGUILayout.LabelField(_poolDescriptions[i], EditorStyles.miniLabel);
|
||||||
|
EditorGUILayout.Space(5);
|
||||||
|
|
||||||
|
if (prefabsProperty != null)
|
||||||
|
{
|
||||||
|
EditorGUILayout.PropertyField(prefabsProperty, new GUIContent("Prefabs"), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (descriptionProperty != null)
|
||||||
|
{
|
||||||
|
EditorGUILayout.PropertyField(descriptionProperty, new GUIContent("Description"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show info about prefab count
|
||||||
|
if (prefabsProperty != null)
|
||||||
|
{
|
||||||
|
if (prefabsProperty.arraySize == 0)
|
||||||
|
{
|
||||||
|
EditorGUILayout.HelpBox("Add prefabs for this pool", MessageType.Warning);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
EditorGUILayout.LabelField($"Prefabs: {prefabsProperty.arraySize}", EditorStyles.miniLabel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUI.indentLevel--;
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUILayout.EndVertical();
|
||||||
|
EditorGUILayout.Space(5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUILayout.Space();
|
||||||
|
EditorGUILayout.HelpBox("Note: Positive/Negative ratio, unlock times, spawn distances, recency tracking, positioning, and debug settings are configured globally in AirplaneSettings. Containers are configured in AirplaneSpawnManager.", MessageType.Info);
|
||||||
|
|
||||||
|
serializedObject.ApplyModifiedProperties();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5d7f009b2123488381edd87bfb3ab2e8
|
||||||
|
timeCreated: 1765985361
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
using Minigames.Airplane.Core.Spawning;
|
||||||
|
using Minigames.Airplane.Data;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Editor.Minigames.Airplane
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Custom editor for ParallaxBackgroundSpawner.
|
||||||
|
/// Enforces exactly 3 pools with custom labels (Background/Middle/Foreground).
|
||||||
|
/// Prevents array manipulation and provides clean, designer-friendly interface.
|
||||||
|
/// </summary>
|
||||||
|
[CustomEditor(typeof(ParallaxBackgroundSpawner))]
|
||||||
|
public class ParallaxBackgroundSpawnerEditor : UnityEditor.Editor
|
||||||
|
{
|
||||||
|
private SerializedProperty _poolsProperty;
|
||||||
|
private SerializedProperty _backgroundSortLayerProperty;
|
||||||
|
private SerializedProperty _middleSortLayerProperty;
|
||||||
|
private SerializedProperty _foregroundSortLayerProperty;
|
||||||
|
private SerializedProperty _cameraManagerProperty;
|
||||||
|
private SerializedProperty _showDebugLogsProperty;
|
||||||
|
|
||||||
|
private readonly string[] _layerNames = { "Background Layer", "Middle Layer", "Foreground Layer" };
|
||||||
|
private readonly string[] _layerDescriptions =
|
||||||
|
{
|
||||||
|
"Slowest parallax - furthest back",
|
||||||
|
"Medium parallax - middle depth",
|
||||||
|
"Fastest parallax - closest to camera"
|
||||||
|
};
|
||||||
|
|
||||||
|
private bool[] _poolFoldouts = new bool[3];
|
||||||
|
|
||||||
|
private void OnEnable()
|
||||||
|
{
|
||||||
|
_poolsProperty = serializedObject.FindProperty("pools");
|
||||||
|
_backgroundSortLayerProperty = serializedObject.FindProperty("backgroundSortLayer");
|
||||||
|
_middleSortLayerProperty = serializedObject.FindProperty("middleSortLayer");
|
||||||
|
_foregroundSortLayerProperty = serializedObject.FindProperty("foregroundSortLayer");
|
||||||
|
_cameraManagerProperty = serializedObject.FindProperty("cameraManager");
|
||||||
|
_showDebugLogsProperty = serializedObject.FindProperty("showDebugLogs");
|
||||||
|
|
||||||
|
// Ensure exactly 3 pools exist
|
||||||
|
EnsureThreePools();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureThreePools()
|
||||||
|
{
|
||||||
|
if (_poolsProperty.arraySize != 3)
|
||||||
|
{
|
||||||
|
_poolsProperty.arraySize = 3;
|
||||||
|
|
||||||
|
// Initialize pool descriptions
|
||||||
|
for (int i = 0; i < 3; i++)
|
||||||
|
{
|
||||||
|
var poolElement = _poolsProperty.GetArrayElementAtIndex(i);
|
||||||
|
var descProperty = poolElement.FindPropertyRelative("description");
|
||||||
|
if (string.IsNullOrEmpty(descProperty.stringValue))
|
||||||
|
{
|
||||||
|
descProperty.stringValue = _layerNames[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
serializedObject.ApplyModifiedProperties();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnInspectorGUI()
|
||||||
|
{
|
||||||
|
serializedObject.Update();
|
||||||
|
EditorGUILayout.HelpBox("Note: Spawn distances, recency tracking, and debug settings are configured globally in AirplaneSettings. Containers are configured in AirplaneSpawnManager.", MessageType.Info);
|
||||||
|
EditorGUILayout.Space();
|
||||||
|
|
||||||
|
// Camera Integration
|
||||||
|
EditorGUILayout.PropertyField(_cameraManagerProperty);
|
||||||
|
EditorGUILayout.Space();
|
||||||
|
|
||||||
|
// Sort Layers
|
||||||
|
EditorGUILayout.LabelField("Sort Layer Configuration", EditorStyles.boldLabel);
|
||||||
|
EditorGUILayout.PropertyField(_backgroundSortLayerProperty, new GUIContent("Background Sort Layer"));
|
||||||
|
EditorGUILayout.PropertyField(_middleSortLayerProperty, new GUIContent("Middle Sort Layer"));
|
||||||
|
EditorGUILayout.PropertyField(_foregroundSortLayerProperty, new GUIContent("Foreground Sort Layer"));
|
||||||
|
EditorGUILayout.Space();
|
||||||
|
|
||||||
|
// Pool Mode (locked to Exclusive)
|
||||||
|
EditorGUILayout.LabelField("Spawn Mode", EditorStyles.boldLabel);
|
||||||
|
EditorGUI.BeginDisabledGroup(true);
|
||||||
|
EditorGUILayout.TextField("Pool Mode", "Exclusive (Fixed)");
|
||||||
|
EditorGUI.EndDisabledGroup();
|
||||||
|
EditorGUILayout.HelpBox("Parallax spawner always uses Exclusive mode - each layer spawns independently", MessageType.Info);
|
||||||
|
EditorGUILayout.Space();
|
||||||
|
|
||||||
|
// Parallax Layers (exactly 3, custom display)
|
||||||
|
EditorGUILayout.LabelField("Parallax Layers (Fixed: 3 layers)", EditorStyles.boldLabel);
|
||||||
|
|
||||||
|
EnsureThreePools(); // Safety check
|
||||||
|
|
||||||
|
for (int i = 0; i < 3; i++)
|
||||||
|
{
|
||||||
|
var poolElement = _poolsProperty.GetArrayElementAtIndex(i);
|
||||||
|
var prefabsProperty = poolElement.FindPropertyRelative("prefabs");
|
||||||
|
var unlockTimeProperty = poolElement.FindPropertyRelative("unlockTime");
|
||||||
|
var descProperty = poolElement.FindPropertyRelative("description");
|
||||||
|
|
||||||
|
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
||||||
|
|
||||||
|
// Foldout with custom label
|
||||||
|
_poolFoldouts[i] = EditorGUILayout.Foldout(_poolFoldouts[i], $"{_layerNames[i]} (Pool {i})", true, EditorStyles.foldoutHeader);
|
||||||
|
|
||||||
|
if (_poolFoldouts[i])
|
||||||
|
{
|
||||||
|
EditorGUI.indentLevel++;
|
||||||
|
|
||||||
|
EditorGUILayout.LabelField(_layerDescriptions[i], EditorStyles.miniLabel);
|
||||||
|
EditorGUILayout.Space(5);
|
||||||
|
|
||||||
|
EditorGUILayout.PropertyField(prefabsProperty, new GUIContent("Prefabs"), true);
|
||||||
|
EditorGUILayout.PropertyField(unlockTimeProperty, new GUIContent("Unlock Time (seconds)"));
|
||||||
|
|
||||||
|
// Show info about prefab count
|
||||||
|
if (prefabsProperty.arraySize == 0)
|
||||||
|
{
|
||||||
|
EditorGUILayout.HelpBox("Add prefabs for this layer", MessageType.Warning);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
EditorGUILayout.LabelField($"Prefabs: {prefabsProperty.arraySize}", EditorStyles.miniLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUI.indentLevel--;
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUILayout.EndVertical();
|
||||||
|
EditorGUILayout.Space(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUILayout.Space();
|
||||||
|
|
||||||
|
serializedObject.ApplyModifiedProperties();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 41d4944f4ed9436f84384fb2361f6e0c
|
||||||
|
timeCreated: 1765972253
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
using Minigames.Airplane.Data;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Editor.Minigames.Airplane
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Custom editor for PrefabSpawnEntryComponent that conditionally shows fields based on spawn mode.
|
||||||
|
/// </summary>
|
||||||
|
[CustomEditor(typeof(PrefabSpawnEntryComponent))]
|
||||||
|
public class PrefabSpawnEntryComponentEditor : UnityEditor.Editor
|
||||||
|
{
|
||||||
|
private SerializedProperty _spawnPositionModeProperty;
|
||||||
|
private SerializedProperty _specifiedYProperty;
|
||||||
|
private SerializedProperty _randomYMinProperty;
|
||||||
|
private SerializedProperty _randomYMaxProperty;
|
||||||
|
|
||||||
|
private void OnEnable()
|
||||||
|
{
|
||||||
|
_spawnPositionModeProperty = serializedObject.FindProperty("spawnPositionMode");
|
||||||
|
_specifiedYProperty = serializedObject.FindProperty("specifiedY");
|
||||||
|
_randomYMinProperty = serializedObject.FindProperty("randomYMin");
|
||||||
|
_randomYMaxProperty = serializedObject.FindProperty("randomYMax");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnInspectorGUI()
|
||||||
|
{
|
||||||
|
serializedObject.Update();
|
||||||
|
|
||||||
|
EditorGUILayout.LabelField("Prefab Spawn Positioning", EditorStyles.boldLabel);
|
||||||
|
EditorGUILayout.HelpBox("Controls how this prefab is positioned vertically when spawned. If this component is missing, spawner uses default settings from AirplaneSettings.", MessageType.Info);
|
||||||
|
EditorGUILayout.Space();
|
||||||
|
|
||||||
|
// Always show the mode selector
|
||||||
|
EditorGUILayout.PropertyField(_spawnPositionModeProperty, new GUIContent("Spawn Position Mode"));
|
||||||
|
|
||||||
|
SpawnPositionMode currentMode = (SpawnPositionMode)_spawnPositionModeProperty.enumValueIndex;
|
||||||
|
|
||||||
|
EditorGUILayout.Space();
|
||||||
|
|
||||||
|
// Show mode-specific fields
|
||||||
|
switch (currentMode)
|
||||||
|
{
|
||||||
|
case SpawnPositionMode.SnapToGround:
|
||||||
|
EditorGUILayout.HelpBox("Object will raycast down to find ground and snap its bottom to the surface. If no ground is found, fallback Y position from settings will be used.", MessageType.Info);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SpawnPositionMode.SpecifiedY:
|
||||||
|
EditorGUILayout.LabelField("Fixed Y Position", EditorStyles.boldLabel);
|
||||||
|
EditorGUILayout.PropertyField(_specifiedYProperty, new GUIContent("Y Position"));
|
||||||
|
EditorGUILayout.HelpBox("Object will spawn at this exact Y coordinate.", MessageType.Info);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SpawnPositionMode.RandomRange:
|
||||||
|
EditorGUILayout.LabelField("Random Y Range", EditorStyles.boldLabel);
|
||||||
|
EditorGUILayout.PropertyField(_randomYMinProperty, new GUIContent("Min Y"));
|
||||||
|
EditorGUILayout.PropertyField(_randomYMaxProperty, new GUIContent("Max Y"));
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
if (_randomYMinProperty.floatValue > _randomYMaxProperty.floatValue)
|
||||||
|
{
|
||||||
|
EditorGUILayout.HelpBox("Min Y should be less than Max Y!", MessageType.Warning);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
EditorGUILayout.HelpBox($"Object will spawn at random Y between {_randomYMinProperty.floatValue:F2} and {_randomYMaxProperty.floatValue:F2}.", MessageType.Info);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
serializedObject.ApplyModifiedProperties();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 0eccbe64a21c4c07a09b2df66faf43b1
|
||||||
|
timeCreated: 1765990535
|
||||||
@@ -181,12 +181,21 @@ namespace AppleHills.Core.Settings.Editor
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EditorGUILayout.Space(10);
|
||||||
|
|
||||||
|
// Use CreateEditor to respect custom editors (like AirplaneSettingsEditor)
|
||||||
|
UnityEditor.Editor editor = UnityEditor.Editor.CreateEditor(settings);
|
||||||
|
if (editor != null)
|
||||||
|
{
|
||||||
|
editor.OnInspectorGUI();
|
||||||
|
DestroyImmediate(editor);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Fallback to default drawing if no custom editor exists
|
||||||
SerializedObject serializedObj = serializedSettingsObjects[typeof(T).Name];
|
SerializedObject serializedObj = serializedSettingsObjects[typeof(T).Name];
|
||||||
serializedObj.Update();
|
serializedObj.Update();
|
||||||
|
|
||||||
EditorGUILayout.Space(10);
|
|
||||||
|
|
||||||
// Draw all properties
|
|
||||||
SerializedProperty property = serializedObj.GetIterator();
|
SerializedProperty property = serializedObj.GetIterator();
|
||||||
bool enterChildren = true;
|
bool enterChildren = true;
|
||||||
while (property.NextVisible(enterChildren))
|
while (property.NextVisible(enterChildren))
|
||||||
@@ -205,6 +214,7 @@ namespace AppleHills.Core.Settings.Editor
|
|||||||
EditorUtility.SetDirty(settings);
|
EditorUtility.SetDirty(settings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Helper method to highlight important fields
|
// Helper method to highlight important fields
|
||||||
private void DrawHighlightedProperty(SerializedProperty property, string tooltip = null)
|
private void DrawHighlightedProperty(SerializedProperty property, string tooltip = null)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ GameObject:
|
|||||||
- component: {fileID: 4572188480515029494}
|
- component: {fileID: 4572188480515029494}
|
||||||
- component: {fileID: 6979039544463298865}
|
- component: {fileID: 6979039544463298865}
|
||||||
- component: {fileID: 1989378388080844566}
|
- component: {fileID: 1989378388080844566}
|
||||||
|
- component: {fileID: 7567962733712432860}
|
||||||
m_Layer: 0
|
m_Layer: 0
|
||||||
m_Name: AirplaneWindZone
|
m_Name: AirplaneWindZone
|
||||||
m_TagString: Untagged
|
m_TagString: Untagged
|
||||||
@@ -96,6 +97,22 @@ MonoBehaviour:
|
|||||||
isWorldSpace: 1
|
isWorldSpace: 1
|
||||||
windParticles: {fileID: 0}
|
windParticles: {fileID: 0}
|
||||||
showDebugLogs: 0
|
showDebugLogs: 0
|
||||||
|
--- !u!114 &7567962733712432860
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 497267990420767357}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: 90239fb003214b4087d0717f6f417161, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: AppleHillsScripts::Minigames.Airplane.Data.PrefabSpawnEntryComponent
|
||||||
|
spawnPositionMode: 0
|
||||||
|
specifiedY: 0
|
||||||
|
randomYMin: -5
|
||||||
|
randomYMax: 5
|
||||||
--- !u!1 &2715744416533832886
|
--- !u!1 &2715744416533832886
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ GameObject:
|
|||||||
- component: {fileID: 8126210844742366787}
|
- component: {fileID: 8126210844742366787}
|
||||||
- component: {fileID: 7874709284498167353}
|
- component: {fileID: 7874709284498167353}
|
||||||
- component: {fileID: 806255670199755721}
|
- component: {fileID: 806255670199755721}
|
||||||
|
- component: {fileID: 6677313130545265604}
|
||||||
m_Layer: 0
|
m_Layer: 0
|
||||||
m_Name: BouncySurface
|
m_Name: BouncySurface
|
||||||
m_TagString: Untagged
|
m_TagString: Untagged
|
||||||
@@ -160,3 +161,19 @@ SpriteRenderer:
|
|||||||
m_SpriteTileMode: 0
|
m_SpriteTileMode: 0
|
||||||
m_WasSpriteAssigned: 1
|
m_WasSpriteAssigned: 1
|
||||||
m_SpriteSortPoint: 0
|
m_SpriteSortPoint: 0
|
||||||
|
--- !u!114 &6677313130545265604
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1917678391913987792}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: 90239fb003214b4087d0717f6f417161, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: AppleHillsScripts::Minigames.Airplane.Data.PrefabSpawnEntryComponent
|
||||||
|
spawnPositionMode: 0
|
||||||
|
specifiedY: 0
|
||||||
|
randomYMin: -5
|
||||||
|
randomYMax: 5
|
||||||
|
|||||||
@@ -173,6 +173,37 @@ MonoBehaviour:
|
|||||||
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
|
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
|
||||||
m_Name:
|
m_Name:
|
||||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Button
|
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Button
|
||||||
|
--- !u!1 &39044400
|
||||||
|
GameObject:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
serializedVersion: 6
|
||||||
|
m_Component:
|
||||||
|
- component: {fileID: 39044401}
|
||||||
|
m_Layer: 0
|
||||||
|
m_Name: GameObject (1)
|
||||||
|
m_TagString: Untagged
|
||||||
|
m_Icon: {fileID: 0}
|
||||||
|
m_NavMeshLayer: 0
|
||||||
|
m_StaticEditorFlags: 0
|
||||||
|
m_IsActive: 1
|
||||||
|
--- !u!4 &39044401
|
||||||
|
Transform:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 39044400}
|
||||||
|
serializedVersion: 2
|
||||||
|
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
|
m_LocalPosition: {x: 78.47819, y: 25.5, z: 0}
|
||||||
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
|
m_ConstrainProportionsScale: 0
|
||||||
|
m_Children: []
|
||||||
|
m_Father: {fileID: 0}
|
||||||
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
--- !u!1 &108199298
|
--- !u!1 &108199298
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -584,6 +615,37 @@ Transform:
|
|||||||
m_CorrespondingSourceObject: {fileID: 5380908876971534942, guid: a9b4569fcc08080479d99b9c3bcee089, type: 3}
|
m_CorrespondingSourceObject: {fileID: 5380908876971534942, guid: a9b4569fcc08080479d99b9c3bcee089, type: 3}
|
||||||
m_PrefabInstance: {fileID: 453576884181909409}
|
m_PrefabInstance: {fileID: 453576884181909409}
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
--- !u!1 &377530944
|
||||||
|
GameObject:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
serializedVersion: 6
|
||||||
|
m_Component:
|
||||||
|
- component: {fileID: 377530945}
|
||||||
|
m_Layer: 0
|
||||||
|
m_Name: SpawnedParalax
|
||||||
|
m_TagString: Untagged
|
||||||
|
m_Icon: {fileID: 0}
|
||||||
|
m_NavMeshLayer: 0
|
||||||
|
m_StaticEditorFlags: 0
|
||||||
|
m_IsActive: 1
|
||||||
|
--- !u!4 &377530945
|
||||||
|
Transform:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 377530944}
|
||||||
|
serializedVersion: 2
|
||||||
|
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
|
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||||
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
|
m_ConstrainProportionsScale: 0
|
||||||
|
m_Children: []
|
||||||
|
m_Father: {fileID: 1784207402}
|
||||||
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
--- !u!1 &469774135
|
--- !u!1 &469774135
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -2128,7 +2190,7 @@ GameObject:
|
|||||||
m_Component:
|
m_Component:
|
||||||
- component: {fileID: 1219431443}
|
- component: {fileID: 1219431443}
|
||||||
m_Layer: 0
|
m_Layer: 0
|
||||||
m_Name: SpawnedObjects
|
m_Name: SpawnedTargets
|
||||||
m_TagString: Untagged
|
m_TagString: Untagged
|
||||||
m_Icon: {fileID: 0}
|
m_Icon: {fileID: 0}
|
||||||
m_NavMeshLayer: 0
|
m_NavMeshLayer: 0
|
||||||
@@ -2467,7 +2529,7 @@ GameObject:
|
|||||||
m_Component:
|
m_Component:
|
||||||
- component: {fileID: 1405572709}
|
- component: {fileID: 1405572709}
|
||||||
m_Layer: 0
|
m_Layer: 0
|
||||||
m_Name: SpawnContainer
|
m_Name: SpawnedObstacles
|
||||||
m_TagString: Untagged
|
m_TagString: Untagged
|
||||||
m_Icon: {fileID: 0}
|
m_Icon: {fileID: 0}
|
||||||
m_NavMeshLayer: 0
|
m_NavMeshLayer: 0
|
||||||
@@ -3734,6 +3796,8 @@ GameObject:
|
|||||||
- component: {fileID: 1784207402}
|
- component: {fileID: 1784207402}
|
||||||
- component: {fileID: 1784207403}
|
- component: {fileID: 1784207403}
|
||||||
- component: {fileID: 1784207404}
|
- component: {fileID: 1784207404}
|
||||||
|
- component: {fileID: 1784207406}
|
||||||
|
- component: {fileID: 1784207405}
|
||||||
m_Layer: 0
|
m_Layer: 0
|
||||||
m_Name: SpawnManager
|
m_Name: SpawnManager
|
||||||
m_TagString: Untagged
|
m_TagString: Untagged
|
||||||
@@ -3750,13 +3814,14 @@ Transform:
|
|||||||
m_GameObject: {fileID: 1784207401}
|
m_GameObject: {fileID: 1784207401}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||||
m_LocalPosition: {x: 17.2, y: -0.1, z: 0}
|
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children:
|
m_Children:
|
||||||
- {fileID: 1219431443}
|
- {fileID: 1219431443}
|
||||||
- {fileID: 1287102785}
|
|
||||||
- {fileID: 1405572709}
|
- {fileID: 1405572709}
|
||||||
|
- {fileID: 1287102785}
|
||||||
|
- {fileID: 377530945}
|
||||||
m_Father: {fileID: 0}
|
m_Father: {fileID: 0}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
--- !u!114 &1784207403
|
--- !u!114 &1784207403
|
||||||
@@ -3771,9 +3836,9 @@ MonoBehaviour:
|
|||||||
m_Script: {fileID: 11500000, guid: 70f14ee4b04b46b793ec2652fd2ca7b9, type: 3}
|
m_Script: {fileID: 11500000, guid: 70f14ee4b04b46b793ec2652fd2ca7b9, type: 3}
|
||||||
m_Name:
|
m_Name:
|
||||||
m_EditorClassIdentifier: AppleHillsScripts::Minigames.Airplane.Core.AirplaneSpawnManager
|
m_EditorClassIdentifier: AppleHillsScripts::Minigames.Airplane.Core.AirplaneSpawnManager
|
||||||
obstacleSpawner: {fileID: 0}
|
obstacleSpawner: {fileID: 1784207404}
|
||||||
groundSpawner: {fileID: 0}
|
groundSpawner: {fileID: 1784207406}
|
||||||
parallaxSpawner: {fileID: 0}
|
parallaxSpawner: {fileID: 1784207405}
|
||||||
targetPrefabs:
|
targetPrefabs:
|
||||||
- targetKey: bob_target
|
- targetKey: bob_target
|
||||||
prefab: {fileID: 3207629437433571205, guid: 9f4bb48933059e543b60ac782d2140d8, type: 3}
|
prefab: {fileID: 3207629437433571205, guid: 9f4bb48933059e543b60ac782d2140d8, type: 3}
|
||||||
@@ -3790,7 +3855,10 @@ MonoBehaviour:
|
|||||||
targetDisplayUI: {fileID: 1520040329}
|
targetDisplayUI: {fileID: 1520040329}
|
||||||
launchController: {fileID: 1309397785}
|
launchController: {fileID: 1309397785}
|
||||||
dynamicSpawnThresholdMarker: {fileID: 1653173574}
|
dynamicSpawnThresholdMarker: {fileID: 1653173574}
|
||||||
targetParent: {fileID: 1405572709}
|
obstacleContainer: {fileID: 1405572709}
|
||||||
|
groundContainer: {fileID: 1287102785}
|
||||||
|
parallaxContainer: {fileID: 377530945}
|
||||||
|
targetParent: {fileID: 1219431443}
|
||||||
showDebugLogs: 0
|
showDebugLogs: 0
|
||||||
--- !u!114 &1784207404
|
--- !u!114 &1784207404
|
||||||
MonoBehaviour:
|
MonoBehaviour:
|
||||||
@@ -3805,19 +3873,76 @@ MonoBehaviour:
|
|||||||
m_Name:
|
m_Name:
|
||||||
m_EditorClassIdentifier: AppleHillsScripts::Minigames.Airplane.Core.Spawning.ObstacleDistanceSpawner
|
m_EditorClassIdentifier: AppleHillsScripts::Minigames.Airplane.Core.Spawning.ObstacleDistanceSpawner
|
||||||
poolMode: 0
|
poolMode: 0
|
||||||
pools: []
|
pools:
|
||||||
globalMinDistance: 5
|
- prefabs:
|
||||||
globalMaxDistance: 15
|
- {fileID: 497267990420767357, guid: dc32284941c693c4f9e422f4197ca61e, type: 3}
|
||||||
spawnPoint: {fileID: 0}
|
- {fileID: 1917678391913987792, guid: 989121c9099e41e469824ddeaf0e34a5, type: 3}
|
||||||
spawnContainer: {fileID: 0}
|
- {fileID: 7032677151789119314, guid: 7dc33e43acead834ba6a231b67cfd2d9, type: 3}
|
||||||
recencyPenaltyDuration: 10
|
unlockTime: 0
|
||||||
showDebugLogs: 0
|
description: Base pool of positive obstacles
|
||||||
positiveNegativeRatio: 0.5
|
overrideMinDistance: 0
|
||||||
positivePoolIndices:
|
overrideMaxDistance: 0
|
||||||
negativePoolIndices:
|
- prefabs:
|
||||||
groundLayer: 0
|
- {fileID: 1186710456879913970, guid: 006b956651124704dbae5bd4faab3152, type: 3}
|
||||||
maxGroundRaycastDistance: 50
|
- {fileID: 2434350760695575337, guid: f3188909ff4e845499a5cbfd0ae93101, type: 3}
|
||||||
defaultObjectYOffset: 0
|
unlockTime: 0
|
||||||
|
description: Base pool of negative obstacles
|
||||||
|
overrideMinDistance: 0
|
||||||
|
overrideMaxDistance: 0
|
||||||
|
--- !u!114 &1784207405
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1784207401}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: b126f2d189024140ac0bc6fb14155c7f, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: AppleHillsScripts::Minigames.Airplane.Core.Spawning.ParallaxBackgroundSpawner
|
||||||
|
poolMode: 0
|
||||||
|
pools:
|
||||||
|
- prefabs: []
|
||||||
|
unlockTime: 0
|
||||||
|
description: Background Layer
|
||||||
|
overrideMinDistance: 0
|
||||||
|
overrideMaxDistance: 0
|
||||||
|
- prefabs: []
|
||||||
|
unlockTime: 0
|
||||||
|
description: Middle Layer
|
||||||
|
overrideMinDistance: 0
|
||||||
|
overrideMaxDistance: 0
|
||||||
|
- prefabs: []
|
||||||
|
unlockTime: 0
|
||||||
|
description: Foreground Layer
|
||||||
|
overrideMinDistance: 0
|
||||||
|
overrideMaxDistance: 0
|
||||||
|
backgroundSortLayer: Background
|
||||||
|
middleSortLayer: Midground
|
||||||
|
foregroundSortLayer: Foreground
|
||||||
|
cameraManager: {fileID: 0}
|
||||||
|
--- !u!114 &1784207406
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1784207401}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: 76db01f1871c4b9ea66fb981b01b3ee1, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: AppleHillsScripts::Minigames.Airplane.Core.Spawning.GroundDistanceSpawner
|
||||||
|
poolMode: 0
|
||||||
|
pools:
|
||||||
|
- prefabs:
|
||||||
|
- {fileID: 5175967588203935335, guid: a9b4569fcc08080479d99b9c3bcee089, type: 3}
|
||||||
|
unlockTime: 0
|
||||||
|
description:
|
||||||
|
overrideMinDistance: 0
|
||||||
|
overrideMaxDistance: 0
|
||||||
|
groundSpawnY: -18
|
||||||
--- !u!1 &1810521056
|
--- !u!1 &1810521056
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -5687,3 +5812,4 @@ SceneRoots:
|
|||||||
- {fileID: 1701327424}
|
- {fileID: 1701327424}
|
||||||
- {fileID: 2041920296}
|
- {fileID: 2041920296}
|
||||||
- {fileID: 1710395163}
|
- {fileID: 1710395163}
|
||||||
|
- {fileID: 39044401}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using AppleHills.Core.Settings;
|
using AppleHills.Core.Settings;
|
||||||
|
using Minigames.Airplane.Data;
|
||||||
using Minigames.StatueDressup.Data;
|
using Minigames.StatueDressup.Data;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
@@ -344,11 +345,18 @@ namespace Core.Settings
|
|||||||
float PositiveNegativeRatio { get; } // 0-1, where 1 = all positive, 0 = all negative
|
float PositiveNegativeRatio { get; } // 0-1, where 1 = all positive, 0 = all negative
|
||||||
float SpawnDistanceAhead { get; }
|
float SpawnDistanceAhead { get; }
|
||||||
float GroundSpawnInterval { get; }
|
float GroundSpawnInterval { get; }
|
||||||
|
float RecencyPenaltyDuration { get; } // Time penalty for recently-spawned prefabs
|
||||||
|
|
||||||
// Ground Snapping
|
// Ground Snapping
|
||||||
int GroundLayer { get; }
|
int GroundLayer { get; }
|
||||||
float MaxGroundRaycastDistance { get; }
|
float MaxGroundRaycastDistance { get; }
|
||||||
float DefaultObjectYOffset { get; }
|
float FallbackYPosition { get; } // Y position when SnapToGround fails OR when using SpecifiedY mode (universal fallback)
|
||||||
|
float GroundSpawnY { get; }
|
||||||
|
|
||||||
|
// Default Obstacle Positioning (used when prefab has no PrefabSpawnEntryComponent)
|
||||||
|
SpawnPositionMode DefaultObstaclePositionMode { get; }
|
||||||
|
float DefaultObstacleRandomYMin { get; } // Min Y when DefaultObstaclePositionMode = RandomRange
|
||||||
|
float DefaultObstacleRandomYMax { get; } // Max Y when DefaultObstaclePositionMode = RandomRange
|
||||||
|
|
||||||
// Debug
|
// Debug
|
||||||
bool ShowDebugLogs { get; }
|
bool ShowDebugLogs { get; }
|
||||||
|
|||||||
@@ -72,6 +72,16 @@ namespace Minigames.Airplane.Core
|
|||||||
[Tooltip("Transform marker in scene where dynamic spawning begins (uses X position)")]
|
[Tooltip("Transform marker in scene where dynamic spawning begins (uses X position)")]
|
||||||
[SerializeField] private Transform dynamicSpawnThresholdMarker;
|
[SerializeField] private Transform dynamicSpawnThresholdMarker;
|
||||||
|
|
||||||
|
[Header("Spawn Organization")]
|
||||||
|
[Tooltip("Parent transform for spawned obstacles (organization)")]
|
||||||
|
[SerializeField] private Transform obstacleContainer;
|
||||||
|
|
||||||
|
[Tooltip("Parent transform for spawned ground tiles (organization)")]
|
||||||
|
[SerializeField] private Transform groundContainer;
|
||||||
|
|
||||||
|
[Tooltip("Parent transform for spawned parallax elements (organization)")]
|
||||||
|
[SerializeField] private Transform parallaxContainer;
|
||||||
|
|
||||||
[Header("Spawn Parents")]
|
[Header("Spawn Parents")]
|
||||||
[Tooltip("Parent transform for spawned target (optional)")]
|
[Tooltip("Parent transform for spawned target (optional)")]
|
||||||
[SerializeField] private Transform targetParent;
|
[SerializeField] private Transform targetParent;
|
||||||
@@ -242,6 +252,14 @@ namespace Minigames.Airplane.Core
|
|||||||
if (groundSpawner != null)
|
if (groundSpawner != null)
|
||||||
{
|
{
|
||||||
groundSpawner.PreSpawn(preSpawnStartX, preSpawnEndX);
|
groundSpawner.PreSpawn(preSpawnStartX, preSpawnEndX);
|
||||||
|
|
||||||
|
// Force physics system to update so ground colliders are available for raycasting
|
||||||
|
Physics2D.SyncTransforms();
|
||||||
|
|
||||||
|
if (showDebugLogs)
|
||||||
|
{
|
||||||
|
Logging.Debug("[AirplaneSpawnManager] Physics synced after ground spawn");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Spawn parallax background (if assigned)
|
// 2. Spawn parallax background (if assigned)
|
||||||
@@ -250,7 +268,7 @@ namespace Minigames.Airplane.Core
|
|||||||
parallaxSpawner.PreSpawn(preSpawnStartX, preSpawnEndX);
|
parallaxSpawner.PreSpawn(preSpawnStartX, preSpawnEndX);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Spawn obstacles (after ground exists)
|
// 3. Spawn obstacles (after ground exists and physics updated)
|
||||||
if (obstacleSpawner != null)
|
if (obstacleSpawner != null)
|
||||||
{
|
{
|
||||||
obstacleSpawner.PreSpawn(preSpawnStartX, preSpawnEndX);
|
obstacleSpawner.PreSpawn(preSpawnStartX, preSpawnEndX);
|
||||||
@@ -279,33 +297,10 @@ namespace Minigames.Airplane.Core
|
|||||||
_isSpawningActive = true;
|
_isSpawningActive = true;
|
||||||
_gameTime = 0f;
|
_gameTime = 0f;
|
||||||
|
|
||||||
// Start tracking in spawners
|
|
||||||
float startX = _lastSpawnedX;
|
|
||||||
|
|
||||||
if (obstacleSpawner != null)
|
|
||||||
{
|
|
||||||
obstacleSpawner.StartTracking(planeTransform, startX);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (groundSpawner != null)
|
|
||||||
{
|
|
||||||
groundSpawner.StartTracking(planeTransform, startX);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parallaxSpawner != null)
|
|
||||||
{
|
|
||||||
parallaxSpawner.StartTracking(planeTransform, startX);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start UI tracking with calculated target position
|
// Start UI tracking with calculated target position
|
||||||
if (targetDisplayUI != null)
|
if (targetDisplayUI != null)
|
||||||
{
|
{
|
||||||
targetDisplayUI.StartTracking(planeTransform);
|
targetDisplayUI.StartTracking(planeTransform);
|
||||||
|
|
||||||
if (showDebugLogs)
|
|
||||||
{
|
|
||||||
Logging.Debug("[AirplaneSpawnManager] UI tracking started");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showDebugLogs)
|
if (showDebugLogs)
|
||||||
@@ -322,22 +317,6 @@ namespace Minigames.Airplane.Core
|
|||||||
_isSpawningActive = false;
|
_isSpawningActive = false;
|
||||||
_planeTransform = null;
|
_planeTransform = null;
|
||||||
|
|
||||||
// Stop spawners
|
|
||||||
if (obstacleSpawner != null)
|
|
||||||
{
|
|
||||||
obstacleSpawner.StopTracking();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (groundSpawner != null)
|
|
||||||
{
|
|
||||||
groundSpawner.StopTracking();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parallaxSpawner != null)
|
|
||||||
{
|
|
||||||
parallaxSpawner.StopTracking();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop UI tracking
|
// Stop UI tracking
|
||||||
if (targetDisplayUI != null)
|
if (targetDisplayUI != null)
|
||||||
{
|
{
|
||||||
@@ -481,22 +460,25 @@ namespace Minigames.Airplane.Core
|
|||||||
{
|
{
|
||||||
if (obstacleSpawner != null)
|
if (obstacleSpawner != null)
|
||||||
{
|
{
|
||||||
obstacleSpawner.Initialize();
|
var initParams = new SpawnInitParameters(obstacleContainer, _settings);
|
||||||
|
obstacleSpawner.Initialize(initParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (groundSpawner != null)
|
if (groundSpawner != null)
|
||||||
{
|
{
|
||||||
groundSpawner.Initialize();
|
var initParams = new SpawnInitParameters(groundContainer, _settings);
|
||||||
|
groundSpawner.Initialize(initParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parallaxSpawner != null)
|
if (parallaxSpawner != null)
|
||||||
{
|
{
|
||||||
parallaxSpawner.Initialize();
|
var initParams = new SpawnInitParameters(parallaxContainer, _settings);
|
||||||
|
parallaxSpawner.Initialize(initParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showDebugLogs)
|
if (showDebugLogs)
|
||||||
{
|
{
|
||||||
Logging.Debug("[AirplaneSpawnManager] All spawners initialized");
|
Logging.Debug("[AirplaneSpawnManager] All spawners initialized - distance-based spawning ready");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -520,7 +502,7 @@ namespace Minigames.Airplane.Core
|
|||||||
|
|
||||||
private void UpdateDynamicSpawning()
|
private void UpdateDynamicSpawning()
|
||||||
{
|
{
|
||||||
if (_planeTransform == null) return;
|
if (_planeTransform == null || !_isSpawningActive) return;
|
||||||
|
|
||||||
float planeX = _planeTransform.position.x;
|
float planeX = _planeTransform.position.x;
|
||||||
|
|
||||||
@@ -535,21 +517,36 @@ namespace Minigames.Airplane.Core
|
|||||||
|
|
||||||
if (shouldSpawnNewContent)
|
if (shouldSpawnNewContent)
|
||||||
{
|
{
|
||||||
float spawnAheadDistance = _settings.SpawnDistanceAhead;
|
// Centralized distance checking - orchestrator decides when to spawn
|
||||||
|
|
||||||
|
// Obstacles - check if next spawn point is within ahead distance
|
||||||
if (obstacleSpawner != null)
|
if (obstacleSpawner != null)
|
||||||
{
|
{
|
||||||
obstacleSpawner.UpdateSpawning(spawnAheadDistance);
|
float nextObstacleX = obstacleSpawner.GetNextSpawnX();
|
||||||
|
if (nextObstacleX <= planeX + _settings.SpawnDistanceAhead)
|
||||||
|
{
|
||||||
|
obstacleSpawner.SpawnNext();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ground - spawn further ahead (2x distance)
|
||||||
if (groundSpawner != null)
|
if (groundSpawner != null)
|
||||||
{
|
{
|
||||||
groundSpawner.UpdateSpawning(spawnAheadDistance * 2f); // Ground spawns further ahead
|
float nextGroundX = groundSpawner.GetNextSpawnX();
|
||||||
|
if (nextGroundX <= planeX + (_settings.SpawnDistanceAhead * 2f))
|
||||||
|
{
|
||||||
|
groundSpawner.SpawnNext();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parallax - spawn at 1.5x distance
|
||||||
if (parallaxSpawner != null)
|
if (parallaxSpawner != null)
|
||||||
{
|
{
|
||||||
parallaxSpawner.UpdateSpawning(spawnAheadDistance * 1.5f);
|
float nextParallaxX = parallaxSpawner.GetNextSpawnX();
|
||||||
|
if (nextParallaxX <= planeX + (_settings.SpawnDistanceAhead * 1.5f))
|
||||||
|
{
|
||||||
|
parallaxSpawner.SpawnNext();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -719,9 +716,9 @@ namespace Minigames.Airplane.Core
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// No ground found - use default
|
// No ground found - use fallback
|
||||||
Logging.Warning($"[SpawnManager] No ground found for target at X={xPosition:F2} (raycast from Y=20 for {_settings.MaxGroundRaycastDistance} units), using default Y={_settings.DefaultObjectYOffset}");
|
Logging.Warning($"[SpawnManager] No ground found for target at X={xPosition:F2} (raycast from Y=20 for {_settings.MaxGroundRaycastDistance} units), using fallback Y={_settings.FallbackYPosition}");
|
||||||
return _settings.DefaultObjectYOffset;
|
return _settings.FallbackYPosition;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -853,7 +850,7 @@ namespace Minigames.Airplane.Core
|
|||||||
return groundY + bounds.extents.y;
|
return groundY + bounds.extents.y;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _settings.DefaultObjectYOffset;
|
return _settings.FallbackYPosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Core;
|
using Core;
|
||||||
using Core.Lifecycle;
|
using Core.Lifecycle;
|
||||||
|
using Core.Settings;
|
||||||
using Minigames.Airplane.Data;
|
using Minigames.Airplane.Data;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
@@ -9,7 +10,9 @@ namespace Minigames.Airplane.Core.Spawning
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base class for distance-based spawners in the airplane minigame.
|
/// Base class for distance-based spawners in the airplane minigame.
|
||||||
/// Handles pool management, time-based unlocking, distance tracking, and recency diversity.
|
/// Handles pool management, time-based unlocking, distance tracking, and recency diversity.
|
||||||
|
/// All spawn parameters (distances, recency, positioning, debug) are configured in AirplaneSettings.
|
||||||
/// Derived classes implement specific spawn logic via SpawnFromPool.
|
/// Derived classes implement specific spawn logic via SpawnFromPool.
|
||||||
|
/// Orchestrator (AirplaneSpawnManager) handles distance checking and calls SpawnNext when needed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class BaseDistanceSpawner : ManagedBehaviour
|
public abstract class BaseDistanceSpawner : ManagedBehaviour
|
||||||
{
|
{
|
||||||
@@ -22,35 +25,16 @@ namespace Minigames.Airplane.Core.Spawning
|
|||||||
[Tooltip("Spawn pools ordered by difficulty/progression")]
|
[Tooltip("Spawn pools ordered by difficulty/progression")]
|
||||||
[SerializeField] protected SpawnPoolConfig[] pools;
|
[SerializeField] protected SpawnPoolConfig[] pools;
|
||||||
|
|
||||||
[Header("Global Spawn Parameters")]
|
|
||||||
[Tooltip("Minimum distance between spawns (used when poolMode = Together or as fallback)")]
|
|
||||||
[SerializeField] protected float globalMinDistance = 5f;
|
|
||||||
|
|
||||||
[Tooltip("Maximum distance between spawns (used when poolMode = Together or as fallback)")]
|
|
||||||
[SerializeField] protected float globalMaxDistance = 15f;
|
|
||||||
|
|
||||||
[Header("Spawn References")]
|
|
||||||
[Tooltip("Transform marking spawn position (typically off-screen right)")]
|
|
||||||
[SerializeField] protected Transform spawnPoint;
|
|
||||||
|
|
||||||
[Tooltip("Optional parent for spawned objects (organization)")]
|
|
||||||
[SerializeField] protected Transform spawnContainer;
|
|
||||||
|
|
||||||
[Header("Recency Tracking")]
|
|
||||||
[Tooltip("Time penalty (seconds) applied to recently-used prefabs for diversity")]
|
|
||||||
[SerializeField] protected float recencyPenaltyDuration = 10f;
|
|
||||||
|
|
||||||
[Header("Debug")]
|
|
||||||
[SerializeField] protected bool showDebugLogs;
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region State
|
#region State
|
||||||
|
|
||||||
|
// Shared references (set by orchestrator via SpawnInitParameters)
|
||||||
|
protected Transform spawnContainer;
|
||||||
|
protected IAirplaneSettings settings;
|
||||||
|
|
||||||
// Tracking
|
// Tracking
|
||||||
protected Transform PlaneTransform;
|
|
||||||
protected float GameTime;
|
protected float GameTime;
|
||||||
protected bool IsSpawning;
|
|
||||||
protected float LastSpawnedX;
|
protected float LastSpawnedX;
|
||||||
|
|
||||||
// Pool state
|
// Pool state
|
||||||
@@ -68,10 +52,15 @@ namespace Minigames.Airplane.Core.Spawning
|
|||||||
#region Initialization
|
#region Initialization
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initialize the spawner. Call this before starting spawning.
|
/// Initialize the spawner with shared configuration. Call this before starting spawning.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual void Initialize()
|
/// <param name="initParams">Initialization parameters containing shared references</param>
|
||||||
|
public virtual void Initialize(SpawnInitParameters initParams)
|
||||||
{
|
{
|
||||||
|
// Store shared references
|
||||||
|
spawnContainer = initParams.SpawnContainer;
|
||||||
|
settings = initParams.Settings;
|
||||||
|
|
||||||
ValidateConfiguration();
|
ValidateConfiguration();
|
||||||
|
|
||||||
// Unlock pool 0 immediately
|
// Unlock pool 0 immediately
|
||||||
@@ -79,7 +68,7 @@ namespace Minigames.Airplane.Core.Spawning
|
|||||||
{
|
{
|
||||||
UnlockedPoolIndices.Add(0);
|
UnlockedPoolIndices.Add(0);
|
||||||
|
|
||||||
if (showDebugLogs)
|
if (settings.ShowDebugLogs)
|
||||||
{
|
{
|
||||||
Logging.Debug($"[{GetType().Name}] Initialized with pool 0 unlocked");
|
Logging.Debug($"[{GetType().Name}] Initialized with pool 0 unlocked");
|
||||||
}
|
}
|
||||||
@@ -87,6 +76,8 @@ namespace Minigames.Airplane.Core.Spawning
|
|||||||
|
|
||||||
// Initialize exclusive mode spawn positions if needed
|
// Initialize exclusive mode spawn positions if needed
|
||||||
if (poolMode == SpawnPoolMode.Exclusive)
|
if (poolMode == SpawnPoolMode.Exclusive)
|
||||||
|
{
|
||||||
|
if (pools != null)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < pools.Length; i++)
|
for (int i = 0; i < pools.Length; i++)
|
||||||
{
|
{
|
||||||
@@ -94,6 +85,7 @@ namespace Minigames.Airplane.Core.Spawning
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected virtual void ValidateConfiguration()
|
protected virtual void ValidateConfiguration()
|
||||||
{
|
{
|
||||||
@@ -103,11 +95,6 @@ namespace Minigames.Airplane.Core.Spawning
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (spawnPoint == null)
|
|
||||||
{
|
|
||||||
Logging.Warning($"[{GetType().Name}] Spawn point not assigned!");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate pool unlock times are monotonically increasing
|
// Validate pool unlock times are monotonically increasing
|
||||||
for (int i = 1; i < pools.Length; i++)
|
for (int i = 1; i < pools.Length; i++)
|
||||||
{
|
{
|
||||||
@@ -122,51 +109,6 @@ namespace Minigames.Airplane.Core.Spawning
|
|||||||
|
|
||||||
#region Public API
|
#region Public API
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Start tracking the plane and enable spawning.
|
|
||||||
/// </summary>
|
|
||||||
public void StartTracking(Transform planeTransform, float startX)
|
|
||||||
{
|
|
||||||
PlaneTransform = planeTransform;
|
|
||||||
IsSpawning = true;
|
|
||||||
GameTime = 0f;
|
|
||||||
LastSpawnedX = startX;
|
|
||||||
|
|
||||||
// Initialize next spawn position
|
|
||||||
if (poolMode == SpawnPoolMode.Together)
|
|
||||||
{
|
|
||||||
NextSpawnX = startX + Random.Range(globalMinDistance, globalMaxDistance);
|
|
||||||
}
|
|
||||||
else // Exclusive
|
|
||||||
{
|
|
||||||
foreach (int poolIndex in UnlockedPoolIndices)
|
|
||||||
{
|
|
||||||
float minDist = pools[poolIndex].GetMinDistance(globalMinDistance);
|
|
||||||
float maxDist = pools[poolIndex].GetMaxDistance(globalMaxDistance);
|
|
||||||
PoolNextSpawnX[poolIndex] = startX + Random.Range(minDist, maxDist);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showDebugLogs)
|
|
||||||
{
|
|
||||||
Logging.Debug($"[{GetType().Name}] Started tracking from X={startX}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Stop spawning and tracking.
|
|
||||||
/// </summary>
|
|
||||||
public void StopTracking()
|
|
||||||
{
|
|
||||||
IsSpawning = false;
|
|
||||||
PlaneTransform = null;
|
|
||||||
|
|
||||||
if (showDebugLogs)
|
|
||||||
{
|
|
||||||
Logging.Debug($"[{GetType().Name}] Stopped tracking");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Update game time and check for pool unlocks.
|
/// Update game time and check for pool unlocks.
|
||||||
/// Call this every frame from the orchestrator.
|
/// Call this every frame from the orchestrator.
|
||||||
@@ -177,6 +119,58 @@ namespace Minigames.Airplane.Core.Spawning
|
|||||||
CheckPoolUnlocks();
|
CheckPoolUnlocks();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Spawn the next object at the predetermined NextSpawnX position.
|
||||||
|
/// Updates LastSpawnedX and calculates new NextSpawnX.
|
||||||
|
/// Called by orchestrator when distance check determines spawning is needed.
|
||||||
|
/// </summary>
|
||||||
|
public virtual void SpawnNext()
|
||||||
|
{
|
||||||
|
if (poolMode == SpawnPoolMode.Together)
|
||||||
|
{
|
||||||
|
SpawnAtPosition(NextSpawnX);
|
||||||
|
LastSpawnedX = NextSpawnX;
|
||||||
|
NextSpawnX = LastSpawnedX + Random.Range(settings.ObjectSpawnMinDistance, settings.ObjectSpawnMaxDistance);
|
||||||
|
}
|
||||||
|
else // Exclusive mode - spawn from each pool independently
|
||||||
|
{
|
||||||
|
foreach (int poolIndex in UnlockedPoolIndices)
|
||||||
|
{
|
||||||
|
if (poolIndex >= pools.Length || !pools[poolIndex].HasPrefabs) continue;
|
||||||
|
if (!PoolNextSpawnX.ContainsKey(poolIndex)) continue;
|
||||||
|
|
||||||
|
SpawnFromPool(poolIndex, PoolNextSpawnX[poolIndex]);
|
||||||
|
|
||||||
|
float minDist = pools[poolIndex].GetMinDistance(settings.ObjectSpawnMinDistance);
|
||||||
|
float maxDist = pools[poolIndex].GetMaxDistance(settings.ObjectSpawnMaxDistance);
|
||||||
|
PoolNextSpawnX[poolIndex] += Random.Range(minDist, maxDist);
|
||||||
|
LastSpawnedX = Mathf.Max(LastSpawnedX, PoolNextSpawnX[poolIndex]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the X position where the next spawn will occur.
|
||||||
|
/// Used by orchestrator for distance checking.
|
||||||
|
/// </summary>
|
||||||
|
public float GetNextSpawnX()
|
||||||
|
{
|
||||||
|
if (poolMode == SpawnPoolMode.Together)
|
||||||
|
{
|
||||||
|
return NextSpawnX;
|
||||||
|
}
|
||||||
|
else // Exclusive - return the closest spawn point across all pools
|
||||||
|
{
|
||||||
|
float closest = float.MaxValue;
|
||||||
|
foreach (var kvp in PoolNextSpawnX)
|
||||||
|
{
|
||||||
|
if (kvp.Value < closest)
|
||||||
|
closest = kvp.Value;
|
||||||
|
}
|
||||||
|
return closest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Pre-spawn objects from start to end X position.
|
/// Pre-spawn objects from start to end X position.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -194,26 +188,6 @@ namespace Minigames.Airplane.Core.Spawning
|
|||||||
LastSpawnedX = endX;
|
LastSpawnedX = endX;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Update spawning based on plane position.
|
|
||||||
/// Call this every frame from the orchestrator.
|
|
||||||
/// </summary>
|
|
||||||
public virtual void UpdateSpawning(float spawnAheadDistance)
|
|
||||||
{
|
|
||||||
if (!IsSpawning || PlaneTransform == null) return;
|
|
||||||
|
|
||||||
float planeX = PlaneTransform.position.x;
|
|
||||||
|
|
||||||
if (poolMode == SpawnPoolMode.Together)
|
|
||||||
{
|
|
||||||
UpdateSpawningTogether(planeX, spawnAheadDistance);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
UpdateSpawningExclusive(planeX, spawnAheadDistance);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Clean up all spawned objects.
|
/// Clean up all spawned objects.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -231,7 +205,7 @@ namespace Minigames.Airplane.Core.Spawning
|
|||||||
UnlockedPoolIndices.Clear();
|
UnlockedPoolIndices.Clear();
|
||||||
UnlockedPoolIndices.Add(0); // Re-add pool 0
|
UnlockedPoolIndices.Add(0); // Re-add pool 0
|
||||||
|
|
||||||
if (showDebugLogs)
|
if (settings.ShowDebugLogs)
|
||||||
{
|
{
|
||||||
Logging.Debug($"[{GetType().Name}] Cleanup complete");
|
Logging.Debug($"[{GetType().Name}] Cleanup complete");
|
||||||
}
|
}
|
||||||
@@ -252,14 +226,15 @@ namespace Minigames.Airplane.Core.Spawning
|
|||||||
UnlockedPoolIndices.Add(i);
|
UnlockedPoolIndices.Add(i);
|
||||||
|
|
||||||
// If exclusive mode, initialize spawn position for this pool
|
// If exclusive mode, initialize spawn position for this pool
|
||||||
if (poolMode == SpawnPoolMode.Exclusive && PlaneTransform != null)
|
if (poolMode == SpawnPoolMode.Exclusive)
|
||||||
{
|
{
|
||||||
float minDist = pools[i].GetMinDistance(globalMinDistance);
|
float minDist = pools[i].GetMinDistance(settings.ObjectSpawnMinDistance);
|
||||||
float maxDist = pools[i].GetMaxDistance(globalMaxDistance);
|
float maxDist = pools[i].GetMaxDistance(settings.ObjectSpawnMaxDistance);
|
||||||
PoolNextSpawnX[i] = PlaneTransform.position.x + Random.Range(minDist, maxDist);
|
// Initialize at last spawned position plus random distance
|
||||||
|
PoolNextSpawnX[i] = LastSpawnedX + Random.Range(minDist, maxDist);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showDebugLogs)
|
if (settings.ShowDebugLogs)
|
||||||
{
|
{
|
||||||
Logging.Debug($"[{GetType().Name}] Unlocked pool {i} '{pools[i].description}' at time {GameTime:F2}s");
|
Logging.Debug($"[{GetType().Name}] Unlocked pool {i} '{pools[i].description}' at time {GameTime:F2}s");
|
||||||
}
|
}
|
||||||
@@ -267,7 +242,7 @@ namespace Minigames.Airplane.Core.Spawning
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected GameObject SelectPrefabFromPools(out int selectedPoolIndex)
|
protected virtual GameObject SelectPrefabFromPools(out int selectedPoolIndex)
|
||||||
{
|
{
|
||||||
selectedPoolIndex = -1;
|
selectedPoolIndex = -1;
|
||||||
|
|
||||||
@@ -294,9 +269,9 @@ namespace Minigames.Airplane.Core.Spawning
|
|||||||
if (LastUsedTimes.TryGetValue(prefab, out float lastUsedTime))
|
if (LastUsedTimes.TryGetValue(prefab, out float lastUsedTime))
|
||||||
{
|
{
|
||||||
float timeSinceUse = GameTime - lastUsedTime;
|
float timeSinceUse = GameTime - lastUsedTime;
|
||||||
if (timeSinceUse < recencyPenaltyDuration)
|
if (timeSinceUse < settings.RecencyPenaltyDuration)
|
||||||
{
|
{
|
||||||
weight = timeSinceUse / recencyPenaltyDuration; // 0 to 1 linear recovery
|
weight = timeSinceUse / settings.RecencyPenaltyDuration; // 0 to 1 linear recovery
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,29 +315,17 @@ namespace Minigames.Airplane.Core.Spawning
|
|||||||
|
|
||||||
protected virtual void PreSpawnTogether(float startX, float endX)
|
protected virtual void PreSpawnTogether(float startX, float endX)
|
||||||
{
|
{
|
||||||
float currentX = startX + Random.Range(globalMinDistance, globalMaxDistance);
|
float currentX = startX + Random.Range(settings.ObjectSpawnMinDistance, settings.ObjectSpawnMaxDistance);
|
||||||
|
|
||||||
while (currentX <= endX)
|
while (currentX <= endX)
|
||||||
{
|
{
|
||||||
SpawnAtPosition(currentX);
|
SpawnAtPosition(currentX);
|
||||||
currentX += Random.Range(globalMinDistance, globalMaxDistance);
|
currentX += Random.Range(settings.ObjectSpawnMinDistance, settings.ObjectSpawnMaxDistance);
|
||||||
}
|
}
|
||||||
|
|
||||||
NextSpawnX = currentX;
|
NextSpawnX = currentX;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void UpdateSpawningTogether(float planeX, float spawnAheadDistance)
|
|
||||||
{
|
|
||||||
float spawnTriggerX = LastSpawnedX + spawnAheadDistance;
|
|
||||||
|
|
||||||
if (planeX >= spawnTriggerX && planeX >= NextSpawnX)
|
|
||||||
{
|
|
||||||
SpawnAtPosition(NextSpawnX);
|
|
||||||
LastSpawnedX = NextSpawnX;
|
|
||||||
NextSpawnX += Random.Range(globalMinDistance, globalMaxDistance);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Exclusive Mode Spawning
|
#region Exclusive Mode Spawning
|
||||||
@@ -373,8 +336,8 @@ namespace Minigames.Airplane.Core.Spawning
|
|||||||
{
|
{
|
||||||
if (poolIndex >= pools.Length || !pools[poolIndex].HasPrefabs) continue;
|
if (poolIndex >= pools.Length || !pools[poolIndex].HasPrefabs) continue;
|
||||||
|
|
||||||
float minDist = pools[poolIndex].GetMinDistance(globalMinDistance);
|
float minDist = pools[poolIndex].GetMinDistance(settings.ObjectSpawnMinDistance);
|
||||||
float maxDist = pools[poolIndex].GetMaxDistance(globalMaxDistance);
|
float maxDist = pools[poolIndex].GetMaxDistance(settings.ObjectSpawnMaxDistance);
|
||||||
float currentX = startX + Random.Range(minDist, maxDist);
|
float currentX = startX + Random.Range(minDist, maxDist);
|
||||||
|
|
||||||
while (currentX <= endX)
|
while (currentX <= endX)
|
||||||
@@ -387,49 +350,24 @@ namespace Minigames.Airplane.Core.Spawning
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void UpdateSpawningExclusive(float planeX, float spawnAheadDistance)
|
|
||||||
{
|
|
||||||
foreach (int poolIndex in UnlockedPoolIndices)
|
|
||||||
{
|
|
||||||
if (poolIndex >= pools.Length || !pools[poolIndex].HasPrefabs) continue;
|
|
||||||
if (!PoolNextSpawnX.ContainsKey(poolIndex)) continue;
|
|
||||||
|
|
||||||
float nextX = PoolNextSpawnX[poolIndex];
|
|
||||||
float spawnTargetX = planeX + spawnAheadDistance;
|
|
||||||
|
|
||||||
if (nextX <= spawnTargetX)
|
|
||||||
{
|
|
||||||
SpawnFromPool(poolIndex, nextX);
|
|
||||||
|
|
||||||
float minDist = pools[poolIndex].GetMinDistance(globalMinDistance);
|
|
||||||
float maxDist = pools[poolIndex].GetMaxDistance(globalMaxDistance);
|
|
||||||
PoolNextSpawnX[poolIndex] = nextX + Random.Range(minDist, maxDist);
|
|
||||||
LastSpawnedX = Mathf.Max(LastSpawnedX, nextX);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Spawning
|
#region Spawning
|
||||||
|
|
||||||
protected void SpawnAtPosition(float xPosition)
|
protected void SpawnAtPosition(float xPosition)
|
||||||
{
|
{
|
||||||
int selectedPoolIndex;
|
GameObject prefab = SelectPrefabFromPools(out int poolIndex);
|
||||||
GameObject prefab = SelectPrefabFromPools(out selectedPoolIndex);
|
if (prefab != null && poolIndex >= 0)
|
||||||
if (prefab == null) return;
|
{
|
||||||
|
SpawnFromPool(poolIndex, xPosition);
|
||||||
SpawnFromPool(selectedPoolIndex, xPosition);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Spawn a specific prefab from a pool at the given X position.
|
/// Spawn a specific prefab from a pool at the given X position.
|
||||||
/// Override this in derived classes to implement specific spawn logic.
|
/// Derived classes must implement this to define spawn behavior.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected virtual void SpawnFromPool(int poolIndex, float xPosition)
|
protected abstract void SpawnFromPool(int poolIndex, float xPosition);
|
||||||
{
|
|
||||||
// Derived classes implement specific spawn logic
|
|
||||||
}
|
|
||||||
|
|
||||||
protected GameObject InstantiatePrefab(GameObject prefab, Vector3 position)
|
protected GameObject InstantiatePrefab(GameObject prefab, Vector3 position)
|
||||||
{
|
{
|
||||||
@@ -444,6 +382,95 @@ namespace Minigames.Airplane.Core.Spawning
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region Object Positioning
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Position an object based on the specified spawn mode.
|
||||||
|
/// Used by spawners to apply Y positioning after instantiation.
|
||||||
|
/// </summary>
|
||||||
|
protected void PositionObject(GameObject obj, float xPosition, SpawnPositionMode mode,
|
||||||
|
float specifiedY, float randomYMin, float randomYMax)
|
||||||
|
{
|
||||||
|
if (obj == null) return;
|
||||||
|
|
||||||
|
float targetY;
|
||||||
|
|
||||||
|
switch (mode)
|
||||||
|
{
|
||||||
|
case SpawnPositionMode.SnapToGround:
|
||||||
|
targetY = SnapToGround(obj, xPosition);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SpawnPositionMode.SpecifiedY:
|
||||||
|
targetY = specifiedY;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SpawnPositionMode.RandomRange:
|
||||||
|
targetY = Random.Range(randomYMin, randomYMax);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
targetY = 0f;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector3 newPosition = obj.transform.position;
|
||||||
|
newPosition.y = targetY;
|
||||||
|
obj.transform.position = newPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Snap an object to the ground using raycast.
|
||||||
|
/// Positions object so its bottom bounds touch the ground surface.
|
||||||
|
/// </summary>
|
||||||
|
protected float SnapToGround(GameObject obj, float xPosition)
|
||||||
|
{
|
||||||
|
// Raycast from high up to ensure we're above the ground
|
||||||
|
Vector2 rayOrigin = new Vector2(xPosition, settings.MaxGroundRaycastDistance);
|
||||||
|
int layerMask = 1 << settings.GroundLayer;
|
||||||
|
RaycastHit2D hit = Physics2D.Raycast(rayOrigin, Vector2.down, settings.MaxGroundRaycastDistance + 100f, layerMask);
|
||||||
|
|
||||||
|
if (hit.collider != null)
|
||||||
|
{
|
||||||
|
float groundY = hit.point.y;
|
||||||
|
Bounds bounds = GetObjectBounds(obj);
|
||||||
|
float finalY = groundY + bounds.extents.y;
|
||||||
|
|
||||||
|
if (settings.ShowDebugLogs)
|
||||||
|
{
|
||||||
|
Logging.Debug($"[{GetType().Name}] SnapToGround: X={xPosition:F2}, Ground={groundY:F2}, Final={finalY:F2}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalY;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.ShowDebugLogs)
|
||||||
|
{
|
||||||
|
Logging.Warning($"[{GetType().Name}] SnapToGround FAILED at X={xPosition:F2} - no ground found! Using fallback Y={settings.FallbackYPosition:F2}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return settings.FallbackYPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the bounds of an object from its Renderer or Collider.
|
||||||
|
/// </summary>
|
||||||
|
protected Bounds GetObjectBounds(GameObject obj)
|
||||||
|
{
|
||||||
|
Renderer objRenderer = obj.GetComponentInChildren<Renderer>();
|
||||||
|
if (objRenderer != null) return objRenderer.bounds;
|
||||||
|
|
||||||
|
Collider2D objCollider2D = obj.GetComponentInChildren<Collider2D>();
|
||||||
|
if (objCollider2D != null) return objCollider2D.bounds;
|
||||||
|
|
||||||
|
Collider objCollider3D = obj.GetComponentInChildren<Collider>();
|
||||||
|
if (objCollider3D != null) return objCollider3D.bounds;
|
||||||
|
|
||||||
|
return new Bounds(obj.transform.position, Vector3.one);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 387858903c2c44e0ab007cb2ac886343
|
guid: f65e211e40b247ffb0ac920be5a9ce53
|
||||||
timeCreated: 1765965907
|
timeCreated: 1765993195
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
using Core;
|
using Core;
|
||||||
|
using Core.Settings;
|
||||||
|
using Minigames.Airplane.Data;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace Minigames.Airplane.Core.Spawning
|
namespace Minigames.Airplane.Core.Spawning
|
||||||
@@ -6,6 +8,7 @@ namespace Minigames.Airplane.Core.Spawning
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Spawns ground tiles at fixed intervals.
|
/// Spawns ground tiles at fixed intervals.
|
||||||
/// Inherits distance-based spawning from BaseDistanceSpawner.
|
/// Inherits distance-based spawning from BaseDistanceSpawner.
|
||||||
|
/// Uses IAirplaneSettings.GroundSpawnInterval for consistent tile spacing.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class GroundDistanceSpawner : BaseDistanceSpawner
|
public class GroundDistanceSpawner : BaseDistanceSpawner
|
||||||
{
|
{
|
||||||
@@ -13,6 +16,49 @@ namespace Minigames.Airplane.Core.Spawning
|
|||||||
[Tooltip("Y position at which to spawn ground tiles")]
|
[Tooltip("Y position at which to spawn ground tiles")]
|
||||||
[SerializeField] private float groundSpawnY = -18f;
|
[SerializeField] private float groundSpawnY = -18f;
|
||||||
|
|
||||||
|
private float _groundSpawnInterval;
|
||||||
|
|
||||||
|
public override void Initialize(SpawnInitParameters initParams)
|
||||||
|
{
|
||||||
|
base.Initialize(initParams);
|
||||||
|
|
||||||
|
// Get fixed ground spawn interval from settings
|
||||||
|
_groundSpawnInterval = initParams.Settings.GroundSpawnInterval;
|
||||||
|
|
||||||
|
// Force Together mode for ground spawning
|
||||||
|
poolMode = SpawnPoolMode.Together;
|
||||||
|
|
||||||
|
if (settings.ShowDebugLogs)
|
||||||
|
{
|
||||||
|
Logging.Debug($"[GroundDistanceSpawner] Using fixed interval: {_groundSpawnInterval}f from settings");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override to use fixed intervals instead of random distances
|
||||||
|
protected override void PreSpawnTogether(float startX, float endX)
|
||||||
|
{
|
||||||
|
float currentX = startX;
|
||||||
|
|
||||||
|
while (currentX <= endX)
|
||||||
|
{
|
||||||
|
SpawnAtPosition(currentX);
|
||||||
|
currentX += _groundSpawnInterval; // FIXED interval, not random
|
||||||
|
}
|
||||||
|
|
||||||
|
NextSpawnX = currentX;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Override SpawnNext to use fixed ground intervals instead of random distances.
|
||||||
|
/// Called by orchestrator when distance check determines spawning is needed.
|
||||||
|
/// </summary>
|
||||||
|
public override void SpawnNext()
|
||||||
|
{
|
||||||
|
SpawnAtPosition(NextSpawnX);
|
||||||
|
LastSpawnedX = NextSpawnX;
|
||||||
|
NextSpawnX += _groundSpawnInterval; // FIXED interval, not random
|
||||||
|
}
|
||||||
|
|
||||||
protected override void SpawnFromPool(int poolIndex, float xPosition)
|
protected override void SpawnFromPool(int poolIndex, float xPosition)
|
||||||
{
|
{
|
||||||
if (poolIndex < 0 || poolIndex >= pools.Length || !pools[poolIndex].HasPrefabs)
|
if (poolIndex < 0 || poolIndex >= pools.Length || !pools[poolIndex].HasPrefabs)
|
||||||
@@ -22,15 +68,15 @@ namespace Minigames.Airplane.Core.Spawning
|
|||||||
GameObject tilePrefab = pools[poolIndex].prefabs[Random.Range(0, pools[poolIndex].prefabs.Length)];
|
GameObject tilePrefab = pools[poolIndex].prefabs[Random.Range(0, pools[poolIndex].prefabs.Length)];
|
||||||
if (tilePrefab == null) return;
|
if (tilePrefab == null) return;
|
||||||
|
|
||||||
// Calculate spawn position
|
// Calculate spawn position using settings
|
||||||
Vector3 spawnPosition = new Vector3(xPosition, groundSpawnY, 0f);
|
Vector3 spawnPosition = new Vector3(xPosition, settings.GroundSpawnY, 0f);
|
||||||
|
|
||||||
// Instantiate
|
// Instantiate
|
||||||
GameObject instance = InstantiatePrefab(tilePrefab, spawnPosition);
|
GameObject instance = InstantiatePrefab(tilePrefab, spawnPosition);
|
||||||
|
|
||||||
if (showDebugLogs)
|
if (settings.ShowDebugLogs)
|
||||||
{
|
{
|
||||||
Logging.Debug($"[GroundDistanceSpawner] Spawned ground tile at X={xPosition:F2}, Y={groundSpawnY:F2}");
|
Logging.Debug($"[GroundDistanceSpawner] Spawned ground tile at X={xPosition:F2}, Y={settings.GroundSpawnY:F2}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,71 +1,183 @@
|
|||||||
using Core;
|
using Core;
|
||||||
using Minigames.Airplane.Data;
|
using Minigames.Airplane.Data;
|
||||||
using Minigames.Airplane.Interactive;
|
using Minigames.Airplane.Interactive;
|
||||||
|
using System.Collections.Generic;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace Minigames.Airplane.Core.Spawning
|
namespace Minigames.Airplane.Core.Spawning
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Spawns obstacle objects (positive and negative) with weighted ratio management.
|
/// Spawns obstacle objects (positive and negative) with weighted ratio management.
|
||||||
|
/// Uses exactly 2 fixed pools: Pool 0 = Positive, Pool 1 = Negative.
|
||||||
/// Inherits distance-based spawning from BaseDistanceSpawner.
|
/// Inherits distance-based spawning from BaseDistanceSpawner.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ObstacleDistanceSpawner : BaseDistanceSpawner
|
public class ObstacleDistanceSpawner : BaseDistanceSpawner
|
||||||
{
|
{
|
||||||
[Header("Obstacle-Specific")]
|
|
||||||
[Tooltip("Ratio of positive to negative objects (0 = all negative, 1 = all positive)")]
|
|
||||||
[Range(0f, 1f)]
|
|
||||||
[SerializeField] private float positiveNegativeRatio = 0.5f;
|
|
||||||
|
|
||||||
[Tooltip("Array indices that should be treated as positive objects")]
|
|
||||||
[SerializeField] private int[] positivePoolIndices;
|
|
||||||
|
|
||||||
[Tooltip("Array indices that should be treated as negative objects")]
|
|
||||||
[SerializeField] private int[] negativePoolIndices;
|
|
||||||
|
|
||||||
[Header("Positioning")]
|
|
||||||
[SerializeField] private int groundLayer;
|
|
||||||
[SerializeField] private float maxGroundRaycastDistance = 50f;
|
|
||||||
[SerializeField] private float defaultObjectYOffset;
|
|
||||||
|
|
||||||
private int _positiveSpawnCount;
|
private int _positiveSpawnCount;
|
||||||
private int _negativeSpawnCount;
|
private int _negativeSpawnCount;
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize(SpawnInitParameters initParams)
|
||||||
{
|
{
|
||||||
base.Initialize();
|
// TEMPORARY: Unconditional log to verify this is being called
|
||||||
|
Logging.Debug("[ObstacleDistanceSpawner] Initialize() called - spawner is active!");
|
||||||
|
|
||||||
|
// Validate exactly 2 pools
|
||||||
|
if (pools == null || pools.Length != 2)
|
||||||
|
{
|
||||||
|
Logging.Error("[ObstacleDistanceSpawner] Must have exactly 2 pools (Positive, Negative)!");
|
||||||
|
}
|
||||||
|
|
||||||
|
base.Initialize(initParams);
|
||||||
_positiveSpawnCount = 0;
|
_positiveSpawnCount = 0;
|
||||||
_negativeSpawnCount = 0;
|
_negativeSpawnCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Override base prefab selection to implement positive/negative ratio management.
|
||||||
|
/// Uses actual spawn counts to maintain target ratio over time.
|
||||||
|
/// </summary>
|
||||||
|
protected override GameObject SelectPrefabFromPools(out int selectedPoolIndex)
|
||||||
|
{
|
||||||
|
selectedPoolIndex = -1;
|
||||||
|
|
||||||
|
// Determine which pool (positive or negative) based on ratio
|
||||||
|
int targetPoolIndex = DeterminePoolIndexByRatio();
|
||||||
|
|
||||||
|
if (targetPoolIndex < 0 || targetPoolIndex >= pools.Length || !pools[targetPoolIndex].HasPrefabs)
|
||||||
|
{
|
||||||
|
if (settings.ShowDebugLogs)
|
||||||
|
{
|
||||||
|
Logging.Warning($"[ObstacleDistanceSpawner] Target pool {targetPoolIndex} has no prefabs!");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all prefabs from the target pool
|
||||||
|
List<GameObject> availablePrefabs = new List<GameObject>();
|
||||||
|
List<float> prefabWeights = new List<float>();
|
||||||
|
|
||||||
|
foreach (GameObject prefab in pools[targetPoolIndex].prefabs)
|
||||||
|
{
|
||||||
|
if (prefab == null) continue;
|
||||||
|
|
||||||
|
availablePrefabs.Add(prefab);
|
||||||
|
|
||||||
|
// Calculate weight based on recency tracking
|
||||||
|
float weight = 1f;
|
||||||
|
if (LastUsedTimes.TryGetValue(prefab, out float lastUsedTime))
|
||||||
|
{
|
||||||
|
float timeSinceUse = GameTime - lastUsedTime;
|
||||||
|
if (timeSinceUse < settings.RecencyPenaltyDuration)
|
||||||
|
{
|
||||||
|
weight = timeSinceUse / settings.RecencyPenaltyDuration; // 0 to 1 linear recovery
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prefabWeights.Add(weight);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (availablePrefabs.Count == 0) return null;
|
||||||
|
|
||||||
|
// Select using weighted random (respects recency)
|
||||||
|
int selectedIndex = WeightedRandom(prefabWeights);
|
||||||
|
GameObject selectedPrefab = availablePrefabs[selectedIndex];
|
||||||
|
selectedPoolIndex = targetPoolIndex;
|
||||||
|
|
||||||
|
// Update recency tracking
|
||||||
|
LastUsedTimes[selectedPrefab] = GameTime;
|
||||||
|
|
||||||
|
if (settings.ShowDebugLogs)
|
||||||
|
{
|
||||||
|
Logging.Debug($"[ObstacleDistanceSpawner] Selected {(targetPoolIndex == 0 ? "positive" : "negative")} prefab from pool {targetPoolIndex}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedPrefab;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines which pool (positive or negative) to spawn from based on target ratio.
|
||||||
|
/// Uses actual spawn counts to push toward target ratio over time.
|
||||||
|
/// </summary>
|
||||||
|
private int DeterminePoolIndexByRatio()
|
||||||
|
{
|
||||||
|
int totalSpawned = _positiveSpawnCount + _negativeSpawnCount;
|
||||||
|
float targetPositiveRatio = settings.PositiveNegativeRatio;
|
||||||
|
|
||||||
|
bool shouldSpawnPositive;
|
||||||
|
|
||||||
|
// First spawn - use ratio as pure probability
|
||||||
|
if (totalSpawned == 0)
|
||||||
|
{
|
||||||
|
shouldSpawnPositive = Random.value < targetPositiveRatio;
|
||||||
|
if (settings.ShowDebugLogs)
|
||||||
|
{
|
||||||
|
Logging.Debug($"[ObstacleDistanceSpawner] First spawn - ratio {targetPositiveRatio:P0} → {(shouldSpawnPositive ? "positive" : "negative")}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Calculate current ratio vs target
|
||||||
|
float currentPositiveRatio = (float)_positiveSpawnCount / totalSpawned;
|
||||||
|
float difference = targetPositiveRatio - currentPositiveRatio;
|
||||||
|
|
||||||
|
// Adjust probability based on how far we are from target
|
||||||
|
// If difference > 0: we need more positives, increase positive chance
|
||||||
|
// If difference < 0: we need more negatives, decrease positive chance
|
||||||
|
float adjustedProbability = Mathf.Clamp01(targetPositiveRatio + difference);
|
||||||
|
|
||||||
|
shouldSpawnPositive = Random.value < adjustedProbability;
|
||||||
|
|
||||||
|
if (settings.ShowDebugLogs)
|
||||||
|
{
|
||||||
|
Logging.Debug($"[ObstacleDistanceSpawner] Ratio tracking: {_positiveSpawnCount}pos/{_negativeSpawnCount}neg ({currentPositiveRatio:P0} current vs {targetPositiveRatio:P0} target) → adjusted probability {adjustedProbability:P0} → {(shouldSpawnPositive ? "positive" : "negative")}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return shouldSpawnPositive ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
protected override void SpawnFromPool(int poolIndex, float xPosition)
|
protected override void SpawnFromPool(int poolIndex, float xPosition)
|
||||||
{
|
{
|
||||||
if (poolIndex < 0 || poolIndex >= pools.Length || !pools[poolIndex].HasPrefabs)
|
if (poolIndex < 0 || poolIndex >= 2 || !pools[poolIndex].HasPrefabs)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Determine if this should spawn positive or negative
|
// Pool index directly determines positive/negative
|
||||||
bool isPositivePool = System.Array.IndexOf(positivePoolIndices, poolIndex) >= 0;
|
// Pool 0 = Positive, Pool 1 = Negative
|
||||||
bool isNegativePool = System.Array.IndexOf(negativePoolIndices, poolIndex) >= 0;
|
bool spawnPositive = (poolIndex == 0);
|
||||||
|
|
||||||
// If pool is not specifically marked, use weighted random
|
|
||||||
bool spawnPositive = isPositivePool || (!isNegativePool && ShouldSpawnPositive());
|
|
||||||
|
|
||||||
// Select random prefab from pool
|
// Select random prefab from pool
|
||||||
GameObject prefab = pools[poolIndex].prefabs[Random.Range(0, pools[poolIndex].prefabs.Length)];
|
GameObject prefab = pools[poolIndex].prefabs[Random.Range(0, pools[poolIndex].prefabs.Length)];
|
||||||
if (prefab == null) return;
|
if (prefab == null) return;
|
||||||
|
|
||||||
// Get spawn position from spawnPoint or use default
|
// Temporary spawn position (Y will be adjusted by PositionObject)
|
||||||
float spawnY = spawnPoint != null ? spawnPoint.position.y : 0f;
|
Vector3 tempPosition = new Vector3(xPosition, 0f, 0f);
|
||||||
Vector3 tempPosition = new Vector3(xPosition, spawnY, 0f);
|
|
||||||
|
|
||||||
// Instantiate
|
// Instantiate
|
||||||
GameObject instance = InstantiatePrefab(prefab, tempPosition);
|
GameObject instance = InstantiatePrefab(prefab, tempPosition);
|
||||||
|
|
||||||
// Try to get spawn entry for positioning
|
// Try to get spawn entry for positioning (per-prefab override)
|
||||||
var spawnEntry = prefab.GetComponent<PrefabSpawnEntryComponent>();
|
var spawnEntry = prefab.GetComponent<PrefabSpawnEntryComponent>();
|
||||||
if (spawnEntry != null)
|
if (spawnEntry != null)
|
||||||
{
|
{
|
||||||
|
// Use per-prefab configuration
|
||||||
PositionObject(instance, xPosition, spawnEntry.spawnPositionMode,
|
PositionObject(instance, xPosition, spawnEntry.spawnPositionMode,
|
||||||
spawnEntry.specifiedY, spawnEntry.randomYMin, spawnEntry.randomYMax);
|
spawnEntry.specifiedY, spawnEntry.randomYMin, spawnEntry.randomYMax);
|
||||||
|
|
||||||
|
if (settings.ShowDebugLogs)
|
||||||
|
{
|
||||||
|
Logging.Debug($"[ObstacleDistanceSpawner] Using per-prefab positioning: {spawnEntry.spawnPositionMode}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Fall back to global default configuration from settings
|
||||||
|
PositionObject(instance, xPosition, settings.DefaultObstaclePositionMode,
|
||||||
|
settings.FallbackYPosition, settings.DefaultObstacleRandomYMin, settings.DefaultObstacleRandomYMax);
|
||||||
|
|
||||||
|
if (settings.ShowDebugLogs)
|
||||||
|
{
|
||||||
|
Logging.Debug($"[ObstacleDistanceSpawner] Using default positioning from settings: {settings.DefaultObstaclePositionMode}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize if implements interface
|
// Initialize if implements interface
|
||||||
@@ -81,100 +193,12 @@ namespace Minigames.Airplane.Core.Spawning
|
|||||||
else
|
else
|
||||||
_negativeSpawnCount++;
|
_negativeSpawnCount++;
|
||||||
|
|
||||||
if (showDebugLogs)
|
if (settings.ShowDebugLogs)
|
||||||
{
|
{
|
||||||
Logging.Debug($"[ObstacleDistanceSpawner] Spawned {(spawnPositive ? "positive" : "negative")} at X={xPosition:F2} from pool {poolIndex}");
|
Logging.Debug($"[ObstacleDistanceSpawner] Spawned {(spawnPositive ? "positive" : "negative")} at X={xPosition:F2} from pool {poolIndex}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool ShouldSpawnPositive()
|
|
||||||
{
|
|
||||||
int totalSpawned = _positiveSpawnCount + _negativeSpawnCount;
|
|
||||||
|
|
||||||
// First few spawns - use pure random
|
|
||||||
if (totalSpawned < 5)
|
|
||||||
{
|
|
||||||
return Random.value <= positiveNegativeRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate current ratio and adjust
|
|
||||||
float currentRatio = totalSpawned > 0 ? (float)_positiveSpawnCount / totalSpawned : 0.5f;
|
|
||||||
float targetRatio = positiveNegativeRatio;
|
|
||||||
|
|
||||||
float adjustedProbability;
|
|
||||||
if (currentRatio < targetRatio)
|
|
||||||
{
|
|
||||||
adjustedProbability = Mathf.Lerp(targetRatio, 1f, (targetRatio - currentRatio) * 2f);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
adjustedProbability = Mathf.Lerp(0f, targetRatio, 1f - (currentRatio - targetRatio) * 2f);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Random.value <= adjustedProbability;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void PositionObject(GameObject obj, float xPosition, SpawnPositionMode mode,
|
|
||||||
float specifiedY, float randomYMin, float randomYMax)
|
|
||||||
{
|
|
||||||
if (obj == null) return;
|
|
||||||
|
|
||||||
float targetY;
|
|
||||||
|
|
||||||
switch (mode)
|
|
||||||
{
|
|
||||||
case SpawnPositionMode.SnapToGround:
|
|
||||||
targetY = SnapToGround(obj, xPosition);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case SpawnPositionMode.SpecifiedY:
|
|
||||||
targetY = specifiedY;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case SpawnPositionMode.RandomRange:
|
|
||||||
targetY = Random.Range(randomYMin, randomYMax);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
targetY = 0f;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
Vector3 newPosition = obj.transform.position;
|
|
||||||
newPosition.y = targetY;
|
|
||||||
obj.transform.position = newPosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
private float SnapToGround(GameObject obj, float xPosition)
|
|
||||||
{
|
|
||||||
Vector2 rayOrigin = new Vector2(xPosition, 0f);
|
|
||||||
int layerMask = 1 << groundLayer;
|
|
||||||
RaycastHit2D hit = Physics2D.Raycast(rayOrigin, Vector2.down, maxGroundRaycastDistance, layerMask);
|
|
||||||
|
|
||||||
if (hit.collider != null)
|
|
||||||
{
|
|
||||||
float groundY = hit.point.y;
|
|
||||||
Bounds bounds = GetObjectBounds(obj);
|
|
||||||
return groundY + bounds.extents.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaultObjectYOffset;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Bounds GetObjectBounds(GameObject obj)
|
|
||||||
{
|
|
||||||
Renderer objRenderer = obj.GetComponentInChildren<Renderer>();
|
|
||||||
if (objRenderer != null) return objRenderer.bounds;
|
|
||||||
|
|
||||||
Collider2D objCollider2D = obj.GetComponentInChildren<Collider2D>();
|
|
||||||
if (objCollider2D != null) return objCollider2D.bounds;
|
|
||||||
|
|
||||||
Collider objCollider3D = obj.GetComponentInChildren<Collider>();
|
|
||||||
if (objCollider3D != null) return objCollider3D.bounds;
|
|
||||||
|
|
||||||
return new Bounds(obj.transform.position, Vector3.one);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Cleanup()
|
public override void Cleanup()
|
||||||
{
|
{
|
||||||
base.Cleanup();
|
base.Cleanup();
|
||||||
@@ -182,18 +206,5 @@ namespace Minigames.Airplane.Core.Spawning
|
|||||||
_negativeSpawnCount = 0;
|
_negativeSpawnCount = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Helper component to store spawn entry data on prefabs.
|
|
||||||
/// Attach this to prefabs that need specific positioning.
|
|
||||||
/// </summary>
|
|
||||||
public class PrefabSpawnEntryComponent : MonoBehaviour
|
|
||||||
{
|
|
||||||
public SpawnPositionMode spawnPositionMode = SpawnPositionMode.SnapToGround;
|
|
||||||
public float specifiedY;
|
|
||||||
public float randomYMin = -5f;
|
|
||||||
public float randomYMax = 5f;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ namespace Minigames.Airplane.Core.Spawning
|
|||||||
|
|
||||||
private Transform _currentCameraTransform;
|
private Transform _currentCameraTransform;
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize(SpawnInitParameters initParams)
|
||||||
{
|
{
|
||||||
// Force exclusive mode
|
// Force exclusive mode
|
||||||
poolMode = SpawnPoolMode.Exclusive;
|
poolMode = SpawnPoolMode.Exclusive;
|
||||||
@@ -35,7 +35,7 @@ namespace Minigames.Airplane.Core.Spawning
|
|||||||
Logging.Error("[ParallaxBackgroundSpawner] Must have exactly 3 pools (Background, Middle, Foreground)!");
|
Logging.Error("[ParallaxBackgroundSpawner] Must have exactly 3 pools (Background, Middle, Foreground)!");
|
||||||
}
|
}
|
||||||
|
|
||||||
base.Initialize();
|
base.Initialize(initParams);
|
||||||
|
|
||||||
// Subscribe to camera changes
|
// Subscribe to camera changes
|
||||||
if (cameraManager != null)
|
if (cameraManager != null)
|
||||||
@@ -76,7 +76,7 @@ namespace Minigames.Airplane.Core.Spawning
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showDebugLogs)
|
if (settings.ShowDebugLogs)
|
||||||
{
|
{
|
||||||
Logging.Debug($"[ParallaxBackgroundSpawner] Camera changed to {newState}, updated parallax elements");
|
Logging.Debug($"[ParallaxBackgroundSpawner] Camera changed to {newState}, updated parallax elements");
|
||||||
}
|
}
|
||||||
@@ -91,9 +91,8 @@ namespace Minigames.Airplane.Core.Spawning
|
|||||||
GameObject prefab = pools[poolIndex].prefabs[Random.Range(0, pools[poolIndex].prefabs.Length)];
|
GameObject prefab = pools[poolIndex].prefabs[Random.Range(0, pools[poolIndex].prefabs.Length)];
|
||||||
if (prefab == null) return;
|
if (prefab == null) return;
|
||||||
|
|
||||||
// Get spawn Y from spawnPoint or default
|
// Spawn at temporary position (Y will be adjusted by PositionObject if component exists)
|
||||||
float spawnY = spawnPoint != null ? spawnPoint.position.y : 0f;
|
Vector3 spawnPosition = new Vector3(xPosition, 0f, 0f);
|
||||||
Vector3 spawnPosition = new Vector3(xPosition, spawnY, 0f);
|
|
||||||
|
|
||||||
// Instantiate
|
// Instantiate
|
||||||
GameObject instance = InstantiatePrefab(prefab, spawnPosition);
|
GameObject instance = InstantiatePrefab(prefab, spawnPosition);
|
||||||
@@ -115,7 +114,16 @@ namespace Minigames.Airplane.Core.Spawning
|
|||||||
// Set sort layer on sprite renderer
|
// Set sort layer on sprite renderer
|
||||||
SetSortLayer(instance, layer);
|
SetSortLayer(instance, layer);
|
||||||
|
|
||||||
if (showDebugLogs)
|
// Try to get spawn entry for Y positioning (same logic as obstacles)
|
||||||
|
var spawnEntry = prefab.GetComponent<PrefabSpawnEntryComponent>();
|
||||||
|
if (spawnEntry != null)
|
||||||
|
{
|
||||||
|
PositionObject(instance, xPosition, spawnEntry.spawnPositionMode,
|
||||||
|
spawnEntry.specifiedY, spawnEntry.randomYMin, spawnEntry.randomYMax);
|
||||||
|
}
|
||||||
|
// Otherwise stays at Y=0
|
||||||
|
|
||||||
|
if (settings.ShowDebugLogs)
|
||||||
{
|
{
|
||||||
Logging.Debug($"[ParallaxBackgroundSpawner] Spawned {layer} element at X={xPosition:F2}");
|
Logging.Debug($"[ParallaxBackgroundSpawner] Spawned {layer} element at X={xPosition:F2}");
|
||||||
}
|
}
|
||||||
@@ -136,7 +144,7 @@ namespace Minigames.Airplane.Core.Spawning
|
|||||||
|
|
||||||
spriteRenderer.sortingLayerName = sortLayerName;
|
spriteRenderer.sortingLayerName = sortLayerName;
|
||||||
|
|
||||||
if (showDebugLogs)
|
if (settings.ShowDebugLogs)
|
||||||
{
|
{
|
||||||
Logging.Debug($"[ParallaxBackgroundSpawner] Set sort layer '{sortLayerName}' for {layer}");
|
Logging.Debug($"[ParallaxBackgroundSpawner] Set sort layer '{sortLayerName}' for {layer}");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using Minigames.Airplane.Data;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Minigames.Airplane.Data
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Component to store spawn positioning data on prefabs.
|
||||||
|
/// Attach this to obstacle prefabs that need specific positioning behavior.
|
||||||
|
/// If not present, spawner will use default positioning from AirplaneSettings.
|
||||||
|
/// </summary>
|
||||||
|
[AddComponentMenu("Airplane/Prefab Spawn Entry")]
|
||||||
|
public class PrefabSpawnEntryComponent : MonoBehaviour
|
||||||
|
{
|
||||||
|
[Tooltip("How to position this object vertically")]
|
||||||
|
public SpawnPositionMode spawnPositionMode = SpawnPositionMode.SnapToGround;
|
||||||
|
|
||||||
|
[Tooltip("Y position to use (when mode is SpecifiedY)")]
|
||||||
|
public float specifiedY;
|
||||||
|
|
||||||
|
[Tooltip("Min Y for random range (when mode is RandomRange)")]
|
||||||
|
public float randomYMin = -5f;
|
||||||
|
|
||||||
|
[Tooltip("Max Y for random range (when mode is RandomRange)")]
|
||||||
|
public float randomYMax = 5f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 90239fb003214b4087d0717f6f417161
|
||||||
|
timeCreated: 1765990367
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using Core.Settings;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Minigames.Airplane.Data
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Parameters passed from AirplaneSpawnManager to spawners during initialization.
|
||||||
|
/// Encapsulates shared references and configuration.
|
||||||
|
/// </summary>
|
||||||
|
public class SpawnInitParameters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Parent transform for spawned objects (organization)
|
||||||
|
/// </summary>
|
||||||
|
public Transform SpawnContainer { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Settings reference for spawn configuration
|
||||||
|
/// </summary>
|
||||||
|
public IAirplaneSettings Settings { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create spawn initialization parameters
|
||||||
|
/// </summary>
|
||||||
|
public SpawnInitParameters(Transform spawnContainer, IAirplaneSettings settings)
|
||||||
|
{
|
||||||
|
SpawnContainer = spawnContainer;
|
||||||
|
Settings = settings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: eb788e2f6d204a5fb1b2ae79ed38e7c2
|
||||||
|
timeCreated: 1765972065
|
||||||
@@ -107,6 +107,9 @@ namespace Minigames.Airplane.Settings
|
|||||||
[Tooltip("Distance interval for ground tile spawning")]
|
[Tooltip("Distance interval for ground tile spawning")]
|
||||||
[SerializeField] private float groundSpawnInterval = 5f;
|
[SerializeField] private float groundSpawnInterval = 5f;
|
||||||
|
|
||||||
|
[Tooltip("Time penalty (seconds) applied to recently-spawned prefabs for diversity")]
|
||||||
|
[SerializeField] private float recencyPenaltyDuration = 10f;
|
||||||
|
|
||||||
[Header("Ground Snapping")]
|
[Header("Ground Snapping")]
|
||||||
[Tooltip("Layer for ground detection (objects will snap to this)")]
|
[Tooltip("Layer for ground detection (objects will snap to this)")]
|
||||||
[Layer]
|
[Layer]
|
||||||
@@ -115,8 +118,24 @@ namespace Minigames.Airplane.Settings
|
|||||||
[Tooltip("Maximum distance to raycast for ground")]
|
[Tooltip("Maximum distance to raycast for ground")]
|
||||||
[SerializeField] private float maxGroundRaycastDistance = 50f;
|
[SerializeField] private float maxGroundRaycastDistance = 50f;
|
||||||
|
|
||||||
[Tooltip("Default Y offset for objects if no ground found")]
|
[Tooltip("Fallback Y position (used when SnapToGround fails OR when using SpecifiedY positioning mode)")]
|
||||||
[SerializeField] private float defaultObjectYOffset = 0f;
|
[SerializeField] private float fallbackYPosition = 0f;
|
||||||
|
|
||||||
|
[Tooltip("Y position at which to spawn ground tiles")]
|
||||||
|
[SerializeField] private float groundSpawnY = -18f;
|
||||||
|
|
||||||
|
[Header("Default Obstacle Positioning")]
|
||||||
|
[Tooltip("Default mode for obstacle positioning")]
|
||||||
|
[SerializeField] private Data.SpawnPositionMode defaultObstaclePositionMode = Data.SpawnPositionMode.SnapToGround;
|
||||||
|
|
||||||
|
[Tooltip("Default Y position for obstacles (if specified)")]
|
||||||
|
[SerializeField] private float defaultObstacleSpecifiedY = -10f;
|
||||||
|
|
||||||
|
[Tooltip("Minimum random Y position for obstacles (if random range used)")]
|
||||||
|
[SerializeField] private float defaultObstacleRandomYMin = -5;
|
||||||
|
|
||||||
|
[Tooltip("Maximum random Y position for obstacles (if random range used)")]
|
||||||
|
[SerializeField] private float defaultObstacleRandomYMax = 5;
|
||||||
|
|
||||||
[Header("Debug")]
|
[Header("Debug")]
|
||||||
[Tooltip("Show debug logs in console")]
|
[Tooltip("Show debug logs in console")]
|
||||||
@@ -155,9 +174,14 @@ namespace Minigames.Airplane.Settings
|
|||||||
public float PositiveNegativeRatio => positiveNegativeRatio;
|
public float PositiveNegativeRatio => positiveNegativeRatio;
|
||||||
public float SpawnDistanceAhead => spawnDistanceAhead;
|
public float SpawnDistanceAhead => spawnDistanceAhead;
|
||||||
public float GroundSpawnInterval => groundSpawnInterval;
|
public float GroundSpawnInterval => groundSpawnInterval;
|
||||||
|
public float RecencyPenaltyDuration => recencyPenaltyDuration;
|
||||||
public int GroundLayer => groundLayer;
|
public int GroundLayer => groundLayer;
|
||||||
public float MaxGroundRaycastDistance => maxGroundRaycastDistance;
|
public float MaxGroundRaycastDistance => maxGroundRaycastDistance;
|
||||||
public float DefaultObjectYOffset => defaultObjectYOffset;
|
public float FallbackYPosition => fallbackYPosition;
|
||||||
|
public float GroundSpawnY => groundSpawnY;
|
||||||
|
public Data.SpawnPositionMode DefaultObstaclePositionMode => defaultObstaclePositionMode;
|
||||||
|
public float DefaultObstacleRandomYMin => defaultObstacleRandomYMin;
|
||||||
|
public float DefaultObstacleRandomYMax => defaultObstacleRandomYMax;
|
||||||
public bool ShowDebugLogs => showDebugLogs;
|
public bool ShowDebugLogs => showDebugLogs;
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@@ -78,15 +78,23 @@ MonoBehaviour:
|
|||||||
introDuration: 2
|
introDuration: 2
|
||||||
personIntroDuration: 2
|
personIntroDuration: 2
|
||||||
evaluationDuration: 2
|
evaluationDuration: 2
|
||||||
dynamicSpawnThresholdMarker: {fileID: 0}
|
targetFlybyLingerDuration: 1.5
|
||||||
targetMinDistance: 100
|
targetFlybyCameraBlendTime: 1
|
||||||
|
preSpawnBeyondTargetDistance: 50
|
||||||
|
targetMinDistance: 200
|
||||||
targetMaxDistance: 300
|
targetMaxDistance: 300
|
||||||
objectSpawnMinDistance: 5
|
objectSpawnMinDistance: 5
|
||||||
objectSpawnMaxDistance: 30
|
objectSpawnMaxDistance: 30
|
||||||
positiveNegativeRatio: 0.5
|
positiveNegativeRatio: 0.7
|
||||||
spawnDistanceAhead: 50
|
spawnDistanceAhead: 75
|
||||||
groundSpawnInterval: 30
|
groundSpawnInterval: 30
|
||||||
|
recencyPenaltyDuration: 10
|
||||||
groundLayer: 14
|
groundLayer: 14
|
||||||
maxGroundRaycastDistance: 50
|
maxGroundRaycastDistance: 50
|
||||||
defaultObjectYOffset: -18
|
fallbackYPosition: 0
|
||||||
showDebugLogs: 0
|
groundSpawnY: -5
|
||||||
|
defaultObstaclePositionMode: 2
|
||||||
|
defaultObstacleSpecifiedY: -10
|
||||||
|
defaultObstacleRandomYMin: 5
|
||||||
|
defaultObstacleRandomYMax: 25
|
||||||
|
showDebugLogs: 1
|
||||||
|
|||||||
Reference in New Issue
Block a user