Merge branch 'master' of https://git.joywaygames.cn/basegames/zeling_v2
This commit is contained in:
@@ -12,14 +12,14 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: f7dd720bca19fcc49b22106fb65f7652, type: 3}
|
||||
m_Name: ENM_E001_AnimConfig
|
||||
m_EditorClassIdentifier:
|
||||
Idle: {fileID: 0}
|
||||
Walk: {fileID: 0}
|
||||
Run: {fileID: 0}
|
||||
Turn: {fileID: 0}
|
||||
Idle: {fileID: 7400000, guid: 74d1c2f7f8e5c66409e9090885e7e007, type: 2}
|
||||
Walk: {fileID: 7400000, guid: b6b9e34e957b9fa4b92e95aaa155099f, type: 2}
|
||||
Run: {fileID: 7400000, guid: b6b9e34e957b9fa4b92e95aaa155099f, type: 2}
|
||||
Turn: {fileID: 7400000, guid: c6d78c8270549254f8c777e0c5d4f9bf, type: 2}
|
||||
Attack: {fileID: 0}
|
||||
Hurt: {fileID: 0}
|
||||
Hurt: {fileID: 7400000, guid: 9d5bb5bb32cdb344b80f01d998ed653f, type: 2}
|
||||
Stagger: {fileID: 0}
|
||||
KnockUp: {fileID: 0}
|
||||
Dead: {fileID: 0}
|
||||
Dead: {fileID: 7400000, guid: 7f07e13e67a5aba4b8a27234e5a84ee6, type: 2}
|
||||
Alert: {fileID: 0}
|
||||
Investigate: {fileID: 0}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -420,43 +420,195 @@ namespace BaseGames.Editor
|
||||
/// 构建触发多边形路径(本地坐标,相对于新 CameraArea):
|
||||
/// 取 TriggerRegion 各点的世界坐标减去 areaWorldPos;
|
||||
/// 若点数不足 3 则兜底使用可视矩形。
|
||||
///
|
||||
/// 处理流程:
|
||||
/// 1. 优先尝试直角多边形重建(坐标分组配对算法)→ 精确还原横竖边构成的触发区域
|
||||
/// 2. 降级:最近邻贪心 + 2-opt 消除自相交 → 适用于含斜边的多边形
|
||||
/// 3. Shoelace 叉积校正绕向 → 确保 CCW(PolygonCollider2D 要求)
|
||||
/// </summary>
|
||||
private static Vector2[] BuildTriggerPath(ZoneEntry entry, Vector3 areaWorldPos, Rect fallback)
|
||||
{
|
||||
if (entry.TriggerWorldPts.Count >= 3)
|
||||
{
|
||||
// 将世界坐标转换为 areaGO 本地坐标
|
||||
var localPts = new Vector2[entry.TriggerWorldPts.Count];
|
||||
for (int i = 0; i < localPts.Length; i++)
|
||||
localPts[i] = entry.TriggerWorldPts[i] - (Vector2)areaWorldPos;
|
||||
|
||||
// 按照质心角度排序,确保顶点顺序能够围成合法多边形
|
||||
return SortPointsByAngle(localPts);
|
||||
// 优先:直角多边形精确重建
|
||||
var rectilinear = TryReconstructRectilinear(localPts);
|
||||
if (rectilinear != null)
|
||||
return EnsureCounterClockwise(rectilinear);
|
||||
|
||||
// 降级:最近邻 + 2-opt(适用于含斜边或点分布不规则的情况)
|
||||
Debug.LogWarning($"[迁移工具] {entry.ZoneObj.name} 无法识别为直角多边形,已使用 2-opt 降级处理,请手动检查形状。");
|
||||
var ordered = NearestNeighborOrder(localPts);
|
||||
TwoOptFix(ordered);
|
||||
return EnsureCounterClockwise(ordered);
|
||||
}
|
||||
return RectToPolygon(fallback);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将一组点按照围绕质心的角度(逆时针)排序,使其能够围成合法的简单多边形。
|
||||
/// 适用于凸多边形及质心在多边形内部的凹多边形。
|
||||
/// 直角多边形(Rectilinear Polygon)顶点排序。
|
||||
///
|
||||
/// 算法原理:
|
||||
/// 简单直角多边形中,每列(同 x)的顶点按 y 升序两两配对为纵向边,
|
||||
/// 每行(同 y)的顶点按 x 升序两两配对为横向边(Scanline 奇偶规则)。
|
||||
/// 每个顶点恰好有 1 条纵向边 + 1 条横向边,即恰好 2 个邻居,可遍历为完整环路。
|
||||
///
|
||||
/// 返回 null 表示点集不满足直角多边形条件(奇数分组、邻居数 ≠ 2 等),调用方应降级处理。
|
||||
/// </summary>
|
||||
private static Vector2[] SortPointsByAngle(Vector2[] points)
|
||||
private static Vector2[] TryReconstructRectilinear(Vector2[] points)
|
||||
{
|
||||
// 计算质心
|
||||
Vector2 centroid = Vector2.zero;
|
||||
foreach (var p in points)
|
||||
centroid += p;
|
||||
centroid /= points.Length;
|
||||
// 同轴容差:小于此值的坐标差视为同行/同列(处理浮点误差)
|
||||
const float kTol = 0.1f;
|
||||
int n = points.Length;
|
||||
|
||||
// 按照相对质心的极角升序排列(逆时针)
|
||||
var sorted = new System.Collections.Generic.List<Vector2>(points);
|
||||
sorted.Sort((a, b) =>
|
||||
var adj = new List<int>[n];
|
||||
for (int i = 0; i < n; i++) adj[i] = new List<int>();
|
||||
|
||||
// 按 x 分组 → 纵向边;按 y 分组 → 横向边
|
||||
if (!AddAxisEdges(points, adj, groupByX: true, tol: kTol)) return null;
|
||||
if (!AddAxisEdges(points, adj, groupByX: false, tol: kTol)) return null;
|
||||
|
||||
// 每个顶点必须恰好有 2 个邻居(1 横 + 1 纵)才构成合法简单多边形
|
||||
for (int i = 0; i < n; i++)
|
||||
if (adj[i].Count != 2) return null;
|
||||
|
||||
// 从顶点 0 出发遍历环路
|
||||
var result = new Vector2[n];
|
||||
var visited = new bool[n];
|
||||
result[0] = points[0];
|
||||
visited[0] = true;
|
||||
int prev = -1, curr = 0;
|
||||
for (int step = 1; step < n; step++)
|
||||
{
|
||||
float angleA = Mathf.Atan2(a.y - centroid.y, a.x - centroid.x);
|
||||
float angleB = Mathf.Atan2(b.y - centroid.y, b.x - centroid.x);
|
||||
return angleA.CompareTo(angleB);
|
||||
});
|
||||
return sorted.ToArray();
|
||||
int next = -1;
|
||||
foreach (int nb in adj[curr])
|
||||
if (nb != prev && !visited[nb]) { next = nb; break; }
|
||||
if (next == -1) return null; // 环路断裂
|
||||
result[step] = points[next];
|
||||
visited[next] = true;
|
||||
prev = curr;
|
||||
curr = next;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按指定轴(x 或 y)将顶点分组,每组内按垂直轴坐标排序后两两配对添加边。
|
||||
/// 若任意组的顶点数为奇数(无法完整配对)则返回 false。
|
||||
/// </summary>
|
||||
private static bool AddAxisEdges(Vector2[] pts, List<int>[] adj, bool groupByX, float tol)
|
||||
{
|
||||
var groups = new Dictionary<int, List<int>>();
|
||||
for (int i = 0; i < pts.Length; i++)
|
||||
{
|
||||
// 将坐标映射到整数 key,容差范围内的值归入同组
|
||||
int key = Mathf.RoundToInt((groupByX ? pts[i].x : pts[i].y) / tol);
|
||||
if (!groups.TryGetValue(key, out var list))
|
||||
groups[key] = list = new List<int>();
|
||||
list.Add(i);
|
||||
}
|
||||
foreach (var group in groups.Values)
|
||||
{
|
||||
if (group.Count % 2 != 0) return false; // 奇数个顶点无法配对
|
||||
// 按垂直轴升序排列后两两配对为边
|
||||
group.Sort((a, b) =>
|
||||
(groupByX ? pts[a].y : pts[a].x).CompareTo(groupByX ? pts[b].y : pts[b].x));
|
||||
for (int k = 0; k + 1 < group.Count; k += 2)
|
||||
{
|
||||
adj[group[k]].Add(group[k + 1]);
|
||||
adj[group[k + 1]].Add(group[k]);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 最近邻贪心排序(降级路径):从最左侧点出发,每步选取欧氏距离最近的未访问点。
|
||||
/// </summary>
|
||||
private static Vector2[] NearestNeighborOrder(Vector2[] points)
|
||||
{
|
||||
int n = points.Length;
|
||||
var visited = new bool[n];
|
||||
var result = new Vector2[n];
|
||||
|
||||
int startIdx = 0;
|
||||
for (int i = 1; i < n; i++)
|
||||
if (points[i].x < points[startIdx].x) startIdx = i;
|
||||
|
||||
result[0] = points[startIdx];
|
||||
visited[startIdx] = true;
|
||||
for (int step = 1; step < n; step++)
|
||||
{
|
||||
float minSqDist = float.MaxValue;
|
||||
int nearest = -1;
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
if (visited[i]) continue;
|
||||
float sq = (points[i] - result[step - 1]).sqrMagnitude;
|
||||
if (sq < minSqDist) { minSqDist = sq; nearest = i; }
|
||||
}
|
||||
result[step] = points[nearest];
|
||||
visited[nearest] = true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 2-opt 优化(降级路径):检测并翻转所有自相交边对,直到无交叉为止。
|
||||
/// </summary>
|
||||
private static void TwoOptFix(Vector2[] pts)
|
||||
{
|
||||
int n = pts.Length;
|
||||
bool improved = true;
|
||||
while (improved)
|
||||
{
|
||||
improved = false;
|
||||
for (int i = 0; i < n - 1; i++)
|
||||
for (int j = i + 2; j < n; j++)
|
||||
{
|
||||
if (i == 0 && j == n - 1) continue;
|
||||
if (SegmentsIntersect(pts[i], pts[(i + 1) % n], pts[j], pts[(j + 1) % n]))
|
||||
{
|
||||
System.Array.Reverse(pts, i + 1, j - i);
|
||||
improved = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>用叉积判断线段 AB 与 CD 是否严格相交(不含端点接触)。</summary>
|
||||
private static bool SegmentsIntersect(Vector2 a, Vector2 b, Vector2 c, Vector2 d)
|
||||
{
|
||||
float d1 = Cross2D(d - c, a - c), d2 = Cross2D(d - c, b - c);
|
||||
float d3 = Cross2D(b - a, c - a), d4 = Cross2D(b - a, d - a);
|
||||
return ((d1 > 0f && d2 < 0f) || (d1 < 0f && d2 > 0f)) &&
|
||||
((d3 > 0f && d4 < 0f) || (d3 < 0f && d4 > 0f));
|
||||
}
|
||||
|
||||
private static float Cross2D(Vector2 u, Vector2 v) => u.x * v.y - u.y * v.x;
|
||||
|
||||
/// <summary>
|
||||
/// 使用 Shoelace 公式(叉积累加)验证绕向:
|
||||
/// 2A = Σ (xᵢ · yᵢ₊₁ − xᵢ₊₁ · yᵢ)
|
||||
/// Unity Y 轴朝上:A > 0 = CCW,A < 0 = CW。
|
||||
/// 若为顺时针则翻转(不改变形状,只调整绕向)。
|
||||
/// </summary>
|
||||
private static Vector2[] EnsureCounterClockwise(Vector2[] points)
|
||||
{
|
||||
float signedArea = 0f;
|
||||
int n = points.Length;
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
Vector2 curr = points[i];
|
||||
Vector2 next = points[(i + 1) % n];
|
||||
signedArea += curr.x * next.y - next.x * curr.y;
|
||||
}
|
||||
if (signedArea < 0f)
|
||||
System.Array.Reverse(points);
|
||||
return points;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -389,7 +389,7 @@ namespace BaseGames.Editor
|
||||
var report = new List<string>();
|
||||
int undoGroup = Undo.GetCurrentGroup();
|
||||
Undo.SetCurrentGroupName("Place E001 草蛭");
|
||||
|
||||
EnemyBase.SuppressValidationWarnings = true;
|
||||
GameObject go = new GameObject("ENM_CaoZhi");
|
||||
Undo.RegisterCreatedObjectUndo(go, "Place E001");
|
||||
go.transform.position = GetDropPosition();
|
||||
@@ -470,6 +470,7 @@ namespace BaseGames.Editor
|
||||
|
||||
Undo.CollapseUndoOperations(undoGroup);
|
||||
Selection.activeGameObject = go;
|
||||
EnemyBase.SuppressValidationWarnings = false;
|
||||
MarkDirtyAndLog("E001 草蛭", go, report);
|
||||
}
|
||||
|
||||
@@ -479,7 +480,7 @@ namespace BaseGames.Editor
|
||||
var report = new List<string>();
|
||||
int undoGroup = Undo.GetCurrentGroup();
|
||||
Undo.SetCurrentGroupName("Place E002 簧蛭");
|
||||
|
||||
EnemyBase.SuppressValidationWarnings = true;
|
||||
GameObject go = new GameObject("ENM_HuangZhi");
|
||||
Undo.RegisterCreatedObjectUndo(go, "Place E002");
|
||||
go.transform.position = GetDropPosition();
|
||||
@@ -548,6 +549,7 @@ namespace BaseGames.Editor
|
||||
|
||||
Undo.CollapseUndoOperations(undoGroup);
|
||||
Selection.activeGameObject = go;
|
||||
EnemyBase.SuppressValidationWarnings = false;
|
||||
MarkDirtyAndLog("E002 簧蛭", go, report);
|
||||
}
|
||||
|
||||
@@ -557,7 +559,7 @@ namespace BaseGames.Editor
|
||||
var report = new List<string>();
|
||||
int undoGroup = Undo.GetCurrentGroup();
|
||||
Undo.SetCurrentGroupName("Place E003 幼蛭");
|
||||
|
||||
EnemyBase.SuppressValidationWarnings = true;
|
||||
GameObject go = new GameObject("ENM_YouZhi");
|
||||
Undo.RegisterCreatedObjectUndo(go, "Place E003");
|
||||
go.transform.position = GetDropPosition();
|
||||
@@ -631,6 +633,7 @@ namespace BaseGames.Editor
|
||||
|
||||
Undo.CollapseUndoOperations(undoGroup);
|
||||
Selection.activeGameObject = go;
|
||||
EnemyBase.SuppressValidationWarnings = false;
|
||||
MarkDirtyAndLog("E003 幼蛭", go, report);
|
||||
}
|
||||
|
||||
@@ -640,7 +643,7 @@ namespace BaseGames.Editor
|
||||
var report = new List<string>();
|
||||
int undoGroup = Undo.GetCurrentGroup();
|
||||
Undo.SetCurrentGroupName("Place E004 蛭母");
|
||||
|
||||
EnemyBase.SuppressValidationWarnings = true;
|
||||
GameObject go = new GameObject("ENM_ZhiMu");
|
||||
Undo.RegisterCreatedObjectUndo(go, "Place E004");
|
||||
go.transform.position = GetDropPosition();
|
||||
@@ -757,6 +760,7 @@ namespace BaseGames.Editor
|
||||
|
||||
Undo.CollapseUndoOperations(undoGroup);
|
||||
Selection.activeGameObject = go;
|
||||
EnemyBase.SuppressValidationWarnings = false;
|
||||
MarkDirtyAndLog("E004 蛭母", go, report);
|
||||
}
|
||||
|
||||
@@ -766,7 +770,7 @@ namespace BaseGames.Editor
|
||||
var report = new List<string>();
|
||||
int undoGroup = Undo.GetCurrentGroup();
|
||||
Undo.SetCurrentGroupName("Place E005 肥蛭");
|
||||
|
||||
EnemyBase.SuppressValidationWarnings = true;
|
||||
GameObject go = new GameObject("ENM_FeiZhi");
|
||||
Undo.RegisterCreatedObjectUndo(go, "Place E005");
|
||||
go.transform.position = GetDropPosition();
|
||||
@@ -849,6 +853,7 @@ namespace BaseGames.Editor
|
||||
|
||||
Undo.CollapseUndoOperations(undoGroup);
|
||||
Selection.activeGameObject = go;
|
||||
EnemyBase.SuppressValidationWarnings = false;
|
||||
MarkDirtyAndLog("E005 肥蛭", go, report);
|
||||
}
|
||||
|
||||
@@ -858,7 +863,7 @@ namespace BaseGames.Editor
|
||||
var report = new List<string>();
|
||||
int undoGroup = Undo.GetCurrentGroup();
|
||||
Undo.SetCurrentGroupName("Place E006 讙");
|
||||
|
||||
EnemyBase.SuppressValidationWarnings = true;
|
||||
GameObject go = new GameObject("ENM_Huan");
|
||||
Undo.RegisterCreatedObjectUndo(go, "Place E006");
|
||||
go.transform.position = GetDropPosition();
|
||||
@@ -951,6 +956,7 @@ namespace BaseGames.Editor
|
||||
|
||||
Undo.CollapseUndoOperations(undoGroup);
|
||||
Selection.activeGameObject = go;
|
||||
EnemyBase.SuppressValidationWarnings = false;
|
||||
MarkDirtyAndLog("E006 讙", go, report);
|
||||
}
|
||||
|
||||
@@ -960,7 +966,7 @@ namespace BaseGames.Editor
|
||||
var report = new List<string>();
|
||||
int undoGroup = Undo.GetCurrentGroup();
|
||||
Undo.SetCurrentGroupName("Place Boss 嘲风");
|
||||
|
||||
EnemyBase.SuppressValidationWarnings = true;
|
||||
GameObject go = new GameObject("ENM_ChaoFeng");
|
||||
Undo.RegisterCreatedObjectUndo(go, "Place ChaoFeng");
|
||||
go.transform.position = GetDropPosition();
|
||||
@@ -1067,6 +1073,7 @@ namespace BaseGames.Editor
|
||||
|
||||
Undo.CollapseUndoOperations(undoGroup);
|
||||
Selection.activeGameObject = go;
|
||||
EnemyBase.SuppressValidationWarnings = false;
|
||||
MarkDirtyAndLog("Boss 嘲风 (ChaoFeng)", go, report);
|
||||
}
|
||||
|
||||
|
||||
@@ -710,8 +710,12 @@ namespace BaseGames.Enemies
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <summary>Set to true during batch editor placement to suppress mid-wiring OnValidate warnings.</summary>
|
||||
public static bool SuppressValidationWarnings { get; set; }
|
||||
|
||||
protected virtual void OnValidate()
|
||||
{
|
||||
if (SuppressValidationWarnings) return;
|
||||
if (_statsSO == null)
|
||||
Debug.LogWarning($"[EnemyBase] {gameObject.name} 缺少 EnemyStatsSO 配置(运行时会 NullRef)。", this);
|
||||
if (_stats == null)
|
||||
|
||||
@@ -49,9 +49,14 @@ namespace BaseGames.Player
|
||||
private bool _wasGrounded;
|
||||
// 跳跃/二段跳期间禁用斜坡吸附,防止把起跳判定成斜坡而立即下压
|
||||
private bool _slopeSnapDisabled;
|
||||
private readonly Collider2D[] _groundBuffer = new Collider2D[4];
|
||||
private int _groundHitCount;
|
||||
private readonly Collider2D[] _groundBuffer = new Collider2D[4];
|
||||
private int _groundHitCount;
|
||||
private readonly ContactPoint2D[] _slopeContactBuffer = new ContactPoint2D[8];
|
||||
private readonly ContactPoint2D[] _wallContactBuffer = new ContactPoint2D[8];
|
||||
// 跳跃上升阶段贴墙时保护 vy:物理摩擦会在碰墙瞬间降低垂直速度,
|
||||
// 通过 OnCollisionEnter/Stay2D 将 vy 恢复到碰撞前的值。
|
||||
private float _savedVy;
|
||||
private bool _preserveVyOnWallContact;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
// ── 运行时调试(Inspector 中可见)───────────────────────────────
|
||||
@@ -111,6 +116,10 @@ namespace BaseGames.Player
|
||||
_pendingHorizontalZero = false;
|
||||
}
|
||||
|
||||
// 保存本帧物理步开始前的垂直速度,用于 OnCollisionEnter/Stay2D 中恢复被墙壁
|
||||
// 摩擦力降低的 vy(状态机 -100 比本脚本 -200 晚执行,不会影响此处的读取时机)。
|
||||
_savedVy = _rb.velocity.y;
|
||||
|
||||
CheckGrounded();
|
||||
CheckWalls();
|
||||
|
||||
@@ -283,6 +292,47 @@ namespace BaseGames.Player
|
||||
/// <summary>消耗墙壁土狼时间,防止同一帧被多次触发。</summary>
|
||||
public void ConsumeWallCoyote() => _wallCoyoteTimer = 0f;
|
||||
|
||||
// ── 跳跃上升贴墙 vy 保护 ──────────────────────────────────────────────
|
||||
/// <summary>
|
||||
/// JumpState.OnStateEnter/Exit 调用,开启/关闭跳跃上升阶段的 vy 保护。
|
||||
/// 开启后,OnCollisionEnter/Stay2D 检测到水平墙壁接触且角色有朝墙速度时,
|
||||
/// 将 vy 恢复到本帧物理步前的值,消除物理摩擦对跳跃最高点的影响。
|
||||
/// </summary>
|
||||
public void SetPreserveVyOnWallContact(bool preserve)
|
||||
=> _preserveVyOnWallContact = preserve;
|
||||
|
||||
private void OnCollisionEnter2D(Collision2D collision)
|
||||
=> TryRestoreVyFromWallFriction(collision);
|
||||
|
||||
private void OnCollisionStay2D(Collision2D collision)
|
||||
=> TryRestoreVyFromWallFriction(collision);
|
||||
|
||||
/// <summary>
|
||||
/// 检测水平墙壁碰撞时是否因摩擦力降低了 vy,若是则恢复到碰撞前的值。
|
||||
/// 只在角色确实以朝向墙壁的水平速度(_inputVelocityX)发生碰撞时才恢复,
|
||||
/// 防止 ZeroHVel 正常工作(vx=0,无摩擦)的帧中错误地抵消重力。
|
||||
/// </summary>
|
||||
private void TryRestoreVyFromWallFriction(Collision2D collision)
|
||||
{
|
||||
if (!_preserveVyOnWallContact || _savedVy <= 0f) return;
|
||||
for (int i = 0; i < collision.contactCount; i++)
|
||||
{
|
||||
float nx = collision.GetContact(i).normal.x;
|
||||
if (Mathf.Abs(nx) > 0.5f)
|
||||
{
|
||||
// 法线朝右(nx > 0.5)= 左侧墙,角色朝左运动时产生摩擦(vx < 0)
|
||||
// 法线朝左(nx < -0.5)= 右侧墙,角色朝右运动时产生摩擦(vx > 0)
|
||||
bool hadVelocityIntoWall = (nx > 0.5f && _inputVelocityX < -0.1f)
|
||||
|| (nx < -0.5f && _inputVelocityX > 0.1f);
|
||||
if (hadVelocityIntoWall)
|
||||
{
|
||||
_rb.velocity = new Vector2(_rb.velocity.x, _savedVy);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── IPassengerReceiver ────────────────────────────────────────────────
|
||||
/// <summary>
|
||||
/// MovingPlatform.FixedUpdate(-300) 推送本帧平台期望位移。
|
||||
@@ -445,6 +495,20 @@ namespace BaseGames.Player
|
||||
|
||||
_isWallLeft = Physics2D.Raycast(pos, Vector2.left, len, _groundLayer);
|
||||
_isWallRight = Physics2D.Raycast(pos, Vector2.right, len, _groundLayer);
|
||||
|
||||
// 物理接触点兜底:补充射线未覆盖图层或长度不足时的漏检。
|
||||
// GetContacts 返回上一物理步的接触点,由本脚本(-200)读取时先于状态机(-100),
|
||||
// 可在状态机决定是否施加水平速度之前获得"当帧最新"的墙壁接触信息。
|
||||
if (!_isWallLeft || !_isWallRight)
|
||||
{
|
||||
int cnt = _rb.GetContacts(_wallContactBuffer);
|
||||
for (int i = 0; i < cnt; i++)
|
||||
{
|
||||
float nx = _wallContactBuffer[i].normal.x;
|
||||
if (nx > 0.5f) _isWallLeft = true; // 法线朝右 = 左侧有墙
|
||||
if (nx < -0.5f) _isWallRight = true; // 法线朝左 = 右侧有墙
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDrawGizmos()
|
||||
|
||||
@@ -41,15 +41,32 @@ namespace BaseGames.Player.States
|
||||
|
||||
_isDoubleJump = false; // 消耗标记
|
||||
Input.JumpCancelledEvent += OnJumpCancelled;
|
||||
// 开启上升阶段贴墙 vy 保护:防止物理摩擦降低跳跃最高点
|
||||
Move.SetPreserveVyOnWallContact(true);
|
||||
}
|
||||
|
||||
public override void OnStateUpdate()
|
||||
{
|
||||
// 上升结束时转为下落
|
||||
// 上升结束时转为下落。
|
||||
// 例外:按住朝墙方向且射线已检测到墙时,物理摩擦可能将 vy 瞬间压到 ≤ 0,
|
||||
// 此时不触发 FallState,让后续抓墙检测在 vy 稳定后接管状态转换,
|
||||
// 防止因一帧摩擦导致跳跃高度降低并错误进入 FallState。
|
||||
if (Move.Rb.velocity.y <= 0f)
|
||||
{
|
||||
_owner.TransitionTo(_owner.GetState<FallState>());
|
||||
return;
|
||||
bool pressingTowardDetectedWall = false;
|
||||
if (Mathf.Abs(Input.MoveInput.x) > 0.01f)
|
||||
{
|
||||
int inputDir = Input.MoveInput.x > 0 ? 1 : -1;
|
||||
bool cwRay = inputDir > 0 ? Move.IsWallRight : Move.IsWallLeft;
|
||||
var wd0 = Owner.WallDetector;
|
||||
pressingTowardDetectedWall = cwRay
|
||||
|| (wd0 != null && wd0.IsTouchingWall && wd0.WallDirection == inputDir);
|
||||
}
|
||||
if (!pressingTowardDetectedWall)
|
||||
{
|
||||
_owner.TransitionTo(_owner.GetState<FallState>());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 下冲刺(下 + 冲刺 → 向下冲刺,优先于普通冲刺)──────────────────────
|
||||
@@ -98,8 +115,10 @@ namespace BaseGames.Player.States
|
||||
}
|
||||
|
||||
// ── 抓墙:贴墙 + 朝向墙壁按键,或蹬墙跳后的自动抓墙──────────────
|
||||
// 仅在上升结束后(vy ≤ 0)才进入抓墙状态;上升阶段阻止转换以保留顶点重力缩减,
|
||||
// 避免贴墙按方向键导致跳跃最大高度降低。
|
||||
var wd = Owner.WallDetector;
|
||||
if (wd != null && wd.IsTouchingWall && !Move.IsGrounded)
|
||||
if (wd != null && wd.IsTouchingWall && !Move.IsGrounded && !Move.IsRising)
|
||||
{
|
||||
int wallDir = wd.WallDirection;
|
||||
bool pressingTowardWall = Mathf.Abs(Input.MoveInput.x) > 0.01f
|
||||
@@ -130,11 +149,19 @@ namespace BaseGames.Player.States
|
||||
{
|
||||
int inputDir = Input.MoveInput.x > 0 ? 1 : -1;
|
||||
var wd = Owner.WallDetector;
|
||||
// PlayerWallDetector(order 0)在状态机(order -100)之后执行,
|
||||
// 其 IsTouchingWall / HasPartialContact 对状态机而言是上一帧的结果。
|
||||
// PlayerMovement.CheckWalls()(order -200)在状态机之前执行,
|
||||
// IsWallLeft / IsWallRight 是当帧最新结果,可提前一帧停止施力,
|
||||
// 防止角色以 RunSpeed 压入墙面后物理引擎的摩擦力降低上升速度。
|
||||
bool currentFrameWall = inputDir > 0 ? Move.IsWallRight : Move.IsWallLeft;
|
||||
if (wd != null && wd.IsTouchingWall && wd.WallDirection == inputDir)
|
||||
{
|
||||
// 按下朝墙方向键 + 在墙上(且未著地)→ 进入抓墙状态
|
||||
// 仅在上升结束后(vy ≤ 0)才进入抓墙状态;上升阶段只停止水平施力,
|
||||
// 保留顶点重力缩减逻辑,防止贴墙按键导致跳跃最大高度降低。
|
||||
var wss = Owner.GetState<WallSlideState>();
|
||||
if (wss != null && !Move.IsGrounded)
|
||||
if (wss != null && !Move.IsGrounded && !Move.IsRising)
|
||||
{
|
||||
wss.PrepareEnter(inputDir);
|
||||
Owner.TransitionTo(wss);
|
||||
@@ -142,10 +169,10 @@ namespace BaseGames.Player.States
|
||||
}
|
||||
Move.ZeroHorizontalVelocity();
|
||||
}
|
||||
else if (wd != null && wd.HasPartialContact(inputDir))
|
||||
else if (currentFrameWall || (wd != null && wd.HasPartialContact(inputDir)))
|
||||
{
|
||||
// 仅部分射线命中(如检测点高于矮墙顶部),停止施加朝墙方向的水平速度,
|
||||
// 防止角色边角被卡在墙顶而无法继续下落。
|
||||
// 当帧单射线已检测到墙(PlayerMovement -200 先于状态机 -100 执行),
|
||||
// 或上一帧部分射线命中——停止施力,防止以 RunSpeed 压入墙面产生摩擦力。
|
||||
Move.ZeroHorizontalVelocity();
|
||||
}
|
||||
else
|
||||
@@ -160,6 +187,7 @@ namespace BaseGames.Player.States
|
||||
// 顶点悬停可能已降低重力,离开本状态时必须恢复;
|
||||
// 否则进入 FallState/DashState 等后续状态时重力仍低于默认值。
|
||||
Move.SetGravityScale(Cfg.DefaultGravityScale);
|
||||
Move.SetPreserveVyOnWallContact(false);
|
||||
Input.JumpCancelledEvent -= OnJumpCancelled;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user