PythonTowerDefense
Tower Defense Game made with Pyglet
Posted on March 27, 2020
Hi all! Trong bài viết này mình xin giới thiệu với tất cả mọi người một thư viện yêu thích của mình để viết game bằng Python, đó là Pyglet. Pyglet rất đơn giản và tiện lợi với những đoạn code ngắn để thiết kế game, giúp cho việc development nhanh hơn rất nhiều so với những ngôn ngữ khác. Thư viện này rất phù hợp cho những bạn mới bắt đầu vào con đường lập trình game, hoặc chỉ đơn giản để viết bài tập lớn cho những môn học trên trường :)
Sau đây chúng ta sẽ thử viết 1 project bằng Pyglet, cụ thể là 1 game Tower Defense vô cùng hấp dân.
Đối với máy Windows, các bạn vào link sau để tải Python3:
https://www.python.org/downloads/release/python-382/
Sau đó các bạn chạy file tải về để cài đặt, lưu ý trong khi cài nhớ chọn option "Add Python to PATH" để sau này ta có thể chạy Python3 thuận tiện trên cmd.
Với phần lớn các phiên bản của Linux, các bạn đã có python3 ngay trong máy của mình
import pyglet
Trước khi bắt tay vào việc viết mã nguồn, mình tạo các folder và file như sau:
.
├── res
│ └── res
│ ├── GFX
│ │ ├── Game
│ │ │ ├── Enemy
│ │ │ │ ├── Boss Enemy
│ │ │ │ ├── HealthBar
│ │ │ │ ├── Normal Enemy
│ │ │ │ ├── Smaller Enemy
│ │ │ │ └── Tanker Enemy
│ │ │ ├── Tilemap
│ │ │ │ ├── Ground
│ │ │ │ ├── Road
│ │ │ │ ├── Spawner
│ │ │ │ └── Target
│ │ │ └── Tower
│ │ │ ├── BuyNUpgrade.png
│ │ │ ├── Machine Gun Tower
│ │ │ ├── Normal Tower
│ │ │ └── Sniper Tower
│ │ └── GUI
│ │ └── Button
│ └── SFX
└── src
├── Entity
├── __init__.py
├── main.py
├── mapInfo.txt
├── Screen
└── Utils
Trong lập trình game, OOP là vô cùng quan trọng, vì vậy việc chia các folder cho những đối tượng làm việc trong game giúp chúng ta dễ dàng quản lý dự án và viết code nhanh hơn.
Thư mục res
sẽ chứa toàn bộ các file đồ họa (GFX), âm thanh (SFX), mọi tài nguyên này sẽ có trong repo Github của dự án mình đính kèm cuối bài viết.
Thư mục src
tất nhiên là sẽ chứa mã nguồn, trong đó :
main.py
là chương trình chính, để khởi động game ta sẽ chạy file này.mapInfo.txt
chứa dữ liệu map, đường đi trong gameScreen
là folder chứa interface cho màn hình hiển thị game, cùng với mã nguồn của màn hình menu bắt đầu game và màn hình chơiUtils
chứa interface và mã nguồn của một số công cụ hỗ trợ render đồ họa, tính toán trong gameEntity
chứa interface và mã nguồn của các đối tượng game như tháp, kẻ địch, đạn và đường đi, shop mua bán vật phẩm,...
Các bạn cũng đừng quên tạo các file
__init__.py
trống ở mỗi thư mục để mọi file mã nguồn có thể import các mã nguồn khác, làm việc liên kết với nhau.Đây là class quan trọng nhất của game, thực hiện công việc hiển thị, di chuyển các object của game, ta sẽ viết mã nguồn cho class này ở file texture.py
trong thư mục Utils
:
import pyglet
from Utils.drawing_util import Utils
class Texture(Utils):
def __init__(self, *args):
if len(args) == 1:
path = args[0]
self.image = pyglet.image.load(path)
self.sprite = pyglet.sprite.Sprite(self.image)
elif len(args) == 3:
path, x, y = args[0], args[1], args[2]
self.image = pyglet.image.load(path)
self.sprite = pyglet.sprite.Sprite(self.image)
self.sprite.x, self.sprite.y = x, y
def change_image(self, path):
backup_data = (self.sprite.scale, self.sprite.x, self.sprite.y)
self.image = pyglet.image.load(path)
self.sprite = pyglet.sprite.Sprite(self.image)
self.sprite.scale = backup_data[0]
self.sprite.x, self.sprite.y = backup_data[1], backup_data[2]
Từ đó chúng ta có thể sử dụng class này ở các file mã nguồn khác, tạo ra những đối tượng đồ họa và làm việc với chúng.
Giải thích các thuộc tính của class Texture:
image
: chứa ảnh hiển thị của đối tượngsprite
: có thể coi chính là bản thân đối tượng được tạo từ ảnh, thư viện Pyglet sẽ draw từ thuộc tínhsprite
cũng là class có sẵn trong Pyglet, có các thuộc tính x
, y
là vị trí hiển thị trên màn hình. scale
là kích cỡ hiển thị, nếu scale == 1.0
thì đối tượng sẽ có kích cỡ y hệt như ảnh.Class Texture được thừa kế từ interface Utils, được cài đặt trong file Utils/drawing_utils.py - chứa các hàm để xử lý đối tượng sprite như căn chỉnh kích cỡ, đổi vị trí,... File này được viết như sau:
class Utils(object):
def __init__(self):
pass
def get_x(self):
return self.sprite.x
def get_y(self):
return self.sprite.y
def set_x(self, value):
self.sprite.x = value
def set_y(self, value):
self.sprite.y = value
def set_coordinate(self, x, y):
self.sprite.x, self.sprite.y = x, y
def scale(self, ratio):
self.sprite.scale = ratio
def get_width(self):
return self.sprite.width
def get_height(self):
return self.sprite.height
def set_width(self, value):
self.sprite.width = value
def set_height(self, value):
self.sprite.height = value
def draw(self):
self.sprite.draw()
main.py
import pyglet
from pyglet.gl import gl
from pkg_resources import parse_version
window = pyglet.window.Window(fullscreen=False)
win_max_x = window.width
win_max_y = window.height
@window.event
def on_draw():
pass
if __name__ == "__main__":
pyglet.app.run()
Mã nguồn trên sẽ tạo ra 1 cửa sổ mới khi chúng ta chạy chương trình. Cửa sổ này đang đen xì vì chúng ta chưa render gì lên cả (hàm on_draw()
chưa làm gì). Nếu muốn game chạy toàn màn hình các bạn có thể thay đổi tham số fullscreen=True
trong constructor pyglet.window.Window()
. win_max_x
, win_max_y
chứa thông tin kích cỡ màn hình. Tiếp đó chúng ta sẽ viết code cho các màn hình game .
Vì game sẽ có nhiều màn hình chơi (menu start game, màn hình chơi game chính,..) mỗi màn có các chức năng khác nhau nên mình sẽ viết ra 1 interface chung cho các screen trong file screen.py
:
class Screen(object):
def __init__(self):
self.on_display = False
def draw(self):
pass
def mouse_clicked(self, x, y):
pass
def on_mouse_motion(self, x, y):
pass
def on_hover(self, x, y, sprite):
if sprite.get_x() <= x <= sprite.get_x() + sprite.get_width():
if sprite.get_y() <= y <= sprite.get_y() + sprite.get_height():
return True
return False
Với code như trên, chúng ta mặc định khi một màn mới được tạo ra thì sẽ không hiển thị, các thao tác làm việc với chuột, phím chưa được định nghĩa mà sẽ mô tả cụ thể ở các class con ( các màn hình cụ thể ). Hàm on_hover()
dùng để xác định xem chúng ta có di chuột lên một đối tượng (spirte) cụ thể nào trên màn hình không - một hàm mà ta sẽ sử dụng rất nhiều sau này, thậm chí là ngay tới đây khi ta viết code cho màn hình bắt đầu game tại file start_screen.py
trong folder Screen
.
from Utils.texture import Texture
from Screen.screen import Screen
import pyglet
class StartScreen(Screen):
def __init__(self, x, y):
self.on_display = True
self.width, self.height = x, y
self.start_button_on_hover, self.load_button_on_hover = False, False
self.switched, self.load = False, False
path = ["./../res/res/GFX/GUI/Background/Background_main_screen.jpg",
"./../res/res/GFX/GUI/Button/button.png",
"./../res/res/GFX/GUI/Button/button-selected.png",
"./../res/res/GFX/GUI/Button/LoadButton.png",
"./../res/res/GFX/GUI/Button/LoadButton_selected.png",]
background_image_path = path[0]
self.start_button_image_path = [path[1], path[2]]
self.load_button_image_path = [path[3], path[4]]
self.background = Texture(path[0])
self.background.scale(self.width / self.background.get_width())
self.start_button = Texture(self.start_button_image_path[0])
x = self.width // 2 - (self.start_button.get_width() // 2)
y = self.height // 2 - 150
self.start_button.set_coordinate(x, y)
self.load_button = Texture(self.load_button_image_path[0])
x = self.width // 2 - (self.start_button.get_width() // 2)
y = self.height // 2 - self.start_button.get_height() - 150
scale_ratio = self.start_button.get_width() / self.load_button.get_width()
self.load_button.set_coordinate(x, y)
self.load_button.scale(scale_ratio)
def draw(self):
self.background.draw()
self.start_button.draw()
self.load_button.draw()
def mouse_clicked(self, x, y):
if self.start_button_on_hover:
self.on_display, self.switched = False, True
elif self.load_button_on_hover:
try:
with open("save_data.txt", "r") as f:
self.on_display, self.switched = False, True
self.load = True
except FileNotFoundError:
pass
def on_mouse_motion(self, x, y):
if self.on_hover(x, y, self.start_button):
if not self.start_button_on_hover:
self.start_button.change_image(self.start_button_image_path[1])
self.start_button_on_hover = True
elif self.start_button_on_hover:
self.start_button.change_image(self.start_button_image_path[0])
self.start_button_on_hover = False
if self.on_hover(x, y, self.load_button):
if not self.load_button_on_hover:
self.load_button.change_image(self.load_button_image_path[1])
self.load_button_on_hover = True
elif self.load_button_on_hover:
self.load_button.change_image(self.load_button_image_path[0])
self.load_button_on_hover = False
Trong class này, ta khai báo một số đường dẫn chứa các file ảnh cho các nút (Button) ở mảng path
. Từ đó tạo các texture với file ảnh để hiển thị lên màn hình ở đoạn code sau đó. Công đoạn render/display được chạy ở hàm on_draw()
.
Tiếp đó ta sẽ tạo hiệu ứng cho các nút khi chuột được di đè lên các nút này. Cách thực hiện rất đơn giản trên hàm on_mouse_motion()
, ta sẽ gọi đến hàm on_hover()
đã được định nghĩa trước ở lớp Screen
cha, từ đó thay đổi ảnh hiển thị của nút bấm tùy vào việc có chuột di lên nút hay không.
Cuối cùng là việc chuyển sang màn hình chơi game khi các nút Start, hoặc Load game được bấm vào, ta sẽ gán biến self.on_display = False
để ngưng hiển thị màn hình bắt đầu, biến self.switched = False
để thông báo màn hình chơi game bắt đầu hiển thị.
Sau đó, ta viết 1 file game_screen.py
đơn giản chưa chạy công việc gì như sau:
from Utils.texture import Texture
from Screen.screen import Screen
class GameScreen(Screen):
def __init__(self, x, y):
self.on_display = False
self.width, self.height = x, y
def draw(self):
pass
def on_mouse_motion(self, x, y):
pass
Khi từ màn hình bắt đầu chuyển qua màn hình chơi game, ta sẽ thấy màn hình đen xì như khi mới chạy chương trình chính, vì game_screen chưa render gì cả.
Cuối cùng, ta chỉnh sửa file main.py
để chạy chức năng chuyển màn hình như sau:
import pyglet
from pyglet.gl import gl
from pkg_resources import parse_version
from Screen.start_screen import StartScreen
from Screen.game_screen import GameScreen
window = pyglet.window.Window(fullscreen=True)
win_max_x = window.width
win_max_y = window.height
start_screen = StartScreen(win_max_x, win_max_y)
game_screen = None
@window.event
def on_draw():
global game_screen
window.clear()
if start_screen.on_display:
start_screen.draw()
else:
if start_screen.switched:
start_screen.switched = False
game_screen = GameScreen(win_max_x, win_max_y)
game_screen.draw()
@window.event
def on_mouse_motion(x, y, dx, dy):
if start_screen.on_display:
start_screen.on_mouse_motion(x, y)
else:
game_screen.on_mouse_motion(x, y)
@window.event('on_mouse_press')
def mouse_clicked(x, y, button, modifiers):
if start_screen.on_display:
start_screen.mouse_clicked(x, y)
else:
game_screen.mouse_clicked(x, y)
if __name__ == "__main__":
screen_index = 0
pyglet.app.run()
Lần này ở chương trình chính ta import 2 class Screen ta đã viết ở trước, hàm on_draw()
sẽ render 1 trong 2 màn hình, các thao tác click, di chuột cũng sẽ gọi hàm on_mouse_motion()
, mouse_clicked()
ở 1 trong 2 màn hình tương ứng, tùy vào việc màn hình nào đang được hiển thị.
Như vậy là xong rồi ! Chúng ta hãy chạy thử game xem sao.
Trên CMD (Windows) hoặc Terminal (Linux), ta dùng lệnh cd
để di chuyển tới thư mục project game, sau đó chạy lệnh sau:
python3 main.py
Ta sẽ thấy màn hình bắt đầu game như sau:
Màn hình render khá đẹp phải không ? Các bạn có thể di chuột lên các nút để check hiệu ứng hover, sau đó click vào nút Start để chuyển sang màn hình chơi game.
Lúc này màn hình sẽ hiển thị chỉ 1 màu đen vì game_screen chưa làm gì. Các bạn nhấn nút Esc
để dừng chương trình.
Ở các phần tiếp theo, chúng ta sẽ viết mã nguồn cho game_screen để game chạy tiếp một số chức năng.
Cảm ơn các bạn đã theo dõi bài viết của mình ! Hãy đón đọc các bài ở phần sau!
Link github của project:
Tower Defense Game made with Pyglet
Posted on March 27, 2020
Sign up to receive the latest update from our blog.