Trong phần này chúng ta viết lại một game rất nổi tiếng đó là game rắn săn mồi (Snake).
Snake
Game này được phát hành vào những năm 70 trên các hệ máy cầm tay, một thời gian sau được đưa lên PC. Trong game người chơi sẽ điều khiển một con rắn. Mục tiêu của người chơi là cố gắng cho con rắn ăn được càng nhiều mồi càng tốt. Mỗi lần con rắn ăn mồi, cơ thể nó sẽ dài ra. Game kết thúc khi người chơi cho rắn “ăn” phải tường hoặc cơ thể chính mình.
Mô tả game
Kích thước của mỗi đốt trên con rắn là 10px. Con rắn sẽ được điều khiển bằng các phím mũi tên. Khi game kết thúc, dòng chữ “Game Over” sẽ hiện lên giữa màn hình.
import sys import random from PIL import Image, ImageTk from tkinter import Tk, Frame, Canvas, ALL, NW WIDTH = 300 HEIGHT = 300 DELAY = 100 DOT_SIZE = 10 ALL_DOTS = 900 #WIDTH * HEIGHT / (DOT_SIZE * DOT_SIZE) RAND_POS = 27 x = [0] * ALL_DOTS y = [0] * ALL_DOTS class Board(Canvas): def __init__(self, parent): Canvas.__init__(self, width=WIDTH, height=HEIGHT, background="black", highlightthickness=0) self.parent = parent self.initGame() self.pack() def initGame(self): self.right = True self.left = False self.up = False self.down = False self.inGame = True self.dots = 3 self.apple_x = 100 self.apple_y = 190 for i in range(self.dots): x[i] = 50 - i * 10 y[i] = 50 try: self.dotImage = Image.open("dot.png") self.dotImage.thumbnail((10, 10), Image.ANTIALIAS) self.dot = ImageTk.PhotoImage(self.dotImage) self.headImage = Image.open("head.png") self.headImage.thumbnail((10, 10), Image.ANTIALIAS) self.head = ImageTk.PhotoImage(self.headImage) self.appleImage = Image.open("apple.png") self.appleImage.thumbnail((10, 10), Image.ANTIALIAS) self.apple = ImageTk.PhotoImage(self.appleImage) except IOError as e: print (e) sys.exit(1) self.createObjects() self.locateApple() self.bind_all("<Key>", self.onKeyPressed) self.after(DELAY, self.onTimer) def createObjects(self): self.create_image(self.apple_x, self.apple_y, image=self.apple, anchor=NW, tag="apple") self.create_image(x[0], y[0], image=self.head, anchor=NW, tag="head") self.create_image(x[1], y[1], image=self.dot, anchor=NW, tag="dot") self.create_image(x[2], y[2], image=self.dot, anchor=NW, tag="dot") def locateApple(self): apple = self.find_withtag("apple") self.delete(apple[0]) r = random.randint(0, RAND_POS) self.apple_x = r * DOT_SIZE r = random.randint(0, RAND_POS) self.apple_y = r * DOT_SIZE self.create_image(self.apple_x, self.apple_y, image=self.apple, anchor=NW, tag="apple") def doMove(self): dots = self.find_withtag("dot") head = self.find_withtag("head") items = dots + head z = 0 while z < len(items) - 1: c1 = self.coords(items[z]) c2 = self.coords(items[z + 1]) self.move(items[z], c2[0]-c1[0], c2[1]-c1[1]) z += 1 if self.left: self.move(head, -DOT_SIZE, 0) if self.right: self.move(head, DOT_SIZE, 0) if self.up: self.move(head, 0, -DOT_SIZE) if self.down: self.move(head, 0, DOT_SIZE) def checkCollisions(self): dots = self.find_withtag("dot") head = self.find_withtag("head") x1, y1, x2, y2 = self.bbox(head) overlap = self.find_overlapping(x1, y1, x2, y2) for dot in dots: for over in overlap: if over == dot: self.inGame = False if x1 < 0: self.inGame = False if x1 > WIDTH - DOT_SIZE: self.inGame = False if y1 < 0: self.inGame = False if y1 > HEIGHT - DOT_SIZE: self.inGame = False def checkApple(self): apple = self.find_withtag("apple") head = self.find_withtag("head") x1, y1, x2, y2 = self.bbox(head) overlap = self.find_overlapping(x1, y1, x2, y2) for ovr in overlap: if apple[0] == ovr: x, y = self.coords(apple) self.create_image(x, y, image=self.dot, anchor=NW, tag="dot") self.locateApple() def onTimer(self): if self.inGame: self.checkCollisions() self.checkApple() self.doMove() self.after(DELAY, self.onTimer) else: self.gameOver() def onKeyPressed(self, e): key = e.keysym if key == "Left" and not self.right: self.left = True self.up = False self.down = False if key == "Right" and not self.left: self.right = True self.up = False self.down = False if key == "Up" and not self.down: self.up = True self.right = False self.left = False if key == "Down" and not self.up: self.down = True self.right = False self.left = False if key == "Escape": self.quit() def gameOver(self): self.delete(ALL) self.create_text(self.winfo_width() / 2, self.winfo_height() / 2, text="Game Over", fill="white") class Snake(Frame): def __init__(self, parent): Frame.__init__(self, parent) parent.title("Snake") self.board = Board(parent) self.pack() root = Tk() snake = Snake(root) root.mainloop()
Đầu tiên chúng ta định nghĩa một số hằng số dùng trong game:
WIDTH
vàHEIGHT
là kích thước cửa sổ chính.DOT_SIZE
là kích thước của mồi và mỗi đốt trên con rắn.ALL_DOTS
là số lượng đốt tối đa của rắn. Vì cửa sổ có kích thước 300 * 300, mỗi đốt rắn có kích thước 10 * 10 nến số lượng đốt tối đa là (300 * 300) / (10 * 10) = 900.RAND_POS
là hằng số để tính vị trí ngẫu nhiên của mồi.DELAY
là tốc độ của game.
x = [0] * ALL_DOTS y = [0] * ALL_DOTS
Tiếp theo chúng ta tạo 2 mảng x và y, hai mảng này lưu trữ tọa độ của tất cả các đốt trên thân con rắn.
Kế tiếp là phương thức initGame()
, nhiệm vụ của phương thức này là khởi tạo toàn bộ mọi thứ có trong game, từ khởi tạo biến, load ảnh đến khởi động timer…
try: self.dotImage = Image.open("dot.png") self.dotImage.thumbnail((10, 10), Image.ANTIALIAS) self.dot = ImageTk.PhotoImage(self.dotImage) self.headImage = Image.open("head.png") self.headImage.thumbnail((10, 10), Image.ANTIALIAS) self.head = ImageTk.PhotoImage(self.headImage) self.appleImage = Image.open("apple.png") self.appleImage.thumbnail((10, 10), Image.ANTIALIAS) self.apple = ImageTk.PhotoImage(self.appleImage) except (IOError, e): print (e) sys.exit(1)
Đoạn code trên thực hiện load ảnh dùng để hiển thị mồi và các đốt của con rắn. Do mỗi đốt của con rắn và mồi có kích thước 10×10 pixel nên chúng ta nên resize kích thước ảnh về 10×10 phòng trường hợp ảnh của chúng ta quá lớn bằng phương thức thumbnail()
.
self.createObjects() self.locateApple()
Phương thức createObjects()
có nhiệm vụ vẽ các đối tượng lên canvas. Phương thức locateApple()
tạo tọa độ ngẫu nhiên của mồi và vẽ lên canvas.
self.bind_all("<Key>", self.onKeyPressed)
Tiếp theo chúng ta gọi phương thức bind_all()
để gắn sự kiện bấm phím vào phương thức onKeyPressed().
def createObjects(self): self.create_image(self.apple_x, self.apple_y, image=self.apple, anchor=NW, tag="apple") self.create_image(50, 50, image=self.head, anchor=NW, tag="head") self.create_image(30, 50, image=self.dot, anchor=NW, tag="dot") self.create_image(40, 50, image=self.dot, anchor=NW, tag="dot")
Bên trong phương thức createObjects()
chúng ta vẽ các đối tượng lên canvas bao gồm mồi và 3 đốt đầu của con rắn. Ý nghĩa của các tham số đã được giải thích trong bài viết trước ngoại trừ tham số tag.
Tham số tag
đơn giản là giống như chúng ta đưa một ID cho ảnh vậy thôi, về sau chúng ta sẽ có các phương thức tìm các đối tượng này nhờ vào tag
.
Bên trong phương thức checkApple()
chúng ta kiểm tra xem con rắn có ăn trúng mồi hay không, nếu có thì thêm một đốt vào sau con rắn và gọi đến phương thức locateApple()
để khởi tạo mồi mới.
apple = self.find_withtag("apple") head = self.find_withtag("head")
Phương thức find_withtag()
sẽ tìm đối tượng trên canvas dựa vào tag
của đối tượng đó. Ở đây chúng ta cần tìm 2 đối tượng là mồi và đầu của con rắn – đốt đầu tiên.
x1, y1, x2, y2 = self.bbox(head) overlap = self.find_overlapping(x1, y1, x2, y2)
Phương thức bbox()
trả về một tuple là tọa độ điểm trái-trên và phải-dưới của một đối tượng. Phương thức find_overlapping()
sẽ tìm các đối tượng nằm đè lên nhau. Tức là ở đây chúng ta kiểm tra xem đầu con rắn có chạm vào mồi hay không.
for ovr in overlap: if apple[0] == ovr: x, y = self.coords(apple) self.create_image(x, y, image=self.dot, anchor=NW, tag="dot") self.locateApple()
Nếu đầu con rắn chạm vào mồi thì chúng ta vẽ một đốt mới tại vị trí của mồi sau đó gọi đến phương thức locateApple()
để khởi tạo mồi mới.
Bên trong phương thức doMove()
chúng ta kiểm tra hướng đi hiện tại của con rắn và cập nhật tọa độ của tất cả các đốt trên con rắn.
z = 0 while z < len(items)-1: c1 = self.coords(items[z]) c2 = self.coords(items[z+1]) self.move(items[z], c2[0]-c1[0], c2[1]-c1[1]) z += 1
Tọa độ của các đốt phía sau sẽ bằng tọa độ của đốt liền trước đó. Riêng tọa độ của đầu thì dựa trên hướng đi hiện tại.
Trong phương thức checkCollisions()
chúng ta kiểm tra xem con rắn có cắn phải mình hay đụng tường hay không.
x1, y1, x2, y2 = self.bbox(head) overlap = self.find_overlapping(x1, y1, x2, y2) for dot in dots: for over in overlap: if over == dot: self.inGame = False
Nếu có thì chúng ta cho thuộc tính inGame
là False
tức là kết thúc game.
if y1 > HEIGHT - DOT_SIZE: self.inGame = False
Phương thức locateApple()
sẽ tạo ngẫu nhiên mồi mới trên màn hình.
apple = self.find_withtag("apple") self.delete(apple[0])
Trước hết chúng ta xóa mồi cũ đi.
r = random.randint(0, RAND_POS)
Sau đó dùng phương thức randint()
để lấy một số ngẫu nhiên từ 0 đến RAND_POS
– 1.
self.apple_x = r * DOT_SIZE ... self.apple_y = r * DOT_SIZE
Hai dòng trên set tọa độ của mồi.
Trong phương thức onKeyPressed()
chúng ta kiểm tra sự kiện bấm phím.
if key == "Left" and not self.right: self.left = True self.up = False self.down = False
Trong trường hợp trên chúng ta kiểm tra nếu người dùng bấm phim mũi trên trái và hướng đi hiện tại của con rắn không phải hướng bên phải thì set các thuộc tính left
là True
, các thuộc tính còn lại là False
. Tương tự với các hướng còn lại.
def onTimer(self): if self.inGame: self.checkCollisions() self.checkApple() self.doMove() self.after(DELAY, self.onTimer) else: self.gameOver()
Bên trong phương thức onTimer()
, chúng ta thực hiện một số công việc cứ sau 100 mili giây (lưu trong hằng số DELAY). Chúng ta kiểm tra sự va chạm của con rắn với mồi hay tường… cập nhật tọa độ con rắn rồi gọi phương thức after()
một lần nữa để đồng hồ chạy tiếp 100ms và lại tiếp tục kiểm tra. Nếu vì một lý do gì đó mà thuộc tính
inGame
là False
thì chúng ta gọi phuonwg thức gameOver().
def gameOver(self): self.delete(ALL) self.create_text(self.winfo_width()/2, self.winfo_height()/2, text="Game Over", fill="white")
Bên trong phương thức gameOver()
chúng ta xóa các đối tượng ra khỏi bộ nhớ và in dòng chữ “Game Over” lên giữa màn hình.