Oop lab/oop in python
- หน้านี้เป็นส่วนหนึ่งของวิชา Oop lab
เราจะหัดเขียน OOP กันผ่านทางตัวอย่าง โดยเราจะเขียนโปรแกรมแสดงผลเป็นกราฟิกส์โดยใช้ไลบรารีชื่อ Arcade ซึ่งต้องการ Python 3.6 เป็นอย่างน้อย
เนื้อหา
ติดตั้ง Arcade
เราจะใช้ game library ชื่อ arcade ในการพัฒนาเกม
Python 3.6
ทดลองเรียก
python --version
ถ้าระบบตอบว่าเป็นเวอร์ชั่นสูงกว่าหรือเท่ากับ 3.6 ก็ไปทำขั้นตอนการติดตั้ง pip ได้เลย ไม่เช่นนั้นให้ไปติดตั้ง Python 3.6 ตามวิธีต่อไปนี้
ติดตั้ง Python 3.6 บน Windows
ให้ทำตามขั้นตอนในนี้ได้เลย การติดตั้ง arcade อย่าลืมเลือกให้ลง python ใน path ด้วย
ติดตั้ง Python 3.6 บน Ubuntu (รุ่นตั้งแต่ 16.10 ขึ้นไป)
สั่ง
sudo apt-get update sudo apt-get install python3.6
เวลาเรียกใช้ให้เรียก python3.6
ติดตั้ง Python 3.6 บน Ubuntu เก่า
sudo add-apt-repository ppa:jonathonf/python-3.6 sudo apt-get update sudo apt-get install python3.6
ติดตั้ง pip/pip3
pip เป็นโปรแกรมสำหรับติดตั้ง library บน Python ในระบบที่มี python3 พร้อม ๆ กับ python2 เราจะเรียก pip3 เพื่อให้ติดตั้ง library ลงในส่วนของ python3
ให้ลองเรียก
pip
หรือ
pip3
ถ้ามีโปรแกรมแล้วก็ข้ามส่วนติดตั้ง pip ได้เลย
1. ติดตั้ง pip บน Windows
pip จะมาพร้อมกับการติดตั้ง python ให้เลือก option ดังกล่าวด้วย ถ้าเปิด command แล้วเรียก pip ขึ้นก็แสดงว่าได้ติดตั้งเรียบร้อย ถ้าไม่มี ให้ลองติดตั้ง python3 อีกครั้ง และให้เลือก pip ในหน้า Optional Features ด้วย
- ดูรายละเอียดการติดตั้งที่นี่ arcade installation บน windows
2. ติดตั้ง pip3 บน Linux
ให้ติดตั้ง pip3 และโปรแกรมประกอบอื่น ๆ โดยสั่ง
sudo apt install -y python3-dev python3-pip libjpeg-dev zlib1g-dev
3. ติดตั้ง pip3 บน Mac
ถ้าติดตั้ง python3 แล้ว น่าจะมี pip3 มาแล้ว ทดลองเรียกดูใน terminal
บน Mac จะต้องติดตั้ง libjpg ด้วย ถ้ายังไม่มี ถ้าคุณมี homebrew อยู่แล้ว ให้สั่ง
brew install libjpeg
ถ้าไม่ได้ ให้ไปดาวน์โหลดและ install จาก [1] (เลือก libjpg)
ใช้ pip ติดตั้ง arcade
ถ้าในการติดตั้ง python เราได้ลง pip มาแล้ว เราจะสามารถติดตั้ง arcade ผ่านทาง pip ได้โดยสั่ง
sudo pip3 install arcade
ถ้าเป็น windows ให้สั่ง
pip install arcade
หมายเหตุ: ถ้าติดตั้งด้วย pip แล้ว pip เอาไปลงกับ Python 3.5 ให้ ให้สั่งแบบนี้แทน (ขอบคุณศิรกร):
python3.6 -m pip install arcade
virtualenv
เราติดตั้ง arcade ลงใน library ของระบบเลย ซึ่งทำแบบนี้บ่อย ๆ อาจจะทำให้ library เละและตีกันได้ Python มีระบบสำหรับติดตั้ง library แยกกัน เรียกว่า virtualenv
- ยังเขียนส่วนนี้ไม่เสร็จ: to do - how to install with virtualenv
ทดสอบการติดตั้ง
ให้ copy code ด้านล่างนี้ ในไฟล์ชื่อ cirtest.py แล้วทดลองรัน
import arcade
from random import randint
SCREEN_WIDTH = 600
SCREEN_HEIGHT = 600
circle_size = 1
size_direction = 1
circle_xs = []
circle_ys = []
num_circles = 100
def random_locations():
for i in range(num_circles):
circle_xs.append(randint(10,SCREEN_WIDTH-10))
circle_ys.append(randint(10,SCREEN_HEIGHT-10))
def on_draw(delta_time):
global circle_size, size_direction
circle_size += size_direction
if circle_size > 50:
size_direction = -1
elif circle_size == 1:
size_direction = 1
arcade.start_render()
for x,y in zip(circle_xs, circle_ys):
arcade.draw_circle_outline(x, y, circle_size, arcade.color.BLACK)
def main():
random_locations()
arcade.open_window(SCREEN_WIDTH, SCREEN_HEIGHT,
"Circles")
arcade.set_background_color(arcade.color.WHITE)
arcade.schedule(on_draw, 1 / 80)
arcade.run()
if __name__ == '__main__':
main()
อธิบายบางประเด็น
- randint -- สุ่มเลขระหว่างเลขที่ระบุ ต้อง import random ก่อน
- global -- ใน Python ฟังก์ชันจะสามารถอ้างถึงตัวแปรโกลบอลได้ถ้าอ่านค่าอย่างเดียว แต่ถ้าต้องการเปลี่ยนแปลงตัวแปรโกลบอลจะต้องประกาศให้ชัดเจนว่าจะมีการใช้ตัวแปรโกลบอลนั้น ในฟังก์ชัน on_draw เราต้องการเปลี่ยนค่าขนาดวงกลมและทิศทางการเปลี่ยนค่า เราจึงต้องประกาศ
- zip -- ฟังก์ชัน zip นำลิสต์สองลิสต์มารวมเข้าด้วยกัน เช่น zip([1,2,3],['a','b','c']) ได้ผลลัพธ์เทียบเท่ากับ [(1, 'a'), (2, 'b'), (3, 'c')] มักใช้เวลาต้องการ for ไปในรายการหลายอันพร้อม ๆ กัน
- ฟังก์ชันจาก arcade
- arcade.start_render ให้เรียกก่อนจะวาดหน้าจอ จะลบหน้าจอเดิม (มีทดลองในส่วนถัดไป)
- arcade.draw_circle_outline วาดวงกลม
- arcade.open_window
- arcade.set_background_color
- arcade.schedule
- arcade.run
วงกลมขยับ
วาดวงกลม
เราจะเริ่มเขียนจากโปรแกรมที่ทำอะไรไม่ค่อยได้ ไปหาโปรแกรมที่ซับซ้อนขึ้น โปรแกรมด้านล่างวาดรูปวงกลมที่กลางหน้าจอ เขียนและเซฟไว้ในไฟล์ชื่อ cir1.py
import arcade
SCREEN_WIDTH = 600
SCREEN_HEIGHT = 600
def on_draw(delta_time):
arcade.start_render()
x = 300
y = 300
arcade.draw_circle_outline(x, y, 20, arcade.color.BLACK)
def main():
arcade.open_window(SCREEN_WIDTH, SCREEN_HEIGHT,
"Circles")
arcade.set_background_color(arcade.color.WHITE)
arcade.schedule(on_draw, 1 / 80)
arcade.run()
if __name__ == '__main__':
main()
ทดลอง: ให้ทดลองลบบรรทัด arcade.start_render() ออก ผลเป็นอย่างไร?
เคลื่อนที่
เราจะบังคับให้วงกลมเคลื่อนที่ โดยปรับค่าตำแหน่งตามความเร็วสองแกนคือ vx แทนความเร็วในแนวแกน x และ vy แทนความเร็วในแกน y
เนื่องจากเราต้องการเปลี่ยนค่าตำแหน่งวงกลมอย่างต่อเนื่อง เราจึงต้องมีตัวแปรโกลบอลเก็บค่าตำแหน่ง คือ x และ y ในฟังก์ชัน on_draw เราจะระบุว่าจะแก้ค่าตัวแปรดังกล่าวด้วย keyword global เราใส่ vx และ vy เผื่อไว้ด้วยเลย (เพราะว่าเราต้องการให้สะท้อนขอบจอ)
โค้ดของ on_draw เป็นดังนี้ บรรทัดที่ประกาศ x,y,vx,vy เป็นการกำหนดค่าให้กับตัวแปรโกลบอล
vx = 2
vy = 1
x = 300
y = 300
def on_draw(delta_time):
arcade.start_render()
global x, y, vx, vy
x += vx
y += vy
arcade.draw_circle_outline(x, y, 20, arcade.color.BLACK)
แบบฝึกหัด 1: ชนและสะท้อนที่ขอบ
ตอนนี้ลูกบอลเมื่อวิ่งทะลุขอบจอแล้วจะหายไปเลย ให้เพิ่มการตรวจสอบว่าลูกบอลทะลุจอไปแล้วแล้วปรับให้ลูกบอลเสมือนเด้งกลับมา
def on_draw(delta_time):
arcade.start_render()
global x, y, vx, vy
x += vx
y += vy
# ตรวจสอบตำแหน่ง ถ้าตกขอบให้เปลี่ยนทิศทาง
# hint: ให้ทำทีละแกน จะง่ายกว่า
arcade.draw_circle_outline(x, y, 20, arcade.color.BLACK)
วงกลมหลาย ๆ วง
เราจะแก้โปรแกรมให้มีลูกบอลวิ่งไปมาหลาย ๆ ลูก ให้เซฟโปรแกรมที่เขียนในไฟล์ใหม่ชื่อ cir2.py
เราจะสุ่มตำแหน่งและความเร็วของลูกบอลด้วยฟังก์ชัน randint ดังนั้นให้เพิ่มบรรทัด import ดังด้านล่าง
import arcade
from random import randint
เราจะเก็บตำแหน่งและความเร็วในแกนต่าง ๆ ของลูกบอลในลิสต์ xs, ys, vxs, vys และจำนวนลูกบอลในตัวแปร n
vxs = []
vys = []
xs = []
ys = []
n = 10
def initialize():
for i in range(n):
xs.append(randint(100, SCREEN_WIDTH-100))
ys.append(randint(100, SCREEN_HEIGHT-100))
vxs.append(randint(-5,5))
vys.append(randint(-5,5))
เราจะเขียนฟังก์ชันสำหรับจัดการลูกบอลลูกที่ i ในรายการดังด้านล่าง จากนั้น on_draw จะเรียกฟังก์ชันนี้ สังเกตว่าเรายังไม่ได้จัดการการชนขอบจอ
def draw_and_move_circle(i):
xs[i] += vxs[i]
ys[i] += vys[i]
arcade.draw_circle_outline(xs[i], ys[i], 20, arcade.color.BLACK)
on_draw วนลูบเรียกฟังก์ชัน draw_and_move_circle
def on_draw(delta_time):
arcade.start_render()
for i in range(n):
draw_and_move_circle(i)
อย่าลืมเรียก initialize ใน main
def main():
initialize()
arcade.open_window(SCREEN_WIDTH, SCREEN_HEIGHT,
"Circles")
arcade.set_background_color(arcade.color.WHITE)
arcade.schedule(on_draw, 1 / 80)
arcade.run()
แยกฟังก์ชัน
ฟังก์ชัน draw_and_move_circle เป็นตัวอย่างของฟังก์ชันที่พยายามทำหลายอย่างมากเกินไป เราจะแยกฟังก์ชันออกเป็นสองฟังก์ชัน แล้วเรียกใน on_draw
def move_circle(i):
# .... อย่าลืมย้ายโค้ดมา
def draw_circle(i):
# .... อย่าลืมย้ายโค้ดมา
def on_draw(delta_time):
arcade.start_render()
for i in range(n):
move_circle(i)
draw_circle(i)
แบบฝึกหัด 2: ชนและสะท้อนที่ขอบ
โปรแกรมที่มีลูกบอลหลายลูกที่เราเขียนยังไม่ได้มีการจัดการเรื่องการชนกับขอบ ให้แก้ฟังก์ชัน move_circle ให้ตรวจสอบและปรับทิศทางของลูกบอลให้สะท้อนกลับมาด้วย
คลาส Circle
สังเกตว่าในโค้ดชุดก่อน เราจะจัดการกับลูกบอลโดยพิจารณาตัวแปรที่เกี่ยวข้องกัน 4 ตัวคือ x, y, vx, และ vy แทบจะตลอดเวลา เราจะ extract ตัวแปรที่เกี่ยวข้องกันนี้ออกมาเป็น object ของคลาส Circle
ให้ย้ายโค้ดจากส่วนที่แล้วมาเขียนใหม่ในไฟล์ cir3.py
คลาสดังกล่าวเขียนบางส่วนได้ดังนี้ อย่าลืมว่าใน python เวลาเขียนเมท็อดจะมีตัวแปร self (ที่เป็นตัวแปรตัวแรก) แทน object ที่ทำงานด้วย และเมท็อด __init__ จะเป็นเมท็อดพิเศษที่ใช้ในการกำหนดค่าเริ่มต้นให้กับ object
class Circle:
def __init__(self, x, y, vx, vy):
self.x = x
self.y = y
self.vx = vx
self.vy = vy
def move(self):
self.x += self.vx
self.y += self.vy
# เพิ่มโค้ดส่วนจัดการการชนขอบจอที่นี่ด้วย
def draw(self):
arcade.draw_circle_outline(self.x, self.y,
20, arcade.color.BLACK)
แก้ส่วนสร้าง object
circles = []
n = 10
def initialize():
for i in range(n):
circle = Circle(randint(100, SCREEN_WIDTH-100),
randint(100, SCREEN_HEIGHT-100),
randint(-5,5),
randint(-5,5))
circles.append(circle)
แก้เมท็อด on_draw
def on_draw(delta_time):
arcade.start_render()
for c in circles:
c.move()
c.draw()
แบบฝึกหัด 3: วงกลมที่มีหลายขนาด
เราจะเพิ่มตัวแปร r ลงใน Circle โดยให้มีค่าเป็น 20 ถ้าไม่มีการระบุตอนสร้าง
class Circle:
def __init__(self, x, y, vx, vy, r=20):
self.x = x
self.y = y
self.vx = vx
self.vy = vy
self.r = r
# ...
ให้แก้โค้ดที่เกี่ยวข้องให้สุ่มมาแล้วมีลูกบอลหลายขนาดเคลื่อนที่ในหน้าจอ
เกมหลบบอล
เราจะเขียนเกมหลบลูกบอล ให้ copy โค้ดจากส่วนก่อน ๆ ลงในไฟล์ชื่อ cir4.py แล้วทำงานต่อที่ไฟล์นี้
เราจะนิยามคลาส Player โดย player จะเป็นวงกลมรัศมี 10 สีน้ำเงิน
class Player:
def __init__(self, x, y):
self.x = x
self.y = y
def draw(self):
arcade.draw_circle_filled(self.x, self.y,
10, arcade.color.BLUE)
สร้าง object ของคลาส Python ไว้บริเวณใกล้ ๆ กับที่ประกาศ circles (การหาร // คือหารปัดเศษ)
circles = []
player = Player(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2)
n = 10
เพิ่มโค้ดให้แสดง player ใน on_draw
def on_draw(delta_time):
arcade.start_render()
for c in circles:
c.move()
c.draw()
player.draw()
ขยับ player
ขั้นตอนนี้จะยุ่งยากเล็กน้อย เนื่องจากเราจะใช้การอ่านการกดปุ่มแบบอ่านสถานะ ในการเขียนครั้งถัด ๆ ไปจะสะดวกกว่านี้
เตรียมการอ่าน keys
เราจะใช้ส่วนอ่านการกดปุ่มจากไลบรารี pyglet ดังนั้นต้องไป import ก่อน (ใส่ตรงหัวโปรแกรม)
from pyglet.window import key
เราจะประกาศตัวแปร keys เพื่อใช้อ่านสถานะการกดปุ่ม ให้ใส่ไว้ก่อน on_draw โดยอาจจะประกาศไว้แถว ๆ ที่เราประกาศตัวแปร circles, players, และ n ก็ได้
# ... โค้ดเก่า
circles = []
player = Player(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2)
n = 10
# ประกาศ keys
keys = key.KeyStateHandler()
ในฟังก์ชัน main ไปบอกกับหน้าต่างให้อัพเดทสถานะการกดปุ่มผ่านทาง keys โดยเพิ่มบรรทัด push_handlers ให้ใส่ไว้ก่อน arcade.schedule
arcade.get_window().push_handlers(keys)
# .. โค้ดเก่า ...
arcade.schedule(on_draw, 1 / 80)
ส่งปุ่มให้ player
เพิ่มเมท็อด control ในคลาส Player ที่ตอนนี้ตรวจการกดปุ่มซ้ายอย่างเดียว
class Player:
# ... ละโค้ดเก่าไว้
def control(self, keys):
if keys[key.LEFT]:
self.x -= 5
จากนั้นเรียก player.control จาก on_draw
def on_draw(delta_time):
arcade.start_render()
for c in circles:
c.move()
c.draw()
player.control(keys)
player.draw()
แบบฝึกหัด 4: เพิ่มการอ่านปุ่มขึ้นลงขวาให้ครบ
แก้เมท็อด Player.control ให้ตรวจสอบการเคลื่อนที่ให้ครบทุกทิศทาง
แบบฝึกหัด 5: โดนชน
เขียนเมท็อด is_hit ที่ตรวจสอบว่า player ชนกับ circle หรือไม่
class Player:
# ... โค้ดอื่น ๆ ไม่แสดง
def is_hit(self, circle):
# ... ตรวจสอบว่า self ชนกับ circle หรือเปล่า
# ทำโดยพิจารณาตำแหน่ง self.x, self.y กับ circle.x และ circle.y กับรัศมี circle.r
# อย่าลืมว่า player มีรัศมี 10
#
# method นี้คืนค่า True หรือ False
ใน on_draw ให้เรียก is_hit ถ้าจริงให้จบเลย
def on_draw(delta_time):
arcade.start_render()
for c in circles:
c.move()
c.draw()
if player.is_hit(c):
quit()
player.draw()
player.control(keys)
ถ้าเกมยากไป (เปิดมาตายเลย) ให้ลดจำนวนลูกบอล หรือเปลี่ยนการสุ่มให้ไม่อยู่กลางหน้าจอมากไป
แบบฝึกหัด 6: จบแบบเนียนๆ
แทนที่จะ quit เลย ให้แก้โปรแกรมให้จบได้เนียนกว่าเดิม เช่นลูกบอลหยุด หรือ player หายไป หรืออะไรก็ได้
แบบฝึกหัด 100: ชนและเด้ง
ถ้าทำมาถึงจุดนี้ ลองเขียนให้ลูกบอลเด้งเวลาชนกันเองด้วย