Some more airplane game doodles

This commit is contained in:
Michal Pikulski
2025-12-08 12:56:14 +01:00
parent 861749485e
commit 41d99a1390
58 changed files with 3319 additions and 147 deletions

View File

@@ -199,7 +199,7 @@ namespace Minigames.Airplane.Core
HandleLanding();
yield break;
}
yield return null; // Update every frame, not just fixed update
}
}
@@ -209,7 +209,7 @@ namespace Minigames.Airplane.Core
#region Collision Detection
/// <summary>
/// Detect trigger collisions with targets
/// Detect trigger collisions with targets and ground
/// </summary>
private void OnTriggerEnter2D(Collider2D other)
{
@@ -227,6 +227,15 @@ namespace Minigames.Airplane.Core
// Land after hitting target
HandleLanding();
return;
}
// Check if it's ground (by layer)
var settings = GameManager.GetSettingsObject<AppleHills.Core.Settings.IAirplaneSettings>();
if (settings != null && other.gameObject.layer == settings.GroundLayer)
{
if (showDebugLogs) Logging.Debug($"[AirplaneController] Hit ground at Y={transform.position.y:F2}");
HandleLanding();
}
}

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using AppleHills.Core.Settings;
using Core;
using Core.Lifecycle;
using Minigames.Airplane.Data;
using Minigames.Airplane.UI;
using UnityEngine;
using Random = UnityEngine.Random;
@@ -25,6 +26,18 @@ namespace Minigames.Airplane.Core
[Tooltip("Prefab to spawn for this target")]
public GameObject prefab;
[Tooltip("How to position this target vertically")]
public SpawnPositionMode spawnPositionMode = SpawnPositionMode.SnapToGround;
[Tooltip("Y position to use (SpecifiedY mode)")]
public float specifiedY = 0f;
[Tooltip("Min Y for random range (RandomRange mode)")]
public float randomYMin = -5f;
[Tooltip("Max Y for random range (RandomRange mode)")]
public float randomYMax = 5f;
}
#endregion
@@ -35,11 +48,11 @@ namespace Minigames.Airplane.Core
[Tooltip("Dictionary of target prefabs (key = target name)")]
[SerializeField] private TargetPrefabEntry[] targetPrefabs;
[Tooltip("Array of positive object prefabs")]
[SerializeField] private GameObject[] positiveObjectPrefabs;
[Tooltip("Array of positive object prefabs with spawn configuration")]
[SerializeField] private PrefabSpawnEntry[] positiveObjectPrefabs;
[Tooltip("Array of negative object prefabs")]
[SerializeField] private GameObject[] negativeObjectPrefabs;
[Tooltip("Array of negative object prefabs with spawn configuration")]
[SerializeField] private PrefabSpawnEntry[] negativeObjectPrefabs;
[Tooltip("Array of ground tile prefabs")]
[SerializeField] private GameObject[] groundTilePrefabs;
@@ -52,6 +65,10 @@ namespace Minigames.Airplane.Core
[Tooltip("Launch controller (provides launch anchor position for distance calculation)")]
[SerializeField] private AirplaneLaunchController launchController;
[Header("Spawn Threshold")]
[Tooltip("Transform marker in scene where dynamic spawning begins (uses X position). If null, uses fallback from settings.")]
[SerializeField] private Transform dynamicSpawnThresholdMarker;
[Header("Spawn Parents")]
[Tooltip("Parent transform for spawned objects (optional, for organization)")]
[SerializeField] private Transform spawnedObjectsParent;
@@ -77,6 +94,7 @@ namespace Minigames.Airplane.Core
private Sprite _targetIconSprite;
private GameObject _spawnedTarget;
private GameObject _targetPrefabToSpawn;
private TargetPrefabEntry _currentTargetEntry;
private bool _hasSpawnedTarget;
// Plane tracking
@@ -84,8 +102,8 @@ namespace Minigames.Airplane.Core
private bool _isSpawningActive;
private bool _hasPassedThreshold;
// Spawning timers
private float _nextObjectSpawnTime;
// Spawning positions (distance-based)
private float _nextObjectSpawnX;
private float _nextGroundSpawnX;
// Spawn statistics (for weighted ratio adjustment)
@@ -97,7 +115,7 @@ namespace Minigames.Airplane.Core
private bool _isRetryAttempt;
// Cached dictionaries
private Dictionary<string, GameObject> _targetPrefabDict;
private Dictionary<string, TargetPrefabEntry> _targetPrefabDict;
private IAirplaneSettings _settings;
#endregion
@@ -148,7 +166,9 @@ namespace Minigames.Airplane.Core
}
// Check if plane has crossed threshold
if (!_hasPassedThreshold && planeX >= _settings.DynamicSpawnThreshold)
float threshold = dynamicSpawnThresholdMarker.position.x;
if (!_hasPassedThreshold && planeX >= threshold)
{
_hasPassedThreshold = true;
InitializeDynamicSpawning();
@@ -167,11 +187,11 @@ namespace Minigames.Airplane.Core
if (shouldSpawnNewContent)
{
// Spawn objects at intervals
if (Time.time >= _nextObjectSpawnTime)
// Spawn objects when plane reaches spawn position
if (planeX >= _nextObjectSpawnX)
{
SpawnRandomObject();
ScheduleNextObjectSpawn();
ScheduleNextObjectSpawn(planeX);
}
// Spawn ground tiles ahead of plane
@@ -229,13 +249,15 @@ namespace Minigames.Airplane.Core
}
}
// Find target prefab and extract icon WITHOUT spawning
if (!_targetPrefabDict.TryGetValue(_currentTargetKey, out _targetPrefabToSpawn))
// Find target entry and extract icon WITHOUT spawning
if (!_targetPrefabDict.TryGetValue(_currentTargetKey, out _currentTargetEntry))
{
Logging.Error($"[SpawnManager] Target prefab not found for key '{_currentTargetKey}'!");
return;
}
_targetPrefabToSpawn = _currentTargetEntry.prefab;
// Extract icon from prefab (doesn't need to be instantiated)
ExtractTargetIconFromPrefab(_targetPrefabToSpawn);
@@ -400,7 +422,7 @@ namespace Minigames.Airplane.Core
/// </summary>
private void BuildTargetDictionary()
{
_targetPrefabDict = new Dictionary<string, GameObject>();
_targetPrefabDict = new Dictionary<string, TargetPrefabEntry>();
if (targetPrefabs == null || targetPrefabs.Length == 0)
{
@@ -428,7 +450,7 @@ namespace Minigames.Airplane.Core
continue;
}
_targetPrefabDict[entry.targetKey] = entry.prefab;
_targetPrefabDict[entry.targetKey] = entry;
}
if (showDebugLogs)
@@ -482,24 +504,28 @@ namespace Minigames.Airplane.Core
/// </summary>
private void SpawnTarget()
{
if (!_targetPrefabDict.TryGetValue(_currentTargetKey, out GameObject targetPrefab))
if (_currentTargetEntry == null || _currentTargetEntry.prefab == null)
{
Logging.Error($"[SpawnManager] Target prefab not found for key '{_currentTargetKey}'!");
return;
}
// Spawn target at initial position
_spawnedTarget = Instantiate(targetPrefab, _targetSpawnPosition, Quaternion.identity);
_spawnedTarget = Instantiate(_currentTargetEntry.prefab, _targetSpawnPosition, Quaternion.identity);
if (spawnedObjectsParent != null)
{
_spawnedTarget.transform.SetParent(spawnedObjectsParent);
}
// Snap target to ground
SnapObjectToGround(_spawnedTarget, _targetSpawnPosition.x);
// Position target using configured spawn mode
PositionObject(_spawnedTarget, _targetSpawnPosition.x,
_currentTargetEntry.spawnPositionMode,
_currentTargetEntry.specifiedY,
_currentTargetEntry.randomYMin,
_currentTargetEntry.randomYMax);
// Update target spawn position to actual snapped position
// Update target spawn position to actual positioned location
_targetSpawnPosition = _spawnedTarget.transform.position;
// Extract sprite for UI icon
@@ -591,11 +617,16 @@ namespace Minigames.Airplane.Core
/// </summary>
private void InitializeDynamicSpawning()
{
ScheduleNextObjectSpawn();
if (showDebugLogs)
// Schedule first spawn trigger from current plane position
// Actual spawning will happen at look-ahead distance
if (_planeTransform != null)
{
Logging.Debug("[SpawnManager] Dynamic spawning initialized");
ScheduleNextObjectSpawn(_planeTransform.position.x);
if (showDebugLogs)
{
Logging.Debug($"[SpawnManager] Dynamic spawning initialized, first spawn trigger at planeX={_nextObjectSpawnX:F2}");
}
}
}
@@ -608,24 +639,32 @@ namespace Minigames.Airplane.Core
}
/// <summary>
/// Schedule the next object spawn based on random interval.
/// Schedule the next object spawn based on random distance from current position.
/// </summary>
private void ScheduleNextObjectSpawn()
/// <param name="currentX">Current X position (usually plane's X or last spawn X)</param>
private void ScheduleNextObjectSpawn(float currentX)
{
float interval = Random.Range((float)_settings.ObjectSpawnMinInterval, (float)_settings.ObjectSpawnMaxInterval);
_nextObjectSpawnTime = Time.time + interval;
float spawnDistance = Random.Range(_settings.ObjectSpawnMinDistance, _settings.ObjectSpawnMaxDistance);
_nextObjectSpawnX = currentX + spawnDistance;
if (showDebugLogs)
{
Logging.Debug($"[SpawnManager] Next object scheduled at X={_nextObjectSpawnX:F2} (distance: {spawnDistance:F2} from {currentX:F2})");
}
}
/// <summary>
/// Spawn a random positive or negative object.
/// Uses weighted randomness to maintain target ratio.
/// Avoids spawning near target position to prevent obscuring it.
/// Objects spawn at look-ahead distance to ensure they're off-screen.
/// </summary>
private void SpawnRandomObject()
{
if (_planeTransform == null) return;
// Calculate spawn X position ahead of plane
// Spawn at look-ahead distance from plane's current position
// This ensures objects always spawn off-screen
float spawnX = _planeTransform.position.x + _settings.SpawnDistanceAhead;
// Check if spawn position is too close to target (avoid obscuring it)
@@ -634,24 +673,26 @@ namespace Minigames.Airplane.Core
if (distanceToTarget < targetClearanceZone)
{
// Too close to target, skip this spawn
// Too close to target, skip this spawn and schedule the next one
if (showDebugLogs)
{
Logging.Debug($"[SpawnManager] Skipped object spawn at X={spawnX:F2} (too close to target at X={_targetSpawnPosition.x:F2})");
}
// Schedule next spawn further ahead
ScheduleNextObjectSpawn(spawnX);
return;
}
// Determine if spawning positive or negative based on weighted ratio
bool spawnPositive = ShouldSpawnPositive();
GameObject prefabToSpawn = null;
PrefabSpawnEntry entryToSpawn = null;
if (spawnPositive)
{
if (positiveObjectPrefabs != null && positiveObjectPrefabs.Length > 0)
{
prefabToSpawn = positiveObjectPrefabs[UnityEngine.Random.Range(0, positiveObjectPrefabs.Length)];
entryToSpawn = positiveObjectPrefabs[UnityEngine.Random.Range(0, positiveObjectPrefabs.Length)];
_positiveSpawnCount++;
}
}
@@ -659,25 +700,36 @@ namespace Minigames.Airplane.Core
{
if (negativeObjectPrefabs != null && negativeObjectPrefabs.Length > 0)
{
prefabToSpawn = negativeObjectPrefabs[UnityEngine.Random.Range(0, negativeObjectPrefabs.Length)];
entryToSpawn = negativeObjectPrefabs[UnityEngine.Random.Range(0, negativeObjectPrefabs.Length)];
_negativeSpawnCount++;
}
}
if (prefabToSpawn == null) return;
if (entryToSpawn == null || entryToSpawn.prefab == null) return;
// Spawn object at temporary position
Vector3 tempPosition = new Vector3(spawnX, 0f, 0f);
GameObject spawnedObject = Instantiate(prefabToSpawn, tempPosition, Quaternion.identity);
GameObject spawnedObject = Instantiate(entryToSpawn.prefab, tempPosition, Quaternion.identity);
if (spawnedObjectsParent != null)
{
spawnedObject.transform.SetParent(spawnedObjectsParent);
}
// Snap to ground
SnapObjectToGround(spawnedObject, spawnX);
// Position object using entry's spawn configuration
PositionObject(spawnedObject, spawnX,
entryToSpawn.spawnPositionMode,
entryToSpawn.specifiedY,
entryToSpawn.randomYMin,
entryToSpawn.randomYMax);
// Initialize components that need post-spawn setup
var initializable = spawnedObject.GetComponent<Interactive.ISpawnInitializable>();
if (initializable != null)
{
initializable.Initialize();
}
if (showDebugLogs)
{
@@ -749,7 +801,57 @@ namespace Minigames.Airplane.Core
#endregion
#region Ground Snapping
#region Object Positioning
/// <summary>
/// Position an object based on the specified spawn mode.
/// </summary>
/// <param name="obj">Object to position</param>
/// <param name="xPosition">X position for the object</param>
/// <param name="mode">Spawn position mode to use</param>
/// <param name="specifiedY">Y value for SpecifiedY mode</param>
/// <param name="randomYMin">Min Y for RandomRange mode</param>
/// <param name="randomYMax">Max Y for RandomRange mode</param>
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;
if (showDebugLogs)
{
Logging.Debug($"[SpawnManager] Positioned object at specified Y={targetY:F2}");
}
break;
case SpawnPositionMode.RandomRange:
targetY = Random.Range(randomYMin, randomYMax);
if (showDebugLogs)
{
Logging.Debug($"[SpawnManager] Positioned object at random Y={targetY:F2} (range: {randomYMin:F2} to {randomYMax:F2})");
}
break;
default:
Logging.Error($"[SpawnManager] Unknown spawn position mode: {mode}");
targetY = 0f;
break;
}
// Apply position
Vector3 newPosition = obj.transform.position;
newPosition.y = targetY;
obj.transform.position = newPosition;
}
/// <summary>
/// Snap an object to the ground using raycast.
@@ -757,10 +859,9 @@ namespace Minigames.Airplane.Core
/// </summary>
/// <param name="obj">Object to snap to ground</param>
/// <param name="xPosition">X position to raycast from</param>
private void SnapObjectToGround(GameObject obj, float xPosition)
/// <returns>The Y position where object was snapped</returns>
private float SnapToGround(GameObject obj, float xPosition)
{
if (obj == null) return;
// Start raycast from high Y position
Vector2 rayOrigin = new Vector2(xPosition, 0.0f);
@@ -800,10 +901,7 @@ namespace Minigames.Airplane.Core
Logging.Warning($"[SpawnManager] No ground found at X={xPosition}, using default Y={targetY}");
}
// Apply position
Vector3 newPosition = obj.transform.position;
newPosition.y = targetY;
obj.transform.position = newPosition;
return targetY;
}
/// <summary>

