สร้างเกมด้วย Pygame

จาก Theory Wiki
ไปยังการนำทาง ไปยังการค้นหา
วิกินี้เป็นส่วนหนึ่งของรายวิชา 01204223

Pygame เป็นโมดูลภาษาไพทอนที่ออกแบบมาเพื่อความสะดวกในการพัฒนาเกม วิกินี้ยกตัวอย่างการสร้างเกมอย่างง่ายที่อาศัยบอร์ดไมโครคอนโทรลเลอร์ในการควบคุมผู้เล่น

การเตรียมตัว

ติดตั้งไลบรารี Pygame

ระบบปฏิบัติการ Ubuntu Linux ใช้คำสั่ง apt-get ติดตั้งได้โดยตรง

sudo apt-get install python-pygame

ระบบปฏิบัตการ Mac OS X ดาวน์โหลดตัวติดตั้งจากเว็บไซท์ http://pygame.org/download.shtml

  • ดาวน์โหลดตัวติดตั้งที่ใช้งานร่วมกับไพทอนที่มีมาให้กับ OS X อยู่แล้ว โดยเลือกให้ตรงกับเวอร์ชันของไพทอนในเครื่อง เช่นไพทอนเวอร์ชัน 2.7 ให้ดาวน์โหลดไฟล์ pygame-1.9.2pre-py2.7-macosx10.7.mpkg.zip
  • ติดตั้งไลบรารี XQuartz เพิ่มเติม

เตรียมบอร์ดไมโครคอนโทรลเลอร์

บอร์ดไมโครคอนโทรลเลอร์ที่นำมาใช้เป็นตัวควบคุมผู้เล่นในวิกินี้ต้องถูกโปรแกรมเฟิร์มแวร์ให้สามารถอ่านค่าแสงผ่านพอร์ท USB ได้แล้ว ให้แน่ใจว่า

เกมตัวอย่าง: สควอช

เกมที่เราจะใช้เป็นตัวอย่างเรียกว่า Squash ดัดแปลงมาจากเกม Pong ที่เป็นคลาสสิคสุดฮิต ลักษณะการเล่นจะเป็นผู้เล่นตั้งแต่หนึ่งคนขึ้นไปตีลูกกระทบกำแพง และพยายามรับลูกที่สะท้อนกลับมาให้ได้

สนามแข่งสควอช
หน้าจอเกมสควอชต้นแบบที่สร้างด้วย Pygame

โค้ดต้นแบบ

ดาวน์โหลดโค้ดต้นแบบจากลิ้งค์ http://www.cpe.ku.ac.th/~cpj/204223/squash.py แล้วนำมาบันทึกไว้ในไดเรคตอรีเดียวกันกับโมดูล practicum.py และ peri.py ที่ได้มาจากการปฏิบัติตามขั้นตอนในวิกิ การติดต่อกับบอร์ด MCU ผ่าน USB ด้วย Arduino ทดลองรันโปรแกรมด้วยไพทอน

python squash.py

ควรปรากฏผลลัพธ์ดังรูปตัวอย่างข้างต้น เกมต้นแบบมีกติกาและการควบคุมดังนี้

  • มีผู้เล่นคนเดียว
  • ใช้ปุ่มลูกศรขึ้น/ลงเลื่อนไม้ตีของผู้เล่นเพื่อรับลูก
  • ทุกครั้งที่รับลูกได้จะได้คะแนนเพิ่ม 1 คะแนน
  • หากรับลูกพลาดและลูกกระทบกำแพงด้านซ้ายมือถือเป็นการจบเกม
  • กดปุ่ม ESC เพื่อออกจากเกมได้ตลอดเวลา

เราจะใช้โค้ดนี้เป็นฐานในการเพิ่มฟีเจอร์อื่น ๆ ให้กับเกม

ควบคุมผู้เล่นด้วยบอร์ดไมโครคอนโทรลเลอร์

เริ่มต้นด้วยการอิมพอร์ตฟังก์ชัน findDevices() และคลาส PeriBoard จากโมดูล practicum และ peri ตามลำดับ

import pygame
from pygame.locals import *
from practicum import findDevices
from peri import PeriBoard
from usb import USBError

