LoginSignup
11
14

More than 3 years have passed since last update.

改造版: Pythonのtkinterでブロック崩しを作ってみた。

Last updated at Posted at 2019-01-17

@motty0713 さんの「Pythonのtkinterでブロック崩しを作ってみた」を拝見したところ、いくつかの点が気になってしまいました。

  • グローバル変数を使用
  • クラス変数とインスタンス変数を混同?
  • 関数とメソッドを混同?
  • 責務が重い

そこで、以下の点を考慮して改造させていただきました。

  • グローバル変数をなくす
  • クラス変数ではなくデフォルト引数にする
  • クラス変数はインスタンス共有値のみ
  • 責務(クラス)分割・委譲
  • 処理(メソッド)分割・委譲
  • ブロックは列ごとなどに色や点数を変えられるようにする
  • パドルが複数になっても対応できる
  • ボールが複数になっても対応できる

以下にコードを示します。
本当なら、モデル(データ)とビュー(表示)にクラスを分けるべきですが、一体型になってます。
Model-View一体型とMVC(Model-View-Controller)分離型の2種類を作りました。
更に、クリーンアーキテクチャ版へのリンク追記しました(プログラミング言語はJavaですが)。
なにか参考になることがあれば幸いです。

ソースコード

Model-View一体型

import tkinter as tk


class Shape:

    def __init__(self, x, y, width, height, center=False):
        if center:
            x -= width // 2
            y -= height // 2
        self.x1 = x
        self.y1 = y
        self.x2 = x + width
        self.y2 = y + height

    def intersect(self, target):
        hit_x = max(self.x1, target.x1) <= min(self.x2, target.x2)
        hit_y = max(self.y1, target.y1) <= min(self.y2, target.y2)
        return hit_x and hit_y


class Paddle(Shape):

    def __init__(self, x, y, width=45, height=8, speed=6, color="blue"):
        super().__init__(x, y, width, height, center=True)
        self.speed = speed
        self.color = color
        self.name = "paddle"

    def right(self, event):
        self.x1 += self.speed
        self.x2 += self.speed

    def left(self, event):
        self.x1 -= self.speed
        self.x2 -= self.speed

    def limit(self, area):
        adjust = (max(self.x1, area.x1) - self.x1 or
                  min(self.x2, area.x2) - self.x2)
        self.x1 += adjust
        self.x2 += adjust

    def move(self):
        pass

    def draw(self, canvas):
        canvas.delete(self.name)
        canvas.create_rectangle(self.x1, self.y1, self.x2, self.y2,
                                fill=self.color, tag=self.name)

    def delete(self, canvas):
        canvas.delete(self.name)


class Ball(Shape):

    def __init__(self, x, y, size=10, dx=2, dy=2, color="red"):
        super().__init__(x, y, size, size, center=True)
        self.dx = dx
        self.dy = dy
        self.color = color
        self.name = "ball"

    def move(self):
        self.x1 += self.dx
        self.y1 += self.dy
        self.x2 += self.dx
        self.y2 += self.dy

    def limit(self, area):
        if self.x1 <= area.x1 or area.x2 <= self.x2:
            self.dx *= -1
        if self.y1 <= area.y1 or area.y2 <= self.y2:
            self.dy *= -1

    def bound(self, target):
        if not self.intersect(target):
            return False
        center_x = (self.x1 + self.x2) // 2
        center_y = (self.y1 + self.y2) // 2
        if (self.dx > 0 and center_x <= target.x1 or
            self.dx < 0 and target.x2 <= center_x):
                self.dx *= -1
        if (self.dy > 0 and center_y <= target.y1 or
            self.dy < 0 and target.y2 <= center_y):
                self.dy *= -1
        return True

    def draw(self, canvas):
        canvas.delete(self.name)
        canvas.create_oval(self.x1, self.y1, self.x2, self.y2,
                           fill=self.color, tag=self.name)

    def delete(self, canvas):
        canvas.delete(self.name)


