Tập làm game (phần 3)
Tiếp tiếp tục code 1 số game cơ bản bằng Pygame
Ở trong phần 2 chúng ta đã hoàn thành con thỏ cũng như điều khiển nó. Ở phần này, mình sẽ tiếp tục hướng dẫn cách tạo class cho các con chuột chũi cũng như các mũi tên nhằm tạo ra 1 trò chơi hoàn thiện. Đầu tiên sẽ là mũi tên.
1. Mũi tên
Ta khởi tạo class Arrow tương tự như class Player.
class Arrow(pygame.sprite.Sprite):
def __init__(self):
super(Arrow,self).__init__()
self.angle = p.angle
self.surf = pygame.transform.rotate(arrow,360-to_deg(self.angle))
self.rect = self.surf.get_rect(center=p.rect.center)
self.speed = 15
Trong class Arrow có 1 thành viên "angle" có giá trị bằng góc người chơi đang nhìn hiện tại. Mỗi khi con thỏ bắn tên, mũi tên sẽ bắn ra từ trước mặt của con thỏ khi đó nên việc lưu lại góc bắn ra sẽ giúp ta mô phỏng được chuyển động của mũi tên trong mọi thời điểm. "surf" của mũi tên cũng sẽ xoay 1 góc bằng góc được bắn ra và "rect" cũng chính là "rect" của hình mũi tên trong resources tuy nhiên do ban đầu mũi tên được bắn ra từ con thỏ nên center của "rect" sẽ trùng với center của "p.rect". Mình cũng sẽ cho tốc độ của mũi tên là 15 (các bạn có thể tùy chỉnh nếu muốn tên chạy nhanh hay chậm). Sau khi hoàn thành hàm khởi tạo ban đầu ta cũng cần thêm 1 hàm cập nhật vị trí của mũi tên sau mỗi vòng lặp trò chơi (vòng lặp game mà mình đã nhắc tới ở phần 1).
def update(self):
x = math.cos(self.angle)*self.speed
y = math.sin(self.angle)*self.speed
self.rect.move_ip(x,y)
if self.rect.top < 0 or self.rect.bottom > height or self.rect.left < 0 or self.rect.right > width:
self.kill()
Để cập nhật tọa độ \(x\) và \(y\) của 1 mũi tên thì ta chỉ cần lấy góc của mũi tên đó rồi nhân với tốc độ là sẽ ra được quãng đường mũi tên di chuyển được theo \(x\) và \(y\). Đồng thời nếu mũi tên chạy ra khỏi màn hình ta sẽ xóa mũi tên đó đi (self.kill()).
Đến đây thì ta đã hoàn thành class Arrow tuy nhiên việc có thể bắn ra tên 1 cách tức thời sẽ khiến người chơi có thể bắn ra tên liên tục khiến cho trò chơi trở nên quá dễ dàng cũng như thiếu tính thực tế. Vì vậy bây giờ mình sẽ hướng dẫn cách thêm thời gian rút cung (reload time) nhằm tạo 1 khoảng thời gian chờ giữa 2 lần bắn cung.
2. Rút cung
Để cho thuận tiện thì ta sẽ tạo 1 class tên "stop_watch" có chức năng như 1 đồng hồ bấm giờ
với 2 chức năng chính.
1. get: trả về thời gian trên đồng hồ hiện tại
2. reset: đặt lại thời gian trên đồng hồ về 0
Việc tạo 1 class riêng sẽ giúp ta chỉ cần gọi ".get" và ".reset" thay vì phải sử dụng câu
lệnh trả về thời gian hiện tại (pygame.time.get_ticks()) liên tục.
class stop_watch:
def __init__(self):
self.init_time = pygame.time.get_ticks()
def get(self):
return pygame.time.get_ticks()-self.init_time
def reset(self):
self.init_time = pygame.time.get_ticks()
Ban đầu khi ta khởi tạo thì ta sẽ lấy thời gian khi được khởi tạo (pygame.time.get_ticks()) làm mốc. Khi đó để lấy được khoảng thời gian từ lần khởi tạo gần nhất ta chỉ cần lấy thời gian hiện tại trừ đi thời gian gần nhất đồng hồ được khởi tạo. Để sử dụng stop_watch ta khai báo:
timer = stop_watch()
Sau khi tạo ra 1 chiếc đồng hồ bấm giờ mình sẽ tiếp tục khởi tạo 2 biến "reload_time" và "reload_until" trong đó "reload_time" là thời gian rút cung tính theo mili giây còn "reload_until" là thời gian con thỏ có thể tiếp tục bắn cung. Ở đây mình đặt thời gian rút cung là 350 mili giây (0.35 giây) nhưng nếu các bạn muốn trò chơi dễ hơn khó hơn thì có thể giảm hoặc tăng nó lên.
reload_until = 0
reload_time = 350
Do trong hầu hết các thời điểm trên màn hình sẽ có nhiều mũi tên đang bay cùng 1 lúc nên ta sẽ cần tìm cách nhóm các mũi tên thành 1 nhóm sử dụng "pygame.sprite.Group()". Mình sẽ dùng 2 group là "arrows" và "all_sprites" nhằm quản lí các mũi tên cũng như toàn bộ các sprites. Ưu điểm của việc nhóm các sprites là ta có thể duyệt qua từng sprites một cũng như cập nhật chúng chỉ với 1 vòng lặp for.
arrows = pygame.sprite.Group()
all_sprites = pygame.sprite.Group()
Do mình cũng muốn đo độ chính xác (accuracy) của con thỏ nên mình sẽ tạo mảng "arr" trong đó \(arr[0]\) là tổng số mũi tên được bắn ra còn \(arr[1]\) là số mũi tên trúng đích (trúng 1 con chuột chũi nào đó). Khi đó độ chính xác sẽ được tính bằng công thức: $$accuracy = \begin{cases} 0, & \text{if $acc[0] = 0$} \\ acc[1]/acc[0], & \text{if $acc[0] \neq 0$} \\ \end{cases} $$ Giờ ta đã có thể thực hiện thao tác bắn cung:
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1: # left click
if timer.get() < reload_until:
continue
reload_until = timer.get() + reload_time
new_arrow = Arrow()
arrows.add(new_arrow)
all_sprites.add(new_arrow)
acc[0]+=1
shoot.play()
Do việc click chuột phải cũng được tính là 1 sự kiện nên ta cũng sẽ kiểm tra thao tác này ở trong vòng lặp sự kiện. Ta cũng sẽ kiểm tra xem liệu thời gian trên đồng hồ bấm giờ đã vượt qua thời gian có thể bắn cung chưa (nếu chưa thì không bắn tên). Trong trường hợp có thể bắn tên, ta cập nhật thời gian có thể bắn mũi tên kế tiếp cũng như tạo ra mũi tên mới (new_arrow) và thêm nó vào 2 nhóm "arrows" và "all_sprites". File âm thanh "shoot" cũng được chạy nhằm thể hiện âm thanh bắn tên.
3. Chuột chũi
Tương tự với con thỏ và mũi tên, mình cũng sẽ tạo 1 sprite class cho chuột chũi có tên Badguy.
class Badguy(pygame.sprite.Sprite):
def __init__(self):
super(Badguy,self).__init__()
self.surf = badguyimg
self.rect = self.surf.get_rect()
self.rect.left = width+100
self.rect.top = random.randint(50,height-100)
self.speed = random.randint(5,8)
def update(self):
self.rect.move_ip(-self.speed,0)
if self.rect.right < 0:
self.kill()
Ở hàm khởi tạo, do các con chuột chũi sẽ bắt đầu chạy từ bên phải màn hình sang tổ của thỏ ở bên phải nên tọa độ \(x\) ban đầu của các con chuột sẽ nằm ngoài màn hình tầm \(100\) pixel (tức \(x_{chuột}=width+100\)). Mình cũng sẽ cho tọa độ \(y\) của chuột ngẫu nhiên sao cho tất cả các kho cà rốt đều có thể có chuột chạy tới. Tốc độ di chuyển cũng sẽ ngẫu nhiên từ 5 tới 8 pixel trên 1 khung hình nhằm tăng tính thực tế cho trò chơi (chuột chũi nhỏ sẽ chạy nhanh hơn chuột chũi lớn). Sau đó ở hàm "update" ta chỉ cần giảm tọa độ \(x\) của các con chuột chũi đi cũng như loại bỏ các con chuột đã chạy ra ngoài màn hình là xong.
Để có thể tạo ra 1 chuột chũi liên tục thì ta gọi 1 sự kiện có tên "add_enemy" với tác dụng đơn giản là tạo ra 1 con chuột chũi. Sở dĩ mình gọi đây là sự kiện vì mình sẽ tạo ra nó cũng như kiểm tra xem sự kiện này có xảy ra không ở trong vòng lặp sự kiện. Trong pygame ngoài các sự kiện có sẵn như click chuột, bấm phím nút hay bấm nút X thoát ra ngoài thì ta cũng có thể tự tạo ra các sự kiện. Các sự kiện nhân tạo này là "pygame.USEREVENT".
badtimer = 500
add_enemy = pygame.USEREVENT + 1
pygame.time.set_timer(add_enemy, badtimer)
Ở dòng đầu tiên mình đặt thời gian tạo ra 1 chuột chũi là 500 mili giây (0.5 giây). Sau đó mình khởi tạo sự kiện "add_enemy" là sự kiện nhân tạo đầu tiên và đặt thời gian nó xảy ra là mỗi 0.5 giây. Mình cũng sẽ tạo 1 sprite group của chuột chũi:
enemies = pygame.sprite.Group()
Giờ ta chỉ cần kiểm tra xem "add_enemy" có xuất hiện không, nếu có thì tạo 1 con chuột chũi mới và cho nó vào 2 sprite group "enemies" và "all_sprites".
if event.type == add_enemy:
new_badguy = Badguy()
enemies.add(new_badguy)
all_sprites.add(new_badguy)
4. Cập nhật sprites
Để cập nhật vị trí của các mũi tên và chuột chũi sau mỗi khung hình, ta đơn giản là duyệt qua sprite group chứa chúng ("arrows" và "enemies") và gọi hàm "update". Sau đó ta duyệt qua toàn bộ sprite và in lên màn hình.
for i in enemies:
i.update()
for i in arrows:
i.update()
for i in all_sprites:
screen.blit(i.surf,i.rect)
5. Tổ thỏ
Sau khi vẽ các nhân vật lên màn hình, mình cũng sẽ vẽ các tổ thỏ. Các kho cà rốt này sẽ trải dài cách đều ở bên trái màn hình và mình sẽ tạo hàm "draw_screen" có nhiệm vụ vẽ hình nền lẫn tổ thỏ.
def draw_screen():
background()
castle_height = castle.get_height()
for x in range((height-50)//castle_height):
screen.blit(castle,(0,50+x*castle_height))
6. Tống kết
Vậy là sau phần 3, chúng ta đã hoàn thành toàn bộ các nhân vật trong trò chơi bao gồm con thỏ, chuột chũi, mũi tên và vẽ toàn bộ màn hình chính của trò chơi. Ở trong phần kế tiếp mình sẽ hướng dẫn cách vẽ các thông số hiện tại của trò chơi như thời gian còn lại, số chuột chũi đã bị đánh đuổi hay số cà rốt còn trong kho.