需要源码请+V:xrbcgfh0214
弹跳小球作为物理引擎的基础实验场,完美展现了经典力学与计算机图形学的交融。本文将深入解析二维弹性碰撞的数学模型,揭示实时物理模拟背后的微分方程与优化策略。
小球运动遵循牛顿第二定律:
{ d v d t = g − μ v d x d t = v \begin{cases} \frac{d\mathbf{v}}{dt} = \mathbf{g} - \mu \mathbf{v} \\ \frac{d\mathbf{x}}{dt} = \mathbf{v} \end{cases} {dtdv=g−μvdtdx=v
离散化后实现数值积分:
v n + 1 = v n + ( g − μ v n ) Δ t x n + 1 = x n + v n Δ t \mathbf{v}_{n+1} = \mathbf{v}_n + (\mathbf{g} - \mu \mathbf{v}_n)\Delta t \\ \mathbf{x}_{n+1} = \mathbf{x}_n + \mathbf{v}_n\Delta t vn+1=vn+(g−μvn)Δtxn+1=xn+vnΔt
碰撞恢复系数决定速度衰减:
v n e w = − e ⋅ v o l d ( 0 ≤ e ≤ 1 ) v_{new} = -e \cdot v_{old} \quad (0 \leq e \leq 1) vnew=−e⋅vold(0≤e≤1)
屏幕边界判断条件:
{ x m i n = r x ≤ r x m a x = W − r x ≥ W − r y m i n = r y ≤ r y m a x = H − r y ≥ H − r \begin{cases} x_{min} = r & x \leq r \\ x_{max} = W-r & x \geq W-r \\ y_{min} = r & y \leq r \\ y_{max} = H-r & y \geq H-r \end{cases} ⎩ ⎨ ⎧xmin=rxmax=W−rymin=rymax=H−rx≤rx≥W−ry≤ry≥H−r
两球碰撞条件:
∣ ∣ x i − x j ∣ ∣ ≤ r i + r j ||\mathbf{x}_i - \mathbf{x}_j|| \leq r_i + r_j ∣∣xi−xj∣∣≤ri+rj
自适应步长算法:
Δ t n e w = min ( Δ t m a x , ϵ max ( ∣ ∣ a ∣ ∣ ) ) \Delta t_{new} = \min(\Delta t_{max}, \frac{\epsilon}{\max(||\mathbf{a}||)}) Δtnew=min(Δtmax,max(∣∣a∣∣)ϵ)
四叉树空间划分:
完全弹性碰撞公式:
{ v 1 ′ = v 1 − 2 m 2 m 1 + m 2 ( v 1 − v 2 ) ⋅ n ∣ ∣ n ∣ ∣ 2 n v 2 ′ = v 2 − 2 m 1 m 1 + m 2 ( v 2 − v 1 ) ⋅ n ∣ ∣ n ∣ ∣ 2 n \begin{cases} \mathbf{v}_1' = \mathbf{v}_1 - \frac{2m_2}{m_1+m_2} \frac{(\mathbf{v}_1-\mathbf{v}_2)\cdot\mathbf{n}}{||\mathbf{n}||^2} \mathbf{n} \\ \mathbf{v}_2' = \mathbf{v}_2 - \frac{2m_1}{m_1+m_2} \frac{(\mathbf{v}_2-\mathbf{v}_1)\cdot\mathbf{n}}{||\mathbf{n}||^2} \mathbf{n} \end{cases} {v1′=v1−m1+m22m2∣∣n∣∣2(v1−v2)⋅nnv2′=v2−m1+m22m1∣∣n∣∣2(v2−v1)⋅nn
旋转速度更新:
ω n e w = ω o l d + r × F f I Δ t \omega_{new} = \omega_{old} + \frac{r \times F_f}{I}\Delta t ωnew=ωold+Ir×FfΔt
弹跳小球的物理模拟展现了连续介质力学在离散世界的完美映射。从微分方程的数值解算到碰撞响应的动量传递,每个技术细节都构建起虚拟世界的物理可信度。这种基础模型为更复杂的刚体仿真奠定了基础。
扩展阅读方向:
import pygame
import sys
import random
import numpy as np
from ball import Ball
from physics_engine import PhysicsEngine
class BallSimulation:
"""弹跳小球物理模拟主程序"""
def __init__(self):
# 初始化pygame
pygame.init()
# 设置屏幕
self.width = 800
self.height = 600
self.screen = pygame.display.set_mode((self.width, self.height))
pygame.display.set_caption("弹跳小球:物理模拟与碰撞检测")
# 创建时钟对象
self.clock = pygame.time.Clock()
# 创建物理引擎
self.physics_engine = PhysicsEngine(self.width, self.height)
# 设置颜色
self.colors = [
(255, 0, 0), # 红色
(0, 255, 0), # 绿色
(0, 0, 255), # 蓝色
(255, 255, 0), # 黄色
(255, 0, 255), # 紫色
(0, 255, 255), # 青色
(255, 165, 0), # 橙色
(128, 0, 128) # 紫色
]
# 加载字体 - 修复字体问题,使用系统默认字体
try:
# 尝试使用微软雅黑(Windows系统常见字体,支持中文)
self.font = pygame.font.SysFont('microsoft yahei', 14)
except:
try:
# 备选Sans字体
self.font = pygame.font.SysFont('simhei', 14)
except:
# 如果都失败,使用默认字体并降低大小
self.font = pygame.font.Font(None, 14)
# 初始状态
self.paused = False
self.show_stats = True
self.adaptive_timestep = False
self.show_all_trajectories = False
# 重力控制相关
self.gravity_rotation_speed = 2.0 # 每帧旋转角度
self.gravity_magnitude_change_rate = 5.0 # 每帧变化量
self.show_gravity_vector = True # 是否显示重力向量
def create_random_ball(self, x=None, y=None):
"""创建一个随机属性的小球"""
# 随机位置
if x is None:
x = random.randint(50, self.width - 50)
if y is None:
y = random.randint(50, self.height - 50)
# 随机半径和质量
radius = random.randint(15, 30)
mass = radius / 10
# 随机初始速度
vx = random.uniform(-200, 200)
vy = random.uniform(-200, 200)
# 随机恢复系数
restitution = random.uniform(0.7, 0.95)
# 随机空气阻力
mu = random.uniform(0.01, 0.05)
# 随机颜色
color = random.choice(self.colors)
return Ball(x, y, radius, mass, color, vx, vy, restitution, mu)
def handle_events(self):
"""处理用户输入事件"""
for event in pygame.event.get():
# 退出程序
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
# 鼠标按下事件
elif event.type == pygame.MOUSEBUTTONDOWN:
# 左键添加小球
if event.button == 1:
x, y = pygame.mouse.get_pos()
ball = self.create_random_ball(x, y)
# 如果全局轨迹显示开启,则设置新球的轨迹也显示
if self.show_all_trajectories:
ball.show_trajectory = True
self.physics_engine.add_ball(ball)
# 右键清除所有小球
elif event.button == 3:
self.physics_engine.balls.clear()
# 键盘按下事件
elif event.type == pygame.KEYDOWN:
# 空格暂停/继续
if event.key == pygame.K_SPACE:
self.paused = not self.paused
# A键添加随机球
elif event.key == pygame.K_a:
ball = self.create_random_ball()
# 如果全局轨迹显示开启,则设置新球的轨迹也显示
if self.show_all_trajectories:
ball.show_trajectory = True
self.physics_engine.add_ball(ball)
# C键清除所有球
elif event.key == pygame.K_c:
self.physics_engine.balls.clear()
# S键显示/隐藏统计信息
elif event.key == pygame.K_s:
self.show_stats = not self.show_stats
# T键切换自适应时间步长
elif event.key == pygame.K_t:
self.adaptive_timestep = not self.adaptive_timestep
self.physics_engine.adaptiveTimeStep = self.adaptive_timestep
# R键切换所有球的轨迹显示
elif event.key == pygame.K_r:
self.show_all_trajectories = not self.show_all_trajectories
for ball in self.physics_engine.balls:
ball.show_trajectory = self.show_all_trajectories
# G键切换重力向量显示
elif event.key == pygame.K_g:
self.show_gravity_vector = not self.show_gravity_vector
# 1-4键设置预设重力场景
elif event.key == pygame.K_1:
# 向下重力 (正常)
self.physics_engine.set_gravity_direction(np.array([0, 1]))
self.physics_engine.set_gravity_magnitude(9.8 * 60)
elif event.key == pygame.K_2:
# 向上重力 (反重力)
self.physics_engine.set_gravity_direction(np.array([0, -1]))
self.physics_engine.set_gravity_magnitude(9.8 * 60)
elif event.key == pygame.K_3:
# 向左重力
self.physics_engine.set_gravity_direction(np.array([-1, 0]))
self.physics_engine.set_gravity_magnitude(9.8 * 60)
elif event.key == pygame.K_4:
# 向右重力
self.physics_engine.set_gravity_direction(np.array([1, 0]))
self.physics_engine.set_gravity_magnitude(9.8 * 60)
elif event.key == pygame.K_5:
# 零重力
self.physics_engine.set_gravity_magnitude(0)
elif event.key == pygame.K_6:
# 月球重力 (约地球的1/6)
self.physics_engine.set_gravity_magnitude(9.8 * 60 / 6)
elif event.key == pygame.K_7:
# 木星重力 (约地球的2.5倍)
self.physics_engine.set_gravity_magnitude(9.8 * 60 * 2.5)
# 数字键8-9选择特定球切换轨迹
elif pygame.K_8 <= event.key <= pygame.K_9:
ball_index = event.key - pygame.K_8
if ball_index < len(self.physics_engine.balls):
self.physics_engine.balls[ball_index].toggle_trajectory()
# 获取按键状态 (持续按下)
keys = pygame.key.get_pressed()
# 旋转重力方向
if keys[pygame.K_LEFT]:
self.physics_engine.rotate_gravity(self.gravity_rotation_speed)
if keys[pygame.K_RIGHT]:
self.physics_engine.rotate_gravity(-self.gravity_rotation_speed)
# 调整重力大小
if keys[pygame.K_UP]:
new_magnitude = self.physics_engine.gravity_magnitude + self.gravity_magnitude_change_rate
self.physics_engine.set_gravity_magnitude(new_magnitude)
if keys[pygame.K_DOWN]:
new_magnitude = max(0, self.physics_engine.gravity_magnitude - self.gravity_magnitude_change_rate)
self.physics_engine.set_gravity_magnitude(new_magnitude)
def draw_gravity_vector(self):
"""绘制重力向量"""
if not self.show_gravity_vector:
return
# 重力向量起点 (屏幕中心)
center_x, center_y = self.width // 2, self.height // 2
# 计算终点 (根据重力方向和大小)
# 缩放向量长度以适应屏幕
scale_factor = min(self.width, self.height) / 5
normalized_magnitude = min(1.0, self.physics_engine.gravity_magnitude / (9.8 * 60 * 3))
end_x = center_x + int(self.physics_engine.gravity_direction[0] * scale_factor * normalized_magnitude)
end_y = center_y + int(self.physics_engine.gravity_direction[1] * scale_factor * normalized_magnitude)
# 绘制箭头主体
arrow_color = (150, 50, 50)
pygame.draw.line(self.screen, arrow_color, (center_x, center_y), (end_x, end_y), 3)
# 绘制箭头头部
arrow_head_length = 10
arrow_head_width = 7
# 计算箭头方向
direction = np.array([end_x - center_x, end_y - center_y], dtype=float)
if np.linalg.norm(direction) < 1e-10: # 避免零向量
return
direction = direction / np.linalg.norm(direction)
# 计算垂直于方向的向量
perpendicular = np.array([-direction[1], direction[0]])
# 箭头尖端
tip = np.array([end_x, end_y])
# 计算箭头底部两个点
base1 = tip - arrow_head_length * direction + arrow_head_width * perpendicular
base2 = tip - arrow_head_length * direction - arrow_head_width * perpendicular
# 绘制箭头头部
pygame.draw.polygon(self.screen, arrow_color, [
(int(tip[0]), int(tip[1])),
(int(base1[0]), int(base1[1])),
(int(base2[0]), int(base2[1]))
])
def draw_stats(self):
"""绘制统计信息"""
if not self.show_stats:
return
stats = self.physics_engine.get_performance_stats()
# 格式化重力信息
gravity_direction = self.physics_engine.gravity_direction
gravity_angle = np.degrees(np.arctan2(gravity_direction[1], gravity_direction[0]))
# 准备显示文本
texts = [
f"FPS: {int(self.clock.get_fps())}",
f"球数量: {stats['ball_count']}",
f"碰撞检测次数: {stats['collision_checks']}",
f"碰撞处理次数: {stats['collisions_resolved']}",
f"自适应时间步长: {'开启' if self.adaptive_timestep else '关闭'}",
f"轨迹显示: {'全部开启' if self.show_all_trajectories else '部分开启'}",
f"重力大小: {self.physics_engine.gravity_magnitude / 60:.1f}g",
f"重力角度: {gravity_angle:.1f}°",
f"暂停状态: {'是' if self.paused else '否'}"
]
# 绘制文本背景 - 使用Surface创建半透明背景
text_height = 20
panel_width = 200
panel_height = 10 + len(texts) * text_height
# 创建半透明surface
s = pygame.Surface((panel_width, panel_height))
s.set_alpha(180) # 设置透明度 (0-255)
s.fill((10, 10, 50)) # 深蓝色背景
self.screen.blit(s, (10, 10))
# 绘制文本
for i, text in enumerate(texts):
text_surface = self.font.render(text, True, (255, 255, 255))
self.screen.blit(text_surface, (15, 15 + i * text_height))
def draw_instructions(self):
"""绘制操作指南"""
instructions = [
"左键点击: 在点击位置添加小球",
"右键点击: 清除所有小球",
"A键: 随机位置添加小球",
"C键: 清除所有小球",
"S键: 显示/隐藏统计信息",
"T键: 开启/关闭自适应时间步长",
"R键: 显示/隐藏所有小球的轨迹",
"G键: 显示/隐藏重力向量",
"1-4键: 切换重力方向 (下/上/左/右)",
"5-7键: 切换重力大小 (零/月球/木星)",
"8-9键: 切换特定小球的轨迹",
"方向键: 调整重力方向和大小",
"空格键: 暂停/继续模拟"
]
# 绘制文本背景 - 使用Surface创建半透明背景
text_height = 20
panel_width = 270
panel_height = 10 + len(instructions) * text_height
# 创建半透明surface
s = pygame.Surface((panel_width, panel_height))
s.set_alpha(180) # 设置透明度 (0-255)
s.fill((50, 10, 50)) # 深紫色背景
self.screen.blit(s, (self.width - 280, 10))
# 绘制文本
for i, text in enumerate(instructions):
text_surface = self.font.render(text, True, (255, 255, 255))
self.screen.blit(text_surface, (self.width - 275, 15 + i * text_height))
def run(self):
"""主循环"""
# 默认添加一些球
for _ in range(5):
ball = self.create_random_ball()
self.physics_engine.add_ball(ball)
while True:
# 处理事件
self.handle_events()
# 清屏
self.screen.fill((240, 240, 240))
# 绘制边界
pygame.draw.rect(self.screen, (200, 200, 200),
(0, 0, self.width, self.height), 2)
# 绘制重力向量
self.draw_gravity_vector()
# 更新物理(如果未暂停)
if not self.paused:
self.physics_engine.update()
# 绘制所有球
for ball in self.physics_engine.balls:
ball.draw(self.screen)
# 绘制统计信息
self.draw_stats()
# 绘制操作指南
self.draw_instructions()
# 更新屏幕
pygame.display.flip()
# 控制帧率
self.clock.tick(60)
# 启动模拟
if __name__ == "__main__":
simulation = BallSimulation()
simulation.run()