canvas circle getting distorted in python - python-3.x

I am writing a code where there are multiple balls (or circles) of various sizes in Tkinter canvas. The problem is that during the running process when ever ball gets close to the canvas boundary and sometimes even away from the boundary the shape of the ball gets distorted. I am unsure why this is happening. is there something I am missing during the update of the window object?
This is the main file:
from tkinter import *
from Ball import *
import time
HEIGHT = 500
WIDTH = 500
window = Tk()
canvas = Canvas(window, height=HEIGHT, width=WIDTH)
canvas.pack()
volley_ball = Ball(canvas, 0, 0, 100, 2, 2,'red')
cricket_ball = Ball(canvas, 10, 10, 30, 4, 2,'green')
golf_ball = Ball(canvas, 100, 100, 20, 8, 6,'white')
while True:
volley_ball.move()
cricket_ball.move()
golf_ball.move()
window.update()
time.sleep(0.01)
window.mainloop()
This is the Ball class module code:
class Ball:
def __init__(self, canvas, x, y, diameter, xVelocity, yVelocity, color):
self.canvas = canvas
self.image = canvas.create_oval(x, y, diameter, diameter, fill=color)
self.xVelocity = xVelocity
self.yVelocity = yVelocity
self.diameter = diameter
def move(self):
self.coordinates = self.canvas.coords(self.image)
if(self.coordinates[2] >= self.canvas.winfo_width() or self.coordinates[0] < 0):
self.xVelocity = -self.xVelocity
if (self.coordinates[3] >= self.canvas.winfo_height() or self.coordinates[1] < 0):
self.yVelocity = -self.yVelocity
self.canvas.move(self.image, self.xVelocity, self.yVelocity)

The visual artifacts are created by over driving your move function. Try changing delay to 1 millisecond and observe the artifacts! Slow it down a little and use xVelocity, yVelocity to control speed.
Also try changing your while loop for window.after.
def animate():
volley_ball.move()
cricket_ball.move()
golf_ball.move()
window.after(20, animate)
window.after(500, animate)
window.mainloop()

Related

Tile based lighting system 2d

