Oop lab/pygame1
- หน้านี้เป็นส่วนหนึ่งของ oop lab
ไลบรารีที่เราจะใช้ในการพัฒนาเกมบน Python คือ PyGame อย่างไรก็ตาม ตัว Pygame นั้นไม่ได้มีโครงสร้างที่เป็นเชิงวัตถุมากนัก ใน Tutorial นี้เราจะสร้างกรอบงานเชิงวัตถุครอบ Pygame อีกครั้ง โดยจะพยายามอ้างอิงโครงสร้างจาก Slick2D เพื่อความคุ้นเคย
เนื้อหา
โค้ดเริ่มต้น
เราจะเริ่มจากโค้ด squash.py จากหน้า สร้างเกมด้วย Pygame โดยเราจะเริ่มจากการแกะส่วนแก่นของเกมในฟังก์ชัน main แยกออกมาเป็นคลาส
โค้ดที่เราสนใจอยู่ในส่วนต่อไปนี้
def main():
global game_over,font,score,score_image
pygame.init()
clock = pygame.time.Clock()
display = pygame.display.set_mode(WINDOW_SIZE)
pygame.display.set_caption('Squash')
game_over = False
font = pygame.font.SysFont("monospace", 20)
score = 0
score_image = None
render_score()
ball = Ball(speed=(200,50))
player = Player(color=pygame.Color('green'),pos=100)
while not game_over:
for event in pygame.event.get(): # process events
if (event.type == QUIT) or \
(event.type == KEYDOWN and event.key == K_ESCAPE):
game_over = True
if pygame.key.get_pressed()[K_UP]:
player.pos -= 5
elif pygame.key.get_pressed()[K_DOWN]:
player.pos += 5
display.fill(BLACK) # clear screen
display.blit(score_image, (10,10)) # draw score
player.draw(display) # draw player
ball.move(1./FPS, display, player) # move ball
ball.draw(display) # draw ball
pygame.display.update() # redraw the screen
clock.tick(FPS) # wait to limit FPS requirement
if __name__=='__main__':
main()
print "Game Over! Total score is %d." % score
pygame.quit()
หน้าที่หลักของฟังก์ชัน main ถ้ามองในแง่ของเกมแล้ว มีดังนี้
- ตั้งค่าเริ่มต้น (init)
- ทำงานใน update/render ลูป
- จัดการกับ event
ถ้าเราพิจารณาจากโค้ด จะแบ่งเป็นส่วน ๆ ได้ตามด้านล่างนี้
def main():
# ------------------------ init -------------------------------------
pygame.init()
clock = pygame.time.Clock()
# ...
player = Player(color=pygame.Color('green'),pos=100)
# ------------------ event loop ------------------------------------
while not game_over:
# ----------------------- event processing -------------------------
for event in pygame.event.get(): # process events
# ...
if pygame.key.get_pressed()[K_UP]:
player.pos -= 5
elif pygame.key.get_pressed()[K_DOWN]:
player.pos += 5
# ---------------------------- render & update ---------------------------------------
display.fill(BLACK) # clear screen
display.blit(score_image, (10,10)) # draw score
player.draw(display) # draw player
ball.move(1./FPS, display, player) # move ball
ball.draw(display) # draw ball
pygame.display.update() # redraw the screen
# ---------------------------- wait inside loop ----------------------------
clock.tick(FPS) # wait to limit FPS requirement
เราจะเริ่มจากส่วน init และ update/render ลูปก่อน
คลาส SimpleGame
มาวางแผนกันก่อน... เราจะเขียนคลาส SimpleGame ที่ทำหน้าที่จัดการแกนหลักของเกม ตามที่นำมาข้างต้น เราต้องการให้คนที่เขียนเกม นำคลาสเราไปใช้เพื่อสร้างเกมขึ้นมา
เรามีทางเลือกในการออกแบบคลาสดังกล่าวสองทางคือ การใช้ inheritance กับการใช้ composition (อ่านรายละเอียดคร่าว ๆ ที่ แบบฝึกหัด 44, หนังสือ Learn Python the Hard Way) อย่างไรก็ตาม เราพยายามจะคงโครงสร้างของ Slick2D เอาไว้ ดังนั้นเราจะเลือกแบบแรก กล่าวคือ เราจะให้คนที่เขียนเกม inherit SimpleGame เพื่อสร้างเป็นคลาสใหม่ของเกมขึ้นมา ดังนั้นเราต้องวางแผนคร่าว ๆ ว่า SimpleGame ของเราจะทำอะไรบ้าง และอะไรจะเหลือให้คลาสที่ inherit ไปเอาไปเขียนเอง และจะเขียนเองอย่างไร
เราต้องการให้คน inherit คลาส SimpleGame เขียนเมท็อดเหล่านี้ที่เฉพาะเจาะจงกับเกมของตัวเอง:
- init
- render
- update
- on_key_up
- on_key_down
คลาสเกมที่ได้ เราต้องการให้เมื่อสร้างแล้วเรียกทำงานโดยเมท็อด run ได้เลย
เราจะเริ่มทยอยเขียนจากส่วนสามเมท็อดแรกก่อน เราจะเขียนคลาสดังกล่าวไว้ในไฟล์ gamelib.py ซึ่งจะเป็นโมดูลย่อยของเกมของเรา โครงของคลาสเราเป็นดังนี้
import pygame
from pygame.locals import *
class SimpleGame:
def __init__(self):
pass
def run(self):
pass
def init(self):
pass
def update(self):
pass
def render(self):
pass
เราจะเขียนโปรแกรมหลักขึ้นมาทดสอบพร้อม ๆ กัน เขียนโปรแกรมหลักในไฟล์ main.py
import pygame
from pygame.locals import *
from gamelib import SimpleGame
class SquashGame(SimpleGame):
pass
def main():
game = SquashGame()
game.run()
if __name__ == '__main__':
main()
ด้วยโค้ดเปล่า ๆ เช่นนี้ โปรแกรมเราควรจะรันแล้วจบออกมาโดยไม่ทำอะไรเลย
ส่วนกำหนดค่าเริ่มต้น
เราจะเริ่มเอาโค้ดมาใส่ส่วนเริ่มต้นเกม ในคลาส SimpleGame ก่อนอื่นกลับไปพิจารณาโค้ดจากโปรแกรมต้นฉบับ เราจะพบว่าเราต้องการนำโค้ดส่วนด้านล่างนี้มาใส่ในส่วนเริ่มต้น
pygame.init()
clock = pygame.time.Clock()
display = pygame.display.set_mode(WINDOW_SIZE)
pygame.display.set_caption('Squash')
game_over = False
font = pygame.font.SysFont("monospace", 20)
score = 0
score_image = None
render_score()
แต่ไม่ใช่ทั้งหมดจะเป็นส่วนเริ่มต้นในทุก ๆ เกม และยิ่งไปกว่านั้น สังเกตว่าพารามิเตอร์เช่น WINDOW_SIZE หรือชื่อเกม (หรือถ้าดูให้ดี ก็จะรวมส่วน FPS ที่ตอนท้ายโปรแกรมด้วย) น่าจะเป็นสิ่งที่ผู้ใช้คลาสของเรากำหนดได้ ดังนั้นเราจะรับค่าพวกนี้มาเก็บใน object เราเสียก่อน เมท็อด __init__ ที่เป็น constructor ของคลาส SimpleGame จะเป็นดังนี้
def __init__(self, title, window_size=(640,480), fps=60):
self.title = title
self.window_size = window_size
self.fps = fps
หมายเหตุ: สังเกตตัวอย่างการใช้ default parameter ในโค้ดข้างต้น
ส่วนโค้ดเริ่มเกม เรานำมาเขียนเป็นเมท็อด game_init ได้ดังด้านล่าง:
def game_init(self):
pygame.init()
self.clock = pygame.time.Clock()
self.display = pygame.display.set_mode(self.window_size)
pygame.display.set_caption(self.title)
self.font = pygame.font.SysFont("monospace", 20)
คำถามคือเราจะเรียกเมท็อดนี้ที่ไหน? เพราะว่าเราเตรียมเมท็อด init เอาไว้ให้คนที่จะเอาคลาสเราไปใช้เขียนส่วนกำหนดค่าเริ่มต้นเองด้วย เรามีสองคำตอบหลัก ๆ
ทางเลือกที่ 1: แบบแรกคือเรียกที่ในเมท็อด run เช่นเราอาจจะเขียนเป็น
def run(self):
self.game_init()
# ... perform the event loop
จากนั้นส่วนเมท็อด init ไม่ต้องเขียนอะไรเลย วิธีนี้ทำให้คนนำคลาสไปใช้ สามารถเขียนเมท็อด init ได้อย่างสะดวก อย่างไรก็ตาม ถ้ามีการ init อะไรที่ต้องทำหลัง pygame.init เราจะทำไม่ได้
ทางเลือกที่ 2: (เราจะเลือกทางนี้) เราจะเรียกเมท็อดดังกล่าวในเมท็อด init เลย ดังนี้:
def init(self):
self.game_init()
การเลือกเช่นนี้ ทำให้ถ้าคลาสที่ inherit ไป มีการแก้ไขเมท็อด init จะต้องเรียกเมท็อด init ของคลาส SimpleGame ให้ทำงานเองด้วย การเลือกทางนี้อาจจะมีข้อยุ่งยาก แต่ให้ความคล่องตัวมากกว่า โดยเราอาจจะเขียนเมท็อด init ในคลาส SquashGame ของเราเป็นดังนี้
# ---- Note: This is an example in the Squash game
def init(self):
# .... do anything you want to do before calling pygame init code
#
super(SquashGame, self).game_init()
# .... do anything you want to do after calling pygame init code
#
หมายเหตุ: สังเกตตัวอย่างการใช้ฟังก์ชัน super เพื่ออ้างถึงคลาสแม่ ในตัวอย่างข้างต้น
ให้แก้และทดลองโปรแกรมให้ทำงาน เราจะพบว่าโปรแกรมไม่จบการทำงานเมื่อเราสั่งปิดโปรแกรมที่หน้าต่าง ให้กด Ctrl-C ที่ใน terminal เพื่อปิดโปรแกรมแทน
คำถาม ทำไมโปรแกรมไม่หยุดการทำงาน?
เมท็อดแบบ private
โค้ดข้างต้นเราเขียนเมท็อด game_init ไว้ในคลาส SimpleGame เราต้องระวังไม่ให้การนำคลาสดังกล่าวไปใช้ มีการสร้างเมท็อดชื่อ game_init ขึ้นมาซ้ำ
ใน Python การระบุให้เมท็อดเป็น private โดยสมบูรณ์นั้นทำไม่ได้ อย่างไรก็ตาม เราทำโดยตั้งชื่อเมท็อดขึ้นด้วย __ เมท็อดนั้นจะถูกเปลี่ยนชื่อภายในระบบของ python ให้มีชื่อเป็น "_classname__method" ทำให้เรียกซ้ำได้ยาก ดังนั้นเราจะเปลี่ยนชื่อเมท็อด game_init และแก้เมท็อด init เป็นดังนี้
def __game_init(self):
# ...
def init(self):
self.__game_init()
อ่านเพิ่มเติมเกี่ยวกับ private methods: https://docs.python.org/2/tutorial/classes.html#private-variables-and-class-local-references
ส่วน render/update loop
เราจะเพิ่ม render/update loop ที่ยังใช้ไม่ได้จริงเข้าไปใน เมท็อด run ของคลาส
def run(self):
self.init()
while True:
self.update()
self.render()
self.clock.tick(self.fps)
สังเกตว่า เราทำงานไปเรื่อย ๆ ไม่มีวันจบ
คำถาม เราจะทดสอบได้อย่างไรว่าโปรแกรมนี้ทำงานถูกต้อง
การจัดการ event พื้นฐาน
โปรแกรมเราไม่จบการทำงานเพราะว่าเราขาดบรรทัดต่อไปนี้ จากโปรแกรมเริ่มต้น
for event in pygame.event.get(): # process events
if (event.type == QUIT) or \
(event.type == KEYDOWN and event.key == K_ESCAPE):
game_over = True
ในการจะจัดการดังกล่าว เราต้องหาวิธีการเอาตัวเองออกจาก event loop ของช่วงก่อน ที่เราใส่ while True เอาไว้...
เราจะเพิ่มตัวแปร is_terminated ในวัตถุคลาส SimpleGame และเมท็อดที่เกี่ยวข้องดังนี้
def __init__(self,
title,
window_size=(640,480),
fps=60):
# ...
self.is_terminated = False
def terminate(self):
self.is_terminated = True
จากนั้นเราจะแก้เงื่อนไข while ของ event loop เป็น
def run(self):
# ...
while not self.is_terminated:
#....
เราจะเพิ่มเมท็อด __handle_events (สังเกตว่านี่จะเป็น private method จากชื่อ) ที่จัดการ event ของเกม ซึ่งในตอนแรกนี้จะทำงานเพียงแค่จัดการ event QUIT เท่านั้น
def __handle_events(self):
for event in pygame.event.get():
if event.type == QUIT:
self.terminate()
และเรียกเมท็อดนี้ใน event loop
def run(self):
# ...
while not self.is_terminated:
self.__handle_events()
# ...
ทดลองโปรแกรมว่าโปรแกรมจบการทำงานเมื่อกดปิดตามต้องการหรือไม่
จัดการ event ของการกดปุ่ม
เราจะเพิ่มเมท็อด on_key_up และ on_key_down ลงในคลาส SimpleGame
class SimpleGame:
# ...
def on_key_up(self, key):
pass
def on_key_down(self, key):
pass
งานของคุณ: ให้แก้เมท็อด __handle_events ให้จัดการเรียกเมท็อดเหล่านี้เมื่อมีการกดและปล่อยปุ่ม จากนั้นให้แก้คลาส SquashGame เพื่อทดสอบว่าการทำงานถูกต้องจริง