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

Python类中.append()失效引发IndexError,Pygame流体模拟求助

Pygame流体模拟:修复IndexError与碰撞检测逻辑问题

错误根源分析

  1. check_collisions函数核心逻辑错误

    • 原跳过条件self.x < self.x + distance_to_ignore * (...)永远为真,导致所有粒子都被跳过,dx/dy列表始终为空,后续move_if_colliding访问self.dx[i]直接触发IndexError。
    • 循环内第一次判断就直接return,只会检查第一个粒子,无法遍历所有粒子,也无法收集完整的dx/dy列表。
    • 未排除粒子自身(自己与自己碰撞的无效情况)。
  2. move_if_colliding函数逻辑缺陷

    • 点积计算错误:正确的向量点积应为(dx1 * dx2) + (dy1 * dy2),而非原代码的(x差)*(y差)。
    • 未处理overlap=0的无碰撞场景,会触发除以0错误。
    • 角度计算冗余,且未直接使用弧度值(math.cos/math.sin需要弧度输入,无需转角度)。
  3. 其他潜在问题

    • start函数中gravity_temp未初始化,第一次运行会触发AttributeError。
    • draw函数中坐标计算错误,self.x - (self.size/WIDTH)会导致粒子位置无意义偏移。

修复方案与完整代码

以下是修复后的完整代码,附带关键修改注释:

import pygame, random, math

# Pygame基础配置
WIDTH = 750
HEIGHT = 750
FPS = 60

# 初始化Pygame
pygame.init()
SCREEN = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("2D Fluid Simulation")

# 颜色定义
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
GREEN = (0, 255, 0)

# 模拟参数
GRAVITY = 0.918
amt_white_particles = 100
amt_red_particles = 50
amt_green_particles = 10
collision_check_depth = 5
distance_to_ignore = 3

class Particle:
    def __init__(self, start_x, start_y, size, color):
        self.start_x = start_x
        self.start_y = start_y
        self.x, self.y = self.start_x, self.start_y
        self.dx, self.dy = [], []  # 存储与其他粒子的x/y距离向量
        self.x_speed, self.y_speed = 0, 0
        self.size = size
        self.color = color
        self.mass = size ** 2  # 质量与粒子面积成正比,符合物理逻辑
        self.gravity_temp = GRAVITY  # 初始化重力临时变量

    def check_collisions(self, particles_list):
        self.dx.clear()
        self.dy.clear()
        max_overlap = 0
        collisions = []

        for particle in particles_list:
            # 跳过粒子自身
            if particle is self:
                continue
            
            dx = self.x - particle.x
            dy = self.y - particle.y
            distance = math.hypot(dx, dy)  # 高效计算两点距离
            min_distance = (self.size + particle.size) / 2

            # 跳过距离过远的粒子,优化性能
            if distance > min_distance + distance_to_ignore:
                continue

            self.dx.append(dx)
            self.dy.append(dy)

            # 计算重叠量并记录碰撞信息
            if distance < min_distance:
                overlap = min_distance - distance
                if overlap > max_overlap:
                    max_overlap = overlap
                collisions.append((particle, overlap, dx, dy))
        
        return max_overlap, collisions

    def move_if_colliding(self, overlap, collisions):
        if overlap <= 0:
            return
        
        for particle, overlap, dx, dy in collisions:
            # 计算碰撞法线方向的单位向量
            distance = math.hypot(dx, dy)
            if distance == 0:
                continue  # 避免除以0错误
            nx = dx / distance
            ny = dy / distance

            # 计算相对速度
            rel_vx = particle.x_speed - self.x_speed
            rel_vy = particle.y_speed - self.y_speed

            # 计算相对速度在法线方向的分量
            dot_product = rel_vx * nx + rel_vy * ny

            # 粒子正在分离时无需处理
            if dot_product > 0:
                continue

            # 完全弹性碰撞公式计算冲量
            impulse = (2 * dot_product) / (self.mass + particle.mass)
            self.x_speed += impulse * particle.mass * nx
            self.y_speed += impulse * particle.mass * ny
            particle.x_speed -= impulse * self.mass * nx
            particle.y_speed -= impulse * self.mass * ny

            # 分离重叠的粒子,避免卡死
            separation = overlap / 2
            self.x += separation * nx
            self.y += separation * ny
            particle.x -= separation * nx
            particle.y -= separation * ny

    def start(self, particles):
        # 地面碰撞处理,加入能量损失模拟真实弹跳
        if self.y + self.size > HEIGHT:
            self.y_speed = -self.y_speed * 0.8
            self.y = HEIGHT - self.size
        else:
            self.y_speed += self.gravity_temp

        # 更新粒子位置
        self.x += self.x_speed
        self.y += self.y_speed

        # 多轮碰撞检测,确保粒子完全分离
        for _ in range(collision_check_depth):
            max_overlap, collisions = self.check_collisions(particles)
            self.move_if_colliding(max_overlap, collisions)

    def draw(self):
        # 修复坐标计算错误,直接使用粒子中心坐标绘制
        pygame.draw.circle(SCREEN, self.color, (int(self.x), int(self.y)), self.size)

def main():
    running = True
    clock = pygame.time.Clock()

    particles = []

    # 创建白色粒子
    for _ in range(amt_white_particles):
        particles.append(Particle(
            random.randint(50, WIDTH-50), 
            random.randint(50, HEIGHT//2), 
            5, WHITE
        ))
    # 创建红色粒子
    for _ in range(amt_red_particles):
        particles.append(Particle(
            random.randint(50, WIDTH-50), 
            random.randint(50, HEIGHT//2), 
            7, RED
        ))
    # 创建绿色粒子
    for _ in range(amt_green_particles):
        particles.append(Particle(
            random.randint(50, WIDTH-50), 
            random.randint(50, HEIGHT//2), 
            10, GREEN
        ))

    while running:
        clock.tick(FPS)

        # 事件处理
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False

        # 绘制背景
        SCREEN.fill(BLACK)

        # 更新并绘制所有粒子
        for particle in particles:
            particle.start(particles)
            particle.draw()

        # 更新屏幕显示
        pygame.display.flip()

    pygame.quit()

if __name__ == "__main__":
    main()

关键修改说明

  1. 碰撞检测逻辑重构

    • 每次检测前清空dx/dy列表,确保数据最新无累积。
    • 排除粒子自身,避免无效计算。
    • 使用math.hypot高效计算距离,避免重复平方运算。
    • 返回所有碰撞的详细信息,支持多粒子碰撞处理。
  2. 碰撞响应优化

    • 采用物理弹性碰撞公式,实现更真实的粒子交互效果。
    • 加入粒子分离逻辑,避免重叠卡死。
    • 地面碰撞加入能量损失,模拟真实弹跳衰减。
  3. 其他细节修复

    • 初始化gravity_temp,避免未定义错误。
    • 修复draw函数坐标计算,确保粒子位置准确。
    • 调整粒子初始位置,避免一开始就出界。

内容的提问来源于stack exchange,提问作者Enrico Bottura

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 07:50:58