I am looking for a tile based lighting system for my tile based game. I have not tried anything because I can't think of an effective way to do this. I have searched stack overflow and I found this but its not what I want. I am making a 2d version of Minecraft with pygame.
here is my tile class
class tile():
def __init__(self, block_category, block_type, x, y, world, win):
self.x, self.y, self.width, self.height = (x*64), (y*64), 64, 64
self.block_type = block_type
self.light_level = 1 # i want light level to range from 0-1
self._image = None
self.world = world
self.win = win
self.posx, self.posy = x, y
try:
self._image = self.world.block_textures[block_category][block_type]
except:
self._image = self.world.block_textures["missing"]["missing_texture"]
self.image = self._image
def draw(self):
#draw code here self.posx, self.win, self.world and self.posy are used here if you are wondering
def change_block(self, block_category, block_type):
try:
self._image = self.world.block_textures[block_category][block_type]
except:
self._image = self.world.block_textures["missing"]["missing_texture"]
self.image = self._image
and my world data looks like this
def generate_world(self):
for x in range(0, self.width):
self.tiles[x] = {}
for y in range(0, self.height):
self.tiles[x][y] = tile("terrain", "air", x, y, self, self.win)
for x in range(0, self.width):
for y in range(0, self.height):
if y == 0:
self.tiles[x][y].change_block("terrain", "bedrock")
elif y == 38:
self.tiles[x][y].change_block("terrain", "grass_block")
elif y < 38 and y > 34:
self.tiles[x][y].change_block("terrain", "dirt")
elif y < 35 and y > 0:
self.tiles[x][y].change_block("terrain", "stone")
if x == 0 or x == self.height - 1:
self.tiles[x][y].change_block("terrain", "bedrock")
return self.tiles
my game looks like this
For 2D games like you're making, how we could apply lighting - more like, shadowing - could go into 2 options:
Change screen color to shadow color & set transparency to objects, as OP suggested
Sandwich entire thing between screen and light layer
Let's start with problem of 1st option:
Problem of setting transparency
Here's demo code based on your idea:
"""
Demonstration of color overlapping
"""
import pygame as pg
class Player(pg.sprite.Sprite):
def __init__(self):
super(Player, self).__init__()
self.image = pg.Surface((50, 50))
self.image.fill((255, 255, 255))
self.rect = self.image.get_rect()
# setting alpha on player
self.image.set_alpha(125)
def update(self, *args, **kwargs):
x, y = pg.mouse.get_pos()
c_x, c_y = self.rect.center
self.rect.move_ip(x - c_x, y - c_y)
def mainloop():
player = Player()
screen = pg.display.set_mode((500, 500))
circle_colors = (255, 0, 0), (0, 255, 0), (0, 0, 255)
circle_coords = (150, 250), (250, 250), (350, 250)
# make surface, set alpha then draw circle
bg_surfaces = []
for (color, center) in zip(circle_colors, circle_coords):
surface = pg.Surface((500, 500), pg.SRCALPHA, 32)
surface.convert_alpha()
surface.set_alpha(125)
pg.draw.circle(surface, color, center, 75)
bg_surfaces.append(surface)
running = True
while running:
screen.fill((0, 0, 0))
# draw background
for surface in bg_surfaces:
screen.blit(surface, surface.get_rect())
for event in pg.event.get():
if event.type == pg.QUIT:
running = False
player.update()
screen.blit(player.image, player.rect)
pg.display.flip()
if __name__ == '__main__':
pg.init()
mainloop()
pg.quit()
As you see, now the player (White square)'s color is Mixed with background circles.
It's basically just like what the drawing program does with layers.
Set layer transparency 50% and stack - everything mixes, producing undesirable effect which is far from lighting effect you wanted.
Unless you want Creeper or Steve to blend with the background and become a ghosty figure, it's better to go for sandwiched layout.
Sandwiched Layout
Following is demo code which uses mouse position as light source position.
Rendering order is Ground > Player > light overlay(shadow)
Demo code:
"""
Code demonstration for https://stackoverflow.com/q/72610504/10909029
Written on Python 3.10 (Using Match on input / event dispatching)
"""
import math
import random
import itertools
from typing import Dict, Tuple, Sequence
import pygame as pg
class Position:
"""Namespace for size and positions"""
tile_x = 20
tile_size = tile_x, tile_x
class SpriteGroup:
"""Namespace for sprite groups, with chain iterator keeping the order"""
ground = pg.sprite.Group()
entities = pg.sprite.Group()
light_overlay = pg.sprite.Group()
#classmethod
def all_sprites(cls):
return itertools.chain(cls.ground, cls.entities, cls.light_overlay)
class Player(pg.sprite.Sprite):
"""Player class, which is merely a rect following pointer in this example."""
def __init__(self):
super(Player, self).__init__()
self.image = pg.Surface((50, 50))
self.image.fill((255, 255, 255))
self.rect = self.image.get_rect()
SpriteGroup.entities.add(self)
self.rect.move_ip(225, 225)
def update(self, *args, **kwargs):
pass
# Intentionally disabling mouse following code
# x, y = pg.mouse.get_pos()
# c_x, c_y = self.rect.center
# self.rect.move_ip(x - c_x, y - c_y)
class TileLightOverlay(pg.sprite.Sprite):
"""
Light overlay tile. Using separate sprites, so we don't have to blit on
every object above ground that requires lighting.
"""
# light lowest boundary
lighting_lo = 255
# light effect radius
light_radius = Position.tile_x * 8
def __init__(self, x, y):
super(TileLightOverlay, self).__init__()
self.image = pg.Surface(Position.tile_size)
self.image.fill((0, 0, 0))
self.rect = self.image.get_rect()
self.rect.move_ip(x * Position.tile_x, y * Position.tile_x)
SpriteGroup.light_overlay.add(self)
def update(self, *args, **kwargs):
self.image.set_alpha(self.brightness)
#property
def brightness(self):
"""Calculate distance between mouse & apply light falloff accordingly"""
distance = math.dist(self.rect.center, pg.mouse.get_pos())
if distance > self.light_radius:
return self.lighting_lo
return (distance / self.light_radius) * self.lighting_lo
class TileGround(pg.sprite.Sprite):
"""Ground tile representation. Not much is going on here."""
def __init__(self, x, y, tile_color: Sequence[float]):
super(TileGround, self).__init__()
self.image = pg.Surface(Position.tile_size)
self.image.fill(tile_color)
self.rect = self.image.get_rect()
self.rect.move_ip(x * Position.tile_x, y * Position.tile_x)
SpriteGroup.ground.add(self)
# create and keep its pair light overlay tile.
self.light_tile = TileLightOverlay(x, y)
class World:
"""World storing ground tile data."""
# tile type storing color etc. for this example only have color.
tile_type: Dict[int, Tuple[float, float, float]] = {
0: (56, 135, 93),
1: (36, 135, 38),
2: (135, 128, 56)
}
def __init__(self):
# coord system : +x → / +y ↓
# generating random tile data
self.tile_data = [
[random.randint(0, 2) for _ in range(25)]
for _ in range(25)
]
# generated tiles
self.tiles = []
def generate(self):
"""Generate world tiles"""
for x, row in enumerate(self.tile_data):
tiles_row = [TileGround(x, y, self.tile_type[col]) for y, col in enumerate(row)]
self.tiles.append(tiles_row)
def process_input(event: pg.event.Event):
"""Process input, in case you need it"""
match event.key:
case pg.K_ESCAPE:
pg.event.post(pg.event.Event(pg.QUIT))
case pg.K_UP:
pass
# etc..
def display_fps_closure(screen: pg.Surface, clock: pg.time.Clock):
"""FPS display"""
font_name = pg.font.get_default_font()
font = pg.font.Font(font_name, 10)
color = (0, 255, 0)
def inner():
text = font.render(f"{int(clock.get_fps())} fps", True, color)
screen.blit(text, text.get_rect())
return inner
def mainloop():
# keeping reference of method/functions to reduce access overhead
fetch_events = pg.event.get
display = pg.display
# local variable setup
screen = display.set_mode((500, 500))
player = Player()
world = World()
world.generate()
clock = pg.time.Clock()
display_fps = display_fps_closure(screen, clock)
running = True
# main loop
while running:
screen.fill((0, 0, 0))
# process event
for event in fetch_events():
# event dispatch
match event.type:
case pg.QUIT:
running = False
case pg.KEYDOWN:
process_input(event)
# draw in ground > entities > light overlay order
for sprite in SpriteGroup.all_sprites():
sprite.update()
screen.blit(sprite.image, sprite.rect)
# draw fps - not related to question, was lazy to remove & looks fancy
clock.tick()
display_fps()
display.flip()
if __name__ == '__main__':
pg.init()
pg.font.init()
mainloop()
pg.quit()
You'll see it's blending properly with shadow without mixing color with ground tiles.
There could be much better approach or ways to implement this - as I never used pygame before, there would be bunch of good/better stuffs I didn't read on document.
But one thing for sure - always approach your goal with mindset that everything is related to your problem until you reach the goal! Comment you thought it wasn't going to be helpful gave me idea for this design.
One option is a black background, then I use set_alpha() to set how light or dark the tile is (how much the black background is seen through the tile) and no overlay is needed. Thanks to #jupiterbjy's original answer for inspiration.

