May 28, 2021

Tập làm game (phần 2)

Tiếp tục code 1 số game cơ bản bằng Pygame

Ở trong phần 1 mình đã hướng dẫn cách khởi tạo chương trình cũng như vẽ hình nền cho trò chơi. Trong blog này mình sẽ tiếp tục hướng dẫn cách vẽ các ảnh nhân vật và cách nhận input từ người chơi.

1. Sprite (không phải nước giải khát)

Trong đồ họa máy tính, sprite là một hình ảnh hai chiều hoặc hình ảnh động được kết hợp vào 1 khung nền lớn hơn. Tất cả những ảnh ta khởi tạo trong phần 1 đều có thể được coi là 1 sprite và sẽ được "dán/in" (blit) lên màn hình để tạo ra đồ họa của trò chơi. Trong pygame ta cũng sẽ sử dụng 1 "class" tên "sprite" nhằm quản lí các nhân vật trong trò chơi như con thỏ và chuột chũi. Ta bắt đầu bằng việc khởi tạo người chơi (con thỏ).

2. Khởi tạo Player

class Player(pygame.sprite.Sprite):
    def __init__(self):
        super(Player,self).__init__()
        self.surf = player_avatar
        self.rect = self.surf.get_rect(center=(100,100))
        self.speed = 10
        self.angle = 0

Việc đầu tiên ta cần làm khi tạo 1 class đối tượng (ở đây đối tượng là Player) là tạo 1 hàm khởi tạo (init). Ở trong hàm "__init__" mình sẽ chọn surf của self tức Player là player_avatar tức con thỏ. Điều này có nghĩa là khi hình ảnh của các đối tượng thuộc class Player sẽ là con thỏ. Để hiểu được câu lệnh kế tiếp thì ta cần hiểu "rect" trong pygame là gì.

"rect" là từ viết gọn của "rectangle" tức hình chữ nhật. Đúng như tên gọi của nó, "rect" là hình chữ nhật chứa hình ảnh mà ta muốn vẽ lên màn hình. "rect" giúp ta điều chỉnh vị trí vẽ hình ảnh (tọa độ mà hình ảnh này sẽ được vẽ lên) cũng như kích thước hiển thị (trong hầu hết các trường hợp kích thước của "rect" sẽ là kích thước của ảnh nhằm hiển thị toàn bộ ảnh). Trở lại với câu lệnh thứ 5, mình sử dụng "self.surf.get_rect" để lấy "rect" của "self.surf" rồi mình chỉnh "center" của "rect" đó thành \((100;100)\). Điều này có nghĩa là "rect" của "self" sẽ có kích thước giống "player_avatar" và tọa độ vị trí trung tâm của nó là \((100;100)\).

Ngoài việc khởi tạo "surf" với "rect" ra mình cũng sẽ khởi tạo thêm 2 giá trị "speed" và "angle" trong đó "speed" là tốc độ của con thỏ còn "angle" là hướng mặt của con thỏ hiện tại (hướng mặt dựa theo vị trí con trỏ chuột hiện tại).

3. Nhận input

Sau khi khởi tạo nhân vật thì ta sẽ đến 1 trong những phần quan trọng nhất của trò chơi đó chính là nhận input của người chơi. Ta sẽ tạo hàm "take_input" để sử lí việc này.

def take_input():
	for event in pygame.event.get():
		if event.type == pygame.QUIT:
			pygame.quit() 
			exit(0)     
		if event.type == pygame.KEYDOWN:    
			if event.key == K_ESCAPE:
				pygame.quit()
				exit(0)