class Block(Shape):

    def __init__(self, x, y, width, height, gap_x=0, gap_y=0, center=False,
                 color="orange", point=1):
        super().__init__(x + gap_x, y + gap_y,
                         width - gap_x * 2, height - gap_y * 2, center=center)
        self.point = point
        self.color = color
        self.name = f"block{x}.{y}"
        self.exists = True

    def break_and_bound(self, target):
        if self.exists and target.bound(self):
            self.exists = False
            return self.point
        else:
            return 0

    def is_broken(self):
        return not self.exists

    def draw(self, canvas):
        canvas.delete(self.name)
        if self.exists:
            canvas.create_rectangle(self.x1, self.y1, self.x2, self.y2,
                                    fill=self.color, tag=self.name)


class BlockRow:
    def __init__(self, color, point):
        self.color = color
        self.point = point


class Blocks:
    #ROWS = [BlockRow("orange", 1)] * 3
    ROWS = BlockRow("cyan", 10), BlockRow("yellow", 20), BlockRow("orange", 30)

    def __init__(self, x, y, width, height, columns=12, rows=None):
        rows = (rows or self.ROWS)[::-1]
        w = width // columns
        h = height // len(rows)
        self.blocks = [Block(x + dx, y + dy, w, h, gap_x=6, gap_y=12,
                             color=row.color, point=row.point)
                       for dy, row in zip(range(0, h * len(rows) + 1, h), rows)
                       for dx in range(0, w * columns + 1, w)]

    def break_and_bound(self, target):
        return sum(block.break_and_bound(target) for block in self.blocks)

    def are_wiped(self):
        return all(block.is_broken() for block in self.blocks)

    def draw(self, canvas):
        for block in self.blocks:
            block.draw(canvas)


class Wall(Shape):
    CATCH_LINES = 3

    def __init__(self, x, y, width, height):
        super().__init__(x, y, width, height)
        self.catch_line = self.y2 - self.CATCH_LINES

    def catch(self, target):
        return target.y2 >= self.catch_line


class Score:
    FONT = ('FixedSys', 16)

    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.score = 0
        self.name = "score"

    def add(self, point):
        self.score += point

    def draw(self, canvas):
        canvas.delete(self.name)
        canvas.create_text(self.x, self.y, text=f"Score = {self.score}",
                           font=self.FONT, tag=self.name)


