Trong chương 9, ta đã thấy ứng dụng của Python, Pygame để vẽ hoạt hình và tương tác. Trong chương này chúng ta sẽ học cách lập trình để tạo trò chơi (game) trên máy Điện Toán.
Cụ thể ta sẽ làm một trò chơi kết hợp vẽ hoạt hình trên màn hình với tương tác giữa người sử dụng và máy Điện toán. Trò chơi thuộc loại cổ điển, loại Pong game (trò chơi bóng bàn ping pong) mà ta gọi là Smiley Pong.
Trò chơi cần:
- một sân chơi : một màn hình tối đen là nửa bàn pingpong
- Điểm: đấu thủ phải cố lập điểm và tránh mất lives (turns)
- Luật chơi: Đấu thủ được 1 điểm khi bóng (cái mặt cười) chạm vợt nhưng sẽ mất một live nếu bóng chạm đáy màn hình
- Cách vận hành trò chơi: ta sẽ cho cây vợt (hình chữ nhật trắng, dưới đáy màn hình) di chuyển qua trái, qua phải bằng con chuột ; bóng có thể di chuyển càng lúc càng nhanh hơn khi trò chơi tiếp diễn.
- Tài nguyên: Đấu thủ sẽ có 5 lives hay turns (lượt) để kiếm càng nhiều điểm (points) càng tốt.
THỰC HIỆN CHƯƠNG TRÌNH:
1) Đầu tiên vẽ CÂY VỢT:
Để khởi động cây vợt ta cần cung cấp những dữ liệu sau cho phần khởi động của chương trình:
WHITE = (255, 255, 255) # bộ màu RGB của màu trắng # vợt màu trắng paddlew = 200 # bề dài vợt (paddle) 200 pixels paddleh = 25 # bề cao vợt 25 pixels paddlex = 300 # hoành độ x của góc trái trên của vợt paddley = 550 # tung độ y của góc trái trên của vợt
Vì cây vợt phải di chuyển qua lại ở dưới đáy màn hình tuỳ theo sự di chuyển theo chiều ngang màn hình (hoành độ x) để đón diệt quả bóng (cái mặt cười) nên ta chỉ cần hoành độ x của chuột, không cần tung độ y. Hoành độ này sẽ do hàm pygame.mouse.get_pos() [0] cho; ta để nó vào biến paddlex:
paddlex = pygame.mouse.get_pos() [0].
Ngoài ra khi di chuyển chuột, ta muốn điểm giữa của vợt sẽ luôn luôn ở nơi của chuột (tung độ x của chuột sẽ ngang với điểm giữa của cây vợt) nghiã là: paddlex – = paddlew/2
Bạn xem hình sẽ rõ hơn:
Sau đó, ta vẽ hình chữ nhật (cây vợt) trên màn hình bằng hàm sau:
pygame.draw.rect(screen, WHITE, (paddlex, paddley, paddlew, paddleh)). Những gì trong các dấu ngoặc là những đối số (arguments) của hàm.
2) GHI ĐIỂM: Khi vợt chạm vào bóng (cái mât cười) thì ta được 1 điểm và ta mất một live khi ta đánh hụt bóng và khi bóng chạm đáy màn hình. Hình 10-3 cho thấy ta còn 5 live và được 8 điểm:
Khởi đầu thì ta có 5 live và 0 điểm: points=0 và lives=5.
- Khi nào thì mất một live? Đó là khi quả bóng bàn (mặt cười) chạm đáy màn hình, khi đấu thủ dùng vợt đánh hụt nó.
Khi bóng chạm trần màn hình (picy <=0) , ta muốn nó dội lại nên ta phải đổi chiều của vận tốc bóng theo chiều y (chiều dọc) bằng –speedy:
If picy <= 0:
speedy = -speedy. Còn nếu bóng dội ra ngoài đáy màn hình (picy >= 500) ta sẽ phải giảm một live và rồi cho bóng dội ngược lại:
if picy >= 500:
lives -= 1
speedy = -speedy
3) Đánh quả bóng bằng vợt
Toạ độ của quả bóng (mặt cười) là (picx, picy)
Bề ngang quả bóng là picw
Bề cao quả bóng là pich (bóng tròn nên bề cao hay ngang đều như nhau)
Toạ độ của góc trên bên trái của cây vợt là (paddlex, paddley)
Bề dầy (chiều cao) của cây vợt là paddleh và chiều dài là paddlew
Nhìn hình vẽ ta thấy muốn chạm mặt vợt cần hội đủ các điều kiện sau:
- picx + picw/2 >= paddlex (trường hợp tận cùng bên trái vợt)
- picx + picw/2 <= paddlex + paddlew (trường hợp tận củng bên phải vợt)
- picy + pich >= paddley
- picy của quả bóng + chiều cao pich của nó không được vượt quá đáy màn hình, nghiã là ta phải có: picy + pich <= paddley + paddleh.
- Ngoài ra ta còn kiểm soát cho quả bóng chạy xuống để có thể chạm cây vợt nằm ở đáy màn hình, tức là vận tốc đi xuống của quả bóng phải là speedy > 0.
Khi bóng đã chạm vợt thì đấu thủ ta được 1 điểm (points) và ta phải cho bóng dội ngược trở lại để tiếp tục bằng cách cho speedy = – speedy. Tổng hợp những điều nói trên, ta viết những câu lệnh sau:
If picy + pich >= paddley and picy + pich <= paddley + paddleh and speedy > 0: If picx + picw/2 >= paddlex and picx + picw/2<= paddlex + paddlew: points += 1 speedy = -speedy
4) cho hiện số điểm lên màn hình
- Đầu tiên ta lập một biến có tên draw_string chứa một string (chuỗi) như sau:
draw_string = “Lives: “ + str(lives) + “Points: “ + str(points)
Những chữ đậm để bạn lưu ý đó là dạng một hàm (function) . Khi gõ chương trình không cần chữ đậm
Vì lives và points là biến số nên cần hàm str() để chuyển chúng thành string (text). Còn kiểu chữ (font), size chữ, ta có thể dùng Times và 24 pixels với hàm pygame.font.SysFont() như sau:
font = pygame.font.SysFont(“Times”, 24)
Để vẽ chữ lên cửa sổ màn hình, trước hết ta phải vẽ string ấy lên một mặt phẳng (surface) của nó bằng hàm render():
text = font.render(draw_string, True, WHITE) . Lệnh này tạo ra một biến có tên là text để lưu trữ một mặt phẳng chứa những pixels màu trắng cho tất cả các chữ, số và các ký hiệu (symbols) của string của chúng ta.
Bước kế tiếp là ta lấy kích thước (bề ngang và chiều cao) của mặt phẳng ấy. String sẽ được trả lại lên một mặt phẳng (surface) hình chữ nhật mà ta cho tên là text_rect để chứa string ấy:
text_rect = text.get_rect()
Xong, ta cho hình chữ nhật text_rect này vào giữa màn hình và cho nó xuống 10 pixels dưới trần (top) màn hình, bằng những lệnh sau:
text_rect.centerx = screen.get_rect().centerx
Sau cùng thì ta cho vẽ text_rect lên màn hình bằng lệnh sau:
screen.blit(text, text_rect) .
Sau đây là chương trình để thực hiện game bóng bàn Smiley Pong:
# SmileyPong1.py import pygame # Setup pygame.init() screen = pygame.display.set_mode([800,600]) pygame.display.set_caption("Ping Pong") keepGoing = True pic = pygame.image.load("CrazySmile.bmp") colorkey = pic.get_at((0,0)) # Để góc hình được trong suốt (transparent) pic.set_colorkey(colorkey) # Để góc hình được trong suốt (transparent) picx = 0 picy = 0 BLACK = (0,0,0) WHITE = (255,255,255) timer = pygame.time.Clock() speedx = 5 speedy = 5 paddlew = 200 paddleh = 25 paddlex = 300 paddley = 550 picw = 100 pich = 100 points = 0 lives = 5 font = pygame.font.SysFont("Times", 24) while keepGoing: # Game loop for event in pygame.event.get(): if event.type == pygame.QUIT: keepGoing = False picx += speedx picy += speedy if picx <= 0 or picx + pic.get_width() >= 800: # bóng chạm rià trái hay phải màn hình speedx = -speedx if picy <= 0: # bóng chạm trần màn hình speedy = -speedy if picy >= 500: lives -= 1 # bóng chạm đáy màn hình, đấu thủ mất một live speedy = -speedy # cho bóng dội lạ screen.fill(BLACK) screen.blit(pic, (picx, picy)) # Vẽ cây vợt paddlex = pygame.mouse.get_pos()[0] paddlex -= paddlew/2 pygame.draw.rect(screen, WHITE, (paddlex, paddley, paddlew, paddleh)) # Kiểm soát bóng dội lại (bounce) if picy + pich >= paddley and picy + pich <= paddley + paddleh \ and speedy > 0: if picx + picw / 2 >= paddlex and picx + picw / 2 <= paddlex + \ paddlew: points += 1 speedy = -speedy # Viết kết quả lên màn hình draw_string = "Lives: " + str(lives) + " Points: " + str(points) text = font.render(draw_string, True, WHITE) text_rect = text.get_rect() text_rect.centerx = screen.get_rect().centerx text_rect.y = 10 screen.blit(text, text_rect) pygame.display.update() timer.tick(60) pygame.quit() # Exit
5) Dứt game
Cuộc đấu ping pong chấm dứt tạm khi đấu thủ chẳng còn một live nào nữa , tức là khi live<1. Lúc đó ta phải cho bóng ngưng (speed x và speed y bằng 0) và ta cho máy báo ra là ván này chấm dứt và bạn đã được bao nhiêu points cũng như bảo bạn nhấn F1 nếu muốn đấu nữa.
Sau đây là những câu lệnh thực hiện:
If lives < 1: speedx = speedy = 0 draw_string = “Game over, your score was: “ + str(points) draw_string += “. Press F1 to play again. “ # để nối câu này vào với câu trên trong draw_string
6) Chơi lại – Muốn biết đấu thủ đã nhấn F1 để chơi nữa hay không, bạn cần phải test trước:
If event.type == pygame.KEYDOWN: If event.key == pygame.K_F1: Nếu đấu thủ muốn chơi lại từ đầu thì phải cho các biến (variables) trở về trị số ban đầu của chúng: points 0 lives = 5 picx = 0 picy = 0 speedx = 5 speedy = 5
7) Càng lúc càng nhanh – Mỗi khi bóng dội lại, ta muốn bóng chạy nhanh hơn cho trận đấu hấp dẫn hơn nhưng đừng quá nhanh làm đấu thủ nản chí. Khi bóng đụng vào các cạnh của màn hình thì ta cho vận tốc theo chiểu ngang tăng 10% và theo chiều đứng (dọc) thì tăng 1 pixel:
If picx <= 0 or picx >= 700 # màn hình 800 nhưng chiều dài quả bóng là 100 speedx = -speedx * 1.1 If picy <= 0: speedy = -speedy + 1
Bóng chạy càng lúc càng nhanh nên đấu thủ không đánh kịp. Cho nên mỗi khi đấu thủ mất một live tức là khi bóng dội đáy màn hình thì ta cho giảm tốc độ quả bóng:
if picy >= 500: lives -= 1 speedy = -5 speedx = 5 picy = 499 # để bóng không biến mất khỏi màn hình nên cho nó trồi lên 1 chút
Sau đây là chương trình:
# SmileyPong2.py import pygame pygame.init() screen = pygame.display.set_mode([800,600]) pygame.display.set_caption("Smiley Pong") keepGoing = True pic = pygame.image.load("CrazySmile.bmp") colorkey = pic.get_at((0,0)) pic.set_colorkey(colorkey) picx = 0 picy = 0 BLACK = (0,0,0) WHITE = (255,255,255) timer = pygame.time.Clock() speedx = 5 speedy = 5 paddlew = 200 paddleh = 25 paddlex = 300 paddley = 550 picw = 100 pich = 100 points = 0 lives = 5 font = pygame.font.SysFont("Times", 24) while keepGoing: # Game loop for event in pygame.event.get(): if event.type == pygame.QUIT: keepGoing = False if event.type == pygame.KEYDOWN: if event.key == pygame.K_F1: # F1 = New Game points = 0 lives = 5 picx = 0 picy = 0 speedx = 5 speedy = 5 picx += speedx picy += speedy if picx <= 0 or picx >= 700: speedx = -speedx * 1.1 if picy <= 0: speedy = -speedy + 1 if picy >= 500: lives -= 1 speedy = -5 speedx = 5 picy = 499 screen.fill(BLACK) screen.blit(pic, (picx, picy)) # Vẽ cây vợt paddlex = pygame.mouse.get_pos()[0] paddlex -= paddlew/2 pygame.draw.rect(screen, WHITE, (paddlex, paddley, paddlew, paddleh)) # Check for paddle bounce if picy + pich >= paddley and picy + pich <= paddley + paddleh \ and speedy > 0: if picx + picw/2 >= paddlex and picx + picw/2 <= paddlex + \ paddlew: speedy = -speedy points += 1 # Draw text on screen draw_string = "Lives: " + str(lives) + " Points: " + str(points) # Check coi dứt game chưa if lives < 1: speedx = speedy = 0 draw_string = "Game Over. Your score was: " + str(points) draw_string += ". Press F1 to play again. " text = font.render(draw_string, True, WHITE) text_rect = text.get_rect() text_rect.centerx = screen.get_rect().centerx text_rect.y = 10 screen.blit(text, text_rect) pygame.display.update() timer.tick(60) pygame.quit() # Exit
Run -> Run Module. Khi dứt chương trình sẽ có hình 10-6 như sau:
- Thêm âm thanh vào game
Module ta cần cho âm thanh trong Pygame là pygame.mixer. Giả sử bạn download được từ Internet một file sound có tên pop.wav và lưu nó trong folder mà bạn lưu các files py. Bạn sẽ viết như sau trong chương trình để gọi pop.wav:
pygame.mixer.init() # khởi động mixer, module về âm thanh pop = pygame.mixer.Sound(“pop.wav”) # cho file âm thanh pop.wav vào biến tên pop
#SmileyPop2.py import pygame import random BLACK = (0,0,0) WHITE = (255,255,255) pygame.init() screen = pygame.display.set_mode([800,600]) pygame.display.set_caption("Pop a Smiley") mousedown = False keep_going = True clock = pygame.time.Clock() pic = pygame.image.load("CrazySmile.bmp") # cái mặt cười là quả bóng bàn colorkey = pic.get_at((0,0)) # làm trong suốt (transparent) góc hình pic.set_colorkey(colorkey) # làm trong suốt (transparent) góc hình sprite_list = pygame.sprite.Group() pygame.mixer.init() # khởi động mixer pop = pygame.mixer.Sound("pop.wav") font = pygame.font.SysFont("Arial", 24) count_smileys = 0 count_popped = 0 class Smiley(pygame.sprite.Sprite): # xem lại các chương trình trước pos = (0,0) xvel = 1 yvel = 1 scale = 100 def __init__(self, pos, xvel, yvel): pygame.sprite.Sprite.__init__(self) self.image = pic self.scale = random.randrange(10,100) self.image = pygame.transform.scale(self.image, (self.scale,self.scale)) self.rect = self.image.get_rect() self.pos = pos self.rect.x = pos[0] - self.scale/2 self.rect.y = pos[1] - self.scale/2 self.xvel = xvel self.yvel = yvel def update(self): self.rect.x += self.xvel self.rect.y += self.yvel if self.rect.x <= 0 or self.rect.x > screen.get_width() - self.scale: self.xvel = -self.xvel if self.rect.y <= 0 or self.rect.y > screen.get_height() - self.scale: self.yvel = -self.yvel while keep_going: for event in pygame.event.get(): if event.type == pygame.QUIT: # bấm X đỏ góc trên bên phải hình để nghỉ chơi keep_going = False # bằng cách cho keep_going là False (Boolean variable) if event.type == pygame.MOUSEBUTTONDOWN: if pygame.mouse.get_pressed()[0]: # Left mouse button, draw mousedown = True elif pygame.mouse.get_pressed()[2]: # Right mouse button, pop pos = pygame.mouse.get_pos() # lấy tọa độ con chuột tại chỗ bấm clicked_smileys = [s for s in sprite_list if s.rect.collidepoint(pos)] # những sprites bị click phải trúng tức là chạm với chỗ bấm chuột sẽ được lưu và chicked_smileys sprite_list.remove(clicked_smileys) # remove những smileys (quả bóng) đã bị bấm nút phải trúng, ra khỏi sprite_list (hạ được chúng) if len(clicked_smileys) > 0: # nếu có bóng bị bấm phải trúng pop.play() # cho nổ count_popped += len(clicked_smileys) # Đếm số lần nổ if event.type == pygame.MOUSEBUTTONUP: mousedown = False screen.fill(BLACK) sprite_list.update() sprite_list.draw(screen) clock.tick(60) draw_string = "Bubbles created: " + str(count_smileys) draw_string += " - Bubbles popped: " + str(count_popped) if (count_smileys > 0): draw_string += " - Percent: " draw_string += str(round(count_popped/count_smileys*100, 1)) draw_string += "%" text = font.render(draw_string, True, WHITE) text_rect = text.get_rect() text_rect.centerx = screen.get_rect().centerx text_rect.y = 10 screen.blit (text, text_rect) pygame.display.update() if mousedown: speedx = random.randint(-5, 5) speedy = random.randint(-5, 5) newSmiley = Smiley(pygame.mouse.get_pos(), speedx, speedy) sprite_list.add(newSmiley) count_smileys += 1 pygame.quit()
Bài tập: Bạn download một ảnh nào đó từ Internet rồi lưu nó vào folder chứa các chương trình py của bạn rồi dùng chương trình này bắn nổ nó. Bạn cũng có thể thay thế file sound tiếng nổ trong bài này bằng một âm thanh khác.
Bonus: Bạn Run thử cáchương trình sau đây:
# NoF20.py – Tàu bay Tàu khựa đến đây tớ bắn cho tan tành import pygame import random BLACK = (0,0,0) pygame.init() screen = pygame.display.set_mode([800,600]) pygame.display.set_caption("Pop a Chinese Jet fighter") mousedown = False keep_going = True clock = pygame.time.Clock() pic = pygame.image.load("j20fighter.jpg") colorkey = pic.get_at((0,0)) pic.set_colorkey(colorkey) sprite_list = pygame.sprite.Group() class Smiley(pygame.sprite.Sprite): pos = (0,0) xvel = 1 yvel = 1 scale = 100 def __init__(self, pos, xvel, yvel): pygame.sprite.Sprite.__init__(self) self.image = pic self.scale = random.randrange(10,100) self.image = pygame.transform.scale(self.image, (self.scale,self.scale)) self.rect = self.image.get_rect() self.pos = pos self.rect.x = pos[0] - self.scale/2 self.rect.y = pos[1] - self.scale/2 self.xvel = xvel self.yvel = yvel def update(self): self.rect.x += self.xvel self.rect.y += self.yvel if self.rect.x <= 0 or self.rect.x > screen.get_width() - self.scale: self.xvel = -self.xvel if self.rect.y <= 0 or self.rect.y > screen.get_height() - self.scale: self.yvel = -self.yvel while keep_going: for event in pygame.event.get(): if event.type == pygame.QUIT: keep_going = False if event.type == pygame.MOUSEBUTTONDOWN: if pygame.mouse.get_pressed()[0]: # Regular left mouse button, draw mousedown = True elif pygame.mouse.get_pressed()[2]: # Right mouse button, pop pos = pygame.mouse.get_pos() clicked_smileys = [s for s in sprite_list if s.rect.collidepoint(pos)] sprite_list.remove(clicked_smileys) if event.type == pygame.MOUSEBUTTONUP: mousedown = False screen.fill(BLACK) sprite_list.update() sprite_list.draw(screen) clock.tick(60) pygame.display.update() if mousedown: speedx = random.randint(-5, 5) speedy = random.randint(-5, 5) newSmiley = Smiley(pygame.mouse.get_pos(),speedx,speedy) sprite_list.add(newSmiley) pygame.quit()
# KillLeDuan3.py import pygame import random BLACK = (0,0,0) WHITE = (255,255,255) pygame.init() screen = pygame.display.set_mode([800,600]) pygame.display.set_caption("Kill Le Duan") mousedown = False keep_going = True clock = pygame.time.Clock() pic = pygame.image.load("lexuan.jpg") colorkey = pic.get_at((0,0)) pic.set_colorkey(colorkey) sprite_list = pygame.sprite.Group() pygame.mixer.init() # Add sounds pop = pygame.mixer.Sound("explo.wav") font = pygame.font.SysFont("Arial", 24) count_smileys = 0 count_popped = 0 class Smiley(pygame.sprite.Sprite): pos = (0,0) xvel = 1 yvel = 1 scale = 100 def __init__(self, pos, xvel, yvel): pygame.sprite.Sprite.__init__(self) self.image = pic self.scale = random.randrange(10,100) self.image = pygame.transform.scale(self.image, (self.scale,self.scale)) self.rect = self.image.get_rect() self.pos = pos self.rect.x = pos[0] - self.scale/2 self.rect.y = pos[1] - self.scale/2 self.xvel = xvel self.yvel = yvel def update(self): self.rect.x += self.xvel self.rect.y += self.yvel if self.rect.x <= 0 or self.rect.x > screen.get_width() - self.scale: self.xvel = -self.xvel if self.rect.y <= 0 or self.rect.y > screen.get_height() - self.scale: self.yvel = -self.yvel while keep_going: for event in pygame.event.get(): if event.type == pygame.QUIT: keep_going = False if event.type == pygame.MOUSEBUTTONDOWN: if pygame.mouse.get_pressed()[0]: # Left mouse button, draw mousedown = True elif pygame.mouse.get_pressed()[2]: # Right mouse button, pop pos = pygame.mouse.get_pos() clicked_smileys = [s for s in sprite_list if s.rect.collidepoint(pos)] sprite_list.remove(clicked_smileys) if len(clicked_smileys) > 0: pop.play() count_popped += len(clicked_smileys) if event.type == pygame.MOUSEBUTTONUP: mousedown = False screen.fill(BLACK) sprite_list.update() sprite_list.draw(screen) clock.tick(60) draw_string = "Bubbles created: " + str(count_smileys) draw_string += " - Bubbles popped: " + str(count_popped) if (count_smileys > 0): draw_string += " - Percent: " draw_string += str(round(count_popped/count_smileys*100, 1)) draw_string += "%" text = font.render(draw_string, True, WHITE) text_rect = text.get_rect() text_rect.centerx = screen.get_rect().centerx text_rect.y = 10 screen.blit (text, text_rect) pygame.display.update() if mousedown: speedx = random.randint(-5, 5) speedy = random.randint(-5, 5) newSmiley = Smiley(pygame.mouse.get_pos(), speedx, speedy) sprite_list.add(newSmiley) count_smileys += 1 pygame.quit()