旋转正方形与小球
创建于:2025年2月3日
创建于:2025年2月3日
用 Python 编写一个脚本,使一个黄色小球在正方形内弹跳,确保正确处理碰撞检测。让正方形缓慢旋转。用 Python 实现它。确保小球始终保持在正方形内。
下面给出一个完整的 Python 示例,使用 Pygame 来实现这个动画。程序中有一个正方形(绕其中心缓慢旋转)和一个黄色的小球,小球在正方形内弹跳,并且在碰到旋转边界时正确反弹,始终保持在正方形内部。你可以直接运行以下代码:
pythonimport pygame import math import sys # 初始化 Pygame pygame.init() # 设置窗口大小和标题 WIDTH, HEIGHT = 800, 600 screen = pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption("旋转正方形内弹跳的小球") clock = pygame.time.Clock() # 定义正方形参数 square_side = 400 # 正方形边长 square_center = (WIDTH // 2, HEIGHT // 2) angle = 0 # 正方形当前旋转角度(弧度) d_angle = math.radians(0.3) # 每帧旋转的角度 # 定义小球参数 ball_radius = 10 ball_color = (255, 255, 0) # 黄色 # 初始位置设在正方形中心附近(注意:所有位置均为屏幕坐标) ball_pos = [square_center[0] + 100, square_center[1] + 50] # 小球的速度(像素/帧),单位为屏幕坐标 ball_vel = [3.5, 2.8] # 辅助函数:将一个点从世界坐标转换到以正方形中心为原点,并逆时针旋转 angle 后的坐标系中 def world_to_square(p, center, angle): # 将 p 转换为以 center 为原点的坐标 dx = p[0] - center[0] dy = p[1] - center[1] # 旋转 -angle,使正方形变为轴对齐(注意:这里用逆时针旋转公式) cos_a = math.cos(angle) sin_a = math.sin(angle) rx = dx * cos_a + dy * sin_a ry = -dx * sin_a + dy * cos_a return rx, ry # 辅助函数:将坐标从正方形内部(轴对齐)坐标系转换回屏幕坐标 def square_to_world(r, center, angle): rx, ry = r cos_a = math.cos(angle) sin_a = math.sin(angle) # 旋转 angle dx = rx * cos_a - ry * sin_a dy = rx * sin_a + ry * cos_a return (center[0] + dx, center[1] + dy) # 辅助函数:将向量从世界坐标转换到正方形坐标系 def vec_world_to_square(v, angle): vx, vy = v cos_a = math.cos(angle) sin_a = math.sin(angle) rvx = vx * cos_a + vy * sin_a rvy = -vx * sin_a + vy * cos_a return rvx, rvy # 辅助函数:将向量从正方形坐标系转换回世界坐标 def vec_square_to_world(rv, angle): rvx, rvy = rv cos_a = math.cos(angle) sin_a = math.sin(angle) vx = rvx * cos_a - rvy * sin_a vy = rvx * sin_a + rvy * cos_a return vx, vy running = True while running: dt = clock.tick(60) # 限制帧率为 60 FPS for event in pygame.event.get(): if event.type == pygame.QUIT: running = False # 更新正方形旋转角度 angle += d_angle # 更新小球在屏幕坐标中的位置 ball_pos[0] += ball_vel[0] ball_pos[1] += ball_vel[1] # 将小球的位置和速度转换到正方形局部(轴对齐)坐标系 rx, ry = world_to_square(ball_pos, square_center, angle) rvx, rvy = vec_world_to_square(ball_vel, angle) half_side = square_side / 2 # 检查碰撞:因为小球是圆形,我们考虑球心距离边界的距离需大于 ball_radius collided = False # 如果碰到左右边界,则反转水平(局部 x)分量 if rx > half_side - ball_radius and rvx > 0: rvx = -rvx collided = True rx = half_side - ball_radius # 同时将位置调整回边界内 elif rx < -half_side + ball_radius and rvx < 0: rvx = -rvx collided = True rx = -half_side + ball_radius # 如果碰到上下边界,则反转垂直(局部 y)分量 if ry > half_side - ball_radius and rvy > 0: rvy = -rvy collided = True ry = half_side - ball_radius elif ry < -half_side + ball_radius and rvy < 0: rvy = -rvy collided = True ry = -half_side + ball_radius # 将调整后的局部坐标和速度转换回世界坐标 ball_pos = list(square_to_world((rx, ry), square_center, angle)) ball_vel = list(vec_square_to_world((rvx, rvy), angle)) # 绘制部分 screen.fill((0, 0, 0)) # 黑色背景 # 绘制旋转正方形:先计算正方形四个角在世界坐标中的位置 half = square_side / 2 corners = [(-half, -half), (half, -half), (half, half), (-half, half)] world_corners = [square_to_world(corner, square_center, angle) for corner in corners] pygame.draw.polygon(screen, (255, 255, 255), world_corners, 2) # 白色边框 # 绘制小球 pygame.draw.circle(screen, ball_color, (int(ball_pos[0]), int(ball_pos[1])), ball_radius) pygame.display.flip() pygame.quit() sys.exit()
正方形和坐标转换
为了方便碰撞检测,我们将小球的位置和速度从屏幕(世界)坐标转换到以正方形中心为原点且正方形“轴对齐”的局部坐标系中。这样正方形的边就分别位于 x = ±square_side/2
和 y = ±square_side/2
。检测时考虑了小球的半径。
碰撞检测与反弹
在局部坐标中,如果小球的横坐标或纵坐标超出允许范围(即距离边界小于小球半径),就反转对应分量的速度(只在小球正向边界运动时反转),并将小球位置调整到边界上。
正方形的绘制
通过将正方形四个角在局部坐标中的位置转换为世界坐标,实现正方形缓慢旋转的效果。
运行代码后,你会看到一个旋转的正方形和一个在其内部弹跳的黄色小球,碰撞检测和反弹效果均得到了正确处理。
不太符合物理规律,球的速度扩大一倍,弹性系数增加,我希望类似网球那样来会弹,增加重力等物理背景
下面给出一个改进版的 Python 示例,同样使用 Pygame 来实现动画,不过这次在模拟中增加了重力(世界坐标下始终向下的加速度)、调整了初始速度(扩大一倍)以及在碰撞时引入了弹性系数(类似网球那样“会弹”)。
注意: 由于正方形在缓慢旋转,为方便碰撞检测,我们依然将小球的状态(位置和速度)转换到以正方形中心为原点、且正方形“轴对齐”的局部坐标系中进行边界判断。当检测到碰撞时,对相应分量作反向(乘以弹性系数)处理,再转换回世界坐标。
你可以直接运行下面的代码:
pythonimport pygame import math import sys # 初始化 Pygame pygame.init() # 设置窗口大小和标题 WIDTH, HEIGHT = 800, 600 screen = pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption("带重力和弹性网球效果的旋转正方形内弹跳") clock = pygame.time.Clock() # ------------------------------- # 正方形参数 square_side = 400 # 正方形边长 square_center = (WIDTH // 2, HEIGHT // 2) angle = 0 # 正方形当前旋转角度(单位:弧度) d_angle = math.radians(0.3) # 每帧旋转角度 # ------------------------------- # 小球参数 ball_radius = 10 ball_color = (255, 255, 0) # 黄色 # 初始位置设在正方形内 ball_pos = [square_center[0] + 100, square_center[1] + 50] # 初始速度(单位:像素/帧),将之前的速度扩大一倍 ball_vel = [7.0, 5.6] # 物理参数 gravity = 0.2 # 重力加速度(单位:像素/帧^2),始终向下(世界坐标) elasticity = 0.9 # 碰撞时的弹性系数(碰撞后速度分量反向并乘以该系数) # -------------------------------------------------- # 坐标系转换函数 # 将一个点从世界坐标转换到以正方形中心为原点,并逆时针旋转 angle 后的局部坐标系中 def world_to_square(p, center, angle): dx = p[0] - center[0] dy = p[1] - center[1] cos_a = math.cos(angle) sin_a = math.sin(angle) rx = dx * cos_a + dy * sin_a ry = -dx * sin_a + dy * cos_a return rx, ry # 将局部坐标系中的点转换回世界坐标 def square_to_world(r, center, angle): rx, ry = r cos_a = math.cos(angle) sin_a = math.sin(angle) dx = rx * cos_a - ry * sin_a dy = rx * sin_a + ry * cos_a return (center[0] + dx, center[1] + dy) # 将向量从世界坐标转换到局部(正方形轴对齐)坐标系 def vec_world_to_square(v, angle): vx, vy = v cos_a = math.cos(angle) sin_a = math.sin(angle) rvx = vx * cos_a + vy * sin_a rvy = -vx * sin_a + vy * cos_a return rvx, rvy # 将局部坐标系下的向量转换回世界坐标 def vec_square_to_world(rv, angle): rvx, rvy = rv cos_a = math.cos(angle) sin_a = math.sin(angle) vx = rvx * cos_a - rvy * sin_a vy = rvx * sin_a + rvy * cos_a return vx, vy # ------------------------------- running = True while running: dt = clock.tick(60) # 固定帧率 60 FPS # 处理退出事件 for event in pygame.event.get(): if event.type == pygame.QUIT: running = False # 更新正方形的旋转角度 angle += d_angle # --- 在世界坐标下更新小球运动状态 --- # 更新速度(重力始终向下,影响 y 分量) ball_vel[1] += gravity # 更新位置 ball_pos[0] += ball_vel[0] ball_pos[1] += ball_vel[1] # 为了处理碰撞,我们将小球的状态转换到正方形的局部坐标系中 rx, ry = world_to_square(ball_pos, square_center, angle) rvx, rvy = vec_world_to_square(ball_vel, angle) half_side = square_side / 2 # --- 边界检测与碰撞处理(局部坐标系内,正方形边界在 x,y = ±half_side) --- # 注意:考虑小球半径,实际允许区域为 [-(half_side-ball_radius), half_side-ball_radius] # 检查左右边界 if rx > half_side - ball_radius and rvx > 0: rvx = -rvx * elasticity rx = half_side - ball_radius # 调整位置防止穿墙 elif rx < -half_side + ball_radius and rvx < 0: rvx = -rvx * elasticity rx = -half_side + ball_radius # 检查上下边界 if ry > half_side - ball_radius and rvy > 0: rvy = -rvy * elasticity ry = half_side - ball_radius elif ry < -half_side + ball_radius and rvy < 0: rvy = -rvy * elasticity ry = -half_side + ball_radius # 将局部坐标系中的状态转换回世界坐标 ball_pos = list(square_to_world((rx, ry), square_center, angle)) ball_vel = list(vec_square_to_world((rvx, rvy), angle)) # ------------------------------- # 绘制 screen.fill((0, 0, 0)) # 黑色背景 # 绘制旋转正方形(先计算局部四角点,再转换为世界坐标) half = square_side / 2 corners = [(-half, -half), (half, -half), (half, half), (-half, half)] world_corners = [square_to_world(corner, square_center, angle) for corner in corners] pygame.draw.polygon(screen, (255, 255, 255), world_corners, 2) # 绘制小球 pygame.draw.circle(screen, ball_color, (int(ball_pos[0]), int(ball_pos[1])), ball_radius) pygame.display.flip() pygame.quit() sys.exit()
运动学更新
在世界坐标下,每帧对小球的速度增加了向下的重力加速度(gravity
),然后根据当前速度更新位置。
坐标转换与碰撞检测
为了在旋转正方形内进行碰撞判断,将小球的世界坐标(位置与速度)转换到以正方形中心为原点且正方形“轴对齐”的局部坐标系中。此时正方形的四条边分别为 x = ±(half_side-ball_radius)
与 y = ±(half_side-ball_radius)
。若检测到小球超界(并且对应分量正向出界),则将该分量反向(乘以弹性系数 elasticity
)并调整小球的位置。最后再将修正后的状态转换回世界坐标。
初始条件调整
[7.0, 5.6]
(是之前示例中速度的两倍)。0.9
,使得碰撞时能量损失较少,接近网球的弹跳效果。0.2
(像素/帧^2),你可根据需要调整这个参数来获得理想的运动效果。运行代码后,你会看到一个缓慢旋转的正方形,内部有一个受重力影响的黄色小球在来回弹跳,并且每次碰撞时会以较高的弹性反弹。你可以根据需要调整重力、弹性系数以及初始速度等参数以获得不同的物理效果。