From 0d7df18324d73b881bf8799a977719bcfe113dc4 Mon Sep 17 00:00:00 2001 From: SepComet <2428390463@qq.com> Date: Fri, 20 Mar 2026 09:55:52 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=20WeaponLance=20=E7=9A=84?= =?UTF-8?q?=E6=94=BB=E5=87=BB=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Assets/GameMain/DataTables/Weapon.txt | 2 +- .../EntityData/Weapon/WeaponLanceData.cs | 23 ++- .../AttackEffects/LanceThrustAttackEffect.cs | 82 +++++++++ .../LanceThrustAttackEffect.cs.meta | 11 ++ .../Entity/EntityLogic/Weapon/WeaponBase.cs | 16 +- .../Weapon/WeaponLance/WeaponLance.cs | 158 +++++++++++------- .../SimulationWorld.CollisionTransient.cs | 10 +- .../SimulationWorld.JobDataChannel.cs | 1 + .../JobStruct/AreaCollisionRequestData.cs | 4 +- .../JobStruct/CollisionQueryData.cs | 4 +- .../QueryCollisionCandidatesBurstJob.cs | 22 ++- .../SimulationWorld.CollisionBroadPhase.cs | 3 +- .../Jobs/SimulationWorld.CollisionRequests.cs | 21 ++- .../Jobs/SimulationWorld.CollisionResolve.cs | 46 ++++- .../Scripts/Utility/ItemDescUtility.cs | 7 +- 数据表/Entity/Weapon.xlsx | Bin 12936 -> 12931 bytes 16 files changed, 321 insertions(+), 89 deletions(-) create mode 100644 Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/AttackEffects/LanceThrustAttackEffect.cs create mode 100644 Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/AttackEffects/LanceThrustAttackEffect.cs.meta diff --git a/Assets/GameMain/DataTables/Weapon.txt b/Assets/GameMain/DataTables/Weapon.txt index 5075f13..921e7c9 100644 --- a/Assets/GameMain/DataTables/Weapon.txt +++ b/Assets/GameMain/DataTables/Weapon.txt @@ -5,4 +5,4 @@ 2 202 手枪 Almighty_Icon White 130 0.05 120 1 15 10000 {} [] 3 203 斧头 Almighty_Icon White 100 0.1 150 2 5 10000 {"sectorAngle":120} [] 4 204 闪电 Almighty_Icon White 150 0.08 80 3 12 10000 {"hitRadius":3} [] - 5 205 长枪 Almighty_Icon White 100 0.1 100 1.5 7 10000 {"hitRadius":0.3,"thrustDistance":1.2,"pierceLength":0.3} + 5 205 长枪 Almighty_Icon White 100 0.1 100 1.5 5 10000 {"hitHalfWidth":0.7,"pierceLength":4.5,"hitHeight":0.5,"hitCenterYOffset":0} diff --git a/Assets/GameMain/Scripts/Entity/EntityData/Weapon/WeaponLanceData.cs b/Assets/GameMain/Scripts/Entity/EntityData/Weapon/WeaponLanceData.cs index c3c3cce..b3183c1 100644 --- a/Assets/GameMain/Scripts/Entity/EntityData/Weapon/WeaponLanceData.cs +++ b/Assets/GameMain/Scripts/Entity/EntityData/Weapon/WeaponLanceData.cs @@ -7,20 +7,35 @@ namespace Entity.EntityData public sealed class WeaponLanceParamsData { /// - /// 枪尖命中半径。 + /// 横向半宽,表示前戳矩形判定的一半宽度。 + /// + public float HitHalfWidth { get; set; } + + /// + /// 旧字段兼容,未配置 HitHalfWidth 时回退使用。 /// public float HitRadius { get; set; } /// - /// 武器模型前刺的位移距离。 + /// 前戳判定盒体的总高度。 /// - public float ThrustDistance { get; set; } + public float HitHeight { get; set; } /// - /// 实际判定的前刺长度。 + /// 判定盒体中心相对战斗平面的高度偏移。 + /// + public float HitCenterYOffset { get; set; } + + /// + /// 前刺距离,同时驱动武器位移和命中长度。 /// public float PierceLength { get; set; } + /// + /// 旧字段兼容,未配置 PierceLength 时回退使用。 + /// + public float ThrustDistance { get; set; } + /// /// 判定起点相对武器当前位置的前置偏移。 /// diff --git a/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/AttackEffects/LanceThrustAttackEffect.cs b/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/AttackEffects/LanceThrustAttackEffect.cs new file mode 100644 index 0000000..b358215 --- /dev/null +++ b/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/AttackEffects/LanceThrustAttackEffect.cs @@ -0,0 +1,82 @@ +using UnityEngine; + +namespace Entity.Weapon +{ + public sealed class LanceThrustAttackEffect : IWeaponAttackEffect + { + private readonly float _duration = 0.18f; + private readonly float _yOffset = 0.06f; + private readonly float _lineWidth = 0.05f; + private readonly Color _color = new(0.2f, 0.95f, 0.7f, 0.92f); + + public LanceThrustAttackEffect() + { + } + + public LanceThrustAttackEffect(float duration, float yOffset, float lineWidth, Color color) + { + _duration = duration; + _yOffset = yOffset; + _lineWidth = lineWidth; + _color = color; + } + + public void Play(WeaponBase weapon, Vector3 position, EntityBase target, float radius) + { + if (weapon is not WeaponLance lance) return; + + Vector3 forward = lance.StrikeDirection; + forward.y = 0f; + if (forward.sqrMagnitude <= Mathf.Epsilon) + { + forward = Vector3.forward; + } + + forward.Normalize(); + Vector3 right = Vector3.Cross(Vector3.up, forward); + float halfWidth = Mathf.Max(0.1f, lance.HitHalfWidth); + float halfLength = Mathf.Max(0.1f, lance.PierceLength * 0.5f); + Vector3 center = position + Vector3.up * _yOffset; + + Vector3 frontCenter = center + forward * halfLength; + Vector3 backCenter = center - forward * halfLength; + + Vector3[] corners = + { + frontCenter - right * halfWidth, + frontCenter + right * halfWidth, + backCenter + right * halfWidth, + backCenter - right * halfWidth, + }; + + GameObject indicator = new GameObject("LanceThrustIndicator"); + indicator.transform.position = center; + + LineRenderer line = indicator.AddComponent(); + line.loop = true; + line.useWorldSpace = true; + line.positionCount = corners.Length; + line.startWidth = _lineWidth; + line.endWidth = _lineWidth; + line.startColor = _color; + line.endColor = _color; + + Shader shader = Shader.Find("Sprites/Default"); + if (shader == null) + { + shader = Shader.Find("Unlit/Color"); + } + + Material material = new Material(shader); + material.color = _color; + line.material = material; + + for (int i = 0; i < corners.Length; i++) + { + line.SetPosition(i, corners[i]); + } + + Object.Destroy(indicator, Mathf.Max(0.01f, _duration)); + } + } +} diff --git a/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/AttackEffects/LanceThrustAttackEffect.cs.meta b/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/AttackEffects/LanceThrustAttackEffect.cs.meta new file mode 100644 index 0000000..ebbae42 --- /dev/null +++ b/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/AttackEffects/LanceThrustAttackEffect.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8c77c590c7233e544a9295ed41ff8827 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/WeaponBase.cs b/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/WeaponBase.cs index 3a4fd7c..0f67bd6 100644 --- a/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/WeaponBase.cs +++ b/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/WeaponBase.cs @@ -246,6 +246,20 @@ namespace Entity.Weapon halfAngleDeg, maxTargets); } + protected bool TryQueueRectangleCollisionQuery(in Vector3 center, float halfWidth, float halfLength, + in Vector3 direction, int maxTargets = 16) + { + var simulationWorld = GameEntry.SimulationWorld; + if (simulationWorld == null) + { + return false; + } + + int ownerEntityId = WeaponData != null ? WeaponData.OwnerId : Id; + return simulationWorld.TryRequestRectangleCollision(Id, ownerEntityId, in center, halfWidth, halfLength, + in direction, maxTargets); + } + protected void SetTargetSelector(TargetSelectorType selectorType) { TargetSelector = CreateSelector(selectorType); @@ -304,4 +318,4 @@ namespace Entity.Weapon public abstract void OnLeave(); public override string ToString() => State.ToString(); } -} \ No newline at end of file +} diff --git a/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/WeaponLance/WeaponLance.cs b/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/WeaponLance/WeaponLance.cs index 2a94a49..ddf076f 100644 --- a/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/WeaponLance/WeaponLance.cs +++ b/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/WeaponLance/WeaponLance.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Components; using CustomUtility; using Definition.DataStruct; using Definition.Enum; @@ -33,14 +34,20 @@ namespace Entity.Weapon private Collider[] _hitResults; private readonly HashSet _hitEntityIds = new(); - // 枪尖判定半径。 - private float _hitRadius; + // 前戳矩形判定的横向半宽。 + private float _hitHalfWidth; + // 前戳矩形判定盒体的总高度。 + private float _hitHeight; + // 盒体中心相对战斗平面的高度偏移。 + private float _hitCenterYOffset; // 从判定起点向前延伸的有效刺击长度。 private float _pierceLength; // 判定起点相对武器当前位置的前移量。 private float _forwardOffset; - // 武器模型本身向前突刺的位移长度。 - private float _thrustDistance; + // 本次攻击锁定的前戳方向,避免受位移动画中的武器位置影响。 + private Vector3 _strikeDirection = Vector3.forward; + // 本次攻击锁定的矩形判定中心。 + private Vector3 _strikeCenter; public override ImpactData GetImpactData() { @@ -59,18 +66,18 @@ namespace Entity.Weapon { StopAttackTween(false); FaceTargetImmediately(); + CacheStrikeSnapshot(); _isAttacking = true; _attackParent = CachedTransform.parent; CachedTransform.SetParent(null); - Vector3 targetPos = CachedTransform.position + CachedTransform.forward * _thrustDistance; + Vector3 targetPos = CachedTransform.position + _strikeDirection * _pierceLength; _attackSequence = DOTween.Sequence(); _attackSequence.Append(CachedTransform.DOMove(targetPos, _attackDuration).SetEase(Ease.OutQuad)); _attackSequence.AppendCallback(() => { - Vector3 strikeCenter = GetStrikeCenter(); - _attackEffect?.Play(this, strikeCenter, _target, _hitRadius); + _attackEffect?.Play(this, _strikeCenter, _target, _hitHalfWidth); ApplyPierceDamage(); if (_attackParent != null) @@ -122,7 +129,17 @@ namespace Entity.Weapon CachedTransform.rotation = Quaternion.LookRotation(directionToTarget.normalized, Vector3.up); } - private Vector3 GetStrikeStart() + private void CacheStrikeSnapshot() + { + _strikeDirection = ResolvePlanarForward(); + Vector3 strikeStart = CachedTransform.position; + strikeStart.y = ResolveStrikePlaneY(); + strikeStart += _strikeDirection * _forwardOffset; + _strikeCenter = strikeStart + _strikeDirection * (_pierceLength * 0.5f); + _strikeCenter.y += _hitCenterYOffset; + } + + private Vector3 ResolvePlanarForward() { Vector3 forward = CachedTransform.forward; forward.y = 0f; @@ -132,59 +149,35 @@ namespace Entity.Weapon } forward.Normalize(); - return CachedTransform.position + forward * _forwardOffset; - } - - private Vector3 GetStrikeEnd() - { - Vector3 start = GetStrikeStart(); - Vector3 forward = CachedTransform.forward; - forward.y = 0f; - if (forward.sqrMagnitude <= Mathf.Epsilon) - { - forward = Vector3.forward; - } - - forward.Normalize(); - return start + forward * _pierceLength; - } - - private Vector3 GetStrikeCenter() - { - return Vector3.Lerp(GetStrikeStart(), GetStrikeEnd(), 0.5f); + return forward; } private void ApplyPierceDamage() { - if (_hitRadius <= 0f || _pierceLength <= 0f) return; + if (_hitHalfWidth <= 0f || _pierceLength <= 0f) return; - Vector3 strikeStart = GetStrikeStart(); - Vector3 strikeEnd = GetStrikeEnd(); - Vector3 strikeDirection = strikeEnd - strikeStart; - strikeDirection.y = 0f; + Vector3 strikeDirection = _strikeDirection; if (strikeDirection.sqrMagnitude <= Mathf.Epsilon) return; - strikeDirection.Normalize(); - float halfAngle = Mathf.Rad2Deg * Mathf.Atan2(_hitRadius, Mathf.Max(0.01f, _pierceLength)); - if (TryQueueSectorCollisionQuery(strikeStart, _pierceLength, in strikeDirection, halfAngle, - Mathf.Max(1, _maxHitColliders))) + Vector3 broadPhaseCenter = _strikeCenter; + Quaternion broadPhaseRotation = Quaternion.LookRotation(strikeDirection, Vector3.up); + + if (TryQueueRectangleCollisionQuery(broadPhaseCenter, _hitHalfWidth, _pierceLength * 0.5f, + strikeDirection, Mathf.Max(1, _maxHitColliders))) { _hitEntityIds.Clear(); return; } int capacity = Mathf.Max(1, _maxHitColliders); - float broadPhaseRadius = _pierceLength * 0.5f + _hitRadius; - Vector3 broadPhaseCenter = Vector3.Lerp(strikeStart, strikeEnd, 0.5f); - if (_hitResults == null || _hitResults.Length != capacity) { _hitResults = new Collider[capacity]; } - int hitCount = Physics.OverlapSphereNonAlloc(broadPhaseCenter, broadPhaseRadius, _hitResults, _hitMask, - QueryTriggerInteraction.Collide); - + Vector3 halfExtents = new(_hitHalfWidth, _hitHeight * 0.5f, _pierceLength * 0.5f); + int hitCount = Physics.OverlapBoxNonAlloc(broadPhaseCenter, halfExtents, _hitResults, broadPhaseRotation, + _hitMask, QueryTriggerInteraction.Collide); _hitEntityIds.Clear(); for (int i = 0; i < hitCount; i++) { @@ -195,24 +188,55 @@ namespace Entity.Weapon if (targetable == null || !targetable.Available || targetable.IsDead) continue; if (!_hitEntityIds.Add(targetable.Id)) continue; - if (!IsTargetInsidePierce(targetable, strikeStart, strikeDirection)) continue; + if (!IsTargetInsidePierce(targetable, broadPhaseCenter, strikeDirection)) continue; AIUtility.PerformCollision(targetable, this); } } - private bool IsTargetInsidePierce(TargetableObject targetable, Vector3 strikeStart, Vector3 strikeDirection) + private bool IsTargetInsidePierce(TargetableObject targetable, Vector3 strikeCenter, Vector3 strikeDirection) { - Vector3 toTarget = targetable.CachedTransform.position - strikeStart; - toTarget.y = 0f; - - float projection = Vector3.Dot(toTarget, strikeDirection); - if (projection < 0f || projection > _pierceLength) return false; - - Vector3 closestPoint = strikeStart + strikeDirection * projection; - Vector3 delta = targetable.CachedTransform.position - closestPoint; + Vector3 delta = targetable.CachedTransform.position - strikeCenter; delta.y = 0f; - return delta.sqrMagnitude <= _hitRadius * _hitRadius; + + Vector3 right = Vector3.Cross(Vector3.up, strikeDirection); + float forwardDistance = Vector3.Dot(delta, strikeDirection); + float lateralDistance = Vector3.Dot(delta, right); + float targetRadius = ResolveTargetCollisionRadius(targetable); + + return Mathf.Abs(forwardDistance) <= _pierceLength * 0.5f + targetRadius && + Mathf.Abs(lateralDistance) <= _hitHalfWidth + targetRadius; + } + + private static float ResolveTargetCollisionRadius(TargetableObject targetable) + { + if (targetable == null) + { + return 0f; + } + + MovementComponent movementComponent = targetable.GetComponent(); + return movementComponent != null ? Mathf.Max(0f, movementComponent.EnemyBodyRadius) : 0f; + } + + private float ResolveStrikePlaneY() + { + if (_target != null && _target.Available) + { + return _target.CachedTransform.position.y; + } + + if (_attackParent != null) + { + return _attackParent.position.y; + } + + if (CachedTransform.parent != null) + { + return CachedTransform.parent.position.y; + } + + return CachedTransform.position.y; } protected override bool OnWeaponShow(object userData) @@ -226,15 +250,21 @@ namespace Entity.Weapon _cachedRotation = CachedTransform.rotation; WeaponLanceParamsData paramsData = _weaponData.ParamsData; - _hitRadius = paramsData != null && paramsData.HitRadius > 0f - ? Mathf.Max(0.1f, paramsData.HitRadius) - : 0.45f; - _thrustDistance = paramsData != null && paramsData.ThrustDistance > 0f - ? paramsData.ThrustDistance - : _weaponData.AttackRange; + float configuredHalfWidth = paramsData != null && paramsData.HitHalfWidth > 0f + ? paramsData.HitHalfWidth + : paramsData != null && paramsData.HitRadius > 0f + ? paramsData.HitRadius + : 0.45f; + _hitHalfWidth = Mathf.Max(0.1f, configuredHalfWidth); + _hitHeight = paramsData != null && paramsData.HitHeight > 0f + ? Mathf.Max(0.2f, paramsData.HitHeight) + : 4f; + _hitCenterYOffset = paramsData != null ? paramsData.HitCenterYOffset : 0f; _pierceLength = paramsData != null && paramsData.PierceLength > 0f ? paramsData.PierceLength - : Mathf.Max(_weaponData.AttackRange, _thrustDistance); + : paramsData != null && paramsData.ThrustDistance > 0f + ? paramsData.ThrustDistance + : _weaponData.AttackRange; _forwardOffset = paramsData != null && paramsData.ForwardOffset > 0f ? paramsData.ForwardOffset : 0f; @@ -260,7 +290,7 @@ namespace Entity.Weapon _hitResults = new Collider[colliderCapacity]; } - _attackEffect = new KnifeRangeAttackEffect(); + _attackEffect = new LanceThrustAttackEffect(); if (_weaponData.OwnerCamp == CampType.Player) { @@ -321,5 +351,9 @@ namespace Entity.Weapon _attackParent = null; _hitEntityIds.Clear(); } + + public float HitHalfWidth => _hitHalfWidth; + public float PierceLength => _pierceLength; + public Vector3 StrikeDirection => _strikeDirection; } } diff --git a/Assets/GameMain/Scripts/Simulation/DataChannel/SimulationWorld.CollisionTransient.cs b/Assets/GameMain/Scripts/Simulation/DataChannel/SimulationWorld.CollisionTransient.cs index ec2f324..a18242b 100644 --- a/Assets/GameMain/Scripts/Simulation/DataChannel/SimulationWorld.CollisionTransient.cs +++ b/Assets/GameMain/Scripts/Simulation/DataChannel/SimulationWorld.CollisionTransient.cs @@ -65,13 +65,15 @@ namespace Simulation MaxTargets = math.max(1, maxTargets), ShapeType = CollisionShapeCircle, Direction = new float3(0f, 0f, 1f), - HalfAngleDeg = 180f + HalfAngleDeg = 180f, + HalfWidth = 0f, + HalfLength = 0f }); } private void AddAreaCollisionQuery(int queryId, int sourceEntityId, int sourceOwnerEntityId, bool sourceWasActiveAtQueryTime, in Vector3 center, float radius, int maxTargets, int shapeType, - in Vector3 direction, float halfAngleDeg) + in Vector3 direction, float halfAngleDeg, float halfWidth, float halfLength) { if (!_collisionQueryInputs.IsCreated || radius <= 0f) { @@ -101,7 +103,9 @@ namespace Simulation MaxTargets = math.max(1, maxTargets), ShapeType = shapeType, Direction = new float3(normalizedDirection.x, normalizedDirection.y, normalizedDirection.z), - HalfAngleDeg = Mathf.Clamp(halfAngleDeg, 0f, 180f) + HalfAngleDeg = Mathf.Clamp(halfAngleDeg, 0f, 180f), + HalfWidth = Mathf.Max(0f, halfWidth), + HalfLength = Mathf.Max(0f, halfLength) }); } diff --git a/Assets/GameMain/Scripts/Simulation/DataChannel/SimulationWorld.JobDataChannel.cs b/Assets/GameMain/Scripts/Simulation/DataChannel/SimulationWorld.JobDataChannel.cs index a4211b5..5dd7ac6 100644 --- a/Assets/GameMain/Scripts/Simulation/DataChannel/SimulationWorld.JobDataChannel.cs +++ b/Assets/GameMain/Scripts/Simulation/DataChannel/SimulationWorld.JobDataChannel.cs @@ -10,6 +10,7 @@ namespace Simulation private const int CollisionSourceTypeArea = 2; private const int CollisionShapeCircle = 0; private const int CollisionShapeSector = 1; + private const int CollisionShapeRectangle = 2; // Kept as top-level fields because current regression tests reflect them directly. private NativeList _collisionQueryInputs; diff --git a/Assets/GameMain/Scripts/Simulation/JobStruct/AreaCollisionRequestData.cs b/Assets/GameMain/Scripts/Simulation/JobStruct/AreaCollisionRequestData.cs index 27df2b4..0e51176 100644 --- a/Assets/GameMain/Scripts/Simulation/JobStruct/AreaCollisionRequestData.cs +++ b/Assets/GameMain/Scripts/Simulation/JobStruct/AreaCollisionRequestData.cs @@ -15,6 +15,8 @@ namespace Simulation public int ShapeType; public Vector3 Direction; public float HalfAngleDeg; + public float HalfWidth; + public float HalfLength; } } -} \ No newline at end of file +} diff --git a/Assets/GameMain/Scripts/Simulation/JobStruct/CollisionQueryData.cs b/Assets/GameMain/Scripts/Simulation/JobStruct/CollisionQueryData.cs index cdf7c23..75d6cd9 100644 --- a/Assets/GameMain/Scripts/Simulation/JobStruct/CollisionQueryData.cs +++ b/Assets/GameMain/Scripts/Simulation/JobStruct/CollisionQueryData.cs @@ -17,6 +17,8 @@ namespace Simulation public int ShapeType; public float3 Direction; public float HalfAngleDeg; + public float HalfWidth; + public float HalfLength; } } -} \ No newline at end of file +} diff --git a/Assets/GameMain/Scripts/Simulation/JobStruct/QueryCollisionCandidatesBurstJob.cs b/Assets/GameMain/Scripts/Simulation/JobStruct/QueryCollisionCandidatesBurstJob.cs index 3765232..b654f41 100644 --- a/Assets/GameMain/Scripts/Simulation/JobStruct/QueryCollisionCandidatesBurstJob.cs +++ b/Assets/GameMain/Scripts/Simulation/JobStruct/QueryCollisionCandidatesBurstJob.cs @@ -23,7 +23,6 @@ namespace Simulation public void Execute(int index) { CollisionQueryData query = Queries[index]; - float radiusSqr = query.Radius * query.Radius; int centerCellX = (int)math.floor(query.Position.x / CellSize); int centerCellZ = (int)math.floor(query.Position.z / CellSize); int queryRange = math.max(1, (int)math.ceil(query.Radius / CellSize)); @@ -34,10 +33,14 @@ namespace Simulation query.SourceOwnerEntityId != PlayerTargetEntityId) { float3 playerPosition = PlayerPosition; - playerPosition.y = query.Position.y; + if (query.SourceType == CollisionSourceTypeArea) + { + playerPosition.y = query.Position.y; + } float3 playerDelta = playerPosition - query.Position; float playerSqrDistance = math.lengthsq(playerDelta); - if (playerSqrDistance <= radiusSqr) + float playerRadiusSqr = query.Radius * query.Radius; + if (playerSqrDistance <= playerRadiusSqr) { Candidates.AddNoResize(new CollisionCandidateData { @@ -86,12 +89,19 @@ namespace Simulation continue; } + float deltaY = query.SourceType == CollisionSourceTypeArea + ? 0f + : enemy.Position.y - query.Position.y; float3 delta = new float3( enemy.Position.x - query.Position.x, - enemy.Position.y - query.Position.y, + deltaY, enemy.Position.z - query.Position.z); float sqrDistance = math.lengthsq(delta); - if (sqrDistance > radiusSqr) + float targetRadius = query.SourceType == CollisionSourceTypeArea + ? math.max(0f, enemy.EnemyBodyRadius) + : 0f; + float effectiveRadius = query.Radius + targetRadius; + if (sqrDistance > effectiveRadius * effectiveRadius) { continue; } @@ -119,4 +129,4 @@ namespace Simulation } } } -} \ No newline at end of file +} diff --git a/Assets/GameMain/Scripts/Simulation/Jobs/SimulationWorld.CollisionBroadPhase.cs b/Assets/GameMain/Scripts/Simulation/Jobs/SimulationWorld.CollisionBroadPhase.cs index d7a3c90..9803c85 100644 --- a/Assets/GameMain/Scripts/Simulation/Jobs/SimulationWorld.CollisionBroadPhase.cs +++ b/Assets/GameMain/Scripts/Simulation/Jobs/SimulationWorld.CollisionBroadPhase.cs @@ -58,7 +58,8 @@ namespace Simulation AreaCollisionRequestData request = _areaCollisionRequests[i]; AddAreaCollisionQuery(queryId, request.SourceEntityId, request.SourceOwnerEntityId, request.SourceWasActiveAtQueryTime, in request.Center, request.Radius, request.MaxTargets, - request.ShapeType, in request.Direction, request.HalfAngleDeg); + request.ShapeType, in request.Direction, request.HalfAngleDeg, request.HalfWidth, + request.HalfLength); queryId++; builtAreaQueryCount++; if (request.Radius > maxQueryRadius) diff --git a/Assets/GameMain/Scripts/Simulation/Jobs/SimulationWorld.CollisionRequests.cs b/Assets/GameMain/Scripts/Simulation/Jobs/SimulationWorld.CollisionRequests.cs index 84e8ebb..4b790c6 100644 --- a/Assets/GameMain/Scripts/Simulation/Jobs/SimulationWorld.CollisionRequests.cs +++ b/Assets/GameMain/Scripts/Simulation/Jobs/SimulationWorld.CollisionRequests.cs @@ -12,18 +12,29 @@ namespace Simulation float radius, int maxTargets = 16) { return TryRequestAreaCollisionInternal(sourceEntityId, sourceOwnerEntityId, in center, radius, - maxTargets, CollisionShapeCircle, Vector3.forward, 180f); + maxTargets, CollisionShapeCircle, Vector3.forward, 180f, 0f, 0f); } public bool TryRequestSectorCollision(int sourceEntityId, int sourceOwnerEntityId, in Vector3 center, float radius, in Vector3 direction, float halfAngleDeg, int maxTargets = 16) { return TryRequestAreaCollisionInternal(sourceEntityId, sourceOwnerEntityId, in center, radius, - maxTargets, CollisionShapeSector, direction, halfAngleDeg); + maxTargets, CollisionShapeSector, direction, halfAngleDeg, 0f, 0f); + } + + public bool TryRequestRectangleCollision(int sourceEntityId, int sourceOwnerEntityId, in Vector3 center, + float halfWidth, float halfLength, in Vector3 direction, int maxTargets = 16) + { + float safeHalfWidth = Mathf.Max(0.01f, halfWidth); + float safeHalfLength = Mathf.Max(0.01f, halfLength); + float boundingRadius = Mathf.Sqrt(safeHalfWidth * safeHalfWidth + safeHalfLength * safeHalfLength); + return TryRequestAreaCollisionInternal(sourceEntityId, sourceOwnerEntityId, in center, boundingRadius, + maxTargets, CollisionShapeRectangle, direction, 0f, safeHalfWidth, safeHalfLength); } private bool TryRequestAreaCollisionInternal(int sourceEntityId, int sourceOwnerEntityId, - in Vector3 center, float radius, int maxTargets, int shapeType, in Vector3 direction, float halfAngleDeg) + in Vector3 center, float radius, int maxTargets, int shapeType, in Vector3 direction, float halfAngleDeg, + float halfWidth, float halfLength) { if (!_useSimulationMovement) { @@ -58,7 +69,9 @@ namespace Simulation MaxTargets = Mathf.Max(1, maxTargets), ShapeType = shapeType, Direction = normalizedDirection, - HalfAngleDeg = Mathf.Clamp(halfAngleDeg, 0f, 180f) + HalfAngleDeg = Mathf.Clamp(halfAngleDeg, 0f, 180f), + HalfWidth = Mathf.Max(0f, halfWidth), + HalfLength = Mathf.Max(0f, halfLength) }); return true; diff --git a/Assets/GameMain/Scripts/Simulation/Jobs/SimulationWorld.CollisionResolve.cs b/Assets/GameMain/Scripts/Simulation/Jobs/SimulationWorld.CollisionResolve.cs index 5caeb77..065d9e8 100644 --- a/Assets/GameMain/Scripts/Simulation/Jobs/SimulationWorld.CollisionResolve.cs +++ b/Assets/GameMain/Scripts/Simulation/Jobs/SimulationWorld.CollisionResolve.cs @@ -1,3 +1,4 @@ +using Components; using CustomDebugger; using CustomUtility; using Definition.DataStruct; @@ -188,7 +189,8 @@ namespace Simulation continue; } - if (!IsAreaTargetInsidePreciseShape(in query, target)) + float targetRadius = ResolveAreaTargetRadius(target); + if (!IsAreaTargetInsidePreciseShape(in query, target, targetRadius)) { continue; } @@ -230,7 +232,24 @@ namespace Simulation return false; } - private static bool IsAreaTargetInsidePreciseShape(in CollisionQueryData query, TargetableObject target) + private float ResolveAreaTargetRadius(TargetableObject target) + { + if (target == null) + { + return 0f; + } + + if (target is EnemyBase && TryGetEnemyData(target.Id, out EnemySimData enemyData)) + { + return Mathf.Max(0f, enemyData.EnemyBodyRadius); + } + + MovementComponent movementComponent = target.GetComponent(); + return movementComponent != null ? Mathf.Max(0f, movementComponent.EnemyBodyRadius) : 0f; + } + + private static bool IsAreaTargetInsidePreciseShape(in CollisionQueryData query, TargetableObject target, + float targetRadius) { if (target == null || target.CachedTransform == null) { @@ -241,7 +260,7 @@ namespace Simulation Vector3 toTarget = target.CachedTransform.position - center; toTarget.y = 0f; - float radius = Mathf.Max(0.01f, query.Radius); + float radius = Mathf.Max(0.01f, query.Radius + Mathf.Max(0f, targetRadius)); float radiusSqr = radius * radius; float sqrDistance = toTarget.sqrMagnitude; if (sqrDistance > radiusSqr) @@ -249,6 +268,27 @@ namespace Simulation return false; } + if (query.ShapeType == CollisionShapeRectangle) + { + Vector3 forwardRect = new Vector3(query.Direction.x, query.Direction.y, query.Direction.z); + forwardRect.y = 0f; + if (forwardRect.sqrMagnitude <= Mathf.Epsilon) + { + forwardRect = Vector3.forward; + } + else + { + forwardRect.Normalize(); + } + + Vector3 rightRect = Vector3.Cross(Vector3.up, forwardRect); + float halfWidth = Mathf.Max(0.01f, query.HalfWidth + Mathf.Max(0f, targetRadius)); + float halfLength = Mathf.Max(0.01f, query.HalfLength + Mathf.Max(0f, targetRadius)); + float forwardDistance = Vector3.Dot(toTarget, forwardRect); + float lateralDistance = Vector3.Dot(toTarget, rightRect); + return Mathf.Abs(forwardDistance) <= halfLength && Mathf.Abs(lateralDistance) <= halfWidth; + } + if (query.ShapeType != CollisionShapeSector) { return true; diff --git a/Assets/GameMain/Scripts/Utility/ItemDescUtility.cs b/Assets/GameMain/Scripts/Utility/ItemDescUtility.cs index 35f582d..3a416ea 100644 --- a/Assets/GameMain/Scripts/Utility/ItemDescUtility.cs +++ b/Assets/GameMain/Scripts/Utility/ItemDescUtility.cs @@ -13,10 +13,13 @@ namespace CustomUtility { private static readonly Dictionary _paramsDict = new(StringComparer.OrdinalIgnoreCase) { - {"hitRadius", "伤害范围"}, + {"hitHalfWidth", "横向半宽"}, + {"hitRadius", "攻击半宽"}, + {"hitHeight", "判定高度"}, + {"hitCenterYOffset", "判定高度偏移"}, {"sectorAngle", "攻击角度"}, {"pierceLength", "前戳距离"}, - {"thrustDistance", "枪尖长度"}, + {"thrustDistance", "前戳距离(旧)"}, {"forwardOffset", "前置偏移"}, {"rotateSpeed", "转向速度"}, {"attackDuration", "突刺时长"}, diff --git a/数据表/Entity/Weapon.xlsx b/数据表/Entity/Weapon.xlsx index e3c59558c149db9baba48560715e40257c31e693..28a267a4ce5be1a17df58dc176dab62edfe05980 100644 GIT binary patch delta 6036 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 Z`^8kjk!X_ps$h4t;e7#u3BiB({{f%2ERg^J