Evelyn
Evelyn

Reputation: 192

Pygame water physics not working as intended

So, I have been trying to implement water physics in pygame based on this tutorial

https://gamedevelopment.tutsplus.com/tutorials/make-a-splash-with-dynamic-2d-water-effects--gamedev-236

The issue is, when I implemented this code in pygame, the water decides that instead of having a nice ripple effect, its going to go crazy and wiggle all over the screen until it eventually crashes.

I looked around on some discord server and found someone else had tried to implement the same thing, and had got the same problem. Their code is basically the same as mine just more neatly organized so I will post it instead of mine.

import pygame, random
import math as m

from pygame import *

pygame.init()

WINDOW_SIZE = (854, 480)
 
screen = pygame.display.set_mode(WINDOW_SIZE,0,32) # initiate the window

clock = pygame.time.Clock()

font = pygame.font.SysFont("Arial", 18)

class surface_water_particle():
    def __init__(self, x,y):
        self.x_pos = x
        self.y_pos = y
        self.target_y = y
        self.velocity = 0
        self.k = 0.02
        self.d = 0.02
        self.time = 1

    def update(self):
        x = -(self.target_y - self.y_pos)
        a = -(self.k * x - self.d * self.velocity)

        self.y_pos += self.velocity
        self.velocity += a

