Since character movement is grid based, characters look a bit odd when going from square to square as they just appear from one square onto another. To make the movement feel more natural, I wanted to add "afterimages" so that there would be a simulation of smooth movement.
Demonstrational image:
Since my code has characters moving directly onto the next square, I don't know how to blit sprites in between.
if IDO.get_key(pygame.K_RIGHT):
if PhaseFocus == 0 or PhaseFocus == 2:
Reticlex +=1
if Currently_Selected != 0 and Currently_Selected.Phase == 2:
if Currently_Selected.x != Reticlex:
Currently_Selected.x = Reticlex
if Currently_Selected.x != Reticley:
Currently_Selected.y = Reticley
if IDO.get_key(pygame.K_LEFT):
if PhaseFocus == 0 or PhaseFocus == 2:
Reticlex -=1
if Currently_Selected != 0 and Currently_Selected.Phase == 2:
if Currently_Selected.x != Reticlex:
Currently_Selected.x = Reticlex
if Currently_Selected.x != Reticley:
Currently_Selected.y = Reticley
When the currently selected character is in Phase 2 (carried around) they should have these afterimages.
Let's see how we can do this.
Step 1: Basic setup
We start with an "empty" pygame game, like this:
import pygame
def main():
screen = pygame.display.set_mode((640, 480))
clock = pygame.time.Clock()
while True:
events = pygame.event.get()
for e in events:
if e.type == pygame.QUIT:
return
screen.fill((30, 30, 30))
pygame.display.flip()
clock.tick(60)
if __name__ == '__main__':
main()
We have a Clock, a single game loop and handle the QUIT event. I always store the events from event.get() in a variable in case I want to iterate over them again, e.g. when a Sprite wants to listen for an event. Always call event.get() only once per frame!
Step 2: A grid
We want a grid based game, so let's draw a grid. Also, we store a bunch of Rects in a list so it will be easy for us to lookup the screen coordinates of an object. We want to center objects in their tile, so using the Rect class will do the dirty work for us.
import pygame
TILESIZE = 32
GRID_W, GRID_H = (20, 15)
def create_grid():
surf = pygame.Surface((TILESIZE * GRID_W, TILESIZE * GRID_H))
surf.set_colorkey((2, 2, 2))
surf.fill((2, 2, 2))
grid = []
for y in range(GRID_H):
line = []
for x in range(GRID_W):
r = pygame.Rect(x * TILESIZE, y * TILESIZE, TILESIZE, TILESIZE)
line.append(r)
pygame.draw.rect(surf, pygame.Color('grey'), r, 1)
grid.append(line)
return grid, surf
def main():
screen = pygame.display.set_mode((TILESIZE * GRID_W, TILESIZE * GRID_H))
clock = pygame.time.Clock()
grid, grid_surf = create_grid()
while True:
events = pygame.event.get()
for e in events:
if e.type == pygame.QUIT:
return
screen.fill((30, 30, 30))
screen.blit(grid_surf, (0, 0))
pygame.display.flip()
clock.tick(60)
Step 3: Something's moving on the grid
We now create an actor that travels on this grid. In this case it's a simple Sprite that moves every second.
import pygame
import random
TILESIZE = 32
GRID_W, GRID_H = (20, 15)
LOOKUP = None
class Actor(pygame.sprite.Sprite):
def __init__(self, grid_pos):
super().__init__()
self.image = pygame.Surface((TILESIZE // 2, TILESIZE // 2))
self.rect = self.image.get_rect()
self.pos = pygame.Vector2()
self.update_pos(grid_pos)
self.image.fill(pygame.Color('dodgerblue'))
self.timeout = 1000
def update_pos(self, grid_pos):
self.grid_pos = grid_pos
self.rect.center = get_grid_rect(grid_pos).center
self.pos = pygame.Vector2(self.rect.topleft)
def move_random(self):
d = random.choice([-1, 1])
x, y = self.grid_pos
if random.randint(0, 2):
x += d
else:
y += d
self.update_pos((x, y))
def update(self, events, dt):
self.timeout -= dt
if self.timeout <= 0:
self.timeout = 1000
self.move_random()
def get_grid_rect(pos):
x, y = pos
return LOOKUP[y][x]
def create_grid():
surf = pygame.Surface((TILESIZE * GRID_W, TILESIZE * GRID_H))
surf.set_colorkey((2, 2, 2))
surf.fill((2, 2, 2))
grid = []
for y in range(GRID_H):
line = []
for x in range(GRID_W):
r = pygame.Rect(x * TILESIZE, y * TILESIZE, TILESIZE, TILESIZE)
line.append(r)
pygame.draw.rect(surf, pygame.Color('grey'), r, 1)
grid.append(line)
return grid, surf
def main():
screen = pygame.display.set_mode((TILESIZE * GRID_W, TILESIZE * GRID_H))
dt, clock = 0, pygame.time.Clock()
grid, grid_surf = create_grid()
global LOOKUP
LOOKUP = grid
sprites = pygame.sprite.Group(Actor((9, 6)))
while True:
events = pygame.event.get()
for e in events:
if e.type == pygame.QUIT:
return
sprites.update(events, dt)
screen.fill((30, 30, 30))
screen.blit(grid_surf, (0, 0))
sprites.draw(screen)
pygame.display.flip()
dt = clock.tick(60)
if __name__ == '__main__':
main()
Step 4: Smooth movement
Instead of always drawing the Actor that the center of the tiles, we set a target_grid_pos when we want to move. Each frame, we move the Actor a little bit until we reached our target tile. We use pygame's Vector2 class and it's lerp and distance_to methods.
import pygame
import random
TILESIZE = 32
GRID_W, GRID_H = (20, 15)
LOOKUP = None
class Actor(pygame.sprite.Sprite):
def __init__(self, grid_pos):
super().__init__()
self.image = pygame.Surface((TILESIZE // 2, TILESIZE // 2))
self.rect = self.image.get_rect()
self.pos = pygame.Vector2()
self.update_pos(grid_pos)
self.image.fill(pygame.Color('dodgerblue'))
self.timeout = 1000
def update_pos(self, grid_pos):#
self.target_pos = None
self.target_grid_pos = None
self.grid_pos = grid_pos
self.rect.center = get_grid_rect(grid_pos).center
self.pos = pygame.Vector2(self.rect.center)
def move_random(self):
d = random.choice([-1, 1])
x, y = self.grid_pos
if random.randint(0, 2):
x += d
else:
y += d
self.target_pos = pygame.Vector2(get_grid_rect((x, y)).center)
self.target_grid_pos = (x, y)
def update(self, events, dt):
self.timeout -= dt
if self.timeout <= 0:
self.timeout = 1000
self.move_random()
if self.target_grid_pos:
self.pos = self.pos.lerp(self.target_pos, 0.1)
if self.pos.distance_to(self.target_pos) < 1:
self.update_pos(self.target_grid_pos)
self.rect.center = self.pos
def get_grid_rect(pos):
x, y = pos
return LOOKUP[y][x]
def create_grid():
surf = pygame.Surface((TILESIZE * GRID_W, TILESIZE * GRID_H))
surf.set_colorkey((2, 2, 2))
surf.fill((2, 2, 2))
grid = []
for y in range(GRID_H):
line = []
for x in range(GRID_W):
r = pygame.Rect(x * TILESIZE, y * TILESIZE, TILESIZE, TILESIZE)
line.append(r)
pygame.draw.rect(surf, pygame.Color('grey'), r, 1)
grid.append(line)
return grid, surf
def main():
screen = pygame.display.set_mode((TILESIZE * GRID_W, TILESIZE * GRID_H))
dt, clock = 0, pygame.time.Clock()
grid, grid_surf = create_grid()
global LOOKUP
LOOKUP = grid
sprites = pygame.sprite.Group(Actor((9, 6)))
while True:
events = pygame.event.get()
for e in events:
if e.type == pygame.QUIT:
return
sprites.update(events, dt)
screen.fill((30, 30, 30))
screen.blit(grid_surf, (0, 0))
sprites.draw(screen)
pygame.display.flip()
dt = clock.tick(60)
if __name__ == '__main__':
main()
Step 5: Adding the effect
Every 300 ms, we create a copy of the Actor's image, blit it at the Actor's position, and make sure to delete it after 200ms. In a real game, you probably want to cache the images or use pre-rendered ones.
I switched the Group for LayeredUpdates so we can make sure the original images is always drawn above the "shadows".
I also added an image (from rltiles) instead of a simple rect so it looks nicer in this demo.
import pygame
import random
import gzip
import base64
TILESIZE = 64
GRID_W, GRID_H = (10, 7)
LOOKUP = None
class Shadow(pygame.sprite.Sprite):
def __init__(self, source):
super().__init__()
self._layer = 5
self.image = source.image.copy().convert_alpha()
self.image.fill((0, 0, 200, 100), special_flags=pygame.BLEND_ADD)
self.rect = source.rect.copy()
self.timeout = 200
def update(self, events, dt):
self.timeout -= dt
if self.timeout <= 0:
self.kill()
class Actor(pygame.sprite.Sprite):
def __init__(self, grid_pos):
super().__init__()
self._layer = 10
data, size = gzip.decompress(base64.b64decode(HYDRA)), (64, 64)
self.image = pygame.image.fromstring(data, size, "RGB")
self.image.set_colorkey((71, 108, 108))
self.rect = self.image.get_rect()
self.pos = pygame.Vector2()
self.update_pos(grid_pos)
self.timeout = 1000
self.shadow_timeout = 100
def update_pos(self, grid_pos):#
self.target_pos = None
self.target_grid_pos = None
self.grid_pos = grid_pos
self.rect.center = get_grid_rect(grid_pos).center
self.pos = pygame.Vector2(self.rect.center)
def move_random(self):
d = random.choice([-1, 1])
x, y = self.grid_pos
if random.randint(0, 2):
x += d
else:
y += d
self.target_pos = pygame.Vector2(get_grid_rect((x, y)).center)
self.target_grid_pos = (x, y)
def update(self, events, dt):
self.timeout -= dt
if self.timeout <= 0:
self.timeout = 1000
self.move_random()
if self.target_grid_pos:
self.shadow_timeout -= dt
if self.shadow_timeout <= 0:
self.shadow_timeout = 100
self.groups()[0].add(Shadow(self))
self.pos = self.pos.lerp(self.target_pos, 0.1)
if self.pos.distance_to(self.target_pos) < 1:
self.update_pos(self.target_grid_pos)
self.rect.center = self.pos
def get_grid_rect(pos):
x, y = pos
return LOOKUP[y][x]
def create_grid():
surf = pygame.Surface((TILESIZE * GRID_W, TILESIZE * GRID_H))
surf.set_colorkey((2, 2, 2))
surf.fill((2, 2, 2))
grid = []
for y in range(GRID_H):
line = []
for x in range(GRID_W):
r = pygame.Rect(x * TILESIZE, y * TILESIZE, TILESIZE, TILESIZE)
line.append(r)
pygame.draw.rect(surf, pygame.Color('grey'), r, 1)
grid.append(line)
return grid, surf
def main():
screen = pygame.display.set_mode((TILESIZE * GRID_W, TILESIZE * GRID_H))
dt, clock = 0, pygame.time.Clock()
grid, grid_surf = create_grid()
global LOOKUP
LOOKUP = grid
sprites = pygame.sprite.LayeredUpdates(Actor((4, 3)))
while True:
events = pygame.event.get()
for e in events:
if e.type == pygame.QUIT:
return
sprites.update(events, dt)
screen.fill((30, 30, 30))
screen.blit(grid_surf, (0, 0))
sprites.draw(screen)
pygame.display.flip()
dt = clock.tick(60)
HYDRA = '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'
if __name__ == '__main__':
main()
Related
I am trying to make realistic water in pygame:
This is till now my code:
from random import randint
import pygame
WIDTH = 700
HEIGHT = 500
win = pygame.display.set_mode((WIDTH, HEIGHT))
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
AQUA = 'aqua'
RADIUS = 1
x, y = 0, HEIGHT//2
K = 1
FORCE = 100
VELOCITY = 0.5
run = True
class Molecule:
def __init__(self, x, y, radius, force, k):
self.x = x
self.y = y
self.radius = radius
self.force = force
self.k = k
self.max_amplitude = y + force/k
self.min_amplitude = y - force/k
self.up = False
self.down = True
self.restore = False
def draw(self, win):
pygame.draw.circle(win, BLACK, (self.x, self.y), self.radius)
def oscillate(self):
if self.y <= self.max_amplitude and self.down == True:
self.y += VELOCITY
if self.y == self.max_amplitude or self.up:
self.up = True
self.down = False
self.y -= VELOCITY
if self.y == self.min_amplitude:
self.up = False
self.down = True
molecules = []
for i in range(100):
FORCE = randint(10, 20)
molecules.append(Molecule(x, y, RADIUS, FORCE, K))
x += 10
while run:
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
win.fill(WHITE)
for molecule in molecules:
molecule.draw(win)
molecule.oscillate()
for i in range(len(molecules)):
try:
pygame.draw.line(win, BLACK, (molecules[i].x, molecules[i].y), (molecules[i+1].x, molecules[i+1].y))
pygame.draw.line(win, AQUA, (molecules[i].x, molecules[i].y), (molecules[i+1].x, HEIGHT))
except:
pass
pygame.display.flip()
pygame.quit()
But as may expected the water curve is not smooth:
Look at it:
Sample Img1
I want to connect the two randomly added wave points using a set of circles not line like in this one so that a smooth curve could occur.
And in this way i could add the water color to it such that it will draw aqua lines or my desired color line from the point to the end of screen and all this will end up with smooth water flowing simulation.
Now the question is how could i make the points connect together smoothly into a smooth curve by drawing point circles at relative points?
I suggest sticking the segments with a Bézier curves. Bézier curves can be drawn with pygame.gfxdraw.bezier
Calculate the slopes of the tangents to the points along the wavy waterline:
ts = []
for i in range(len(molecules)):
pa = molecules[max(0, i-1)]
pb = molecules[min(len(molecules)-1, i+1)]
ts.append((pb.y-pa.y) / (pb.x-pa.x))
Use the the tangents to define 4 control points for each segment and draw the curve with pygame.gfxdraw.bezier:
for i in range(len(molecules)-1):
p0 = molecules[i].x, molecules[i].y
p3 = molecules[i+1].x, molecules[i+1].y
p1 = p0[0] + 10, p0[1] + 10 * ts[i]
p2 = p3[0] - 10, p3[1] - 10 * ts[i+1]
pygame.gfxdraw.bezier(win, [p0, p1, p2, p3], 4, BLACK)
Complete example:
from random import randint
import pygame
import pygame.gfxdraw
WIDTH = 700
HEIGHT = 500
win = pygame.display.set_mode((WIDTH, HEIGHT))
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
AQUA = 'aqua'
RADIUS = 1
x, y = 0, HEIGHT//2
K = 1
FORCE = 100
VELOCITY = 0.5
class Molecule:
def __init__(self, x, y, radius, force, k):
self.x = x
self.y = y
self.radius = radius
self.force = force
self.k = k
self.max_amplitude = y + force/k
self.min_amplitude = y - force/k
self.up = False
self.down = True
self.restore = False
def draw(self, win):
pygame.draw.circle(win, BLACK, (self.x, self.y), self.radius)
def oscillate(self):
if self.y <= self.max_amplitude and self.down == True:
self.y += VELOCITY
if self.y == self.max_amplitude or self.up:
self.up = True
self.down = False
self.y -= VELOCITY
if self.y == self.min_amplitude:
self.up = False
self.down = True
molecules = []
for i in range(50):
FORCE = randint(10, 20)
molecules.append(Molecule(x, y, RADIUS, FORCE, K))
x += 20
clock = pygame.time.Clock()
run = True
while run:
clock.tick(100)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
win.fill(WHITE)
for molecule in molecules:
molecule.draw(win)
molecule.oscillate()
ts = []
for i in range(len(molecules)):
pa = molecules[max(0, i-1)]
pb = molecules[min(len(molecules)-1, i+1)]
ts.append((pb.y-pa.y) / (pb.x-pa.x))
for i in range(len(molecules)-1):
p0 = molecules[i].x, molecules[i].y
p3 = molecules[i+1].x, molecules[i+1].y
p1 = p0[0] + 10, p0[1] + 10 * ts[i]
p2 = p3[0] - 10, p3[1] - 10 * ts[i+1]
pygame.gfxdraw.bezier(win, [p0, p1, p2, p3], 4, BLACK)
for i in range(len(molecules)-1):
pygame.draw.line(win, AQUA, (molecules[i].x, molecules[i].y), (molecules[i].x, HEIGHT))
pygame.display.flip()
pygame.quit()
If you want to "fill" the water, you must calculate the points along the Bézier line and draw a filled polygon. How to calculate a Bézier curve is explained in Trying to make a Bezier Curve on PyGame library How Can I Make a Thicker Bezier in Pygame? and "X". You can use the following function:
def ptOnCurve(b, t):
q = b.copy()
for k in range(1, len(b)):
for i in range(len(b) - k):
q[i] = (1-t) * q[i][0] + t * q[i+1][0], (1-t) * q[i][1] + t * q[i+1][1]
return round(q[0][0]), round(q[0][1])
def bezier(b, samples):
return [ptOnCurve(b, i/samples) for i in range(samples+1)]
Use the bezier to stitch the wavy water polygon:
ts = []
for i in range(len(molecules)):
pa = molecules[max(0, i-1)]
pb = molecules[min(len(molecules)-1, i+1)]
ts.append((pb.y-pa.y) / (pb.x-pa.x))
pts = [(WIDTH, HEIGHT), (0, HEIGHT)]
for i in range(len(molecules)-1):
p0 = molecules[i].x, molecules[i].y
p3 = molecules[i+1].x, molecules[i+1].y
p1 = p0[0] + 10, p0[1] + 10 * ts[i]
p2 = p3[0] - 10, p3[1] - 10 * ts[i+1]
pts += bezier([p0, p1, p2, p3], 4)
Draw the polygon with pygame.draw.polygon():
pygame.draw.polygon(win, AQUA, pts)
Complete example:
from random import randint
import pygame
class Node:
def __init__(self, x, y, force, k, v):
self.x = x
self.y = y
self.y0 = y
self.force = force
self.k = k
self.v = v
self.direction = 1
def oscillate(self):
self.y += self.v * self.direction
if self.y0 - self.force / self.k > self.y or self.y0 + self.force / self.k < self.y:
self.direction *= -1
def draw(self, surf):
pygame.draw.circle(surf, "black", (self.x, self.y), 3)
window = pygame.display.set_mode((700, 500))
clock = pygame.time.Clock()
width, height = window.get_size()
no_of_nodes = 25
dx = width / no_of_nodes
nodes = [Node(i*dx, height//2, randint(15, 30), 1, 0.5) for i in range(no_of_nodes+1)]
def ptOnCurve(b, t):
q = b.copy()
for k in range(1, len(b)):
for i in range(len(b) - k):
q[i] = (1-t) * q[i][0] + t * q[i+1][0], (1-t) * q[i][1] + t * q[i+1][1]
return round(q[0][0]), round(q[0][1])
def bezier(b, samples):
return [ptOnCurve(b, i/samples) for i in range(samples+1)]
run = True
while run:
clock.tick(100)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
for molecule in nodes:
molecule.oscillate()
ts = []
for i in range(len(nodes)):
pa = nodes[max(0, i-1)]
pb = nodes[min(len(nodes)-1, i+1)]
ts.append((pb.y-pa.y) / (pb.x-pa.x))
pts = [(width, height), (0, height)]
for i in range(len(nodes)-1):
p0 = nodes[i].x, nodes[i].y
p3 = nodes[i+1].x, nodes[i+1].y
p1 = p0[0] + 10, p0[1] + 10 * ts[i]
p2 = p3[0] - 10, p3[1] - 10 * ts[i+1]
pts += bezier([p0, p1, p2, p3], 4)
window.fill("white")
pygame.draw.polygon(window, 'aqua', pts)
for molecule in nodes:
molecule.draw(window)
pygame.display.flip()
pygame.quit()
exit()
I'm not managing to get this math correct, and it's a little bit difficult to explain in words. I have managed to create a isometric grid, which you can select the tiles with the mouse perfectly, and I have managed to implement a camera movement using wasd keys and still get the tiles correctly selected, but there is a slightly bug which I can not figure out where is coming from.
This is what happens, but only sometimes, depend where the camera offset is:
when this happens, it is only on the x axis, and not in every tile.
I'm almost giving up on this cause I can't find the bug, thought of posting here to see if anyone had similar problem.
import time
import pygame
import sys
import math
from os import path
from settings import *
from sprites import *
# ------------------------- SETTINGS ---------------------------- #
# COLORS (r, g, b)
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
DARKGREY = (40, 40, 40)
LIGHTGREY = (100, 100, 100)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
RED = (255, 0, 0)
YELLOW = (255, 255, 0)
# game settings
WIDTH = 1024
HEIGHT = 768
FPS = 60
title = "Isometric-Based game"
BGCOLOUR = DARKGREY
TILE_X = 80
TILE_Y = 40
WORLD_X, WORLD_Y = 14, 10
ORIGIN_X, ORIGIN_Y = 5, 1
# Debug
pygame.init()
font = pygame.font.Font(None, 25)
CAMERA_SPEED = 300
def get_info(info_list):
display_surface = pygame.display.get_surface()
for i, key in enumerate(info_list):
text = font.render(str(key) + " : " + str(info_list[key]), True, (255, 255, 255), (0, 0, 0))
text_rect = text.get_rect()
text_rect.y = 20 * i
display_surface.blit(text, text_rect)
# ------------------------- SPRITES ---------------------------- #
class Camera:
def __init__(self, game, x, y):
self.game = game
self.x, self.y = self.game.to_screen(x, y)
self.vx, self.vy = 0, 0
def update(self):
self.get_keys()
self.x += self.vx * self.game.dt
self.y += self.vy * self.game.dt
def get_keys(self):
self.vx, self.vy = 0, 0
keys = pygame.key.get_pressed()
if keys[pygame.K_w]:
self.vy = -CAMERA_SPEED
if keys[pygame.K_s]:
self.vy = CAMERA_SPEED
if keys[pygame.K_a]:
self.vx = -CAMERA_SPEED
if keys[pygame.K_d]:
self.vx = CAMERA_SPEED
if self.vx != 0 and self.vy != 0:
self.vx *= 1.0
self.vy *= 0.50
class MouseSelection:
def __init__(self, game, image):
self.game = game
self.image = image
def update(self):
# get mouse x and y
self.mouse_x, self.mouse_y = pygame.mouse.get_pos()
# get the mouse offset position inside the tile
self.offset_x, self.offset_y = self.mouse_x % TILE_X, self.mouse_y % TILE_Y
self.offset_x += self.game.scroll_x % TILE_X # Add camera scroll to offset
self.offset_y += self.game.scroll_y % TILE_Y
# get the cell number
self.cell_x, self.cell_y = (self.mouse_x // TILE_X), (self.mouse_y // TILE_Y)
self.cell_x += int((self.game.scroll_x // TILE_X)) # Add camera scroll to cell
self.cell_y += int((self.game.scroll_y // TILE_Y))
# get the selected cell in iso grid
self.selected_x = (self.cell_y - ORIGIN_Y) + (self.cell_x - ORIGIN_X)
self.selected_y = (self.cell_y - ORIGIN_Y) - (self.cell_x - ORIGIN_X)
# height and width of a quarter of a tile, select the corner of the tile to nodge to a direction
h, w = TILE_Y / 2, TILE_X / 2
if self.offset_y < (h / w) * (w - self.offset_x):
self.selected_x -= 1
if self.offset_y > (h / w) * self.offset_x + h:
self.selected_y += 1
if self.offset_y < (h / w) * self.offset_x - h:
self.selected_y -= 1
if self.offset_y > (h / w) * (2 * w - self.offset_x) + h:
self.selected_x += 1
# translate the selected cell to world coordinate
self.selectedWorld_x, self.selectedWorld_y = self.game.to_screen(self.selected_x, self.selected_y)
def draw(self):
# Draw the selected tile with the camera scroll offset
self.game.screen.blit(self.image, (self.selectedWorld_x - self.game.scroll_x,
self.selectedWorld_y - self.game.scroll_y))
class SpriteSheet:
def __init__(self, image):
self.image = image
self.frames = []
def get_image(self):
for row in range(2):
for col in range(4):
if row == 0:
image = pygame.Surface((TILE_Y, TILE_Y / 2)).convert_alpha()
image.blit(self.image, (0, 0), (col * TILE_X / 2, row * TILE_Y / 2, TILE_X, TILE_Y))
image = pygame.transform.scale(image, (TILE_X, TILE_Y))
else:
image = pygame.Surface((TILE_Y, TILE_Y)).convert_alpha()
image.blit(self.image, (0, 0), (col * TILE_X / 2, row * TILE_Y / 2, TILE_X, TILE_Y * 2))
image = pygame.transform.scale(image, (TILE_X, TILE_Y * 2))
image.set_colorkey(WHITE)
self.frames.append(image)
return self.frames
# ------------------------- GAME LOOP ---------------------------- #
class Game:
def __init__(self):
pygame.init()
self.screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption(title)
self.clock = pygame.time.Clock()
pygame.key.set_repeat(400, 100)
self.debug = {}
self.sprite_sheet_image = pygame.image.load("isometric_whitebg - Copy.png")
self.index = 1
self.scroll_x, self.scroll_y = 0, 0
def new(self):
# initialize all variables and do all the setup for a new game
self.sprite_sheet = SpriteSheet(self.sprite_sheet_image)
self.tile_selected = self.sprite_sheet.get_image()[0]
self.tiles = self.sprite_sheet.get_image()
self.mouse_selection = MouseSelection(self, self.tile_selected)
self.camera = Camera(self, 1, 1)
def run(self):
# game loop - set self.playing = False to end the game
self.playing = True
while self.playing:
self.dt = self.clock.tick(FPS) / 1000
self.events()
self.update()
self.draw()
def quit(self):
pygame.quit()
sys.exit()
def update(self):
# update portion of the game loop
self.camera.update()
self.mouse_selection.update()
self.mx, self.my = pygame.mouse.get_pos()
# -------------------------------------------------- CAMERA SCROLLING ----------------------------------------#
if self.camera.x - self.scroll_x != WIDTH / 2:
self.scroll_x += (self.camera.x - (self.scroll_x + WIDTH / 2)) / 10
if self.camera.y - self.scroll_y != HEIGHT / 2:
self.scroll_y += (self.camera.y - (self.scroll_y + HEIGHT / 2)) / 10
# -------------------------------------------------- CAMERA SCROLLING ----------------------------------------#
self.debug_info()
def to_screen(self, x, y):
screen_x = (ORIGIN_X * TILE_X) + (x - y) * (TILE_X / 2)
screen_y = (ORIGIN_Y * TILE_Y) + (x + y) * (TILE_Y / 2)
return screen_x, screen_y
def draw_world(self):
for y in range(WORLD_Y):
for x in range(WORLD_X):
vWorld_x, vWorld_y = self.to_screen(x, y)
# Invisible tile
if self.index == 0:
self.screen.blit(self.tiles[1], (vWorld_x, vWorld_y))
# Grass
elif self.index == 1:
self.screen.blit(self.tiles[2], (vWorld_x - self.scroll_x, vWorld_y - self.scroll_y))
def draw(self):
self.screen.fill(BGCOLOUR)
self.draw_world()
self.mouse_selection.draw()
get_info(self.debug)
pygame.display.flip()
def debug_info(self):
self.debug["FPS"] = int(self.clock.get_fps())
self.debug["Cell"] = self.mouse_selection.cell_x, self.mouse_selection.cell_y
self.debug["Selected"] = int(self.mouse_selection.selected_x), int(self.mouse_selection.selected_y)
self.debug["Scroll"] = int(self.scroll_x), int(self.scroll_y)
self.debug["Mouse"] = int(self.mx), int(self.my)
self.debug["Mouse_offset"] = int(self.mouse_selection.offset_x), int(self.mouse_selection.offset_y)
def events(self):
# catch all events here
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.quit()
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
self.quit()
if event.type == pygame.MOUSEBUTTONDOWN:
if event.button == 1:
pass
game = Game()
while True:
game.new()
game.run()
Define the corner points of the map:
map_outline = [
pygame.math.Vector2(left_x, left_y),
pygame.math.Vector2(top_x, top_y),
pygame.math.Vector2(right_x, right_y,
pygame.math.Vector2(bottom_x, bottom_y)
]
With this information you can calculate the x and y axis of the map:
origin = map_outline[0]
x_axis = (map_outline[1] - map_outline[0]) / columns
y_axis = (map_outline[3] - map_outline[0]) / rows
You can use the x-axis and the y-axis to calculate a point in the map as a function of the row and column:
def transform(p, mat2x2):
x = p[0] * mat2x2[0][0] + p[1] * mat2x2[1][0]
y = p[0] * mat2x2[0][1] + p[1] * mat2x2[1][1]
return pygame.math.Vector2(x, y)
p_position = transform((column + 0.5, row + 0.5), (x_axis, y_axis)) + origin
If you want to get the row and column depending on the mouse cursor, you need to do the opposite. You need to calculate the inverse 2x2 matrix from the x and y axis. Using the inverse matrix, you can calculate the row and column as a function of a point on the map:
def inverseMat2x2(m):
a, b, c, d = m[0].x, m[0].y, m[1].x, m[1].y
det = 1 / (a*d - b*c)
return [(d*det, -b*det), (-c*det, a*det)]
m_pos = pygame.mouse.get_pos()
m_grid_pos = transform(pygame.math.Vector2(m_pos) - origin, point_to_grid)
m_col, m_row = int(m_grid_pos[0]), int(m_grid_pos[1])
Also see PyGameExamplesAndAnswers - Isometric
Minimal example:
replit.com/#Rabbid76/Pygame-IsometircMap
import pygame
pygame.init()
window = pygame.display.set_mode((500, 300))
clock = pygame.time.Clock()
colors = {'g': (40, 128, 40), 'd': (90, 60, 40)}
tilemap = [
'gdddg',
'dgddd',
'ggddg',
'ggddg',
'ddddg',
'dgggd'
]
columns, rows = len(tilemap[0]), len(tilemap)
isometric_tiles = {}
for key, color in colors.items():
tile_surf = pygame.Surface((50, 50), pygame.SRCALPHA)
tile_surf.fill(color)
tile_surf = pygame.transform.rotate(tile_surf, 45)
isometric_size = tile_surf.get_width()
tile_surf = pygame.transform.scale(tile_surf, (isometric_size, isometric_size//2))
isometric_tiles[key] = tile_surf
tile_size = (isometric_size, isometric_size//2)
def tileRect(column, row, tile_size):
x = (column + row) * tile_size[0] // 2
y = ((columns - column - 1) + row) * tile_size[1] // 2
return pygame.Rect(x, y, *tile_size)
game_map = pygame.Surface(((columns+rows) * isometric_size // 2, (columns+rows) * isometric_size // 4), pygame.SRCALPHA)
for column in range(columns):
for row in range(rows):
tile_surf = isometric_tiles[tilemap[row][column]]
tile_rect = tileRect(column, row, tile_size)
game_map.blit(tile_surf, tile_rect)
map_rect = game_map.get_rect(center = window.get_rect().center)
map_outline = [
pygame.math.Vector2(0, columns * isometric_size / 4),
pygame.math.Vector2(columns * isometric_size / 2, 0),
pygame.math.Vector2((columns+rows) * isometric_size // 2, rows * isometric_size / 4),
pygame.math.Vector2(rows * isometric_size / 2, (columns+rows) * isometric_size // 4)
]
for pt in map_outline:
pt += map_rect.topleft
origin = map_outline[0]
x_axis = (map_outline[1] - map_outline[0]) / columns
y_axis = (map_outline[3] - map_outline[0]) / rows
def inverseMat2x2(m):
a, b, c, d = m[0].x, m[0].y, m[1].x, m[1].y
det = 1 / (a*d - b*c)
return [(d*det, -b*det), (-c*det, a*det)]
point_to_grid = inverseMat2x2((x_axis, y_axis))
def transform(p, mat2x2):
x = p[0] * mat2x2[0][0] + p[1] * mat2x2[1][0]
y = p[0] * mat2x2[0][1] + p[1] * mat2x2[1][1]
return pygame.math.Vector2(x, y)
font = pygame.font.SysFont(None, 30)
textO = font.render("O", True, (255, 255, 255))
textX = font.render("X", True, (255, 0, 0))
textY = font.render("Y", True, (0, 255, 0))
p_col, p_row = 2, 2
run = True
while run:
clock.tick(100)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_a and p_col > 0:
p_col -= 1
if event.key == pygame.K_d and p_col < columns-1:
p_col += 1
if event.key == pygame.K_w and p_row > 0:
p_row -= 1
if event.key == pygame.K_s and p_row < rows-1:
p_row += 1
p_position = transform((p_col + 0.5, p_row + 0.5), (x_axis, y_axis)) + origin
m_pos = pygame.mouse.get_pos()
m_grid_pos = transform(pygame.math.Vector2(m_pos) - origin, point_to_grid)
m_col, m_row = int(m_grid_pos[0]), int(m_grid_pos[1])
window.fill((0, 0, 0))
window.blit(game_map, map_rect)
pygame.draw.lines(window, (164, 164, 164), True, map_outline, 3)
pygame.draw.line(window, (255, 0, 0), origin, origin+x_axis, 3)
pygame.draw.line(window, (0, 255, 0), origin, origin+y_axis, 3)
pygame.draw.circle(window, (255, 255, 255), origin, 5)
pygame.draw.circle(window, (255, 0, 0), origin+x_axis, 5)
pygame.draw.circle(window, (0, 255, 0), origin+y_axis, 5)
window.blit(textO, textO.get_rect(topright = origin+(-5, 5)))
window.blit(textX, textX.get_rect(bottomright = origin+x_axis+(-5, -5)))
window.blit(textY, textX.get_rect(topright = origin+y_axis+(-5, 5)))
pygame.draw.ellipse(window, (255, 255, 0), (p_position[0]-16, p_position[1]-8, 32, 16))
if 0 <= m_grid_pos[0] < columns and 0 <= m_grid_pos[1] < rows:
tile_rect = tileRect(m_col, m_row, tile_size).move(map_rect.topleft)
pts = [tile_rect.midleft, tile_rect.midtop, tile_rect.midright, tile_rect.midbottom]
pygame.draw.lines(window, (255, 255, 255), True, pts, 4)
pygame.display.update()
pygame.quit()
exit()
This question already has answers here:
Pygame doesn't let me use float for rect.move, but I need it
(2 answers)
Problem with Pygame movement acceleration, platformer game
(1 answer)
Closed 1 year ago.
I tried to make the enemies(yellow box) follow the player(red box) with a constant velocity from all directions.
The problem of my code is the speed of the enemy from x+(Right) and y+(Bottom) axis to the player is slower than x-(Left) and y-(Above).
Image: https://i.stack.imgur.com/qJNp7.png
I think the problem is in here. I can't find it. It may be somewhere else.
for enemy_types in Global_Obj[2]:
zetas = math.sqrt((enemy_types.rect.center[1] - FirstPlayer.rect.center[1])**2 + (FirstPlayer.rect.center[0] - enemy_types.rect.center[0])**2)
print(enemy_types.rect.center)
x,y = enemy_types.rect.center
x += dt * enemy_types.speed * (FirstPlayer.rect.center[0] - enemy_types.rect.center[0]) / zetas
y += dt * enemy_types.speed * (FirstPlayer.rect.center[1] - enemy_types.rect.center[1]) / zetas
enemy_types.rect.center = (x,y)
Here is all of my code.
# -*- coding: utf-8 -*-
"""
Created on Wed Sep 8 21:56:06 2021
#author: Toon
"""
import pygame
import cv2
import numpy as np
import math
import random
import time
dt = 0.01
window = (1280, 720)
win = pygame.display.set_mode(window)
background = pygame.Surface(window)
pygame.display.set_caption("First Game")
run = True
class playerIO():
def __init__(self):
self.name = 'Player'
self.width = 30
self.height = 30
self.speed = 1000
self.color = (255,0,0)
self.HPwidth = 50
self.HPheight = 8
self.HPoffset = 10
self.MAXHP = 1000
self.HP = self.MAXHP
self.HPregeneration_per_sec = 5
self.MANAwidth = 50
self.MANAheight = 8
self.MANAoffset = 10
self.MAXMANA = 3000
self.MANA = self.MAXMANA
self.MANAregeneration_per_sec = 10
self.image = pygame.Surface([self.width, self.height])
self.image.fill(self.color)
self.rect = self.image.get_rect(topleft=(80, 80))
self.bullet_delay = [0, 0, 0]
def draw(self):
self.HPbar_high = self.rect.y-self.HPheight-self.HPoffset
self.MANAbar_high = self.HPbar_high-self.MANAheight-self.MANAoffset
pygame.draw.rect(win, self.color, self.rect)
pygame.draw.rect(win, (255,255,255), (self.rect.x + self.width/2 - self.HPwidth/2, self.HPbar_high, self.HPwidth, self.HPheight))
pygame.draw.rect(win, (0,255,0), (self.rect.x + self.width/2 - self.HPwidth/2, self.HPbar_high, self.HPwidth*self.HP/self.MAXHP, self.HPheight))
pygame.draw.rect(win, (255,255,255), (self.rect.x + self.width/2 - self.MANAwidth/2, self.MANAbar_high, self.MANAwidth, self.MANAheight))
pygame.draw.rect(win, (0,0,255), (self.rect.x + self.width/2 - self.MANAwidth/2, self.MANAbar_high, self.MANAwidth*self.MANA/self.MAXMANA, self.MANAheight))
class BulletIO():
def __init__(self):
self.MANA_usage = 5
self.zeta = 0
self.width = 15
self.height = 15
self.speed = 1600
self.damage = 8
self.color = (0,255,0)
self.image = pygame.Surface([self.width, self.height])
self.image.fill(self.color)
self.rect = self.image.get_rect()
self.reload_delay = 0.2 #shot delay
def variant(self,X,Y, Zeta):
self.rect.x = X
self.rect.y = Y
self.zeta = Zeta
def draw(self):
pygame.draw.rect(win, self.color, self.rect)
class LaserIO():
def __init__(self):
self.MANA_usage = 10
self.zeta = 0
self.width = 8
self.height = 8
self.speed = 500
self.damage = 20
self.color = (0,0,160)
self.image = pygame.Surface([self.width, self.height])
self.image.fill(self.color)
self.rect = self.image.get_rect()
self.reload_delay = 0 #shot delay
def variant(self,X,Y, Zeta):
self.rect.x = X
self.rect.y = Y
self.zeta = Zeta
def draw(self):
pygame.draw.rect(win, self.color, self.rect)
class LaserBeamIO():
def __init__(self):
self.MANA_usage_per_sec = 1000
self.zeta = 0
self.width = 8
self.height = 8
self.speed = 500
self.damage = 20
self.color = (0,160,160)
self.image = pygame.Surface([self.width, self.height])
self.image.fill(self.color)
self.rect = self.image.get_rect()
self.reload_delay = 5 #shot delay
self.charge_time = 0
self.beam_distance = 60
self.max_charge_time = 10
self.charge_size_per_sec = 20
self.charge_damage_per_sec = 100
def charge_beam(self, player, Zeta):
self.charge_time+=dt
self.width += self.charge_size_per_sec * dt
self.height += self.charge_size_per_sec * dt
self.damage += self.charge_damage_per_sec * dt
self.image = pygame.Surface([self.width, self.height])
self.image.fill(self.color)
self.rect = self.image.get_rect()
self.rect.center = (player[0] + self.beam_distance * math.cos(Zeta), player[1] + self.beam_distance * math.sin(Zeta))
self.zeta = Zeta
def draw(self):
pygame.draw.rect(win, self.color, self.rect)
class EnemyIO():
def __init__(self):
self.name = 'enemy'
self.width = 15
self.height = 15
self.zeta = 0
self.speed = 200
self.color = (255,255,0)
self.MAXHP = 16
self.regeneration = 0.2
self.HPwidth = 20
self.HPheight = 5
self.HPoffset = 8
self.HP = self.MAXHP
self.damage = 10
self.image = pygame.Surface([self.width, self.height])
self.image.fill(self.color)
self.rect = self.image.get_rect(topleft=(20, 20))
def randompos(self, spawn_offset = 200):
while 1 :
try:
rand_pole = random.randint(1,4)
if rand_pole == 1 or rand_pole == 2:
self.rect.x = random.randint(FirstPlayer.rect.x+1+spawn_offset, window[0]-self.width)
else:
self.rect.x = random.randint(1,FirstPlayer.rect.x-spawn_offset)
if rand_pole == 1 or rand_pole == 4:
self.rect.y = random.randint(FirstPlayer.rect.y+1+spawn_offset, window[1]-self.height)
else:
self.rect.x = random.randint(1,FirstPlayer.rect.y-spawn_offset)
return 0
except:
pass
def draw(self):
self.HPbar_high = self.rect.y-self.HPheight-self.HPoffset
pygame.draw.rect(win, self.color, self.rect)
pygame.draw.rect(win, (255,255,255), (self.rect.x + self.width/2 - self.HPwidth/2, self.HPbar_high, self.HPwidth, self.HPheight))
pygame.draw.rect(win, (0,255,0), (self.rect.x + self.width/2 - self.HPwidth/2, self.HPbar_high, self.HPwidth*self.HP/self.MAXHP, self.HPheight))
def direction(mouse, obj):
#y = 1 clockwise
zeta = math.atan2((mouse[1]-obj[1]),(mouse[0]-obj[0]))
return zeta
def enemy_spawn(Enemy_max = 30, slow_rate = 2):
num = len(Global_Obj[2])
if num < Enemy_max:
# random spawn enemy 1/100
rand = random.randint(1, int((num+1)*slow_rate))
if rand == 1:
ene = EnemyIO()
ene.randompos()
Global_Obj[2].append(ene)
#Global_Obj[0] are player obj type, Global_Obj[1] are bullet obj type, Global_Obj[2] are enemy obj type, Global_Obj[3] are beam obj type
Global_Obj = [[],[],[],[]]
FirstPlayer = playerIO()
Global_Obj[0].append(FirstPlayer)
win.blit(background,(0, 0))
mouse_pos = (-1,-1)
beam_load = False
pygame.init()
# define the RGB value for white,
# green, blue colour .
text_color = (0, 0, 0)
white = (255, 255, 255)
# set the pygame window name
pygame.display.set_caption('Game')
# create a font object.
# 1st parameter is the font file
# which is present in pygame.
# 2nd parameter is size of the font
font = pygame.font.Font('freesansbold.ttf', 32)
# create a text surface object,
# on which text is drawn on it.
text = font.render('Game start', True, text_color)
# create a rectangular object for the
# text surface object
textRect = text.get_rect()
# set the center of the rectangular object.
textRect.center = (window[0] // 2, window[1] // 2)
inteface_run = True
# infinite loop
while inteface_run:
# completely fill the surface object
# with white color
win.fill(white)
# copying the text surface object
# to the display surface object
# at the center coordinate.
win.blit(text, textRect)
# iterate over the list of Event objects
# that was returned by pygame.event.get() method.
for event in pygame.event.get():
if pygame.mouse.get_pressed()[0] or event.type == pygame.QUIT:
inteface_run = False
# Draws the surface object to the screen.
pygame.display.update()
pygame.time.delay(1000)
font = pygame.font.Font('freesansbold.ttf', 16)
end_time = 300 # sec
t0= time.time()
while run:
pygame.display.flip()
pygame.time.delay(int(dt*1000))
# player move event
keys = pygame.key.get_pressed()
if keys[pygame.K_ESCAPE]:
run = False
if keys[pygame.K_SPACE] :
FirstPlayer.rect.x, FirstPlayer.rect.y = (80,80)
if (keys[pygame.K_a] or keys[pygame.K_LEFT]):
FirstPlayer.rect.x, FirstPlayer.rect.y = (FirstPlayer.rect.x - FirstPlayer.speed * dt, FirstPlayer.rect.y)
if keys[pygame.K_d] or keys[pygame.K_RIGHT]:
FirstPlayer.rect.x, FirstPlayer.rect.y = (FirstPlayer.rect.x + FirstPlayer.speed * dt, FirstPlayer.rect.y)
if keys[pygame.K_w] or keys[pygame.K_UP]:
FirstPlayer.rect.x, FirstPlayer.rect.y = (FirstPlayer.rect.x, FirstPlayer.rect.y - FirstPlayer.speed * dt)
if keys[pygame.K_s] or keys[pygame.K_DOWN]:
FirstPlayer.rect.x, FirstPlayer.rect.y = (FirstPlayer.rect.x, FirstPlayer.rect.y + FirstPlayer.speed * dt)
if FirstPlayer.rect.x < 0:
FirstPlayer.rect.x = 0
if FirstPlayer.rect.x+FirstPlayer.width > window[0]:
FirstPlayer.rect.x = window[0] - FirstPlayer.width
if FirstPlayer.rect.y < 0:
FirstPlayer.rect.y = 0
if FirstPlayer.rect.y+FirstPlayer.height > window[1]:
FirstPlayer.rect.y = window[1] - FirstPlayer.height
#player HP regen
if FirstPlayer.HP < FirstPlayer.MAXHP:
FirstPlayer.HP +=FirstPlayer.HPregeneration_per_sec*dt
if FirstPlayer.HP > FirstPlayer.MAXHP:
FirstPlayer.HP = FirstPlayer.MAXHP
#player MANA regen
if FirstPlayer.MANA < FirstPlayer.MAXMANA:
FirstPlayer.MANA +=FirstPlayer.MANAregeneration_per_sec*dt
if FirstPlayer.MANA > FirstPlayer.MAXMANA:
FirstPlayer.MANA = FirstPlayer.MAXMANA
#bullet reload time
for i in range(len(FirstPlayer.bullet_delay)):
if FirstPlayer.bullet_delay[i]>0:
FirstPlayer.bullet_delay[i]-= dt
if FirstPlayer.bullet_delay[i]<0:
FirstPlayer.bullet_delay[i] =0
# bullet click event
mouse_pos = pygame.mouse.get_pos()
if pygame.mouse.get_pressed()[0] and FirstPlayer.MANA >= BulletIO().MANA_usage and FirstPlayer.bullet_delay[0]==0:
bullet = BulletIO()
FirstPlayer.MANA -= bullet.MANA_usage
zeta = direction(mouse_pos, FirstPlayer.rect.center)
bullet.variant(*FirstPlayer.rect.center, zeta)
FirstPlayer.bullet_delay[0] = bullet.reload_delay
Global_Obj[1].append(bullet)
if pygame.mouse.get_pressed()[2] and FirstPlayer.MANA >= LaserIO().MANA_usage and FirstPlayer.bullet_delay[1]==0:
laser = LaserIO()
FirstPlayer.MANA -= laser.MANA_usage
zeta = direction(mouse_pos, FirstPlayer.rect.center)
laser.variant( *FirstPlayer.rect.center, zeta)
FirstPlayer.bullet_delay[1] = laser.reload_delay
Global_Obj[1].append(laser)
# Global_Obj[1] bullet obj type movement
for bullet_type in Global_Obj[1]:
bullet_type.rect.x += dt * bullet_type.speed * math.cos(bullet_type.zeta)
bullet_type.rect.y += dt * bullet_type.speed * math.sin(bullet_type.zeta)
if bullet_type.rect.x > window[0] or bullet_type.rect.x < 0 or bullet_type.rect.y > window[1] or bullet_type.rect.y < 0:
Global_Obj[1].remove(bullet_type)
# Global_Obj[3] beam obj type movement
for bullet_type in Global_Obj[3]:
bullet_type.rect.x += dt * bullet_type.speed * math.cos(bullet_type.zeta)
bullet_type.rect.y += dt * bullet_type.speed * math.sin(bullet_type.zeta)
if bullet_type.rect.x > window[0] or bullet_type.rect.x < 0 or bullet_type.rect.y > window[1] or bullet_type.rect.y < 0:
Global_Obj[3].remove(bullet_type)
# enemy
enemy_spawn()
# Global_Obj[2] enemy obj type movement
for enemy_types in Global_Obj[2]:
zetas = math.sqrt((enemy_types.rect.center[1] - FirstPlayer.rect.center[1])**2 + (FirstPlayer.rect.center[0] - enemy_types.rect.center[0])**2)
print(enemy_types.rect.center)
x,y = enemy_types.rect.center
x += dt * enemy_types.speed * (FirstPlayer.rect.center[0] - enemy_types.rect.center[0]) / zetas
y += dt * enemy_types.speed * (FirstPlayer.rect.center[1] - enemy_types.rect.center[1]) / zetas
enemy_types.rect.center = (x,y)
# enemy-player damage check
for enemy_type in Global_Obj[2]:
if FirstPlayer.rect.colliderect(enemy_type.rect):
FirstPlayer.HP -= enemy_type.damage
Global_Obj[2].remove(enemy_type)
# bullet - enemy damage check
for bullet_type in Global_Obj[1]:
for enemy_type in Global_Obj[2]:
if bullet_type.rect.colliderect(enemy_type.rect):
enemy_type.HP -= bullet_type.damage
if enemy_type.HP<=0:
Global_Obj[2].remove(enemy_type)
Global_Obj[1].remove(bullet_type)
break
# beam - enemy damage check
for bullet_type in Global_Obj[3]:
for enemy_type in Global_Obj[2]:
if bullet_type.rect.colliderect(enemy_type.rect):
enemy_type.HP -= bullet_type.damage
if enemy_type.HP<=0:
Global_Obj[2].remove(enemy_type)
#end game
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
elif event.type == pygame.MOUSEBUTTONDOWN:
if event.button == 2 and FirstPlayer.bullet_delay[2]==0: # left mouse button
beam = LaserBeamIO()
Global_Obj[3].append(beam)
beam_load = True
elif event.type == pygame.MOUSEBUTTONUP:
if event.button == 2 and beam_load:
zeta = direction(mouse_pos, FirstPlayer.rect.center)
beam.charge_beam( FirstPlayer.rect.center, zeta)
FirstPlayer.bullet_delay[2] = beam.reload_delay
beam_load = False
if beam_load:
if FirstPlayer.MANA <= beam.MANA_usage_per_sec * dt:
zeta = direction(mouse_pos, FirstPlayer.rect.center)
beam.charge_beam( FirstPlayer.rect.center, zeta)
FirstPlayer.bullet_delay[2] = beam.reload_delay
beam_load = False
else:
FirstPlayer.MANA -= beam.MANA_usage_per_sec * dt
zeta = direction(mouse_pos, FirstPlayer.rect.center)
beam.charge_beam( FirstPlayer.rect.center, zeta)
if FirstPlayer.HP <= 0:
run = False
te = int(time.time() - t0)
if te >= end_time:
run = False
# draw obj
win.blit(background,(0, 0))
for obj_types in Global_Obj:
for obj in obj_types:
obj.draw()
text = font.render(f'timer:{te}/{end_time} sec', True, white)
# create a rectangular object for the
# text surface object
textRect = text.get_rect()
# set the center of the rectangular object.
textRect.center = (window[0] // 2, window[1]-40)
win.blit(text, textRect)
pygame.quit()
Feel free to give me any suggestions to improve my code. I'm very new to Pygame.
Your enemies do move at the same speed in all directions, which can be measured by adding a few lines:
# Global_Obj[2] enemy obj type movement
for enemy_types in Global_Obj[2]:
zetas = math.dist(enemy_types.rect.center, FirstPlayer.rect.center)
x, y = enemy_types.rect.center
dx = dt * enemy_types.speed * (FirstPlayer.rect.center[0] - enemy_types.rect.center[0]) / zetas
dy = dt * enemy_types.speed * (FirstPlayer.rect.center[1] - enemy_types.rect.center[1]) / zetas
x += dx
y += dy
print((dx ** 2 + dy ** 2) ** 0.5)
enemy_types.rect.center = (x, y)
The code consistently prints out 2.
I suspect that it has something to do with the amount of time it takes to evaluate negative/positive calculations, which can be measured using the time.perf_counter() method.
Tip: Your
zetas = math.sqrt((enemy_types.rect.center[1] - FirstPlayer.rect.center[1])**2 + (FirstPlayer.rect.center[0] - enemy_types.rect.center[0])**2)
can be replaced with
zetas = math.dist(enemy_types.rect.center, FirstPlayer.rect.center)
a much more efficient method.
Since pygame.Rect is supposed to represent an area on the screen, a pygame.Rect object can only store integral data:
The coordinates for Rect objects are all integers. [...]
When you do
enemy_types.rect.center = (x,y)
it is the same as you would do:
enemy_types.rect.center = (int(x), int(y))
The fraction component of the coordinate get lost. This causes that the movement to the left and to the top is faster than to the right and to the bottom.
If you want to store object positions with floating point accuracy, you have to store the location of the object in separate variables respectively attributes and to synchronize the pygame.Rect object. round the coordinates and assign it to the location of the rectangle:
class EnemyIO():
def __init__(self):
# [...]
self.x, self.y = self.rect.center
for enemy_types in Global_Obj[2]:
zetas = math.sqrt((enemy_types.rect.center[1] - FirstPlayer.rect.center[1])**2 + (FirstPlayer.rect.center[0] - enemy_types.rect.center[0])**2)
enemy_types.x += dt * enemy_types.speed * (FirstPlayer.rect.center[0] - enemy_types.rect.center[0]) / zetas
enemy_types.y += dt * enemy_types.speed * (FirstPlayer.rect.center[1] - enemy_types.rect.center[1]) / zetas
enemy_types.rect.center = round(enemy_types.x), round(enemy_types.y)
I'm making a game with pygame and pymunk as a physics engine. I'm trying to kill a bullet whenever it hits a player or goes past its lifetime.
When I tried to space.remove(self.shape) and the second bullet hits the player, it gives me an "AssertionError: shape not in space, already removed. I simply changed it to teleport the bullets away, and then learned of the real error.
When I have more than one bullet in the space and a bullet hits the enemy player, all the current bullets teleport away, which means that when I tried to remove one bullet, it called the remove on all the bullets and thats why I had the initial error.
However the problem still remains that one bullet is being treated as every bullet.
Why is something that should be a non-static variable being called as a static variable?
I even tried to use deepcopy to see if that fixed it, but to no avail
This is my chunk of code, apologies since I don't know what is needed to understand it.
The key parts are most likely the Bullet class, the shoot() function in the Player class, and the drawBulletCollision() function
# PyGame template.
# Import modules.
import sys, random, math, time, copy
from typing import List
import pygame
from pygame.locals import *
from pygame import mixer
import pymunk
import pymunk.pygame_util
from pymunk.shapes import Segment
from pymunk.vec2d import Vec2d
pygame.mixer.pre_init(44110, -16, 2, 512)
mixer.init()
# Set up the window.
width, height = 1440, 640
screen = pygame.display.set_mode((width, height))
bg = pygame.image.load("space.png")
def draw_bg():
screen.blit(bg, (0, 0))
#load sounds
#death_fx = pygame.mixer.Sound("")
#death_fx.set_volume(0.25)
shoot_fx = mixer.Sound("shot.wav")
shoot_fx.set_volume(0.25)
#mixer.music.load("video.mp3")
#mixer.music.play()
#time.sleep(2)
#mixer.music.stop()
#gun_mode_fx = pygame.mixer.Sound("")
#gun_mode_fx.set_volume(0.25)
#thrust_mode_fx = pygame.mixer.Sound("")
#thrust_mode_fx.set_volume(0.25)
collision_fx = mixer.Sound("thump.wav")
collision_fx.set_volume(0.25)
ship_group = pygame.sprite.Group()
space = pymunk.Space()
space.gravity = 0, 0
space.damping = 0.6
draw_options = pymunk.pygame_util.DrawOptions(screen)
bulletList = []
playerList = []
environmentList = []
arbiterList = []
b0 = space.static_body
segmentBot = pymunk.Segment(b0, (0,height), (width, height), 4)
segmentTop = pymunk.Segment(b0, (0,0), (width, 0), 4)
segmentLef = pymunk.Segment(b0, (width,0), (width, height), 4)
segmentRit = pymunk.Segment(b0, (0,0), (0, height), 4)
walls = [segmentBot,segmentLef,segmentRit,segmentTop]
for i in walls:
i.elasticity = 1
i.friction = 0.5
i.color = (255,255,255,255)
environmentList.append(i)
class Player(object):
radius = 30
def __init__(self, position, space, color):
self.body = pymunk.Body(mass=5,moment=10)
self.mode = 0 # 0 is gun, 1 is thrust, ? 2 is shield
self.body.position = position
self.shape = pymunk.Circle(self.body, radius = self.radius)
#self.image
#self.shape.friction = 0.9
self.shape.elasticity= 0.2
space.add(self.body,self.shape)
self.angleGun = 0
self.angleThrust = 0
self.health = 100
self.speed = 500
self.gearAngle = 0
self.turningSpeed = 5
self.shape.body.damping = 1000
self.cooldown = 0
self.fireRate = 30
self.shape.collision_type = 1
self.shape.color = color
playerList.append(self)
def force(self,force):
self.shape.body.apply_force_at_local_point(force,(0,0))
def rocketForce(self):
radians = self.angleThrust * math.pi/180
self.shape.body.apply_force_at_local_point((-self.speed * math.cos(radians),-self.speed * math.sin(radians)),(0,0))
def draw(self):
gear = pygame.image.load("gear.png")
gearBox = gear.get_rect(center=self.shape.body.position)
gearRotated = pygame.transform.rotate(gear, self.gearAngle)
#gearRotated.rect.center=self.shape.body.position
x,y = self.shape.body.position
radianGun = self.angleGun * math.pi/180
radianThrust = self.angleThrust * math.pi/180
radiyus = 30 *(100-self.health)/100
screen.blit(gearRotated,gearBox)
self.gearAngle += 1
if radiyus == 30:
radiyus = 32
pygame.draw.circle(screen,self.shape.color,self.shape.body.position,radiyus,0)
pygame.draw.circle(screen,(0,0,0),self.shape.body.position,radiyus,0)
pygame.draw.line(
screen,(0,255,0),
(self.radius * math.cos(radianGun) * 1.5 + x,self.radius * math.sin(radianGun) * 1.5 + y),
(x,y), 5
)
pygame.draw.line(
screen,(200,200,0),
(self.radius * math.cos(radianThrust) * 1.5 + x,self.radius * math.sin(radianThrust) * 1.5 + y),
(x,y), 5
)
#more
def targetAngleGun(self,tAngle):
tempTAngle = tAngle - self.angleGun
tempTAngle = tempTAngle % 360
if(tempTAngle < 180 and not tempTAngle == 0):
self.angleGun -= self.turningSpeed
elif(tempTAngle >= 180 and not tempTAngle == 0):
self.angleGun += self.turningSpeed
self.angleGun = self.angleGun % 360
#print(tAngle, "target Angle")
#print(self.angleGun, "selfangleGun")
#print(tempTAngle, "tempTAngle")
def targetAngleThrust(self,tAngle):
tempTAngle = tAngle - self.angleThrust
tempTAngle = tempTAngle % 360
if(tempTAngle < 180 and not tempTAngle == 0):
self.angleThrust -= self.turningSpeed
elif(tempTAngle >= 180 and not tempTAngle == 0):
self.angleThrust += self.turningSpeed
self.angleThrust = self.angleThrust % 360
#print(tAngle, "target Angle")
#print(self.angleThrust, "selfangleGun")
#print(tempTAngle, "tempTAngle")
def targetAngle(self,tAngle):
if(self.mode == 0):
self.targetAngleGun(tAngle)
elif(self.mode == 1):
self.targetAngleThrust(tAngle)
def shoot(self):
if(self.cooldown == self.fireRate):
x,y = self.shape.body.position
radianGun = self.angleGun * math.pi/180
spawnSpot = (self.radius * math.cos(radianGun) * 1.5 + x,self.radius * math.sin(radianGun)*1.5+y)
self.shape.body.apply_impulse_at_local_point((-20 * math.cos(radianGun),-20 * math.sin(radianGun)),(0,0))
print(spawnSpot)
bT = Bullet(spawnSpot, 5, 50,self.shape.color)
b = copy.deepcopy(bT)
bulletList.append(b)
space.add(b.shape,b.shape.body)
b.getShot(self.angleGun)
self.cooldown = 0
print('pew')
shoot_fx.play()
# HEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEREEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE
def tick(self):
self.draw()
if(self.cooldown < self.fireRate):
self.cooldown += 1
#for o in playerList:
# c = self.shape.shapes_collide(o.shape)
# if(len(c.points)>0):
# self.damage(c.points[0].distance/10)
for o in bulletList:
c = self.shape.shapes_collide(o.shape)
#print(c)
for o in walls:
c = self.shape.shapes_collide(o)
if(len(c.points)>0):
self.damage(c.points[0].distance * 3)
def damage(self, damage):
self.health -= abs(damage)
if self.health < 0:
self.health = 0
#maybe make it part of the player class
def drawWallCollision(arbiter, space, data):
for c in arbiter.contact_point_set.points:
r = max(3, abs(c.distance * 5))
r = int(r)
p = tuple(map(int, c.point_a))
pygame.draw.circle(data["surface"], pygame.Color("red"), p, r, 0)
print('magnitude', math.sqrt(arbiter.total_impulse[0]**2 + arbiter.total_impulse[1]**2))
#print('position', p)
#print(data)
print("its all arbitrary")
s1, s2 = arbiter.shapes
collision_fx.play()
def drawBulletCollision(arbiter, space, data):
s1, s2 = arbiter.shapes
for c in arbiter.contact_point_set.points:
magnitude = math.sqrt(arbiter.total_impulse[0]**2 + arbiter.total_impulse[1]**2)
for p in playerList:
avr = ((c.point_a[0] + c.point_b[0])/2, (c.point_a[1] + c.point_b[1])/2)
distance = (math.sqrt((avr[0] - p.shape.body.position[0]) **2 + (avr[1] - p.shape.body.position[1]) **2 ))
if(distance < Bullet.explosionRadius + Player.radius):
if not(s1.color == s2.color):
p.damage(magnitude)
for b in bulletList:
avr = ((c.point_a[0] + c.point_b[0])/2, (c.point_a[1] + c.point_b[1])/2)
distance = (math.sqrt((avr[0] - p.shape.body.position[0]) **2 + (avr[1] - p.shape.body.position[1]) **2 ))
if(distance < Bullet.explosionRadius + Player.radius):
if not(s1.color == s2.color):
b.damage(magnitude)
pygame.draw.circle(data["surface"], pygame.Color("red"), tuple(map(int, c.point_a)), 10, 0)
print('magnitude', magnitude)
#print('position', p)
#print(data)
print("its all arbitrary")
def drawArbitraryCollision(arbiter, space, data):
collision_fx.play()
class Ship(pygame.sprite.Sprite):
def __init__(self, x, y):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.image.load("gear.png")
self.rect = self.image.get_rect()
self.rect.center = [x, y]
def rotate(self):
self.image = pygame.transform.rotate(self.image,1)
class Bullet(object):
damage = 2
explosionRadius = 5
def __init__(self, position, size, speed,color):
pts = [(-size, -size), (size, -size), (size, size), (-size, size)]
self.body = copy.deepcopy(pymunk.Body(mass=0.1,moment=1))
self.shape = copy.deepcopy(pymunk.Poly(self.body, pts))
self.shape.body.position = position
self.shape.friction = 0.5
self.shape.elasticity = 1
self.shape.color = color
self.speed = speed
self.size = size
self.shape.collision_type = 2
#space.add(self.body,self.shape)
#bulletList.append(self)
self.lifetime = 0
def getShot(self,angle):
radians = angle * math.pi/180
self.shape.body.apply_impulse_at_local_point((self.speed * math.cos(radians),self.speed * math.sin(radians)),(0,0))
def tick(self):
self.lifetime += 1
if(self.lifetime > 300):
self.shape.body.position = (10000,30)
def damage(self, damage):
self.lifetime = 300
#VELOCITY OF BULLET STARTS WITH VELOCITY OF PLAYER
#MAKE VOLUME OF SOUND DEPEND ON THE IMPULSE FOR THE IMPACTS
#error on purpose so you notice this
#INSTANCES NOT WORKING????
def runPyGame():
# Initialise PyGame.
pygame.init()
# Set up the clock. This will tick every frame and thus maintain a relatively constant framerate. Hopefully.
fps = 60.0
fpsClock = pygame.time.Clock()
running = True
font = pygame.font.SysFont("Arial", 16)
p1 = Player((240,240),space,(132, 66, 245,255))
p2 = Player((1200,400),space,(47, 247, 184,255))
space.add(segmentBot,segmentTop,segmentLef,segmentRit)
# Main game loop.
ch = space.add_collision_handler(1, 0)
ch.data["surface"] = screen
ch.post_solve = drawWallCollision
ch = space.add_collision_handler(1, 2)
ch.data["surface"] = screen
ch.post_solve = drawBulletCollision
ch = space.add_collision_handler(0, 2)
ch.data["surface"] = screen
ch.post_solve = drawArbitraryCollision
dt = 1/fps # dt is the time since last frame.
while True: # Loop forever!
keys = pygame.key.get_pressed()
for event in pygame.event.get():
# We need to handle these events. Initially the only one you'll want to care
# about is the QUIT event, because if you don't handle it, your game will crash
# whenever someone tries to exit.
if event.type == QUIT:
pygame.quit() # Opposite of pygame.init
sys.exit() # Not including this line crashes the script on Windows.
if event.type == KEYDOWN:
if event.key == pygame.K_s:
p1.mode = -(p1.mode - 0.5) + 0.5
print(p1.mode)
if (event.key == pygame.K_k and p1.mode == 0):
p1.shoot()
if event.key == pygame.K_KP_5:
p2.mode = -(p2.mode - 0.5) + 0.5
print(p2.mode)
if (event.key == pygame.K_m and p2.mode == 0):
p2.shoot()
#b = Bullet((200,200),51,51)
if(keys[K_w]):
p1.targetAngle(90)
if(keys[K_q]):
p1.targetAngle(45)
if(keys[K_a]):
p1.targetAngle(0)
if(keys[K_z]):
p1.targetAngle(315)
if(keys[K_x]):
p1.targetAngle(270)
if(keys[K_c]):
p1.targetAngle(225)
if(keys[K_d]):
p1.targetAngle(180)
if(keys[K_e]):
p1.targetAngle(135)
if(keys[K_k] and p1.mode == 1):
p1.rocketForce()
if(keys[K_KP_8]):
p2.targetAngle(90)
if(keys[K_KP_7]):
p2.targetAngle(45)
if(keys[K_KP_4]):
p2.targetAngle(0)
if(keys[K_KP_1]):
p2.targetAngle(315)
if(keys[K_KP_2]):
p2.targetAngle(270)
if(keys[K_KP_3]):
p2.targetAngle(225)
if(keys[K_KP_6]):
p2.targetAngle(180)
if(keys[K_KP_9]):
p2.targetAngle(135)
if(keys[K_m] and p2.mode == 1):
p2.rocketForce()
# Handle other events as you wish.
screen.fill((250, 250, 250)) # Fill the screen with black.
# Redraw screen here.
### Draw stuff
draw_bg()
space.debug_draw(draw_options)
for i in playerList:
i.tick()
screen.blit(
font.render("P1 Health: " + str(p1.health), True, pygame.Color("white")),
(50, 10),
)
screen.blit(
font.render("P2 Health: " + str(p2.health), True, pygame.Color("white")),
(50, 30),
)
for i in bulletList:
i.tick()
ship_group.draw(screen)
# Flip the display so that the things we drew actually show up.
pygame.display.update()
dt = fpsClock.tick(fps)
space.step(0.01)
pygame.display.update()
runPyGame()
I cant point to the exact error since the code is quite long and depends on files I dont have. But here is a general advice for troubleshooting:
Try to give a name to each shape when you create them, and then print it out. Also print out the name of each shape that you add or remove from the space. This should show which shape you are actually removing and will probably make it easy to understand whats wrong.
For example:
...
self.shape = pymunk.Circle(self.body, radius = self.radius)
self.shape.name = "circle 1"
print("Created", self.shape.name)
...
print("Adding", self.shape.name)
space.add(self.body,self.shape)
...
(Note that you need to reset the name of shapes you copy, since otherwise the copy will have the same name.)
I have a 2d pygame water simulation thingy that I followed a tutorial to make. I also found the answer to this question to fix issues with the tutorial: Pygame water physics not working as intended
I have since been trying to convert this program over to using pyopengl to render things. However, I have been struggling to:
A: Draw the water polygon
B: texture the water polygon with a tiled texture
Here is my (rather poor) attempt at converting this code to pyopengl.
import pygame, random
import math as m
from pygame import *
from OpenGL import *
from OpenGL.GLU import *
from OpenGL.GL import *
pygame.init()
WINDOW_SIZE = (854, 480)
screen = pygame.display.set_mode(WINDOW_SIZE,0,32,DOUBLEBUF|OPENGL) # initiate the window
clock = pygame.time.Clock()
def draw_polygon(polygon_points):
glBegin(GL_POLYGON);
for i in polygon_points:
glVertex3fv(i)
#glEnd()
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.04
self.d = 0.08
self.time = 1
def update(self):
x = self.y_pos - self.target_y
a = -(self.k * x + self.d * self.velocity)
if self.y_pos > self.target_y:
self.y_pos -= 0.1
if self.y_pos < self.target_y:
self.y_pos += 0.1
self.velocity = round(self.velocity)
self.y_pos += self.velocity
self.velocity += a
self.time += 1
class water_tile():
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)):
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):
rightDeltas[i] = spread * (self.springs[i].y_pos - self.springs[(i + 1)%len(self.springs)].y_pos)
self.springs[(i + 1)%len(self.springs)].velocity += rightDeltas[i]
for i in range(0, len(self.springs)):
if round (leftDeltas[i],12) == 0 or round (rightDeltas[i],12) == 0:
self.springs[i - 1].y_pos = self.y_end+10
if i > 0:
self.springs[i - 1].y_pos += leftDeltas[i] # you were updating velocity here!
if i < len(self.springs):
self.springs[(i + 1)%len(self.springs)].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_end-self.x_start), abs(self.y_start - self.y_end)), depth=8).convert_alpha()
polygon_points = []
polygon_points.append((self.x_start, self.y_start,0))
for spring in range(len(self.springs)):
polygon_points.append((self.springs[spring].x_pos, self.springs[spring].y_pos,0))
polygon_points.append((self.springs[len(self.springs) - 1].x_pos, self.y_start,0))
draw_polygon(polygon_points)
return water_surface
class water_object:
def __init__(self, x_start, x_end, y_start, y_end, segment_length, x_pos, y_pos):
self.water = water_tile(x_start,x_end,y_start,y_end,segment_length)
self.image = self.water.draw()
self.rect = self.image.get_rect()
self.rect.x = x_pos
self.rect.y = y_pos
def update(self):
self.water.update(0.1)
self.image = self.water.draw()
water_list = [water_object(0,276+16,64,0,16,0,20)]
while True:
screen.fill((0,0,0))
for water in water_list:
gluPerspective(45, (WINDOW_SIZE[0]/WINDOW_SIZE[1]), 0.1, 50.0)
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)
water.update()
#screen.blit(water.image, (water.rect.x,water.rect.y))
#water_test.x_start = water_test.x_start + 1
#if random.randint(0,8) == 1:
#water_test.splash(random.randint(0, len(water_test.springs) - 1),2)
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
if event.type == MOUSEBUTTONDOWN:
print (len(water.water.springs))
water.water.splash(random.randint(0, len(water.water.springs) - 1),50)
pygame.display.update()
clock.tick(60)
However, despite my attempt, I couldnt get anything to display on screen at all. How can I fix this/how can I attain the 2 things I have been struggling with?
You cannot draw an OpenGL primitive to a pygame.Surface. Anyway there is no need to do so.
For the best performance, directly draw to the default framebuffer (window).
Since you want to draw a line, you have to use a Line primitive type. GL_POLYGON would draw a filed convex polygon. Use the primitive type GL_LINE_STRIP:
def draw_polygon(polygon_points):
glBegin(GL_LINE_STRIP)
for pt in polygon_points:
glVertex2f(*pt)
glEnd()
Before you draw the line, ser the current color by glColor:
glColor3f(0, 0, 1)
draw_polygon(polygon_points)
The vertex coordinates of the lie are specified in window space. Hence you have to setup an Orthographic projection rather than a Perspective projection. Specify the current matrix by [glMatrixMode] and set the projection matrix by glOrtho. Since the matrix operations do not set a matrix, but multiply the current matrix by the specified matrix, I recommend to load the identity matrix before (glLoadIdentity):
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
glOrtho(0, WINDOW_SIZE[0], WINDOW_SIZE[1], 0, -1, 1)
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
Before you draw the line you have to clear the framebuffer by glClear. The clear color can be defined by glClearColor:
glClearColor(1, 1, 1, 1)
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
Complete example:
import pygame
from OpenGL import *
from OpenGL.GL import *
def draw_polygon(surf_rect, polygon_points):
glBegin(GL_LINE_STRIP)
#glBegin(GL_TRIANGLE_STRIP)
for pt in polygon_points:
glVertex2f(*pt)
glVertex2f(pt[0], surf_rect.height)
glEnd()
class WaterParticle():
def __init__(self, x, y):
self.x, self.y = x, y
self.target_y = y
self.velocity = 0
self.k = 0.04
self.d = 0.08
def update(self):
x = self.y - self.target_y
a = -(self.k * x + self.d * self.velocity)
#self.p[1] += -0.1 if x > 0 else 0.1 if x < 0 else 0
self.y += self.velocity
self.velocity += a
class Water():
def __init__(self, x_start, x_end, y_start, segment_length, passes, spread):
n = abs(x_end - x_start + segment_length - 1) // segment_length + 1
self.particles = [WaterParticle(i * segment_length + x_start, y_start) for i in range(n)]
self.passes = passes
self.spread = spread
def update(self):
for particle in self.particles:
particle.update()
left_deltas = [0] * len(self.particles)
right_deltas = [0] * len(self.particles)
for _ in range(self.passes):
for i in range(len(self.particles)):
if i > 0:
left_deltas[i] = self.spread * (self.particles[i].y - self.particles[i - 1].y)
self.particles[i - 1].velocity += left_deltas[i]
if i < len(self.particles)-1:
right_deltas[i] = self.spread * (self.particles[i].y - self.particles[i + 1].y)
self.particles[i + 1].velocity += right_deltas[i]
for i in range(len(self.particles)):
if i > 0:
self.particles[i-1].y += left_deltas[i]
if i < len(self.particles) - 1:
self.particles[i+1].y += right_deltas[i]
def splash(self, index, speed):
if index > 0 and index < len(self.particles):
self.particles[index].velocity += speed
def draw(self, surf_rect):
polygon_points = []
for spring in range(len(self.particles)):
polygon_points.append((self.particles[spring].x, self.particles[spring].y))
glColor3f(0, 0, 1)
draw_polygon(surf_rect, polygon_points)
pygame.init()
window = pygame.display.set_mode((640, 480), pygame.DOUBLEBUF | pygame.OPENGL)
clock = pygame.time.Clock()
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
glOrtho(0, *window.get_size(), 0, -1, 1)
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
glClearColor(1, 1, 1, 1)
water_line_y = window.get_height() // 2
water = Water(0, window.get_width(), window.get_height() // 2, 3, 8, 0.025)
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
if event.type == pygame.MOUSEBUTTONDOWN:
velocity = water_line_y - event.pos[1]
if velocity > 0:
index = int(len(water.particles) * event.pos[0] / window.get_width())
water.splash(index, velocity)
water.update()
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
water.draw(window.get_rect())
pygame.display.flip()
clock.tick(50)