diff --git a/Assets/Resources/Prefabs/PickableProjectile.prefab b/Assets/Resources/Prefabs/PickableProjectile.prefab new file mode 100644 index 0000000..5940575 --- /dev/null +++ b/Assets/Resources/Prefabs/PickableProjectile.prefab @@ -0,0 +1,226 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &5993946430821065449 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1452460312376183192} + - component: {fileID: -640855425046532091} + - component: {fileID: 8375585699851212105} + - component: {fileID: -7990430535451479416} + m_Layer: 0 + m_Name: PickableProjectile + m_TagString: Projectile + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1452460312376183192 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5993946430821065449} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 2, y: 2, z: 2} + m_ConstrainProportionsScale: 1 + m_Children: + - {fileID: 8859797121973546175} + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!50 &-640855425046532091 +Rigidbody2D: + serializedVersion: 5 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5993946430821065449} + m_BodyType: 0 + m_Simulated: 1 + m_UseFullKinematicContacts: 0 + m_UseAutoMass: 0 + m_Mass: 1 + m_LinearDamping: 0 + m_AngularDamping: 0.05 + m_GravityScale: 1 + m_Material: {fileID: 6200000, guid: 63f618adb0dc58844abe8ec1a9bd45a8, type: 2} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_Interpolate: 0 + m_SleepingMode: 1 + m_CollisionDetection: 0 + m_Constraints: 0 +--- !u!58 &8375585699851212105 +CircleCollider2D: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5993946430821065449} + m_Enabled: 1 + serializedVersion: 3 + m_Density: 1 + m_Material: {fileID: 6200000, guid: 63f618adb0dc58844abe8ec1a9bd45a8, type: 2} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_ForceSendLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_ForceReceiveLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_ContactCaptureLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_CallbackLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_IsTrigger: 0 + m_UsedByEffector: 0 + m_CompositeOperation: 0 + m_CompositeOrder: 0 + m_Offset: {x: 0, y: 0} + m_Radius: 0.5 +--- !u!114 &-7990430535451479416 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5993946430821065449} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5bf80b3f74c7fe04e8064ebca52b7ad4, type: 3} + m_Name: + m_EditorClassIdentifier: + initialSpeed: 15 + maxLifetime: 5 + alignRotationToVelocity: 1 + knockback: 0 + hitLayers: + serializedVersion: 2 + m_Bits: 0 + maxPierce: 0 + useGravity: 1 + gravityScale: 3 + owner: {fileID: 0} + hitVfxPrefab: {fileID: 0} + hitSfx: {fileID: 0} + sfxVolume: 1 + heldLocalOffset: {x: 0.25, y: 0.1, z: 0} + ignoreOwnerTimeOnThrow: 0.15 + attachPointChildName: HandPoint + sleepVelocity: 0.05 + lockWhenLanded: 1 + maxBounces: 3 + minBounceSpeed: 0.8 + restitution: 0.6 + tangentDamping: 0.85 + groundUpTolerance: 0.25 + stickDelayAfterImpact: 0.03 + drawGizmos: 1 + gizmoColor: {r: 0.2, g: 0.8, b: 1, a: 0.35} +--- !u!1 &6093063039154122651 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8859797121973546175} + - component: {fileID: 3362982869789392483} + m_Layer: 0 + m_Name: Circle + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &8859797121973546175 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6093063039154122651} + 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: 1452460312376183192} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!212 &3362982869789392483 +SpriteRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6093063039154122651} + m_Enabled: 1 + m_CastShadows: 0 + m_ReceiveShadows: 0 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 0 + m_RayTraceProcedural: 0 + m_RayTracingAccelStructBuildFlagsOverride: 0 + m_RayTracingAccelStructBuildFlags: 1 + m_SmallMeshCulling: 1 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: a97c105638bdf8b4a8650670310a4cd3, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 0 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 + m_Sprite: {fileID: -2413806693520163455, guid: 7f76a84576351334b8517b4cd5666786, type: 3} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_FlipX: 0 + m_FlipY: 0 + m_DrawMode: 0 + m_Size: {x: 1, y: 1} + m_AdaptiveModeThreshold: 0.5 + m_SpriteTileMode: 0 + m_WasSpriteAssigned: 1 + m_MaskInteraction: 0 + m_SpriteSortPoint: 0 diff --git a/Assets/Resources/Prefabs/PickableProjectile.prefab.meta b/Assets/Resources/Prefabs/PickableProjectile.prefab.meta new file mode 100644 index 0000000..fc3b501 --- /dev/null +++ b/Assets/Resources/Prefabs/PickableProjectile.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 7fd3a37dc702ece42b51e5867473f0ff +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Resources/Prefabs/Player.prefab b/Assets/Resources/Prefabs/Player.prefab index e3faa1f..445b7cb 100644 --- a/Assets/Resources/Prefabs/Player.prefab +++ b/Assets/Resources/Prefabs/Player.prefab @@ -2509,7 +2509,7 @@ GameObject: - component: {fileID: -592641019219869902} - component: {fileID: 8787207208699122079} - component: {fileID: 5723917688524288437} - - component: {fileID: 1401621311817880213} + - component: {fileID: 6106630833600239313} m_Layer: 0 m_Name: Player m_TagString: Player @@ -2639,7 +2639,7 @@ MonoBehaviour: jumpBuffer: 0.1 jumpCut: 0.5 animator: {fileID: 0} ---- !u!114 &1401621311817880213 +--- !u!114 &6106630833600239313 MonoBehaviour: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} @@ -2648,12 +2648,15 @@ MonoBehaviour: m_GameObject: {fileID: 6550761235523395909} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 585b6e60cdd59114893f0ba218225d47, type: 3} + m_Script: {fileID: 11500000, guid: 4e22b17be0e015040bb8efb0e96fa590, type: 3} m_Name: m_EditorClassIdentifier: - projectilePrefab: {fileID: 6769734275023686989, guid: 061e8ccf0e9e36f43ae0e92dadeb6b53, type: 3} - muzzle: {fileID: 0} - shotSpeed: 20 + projectilePrefab: {fileID: -7990430535451479416, guid: 7fd3a37dc702ece42b51e5867473f0ff, type: 3} + spawnIfMissing: 1 + attachPoint: {fileID: 6550761233899429359} + actionKey: 102 + throwSpeed: 20 + pickupRadius: 1.2 --- !u!1 &6550761235546795891 GameObject: m_ObjectHideFlags: 0 diff --git a/Assets/Resources/Scripts/Abstract/Pojectile2D.cs b/Assets/Resources/Scripts/Abstract/Pojectile2D.cs index bba00ed..d0aa886 100644 --- a/Assets/Resources/Scripts/Abstract/Pojectile2D.cs +++ b/Assets/Resources/Scripts/Abstract/Pojectile2D.cs @@ -36,7 +36,7 @@ namespace Projectiles2D protected int remainingPierce; protected bool launched; - protected GameObject owner; + public GameObject owner; protected virtual void Awake() { diff --git a/Assets/Resources/Scripts/EnemyController.cs b/Assets/Resources/Scripts/EnemyController.cs index 0213533..7338627 100644 --- a/Assets/Resources/Scripts/EnemyController.cs +++ b/Assets/Resources/Scripts/EnemyController.cs @@ -22,8 +22,8 @@ public class EnemyController : MonoBehaviour playerObj = GameManager.Instance.playerObj; if (playerObj == null) { - Debug.Log("Player not found!"); playerObj = GameObject.FindGameObjectWithTag("Player"); + Debug.Log("Player not found!"); } } diff --git a/Assets/Resources/Scripts/PhysicsProjectile.cs b/Assets/Resources/Scripts/PhysicsProjectile.cs index 7f5de28..4041be3 100644 --- a/Assets/Resources/Scripts/PhysicsProjectile.cs +++ b/Assets/Resources/Scripts/PhysicsProjectile.cs @@ -23,7 +23,7 @@ public class PhysicsProjectile : BasicProjectile rb.linearVelocity = Vector2.zero; rb.angularVelocity = 0f; - rb.AddForce(dir * finalSpeed * rb.mass, ForceMode2D.Impulse); + rb.AddForce(finalSpeed * rb.mass * dir, ForceMode2D.Impulse); transform.right = dir; diff --git a/Assets/Resources/Scripts/PickableProjectile.cs b/Assets/Resources/Scripts/PickableProjectile.cs new file mode 100644 index 0000000..31171cb --- /dev/null +++ b/Assets/Resources/Scripts/PickableProjectile.cs @@ -0,0 +1,246 @@ +using UnityEngine; + +[DisallowMultipleComponent] +[RequireComponent(typeof(Rigidbody2D))] +[RequireComponent(typeof(Collider2D))] +public class PickableProjectile : BasicProjectile +{ + public static PickableProjectile Instance { get; private set; } + + public enum State { Held, Airborne, Grounded } + public State CurrentState { get; private set; } = State.Grounded; + + [Header("Pickup/Attach")] + public Vector3 heldLocalOffset = new Vector3(0.25f, 0.1f, 0f); + public float ignoreOwnerTimeOnThrow = 0.15f; + public string attachPointChildName = "HandPoint"; + + [Header("Physics on ground")] + public float sleepVelocity = 0.05f; + public bool lockWhenLanded = true; + + [Header("Bouncing")] + [Tooltip("Максимум отскоков прежде чем лечь на землю.")] + public int maxBounces = 3; + + [Tooltip("Минимальная скорость для отскока.")] + public float minBounceSpeed = 0.8f; + + [Tooltip("Коэффициент упругости (0..1).")] + [Range(0f, 1f)] + public float restitution = 0.6f; + + [Tooltip("Демпфирование вдоль поверхности (0..1), меньше — сильнее гасит скольжение.")] + [Range(0f, 1f)] + public float tangentDamping = 0.85f; + + [Tooltip("Насколько «вверх» должна смотреть средняя нормаль, чтобы считать поверхность землёй (чем больше, тем мягче фильтр).")] + [Range(0f, 1f)] + public float groundUpTolerance = 0.25f; + + [Tooltip("Маленькая пауза, чтобы не прилипнуть в кадр столкновения.")] + public float stickDelayAfterImpact = 0.03f; + + [Header("Gizmos")] + public bool drawGizmos; + public Color gizmoColor = new Color(0.2f, 0.8f, 1f, 0.35f); + + private GameObject holder; + private Transform holderAttachPoint; + + private int bounceCounter = 0; + private float lastImpactTime = -999f; + + protected override void Awake() + { + base.Awake(); + + if (Instance != null && Instance != this) + { + Destroy(gameObject); + return; + } + Instance = this; + + rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous; + col.isTrigger = false; + + BecomeGrounded(null); + } + + private void OnDestroy() + { + if (Instance == this) Instance = null; + } + + public bool TryPickup(GameObject newHolder, Transform optionalAttachPoint = null) + { + if (CurrentState == State.Held) return true; + + holder = newHolder; + holderAttachPoint = optionalAttachPoint ?? FindAttachPointOn(holder); + if (holderAttachPoint == null) holderAttachPoint = holder.transform; + + rb.linearVelocity = Vector2.zero; + rb.angularVelocity = 0f; + rb.bodyType = RigidbodyType2D.Kinematic; + col.enabled = false; + + transform.SetParent(holderAttachPoint, worldPositionStays: false); + transform.localPosition = heldLocalOffset; + transform.localRotation = Quaternion.identity; + transform.localScale = Vector3.one * 2; + + StartCoroutine(IgnoreOwnerTemporarily(holder, ignoreOwnerTimeOnThrow)); + + CurrentState = State.Held; + launched = false; + owner = holder; + return true; + } + + public void Launch(Vector2 direction, float speed, GameObject throwOwner = null) + { + if (CurrentState == State.Held) + { + transform.SetParent(null, true); + } + + holder = throwOwner; + holderAttachPoint = null; + + rb.bodyType = RigidbodyType2D.Dynamic; + col.enabled = true; + + launched = true; + owner = throwOwner; + + Vector2 dir = direction.sqrMagnitude > 0.0001f ? direction.normalized : Vector2.right; + float finalSpeed = (speed > 0f) ? speed : initialSpeed; + + rb.linearVelocity = Vector2.zero; + rb.angularVelocity = 0f; + rb.AddForce(dir * finalSpeed * rb.mass, ForceMode2D.Impulse); + transform.right = dir; + + if (throwOwner != null && ignoreOwnerTimeOnThrow > 0f) + StartCoroutine(IgnoreOwnerTemporarily(throwOwner, ignoreOwnerTimeOnThrow)); + + CurrentState = State.Airborne; + bounceCounter = 0; + } + + protected override void OnCollisionEnter2D(Collision2D collision) + { + base.OnCollisionEnter2D(collision); + + if (CurrentState == State.Airborne) + { + if (TryBounce(collision)) + { + lastImpactTime = Time.time; + return; + } + + BecomeGrounded(collision); + } + } + + protected override void OnImpactFinal(Collision2D collision) + { + if (!TryBounce(collision)) + BecomeGrounded(collision); + } + + protected override void OnImpactFinal(Collider2D other) + { + if (Time.time - lastImpactTime > stickDelayAfterImpact) + BecomeGrounded(null); + } + + private bool TryBounce(Collision2D collision) + { + if (Time.time - lastImpactTime < stickDelayAfterImpact) + return true; + + var v = rb.linearVelocity; + float speed = v.magnitude; + if (speed < minBounceSpeed) return false; + + Vector2 avgNormal = Vector2.up; + if (collision.contactCount > 0) + { + avgNormal = Vector2.zero; + for (int i = 0; i < collision.contactCount; i++) + avgNormal += collision.GetContact(i).normal; + avgNormal.Normalize(); + } + + Vector2 reflected = Vector2.Reflect(v, avgNormal) * restitution; + + Vector2 n = avgNormal; + Vector2 vn = Vector2.Dot(reflected, n) * n; + Vector2 vt = reflected - vn; + reflected = vn + vt * tangentDamping; + + rb.linearVelocity = reflected; + rb.position += n * 0.005f; + + bounceCounter++; + + if (bounceCounter >= maxBounces) + return false; + + CurrentState = State.Airborne; + return true; + } + + private void BecomeGrounded(Collision2D collision) + { + launched = false; + CurrentState = State.Grounded; + + if (lockWhenLanded) + { + rb.linearVelocity = Vector2.zero; + rb.angularVelocity = 0f; + rb.bodyType = RigidbodyType2D.Kinematic; + } + else + { + rb.bodyType = RigidbodyType2D.Dynamic; + } + + col.enabled = true; + transform.SetParent(null, true); + bounceCounter = 0; + } + + private Transform FindAttachPointOn(GameObject who) + { + if (string.IsNullOrWhiteSpace(attachPointChildName)) return null; + var t = who.transform.Find(attachPointChildName); + return t; + } + + protected override void FixedUpdate() + { + base.FixedUpdate(); + + if (!lockWhenLanded && CurrentState == State.Airborne) + { + if (rb.linearVelocity.sqrMagnitude < (sleepVelocity * sleepVelocity)) + BecomeGrounded(null); + } + } + +#if UNITY_EDITOR + private void OnDrawGizmosSelected() + { + if (!drawGizmos) return; + Gizmos.color = gizmoColor; + Gizmos.matrix = transform.localToWorldMatrix; + Gizmos.DrawCube(new Vector3(heldLocalOffset.x, heldLocalOffset.y, 0), new Vector3(0.12f, 0.12f, 0.02f)); + } +#endif +} diff --git a/Assets/Resources/Scripts/PickableProjectile.cs.meta b/Assets/Resources/Scripts/PickableProjectile.cs.meta new file mode 100644 index 0000000..2eec332 --- /dev/null +++ b/Assets/Resources/Scripts/PickableProjectile.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5bf80b3f74c7fe04e8064ebca52b7ad4 \ No newline at end of file diff --git a/Assets/Resources/Scripts/PlayerFireScript.cs b/Assets/Resources/Scripts/PlayerFireScript.cs new file mode 100644 index 0000000..8495963 --- /dev/null +++ b/Assets/Resources/Scripts/PlayerFireScript.cs @@ -0,0 +1,78 @@ +using UnityEngine; + +public class PlayerFireScript : MonoBehaviour +{ + [Header("Projectile (single instance)")] + [SerializeField] public PickableProjectile projectilePrefab; + public bool spawnIfMissing = true; + + [Header("Throw / Pickup")] + public Transform attachPoint; + public KeyCode actionKey = KeyCode.F; + public float throwSpeed = 20f; + public float pickupRadius = 1.2f; + + private PickableProjectile P => PickableProjectile.Instance; + + void Awake() + { + if (!attachPoint) + { + var hand = transform.Find("HandPoint"); + if (hand) attachPoint = hand; + } + } + + void Update() + { + if (!Input.GetKeyDown(actionKey)) return; + + var proj = P; + + if (proj == null) + { + if (!spawnIfMissing || projectilePrefab == null) + { + Debug.Log("FireScript2D: projectile missing; set spawnIfMissing=true and assign projectilePrefab."); + return; + } + + Vector3 spawnPos = attachPoint ? attachPoint.position : transform.position; + proj = Instantiate(projectilePrefab, spawnPos, Quaternion.identity); + proj.TryPickup(gameObject, attachPoint); + return; + } + + if (proj.CurrentState == PickableProjectile.State.Held && proj.owner == gameObject) + { + float sx = transform.localScale.x; + float facing = Mathf.Sign(sx == 0 ? 1 : sx); + Vector2 dir = facing > 0 ? Vector2.right : Vector2.left; + + proj.Launch(dir, throwSpeed, gameObject); + return; + } + + Vector3 grabFrom = attachPoint ? attachPoint.position : transform.position; + float dist = Vector2.Distance(proj.transform.position, grabFrom); + + if (proj.CurrentState != PickableProjectile.State.Held && dist <= pickupRadius) + { + proj.TryPickup(gameObject, attachPoint); + return; + } + + // Не в радиусе подбора — ничего не делаем + // Debug.Log("FireScript2D: projectile is too far to pick up."); + } + +#if UNITY_EDITOR + void OnDrawGizmosSelected() + { + // визуально показать радиус подбора + Gizmos.color = new Color(0.2f, 0.8f, 1f, 0.35f); + Vector3 center = attachPoint ? attachPoint.position : transform.position; + Gizmos.DrawWireSphere(center, pickupRadius); + } +#endif +} diff --git a/Assets/Resources/Scripts/PlayerFireScript.cs.meta b/Assets/Resources/Scripts/PlayerFireScript.cs.meta new file mode 100644 index 0000000..556a272 --- /dev/null +++ b/Assets/Resources/Scripts/PlayerFireScript.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4e22b17be0e015040bb8efb0e96fa590 \ No newline at end of file