I have a working code for buoyancy simulation but it's not behaving properly. I have 3 scenarios and the expected behavior as follows:
To visualize the above expectations (particularly scenario 3), you may refer to this video (https://www.youtube.com/watch?v=Z_vfP_S5wis) and skip to 00:06:00 time frame.
I also have the code as follows:
import pygame
import sys
def get_overlapping_area(rect1, rect2):
overlap_width = min(rect1.right, rect2.right) - max(rect1.left, rect2.left)
overlap_height = min(rect1.bottom, rect2.bottom) - max(rect1.top, rect2.top)
return overlap_width * overlap_height
class Player(pygame.sprite.Sprite):
def __init__(self, pos):
super().__init__()
self.image = pygame.Surface((16, 32))
self.image.fill((0, 0, 0))
self.rect = self.image.get_rect(center=pos)
self.y_vel = 0
def apply_gravity(self):
self.y_vel += GRAVITY
def check_buoyancy_collisions(self):
buoyant_force = 0
for sprite in buoyant_group:
if sprite.rect.colliderect(self.rect):
submerged_area = get_overlapping_area(sprite.rect, self.rect)
buoyant_force -= sprite.buoyancy * GRAVITY * submerged_area
self.y_vel += buoyant_force
def update(self):
self.apply_gravity()
self.check_buoyancy_collisions()
self.rect.top += self.y_vel
def draw(self, screen: pygame.display):
screen.blit(self.image, self.rect)
class Fluid(pygame.sprite.Sprite):
def __init__(self, pos, tile_size):
super().__init__()
self.image = pygame.Surface((tile_size, tile_size))
self.image.fill((0, 255, 255))
self.rect = self.image.get_rect(topleft=pos)
self.buoyancy = 0.00225
pygame.init()
WIDTH, HEIGHT = 500, 700
screen = pygame.display.set_mode((WIDTH, HEIGHT))
TILE_SIZE = 32
GRAVITY = 0.05
buoyant_group = pygame.sprite.Group()
player_y = HEIGHT * 3 // 4 # player drop point few pixels below fluid surface
# player_y = HEIGHT // 4 # player drop point at fluid surface
# player_y = HEIGHT // 8 # player drop point few pixels above fluid surface
# Instantiate Player and Fluid objects
player = Player((WIDTH // 2, player_y))
for r in range(HEIGHT // 4, HEIGHT * 2, TILE_SIZE):
for c in range(0, WIDTH, TILE_SIZE):
buoyant_group.add(Fluid(pos=(c, r), tile_size=TILE_SIZE))
# Game Loop
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
player.update()
screen.fill((100, 100, 100))
buoyant_group.draw(surface=screen)
player.draw(screen=screen)
pygame.display.update()
Few explanations for the code:
get_overlapping_area function : The buoyant force is computed as fluid weight density x displaced fluid volume, and since I'm dealing with 2D the overlapping area between the object and the fluid is computed rather than the displaced fluid volume.
Player class : A 16x32 sprite affected by both gravity and buoyant force. It has a check_buoyancy_collisions function responsible for the computation of the upward force on the object.
Fluid class : A 32x32 sprite responsible for the buoyancy. It has the self.buoyancy attribute calculated enough to overcome the Player object's weight due to gravity, thus an upward resultant force acting on the Player object.
player_y varaible : I have provided 3 set-ups to simulate the 3 scenarios mentioned above. You may comment/uncomment them accordingly.
PS: I have been on this problem for a week now. Most resources I found talks about the theory. The remaining few resources uses game engines that does the physics for them.
Finally found the solution, though not as perfect as it should be. I'm no physics expert but based on my research, in addition to the buoyant force, there are three drag forces acting on a moving/submerging body namely (1) skin friction drag, (2) form drag, and (3) interference drag.
To my understanding, skin friction drag acts on the body's surface parallel to the direction of movement; form drag acts on the body's surface perpendicular to the direction of movement; and interference drag which is generated by combination of fluid flows.
In general, these drag forces acts in opposition to the body's movement direction. Thus, for simplicity's sake, we can generalize them into a drag attribute to a specific fluid like so:
class Fluid(pygame.sprite.Sprite):
def __init__(self, pos, tile_size):
super().__init__()
self.image = pygame.Surface((tile_size, tile_size))
self.image.fill((0, 255, 255))
self.rect = self.image.get_rect(topleft=pos)
self.buoyancy = 0.00215
self.drag = 0.0000080
The total drag/resistance to the moving/submerging body is then proportional to the body's submerged area (area in contact with the fluid). Finally, the resulting drag force is applied opposite to the direction of the body's movement.
class Player(pygame.sprite.Sprite):
def check_buoyancy_collisions(self):
buoyant_force = 0
drag = 0
for sprite in buoyant_group:
if sprite.rect.colliderect(self.rect):
submerged_area = get_overlapping_area(sprite.rect, self.rect)
buoyant_force -= sprite.buoyancy * GRAVITY * submerged_area
drag += sprite.drag * submerged_area
self.y_vel += buoyant_force
if self.y_vel > 0:
self.y_vel -= drag
else:
self.y_vel += drag
This is only for the movement in y-direction. I believe drag forces should also be applied on the body's movement in x-direction.
Moreover, for a sophisticated simulation, the three types of drags may have to be applied separately.