using Assets.MyGame.Scripts.Net; using Assets.MyGame.Scripts.Net.API; using Assets.MyGame.Scripts.Net.WebResponses; using System.Collections; using System.Collections.Generic; using System.Text; using UnityEngine; public class BullitMove : MonoBehaviour { public string userid; public float moveSpeed = 5; public float desTime = 10f; public string playerID; public GameObject effectPrefab; public GameObject diePrefab; float currShowTime = 0f; Vector3 vector3_startPosition; public bool isTriggerFirst = true; MonsterContral target; public Transform targetTra; public string bulletID; // 转向参数 public float maxTurnSpeed = 15f; // 最大转向速度(度/秒) public float minTurnSpeed = 2f; // 最小转向速度 public float distanceForMaxTurn = 1f; // 在此距离内达到最大转向速度 float hitDistance = 2f; // 命中判定距离 // ⭐ 三合一抛物线(正常炮台散射+锁定时,上下两颗子弹的弧线飞行) // 出膛偏移0 → 中段鼓出到 parabolaAmplitude(弧线最高点)→ 命中前归零(三发收敛到同一条鱼) Vector2 parabolaAxis = Vector2.right; // 横向鼓出方向(垂直于炮口→目标连线) float parabolaAmplitude = 0f; // 最高点横向鼓出幅度(<=0 关闭抛物线) float parabolaInitialDistance = 1f; // 开火时到目标的距离(进度基准) float parabolaPrevLateral = 0f; // 上一帧横向偏移(子弹是增量移动,偏移按帧差叠加) float parabolaBaseSpeed = 0f; // 降速前的原始速度(清理时恢复,防池化复用残留慢速) // ---------------------------------------------------------------- // ⭐ 渲染层级:sortingOrder 优先于 z 深度排序, // 2D 鱼/飞机设置了 sortingOrder 后会把默认 0 的子弹压在下面, // 所以子弹需要显式设置更高的 sortingOrder 保证显示在飞机和鱼的上方 // ---------------------------------------------------------------- [Header("渲染层级")] [Tooltip("子弹sortingOrder层级值,需大于场上鱼/飞机的最大层级,保证子弹显示在最上层")] public int sortOrder = 100; /// 子弹渲染器缓存(设置sortingOrder用,Awake 后只取一次) private Renderer[] _cachedRenderers; /// 应用sortingOrder到子弹所有渲染器(多渲染器依次 +i 递增,保持内部遮挡关系) private void ApplySortOrder() { if (_cachedRenderers == null) _cachedRenderers = GetComponentsInChildren(true); for (int i = 0; i < _cachedRenderers.Length; i++) { var r = _cachedRenderers[i]; if (r != null) r.sortingOrder = sortOrder + i; } } void Awake() { Init(); } bool isLock = false; public void Lock(MonsterContral monsterContral, Transform endTra) { foreach (var item in GetComponents()) { item.enabled = false; } isLock = true; target = monsterContral; targetTra = endTra; } public void NotLock() { foreach (var item in GetComponents()) { item.enabled = true; } isLock = false; target = null; targetTra = null; // 解锁后重置运动基准:锁定期间子弹是追踪移动的,位置早已偏离起点 // 若不重置,非锁定模式的 vector3_startPosition + up*currShowTime*speed 会让子弹瞬移 vector3_startPosition = transform.position; vector3_startPosition.z = -400; currShowTime = 0f; // 解锁同时清除抛物线,防止直线飞行时残留横向偏移 ClearParabola(); } /// /// ⭐ 三合一抛物线:子弹在追踪目标的轨迹上叠加横向正弦偏移—— /// 出膛偏移0 → 中段鼓出到 amplitude(弧线最高点)→ 命中前归零(和另外两发收敛到同一条鱼)。 /// lateralAxis=横向鼓出方向(垂直于炮口→目标连线),totalDistance=开火时到目标的距离。 /// bulletSpeed>0 时给这发子弹降速:子弹默认速度太快(如100)时全程飞行仅零点几秒,弧线根本看不清。 /// public void SetParabola(Vector2 lateralAxis, float amplitude, float totalDistance, float bulletSpeed = 0f) { parabolaAxis = lateralAxis.sqrMagnitude > 0.0001f ? lateralAxis.normalized : Vector2.right; parabolaAmplitude = amplitude; parabolaInitialDistance = Mathf.Max(0.01f, totalDistance); parabolaPrevLateral = 0f; // ⭐ 降速拉长飞行时间,弧线才看得清;记录原始速度,清理时恢复 if (bulletSpeed > 0f) { if (parabolaBaseSpeed <= 0f) parabolaBaseSpeed = moveSpeed; moveSpeed = bulletSpeed; } } /// 清除抛物线状态(解锁/回池时调用):关偏移 + 恢复原速度 void ClearParabola() { parabolaAmplitude = 0f; parabolaPrevLateral = 0f; if (parabolaBaseSpeed > 0f) { moveSpeed = parabolaBaseSpeed; parabolaBaseSpeed = 0f; } } public void Init() { vector3_startPosition = transform.position; vector3_startPosition.z = -400; transform.position = vector3_startPosition; // ⭐ 应用渲染层级(池化复用时 Init 会重新调用,Inspector 改了值也能生效) ApplySortOrder(); } /// 对象池回收前调用:重置所有运行时状态,避免复用时残留旧数据 public void ResetState() { userid = null; playerID = null; bulletID = null; currShowTime = 0f; isTriggerFirst = true; target = null; targetTra = null; // 解除锁定 + 恢复碰撞器 NotLock(); // 重置起始位置(z=-400 是原 Awake→Init 的默认值) vector3_startPosition = transform.position; vector3_startPosition.z = -400; } void Update() { // 子弹生命周期:超时自动销毁(默认10秒) currShowTime += Time.deltaTime; if (currShowTime >= desTime) { BulletPool.Instance.Return(gameObject); return; } if (isLock) { // 目标无效或已死亡 → 解除锁定 if (target == null || target.isDead || targetTra == null || !target.gameObject.activeInHierarchy || !targetTra.gameObject.activeInHierarchy) { NotLock(); return; } Vector3 myPos = transform.position; myPos.z = 0; Vector3 targetPos = targetTra.position; targetPos.z = 0; float distance = Vector3.Distance(myPos, targetPos); // 命中判定 if (distance <= hitDistance) { AttackFish(target); return; } // 计算指向目标的方向 Vector3 targetDir = (targetPos - myPos).normalized; // 根据距离动态计算转向速度:距离越近转向越快 float t = Mathf.Clamp01(1f - (distance / distanceForMaxTurn)); float turnSpeed = Mathf.Lerp(minTurnSpeed, maxTurnSpeed, t); // 使用 RotateTowards 平滑转向(角度/秒) float currentAngle = Vector2.SignedAngle(transform.up, targetDir); float newAngle = Mathf.MoveTowardsAngle(currentAngle, 0f, turnSpeed * Time.deltaTime); transform.up = Quaternion.Euler(0, 0, newAngle) * transform.up; // 沿当前方向移动 transform.position += transform.up * moveSpeed * Time.deltaTime; // ⭐ 三合一抛物线:在追踪轨迹上叠加横向正弦偏移 // 进度按剩余距离算(dist=初始距离→0,dist=命中距离→1),命中前偏移正好归零,三发收敛到目标身上 if (parabolaAmplitude > 0f) { float range = parabolaInitialDistance - hitDistance; float progress = range > 0.1f ? Mathf.Clamp01(1f - (distance - hitDistance) / range) : 1f; float lateralNow = parabolaAmplitude * Mathf.Sin(progress * Mathf.PI); transform.position += (Vector3)parabolaAxis * (lateralNow - parabolaPrevLateral); parabolaPrevLateral = lateralNow; } } else { // 非锁定模式:保持原有直线 + 反弹逻辑(不变) Vector3 targetPoint = transform.up.normalized * currShowTime * moveSpeed; targetPoint.z = 0; transform.position = vector3_startPosition + targetPoint; } } private void OnCollisionEnter2D(Collision2D collision) { if (isTriggerFirst) { if (collision.transform.tag == "Fish") { // ⭐ 已死亡的鱼(死亡特效播放窗口期)不可被命中,子弹穿透继续飞 var hitMonster = collision.transform.GetComponentInParent(); if (hitMonster != null && hitMonster.isDead) return; isTriggerFirst = false; if (effectPrefab != null) { EffectPool.Instance.Get(effectPrefab, transform.position, Quaternion.identity, transform.parent); } if (userid == GameManager.Instance.user.id) { hitMonster.monsterData.charactersHealth.OnHurt(10, CharacterHurtType.none); Destroy(hitMonster.gameObject); } BulletPool.Instance.Return(gameObject); } } } public static Quaternion RotationFromVector2(Vector2 dir) { float angle = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg; return Quaternion.Euler(0, 0, angle); } Vector2 GetReflectionAngle(Vector2 velocity, Vector2 normal) { return Vector2.Reflect(velocity.normalized, normal.normalized); } public void OnTriggerEnter2D(Collider2D collision) { if (isTriggerFirst) { if (collision.tag == "Fish") { // ⭐ 已死亡的鱼(死亡特效播放窗口期)不可被命中,子弹穿透继续飞 var monster = collision.transform.GetComponentInParent(); if (monster != null && monster.isDead) return; isTriggerFirst = false; AttackFish(monster); } else if (collision.tag == "Wall") { Vector3 v3 = transform.eulerAngles; switch (collision.name) { case "up": v3.z = -v3.z + 180; break; case "down": v3.z = -v3.z + 180; break; case "left": v3.z = -v3.z; break; case "right": v3.z = -v3.z; break; default: break; } vector3_startPosition = transform.position; currShowTime = 0; transform.eulerAngles = v3; } } } private void AttackFish(MonsterContral monster) { // ⭐ 兜底:已死亡的鱼(死亡特效播放窗口期)不可被命中,子弹回池不结算 if (monster == null || monster.isDead) { BulletPool.Instance.Return(gameObject); return; } if (effectPrefab != null) { EffectPool.Instance.Get(effectPrefab, transform.position, Quaternion.identity, transform.parent); } if (userid == GameManager.Instance.user.id) { // 真人玩家击杀 var v2 = GameManager.Instance.gameData.currRoomData; WebSocketSendList.KillFish(v2.roomId, userid, monster.monsterData.fishTypeConfig.fishNumber, monster.guid, (int)CreateFishManager.Instance.currGameType.currSitData.consumeGoldNum, bulletID, "63215f0afa3f5b25"); } else if (!string.IsNullOrEmpty(userid) && userid.StartsWith("bot_")) { // 机器人击杀:只有房主客户端发送 BotKillFish if (SitPosData.roomOwnerSit != null && SitPosData.roomOwnerSit.user == GameManager.Instance.user.id) { var v2 = GameManager.Instance.gameData.currRoomData; // 从 bot 座位获取炮倍 long gold = 100; var panel = CreateFishManager.Instance.currGameType; if (panel != null && panel.list_Sits != null) { var sit = panel.list_Sits.Find(s => s.user == userid); if (sit != null) gold = sit.consumeGoldNum; } WebSocketSendList.BotKillFish( v2.roomId, userid, monster.monsterData.fishTypeConfig.fishNumber, monster.guid, gold, bulletID ); } } monster.OnHurt(); BulletPool.Instance.Return(gameObject); } }