DEV Community

Kajiru
Kajiru

Posted on

Getting Started with 2D Games Using Tkinter (Part 7): Stopping Sprites with Mouse Clicks

Stopping Sprites with Mouse Clicks

In this article, we will implement mouse click detection for sprites and introduce basic state management.

By clicking on a sprite, we will stop it and change its state.

Improving the DemonSprite Class

First, we add a new member variable called dead to the DemonSprite constructor.
This flag represents whether the sprite is alive or dead.

The initial value is set to False (alive).
When a demon sprite is clicked, the value will be changed to True (dead).

# sprite.py (excerpt)

def __init__(self, cvs, x, y, r):
    self.x = x
    self.y = y
    self.r = r
    self.vx = 0
    self.vy = 0
    self.dead = False  # death flag
    # draw a circle
    self.oval = cvs.create_oval(
        x - r, y - r,
        x + r, y + r,
        fill="white", width=0
    )
Enter fullscreen mode Exit fullscreen mode

Next, we add a die() method.
This method changes the sprite into a dead state by setting the dead flag to True,
stopping the sprite, and changing its color to red.

# sprite.py (excerpt)

def die(self, cvs):
    self.dead = True   # set death flag
    self.stop()        # stop movement
    # update circle color
    cvs.itemconfig(self.oval, fill="red")
Enter fullscreen mode Exit fullscreen mode

Then, we add an is_dead() method that returns the current death state of the sprite.
If it returns False, the sprite is alive.
If it returns True, the sprite is dead.

# sprite.py (excerpt)

def is_dead(self):
    return self.dead  # return death flag
Enter fullscreen mode Exit fullscreen mode

Finally, we add an is_hit() method.
This method receives the mouse click coordinates and calculates the distance between
the sprite and the clicked position.

If the calculated distance is smaller than the sprite’s radius, the method returns True.
Otherwise, it returns False.

# sprite.py (excerpt)

def is_hit(self, x, y):
    dist_x = (self.x - x) ** 2  # horizontal distance
    dist_y = (self.y - y) ** 2  # vertical distance
    dist = (dist_x + dist_y) ** 0.5  # distance to sprite
    return dist < self.r  # hit if inside radius
Enter fullscreen mode Exit fullscreen mode

Updating main.py

In the on_mouse_clicked() function implemented in main.py,
we can access the mouse click coordinates.

At this point, we check each sprite to see if it was clicked.
There is no need to check sprites that are already dead, so we skip them using is_dead().

If a living sprite is hit, we call the die() method and stop checking further sprites.

# main.py (excerpt)

def on_mouse_clicked(e):
    print("Clicked:", e.x, e.y)

    # demon group
    for demon in demons:
        if demon.is_dead():
            continue  # skip already dead sprites

        if demon.is_hit(e.x, e.y):
            demon.die(cvs)  # kill the sprite
            break           # stop checking
Enter fullscreen mode Exit fullscreen mode

With this implementation, you can now stop sprites by clicking on them.

Complete Code

Below is the complete code with all features implemented so far.

# sprite.py (complete)

import math
import random
import tkinter

# Demon sprite class
class DemonSprite:

    def __init__(self, cvs, x, y, r):
        self.x = x
        self.y = y
        self.r = r
        self.vx = 0
        self.vy = 0
        self.dead = False  # death flag

        # draw a circle
        self.oval = cvs.create_oval(
            x - r, y - r,
            x + r, y + r,
            fill="white", width=0
        )

    def update(self, cvs):
        self.x = self.x + self.vx
        self.y = self.y + self.vy

        cvs.coords(
            self.oval,
            self.x - self.r, self.y - self.r,
            self.x + self.r, self.y + self.r
        )

    def set_x(self, x):
        self.x = x

    def set_y(self, y):
        self.y = y

    def move(self, spd, deg):
        radian = deg * math.pi / 180
        self.vx = spd * math.cos(radian)
        self.vy = spd * math.sin(radian)

    def stop(self):
        self.move(0, 0)

    def die(self, cvs):
        self.dead = True
        self.stop()
        cvs.itemconfig(self.oval, fill="red")

    def is_dead(self):
        return self.dead

    def is_hit(self, x, y):
        dist_x = (self.x - x) ** 2
        dist_y = (self.y - y) ** 2
        dist = (dist_x + dist_y) ** 0.5
        return dist < self.r
Enter fullscreen mode Exit fullscreen mode
# main.py (complete)

import math
import random
import sprite
import tkinter

W, H = 480, 320

F_RATE = 30
F_INTERVAL = int(1000 / F_RATE)

FONT = ("Arial", 16)

mx, my = 0, 0

bg_photo, bg_image = None, None

TOTAL_DEMONS = 10
demons = []

def init():
    """ initialization """
    global bg_photo, bg_image

    bg_photo = tkinter.PhotoImage(file="images/bg_jigoku.png")
    bg_image = cvs.create_image(W / 2, H / 2, image=bg_photo)

    # create demon sprites
    for i in range(TOTAL_DEMONS):
        x = random.random() * W
        y = random.random() * H
        demon = sprite.DemonSprite(cvs, x, y, 20)
        spd = random.randint(1, 4)
        deg = random.randint(0, 360)
        demon.move(spd, deg)
        demons.append(demon)

def update():
    """ update loop """
    cvs.delete("hud")

    msg = "x:{}, y:{}".format(mx, my)
    cvs.create_text(
        mx, my,
        text=msg,
        fill="white",
        font=FONT,
        tag="hud"
    )

    for demon in demons:
        overlap_area(demon)
        demon.update(cvs)

    root.after(F_INTERVAL, update)

def overlap_area(obj):
    if obj.x < 0:
        obj.set_x(W)
    if W < obj.x:
        obj.set_x(0)
    if obj.y < 0:
        obj.set_y(H)
    if H < obj.y:
        obj.set_y(0)

def on_mouse_clicked(e):
    print("Clicked:", e.x, e.y)

    for demon in demons:
        if demon.is_dead():
            continue

        if demon.is_hit(e.x, e.y):
            demon.die(cvs)
            break

def on_mouse_moved(e):
    global mx, my
    mx, my = e.x, e.y

# Tkinter setup
root = tkinter.Tk()
root.title("Hello, Tkinter!!")
root.resizable(False, False)
root.bind("<Button>", on_mouse_clicked)
root.bind("<Motion>", on_mouse_moved)

# Canvas
cvs = tkinter.Canvas(width=W, height=H, bg="black")
cvs.pack()

init()
update()
root.mainloop()
Enter fullscreen mode Exit fullscreen mode

What’s Next?

Thank you for reading!
In the next article, we will attach demon images to the sprites.

Stay tuned!

Top comments (0)