Unity C#中访问GameObject克隆体变量?Verlet粒子碰撞问题
Verlet积分实现水粒子碰撞的问题与解决方案
问题描述
我正在用Verlet积分实现大量水粒子在盒子内弹跳和互相碰撞的效果,目前通过另一脚本的Instantiate()按空格键生成粒子,任意数量都能正常运行,但粒子间碰撞的弹跳计算遇到了核心问题:
- Verlet积分需要当前位置和上一位置来计算速度,但无法获取预制体克隆体的这些变量来完成碰撞速度计算
- 已通过勾股定理检测粒子间距离是否小于半径之和来判断碰撞,这部分逻辑可行(能直接获取
GameObject.transform) - 考虑过记录所有粒子上一帧位置,但担心内存占用过高
- 不确定用Unity的GameObject是否是正确实现方式,找不到直接渲染圆形到屏幕的方法
当前代码
using System; using System.Collections; using System.Collections.Generic; using UnityEngine; public class WaterScript : MonoBehaviour { //Variables for detecting collisions public float radius; public string cloneTag = "Water"; private bool collide; //Variables for Verlet Integration public Vector2 pos; public Vector2 posLast = new Vector2(0f, 0f); public Vector2 posNext; //Particle Properties public float mass = 1; public float elasticity = 1; //Parameters are current position and last position. Outputs next position Vector2 movement(Vector2 pos, Vector2 posLast, float mass) { //BasicVerletForCalculatingMovement posNext[0] = pos[0] + (pos[0] - posLast[0]) + 0f / Time.deltaTime; posNext[1] = pos[1] + (pos[1] - posLast[1]) - 0.00000001f / Time.deltaTime; return posNext; } // Start is called before the first frame update void Start() { } // Update is called once per frame void Update() { //Create a List for all the GameObjects with the "Water" Tag GameObject[] waterObjects = GameObject.FindGameObjectsWithTag("Water"); foreach (GameObject waterObject in waterObjects) { Vector2 position = waterObject.transform.position; //Collision Detection Scripts if (((transform.position.x - position[0]) * (transform.position.x - position[0])) + ((transform.position.y - position[1]) * (transform.position.y - position[1])) < radius*radius) { if(position[0] == transform.position.x && position[1] == transform.position.y) { } else { Debug.Log("Collide!"); } } } //Boundary collision handling float wall1 = -15; float wall2 = 10; float floor1 = -15; float floor2 = 10; if (transform.position.y < floor1) { float offset = pos[1] - floor1; pos[1] = floor1; posLast[1] = floor1 + ((-posLast[1] + floor1) + offset)*elasticity; } if (transform.position.x < wall1) { float offset = pos[0] - wall1; pos[0] = wall1; posLast[0] = wall1 + ((-posLast[0] + wall1) + offset)*elasticity; } if (transform.position.y > floor2) { float offset = pos[1] - floor2; pos[1] = floor2; posLast[1] = floor2 + ((-posLast[1] + floor2) + offset)*elasticity; } if (transform.position.x > wall2) { float offset = pos[0] - wall2; pos[0] = wall2; posLast[0] = wall2 + ((-posLast[0] + wall2) + offset)*elasticity; } movement(pos, posLast, 1); posLast = pos; pos = posNext; transform.position = pos; } }
解决方案
一、获取克隆体的Verlet变量(核心问题解决)
抛弃GameObject.FindGameObjectsWithTag()这种低效的全局查找,改用集中管理+组件引用的方式:
- 确保粒子预制体已挂载
WaterScript,每个粒子自己维护pos、posLast等Verlet变量 - 创建全局粒子管理器脚本,统一维护所有粒子的组件引用:
public class ParticleManager : MonoBehaviour { public static ParticleManager Instance; public List<WaterScript> particles = new List<WaterScript>(); public WaterScript particlePrefab; void Awake() { Instance = this; } public void SpawnParticle(Vector2 position) { WaterScript newParticle = Instantiate(particlePrefab, position, Quaternion.identity).GetComponent<WaterScript>(); newParticle.pos = position; newParticle.posLast = position; // 初始速度为0 particles.Add(newParticle); } }
- 在
WaterScript中直接通过管理器获取其他粒子的Verlet变量:
// 替换原有的GameObject遍历逻辑 foreach (WaterScript otherParticle in ParticleManager.Instance.particles) { if (otherParticle == this) continue; // 跳过自身 float distanceSq = (pos - otherParticle.pos).sqrMagnitude; float radiusSumSq = (radius + otherParticle.radius) * (radius + otherParticle.radius); if (distanceSq < radiusSumSq) { ResolveCollision(this, otherParticle); } }
二、Verlet碰撞的速度计算与修正
Verlet积分中速度由v = pos - posLast推导而来,碰撞时不需要直接修改速度,而是通过修正posLast来模拟速度变化,以下是弹性碰撞的实现:
void ResolveCollision(WaterScript a, WaterScript b) { Vector2 delta = a.pos - b.pos; float dist = delta.magnitude; Vector2 normal = delta / dist; // 计算相对速度(基于Verlet位置差) Vector2 velA = a.pos - a.posLast; Vector2 velB = b.pos - b.posLast; float relativeVel = Vector2.Dot(velA - velB, normal); // 分离状态不处理 if (relativeVel > 0) return; // 弹性碰撞 impulse 计算,修正posLast改变速度 float restitution = Mathf.Min(a.elasticity, b.elasticity); float impulse = -(1 + restitution) * relativeVel / (1/a.mass + 1/b.mass); a.posLast += impulse * normal / a.mass; b.posLast -= impulse * normal / b.mass; // 位置修正,避免粒子重叠 float overlap = (a.radius + b.radius) - dist; a.pos += normal * overlap * 0.5f; b.pos -= normal * overlap * 0.5f; }
三、内存与性能优化
- 避免重复查找:不在
Update()中调用全局查找方法,仅在粒子生成/销毁时更新管理器的粒子列表 - 空间分区优化:粒子数量超过100时,将盒子划分为网格,仅检测同一网格或相邻网格内的粒子,大幅减少碰撞检测次数
- 使用FixedUpdate:物理模拟放在
FixedUpdate()中,保证时间步长稳定,提升Verlet积分精度 - 内存顾虑消除:每个粒子的
pos、posLast仅占8字节,1000个粒子总内存仅16KB,完全无需担心
四、圆形渲染方案
无需纠结GameObject的局限性,有三种高效方案可选:
- Sprite渲染:给粒子预制体使用圆形Sprite,调整大小匹配半径,简单直接
- GPU实例化:粒子数量上万时,用
Graphics.DrawMeshInstanced渲染单个圆形Mesh,通过MaterialPropertyBlock传递每个粒子的位置和大小,性能远超单个GameObject - 调试渲染:仅用于调试的话,在
WaterScript中添加Gizmos绘制:
void OnDrawGizmos() { Gizmos.color = Color.blue; Gizmos.DrawWireSphere(transform.position, radius); }
五、修正Verlet积分代码
原movement函数未正确处理重力和时间步长,以下是标准Verlet积分实现:
Vector2 movement(Vector2 currentPos, Vector2 lastPos, float mass) { Vector2 velocity = currentPos - lastPos; Vector2 acceleration = new Vector2(0, -9.8f) / mass; // 重力加速度,可调整 // 标准Verlet公式:nextPos = 2*currentPos - lastPos + acceleration*dt² Vector2 nextPos = 2 * currentPos - lastPos + acceleration * Time.fixedDeltaTime * Time.fixedDeltaTime; return nextPos; }
调用时移至FixedUpdate():
void FixedUpdate() { posNext = movement(pos, posLast, mass); posLast = pos; pos = posNext; transform.position = pos; // 在此处理碰撞与边界检测 }
内容的提问来源于stack exchange,提问作者Andrew Martin
相关产品推荐
相关产品推荐

