You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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()这种低效的全局查找,改用集中管理+组件引用的方式:

  1. 确保粒子预制体已挂载WaterScript,每个粒子自己维护pos、posLast等Verlet变量
  2. 创建全局粒子管理器脚本,统一维护所有粒子的组件引用:
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);
    }
}
  1. 在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的局限性,有三种高效方案可选:

  1. Sprite渲染:给粒子预制体使用圆形Sprite,调整大小匹配半径,简单直接
  2. GPU实例化:粒子数量上万时,用Graphics.DrawMeshInstanced渲染单个圆形Mesh,通过MaterialPropertyBlock传递每个粒子的位置和大小,性能远超单个GameObject
  3. 调试渲染:仅用于调试的话,在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 20:48:12