Đầu tiên ta tạo phím tắt kết thúc trò chơi. Ở dòng thứ 2 ta sẽ duyệt qua các sự kiện (event) trong "pygame.event.get". Các sự kiện mà ta đang nhắc đến ở đây chính là việc người chơi bấm phím nào đó trên bàn phím (pygame.KEYDOWN) hay bấm nút X exit của trò chơi (pygame.QUIT). Vì vậy ta sẽ kiểm tra xem loại event tức event.type có phải 2 loại sự kiện đó không, với "pygame.KEYDOWN" ta sẽ kiểm tra xem liệu nút được bấm có phải nút escape hay không (K_ESCAPE). Để kết thúc chương trình ta sử dụng 2 câu lệnh "pygame.quit()" và "exit(0)".

Kế tiếp ngoài việc thoát game, người chơi cũng sẽ cần điều khiển nhân vật con thỏ của mình. Để làm được điều này ta sẽ thêm hàm "update" trong class Player.

def update(self,press):
	if press[K_UP] or press[K_w]:
		self.rect.move_ip(0, -self.speed)
	if press[K_DOWN] or press[K_s]:
		self.rect.move_ip(0, self.speed)
	if press[K_LEFT] or press[K_a]:
		self.rect.move_ip(-self.speed, 0)
	if press[K_RIGHT] or press[K_d]:
		self.rect.move_ip(self.speed, 0)
	self.rect.left = max(self.rect.left, 0)
	self.rect.right = min(self.rect.right, width)
	self.rect.top = max(self.rect.top,0)
	self.rect.bottom = min(self.rect.bottom, height)

Hàm "update" này nhận "press" làm tham số trong đó "press" là 1 mảng bool nhằm thể hiện nút thứ \(i\) có được bấm xuống hay không (nếu có thì \(press[i] == True\)). Khi đó ta chỉ cần kiểm xem các nút di chuyển như WASD và nút mũi tên có được bấm hay không và di chuyển con thỏ tương ứng. Ngoài ra để tránh trường hợp con thỏ đi ra ngoài màn hình thì ta cần giới hạn tọa độ của con thỏ sao cho nó luôn không âm và không vượt quá kích thước màn hình. Ngoài di chuyển ra thì con thỏ cũng cần phải quay mặt theo con trỏ chuột.

position = pygame.mouse.get_pos()
playerpos = self.rect.center
self.angle = math.atan2(position[1]-playerpos[1],position[0]-playerpos[0])
self.surf = pygame.transform.rotate(player_avatar,360-to_deg(self.angle)) # change angle from rad to deg

Ở dòng đầu tiên, ta sẽ lấy tọa độ của con trỏ bằng câu lệnh "pygame.mouse.get_pos()". Câu lệnh này sẽ trả về tọa độ \(x\) và \(y\) của chuột theo góc trên bên trái của màn hình. Còn ở dòng thứ 2 mình sẽ lấy tọa độ tâm của con thỏ bằng "self.rect.center". Sau đó góc con thỏ đang nhìn sẽ được tính bằng hàm "atan2" (hàm này sẽ trả về tan của thương tham số đầu tiên với tham số thứ hai). "surf" của con thỏ sẽ được quay tương ứng với góc sao cho con thỏ quay người theo con trỏ chuột.

Một chút lưu ý là do "self.angle" đơn vị là radian mà "pygame.transform.rotate" nhận tham số theo đơn vị độ nên ta cần viết 1 hàm "to_deg" nhằm chuyển radian về độ. Để chuyển đổi đơn vị ta sẽ dùng công thức $$ degrees = radians * 180 / 2 \pi \approx radians * 57.29 $$

def to_deg(x):
	return x*57.29

Giờ sau khi hoàn thiện class Player ta chỉ cần tạo nhân vật cũng như tạo hàm "update" để cập nhật vị trí và hướng nhìn của con thỏ. Hàm này còn có thể dùng để cập nhật các con chuột chũi cũng như mũi tên được bắn ra.

p = Player()
def update():
	press = pygame.key.get_pressed()
	p.update(press)

4. Tổng kết

Vậy là sau phần 2, các bạn đã có thể điều khiển cũng như quay mặt con thỏ của mình. Ở phần kế tiếp, 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 sản phẩm gần như hoàn thiện.