Search code examples
pythonpygamegame-physicsgame-development

How do you simulate buoyancy in games?


I have a working code for buoyancy simulation but it's not behaving properly. I have 3 scenarios and the expected behavior as follows:

  1. Object initial position is already submerged : (a) expected to move up; (b) then down but not beyond its previous submersion depth; (c) repeat a-b until object stops or floats at surface level;
  2. Object initial position is at surface level : (a) remains at that surface level or have a floating effect;
  3. Object initial position above the surface level : (a) object falls down to the water, until it reaches a certain depth; (b) expected to move up; (c) then down but not beyond its previous submersion depth; (d) repeat b-c until object stops or floats at surface level;

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.


Solution

  • 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.