How to make objects collide with tkinter

I am very new to programming and I'm trying to make a simple animation where I append 10 balls of random size, random color and random speed to the canvas. Here's the code:
from tkinter import *
import random
import time
WIDTH = 900
HEIGHT = 700
tk = Tk()
canvas = Canvas(tk, width = WIDTH, height = HEIGHT)
tk.title("Drawing")
canvas.pack()
COLORS = ["white", "black", "red", "blue", "green", "yellow", "purple", "orange", "gray"]
balls = []
class Ball:
def __init__(self, color, size):
self.shape = canvas.create_oval(10, 10, size, size, fill = color)
self.xspeed = random.randrange(-11, 11)
self.yspeed = random.randrange(-11, 11)
def move(self):
canvas.move(self.shape, self.xspeed, self.yspeed)
pos = canvas.coords(self.shape)
if pos[3] >= HEIGHT or pos[1] <= 0:
self.yspeed = -self.yspeed
if pos[2] >= WIDTH or pos[0] <= 0:
self.xspeed = -self.xspeed
for i in range(10):
size = random.randrange(20, 150)
color = random.choice(COLORS)
balls.append(Ball(color, size))
while True:
for ball in balls:
ball.move()
tk.update()
time.sleep(0.01)
tk.mainloop()
The problem is that balls are able to overlap each other i. e. when balls touch each other, nothing happens. I want the balls to bounce off of each other when they touch. I have spent several hours searching for answer and trying different things but nothing seems to help. Could you guys help me?

How do I pass a rect into pygame.display.update() to update a specific area of the window?