class water():
    def __init__(self, x_start, x_end, y_start, y_end, segment_length):
        self.springs = []
        self.x_start = x_start
        self.y_start = y_start
        self.x_end = x_end
        self.y_end = y_end - 10
        for i in range(abs(x_end - x_start) // segment_length):
            self.springs.append(surface_water_particle(i * segment_length + x_start, y_end))

    def update(self, spread):
        for i in range(len(self.springs)):
            self.springs[i].update()

        leftDeltas = [0] * len(self.springs)
        rightDeltas = [0] * len(self.springs)

        for i in range(0, len(self.springs) ):
            if i > 0:
                leftDeltas[i] = spread * (self.springs[i].y_pos - self.springs[i - 1].y_pos)
                self.springs[i - 1].velocity += leftDeltas[i]
            if i < len(self.springs) - 1:
                rightDeltas[i] = spread * (self.springs[i].y_pos - self.springs[i + 1].y_pos)
                self.springs[i + 1].velocity += rightDeltas[i]

        for i in range(0, len(self.springs) ):
            if i > 0:
                self.springs[i - 1].velocity += leftDeltas[i]
            if i < len(self.springs) - 1:
                self.springs[i + 1].velocity += rightDeltas[i]
                
                
    def splash(self, index, speed):
        if index > 0 and index < len(self.springs) :
            self.springs[index].velocity = speed
                
    def draw(self):
        water_surface = pygame.Surface((abs(self.x_start - self.x_end), abs(self.y_start - self.y_end))).convert_alpha()
        water_surface.fill((0,0,0,0))
        water_surface.set_colorkey((0,0,0,0))
        polygon_points = []
        polygon_points.append((self.x_start, self.y_start))
        for spring in range(len(self.springs)):
            polygon_points.append((water_test.springs[spring].x_pos, water_test.springs[spring].y_pos))
        polygon_points.append((water_test.springs[len(self.springs) - 1].x_pos, self.y_start))

        #pygame.draw.polygon(water_surface, (0,0,255), polygon_points)
        
        for spring in range(0,len(self.springs) - 1):
            pygame.draw.line(screen, (0,0,255), (water_test.springs[spring].x_pos, water_test.springs[spring].y_pos), (water_test.springs[spring + 1].x_pos, water_test.springs[spring + 1].y_pos), 2)

        #water_surface.set_alpha(100)

        return water_surface

def update_fps():
    fps_text = font.render(str(int(clock.get_fps())), 1, pygame.Color("coral"))
    screen.blit(fps_text, (0,0))

water_test = water(0,800,200,80, 20)

while True:
    screen.fill((255,255,255))
    water_test.update(0.5)
    screen.blit(water_test.draw(), (0,0))
    
    for event in pygame.event.get():
        if event.type == QUIT:
            pygame.quit()
        if event.type == MOUSEBUTTONDOWN:
            water_test.splash(10,0.1)

    pygame.display.update()

    clock.tick(60)

I tried looking for any issues with our implementations of the tutorial but I could not find anything that seemed out of place. I then tried adding a resistance function that would divide the water velocity each time it updates. This however did not fix the issue.

Does anyone know what was done wrong and how to fix this?

Upvotes: 6

Views: 790

Answers (2)

LivelyLiz
LivelyLiz

Reputation: 43

I modified the code to match the example exactly and at least it doesn't explode instantly any more.

The main problem seemed to be the loops with the deltas. Adjusting the code for the spring update and commenting those out lead to a nice oscillation which didn't stop, but also did not explode.

The outer loop seems to be important to ease everything out a bit.

I would think it is because of the added height, as changing the height changes the force of the spring.

import pygame, random
import math as m

from pygame import *

pygame.init()

WINDOW_SIZE = (854, 480)
 
screen = pygame.display.set_mode(WINDOW_SIZE,0,32) # initiate the window

clock = pygame.time.Clock()

font = pygame.font.SysFont("Arial", 18)

class surface_water_particle():
    def __init__(self, x,y):
        self.x_pos = x
        self.y_pos = y
        self.target_y = y
        self.velocity = 0
        self.k = 0.02
        self.d = 0.02
        self.time = 1

    def update(self):
        x = self.y_pos - self.target_y #this was overly complicated for some reason
        a = -self.k * x - self.d * self.velocity #here was a wrong sign for the dampening

        self.y_pos += self.velocity
        self.velocity += a

class water():
    def __init__(self, x_start, x_end, y_start, y_end, segment_length):
        self.springs = []
        self.x_start = x_start
        self.y_start = y_start
        self.x_end = x_end
        self.y_end = y_end - 10
        for i in range(abs(x_end - x_start) // segment_length):
            self.springs.append(surface_water_particle(i * segment_length + x_start, y_end))

    def update(self, spread):
        for i in range(len(self.springs)):
            self.springs[i].update()

        leftDeltas = [0] * len(self.springs)
        rightDeltas = [0] * len(self.springs)

        # adding this outer loop gave the real success
        for j in range(0,8):

            for i in range(0, len(self.springs) ):
                if i > 0:
                    leftDeltas[i] = spread * (self.springs[i].y_pos - self.springs[i - 1].y_pos)
                    self.springs[i - 1].velocity += leftDeltas[i]
                if i < len(self.springs) - 1:
                    rightDeltas[i] = spread * (self.springs[i].y_pos - self.springs[i + 1].y_pos)
                    self.springs[i + 1].velocity += rightDeltas[i]

            # here you used velocity instead of height before
            for i in range(0, len(self.springs) ):
                if i > 0:
                    self.springs[i - 1].y_pos += leftDeltas[i]
                if i < len(self.springs) - 1:
                    self.springs[i + 1].y_pos += rightDeltas[i]
                
                
    def splash(self, index, speed):
        if index > 0 and index < len(self.springs) :
            self.springs[index].velocity = speed
                
    def draw(self):
        water_surface = pygame.Surface((abs(self.x_start - self.x_end), abs(self.y_start - self.y_end))).convert_alpha()
        water_surface.fill((0,0,0,0))
        water_surface.set_colorkey((0,0,0,0))
        polygon_points = []
        polygon_points.append((self.x_start, self.y_start))
        for spring in range(len(self.springs)):
            polygon_points.append((water_test.springs[spring].x_pos, water_test.springs[spring].y_pos))
        polygon_points.append((water_test.springs[len(self.springs) - 1].x_pos, self.y_start))

        #pygame.draw.polygon(water_surface, (0,0,255), polygon_points)
        
        for spring in range(0,len(self.springs) - 1):
            pygame.draw.line(screen, (0,0,255), (water_test.springs[spring].x_pos, water_test.springs[spring].y_pos), (water_test.springs[spring + 1].x_pos, water_test.springs[spring + 1].y_pos), 2)

        #water_surface.set_alpha(100)

        return water_surface

def update_fps():
    fps_text = font.render(str(int(clock.get_fps())), 1, pygame.Color("coral"))
    screen.blit(fps_text, (0,0))

water_test = water(0,800,200,80, 20)

while True:
    screen.fill((255,255,255))
    water_test.update(0.5)
    screen.blit(water_test.draw(), (0,0))
    
    for event in pygame.event.get():
        if event.type == QUIT:
            pygame.quit()
        if event.type == MOUSEBUTTONDOWN:
            water_test.splash(10,0.1)

    pygame.display.update()

    clock.tick(60)

Generally, what you want is that the acceleration goes in the opposite direction of the height difference (it wants to restore the original position). The velocity should decreases and eventually get back to 0.

With the original code you had, even without the loops that perform the spread, the velocity increased instead of decreased.

The velocity will probably never reach 0, so you should probably also add a threshold where you will just set it to 0 so avoid flickering.

For debugging, always print your values and have a look if they make sense. In this case you would have noticed the velocity goes up even for the single spring update step.

Edit: This answer fixes the issue but does not tweak parameters to make it look good. See Jeff H.'s answer for a good choice of parameters.

Upvotes: 2

AirSquid
AirSquid

Reputation: 11883

Many of the same observations as the other post. I commented a few things up, changed your velocity statement a bit. And in the "spreading" I added the passes.

Tinkered with the numbers a bit, and got some real nice splashes. (better than the accepted solution! :( lol.

If you need to troubleshoot this some more, I would suggest you comment out the spreading piece (or give it a value of zero) and you can check the springiness of the water by itself. This made it much easier to T/S.

import pygame, random
import math as m

from pygame import *

pygame.init()

WINDOW_SIZE = (854, 480)
 
screen = pygame.display.set_mode(WINDOW_SIZE,0,32) # initiate the window

clock = pygame.time.Clock()

font = pygame.font.SysFont("Arial", 18)

class surface_water_particle():
    k = 0.04  # spring constant
    d = 0.08  # damping constant
    def __init__(self, x,y):
        self.x_pos = x
        self.y_pos = y
        self.target_y = y
        self.velocity = 0

    def update(self):
        x = self.y_pos - self.target_y              # displacement of "spring"
        a = -self.k * x - self.d * self.velocity    # unit of acceleration

        self.y_pos += self.velocity
        self.velocity += a

class water():
    
    def __init__(self, x_start, x_end, y_start, y_end, segment_length):
        self.springs = []
        self.x_start = x_start
        self.y_start = y_start
        self.x_end = x_end
        self.y_end = y_end - 10
        for i in range(abs(x_end - x_start) // segment_length):
            self.springs.append(surface_water_particle(i * segment_length + x_start, y_end))

    def update(self, spread):
        passes = 4  # more passes = more splash spreading
        for i in range(len(self.springs)):
            self.springs[i].update() 

        leftDeltas = [0] * len(self.springs)
        rightDeltas = [0] * len(self.springs)
        for p in range(passes):  
            for i in range(0, len(self.springs) -1 ):
                if i > 0:  
                    leftDeltas[i] = spread * (self.springs[i].y_pos - self.springs[i - 1].y_pos)
                    self.springs[i - 1].velocity += leftDeltas[i]
                if i < len(self.springs) - 1:
                    rightDeltas[i] = spread * (self.springs[i].y_pos - self.springs[i + 1].y_pos)
                    self.springs[i + 1].velocity += rightDeltas[i]

            for i in range(0, len(self.springs) -1):
                if i > 0:
                    self.springs[i - 1].y_pos += leftDeltas[i]  # you were updating velocity here!
                if i < len(self.springs) - 1:
                    self.springs[i + 1].y_pos += rightDeltas[i]

               
                
    def splash(self, index, speed):
        if index > 0 and index < len(self.springs) :
            self.springs[index].velocity = speed
                
    def draw(self):
        water_surface = pygame.Surface((abs(self.x_start - self.x_end), abs(self.y_start - self.y_end))).convert_alpha()
        water_surface.fill((0,0,0,0))
        water_surface.set_colorkey((0,0,0,0))
        polygon_points = []
        polygon_points.append((self.x_start, self.y_start))
        for spring in range(len(self.springs)):
            polygon_points.append((water_test.springs[spring].x_pos, water_test.springs[spring].y_pos))
        polygon_points.append((water_test.springs[len(self.springs) - 1].x_pos, self.y_start))

        #pygame.draw.polygon(water_surface, (0,0,255), polygon_points)
        
        for spring in range(0,len(self.springs) - 1):
            pygame.draw.line(screen, (0,0,255), (water_test.springs[spring].x_pos, water_test.springs[spring].y_pos), (water_test.springs[spring + 1].x_pos, water_test.springs[spring + 1].y_pos), 2)

        #water_surface.set_alpha(100)

        return water_surface

def update_fps():
    fps_text = font.render(str(int(clock.get_fps())), 1, pygame.Color("coral"))
    screen.blit(fps_text, (0,0))

water_test = water(0,800,200,200, 3)

while True:
    screen.fill((255,255,255))
    water_test.update(0.025)
    screen.blit(water_test.draw(), (0,0))
    
    for event in pygame.event.get():
        if event.type == QUIT:
            pygame.quit()
        if event.type == MOUSEBUTTONDOWN:
            water_test.splash(50,100)

    pygame.display.update()

    clock.tick(60)

Upvotes: 3

Related Questions