class Breakout:
    TICK = 20  # 更新間隔
    FONT = ('FixedSys', 40)

    def __init__(self, width, height):
        self.center = (width // 2, height // 2)
        self.root = tk.Tk()
        self.root.title("ブロック崩し")
        self.root.minsize(width, height)
        self.root.maxsize(width, height)
        self.canvas = tk.Canvas(self.root, width=width, height=height)
        self.paddle = Paddle(width // 2, height - 30)
        self.ball = Ball(width // 3, height * 2 // 3)
        self.blocks = Blocks(0, 40, width, 120)
        self.wall = Wall(0, 0, width, height)
        self.score = Score(width - 70, 20)
        self.keybind()
        self.draw()

    def keybind(self):
        self.root.bind("q", self.quit)
        self.root.bind("<Right>", self.paddle.right)
        self.root.bind("<Left>", self.paddle.left)

    def start(self):
        self.playing = True
        self.play()
        try:
            self.root.mainloop()
        except KeyboardInterrupt:
            self.quit()

    def end(self, message):
        self.paddle.delete(self.canvas)
        self.ball.delete(self.canvas)
        self.canvas.create_text(*self.center, text=message, font=self.FONT)
        self.canvas.pack()
        self.playing = False 

    def quit(self, *args):
        self.root.quit()

    def play(self):
        try:
            if self.playing:
                self.operate()
                self.draw()
                self.root.after(self.TICK, self.play)
            else:
                input("Hit return key to end")
                self.quit()
        except KeyboardInterrupt:
            self.quit()

    def operate(self):
        self.paddle.move()
        self.paddle.limit(self.wall)
        self.ball.move()
        self.ball.limit(self.wall)
        self.ball.bound(self.paddle)
        point = self.blocks.break_and_bound(self.ball)
        self.score.add(point)
        self.over()
        self.clear()

    def draw(self):
        self.ball.draw(self.canvas)
        self.paddle.draw(self.canvas)
        self.blocks.draw(self.canvas)
        self.score.draw(self.canvas)
        self.canvas.pack()

    def over(self):
        if self.wall.catch(self.ball):
            self.end("GAME OVER(T_T)")

    def clear(self):
        if self.blocks.are_wiped():
            self.end("GAME CLEAR(^0^)")


if __name__ == '__main__':
    Breakout(width=600, height=480).start()

MVC(Model-View-Controller)分離型

import tkinter as tk


class Shape:

    def __init__(self, x, y, width, height, center=False):
        if center:
            x -= width // 2
            y -= height // 2
        self.x1 = x
        self.y1 = y
        self.x2 = x + width
        self.y2 = y + height

    def intersect(self, target):
        hit_x = max(self.x1, target.x1) <= min(self.x2, target.x2)
        hit_y = max(self.y1, target.y1) <= min(self.y2, target.y2)
        return hit_x and hit_y


class Paddle(Shape):

    def __init__(self, x, y, width=45, height=8, speed=6, color="blue"):
        super().__init__(x, y, width, height, center=True)
        self.speed = speed
        self.color = color
        self.name = "paddle"

    def right(self, event):
        self.x1 += self.speed
        self.x2 += self.speed

    def left(self, event):
        self.x1 -= self.speed
        self.x2 -= self.speed

    def move(self):
        pass

    def limit(self, area):
        adjust = (max(self.x1, area.x1) - self.x1 or
                  min(self.x2, area.x2) - self.x2)
        self.x1 += adjust
        self.x2 += adjust


class Ball(Shape):

    def __init__(self, x, y, size=10, dx=2, dy=2, color="red"):
        super().__init__(x, y, size, size, center=True)
        self.dx = dx
        self.dy = dy
        self.color = color
        self.name = "ball"

    def move(self):
        self.x1 += self.dx
        self.y1 += self.dy
        self.x2 += self.dx
        self.y2 += self.dy

    def limit(self, area):
        if self.x1 <= area.x1 or area.x2 <= self.x2:
            self.dx *= -1
        if self.y1 <= area.y1 or area.y2 <= self.y2:
            self.dy *= -1

    def bound(self, target):
        if not self.intersect(target):
            return False
        center_x = (self.x1 + self.x2) // 2
        center_y = (self.y1 + self.y2) // 2
        if (self.dx > 0 and center_x <= target.x1 or
            self.dx < 0 and target.x2 <= center_x):
                self.dx *= -1
        if (self.dy > 0 and center_y <= target.y1 or
            self.dy < 0 and target.y2 <= center_y):
                self.dy *= -1
        return True


class Block(Shape):

    def __init__(self, x, y, width, height, gap_x=0, gap_y=0, center=False,
                 color="orange", point=1):
        super().__init__(x + gap_x, y + gap_y,
                         width - gap_x * 2, height - gap_y * 2, center=center)
        self.point = point
        self.color = color
        self.name = f"block{x}.{y}"
        self.exists = True

    def break_and_bound(self, target):
        if self.exists and target.bound(self):
            self.exists = False
            return self.point
        else:
            return 0

    def is_broken(self):
        return not self.exists


class BlockRow:
    def __init__(self, color, point):
        self.color = color
        self.point = point


class Blocks:
    #ROWS = [BlockRow("orange", 1)] * 3
    ROWS = BlockRow("cyan", 10), BlockRow("yellow", 20), BlockRow("orange", 30)

    def __init__(self, x, y, width, height, columns=12, rows=None):
        rows = (rows or self.ROWS)[::-1]
        w = width // columns
        h = height // len(rows)
        self.blocks = [Block(x + dx, y + dy, w, h, gap_x=6, gap_y=12,
                             color=row.color, point=row.point)
                       for dy, row in zip(range(0, h * len(rows) + 1, h), rows)
                       for dx in range(0, w * columns + 1, w)]

    def __iter__(self):
        return iter(self.blocks)

    def break_and_bound(self, target):
        return sum(block.break_and_bound(target) for block in self.blocks)

    def are_wiped(self):
        return all(block.is_broken() for block in self.blocks)


class Wall(Shape):
    CATCH_LINES = 3

    def __init__(self, x, y, width, height):
        super().__init__(x, y, width, height)
        self.catch_line = self.y2 - self.CATCH_LINES

    def catch(self, target):
        return target.y2 >= self.catch_line


class Score:

    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.score = 0
        self.name = "score"

    def __str__(self):
        return str(self.score)

    def add(self, point):
        self.score += point


class BreakoutModel:

    def __init__(self, width, height):
        self.paddle = Paddle(width // 2, height - 30)
        self.ball = Ball(width // 3, height * 2 // 3)
        self.blocks = Blocks(0, 40, width, 120)
        self.wall = Wall(0, 0, width, height)
        self.score = Score(width - 70, 20)
        self.message = None

    def update(self):
        self.paddle.move()
        self.paddle.limit(self.wall)
        self.ball.move()
        self.ball.limit(self.wall)
        self.ball.bound(self.paddle)
        point = self.blocks.break_and_bound(self.ball)
        self.score.add(point)
        self.over()
        self.clear()

    def over(self):
        if self.wall.catch(self.ball):
            self.message = "GAME OVER(T_T)"

    def clear(self):
        if self.blocks.are_wiped():
            self.message = "GAME CLEAR(^0^)"


class BreakoutView:
    SCORE_FONT = ('FixedSys', 16)
    MESSAGE_FONT = ('FixedSys', 40)

    def __init__(self, window, width, height):
        self.canvas = tk.Canvas(window, width=width, height=height)
        self.center = (width // 2, height // 2)
        window.title("ブロック崩し")
        window.minsize(width, height)
        window.maxsize(width, height)
        self.rectangle = self.canvas.create_rectangle
        self.oval = self.canvas.create_oval
        self.text = self.canvas.create_text

    def update(self, model):
        self.ball(model.ball)
        self.paddle(model.paddle)
        self.blocks(model.blocks)
        self.score(model.score)
        self.message(model.message)
        self.canvas.pack()

    def delete(self, model):
        self.canvas.delete(model.name)

    def paddle(self, paddle):
        self.delete(paddle)
        self.rectangle(paddle.x1, paddle.y1, paddle.x2, paddle.y2,
                       fill=paddle.color, tag=paddle.name)

    def ball(self, ball):
        self.delete(ball)
        self.oval(ball.x1, ball.y1, ball.x2, ball.y2,
                  fill=ball.color, tag=ball.name)

    def block(self, block):
        self.delete(block)
        if block.exists:
            self.rectangle(block.x1, block.y1, block.x2, block.y2,
                           fill=block.color, tag=block.name)

    def blocks(self, blocks):
        for block in blocks:
            self.block(block)

    def score(self, score):
        self.delete(score)
        self.text(score.x, score.y, text=f"Score = {score}",
                  font=self.SCORE_FONT, tag=score.name)

    def message(self, message):
        if message:
            self.text(*self.center, text=message, font=self.MESSAGE_FONT)


class Breakout:
    TICK = 20  # 更新間隔

    def __init__(self, width, height):
        self.controller = window = tk.Tk()
        self.model = BreakoutModel(width, height)
        self.view = BreakoutView(window, width, height)
        self.view.update(self.model)
        self.keybind()

    def keybind(self):
        self.controller.bind("q", self.controller.quit)
        self.controller.bind("<Right>", self.model.paddle.right)
        self.controller.bind("<Left>", self.model.paddle.left)

    def start(self):
        self.update()
        try:
            self.controller.mainloop()
        except KeyboardInterrupt:
            self.controller.quit()

    def update(self):
        try:
            if self.model.message:
                input("Hit return key to end")
                self.controller.quit()
            else:
                self.model.update()
                self.view.update(self.model)
                self.controller.after(self.TICK, self.update)
        except KeyboardInterrupt:
            self.controller.quit()


if __name__ == '__main__':
    Breakout(width=600, height=480).start()

さらに

クリーンアーキテクチャ版

最近は、クリーンアーキテクチャでの設計が話題です。
ソースコードはJavaですが、クリーンアーキテクチャでブロック崩しゲームを作りましたので是非参考にしてみてください。
参考: クリーンアーキテクチャでブロック崩しゲームを設計・実装 - Qiita

11
14
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
14