我正在尝试制作一个相当准确的落沙模拟,但我似乎无法弄清楚如何使沙堆自然倒塌。 它的基本工作原理是,当用户按下鼠标按钮时,一大块沙子会掉下来,当碰到底行时,它会以一种随机的方式堆积起来。但它创建了一个完美的金字塔,我不知道如何解决这个问题。
import pygame as pg
from pygame.locals import *
import time
import random
import numpy as np
pg.init()
# Setup
screenW = 600
screenH = 600
screen = pg.display.set_mode((screenW, screenH))
clock = pg.time.Clock()
bg_color = (85, 35, 65)
black = (0, 0, 0)
white = (255, 255, 255)
# Grid
cell_size = 5
cols = screenW // cell_size
rows = screenH // cell_size
grid = np.zeros((cols, rows), dtype=int)
rect_coords = [] # Keeps track of where the sand is
# \--- CODE SIMPLIFICATION FUNCTIONS ---/
# -> Enhance code clarity and streamlining through functions <-
"""
-> Returns the index of the given coordinates (x, y) in the rect_coords list <-
Parameters:
- x (int): The x-coordinate.
- y (int): The y-coordinate.
Returns:
- int: The index of the coordinates in the rect_coords list.
"""
def index(x, y):
for i, (col, row) in enumerate(rect_coords):
if col == x and row == y:
return i
"""
-> Move a cell in the grid to a new position <-
Note:
- The grid is a two-dimensional array.
Args:
col (int): The column index of the cell to be moved.
row (int): The row index of the cell to be moved.
new_col (int): The new column index for the cell.
"""
def cellMovement(col, row, new_col):
grid[col, row] = 0
del rect_coords[index(col, row)]
grid[new_col, row+1] = 1
rect_coords.append((new_col, row+1))
def randomChoice(values):
return random.choice(values)
"""
-> Decorator that limits the rate at which a function can be invoked <-
"""
def rate_limit(interval):
def decorator(func):
last_invocation = 0
def wrapper(*args, **kwargs):
nonlocal last_invocation
elapsed = time.time() - last_invocation
if elapsed >= interval:
last_invocation = time.time()
return func(*args, **kwargs)
return wrapper
return decorator
"""
-> Moves the sand particles in the grid <-
-> This method iterates through the coordinates of the sand particles in the grid and checks if there is sand below each one.
-> The sand particles fall down one row at a time, and if there is an empty cell below, the sand particle moves to that cell.
-> If the cell below has sand and the diagonal is empty, the sand particle may randomly move to the left or right (depending on which is available).
Note:
- The sand particles are represented by the value 1 in the grid.
- rect_coords is a list containing tuples representing the coordinates of particles in the format (column, row).
"""
def moveSand():
for _, (i, j) in enumerate(rect_coords):
if grid[i, j] == 1:
if j+1 <= rows-1:
below = grid[i, j+1];
if below == 0: # Checking if there's already sand below
cellMovement(i, j, i)
else:
rand = randomChoice([-1, 1])
belowA = grid[i+rand, j+1] if i+rand in range(cols) else 1
belowB = grid[i-rand, j+1] if i-rand in range(cols) else 1
if belowA == 0:
cellMovement(i, j, i+rand)
elif belowB == 0:
cellMovement(i, j, i-rand)
"""
-> Draws the sand on the screen <-
-> This function calls the moveSand() function to update the sand positions,
and then draws each sand rectangle on the screen using pygame's draw.rect() function.
-> Also by iterating through the rect_coords list (which keeps track of the occupied cells)
there's an increase in performance due to optimizing the search process and minimizing redundant computations
"""
def drawSand():
moveSand()
for _, (x, y) in enumerate(rect_coords):
pg.draw.rect(screen, white, ((x * cell_size, y * cell_size), (cell_size, cell_size)))
"""
-> Spawns sand particles on the grid based on the mouse position <-
-> The sand particles are created within a specific radius around the mouse position,
and if a cell, within the radius, is empty, a sand particle is created in it.
Note:
-
"""
@rate_limit(interval=0.05) # the bigger the interval the less frequency the sand will spawn
def summonSand():
# The numbers should be around the cell_size value (5), to give an extra flailing/flaming effect without exagerating
# Assigning 1 creates a reminiscent feel of a dying flame rekindling
# Opt for odd numbers to avoid redundancy; even numbers yield the same result due to floor division "//" halving them
radius = randomChoice([3, 5, 7, 9])
sand_radius = radius // 2
mouse_state = pg.mouse.get_pressed()
if mouse_state[0]:
mouseX, mouseY = pg.mouse.get_pos()
for i in range(-sand_radius, sand_radius, 1):
for j in range(-sand_radius, sand_radius, 1):
if randomChoice([True, False]):
col = (mouseX // cell_size) + i
row = (mouseY // cell_size) + j
if col in range(cols) and row in range(rows):
if grid[col, row] == 0:
grid[col, row] = 1 # setting the cell state to 1
rect_coords.append((col, row)) # tracking the current coorfinate the particle
# Main loop
running = True
while running:
for event in pg.event.get():
if event.type == QUIT:
running = False
screen.fill(black)
summonSand()
drawSand()
pg.display.flip()
clock.tick(60)
我前段时间在YouTube视频中看到了这个问题的答案。 用我自己的话来说,答案是,当沙子像素下落并撞击其下方的固体物体时,它会将其垂直速度转换为水平速度,并且不要忘记施加地面摩擦力。这将使沙子在与固体碰撞时散开。还有更多可以实现的方法,如参考视频所示: https://www.youtube.com/watch?v=5Ka3tbbT-9E&t=822s
在实现它时,我还会存储每帧的沙块速度,而不是重新计算它,因为这意味着您可以存储以前的状态信息。