View File

@@ -0,0 +1,29 @@
using System;
using UnityEngine;
namespace Minigames.Airplane.Data
{
/// <summary>
/// Defines a prefab with spawn position configuration.
/// Used for positive/negative objects in the airplane minigame.
/// </summary>
[Serializable]
public class PrefabSpawnEntry
{
[Tooltip("Prefab to spawn")]
public GameObject prefab;
[Tooltip("How to position this object vertically")]
public SpawnPositionMode spawnPositionMode = SpawnPositionMode.SnapToGround;
[Tooltip("Y position to use (SpecifiedY mode)")]
public float specifiedY;
[Tooltip("Min Y for random range (RandomRange mode)")]
public float randomYMin = -5f;
[Tooltip("Max Y for random range (RandomRange mode)")]
public float randomYMax = 5f;
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d0290f7749fa4d2bb5cc8830a371705f
timeCreated: 1765189597

View File

@@ -0,0 +1,24 @@
namespace Minigames.Airplane.Data
{
/// <summary>
/// Defines how spawned objects are positioned vertically.
/// </summary>
public enum SpawnPositionMode
{
/// <summary>
/// Raycast down to find ground and snap object's bottom to ground surface.
/// </summary>
SnapToGround,
/// <summary>
/// Spawn at a specific Y coordinate.
/// </summary>
SpecifiedY,
/// <summary>
/// Spawn at a random Y coordinate within a specified range.
/// </summary>
RandomRange
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 97deb3f78d56408e83dc9f766418a5a2
timeCreated: 1765189120

View File

@@ -3,11 +3,13 @@
namespace Minigames.Airplane.Interactive
{
/// <summary>
/// Gravity well that pulls airplanes toward its center.
/// Gravity well that pulls airplanes toward its Y position (vertical only).
/// Plane below the well gets pulled UP, plane above gets pulled DOWN.
/// Does NOT affect horizontal (X) velocity - plane maintains forward momentum.
/// Creates challenging "danger zones" that players must avoid or escape from.
/// </summary>
[RequireComponent(typeof(Collider2D))]
public class AirplaneGravityWell : MonoBehaviour
public class AirplaneGravityWell : MonoBehaviour, ISpawnInitializable
{
[Header("Gravity Configuration")]
[SerializeField] private float pullStrength = 5f;
@@ -37,7 +39,14 @@ namespace Minigames.Airplane.Interactive
{
collider.isTrigger = true;
}
}
/// <summary>
/// Called by spawn manager after object is positioned.
/// Caches the final Y position for pull calculations.
/// </summary>
public void Initialize()
{
centerPosition = transform.position;
}
@@ -59,31 +68,31 @@ namespace Minigames.Airplane.Interactive
var rb = other.GetComponent<Rigidbody2D>();
if (rb == null) return;
// Calculate direction and distance to center
// Calculate VERTICAL distance only (Y-axis only)
Vector2 airplanePos = rb.position;
Vector2 toCenter = centerPosition - airplanePos;
float distance = toCenter.magnitude;
float yDistance = centerPosition.y - airplanePos.y;
float absYDistance = Mathf.Abs(yDistance);
// Prevent division by zero
if (distance < minPullDistance)
if (absYDistance < minPullDistance)
{
distance = minPullDistance;
absYDistance = minPullDistance;
}
// Calculate pull force
// Calculate pull force based on vertical distance
float forceMagnitude;
if (useInverseSquare)
{
// Realistic gravity-like force (inverse square law)
forceMagnitude = pullStrength / (distance * distance);
forceMagnitude = pullStrength / (absYDistance * absYDistance);
}
else
{
// Linear falloff based on distance
// Linear falloff based on vertical distance
var collider = GetComponent<Collider2D>();
float maxDistance = collider != null ? collider.bounds.extents.magnitude : 5f;
float normalizedDistance = Mathf.Clamp01(distance / maxDistance);
float maxDistance = collider != null ? collider.bounds.extents.y * 2f : 5f; // Use height, not diagonal
float normalizedDistance = Mathf.Clamp01(absYDistance / maxDistance);
float falloff = pullFalloff.Evaluate(1f - normalizedDistance);
forceMagnitude = pullStrength * falloff;
}
@@ -91,13 +100,16 @@ namespace Minigames.Airplane.Interactive
// Clamp force
forceMagnitude = Mathf.Min(forceMagnitude, maxPullForce);
// Apply force toward center
Vector2 pullForce = toCenter.normalized * forceMagnitude;
// Apply force ONLY in Y direction, toward the well's Y position
// If plane is below (yDistance > 0), pull up (+Y)
// If plane is above (yDistance < 0), pull down (-Y)
float yForceDirection = yDistance > 0 ? 1f : -1f;
Vector2 pullForce = new Vector2(0f, yForceDirection * forceMagnitude);
rb.AddForce(pullForce, ForceMode2D.Force);
if (showDebugLogs && Time.frameCount % 30 == 0) // Log every 30 frames
{
Debug.Log($"[AirplaneGravityWell] Pulling {other.name}: force={forceMagnitude:F2}, distance={distance:F2}");
Debug.Log($"[AirplaneGravityWell] Pulling {other.name}: force={forceMagnitude:F2} {(yForceDirection > 0 ? "UP" : "DOWN")}, Y-distance={absYDistance:F2}");
}
}

View File

@@ -40,7 +40,7 @@ namespace Minigames.Airplane.Interactive
if (rb != null)
{
Vector2 force = isWorldSpace ? windForce : transform.TransformDirection(windForce);
rb.AddForce(force * Time.fixedDeltaTime, ForceMode2D.Force);
rb.AddForce(force, ForceMode2D.Force);
if (showDebugLogs)
{

View File

@@ -0,0 +1,16 @@
namespace Minigames.Airplane.Interactive
{
/// <summary>
/// Interface for objects that need initialization after being spawned and positioned.
/// The spawn manager will call Initialize() after setting the object's position.
/// </summary>
public interface ISpawnInitializable
{
/// <summary>
/// Called by the spawn manager after the object has been instantiated and positioned.
/// Use this to cache position-dependent state instead of using Awake().
/// </summary>
void Initialize();
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a4d1d877662847eeae0daa9fd4fa4788
timeCreated: 1765193296

View File

@@ -75,8 +75,8 @@ namespace Minigames.Airplane.Settings
[SerializeField] private float evaluationDuration = 1f;
[Header("Spawn System")]
[Tooltip("X position where dynamic spawning begins")]
[SerializeField] private float dynamicSpawnThreshold = 10f;
[Tooltip("Transform marker in scene where dynamic spawning begins (uses X position). If null, uses fallback distance.")]
[SerializeField] private Transform dynamicSpawnThresholdMarker;
[Tooltip("Minimum random distance for target spawn")]
[SerializeField] private float targetMinDistance = 30f;
@@ -84,11 +84,11 @@ namespace Minigames.Airplane.Settings
[Tooltip("Maximum random distance for target spawn")]
[SerializeField] private float targetMaxDistance = 50f;
[Tooltip("Minimum time interval between object spawns (seconds)")]
[SerializeField] private float objectSpawnMinInterval = 1f;
[Tooltip("Minimum distance between spawned objects (units)")]
[SerializeField] private float objectSpawnMinDistance = 5f;
[Tooltip("Maximum time interval between object spawns (seconds)")]
[SerializeField] private float objectSpawnMaxInterval = 3f;
[Tooltip("Maximum distance between spawned objects (units)")]
[SerializeField] private float objectSpawnMaxDistance = 20f;
[Tooltip("Ratio of positive to negative objects (0 = all negative, 1 = all positive)")]
[Range(0f, 1f)]
@@ -138,11 +138,10 @@ namespace Minigames.Airplane.Settings
public float IntroDuration => introDuration;
public float PersonIntroDuration => personIntroDuration;
public float EvaluationDuration => evaluationDuration;
public float DynamicSpawnThreshold => dynamicSpawnThreshold;
public float TargetMinDistance => targetMinDistance;
public float TargetMaxDistance => targetMaxDistance;
public float ObjectSpawnMinInterval => objectSpawnMinInterval;
public float ObjectSpawnMaxInterval => objectSpawnMaxInterval;
public float ObjectSpawnMinDistance => objectSpawnMinDistance;
public float ObjectSpawnMaxDistance => objectSpawnMaxDistance;
public float PositiveNegativeRatio => positiveNegativeRatio;
public float SpawnDistanceAhead => spawnDistanceAhead;
public float GroundSpawnInterval => groundSpawnInterval;