2d物理引擎学习 - 解决堆叠不稳定问题
现象
原因的话,还是和之前接触不稳定一样的情况:
那之前不都解决过了吗?为啥现在又出现不稳定的情况了?
主要原因是接触点更多了,接触点间的相互影响太大,10次迭代修正根本不够。
那提高迭代次数看看?
100次的时候,倒的没那么快了,所以增加迭代次数是有效果的
增加到500次,倒的更慢了
虽然提高迭代次数有效,但是游戏中不可能这么做,因为会让游戏卡成ppt。
有没有其他的解决办法呢?
那肯定是有的,那怎么做呢?
1) 一帧内不允许这么多次迭代次数,那我改成分帧来迭代是不是就可以了
2) 接触点之间相互影响,主要是它们是独立的,没有关联性,就是自己在修正速度的时候从不去考虑其他接触点会不会有影响;
那是不是可以把碰撞点之间关联起来。
Box2d中类似的解决方法名字叫:连续冲量法 (Sequential Impulse) ,是基于约束的求解方式中的一种,
他使用“冲量累加”来解决接触点之间相互影响的问题,使用“热启动”来解决分帧迭代问题(一帧内无法迭代完成,那我保留这次的计算结果,下一帧继续从上次的结果继续迭代)。
基于约束的求解
1) 确定约束方程
比如,之前接触稳定性时,最终确定的约束方程:
2) 将约束方程化简成Ax = b的形式,x即为我们要求的冲量(用冲量修正速度)
其中
b) 的推导:
因为,写成矩阵的表达方式:
,可以得到一样的结果
因为ra×n得到的是一个大小值,但这个大小值是z轴的一个分量,所以再×ra,要按3d方式计算:
c) 方程的左侧改成矩阵的写法:
d) 方程的右侧改成矩阵写法:
e) 最终化简得出:(J * M-1 * JT) * Δp = -J * V,所以A对应J * M-1 * JT,x对应Δp,b对应-J * V
其中
3) 解方程
解方程的方式有很多,但这边我们要找这样一种解方程方式:1) 可以迭代去解,2) 一次性解不出来还能保留计算结果下次继续解。
Projected Jacobi 与 Projected Gauss Seidel解方程法,都可以满足上面的要求,这边的话选用Projected Gauss Seidel法。
以上是从我的理解介绍了下大概的原理,如果对理论感兴趣的参考这边:
2D 游戏物理引擎 - 求解器 - 知乎 (zhihu.com)
2D 游戏物理引擎 - 碰撞约束 - 知乎 (zhihu.com)
效果
代码
public class MyRigidbody : MonoBehaviour { private int m_Id; [SerializeField] private Vector2 m_Size; //刚体形状现在固定为Box [SerializeField] private float m_Friction = 0.2f; //表面摩擦系数 //---------- 线性运动 [SerializeField] private float m_Mass; //质量 private float m_InvMass; private Vector2 m_Force; //持续作用的力 private Vector2 m_ForceImpulse; //脉冲力 [SerializeField] private Vector2 m_Velocity; //当前移动速度 [SerializeField] private Vector2 m_Position; //当前位置 //---------- //----------角运动 private float m_Inertia; //转动惯量 private float m_InvInertia; private float m_Torque; //持续作用的力矩 private float m_TorqueImpulse; //冲量矩 [SerializeField] private float m_AngleVelocity; //角速度(单位角度) [SerializeField] private float m_Rotation; //旋转角度 //---------- void Start() { if (m_Mass <= 0) { Mass = float.PositiveInfinity; Inertia = float.PositiveInfinity; } else { Mass = m_Mass; Inertia = m_Mass * (m_Size.x * m_Size.x + m_Size.y * m_Size.y) / 12.0f; } } public MyRigidbody() { } public MyRigidbody(float mass, float inertia) { Mass = mass; Inertia = inertia; } public int Id { get { return m_Id; } set { m_Id = value; } } public Vector2 Size { get { return m_Size; } set { m_Size = value; } } public float Friction { get { return m_Friction; } set { m_Friction = value; } } //---------- 线性运动 public float Mass { get { return m_Mass; } set { m_Mass = value; if (value >= float.PositiveInfinity) m_InvMass = 0; else m_InvMass = 1 / value; } } public float InvMass { get { return m_InvMass; } } public Vector2 Position { get { return m_Position; } set { m_Position = value; } } public Vector2 Velocity { get { return m_Velocity; } } //线性冲量产生线速度变化 public void ApplyImpulse(Vector2 impulse) { // 动量定理: I = Δp = m * Δv m_Velocity += impulse * m_InvMass; } //---------- //----------角运动 public float Inertia { get { return m_Inertia; } set { m_Inertia = value; if (value >= float.PositiveInfinity) m_InvInertia = 0; else m_InvInertia = 1 / value; } } public float InvInertia { get { return m_InvInertia; } } public float AngleVelocity { get { return m_AngleVelocity; } } public float Rotation { get { return m_Rotation; } set { m_Rotation = value; } } /// <summary> /// 线性冲量产生角速度变化 /// </summary> /// <param name="r">矢径, 质心指向碰撞点</param> /// <param name="impulse">线性冲量</param> public void ApplyTorqueImpulse(Vector2 r, Vector2 impulse) { //角冲量: H = r × 线性冲量; r为力臂向量(或叫矢径), r与线性冲量方向垂直; float torqueImpulse = r.x * impulse.y - r.y * impulse.x; //角动量定理: H = ΔL = I * Δω //所以角速度的变化: Δω = H / I m_AngleVelocity += torqueImpulse * m_InvInertia; } //---------- public Vector2 GetPointVelocity(Vector2 r) { //角运动线速度: v = r * dir * ω, dir为线速度方向 Vector2 v = new Vector2(-r.y, r.x) * m_AngleVelocity; return m_Velocity + v; } //计算力和冲量引起的速度变化 public void PreUpdate(Vector2 gravity, float dt) { //----- 持续力 //a = F / m //v1 = v0 + a * t m_Velocity += (m_Force * m_InvMass + gravity) * dt; //角加速度 = 力矩 / 惯量 //ω1 = ω0 + 角加速度 * t m_AngleVelocity += m_Torque * m_InvInertia * dt; //----- //----- 脉冲力(冲量) //动量定理: 冲量 = Δp = m * Δv // >>> Δv = 冲量 / m m_Velocity += m_ForceImpulse * m_InvMass; //角动量定理: 角冲量 = ΔL = 惯量 * Δω // >>> Δω = 角冲量 / 惯量 m_AngleVelocity += m_TorqueImpulse * m_InvInertia; m_ForceImpulse = Vector2.zero; //冲量是瞬时效果, 作用完就置零 m_TorqueImpulse = 0; //----- } //根据速度进行运动 public void PostUpdate(float dt) { m_Position += m_Velocity * dt; m_Rotation += m_AngleVelocity * dt; } #if UNITY_EDITOR public Color m_GizmosColor = Color.white; private Vector2[] m_TempVerts = new Vector2[4]; private void UpdateCorners() { Vector2 halfSize = m_Size * 0.5f; m_TempVerts[0] = -halfSize; //lb m_TempVerts[1] = new Vector2(-halfSize.x, halfSize.y); //lt m_TempVerts[2] = halfSize; //rt m_TempVerts[3] = new Vector2(halfSize.x, -halfSize.y); //rb } public Vector2[] GetVerts() { UpdateCorners(); for (int i = 0; i < m_TempVerts.Length; ++i) m_TempVerts[i] = this.transform.TransformPoint(m_TempVerts[i]); return m_TempVerts; } private void OnDrawGizmos() {
if (m_Size.sqrMagnitude <= 0) return; var oldColor = Gizmos.color; Gizmos.color = m_GizmosColor; var verts = GetVerts(); var trans = this.transform; if (Application.isPlaying) { trans.position = m_Position; trans.eulerAngles = new Vector3(0, 0, Mathf.Rad2Deg * m_Rotation); } else { m_Position = trans.position; m_Rotation = trans.eulerAngles.z * Mathf.Deg2Rad; } for (int i = 0; i < verts.Length; ++i) { var pos1 = verts[i]; var pos2 = verts[(i + 1) % verts.Length]; Gizmos.DrawLine(pos1, pos2); } Gizmos.color = oldColor; } #endif }
enum CollisionStage { None, Enter, Stay, Exit, } //CollisionPair使用两个刚体的id作为索引 struct CollisionPairKey { public int m_IdA; public int m_IdB; public CollisionPairKey(int idA, int idB) { m_IdA = idA; m_IdB = idB; } } class CollisionPair { public int m_UpdateIndex; //发生碰撞时的帧 public MyRigidbody m_RigidbodyA; public MyRigidbody m_RigidbodyB; public CollisionStage m_Stage = CollisionStage.None; public ContactInfo[] m_Contacts = new ContactInfo[2]; public int m_NumContacts; } //碰撞点信息 public class ContactInfo { public Vector2 m_Point; //碰撞点 public Vector2 m_Normal; //碰撞法向量(分离方向), 这边用A指向B, 即: B反弹方向 public float m_Penetration; //穿透深度(分离距离) public uint m_Fp; //用于识别两box的碰撞 public float m_ImpulseNormal; //法线方向累加冲量 public float m_ImpulseTangent; //切线方向累加冲量 public float m_MassNormal; //碰撞法线方向有效质量 public float m_MassTangent; }
public class MyPhysics : MonoBehaviour { public int m_MaxIterCount = 10; [SerializeField] private Vector2 m_Gravity = Vector2.zero; [SerializeField] private bool m_WarmStarting = false; //碰撞点没变化时, 沿用之前碰撞点的冲量信息 [SerializeField] private bool m_AccumulateImpulses = false; //冲量累加 private List<MyRigidbody> m_RigidbodyList = new List<MyRigidbody>(); private List<MyRigidbody> m_PendingAddList = new List<MyRigidbody>(); //要添加的刚体会在下一帧添加 private List<MyRigidbody> m_PendingRemoveList = new List<MyRigidbody>(); //要删除的刚体在下一帧删除 private Dictionary<CollisionPairKey, CollisionPair> m_CollisionPairDict = new Dictionary<CollisionPairKey, CollisionPair>(); //两个发生碰撞的物体 private List<CollisionPairKey> m_TempRemoveCollisionPairList = new List<CollisionPairKey>(); private int m_IdCounter; //刚体id计数 private int m_UpdateCounter; //更新计数 private B2SatCollide m_Sat = new B2SatCollide(); void Start() { var initRigidbodys = GetComponentsInChildren<MyRigidbody>(); foreach (var rigidbody in initRigidbodys) { AddRigidbody(rigidbody); } } void FixedUpdate() { if (Time.fixedDeltaTime <= 0) return; Step(Time.fixedDeltaTime); } public void Step(float dt) { CheckPendingList(); m_UpdateCounter++; for (int i = 0; i < m_RigidbodyList.Count; ++i) { var rigidbody = m_RigidbodyList[i]; if (0 == rigidbody.InvMass) continue; rigidbody.PreUpdate(m_Gravity, dt); } CheckCollision(); UpdateSeperation(dt); for (int i = 0; i < m_RigidbodyList.Count; ++i) { var rigidbody = m_RigidbodyList[i]; rigidbody.PostUpdate(dt); } } //检查发生碰撞的物体 private void CheckCollision() { for (int i = 0; i < m_RigidbodyList.Count; ++i) { var rigidbodyA = m_RigidbodyList[i]; for (int j = i + 1; j < m_RigidbodyList.Count; ++j) { var rigidbodyB = m_RigidbodyList[j]; if (0 == rigidbodyA.InvMass && 0 == rigidbodyB.InvMass) continue; if (m_Sat.Collide(rigidbodyA, rigidbodyB) > 0) { if (rigidbodyA.Id < rigidbodyB.Id) OnCollide(rigidbodyA, rigidbodyB); else OnCollide(rigidbodyB, rigidbodyA); } } } } //刚体上关联的形状发生碰撞 private void OnCollide(MyRigidbody rigidbodyA, MyRigidbody rigidbodyB) { var key = new CollisionPairKey(rigidbodyA.Id, rigidbodyB.Id); if (!m_CollisionPairDict.TryGetValue(key, out var collisionInfo)) //之前没发生过碰撞(第1次碰撞) { collisionInfo = new CollisionPair(); collisionInfo.m_RigidbodyA = rigidbodyA; collisionInfo.m_RigidbodyB = rigidbodyB; m_CollisionPairDict.Add(key, collisionInfo); } collisionInfo.m_UpdateIndex = m_UpdateCounter; //发生了碰撞就更新帧id, 如果有一帧没更新, 就说明那一帧没发生碰撞 int oldNumContacts = collisionInfo.m_NumContacts; collisionInfo.m_NumContacts = m_Sat.NumContacts; if (collisionInfo.m_Stage == CollisionStage.None) //第1次碰撞 { collisionInfo.m_Stage = CollisionStage.Enter; for (int i = 0; i < collisionInfo.m_NumContacts; ++i) collisionInfo.m_Contacts[i] = m_Sat.GetContact(i); } else { for (int i = 0; i < collisionInfo.m_NumContacts; ++i) { var contactInfo = m_Sat.GetContact(i); if (m_WarmStarting) { for (int j = 0; j < oldNumContacts; ++j) { var oldContactInfo = collisionInfo.m_Contacts[j]; if (contactInfo.m_Fp == oldContactInfo.m_Fp) //碰撞点没变, 冲量继续沿用 { contactInfo.m_ImpulseNormal = oldContactInfo.m_ImpulseNormal; contactInfo.m_ImpulseTangent = oldContactInfo.m_ImpulseTangent; } } } collisionInfo.m_Contacts[i] = contactInfo; } } } //物体发生弹性碰撞, 会相互弹开 private void UpdateSeperation(float dt) { foreach (var entry in m_CollisionPairDict) { var collisionPair = entry.Value; if (collisionPair.m_UpdateIndex != m_UpdateCounter) //上一帧没发生碰撞 { collisionPair.m_Stage = CollisionStage.Exit; } switch (collisionPair.m_Stage) { case CollisionStage.Enter: //todo: 通知Enter事件 collisionPair.m_Stage = CollisionStage.Stay; break; case CollisionStage.Exit: //todo: 通知Exit事件 collisionPair.m_Stage = CollisionStage.None; var key = new CollisionPairKey(collisionPair.m_RigidbodyA.Id, collisionPair.m_RigidbodyB.Id); m_TempRemoveCollisionPairList.Add(key); //for循环中删除会报错 break; } if (CollisionStage.Stay == collisionPair.m_Stage) { //todo: 通知Stay事件 PreSeperation(dt, collisionPair); } } if (m_TempRemoveCollisionPairList.Count > 0) { for (int i = 0; i < m_TempRemoveCollisionPairList.Count; ++i) { var key = m_TempRemoveCollisionPairList[i]; m_CollisionPairDict.Remove(key); } m_TempRemoveCollisionPairList.Clear(); } for (int i = 0; i < m_MaxIterCount; ++i) { foreach (var entry in m_CollisionPairDict) { PostSeperation(dt, entry.Value); } } } private void PreSeperation(float dt, CollisionPair collisionPair) { var rigidbodyA = collisionPair.m_RigidbodyA; var rigidbodyB = collisionPair.m_RigidbodyB; for (int i = 0; i < collisionPair.m_NumContacts; ++i) { var contact = collisionPair.m_Contacts[i]; Vector2 ra = contact.m_Point - rigidbodyA.Position; Vector2 rb = contact.m_Point - rigidbodyB.Position; Vector2 normal = contact.m_Normal; Vector2 tangent = new Vector2(normal.y, -normal.x); //切线(顺时针) float kMassNormal = rigidbodyA.InvMass + rigidbodyB.InvMass; float raN = Vector2.Dot(ra, normal); float rbN = Vector2.Dot(rb, normal); kMassNormal += rigidbodyA.InvInertia * (Vector2.Dot(ra, ra) - raN * raN) + rigidbodyB.InvInertia * (Vector2.Dot(rb, rb) - rbN * rbN); contact.m_MassNormal = 1 / kMassNormal; float kMassTangent = rigidbodyA.InvMass + rigidbodyB.InvMass; float raT = Vector2.Dot(ra, tangent); float rbT = Vector2.Dot(rb, tangent); kMassTangent += rigidbodyA.InvInertia * (Vector2.Dot(ra, ra) - raT * raT) + rigidbodyB.InvInertia * (Vector2.Dot(rb, rb) - rbT * rbT); contact.m_MassTangent = 1 / kMassTangent; if (m_AccumulateImpulses) { Vector2 impulse = contact.m_ImpulseNormal * normal; //冲量大小转成冲量向量 impulse += contact.m_ImpulseTangent * tangent; //切线方向 rigidbodyA.ApplyImpulse(-impulse); rigidbodyA.ApplyTorqueImpulse(ra, -impulse); rigidbodyB.ApplyImpulse(impulse); rigidbodyB.ApplyTorqueImpulse(rb, impulse); } } } private void PostSeperation(float dt, CollisionPair collisionPair) { var rigidbodyA = collisionPair.m_RigidbodyA; var rigidbodyB = collisionPair.m_RigidbodyB; float mergeFriction = Mathf.Sqrt(rigidbodyA.Friction * rigidbodyB.Friction); for (int i = 0; i < collisionPair.m_NumContacts; ++i) { var contact = collisionPair.m_Contacts[i]; Vector2 ra = contact.m_Point - rigidbodyA.Position; Vector2 rb = contact.m_Point - rigidbodyB.Position; var relativeV = rigidbodyB.GetPointVelocity(rb) - rigidbodyA.GetPointVelocity(ra); var normal = contact.m_Normal; float relativeVN = Vector2.Dot(relativeV, normal); //投影到法向量 //if (relativeVN > 0) //相对速度>0时, 表明没有碰撞趋势了 // return; //Δp = (v2 - v1) / kMass float deltaPN = relativeVN * contact.m_MassNormal; deltaPN = -deltaPN; //对Δp取反, 主要是为了让累加冲量是正值 if (m_AccumulateImpulses) { float deltaPNBak = deltaPN; float lastImpulseN = contact.m_ImpulseNormal; contact.m_ImpulseNormal += deltaPN; //叠加本次冲量(冲量=Δp) if (contact.m_ImpulseNormal <= 0) //防止弹开过程中, 变成拉回来的冲量 { contact.m_ImpulseNormal = 0; deltaPN = -lastImpulseN; } } else { deltaPN = Mathf.Max(deltaPN, 0); //冲量为负, 碰撞后就加速了, 这样不对 } Vector2 impulseN = deltaPN * normal; //转为矢量 rigidbodyA.ApplyImpulse(-impulseN); rigidbodyA.ApplyTorqueImpulse(ra, -impulseN); rigidbodyB.ApplyImpulse(impulseN); rigidbodyB.ApplyTorqueImpulse(rb, impulseN); //摩擦力(切线方向) relativeV = rigidbodyB.GetPointVelocity2(rb) - rigidbodyA.GetPointVelocity2(ra); //上面的冲量生效后, 再计算相对速度 var tangent = new Vector2(normal.y, -normal.x); //切线(顺时针) float relativeVT = Vector2.Dot(relativeV, tangent); //投影到切线方向 relativeVT = -relativeVT; //取反让值为正(与运动方向相同) float deltaPT = relativeVT * contact.m_MassTangent; if (m_AccumulateImpulses) { float deltaPTBak = deltaPT; float maxDeltaPt = mergeFriction * contact.m_ImpulseNormal; //不能超过法向量的冲量 float lastImpulseT = contact.m_ImpulseTangent; contact.m_ImpulseTangent = Mathf.Clamp(contact.m_ImpulseTangent + deltaPT, -maxDeltaPt, maxDeltaPt); deltaPT = contact.m_ImpulseTangent - lastImpulseT; } else { float maxDeltaPt = mergeFriction * deltaPN; deltaPT = Mathf.Clamp(deltaPT, -maxDeltaPt, maxDeltaPt); } Vector2 impulseT = deltaPT * tangent; rigidbodyA.ApplyImpulse(-impulseT); //摩擦力与运动(趋势)方向相反 rigidbodyA.ApplyTorqueImpulse(ra, -impulseT); rigidbodyB.ApplyImpulse(impulseT); rigidbodyB.ApplyTorqueImpulse(rb, impulseT); } } private void CheckPendingList() { if (m_PendingAddList.Count > 0) { for (int i = 0; i < m_PendingAddList.Count; ++i) { var rigidbody = m_PendingAddList[i]; rigidbody.Id = m_IdCounter++; rigidbody.OnAddToPhysics(); m_RigidbodyList.Add(rigidbody); } m_PendingAddList.Clear(); } if (m_PendingRemoveList.Count > 0) { for (int i = 0; i < m_PendingRemoveList.Count; ++i) { var rigidbody = m_PendingRemoveList[i]; m_RigidbodyList.Remove(rigidbody); rigidbody.OnRemoveFromPhysics(); } m_PendingRemoveList.Clear(); } } public void AddRigidbody(MyRigidbody rigidbody) { m_PendingAddList.Add(rigidbody); } public void RemoveRigidbody(MyRigidbody rigidbody) { m_PendingRemoveList.Add(rigidbody); } #if UNITY_EDITOR public Color m_GizmosColor = Color.white; private void OnDrawGizmos() { var oldColor = Gizmos.color; Gizmos.color = m_GizmosColor; Handles.color = m_GizmosColor; foreach (var entry in m_CollisionPairDict) { var collisionPair = entry.Value; for (int i = 0; i < collisionPair.m_NumContacts; ++i) { var contactInfo = collisionPair.m_Contacts[i]; DrawGizmosHelper.DrawPoint2(contactInfo.m_Point); DrawGizmosHelper.DrawArrowLine(contactInfo.m_Point, contactInfo.m_Normal, 0.5f); } } Gizmos.color = oldColor; Handles.color = oldColor; } #endif }
参考
参考Box2d算法实现的一个平衡球游戏_toi小游戏代码-CSDN博客
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!