On the documentation page for pygame.display.update(), it says you can pass a rect into the method to update part of the screen. However, all of the examples I see just pass an existing rect from an image or shape in the program. How can I tell it to update an area on the screen directly? For example, when drawing a rectangle, I could use a rect argument of (100,200,30,40). This would draw a rectangle with a top at 200, a left at 100, a width of 30, and a height of 40. How can I pass a similar argument into pygame.display.update()? I tried pygame.display.update((100,200,30,40)), but this updates the entire window.
Just define a rect and pass it to pygame.display.update() to update only this specific region of the display. You can also pass a list of rects.
import random
import pygame as pg
from pygame.math import Vector2
# A simple sprite, just to have something moving on the screen.
class Ball(pg.sprite.Sprite):
def __init__(self, screen_rect):
super().__init__()
radius = random.randrange(5, 31)
self.image = pg.Surface((radius*2, radius*2), pg.SRCALPHA)
pg.draw.circle(self.image, pg.Color('dodgerblue1'), (radius, radius), radius)
pg.draw.circle(self.image, pg.Color('dodgerblue3'), (radius, radius), radius-2)
self.rect = self.image.get_rect(center=screen_rect.center)
self.vel = Vector2(random.uniform(-2, 2), random.uniform(-2, 2))
self.pos = Vector2(self.rect.center)
self.screen_rect = screen_rect
self.lifetime = 350
def update(self):
self.pos += self.vel
self.rect.center = self.pos
self.lifetime -= 1
if not self.screen_rect.contains(self.rect) or self.lifetime <= 0:
self.kill()
def main():
screen = pg.display.set_mode((800, 600))
screen.fill((20, 40, 70))
pg.display.update()
screen_rect = screen.get_rect()
clock = pg.time.Clock()
all_sprites = pg.sprite.Group()
# Pass this rect to `pg.display.update` to update only this area.
update_rect = pg.Rect(50, 50, 500, 400)
done = False
while not done:
for event in pg.event.get():
if event.type == pg.QUIT:
done = True
all_sprites.add(Ball(screen_rect))
all_sprites.update()
screen.fill((20, 50, 90))
all_sprites.draw(screen)
# Update only the area that we specified with the `update_rect`.
pg.display.update(update_rect)
clock.tick(60)
if __name__ == '__main__':
pg.init()
main()
pg.quit()

Tkinter - How to move image from canvas in slow motion