จากนั้นทำให้ระบุว่าได้ว่าอ็อบเจกต์ Player ที่สร้างขึ้นจะผูกกับบอร์ดไมโครคอนโทรลเลอร์ใด โดยกำหนดไว้ในคอนสตรัคเตอร์ของคลาส Player และเพิ่มเมท็อต move() เพื่อให้เมนลูปเรียกใช้ในการคำนวณตำแหน่งของผู้เล่นตามความเข้มแสง

class Player(object):

    THICKNESS = 10

    def __init__(self, board, pos=WINDOW_SIZE[1]/2, width=100, color=WHITE):
        self.width = width
        self.pos = pos
        self.color = color
        self.board = board

    def move(self):
        try:
            self.pos = self.board.getLight()
        except USBError:
            pass

สังเกตว่ามีการใช้บล็อก try..except ครอบการเรียกใช้เมท็อด getLight() เอาไว้เพื่อมองข้าม exception UsbError ที่เกิดจากการที่บางครั้งบอร์ดไมโครคอนโทรลเลอร์ไม่ตอบสนองต่อการร้องขอที่ส่งไปจากโฮสท์

ในฟังก์ชัน main() ให้เรียกฟังก์ชัน findDevices() เพื่อค้นหาบอร์ดไมโครคอนโทรลเลอร์ และนำบอร์ดแรกที่พบมาสร้างเป็นอ็อบเจกต์ขึ้นจากคลาส PeriBoard จากนั้นให้ผูกบอร์ดนี้เข้ากับอ็อบเจกต์ Player ที่สร้างขึ้น ในลูป while ให้ยกเลิกการกำหนดตำแหน่งไม้ตีจากปุ่มลูกศร และเรียกเมท็อด move() ของอ็อบเจกต์ Player แทนเพื่อกำหนดค่าตำแหน่งไม้ตีจากความเข้มแสง

