Search code examples
pythonpygamepygame-surfacepygame2

Rotate a rectangle over waves


I am trying to move a rectangle over the waves,trying to simulate a boat sailing. For that, I rotate the rectangle using the height of the drawn lines and calculating the angle they form with the rectangle. However, for some reason, in some points of the waves the rectangle is flickering. The code shows the problem.

import sys
import math
import pygame
from pygame.locals import *

pygame.init()
SCREEN_WIDTH = 900
SCREEN_HEIGHT = 900
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))

clock = pygame.time.Clock()

BLUE = (0, 103, 247)
RED = (255,0,0)

watter_levels = [0 for _ in range(SCREEN_WIDTH)]

def draw_water(surface, dy):
    amplitude = 35
    global watter_levels
    for x in range(SCREEN_WIDTH):
        y = int(math.sin((x)*(0.01)) * amplitude + dy)
        watter_levels[x] = y
        pygame.draw.aaline(surface,BLUE,(x,y),(x,y))

def get_water_level(index):
    if index <= 0:
        return watter_levels[0]
    if index >= SCREEN_WIDTH:
        return watter_levels[-1]
    return watter_levels[index]

font = pygame.font.Font(None,30)
def debug(info,x=10,y=10):
    display_surf = pygame.display.get_surface()
    debug_surface = font.render(str(info),True,'White')
    debug_rect = debug_surface.get_rect(topleft=(x,y))
    pygame.draw.rect(display_surf,'Black',debug_rect)
    display_surf.blit(debug_surface,debug_rect)

class Ship(pygame.sprite.Sprite):
    def __init__(self):
        super().__init__()
        self.image = pygame.Surface((80,80),pygame.SRCALPHA)
        self.image.fill('red')
        self.rect = self.image.get_rect(topleft=(0,0))
        self.copy_img = self.image.copy()
        self.move = pygame.math.Vector2(0,0)
        self.copy_img = self.image.copy()
        self.velocity = 8

    def rotate(self, angle):
        self.image = pygame.transform.rotate(self.copy_img, int(angle))
        self.rect = self.image.get_rect(center=(self.rect.center))

    def update(self):
        self.get_input()
        self.rect.bottom = get_water_level(self.rect.centerx)
        left_y = get_water_level(self.rect.left)
        right_y = get_water_level(self.rect.right)
        angle = 180*math.atan2(left_y-right_y,self.image.get_width())/math.pi
        debug("angle: "+str(int(angle)))
        print(self.rect.left,self.rect.right)
        self.rotate(angle)

    def replicate(self):
        if self.rect.left == 363:
            return
        self.rect.x += 1

    def get_input(self):
        self.replicate()
        # keys = pygame.key.get_pressed()
        # if keys[K_LEFT]:
        #     self.move.x = -self.velocity
        # elif keys[K_RIGHT]:
        #     self.move.x = self.velocity
        # else:
        #     self.move.x = 0
        # if keys[K_UP]:
        #     self.move.y = -self.velocity
        # elif keys[K_DOWN]:
        #     self.move.y = self.velocity
        # else:
        #     self.move.y = 0
        # self.rect.x += self.move.x
        # self.rect.y += self.move.y
        # if self.rect.left <= 0:
        #     self.rect.left = 0
        # if self.rect.right >= SCREEN_WIDTH:
        #     self.rect.right = SCREEN_WIDTH


ship_sprite = pygame.sprite.GroupSingle()
ship_sprite.add(Ship())
while True:
    screen.fill((200,210,255,0))
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()

    ship_sprite.update()
    ship_sprite.draw(screen) 
    draw_water(screen,SCREEN_HEIGHT-300)   
    clock.tick(60)
    pygame.display.update()

Solution

  • It's about accuracy. You can improve the result by storing the water level with floating point precision:

    def draw_water(surface, dy):
        amplitude = 35
        global watter_levels
        for x in range(SCREEN_WIDTH):
            y = math.sin(x * 0.01) * amplitude + dy
            watter_levels[x] = y
            pygame.draw.aaline(surface, BLUE, (x, round(y)), (x, round(y)))
    

    Furthermore you have to get the left and right from the "unrotated" rectangle:

    class Ship(pygame.sprite.Sprite):
        def __init__(self):
            super().__init__()
            self.image = pygame.Surface((80,80),pygame.SRCALPHA)
            self.image.fill('red')
            self.rect = self.image.get_rect(topleft = (0, 0))
            self.copy_img = self.image.copy()
            self.copy_rect = pygame.Rect(self.rect)
            self.move = pygame.math.Vector2(0, 0)
            self.velocity = 8
            
        def rotate(self, angle):
            self.image = pygame.transform.rotate(self.copy_img, round(angle))
            self.rect = self.image.get_rect(center = (self.copy_rect.center))
    
        def update(self):
            self.get_input()
            self.copy_rect.bottom = round(get_water_level(self.copy_rect.centerx))
            left_y = get_water_level(self.copy_rect.left)
            right_y = get_water_level(self.copy_rect.right)
            angle = math.degrees(math.atan2(left_y-right_y, self.copy_img.get_width()))
            debug("angle: " + str(round(angle)))
            print(self.copy_rect.left, self.copy_rect.right)
            self.rotate(angle)
    
        def replicate(self):
            if self.copy_rect.left == 363:
                return
            self.copy_rect.x += 1