guys. I am trying to create my own version of a card game. I got the following problem trying to move my cards to the center of the canvas on click event. Here is an example of my code
import tkinter as tk
class gui(tk.Frame):
def __init__(self, parent, *args, **kwargs):
tk.Frame.__init__(self, parent, *args, **kwargs)
self.canvas = tk.Canvas(parent, bg="blue", highlightthickness=0)
self.canvas.pack(fill="both", expand=True)
self.img = PhotoImage(file="card.gif")
self.card = self.canvas.create_image(10, 10, image=self.img)
self.canvas.tag_bind(self.card, '<Button-1>', self.onObjectClick1)
def onObjectClick1(self, event):
if self.canvas.find_withtag("current"):
x = 400
y = 400
self.canvas.coords("current", x, y)
self.canvas.tag_raise("current")
if __name__ == "__main__":
root = tk.Tk()
w, h = root.winfo_screenwidth(), root.winfo_screenheight()
root.geometry("%dx%d+0+0" % (w, h))
gui(root)
root.mainloop()
What I want is to move my card but not just move from one coordinate to another but giving it in slow motion effect.
The basic idea is to write a function that moves an object a small amount, and then schedules itself to be called again after a short delay. It does this until it reaches its destination.
Here is a very simple example that moves a couple items independently. You can adjust the speed by changing the speed parameter, or by changing the values of delta_x and delta_y.
This is a very simplistic algorithm that just increases the x and y coordinates by a fixed amount. You could instead calculate equally spaced points along a curve or straight line. Regardless, the animation technique remains the same.
import Tkinter as tk
def move_object(canvas, object_id, destination, speed=50):
dest_x, dest_y = destination
coords = canvas.coords(object_id)
current_x = coords[0]
current_y = coords[1]
new_x, new_y = current_x, current_y
delta_x = delta_y = 0
if current_x < dest_x:
delta_x = 1
elif current_x > dest_x:
delta_x = -1
if current_y < dest_y:
delta_y = 1
elif current_y > dest_y:
delta_y = -1
if (delta_x, delta_y) != (0, 0):
canvas.move(object_id, delta_x, delta_y)
if (new_x, new_y) != (dest_x, dest_y):
canvas.after(speed, move_object, canvas, object_id, destination, speed)
root = tk.Tk()
canvas = tk.Canvas(root, width=400, height=400)
canvas.pack()
item1 = canvas.create_rectangle(10, 10, 30, 30, fill="red")
item2 = canvas.create_rectangle(360, 10, 380, 30, fill="green")
move_object(canvas, item1, (200, 180), 25)
move_object(canvas, item2, (200, 220), 50)
root.mainloop()
In order to 'animate' your cards moving, a system of breaking down the total distance to be moved, and then moving/updating by smaller distances over a time-period would work.
For example, if you wish to move a card 400 units in x & y, something like this could work:
total_time = 500 #Time in milliseconds
period = 8
dx = 400/period
dy = 400/period
for i in range(period):
self.canvas.move(chosen_card, dx, dy)
root.after(total_time/period) #Pause for time, creating animation effect
root.update() #Update position of card on canvas
This could be a basic premise for an animation. Of course you would need to edit the total_time and period variables in my example to create what you feel is right.
This code below (ready for copy/paste and run as it is) gives a nice smooth motion on my box:
import tkinter as tk
import time
class gui(tk.Frame):
def __init__(self, parent, *args, **kwargs):
tk.Frame.__init__(self, parent, *args, **kwargs)
self.canvas = tk.Canvas(parent, bg="blue", highlightthickness=0)
self.canvas.pack(fill="both", expand=True)
self.img = tk.PhotoImage(file="card.gif")
self.card = self.canvas.create_image(10, 10, image=self.img)
self.canvas.tag_bind(self.card, '<Button-1>', self.onObjectClick1)
def onObjectClick1(self, event):
if self.canvas.find_withtag("current"):
x = 400
y = 400
self.canvas.coords("current", x, y)
self.canvas.tag_raise("current")
total_time = 500 #Time in milliseconds
period = 400
dx = 400/period
dy = 400/period
for i in range(period):
self.canvas.move(self.card, dx, dy) # chosen_card
time.sleep(0.01)
# root.after(total_time/period) #Pause for time, creating animation effect
root.update() #Update position of card on canvas
if __name__ == "__main__":
root = tk.Tk()
w, h = root.winfo_screenwidth(), root.winfo_screenheight()
root.geometry("%dx%d+0+0" % (w, h))
gui(root)
root.mainloop()

Generate 10 balls in tkinter canvas

Ok guys.
I am trying to generate 10 balls of random color in Tkinter canvas when I click the generate button.
Program works, and random color choice works for the ball, but I only get one ball generated at a time.
Every time I click the button it randomly moves the ball around, but all I want is 10 balls in 10 random positions at a time. I am using Python 3.4 on a Linux box.
This is a code I've got:
from tkinter import *
import random # to generate random balls
colors = ["red", "blue", "purple", "green", "violet", "black"]
class RandomBalls:
"""
Boilerplate code for window in Tkinter
window = Tk()
window.title("Random title")
window.mainloop()
"""
def __init__(self):
"""
Initialize the window and add two frames, one with button, and another one with
canvas
:return:
"""
window = Tk()
window.title("Random balls")
# A canvas frame
frame1 = Frame(window)
frame1.pack()
self.canvas = Canvas(frame1, width = 200, height = 300, bg = "white")
self.canvas.pack()
# A button frame
frame2 = Frame(window)
frame2.pack()
displayBtn = Button(frame2, text = "Display", command = self.display)
displayBtn.pack()
window.mainloop()
def display(self):
for i in range(0, 10):
self.canvas.delete("circle") # delete references to the old circle
self.x1 = random.randrange(150)
self.y1 = random.randrange(200)
self.x2 = self.x1 + 5
self.y2 = self.y1 + 5
self.coords = self.x1, self.y1, self.x2, self.y2
self.canvas.create_oval(self.coords, fill = random.choice(colors), tags = "circle")
self.canvas.update()
RandomBalls()
Every time through your loop you are deleting everything you created before, including what you created the previous iteration. Move the delete statement outside of the loop:
def display(self):
self.canvas.delete("circle")
for i in range(0, 10):
...

Resources