def main():
    :
    ball = Ball(speed=(200,50))
    board = PeriBoard(findDevices()[0])
    player = Player(board, 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.move()  # move player
        player.draw(display)  # draw player

จะเห็นว่าโค้ดข้างต้นถือว่าต้องมีบอร์ดไมโครคอนโทรลเลอร์เสียบอยู่อย่างน้อยหนึ่งบอร์ดเสมอ โปรแกรมจะแสดงความผิดพลาดและจบการทำงานทันทีหากไม่มีบอร์ดเสียบอยู่

ทดลองเสียบบอร์ดและรันโปรแกรม ตอนนี้ไม้ตีควรเลื่อนไปมาได้จากการเอามือบังแสงไปมา

ลดการส่ายของไม้ตี

แม้ว่าเกมที่ปรับแก้ไปในขั้นตอนที่แล้วจะทำให้เราควบคุมไม้ตีด้วยความเข้มแสงได้ แต่จะเห็นว่าตำแหน่งไม้ตีค่อนข้างสั่นไปมา โดยเฉพาะอย่างยิ่งหากใช้งานในห้องที่ใช้หลอดไฟแบบฟลูออเรสเซ้นท์ที่มีการกระพริบถี่ ๆ ตลอดเวลา เราสามารถเกลี่ยตำแหน่งไม้ตีให้เรียบขึ้นได้โดยอาศัยกลไก exponential smoothing ซึ่งมีสูตรดังนี้

โดยที่ α มีค่าอยู่ระหว่าง 0 ถึง 1 ลำดับ x0,x1,x2,... เป็นข้อมูลดิบ ส่วนลำดับ s0,s1,s2,... เป็นลำดับที่ผ่านการปรับให้เรียบแล้ว

สูตรนี้เป็นการนำเอาค่าก่อนหน้ามาคำนวณแบบถ่วงน้ำหนักร่วมกับค่าที่อ่านได้ในปัจจุบัน โดยหากกำหนดให้ α มีค่ามากทำให้ค่า st (ในที่นี้คือตำแหน่งปัจจุบันของไม้ตี) ขึ้นอยู่กับค่าก่อนหน้าค่อนข้างมาก มีผลทำให้การเปลี่ยนแปลงค่าของ st เป็นไปอย่างช้า ๆ และราบเรียบ แต่ก็ทำให้การตอบสนองต่อค่า xt ที่เข้ามาใหม่ (ในที่นี้คือค่าความเข้มแสง) ช้าลงเช่นกัน

โค้ดด้านล่างนำเอาสูตร exponential smoothing มาประยุกต์ใช้ โดยให้ α มีค่าเท่ากับ 0.1 คือให้ความสำคัญกับค่าใหม่ 10% และค่าเก่า 90%

class Player(object):
    :
    def move(self):
        try:
            self.pos = self.board.getLight()
            self.pos = 0.1*self.board.getLight() + 0.9*self.pos
        except:
            pass

ลองรันโปรแกรมและสังเกตการเปลี่ยนแปลงตำแหน่งของไม้ตี เทียบกับการที่ไม่มีการใช้ exponential smoothing ทดลองกับ α ค่าอื่น ๆ เพื่อดูพฤติกรรมการตอบสนองของไม้ตี

รองรับผู้เล่นหลายคน

ขั้นตอนต่อไปคือทำให้เกมรองรับผู้เล่นได้หลายคน จำนวนไม้ตีที่เพิ่มมากขึ้นอาจสร้างความสับสนให้ผู้เล่นได้มาก เราจะกำหนดสีที่แตกต่างกันให้กับผู้เล่นโดยเริ่มต้นจากการนิยามค่าคงที่เพื่อเก็บรายการสีไว้ดังนี้

FPS = 50
WINDOW_SIZE = (500,500)
BLACK = pygame.Color('black')
WHITE = pygame.Color('white')
GREY  = pygame.Color('grey')
PLAYER_COLORS = ('green','yellow','red','cyan')

ในฟังก์ชัน main() กำจัดโค้ดส่วนที่ดึงเฉพาะบอร์ดไมโครคอนโทรลเลอร์บอร์ดแรกที่พบออก แล้วแทนที่ด้วยลูปที่สร้างอ็อบเจกต์ Player หนึ่งตัวต่อหนึ่งบอร์ดที่พบ โดยกำหนดสีให้กับผู้เล่นที่สร้างขึ้นตามรายการสี PLAYER_COLORS ที่นิยามเอาไว้ตั้งแต่แรก พร้อมทั้งรายงานว่าผู้เล่นรหัสประจำตัวใดกำลังควบคุมไม้ตีสีใด

def main():
    :
    ball = Ball(speed=(200,50))
    board = PeriBoard(findDevices()[0])
    player = Player(board, color=pygame.Color('green'),pos=100)
    players = []
    for i,dev in enumerate(findDevices()):
        color = PLAYER_COLORS[i % len(PLAYER_COLORS)]
        board = PeriBoard(dev)
        players.append(Player(board,color=pygame.Color(color),pos=100,width=150))
        print "Player#%d (%s): %s" % (i+1, color, board.getDeviceName())

สังเกตว่าโค้ดในฟังก์ชัน main() มีการเรียกใช้ฟังก์ชันพิเศษของไพทอนคือ enumerate() ฟังก์ชันนี้รับรายการใด ๆ แล้วสร้างเป็นรายการใหม่ของคู่ลำดับ (i,d) โดยที่ d เป็นข้อมูลแต่ละตัวในรายการเดิม และ i เป็นลำดับของข้อมูลในรายการที่เริ่มต้นนับจาก 0

พิจารณาตัวอย่างการใช้งานฟังก์ชัน enumerate() ผ่านไพทอนเชลล์

$ python
>>> data = ['a','b','c']
>>> list(enumerate(data))
[(0, 'a'), (1, 'b'), (2, 'c')]
>>> for i,x in enumerate(data):
...     print i,x
... 
0 a
1 b
2 c
>>>

จากนั้นในลูป while ให้วนลูปอัพเดทและวาดผู้เล่นทั้งหมดที่มี แทนที่จะอัพเดทแค่ผู้เล่นเดียวเหมือนที่ผ่านมา และส่งรายการผู้เล่นทั้งหมดที่มีให้อ็อบเจกต์ ball เพื่อให้ลูกบอลคำนวณการเคลื่อนที่ของตัวเองได้จากการพิจารณาตำแหน่งไม้ตีทุกอัน

    while not game_over:
        :
        display.fill(BLACK)  # clear screen
        display.blit(score_image, (10,10))  # draw score

        player.move()
        player.draw(display)  # draw player
        for p in players:
            p.move()  # move player
            p.draw(display)  # draw player

        ball.move(1./FPS, display, player players)  # move ball
        ball.draw(display)  # draw ball

เนื่องจากตอนนี้เราส่งผู้เล่นมาเป็นรายการให้อ็อบเจกต์ของคลาส Ball จึงต้องมีการคำนวณตำแหน่งและความเร็วจากไม้ตีของผู้เล่นทุกคน ให้แก้ไขเมท็อด move() ของคลาส Ball ดังนี้

class Ball(object):
    :
    def move(self, delta_t, display, player players):
        global score, game_over
        self.x += self.vx*delta_t
        self.y += self.vy*delta_t

        # player-hitting check
        if player.can_hit(self):
            score += 1
            render_score()
            self.vx = abs(self.vx) # bounce ball back
        for p in players:
            if p.can_hit(self):
                score += 1
                render_score()
                self.vx = abs(self.vx) # bounce ball back

จับกลุ่มกับเพื่อน ๆ และลองเสียบบอร์ดไมโครคอนโทรลเลอร์ตั้งแต่สองบอร์ดขึ้นไป รันโปรแกรมเพื่อทดสอบความถูกต้อง

ปรับความเร็วลูกเมื่อกระทบไม้ตี

เพื่อเพิ่มอรรถรสในการเล่น เมื่อผู้เล่นสามารถรับลูกได้ควรให้ลูกเพิ่มความเร็วให้มากขึ้น รวมถึงสุ่มให้ลูกกระดอนออกไปในทิศทางที่แตกต่างกัน

เพิ่มความเร็วลูกหลังถูกตี

โค้ดด้านล่างเป็นการแก้ไขให้ลูกมีความเร็วเพิ่มขึ้น 20% หลังจากกระทบไม้ แต่จำกัดความเร็วไว้ไม่ให้เกิน 1000 จุดต่อวินาที

class Ball(object):
    :
    def move(self, delta_t, display, players):
        :
        for p in players:
            if p.can_hit(self):
                score += 1
                render_score()
                self.vx = abs(self.vx) # bounce ball back
                self.vx = min(1.2*abs(self.vx),1000) # bounce ball back

ปรับให้ลูกกระดอนตามตำแหน่งที่ตกกระทบไม้ตี

เพื่อให้ผู้เล่นสามารถควบคุมทิศทางของลูกได้บ้าง เราจะนำเอาตำแหน่งที่ลูกตกกระทบไม้มาพิจารณาว่าอยู่ห่างจากจุดกึ่งกลางของหน้าไม้เท่าใด จากนั้นนำค่าที่ได้ไปรวมกับความเร็วในแกน y เดิมที่มีอยู่แล้ว ผลที่ได้คือเมื่อลูกกระทบบริเวณด้านบนของหน้าไม้จะทำให้ความเร็วในแกน y เปลี่ยนไปในทิศที่ชี้ขึ้น (แต่ลูกอาจจะยังวิ่งไปในทิศทางลงอยู่หากกำลังเคลื่อนที่ลงด้วยความเร็วสูงพอ) และให้ผลตรงกันข้ามเมื่อกระทบบริเวณด้านล่างของหน้าไม้ ปรับตัวคูณจาก 2 เป็นค่าอื่นเพื่อเพิ่มหรือลดระดับการกระดอนตามต้องการ

class Ball(object):
   :
   def move(self, delta_t, display, players):
       :
       for p in players:
           if p.can_hit(self):
               score += 1
               render_score()
               self.vx = min(1.2*abs(self.vx),1000) # bounce ball back
               self.vy += (self.y-p.pos)*2

ทดสอบเกมที่ปรับแก้ไขแล้วเพื่อสังเกตพฤติกรรมของลูกบอล ซึ่งอาจต้องลองรับลูกให้ได้หลาย ๆ ครั้งก่อนจะเริ่มเห็นความเปลี่ยนแปลงที่ชัดเจน เนื่องจากตัวเกมถูกโปรแกรมให้จบการทำงานทันทีที่รับลูกพลาด แนะนำว่าให้คอมเม้นต์โค้ดส่วนที่ตรวจสอบการชนกำแพงด้านซ้ายแล้วจบเกมออกไปก่อนหากไม่ต้องการเริ่มต้นการทดสอบใหม่ทุกครั้งที่รับลูกพลาด

เพิ่มจำนวนลูก

เกมที่มีลูกบอลเพียงลูกเดียวแต่มีผู้เล่นหลายคนนั้นค่อนข้างน่าเบื่อ เราจะแก้ไขโปรแกรมให้เพิ่มจำนวนลูกตามต้องการเมื่อกด space bar บอลลูกใหม่จะปรากฏขึ้นที่กึ่งกลางหน้าจอโดยถูกสุ่มให้ความเร็วในแนวดิ่งต่าง ๆ กัน

ในที่นี้เราอาศัยฟังก์ชัน randrange() จากโมดูล random

from random import randrange
import pygame
from pygame.locals import *
from practicum import findDevices
from peri import PeriBoard

เปลี่ยนตัวแปร ball ที่ใช้เก็บลูกบอลเพียงลูกเดียวมาเป็นตัวแปร balls เพื่อเก็บเป็นลิสต์ของลูกบอลแทน โดยเริ่มต้นจากการมีลูกบอลเพียงหนึ่งลูก

def main():
    :
    ball = Ball(speed=(200,50))
    balls = [Ball(speed=(100,randrange(-50,50)))]

ในลูป while ตรวจสอบการเคาะแป้น หากเป็นคีย์ space bar ให้สร้างลูกบอลลูกใหม่ที่มีความเร็วไปในทิศทางขวา 100 จุด/วินาที และสุ่มความเร็วในแนวดิ่งในช่วง -50 ถึง 50 จุด/วินาที จากนั้นเพิ่มอ็อบเจกต์บอลลูกใหม่ลงไปในรายการ balls

    :
    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 (event.type == KEYDOWN and event.key == K_SPACE):
                newBall = Ball(speed=(100,randrange(-50,50)))
                balls.append(newBall)

แก้ไขโค้ดให้อัพเดทและวาดลูกบอลทุกลูกในรายการ เป็นอันเสร็จขั้นตอน

        :
        ball.move(1./FPS, display, players)  # move ball
        ball.draw(display)  # draw ball
        for b in balls:
            b.move(1./FPS, display, players)  # move ball
            b.draw(display)  # draw ball

ทดลองเล่นเกมแล้วเคาะแป้น space bar บอลลูกใหม่ต้องปรากฏขึ้นที่กึ่งกลางหน้าต่างเกม

เพิ่มแรงโน้มถ่วงตามแนวดิ่ง

ทำให้ลูกบอลเคลื่อนที่อยู่ภายใต้แรงโน้มถ่วงที่มีความเร่ง 100 จุด/วินาที2 โดยปรับโค้ดการคำนวณการเคลื่อนที่ให้เพิ่มความเร็วในแกน y ของลูกบอลดังนี้

class Ball(object):
    :
    def move(self, delta_t, display, players):
        global score, game_over
        self.vy += 100*delta_t
        self.x += self.vx*delta_t
        self.y += self.vy*delta_t

ทดสอบโปรแกรมควรจะเห็นว่าลูกบอลเคลื่อนที่เป็นวิถีโค้งแบบพาราโบลา ทดลองคิดสูตรความเร่งในรูปแบบต่าง ๆ เช่นมีความเร่งเข้าสู่จุดศูนย์กลางของหน้าจอเกม เสมือนว่ามีหลุมดำอยู่ ณ จุดนั้น

ตัวอย่างแนวคิดอื่น ๆ การการปรับปรุงเกม

  • เนื่องจากสภาพแสงในขณะเล่น เช่นแสงน้อยเกินไปหรือมากเกินไป มีผลต่อการควบคุม อาจมีโหมดให้ผู้เล่นวัดช่วงแสง (calibrate) ก่อนเริ่มเล่นเกม
  • กระจายให้ผู้เล่นคุมกำแพงกันคนละทิศ
  • ปรับให้เล่นแบบแข่งขันกันและคิดคะแนนแยกตามผู้เล่น
  • ยอมให้ผู้เล่นรับลูกพลาดได้มากกว่าหนึ่งครั้ง อาจอาศัย LED บนบอร์ดพ่วงแสดงชีวิตที่เหลือ
  • สุ่มให้มีไอเท็มพิเศษปรากฏขึ้นเพื่อเพิ่ม/ลดความสามารถของผู้เล่นที่เก็บได้
  • เปลี่ยนรูปแบบการเล่นจากการตีสควอชเป็นเตะตะกร้อลอดห่วง

เอกสารเพิ่มเติม