ผลต่างระหว่างรุ่นของ "Oop lab/arcade/space"
Jittat (คุย | มีส่วนร่วม) |
Jittat (คุย | มีส่วนร่วม) |
||
(ไม่แสดง 80 รุ่นระหว่างกลางโดยผู้ใช้คนเดียวกัน) | |||
แถว 1: | แถว 1: | ||
: ''หน้านี้เป็นส่วนหนึ่งของ [[oop lab]]'' | : ''หน้านี้เป็นส่วนหนึ่งของ [[oop lab]]'' | ||
− | == ติดตั้ง Arcade == | + | == ติดตั้ง Arcade (ข้ามได้ถ้าติดตั้งแล้ว) == |
+ | |||
+ | : '''ในส่วนนี้นิสิตน่าจะติดตั้ง Arcade เรียบร้อยตั้งแต่สัปดาห์ที่แล้วแล้ว สามารถข้ามไปส่วนถัดไปได้เลย''' | ||
เราจะใช้ game library ชื่อ [https://pythonhosted.org/arcade/ arcade] ในการพัฒนาเกม | เราจะใช้ game library ชื่อ [https://pythonhosted.org/arcade/ arcade] ในการพัฒนาเกม | ||
แถว 24: | แถว 26: | ||
ถ้าติดตั้ง python3 แล้ว น่าจะมี pip3 มาแล้ว ทดลองเรียกดูใน terminal | ถ้าติดตั้ง python3 แล้ว น่าจะมี pip3 มาแล้ว ทดลองเรียกดูใน terminal | ||
+ | |||
+ | บน Mac จะต้องติดตั้ง libjpg ด้วย ถ้ายังไม่มี ถ้าคุณมี homebrew อยู่แล้ว ให้สั่ง | ||
+ | |||
+ | brew install libjpeg | ||
+ | |||
+ | ถ้าไม่ได้ ให้ไปดาวน์โหลดและ install จาก [http://ethan.tira-thompson.com/Mac_OS_X_Ports.html] (เลือก libjpg) | ||
=== ใช้ pip ติดตั้ง arcade === | === ใช้ pip ติดตั้ง arcade === | ||
แถว 29: | แถว 37: | ||
ถ้าในการติดตั้ง python เราได้ลง pip มาแล้ว เราจะสามารถติดตั้ง arcade ผ่านทาง pip ได้โดยสั่ง | ถ้าในการติดตั้ง python เราได้ลง pip มาแล้ว เราจะสามารถติดตั้ง arcade ผ่านทาง pip ได้โดยสั่ง | ||
− | pip3 install arcade | + | sudo pip3 install arcade |
ถ้าเป็น windows ให้สั่ง | ถ้าเป็น windows ให้สั่ง | ||
แถว 41: | แถว 49: | ||
== เกมว่าง ๆ == | == เกมว่าง ๆ == | ||
− | === ก่อนเริ่ม === | + | === ก่อนเริ่ม + git === |
สร้าง directory สำหรับเก็บเกม เราจะใช้ git อย่างสม่ำเสมอและตลอดเวลา ดังนั้น อย่าลืมเรียก | สร้าง directory สำหรับเก็บเกม เราจะใช้ git อย่างสม่ำเสมอและตลอดเวลา ดังนั้น อย่าลืมเรียก | ||
แถว 48: | แถว 56: | ||
ก่อนเขียน | ก่อนเขียน | ||
+ | |||
+ | ถ้าลืมไปแล้วว่าใช้ git บน command line ได้อย่างไร กรุณากลับไปดูคลิป [https://www.youtube.com/watch?v=9vtjSo9IIBM แนะนำ git 1] | ||
=== template === | === template === | ||
แถว 92: | แถว 102: | ||
{{gitcomment|commit งานด้วย!}} | {{gitcomment|commit งานด้วย!}} | ||
+ | |||
+ | === รูปอธิบายโครงสร้างโปรแกรมและความรับผิดชอบส่วนต่าง ๆ จนถึงจุดนี้ === | ||
+ | |||
+ | [[Image:Arcade-space-modules1.png]] | ||
== แสดงรูปยานอวกาศ == | == แสดงรูปยานอวกาศ == | ||
+ | รูปประกอบต่าง ๆ รวมถึงรูปของตัวละครในเกมที่มีพื้นที่ว่างเพื่อแสดงพื้นหลังโดยมากจะเรียกว่า sprite เกม 2 มิติโดยมากก็จะไม่ได้วาดรูปพิเศษอะไรแต่เน้นการใช้ sprite ในการแสดงเรื่องราวเป็นหลัก | ||
=== สร้าง sprite === | === สร้าง sprite === | ||
แถว 107: | แถว 122: | ||
ถ้าขี้เกียจวาด ไปเอารูปได้ที่ [http://theory.cpe.ku.ac.th/~jittat/cocos/files/ship.png ship.png]. | ถ้าขี้เกียจวาด ไปเอารูปได้ที่ [http://theory.cpe.ku.ac.th/~jittat/cocos/files/ship.png ship.png]. | ||
+ | |||
+ | === Sprite === | ||
+ | |||
+ | เราจะสร้างวัตถุจากคลาส arcade.Sprite แทนยานอวกาศดังกล่าว เราจะเพิ่ม attribute <tt>ship</tt> เพื่อเก็บ sprite นี้ | ||
+ | |||
+ | <syntaxhighlight lang="python"> | ||
+ | def __init__(self, width, height): | ||
+ | super().__init__(width, height) | ||
+ | |||
+ | arcade.set_background_color(arcade.color.BLACK) | ||
+ | |||
+ | self.ship = arcade.Sprite('images/ship.png') | ||
+ | self.ship.set_position(100, 100) | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | สังเกตว่าเราเรียกเมท็อด <tt>set_position</tt> ในการระบุตำแหน่งของยานอวกาศบนหน้าจอ ตำแหน่งนี้จะเป็นตำแหน่งของจุดตรงกลางของ sprite ซึ่งสามารถอ่านได้จาก attribute center_x และ center_y | ||
+ | |||
+ | เราจะวาดรูปยานอวกาศในเมท็อด <tt>on_draw</tt> ดังนี้ | ||
+ | |||
+ | <syntaxhighlight lang="python"> | ||
+ | def on_draw(self): | ||
+ | arcade.start_render() | ||
+ | |||
+ | self.ship.draw() | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | ทดลองรันโปรแกรม จะเห็นว่ามียานอวกาศนิ่ง ๆ ปรากฏบนหน้าจอ | ||
+ | |||
+ | {{gitcomment|ได้เวลา commit สักที (อย่าลืม add images/ship.png ด้วย)}} | ||
+ | |||
+ | === รูปอธิบายโครงสร้างโปรแกรมและความรับผิดชอบส่วนต่าง ๆ จนถึงจุดนี้ === | ||
+ | |||
+ | [[Image:Arcade-space-modules2.png]] | ||
== เคลื่อนที่ == | == เคลื่อนที่ == | ||
+ | เราจะปรับตำแหน่งของ self.ship เป็นระยะ ๆ โดยเขียนเมท็อด update ในคลาส SpaceGameWindow ซึ่งจะถูกเรียกเป็นระยะ ๆ โดยอัตโนมัติโดย framework เมท็อดดังกล่าวจะได้รับ parameter delta แทนระยะเวลาระหว่างการเรียกเมท็อด update ครั้งที่แล้ว กับครั้งนี้ มาด้วย ซึ่งเราจะใช้หรือไม่ก็ได้ | ||
+ | |||
+ | โค้ดด้านล่างปรับตำแหน่งของ self.ship สังเกตว่าเราปรับตำแหน่งโดยคงค่า center_x ไว้ แต่เพิ่ม center_y ขึ้น 5 | ||
+ | |||
+ | <syntaxhighlight lang="python"> | ||
+ | class SpaceGameWindow(arcade.Window): | ||
+ | # ... | ||
+ | |||
+ | def update(self, delta): | ||
+ | self.ship.set_position(self.ship.center_x, self.ship.center_y + 5) | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | ให้สังเกตการใช้ self.ship ในโค้ดด้านบนด้วย | ||
+ | |||
+ | เมื่อทดลองรัน เราจะเห็นว่ายานจะวิ่งทะลุไปด้านบนของหน้าจอ | ||
+ | |||
+ | เราจะปรับให้ยานวิ่งทะลุมาจากด้านล่างโดยตรวจสอบพิกัดในแกน y ของยานก่อน ดังโค้ดด้านล่าง | ||
+ | |||
+ | <syntaxhighlight lang="python"> | ||
+ | def update(self, delta): | ||
+ | ship = self.ship | ||
+ | |||
+ | if ship.center_y > SCREEN_HEIGHT: | ||
+ | ship.center_y = 0 | ||
+ | ship.set_position(ship.center_x, ship.center_y + 5) | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | สังเกตว่าเราจะต้องเรียกใช้ self.ship บ่อย เราเลยเอามาเก็บไว้ในตัวแปร local ชื่อ ship เสียก่อนเลย | ||
+ | |||
+ | ทดลองรันทดสอบโปรแกรมว่าทำงานได้ถูกต้องหรือไม่ | ||
+ | |||
+ | {{gitcomment|ได้เวลา commit อีกทีล่ะ}} | ||
+ | |||
+ | === รูปอธิบายโครงสร้างโปรแกรมและความรับผิดชอบส่วนต่าง ๆ จนถึงจุดนี้ === | ||
+ | |||
+ | [[Image:Arcade-space-modules3.png]] | ||
+ | |||
+ | == World, ModelSprite == | ||
+ | โปรแกรมที่เราเขียนมามีปัญหาหลายอย่าง หลัก ๆ คือโค้ดทั้งหมดรวมอยู่ใน space.py ที่แย่ไปกว่านั้นก็คือสถานะของยานอวกาศผูกอยู่กับ sprite ทำให้ถ้าเราต้องการจะทดสอบโค้ดที่เกี่ยวข้องกับการทำงานของยานอวกาศ เราต้องไปยุ่งกับ sprite ด้วย อาจจะทำให้ทำ unit test ได้ลำบาก | ||
+ | |||
+ | ในส่วนนี้เราจะพยายามแยกโค้ดต่าง ๆ ออกมาจากโปรแกรมหลัก โดยพยายามอิงกับโครงสร้างของเกมจาก libGDX ที่เราเคยเขียนบน Java เราจะจัดการเป็นขั้น ๆ ดังนี้ | ||
+ | |||
+ | === คลาส Ship === | ||
+ | เราจะแยกโค้ดเกี่ยวกับการจัดการตำแหน่ง (และอื่น ๆ ในอนาคต) ของยานอวกาศ มาไว้ในคลาส Ship เราจะรวมโค้ดส่วนนี้ไว้ในโมดูลชื่อว่า models | ||
+ | |||
+ | เขียน class Ship ใน models.py ดังนี้ สังเกตว่าเราแทบจะย้ายโค้ดมาจาก space.py เลย เรายังมีปัญหาในการนำค่า SCREEN_HEIGHT มาใช้ ตอนนี้ให้ใส่เป็นค่าคงที่ไว้ก่อน (เดี๋ยวเราจะไปแก้) | ||
+ | |||
+ | <syntaxhighlight lang="python"> | ||
+ | class Ship: | ||
+ | def __init__(self, x, y): | ||
+ | self.x = x | ||
+ | self.y = y | ||
+ | |||
+ | def update(self, delta): | ||
+ | if self.y > 600: | ||
+ | self.y = 0 | ||
+ | self.y += 5 | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | เราจะกลับไปปรับโค้ดใน space.py ให้ใช้คลาสนี้ ด้านล่างเป็นโค้ดหลังแก้ (ไม่ได้แสดงทั้งหมด) อย่าลืมอ่านคำอธิบายถัดไปด้วย | ||
+ | |||
+ | <syntaxhighlight lang="python"> | ||
+ | import arcade | ||
+ | |||
+ | from models import Ship | ||
+ | |||
+ | # ... | ||
+ | class SpaceGameWindow(arcade.Window): | ||
+ | def __init__(self, width, height): | ||
+ | super().__init__(width, height) | ||
+ | |||
+ | arcade.set_background_color(arcade.color.BLACK) | ||
+ | |||
+ | self.ship = Ship(100,100) | ||
+ | self.ship_sprite = arcade.Sprite('images/ship.png') | ||
+ | |||
+ | |||
+ | def on_draw(self): | ||
+ | arcade.start_render() | ||
+ | self.ship_sprite.draw() | ||
+ | |||
+ | def update(self, delta): | ||
+ | ship = self.ship | ||
+ | |||
+ | ship.update(delta) | ||
+ | self.ship_sprite.set_position(ship.x, ship.y) | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | ก่อนอื่นต้อง import มาก่อน เราสามารถเขียน import models แล้วอ้างคลาสเป็น models.Ship ก็ได้ แต่เราจะ import โดยตรงมาเฉพาะคลาสนี้เท่านั้นเพื่อให้สะดวกตอนเรียกใช้ เราจะเขียนดังนี้ | ||
+ | |||
+ | from models import Ship | ||
+ | |||
+ | เราสร้างวัตถุ ship โดยสั่ง | ||
+ | |||
+ | self.ship = Ship(100,100) | ||
+ | |||
+ | เราเปลี่ยน attribute ship เดิม ให้เป็น ship_sprite | ||
+ | |||
+ | self.ship_sprite = arcade.Sprite('images/ship.png') | ||
+ | |||
+ | เนื่องจากเราแยกโมเดลออกจาก sprite ดังนั้นเมื่ออัพเดทโมเดลแล้ว ต้องไปอัพเดท sprite ตามด้วย (ดูในเมท็อด update) อันนี้จะเป็นความยุ่งยากที่เพิ่มขึ้น ที่เราแลกมาถ้าต้องการให้โค้ดส่วนโมเดล test ได้สะดวก (ถ้าต้องการ) | ||
+ | |||
+ | ถ้าทดลองเล่นแล้วโอเค อย่าลืม | ||
+ | |||
+ | {{gitcomment|add models.py แล้ว commit ด้วย (อย่าลืม -a หรือ -am ด้วย ไม่เช่นนั้นที่แก้มาใน space.py จะไม่โดนเก็บไปด้วย)}} | ||
+ | |||
+ | === รูปอธิบายโครงสร้างโปรแกรมและความรับผิดชอบส่วนต่าง ๆ จนถึงจุดนี้ === | ||
+ | |||
+ | [[Image:Arcade-space-modules4.png]] | ||
+ | |||
+ | === คลาส World === | ||
+ | เราจะแก้ปัญหาการต้องอ้างถึง 600 โดยนำค่ามาเก็บไว้ในคลาส World (ซึ่ง Ship จะรู้จัก) และจะย้ายการจัดการโมเดลทั้งหมดมาไว้ในคลาส World (โค้ดเราตอนนี้มีโมเดลเดียวคือ Ship) | ||
+ | |||
+ | เราจะเขียนคลาส World ไว้ใน models.py เช่นเดียวกับ Ship เพิ่มเข้าไปตอนหลัง Ship | ||
+ | |||
+ | <syntaxhighlight lang="python"> | ||
+ | class World: | ||
+ | def __init__(self, width, height): | ||
+ | self.width = width | ||
+ | self.height = height | ||
+ | |||
+ | self.ship = Ship(100, 100) | ||
+ | |||
+ | |||
+ | def update(self, delta): | ||
+ | self.ship.update(delta) | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | จากนั้นเราไปแก้ space.py ให้เรียกคลาส World แทนที่จะไปสร้าง Ship โดยตรง ตามขั้นตอนนี้ | ||
+ | |||
+ | 1. อย่าลืม import (เพิ่ม World เข้าไปในรายการ) | ||
+ | |||
+ | from models import World, Ship | ||
+ | |||
+ | 2. ปรับ constructor ของ SpaceGameWindow | ||
+ | |||
+ | <syntaxhighlight lang="python"> | ||
+ | class SpaceGameWindow(arcade.Window): | ||
+ | def __init__(self, width, height): | ||
+ | super().__init__(width, height) | ||
+ | |||
+ | arcade.set_background_color(arcade.color.BLACK) | ||
+ | |||
+ | self.ship_sprite = arcade.Sprite('images/ship.png') | ||
+ | |||
+ | self.world = World(width, height) # แทนที่บรรทัด ship | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | 3. แก้ update สังเกตว่าในการเชื่อมระหว่างโมเดลกับ sprite เราต้องไปอ้างเอา ship มาจาก world อีกที สังเกตว่าเราจะไม่เขียน getter หรือ setter เพราะว่าโดยมากจะไม่ค่อยจำเป็นใน Python (อ่านเพิ่มที่ [http://dirtsimple.org/2004/12/python-is-not-java.html]) | ||
+ | |||
+ | <syntaxhighlight lang="python"> | ||
+ | def update(self, delta): | ||
+ | self.world.update(delta) | ||
+ | self.ship_sprite.set_position(self.world.ship.x, self.world.ship.y) | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | ทดลองรันว่าทำงานได้ถูกต้อง | ||
+ | |||
+ | เราจะแก้ให้คลาส Ship ไม่ต้องใช้ magic number 600 โดยให้อ่านจาก world | ||
+ | |||
+ | แก้คลาส Ship ดังนี้ | ||
+ | |||
+ | <syntaxhighlight lang="python"> | ||
+ | class Ship: | ||
+ | def __init__(self, world, x, y): | ||
+ | self.world = world | ||
+ | self.x = x | ||
+ | self.y = y | ||
+ | |||
+ | def update(self, delta): | ||
+ | if self.y > self.world.height: | ||
+ | self.y = 0 | ||
+ | self.y += 5 | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | อย่าลืมแก้บรรทัดที่สร้าง ship ในเมท็อด __init__ ใน World ด้วย | ||
+ | |||
+ | <syntaxhighlight lang="python"> | ||
+ | self.ship = Ship(self, 100, 100) | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | ถ้าทดลองรันแล้วทำงานได้เหมือนเดิม อย่าลืม | ||
+ | |||
+ | {{gitcomment|commit โค้ด}} | ||
+ | |||
+ | === รูปอธิบายโครงสร้างโปรแกรมและความรับผิดชอบส่วนต่าง ๆ จนถึงจุดนี้ === | ||
+ | |||
+ | [[Image:Arcade-space-modules5.png]] | ||
+ | |||
+ | === คลาส ModelSprite === | ||
+ | : ''หมายเหตุ: คลาส WorldRenderer เป็นคลาสที่จัดการวาด sprite ทั้งหมดในโค้ดที่เขียนบน Java สำหรับนิสิตที่เรียนปีการศึกษา 2560 ถ้าอ่านแล้วไม่เข้าใจไม่ต้องกังวล ให้อ่านข้ามไปได้เลย'' | ||
+ | |||
+ | ถ้าเราพยายามอิงโครงสร้างจาก libGDX เราจะพบว่าเราจะต้องสร้าง WorldRenderer อย่างไรก็ตามเนื่องจากเรายังไม่ค่อยมีของบนหน้าจอมาก เราจะยังไม่แยกส่วนดังกล่าวออกในตอนนี้ แต่จะปรับคลาส Sprite เพื่อให้การเชื่อมกับโมเดลของเราสะดวกขึ้น | ||
+ | |||
+ | เราจะสร้างคลาส ModelSprite ที่เป็น subclass ของ Sprite เพื่อรับผิดชอบกิจกรรมต่าง ๆ ดังนี้ (เขียนไว้ด้านบนของ space.py ก่อนคลาส SpaceGameWindow) | ||
+ | |||
+ | <syntaxhighlight lang="python"> | ||
+ | class ModelSprite(arcade.Sprite): | ||
+ | def __init__(self, *args, **kwargs): | ||
+ | self.model = kwargs.pop('model', None) | ||
+ | |||
+ | super().__init__(*args, **kwargs) | ||
+ | |||
+ | def sync_with_model(self): | ||
+ | if self.model: | ||
+ | self.set_position(self.model.x, self.model.y) | ||
+ | |||
+ | def draw(self): | ||
+ | self.sync_with_model() | ||
+ | super().draw() | ||
+ | </syntaxhighlight> | ||
− | == World, | + | '''หมายเหตุ''' เราใช้ความสามารถพิเศษในการส่งและรับ parameter ที่หลากหลายของ python คือการใช้ keyword arguments ในส่วนนี้อาจจะดูยุ่งบ้าง ตอนที่ผมเขียนก็ต้องไปกดหาตัวอย่างจากในเน็ต ดังนั้นถ้ามึนส่วน *args, กับ **kwargs ยังไม่ต้องตกใจตอนนี้ |
+ | |||
+ | <div style="padding: 3px; background-color: #e0e0ff; border: 1px solid gray; margin-bottom: 5px;"> | ||
+ | อ่านเพิ่มเติมเกี่ยวกับ *args และ **kwargs ได้ที่ [https://pythontips.com/2013/08/04/args-and-kwargs-in-python-explained/ บทความนี้] หรือ [http://nbviewer.jupyter.org/github/jittat/python-oop-notebooks/blob/master/Python%20parameter%20passing.ipynb?flush_cache=true jupyter notebook นี้] | ||
+ | </div> | ||
+ | |||
+ | สังเกตว่าเราแก้เมท็อด draw ใหม่ ให้ sync ก่อนที่จะวาดรูป แน่นอนการทำเช่นนี้ทำให้เราเขียนได้สะดวกขึ้น โดยแลกกับการทำงานที่อาจจะซ้ำซ้อนขึ้นบ้าง | ||
+ | |||
+ | จากนั้นแก้ส่วนสร้าง sprite เป็นดังนี้ (เขียนหลังบรรทัดสร้าง world) | ||
+ | |||
+ | <syntaxhighlight lang="python"> | ||
+ | self.ship_sprite = ModelSprite('images/ship.png',model=self.world.ship) | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | '''หมายเหตุ''' สังเกตว่าเราส่ง model ด้วย keyword arguments (model=self.world.ship) | ||
+ | |||
+ | จากนั้นลบบรรทัดที่เคยต้อง set_position ใน update ได้เลย | ||
+ | |||
+ | <syntaxhighlight lang="python"> | ||
+ | def update(self, delta): | ||
+ | self.world.update(delta) | ||
+ | # ----- self.ship_sprite.set_position(self.world.ship.x, self.world.ship.y) | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | {{gitcomment|ถ้าทดลองแล้วรันได้ตามปกติ อย่าลืม commit โค้ด}} | ||
+ | |||
+ | === รูปอธิบายโครงสร้างโปรแกรมและความรับผิดชอบส่วนต่าง ๆ จนถึงจุดนี้ === | ||
+ | |||
+ | [[Image:Arcade-space-modules6.png]] | ||
+ | |||
+ | === หมายเหตุเกี่ยวกับ WorldRenderer === | ||
+ | : ''หมายเหตุ: คลาส WorldRenderer เป็นคลาสที่จัดการวาด sprite ทั้งหมดในโค้ดที่เขียนบน Java สำหรับนิสิตที่เรียนปีการศึกษา 2560 ถ้าอ่านแล้วไม่เข้าใจไม่ต้องกังวล ให้อ่านข้ามไปได้เลย'' | ||
+ | |||
+ | ถ้าต้องการ เราสามารถสร้าง WorldRenderer เพื่อย้ายงานส่วนวาดภาพไปได้ แต่เราจะยังไม่ทำตอนนี้ | ||
+ | |||
+ | == Python coding convention == | ||
+ | แบบสรุปสั้น ๆ | ||
+ | |||
+ | * ชื่อคลาสเป็น CamelCase | ||
+ | * ชื่อตัวแปร เมท็อด และฟังก์ชัน เป็น snake_case | ||
+ | * ใช้ self เป็นพารามิเตอร์แรก แทน object ในการเขียนเมท็อด | ||
+ | |||
+ | อ่านแบบยาวที่ [https://www.python.org/dev/peps/pep-0008/ PEP 8 -- Style Guide for Python Code] | ||
+ | |||
+ | == บังคับทิศทาง == | ||
+ | === รับการกดปุ่ม === | ||
+ | การจัดการกับการกดปุ่มนั้น ระบบของ Arcade จะแจ้งกับ Window ของเราผ่านทางเมท็อดเมท็อดดังด้านล่าง | ||
+ | |||
+ | * on_key_press -- เรียกเมื่อปุ่มถูกกด | ||
+ | * on_key_release -- เรียกเมื่อปุ่มถูกปล่อย | ||
+ | |||
+ | ในทำนองเดียวกัน มีอีกสองเมท็อดที่จะถูกเรียกเมื่อมีการกดเมาส์และขยับเมาส์ | ||
+ | |||
+ | * on_mouse_motion | ||
+ | * on_mouse_press | ||
+ | * on_mouse_release | ||
+ | |||
+ | เราจะรับการกดปุ่ม space แล้วปรับทิศทางของยานอวกาศ เราจะเพิ่มเมท็อด on_key_press ในคลาส SpaceGameWindow ใน space.py โดยหลัก ๆ เราจะมอบหน้าที่ให้คลาส World จัดการ | ||
+ | |||
+ | <syntaxhighlight lang="python"> | ||
+ | def on_key_press(self, key, key_modifiers): | ||
+ | self.world.on_key_press(key, key_modifiers) | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | คลาส World ใน models.py จะรับการกดปุ่มและนำไปประมวลผล ก่อนอื่นเราต้อง import โมดูล arcade.key ไว้ก่อน เพื่อให้มีข้อมูลค่าคงที่แทนรหัสปุ่มต่าง ๆ | ||
+ | |||
+ | <syntaxhighlight lang="python"> | ||
+ | import arcade.key | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | ในคลาส World เราจะเพิ่มเมท็อด on_key_press ที่จะตรวจสอบว่ามีการกดปุ่ม space หรือไม่ ถ้ามีการกดให้สั่งให้ ship เปลี่ยนทิศทาง | ||
+ | |||
+ | <syntaxhighlight lang="python"> | ||
+ | class World: | ||
+ | # ... | ||
+ | |||
+ | def on_key_press(self, key, key_modifiers): | ||
+ | if key == arcade.key.SPACE: | ||
+ | self.ship.switch_direction() | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | จากนั้นแก้คลาส Ship ให้เก็บข้อมูลทิศทางและเพิ่มเมท็อด switch_direction | ||
+ | |||
+ | <syntaxhighlight lang="python"> | ||
+ | class Ship: | ||
+ | DIR_HORIZONTAL = 0 | ||
+ | DIR_VERTICAL = 1 | ||
+ | |||
+ | def __init__(self, world, x, y): | ||
+ | self.world = world | ||
+ | self.x = x | ||
+ | self.y = y | ||
+ | self.direction = Ship.DIR_VERTICAL | ||
+ | |||
+ | |||
+ | def switch_direction(self): | ||
+ | if self.direction == Ship.DIR_HORIZONTAL: | ||
+ | self.direction = Ship.DIR_VERTICAL | ||
+ | else: | ||
+ | self.direction = Ship.DIR_HORIZONTAL | ||
+ | |||
+ | |||
+ | def update(self, delta): | ||
+ | if self.direction == Ship.DIR_VERTICAL: | ||
+ | if self.y > self.world.height: | ||
+ | self.y = 0 | ||
+ | self.y += 5 | ||
+ | else: | ||
+ | if self.x > self.world.width: | ||
+ | self.x = 0 | ||
+ | self.x += 5 | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | ทดลองโปรแกรมว่ายานอวกาศเปลี่ยนทิศทางได้ อย่างไรก็ตาม sprite ของเราจะยังมีทิศทางในการหันเป็นอย่างเดิม เราจะแก้ต่อไป | ||
+ | |||
+ | === ปรับ sprite ให้หมุน === | ||
+ | คลาส Sprite มี attribute <tt>angle</tt> ที่ระบุมุมในการแสดงผลของรูป sprite เราจะเพิ่ม attribute นี้ใน ship และ sync มุมนี้มาที่ ModelSprite ด้วย ดังโค้ดด้านล่าง | ||
+ | |||
+ | แก้คลาส Ship ใน models.py | ||
+ | |||
+ | <syntaxhighlight lang="python"> | ||
+ | class Ship: | ||
+ | # ... | ||
+ | def __init__(self, world, x, y): | ||
+ | # ... | ||
+ | self.angle = 0 | ||
+ | |||
+ | def switch_direction(self): | ||
+ | if self.direction == Ship.DIR_HORIZONTAL: | ||
+ | self.direction = Ship.DIR_VERTICAL | ||
+ | self.angle = 0 | ||
+ | else: | ||
+ | self.direction = Ship.DIR_HORIZONTAL | ||
+ | self.angle = -90 | ||
+ | |||
+ | # ... | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | จากนั้นให้แก้เมท็อด sync_with_model ใน ModelSprite ใน space.py ให้อ่านค่า angle มากำหนดให้กับ sprite ด้วย | ||
+ | |||
+ | <syntaxhighlight lang="python"> | ||
+ | def sync_with_model(self): | ||
+ | if self.model: | ||
+ | self.set_position(self.model.x, self.model.y) | ||
+ | self.angle = self.model.angle | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | === เกี่ยวกับการ sync === | ||
+ | : ''to do'' | ||
+ | |||
+ | === รูปอธิบายโครงสร้างโปรแกรมและความรับผิดชอบส่วนต่าง ๆ จนถึงจุดนี้ === | ||
+ | |||
+ | [[Image:Arcade-space-modules7.png]] | ||
+ | |||
+ | == อัพโหลดโค้ดขึ้น github, bitbucket, หรือ gitlab == | ||
+ | |||
+ | ในส่วนนี้จะให้นิสิตอัพโค้ดที่เขียนมาแล้วขึ้นเว็บที่ให้บริการ git ออนไลน์ มีหลายบริการให้เลือกใช้ ข้อแตกต่างคือ github จะไม่มีบริการ private repository ฟรี ส่วน bitbucket และ gitlab มีบริการฟรีตามเงื่อนไขแตกต่างกันไป | ||
+ | |||
+ | ให้ไปสร้างผู้ใช้ในระบบดังกล่าว จากนั้นกดสร้าง repository (เลือกแบบ public) ระวังอย่ากดให้สร้าง README จะทำให้อัพยุ่งขึ้นมาก | ||
+ | |||
+ | เมื่อสร้างเสร็จ เว็บจะมีคำสั่งให้ใส่เพื่อให้เราสามารถ push ไปยังเว็บเหล่านั้นได้ ให้ดูในหัวข้อที่เขียนถึงกรณีที่มี repository อยู่แล้ว โดยคำสั่งจะมีลักษณะเช่น | ||
+ | |||
+ | git remote add origin XXXXXXXXXXXXXXXXXXXXX | ||
+ | git push -u origin master | ||
+ | |||
+ | คำสั่ง git remote add origin เป็นการสั่งให้เพิ่ม repository ในเว็บดังกล่าวเป็นต้นทาง (origin) ส่วนคำสั่ง git push -u origin master เป็นการสั่งให้ push แขนงหลัก (master) ไปยังเซิร์ฟเวอร์ | ||
+ | |||
+ | ตัวอย่างข้อความที่เว็บ github แสดงดังด้านล่าง | ||
+ | |||
+ | [[Image:Github-push.png]] | ||
+ | |||
+ | === การอัพโค้ดขึ้นเซิร์ฟเวอร์รอบถัด ๆ ไป === | ||
+ | |||
+ | หลังจากที่คุณ <tt>git push -u origin master</tt> ไปแล้ว การ push โค้ดขึ้น github/gitlab/bitbucket รอบถัด ๆ ไป สามารถสั่ง git push เฉย ๆ ได้เลย ดังด้านล่าง | ||
+ | |||
+ | git push | ||
== ยานอวกาศเก็บสมบัติ == | == ยานอวกาศเก็บสมบัติ == | ||
+ | เราจะเพิ่มก้อนหินสมบัติบนหน้าจอ ให้ยานอวกาศคอยเก็บ เมื่อเก็บแล้ว ก้อนหินสมบัติจะหายไปปรากฏที่ตำแหน่งใหม่ | ||
+ | |||
+ | === คลาส Gold === | ||
+ | เราจะเพิ่มคลาส Gold ใน models.py เพื่อแทนข้อมูลของแท่งสมบัติ คลาสดังกล่าวยังไม่มีข้อมูลอะไรมาก (มีแค่ x กับ y) | ||
+ | |||
+ | <syntaxhighlight lang="python"> | ||
+ | class Gold: | ||
+ | def __init__(self, world, x, y): | ||
+ | self.world = world | ||
+ | self.x = x | ||
+ | self.y = y | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | จากนั้นให้สร้าง gold ใน World ด้วย | ||
+ | |||
+ | <syntaxhighlight lang="python"> | ||
+ | class World: | ||
+ | def __init__(self, width, height): | ||
+ | # ... | ||
+ | self.gold = Gold(self, 400, 400) | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | === sprite ของ Gold === | ||
+ | |||
+ | เราจะแสดงรูปแท่งสมบัติบนจอด้วย ModelSprite สามารถวาดรูปเองก็ได้ เราจะสร้างรูปขนาด 40x40 ดังด้านล่าง | ||
+ | |||
+ | [[Image:Gold.png]] | ||
+ | |||
+ | สามารถโหลดรูปที่นี่ [http://theory.cpe.ku.ac.th/wiki/images/Gold.png gold.png] เก็บเป็นไฟล์ชื่อ images/gold.png '''หมายเหตุ ตอน save ให้ระวัง ดูชื่อไฟล์ให้เป็นชื่อตัวเล็กด้วย เช่น gold.png''' | ||
+ | |||
+ | จากนั้นเพิ่ม attribute <tt>gold_sprite</tt> ใน SpaceGameWindow (ใน space.py) สังเกตว่าในการวาดรูป เราจะวาด gold_sprite ก่อน ship_sprite เพื่อให้รูปยานอวกาศอยู่ด้านบนในกรณีที่ทับกัน | ||
+ | |||
+ | <syntaxhighlight lang="python"> | ||
+ | class SpaceGameWindow(arcade.Window): | ||
+ | def __init__(self, width, height): | ||
+ | # ... | ||
+ | self.gold_sprite = ModelSprite('images/gold.png',model=self.world.gold) | ||
+ | |||
+ | def on_draw(self): | ||
+ | arcade.start_render() | ||
+ | self.gold_sprite.draw() | ||
+ | self.ship_sprite.draw() | ||
+ | |||
+ | # ... | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | อย่างไรก็ตาม โค้ดดังกล่าวเมื่อรันจะเกิด run-time error เพราะอะไร? | ||
+ | |||
+ | === base class ของโมเดล === | ||
+ | ปัญหาที่เราพบคือคลาส Gold ไม่มี attribute <tt>angle</tt> เพื่อใช้งานกับ ModelSprite เราสามารถเพิ่มเข้าไปโดยตรงในคลาส Gold ก็ได้ แต่ถ้าเราต้องการแสดงของอย่างอื่นต่อไป ก็ต้องเพิ่มอีก หรือถ้าจะมี attribute อื่นที่ต้อง sync อีก เราก็ต้องไปไล่แก้ทุกโมเดล สำหรับปัญหานี้วิธีที่สะดวกสุดคือสร้าง parent class (หรือเรียกว่า base class) ขึ้นมาให้มี attribute พื้นฐานให้ครบ | ||
+ | |||
+ | เพิ่มคลาส Model เข้าไปตอนต้นของไฟล์ models.py (หลังคำสั่ง import) | ||
+ | |||
+ | <syntaxhighlight lang="python"> | ||
+ | class Model: | ||
+ | def __init__(self, world, x, y, angle): | ||
+ | self.world = world | ||
+ | self.x = x | ||
+ | self.y = y | ||
+ | self.angle = 0 | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | จากนั้นแก้ Ship ให้ inherit มาจาก Model | ||
+ | |||
+ | <syntaxhighlight lang="python"> | ||
+ | class Ship(Model): | ||
+ | # ... | ||
+ | |||
+ | def __init__(self, world, x, y): | ||
+ | super().__init__(world, x, y, 0) | ||
+ | |||
+ | self.direction = Ship.DIR_VERTICAL | ||
+ | |||
+ | # ... | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | รวมทั้งแก้คลาส Gold ด้วย | ||
+ | |||
+ | <syntaxhighlight lang="python"> | ||
+ | class Gold(Model): | ||
+ | def __init__(self, world, x, y): | ||
+ | super().__init__(world, x, y, 0) | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | ทดลองรันอีกทีว่ามีการแสดงรูป gold.png อย่างถูกต้อง | ||
+ | |||
+ | {{gitcomment|อย่าลืม commit เมื่อทดลองเสร็จแล้ว}} | ||
+ | |||
+ | === ชน gold === | ||
+ | เราจะให้แท่งทองเปลี่ยนที่หลังจากถูกชน | ||
+ | |||
+ | เราจะมาเพิ่มเมท็อด random_location ที่คลาส Gold เสียก่อน เราจะใช้ฟังก์ชัน [https://docs.python.org/3/library/random.html#random.randint randint] จากโมดูลมาตรฐาน random ดังนั้นต้อง import มาก่อน | ||
+ | |||
+ | <syntaxhighlight lang="python"> | ||
+ | from random import randint | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | สังเกตว่าเรา import ฟังก์ชันมาในชื่อดังกล่าวเลย (โดยการระบุว่าจะ import อะไร ไม่ได้ import ทั้งโมดูล) ด้วยฟังก์ชันดังกล่าวเราสามารถเขียนเมท็อด random_location ได้ดังด้านล่าง | ||
+ | |||
+ | <syntaxhighlight lang="python"> | ||
+ | class Gold(Model): | ||
+ | # ... | ||
+ | |||
+ | def random_location(self): | ||
+ | self.x = randint(0, self.world.width - 1) | ||
+ | self.y = randint(0, self.world.height - 1) | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | ส่วนถัดไปคือโค้ดสำหรับตรวจสอบการชน เนื่องจากเราน่าจะต้องใช้อีกบ่อย ๆ เราจะเขียนเมท็อด hit ไว้ที่ Model เลย เราจะตรวจสอบแบบประมาณอย่างง่าย คือถ้าจุดศูนย์กลางห่างกันไม่เกิน hit_size เราจะถือว่าชน | ||
+ | |||
+ | <syntaxhighlight lang="python"> | ||
+ | class Model: | ||
+ | # ... | ||
+ | def hit(self, other, hit_size): | ||
+ | return (abs(self.x - other.x) <= hit_size) and (abs(self.y - other.y) <= hit_size) | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | จากนั้นในคลาส World ก็ไปเพิ่มโค้ดจัดการตรวจสอบการชนและการสุ่มตำแหน่งใหม่ | ||
+ | |||
+ | <syntaxhighlight lang="python"> | ||
+ | class World: | ||
+ | # ... | ||
+ | def update(self, delta): | ||
+ | self.ship.update(delta) | ||
+ | |||
+ | if self.ship.hit(self.gold, 15): | ||
+ | self.gold.random_location() | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | เราใส่ hit_size = 15 เพราะขนาดของแท่งทองคือ 40x40 เราอยากให้ชนตรงกลางสักหน่อย | ||
+ | |||
+ | {{gitcomment|เกมเล่นได้ระดับหนึ่งแล้ว อย่าลืม commit}} | ||
+ | |||
+ | |||
+ | === รูปอธิบายโครงสร้างโปรแกรมและความรับผิดชอบส่วนต่าง ๆ จนถึงจุดนี้ === | ||
+ | |||
+ | : ''TODO'' | ||
== แสดงคะแนน == | == แสดงคะแนน == | ||
+ | เราจะปรับให้ world เก็บคะแนน (score) | ||
+ | |||
+ | <syntaxhighlight lang="python"> | ||
+ | class World: | ||
+ | def __init__(self, width, height): | ||
+ | # ... | ||
+ | self.score = 0 | ||
+ | |||
+ | def update(self, delta): | ||
+ | self.ship.update(delta) | ||
+ | |||
+ | if self.ship.hit(self.gold, 10): | ||
+ | self.gold.random_location() | ||
+ | self.score += 1 | ||
+ | |||
+ | # ... | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | จากนั้นใน SpaceGameWindow เราจะอ่านค่ามาแสดงใน on_draw | ||
+ | |||
+ | <syntaxhighlight lang="python"> | ||
+ | def on_draw(self): | ||
+ | arcade.start_render() | ||
+ | self.gold_sprite.draw() | ||
+ | self.ship_sprite.draw() | ||
+ | |||
+ | arcade.draw_text(str(self.world.score), | ||
+ | self.width - 30, self.height - 30, | ||
+ | arcade.color.WHITE, 20) | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | {{gitcomment|อย่าลืม commit}} | ||
== อุกกาบาต == | == อุกกาบาต == | ||
+ | |||
+ | : ดู commit diff ได้ที่ [https://gitlab.com/jittat/arcade-space/commit/4a44b08be828d0f3d6dbe79f29e29a8fb7e2b61e gitlab] | ||
+ | |||
+ | == อุกกาบาตที่เพิ่มลดจำนวนได้ และ SpriteList == |
รุ่นแก้ไขปัจจุบันเมื่อ 01:51, 2 ตุลาคม 2560
- หน้านี้เป็นส่วนหนึ่งของ oop lab
เนื้อหา
ติดตั้ง Arcade (ข้ามได้ถ้าติดตั้งแล้ว)
- ในส่วนนี้นิสิตน่าจะติดตั้ง Arcade เรียบร้อยตั้งแต่สัปดาห์ที่แล้วแล้ว สามารถข้ามไปส่วนถัดไปได้เลย
เราจะใช้ game library ชื่อ arcade ในการพัฒนาเกม
ติดตั้ง pip/pip3
pip เป็นโปรแกรมสำหรับติดตั้ง library บน Python ในระบบที่มี python3 พร้อม ๆ กับ python2 เราจะเรียก pip3 เพื่อให้ติดตั้ง library ลงในส่วนของ python3
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
virtualenv
เราติดตั้ง arcade ลงใน library ของระบบเลย ซึ่งทำแบบนี้บ่อย ๆ อาจจะทำให้ library เละและตีกันได้ Python มีระบบสำหรับติดตั้ง library แยกกัน เรียกว่า virtualenv
- ยังเขียนส่วนนี้ไม่เสร็จ: to do - how to install with virtualenv
เกมว่าง ๆ
ก่อนเริ่ม + git
สร้าง directory สำหรับเก็บเกม เราจะใช้ git อย่างสม่ำเสมอและตลอดเวลา ดังนั้น อย่าลืมเรียก
git init
ก่อนเขียน
ถ้าลืมไปแล้วว่าใช้ git บน command line ได้อย่างไร กรุณากลับไปดูคลิป แนะนำ git 1
template
ด้านล่างจะเป็น template เริ่มต้นของเกม เราจะสร้างคลาส SpaceGameWindow ที่ inherit มาจาก arcade.Window เขียนโปรแกรมหลักไว้ที่ space.py ดังด้านล่าง
import arcade
SCREEN_WIDTH = 600
SCREEN_HEIGHT = 600
class SpaceGameWindow(arcade.Window):
def __init__(self, width, height):
super().__init__(width, height)
arcade.set_background_color(arcade.color.BLACK)
def on_draw(self):
arcade.start_render()
if __name__ == '__main__':
window = SpaceGameWindow(SCREEN_WIDTH, SCREEN_HEIGHT)
arcade.run()
โครงดังกล่าวตัดบางส่วนมาจากตัวอย่างที่ [2]
คำอธิบายทั่วไป
- เราสั่ง import arcade ด้านบน เมื่อสั่งแล้ว ในโค้ดจะมี module arcade ให้ใช้ได้ เราจะใช้ function หรือ class อะไรจากโมดูลดังกล่าวได้ โดยต้องขึ้นต้นว่า arcade เช่น arcade.Window arcade.set_background_color เป็นต้น
- คลาส SpaceGameWindow เป็น subclass ของ arcade.Window โดยเราเขียนเมท็อดเพิ่มดังนี้
- __init__
- on_draw เมท็อดนี้จะถูกเรียกซ้ำ ๆ เราสามารถปรับอัตราการอัพเดทได้โดยเรียกเมท็อด set_update_rate ตอนนี้เมท็อดดังกล่าวยังไม่ได้ทำอะไรเท่าใด เรียกแค่ฟังก์ชันเตรียมพร้อมเท่านั้น (arcade.start_render)
- ส่วน if __name__ == '__main__' ด้านล่าง เป็นโค้ดมาตรฐานในการตรวจสอบว่าไฟล์ที่เราเขียนอยู่ ถูกเรียกเป็นโปรแกรมหลักในการทำงานหรือไม่
ทดลองรัน
ถ้าทดลองรันได้ อย่าลืม add space.py แล้วก็
รูปอธิบายโครงสร้างโปรแกรมและความรับผิดชอบส่วนต่าง ๆ จนถึงจุดนี้
แสดงรูปยานอวกาศ
รูปประกอบต่าง ๆ รวมถึงรูปของตัวละครในเกมที่มีพื้นที่ว่างเพื่อแสดงพื้นหลังโดยมากจะเรียกว่า sprite เกม 2 มิติโดยมากก็จะไม่ได้วาดรูปพิเศษอะไรแต่เน้นการใช้ sprite ในการแสดงเรื่องราวเป็นหลัก
สร้าง sprite
เราจะใช้โปรแกรมเช่น GIMP, Photoshop, Paint.NET เพื่อวาด sprite ขนาด 64 x 64 จุด วาดยานอวกาศที่มีทิศหันขึ้น อย่าลืมทำให้ background นั้นใส (transparent) ด้วย โปรแกรมมักจะแสดงดังด้านล่าง (ถ้า background ใส)
ถ้าไม่ได้ทำให้ background ใส เวลาแสดงจะเห็นเป็นกรอบขาว ๆ ดังรูปด้านล่าง
สร้างไดเรกทอรี images เพื่อเก็บ assets รูป จากนั้นจัดเก็บรูปดังกล่าวชื่อ ship.png
ถ้าขี้เกียจวาด ไปเอารูปได้ที่ ship.png.
Sprite
เราจะสร้างวัตถุจากคลาส arcade.Sprite แทนยานอวกาศดังกล่าว เราจะเพิ่ม attribute ship เพื่อเก็บ sprite นี้
def __init__(self, width, height):
super().__init__(width, height)
arcade.set_background_color(arcade.color.BLACK)
self.ship = arcade.Sprite('images/ship.png')
self.ship.set_position(100, 100)
สังเกตว่าเราเรียกเมท็อด set_position ในการระบุตำแหน่งของยานอวกาศบนหน้าจอ ตำแหน่งนี้จะเป็นตำแหน่งของจุดตรงกลางของ sprite ซึ่งสามารถอ่านได้จาก attribute center_x และ center_y
เราจะวาดรูปยานอวกาศในเมท็อด on_draw ดังนี้
def on_draw(self):
arcade.start_render()
self.ship.draw()
ทดลองรันโปรแกรม จะเห็นว่ามียานอวกาศนิ่ง ๆ ปรากฏบนหน้าจอ
รูปอธิบายโครงสร้างโปรแกรมและความรับผิดชอบส่วนต่าง ๆ จนถึงจุดนี้
เคลื่อนที่
เราจะปรับตำแหน่งของ self.ship เป็นระยะ ๆ โดยเขียนเมท็อด update ในคลาส SpaceGameWindow ซึ่งจะถูกเรียกเป็นระยะ ๆ โดยอัตโนมัติโดย framework เมท็อดดังกล่าวจะได้รับ parameter delta แทนระยะเวลาระหว่างการเรียกเมท็อด update ครั้งที่แล้ว กับครั้งนี้ มาด้วย ซึ่งเราจะใช้หรือไม่ก็ได้
โค้ดด้านล่างปรับตำแหน่งของ self.ship สังเกตว่าเราปรับตำแหน่งโดยคงค่า center_x ไว้ แต่เพิ่ม center_y ขึ้น 5
class SpaceGameWindow(arcade.Window):
# ...
def update(self, delta):
self.ship.set_position(self.ship.center_x, self.ship.center_y + 5)
ให้สังเกตการใช้ self.ship ในโค้ดด้านบนด้วย
เมื่อทดลองรัน เราจะเห็นว่ายานจะวิ่งทะลุไปด้านบนของหน้าจอ
เราจะปรับให้ยานวิ่งทะลุมาจากด้านล่างโดยตรวจสอบพิกัดในแกน y ของยานก่อน ดังโค้ดด้านล่าง
def update(self, delta):
ship = self.ship
if ship.center_y > SCREEN_HEIGHT:
ship.center_y = 0
ship.set_position(ship.center_x, ship.center_y + 5)
สังเกตว่าเราจะต้องเรียกใช้ self.ship บ่อย เราเลยเอามาเก็บไว้ในตัวแปร local ชื่อ ship เสียก่อนเลย
ทดลองรันทดสอบโปรแกรมว่าทำงานได้ถูกต้องหรือไม่
รูปอธิบายโครงสร้างโปรแกรมและความรับผิดชอบส่วนต่าง ๆ จนถึงจุดนี้
World, ModelSprite
โปรแกรมที่เราเขียนมามีปัญหาหลายอย่าง หลัก ๆ คือโค้ดทั้งหมดรวมอยู่ใน space.py ที่แย่ไปกว่านั้นก็คือสถานะของยานอวกาศผูกอยู่กับ sprite ทำให้ถ้าเราต้องการจะทดสอบโค้ดที่เกี่ยวข้องกับการทำงานของยานอวกาศ เราต้องไปยุ่งกับ sprite ด้วย อาจจะทำให้ทำ unit test ได้ลำบาก
ในส่วนนี้เราจะพยายามแยกโค้ดต่าง ๆ ออกมาจากโปรแกรมหลัก โดยพยายามอิงกับโครงสร้างของเกมจาก libGDX ที่เราเคยเขียนบน Java เราจะจัดการเป็นขั้น ๆ ดังนี้
คลาส Ship
เราจะแยกโค้ดเกี่ยวกับการจัดการตำแหน่ง (และอื่น ๆ ในอนาคต) ของยานอวกาศ มาไว้ในคลาส Ship เราจะรวมโค้ดส่วนนี้ไว้ในโมดูลชื่อว่า models
เขียน class Ship ใน models.py ดังนี้ สังเกตว่าเราแทบจะย้ายโค้ดมาจาก space.py เลย เรายังมีปัญหาในการนำค่า SCREEN_HEIGHT มาใช้ ตอนนี้ให้ใส่เป็นค่าคงที่ไว้ก่อน (เดี๋ยวเราจะไปแก้)
class Ship:
def __init__(self, x, y):
self.x = x
self.y = y
def update(self, delta):
if self.y > 600:
self.y = 0
self.y += 5
เราจะกลับไปปรับโค้ดใน space.py ให้ใช้คลาสนี้ ด้านล่างเป็นโค้ดหลังแก้ (ไม่ได้แสดงทั้งหมด) อย่าลืมอ่านคำอธิบายถัดไปด้วย
import arcade
from models import Ship
# ...
class SpaceGameWindow(arcade.Window):
def __init__(self, width, height):
super().__init__(width, height)
arcade.set_background_color(arcade.color.BLACK)
self.ship = Ship(100,100)
self.ship_sprite = arcade.Sprite('images/ship.png')
def on_draw(self):
arcade.start_render()
self.ship_sprite.draw()
def update(self, delta):
ship = self.ship
ship.update(delta)
self.ship_sprite.set_position(ship.x, ship.y)
ก่อนอื่นต้อง import มาก่อน เราสามารถเขียน import models แล้วอ้างคลาสเป็น models.Ship ก็ได้ แต่เราจะ import โดยตรงมาเฉพาะคลาสนี้เท่านั้นเพื่อให้สะดวกตอนเรียกใช้ เราจะเขียนดังนี้
from models import Ship
เราสร้างวัตถุ ship โดยสั่ง
self.ship = Ship(100,100)
เราเปลี่ยน attribute ship เดิม ให้เป็น ship_sprite
self.ship_sprite = arcade.Sprite('images/ship.png')
เนื่องจากเราแยกโมเดลออกจาก sprite ดังนั้นเมื่ออัพเดทโมเดลแล้ว ต้องไปอัพเดท sprite ตามด้วย (ดูในเมท็อด update) อันนี้จะเป็นความยุ่งยากที่เพิ่มขึ้น ที่เราแลกมาถ้าต้องการให้โค้ดส่วนโมเดล test ได้สะดวก (ถ้าต้องการ)
ถ้าทดลองเล่นแล้วโอเค อย่าลืม
- add models.py แล้ว commit ด้วย (อย่าลืม -a หรือ -am ด้วย ไม่เช่นนั้นที่แก้มาใน space.py จะไม่โดนเก็บไปด้วย)
รูปอธิบายโครงสร้างโปรแกรมและความรับผิดชอบส่วนต่าง ๆ จนถึงจุดนี้
คลาส World
เราจะแก้ปัญหาการต้องอ้างถึง 600 โดยนำค่ามาเก็บไว้ในคลาส World (ซึ่ง Ship จะรู้จัก) และจะย้ายการจัดการโมเดลทั้งหมดมาไว้ในคลาส World (โค้ดเราตอนนี้มีโมเดลเดียวคือ Ship)
เราจะเขียนคลาส World ไว้ใน models.py เช่นเดียวกับ Ship เพิ่มเข้าไปตอนหลัง Ship
class World:
def __init__(self, width, height):
self.width = width
self.height = height
self.ship = Ship(100, 100)
def update(self, delta):
self.ship.update(delta)
จากนั้นเราไปแก้ space.py ให้เรียกคลาส World แทนที่จะไปสร้าง Ship โดยตรง ตามขั้นตอนนี้
1. อย่าลืม import (เพิ่ม World เข้าไปในรายการ)
from models import World, Ship
2. ปรับ constructor ของ SpaceGameWindow
class SpaceGameWindow(arcade.Window):
def __init__(self, width, height):
super().__init__(width, height)
arcade.set_background_color(arcade.color.BLACK)
self.ship_sprite = arcade.Sprite('images/ship.png')
self.world = World(width, height) # แทนที่บรรทัด ship
3. แก้ update สังเกตว่าในการเชื่อมระหว่างโมเดลกับ sprite เราต้องไปอ้างเอา ship มาจาก world อีกที สังเกตว่าเราจะไม่เขียน getter หรือ setter เพราะว่าโดยมากจะไม่ค่อยจำเป็นใน Python (อ่านเพิ่มที่ [3])
def update(self, delta):
self.world.update(delta)
self.ship_sprite.set_position(self.world.ship.x, self.world.ship.y)
ทดลองรันว่าทำงานได้ถูกต้อง
เราจะแก้ให้คลาส Ship ไม่ต้องใช้ magic number 600 โดยให้อ่านจาก world
แก้คลาส Ship ดังนี้
class Ship:
def __init__(self, world, x, y):
self.world = world
self.x = x
self.y = y
def update(self, delta):
if self.y > self.world.height:
self.y = 0
self.y += 5
อย่าลืมแก้บรรทัดที่สร้าง ship ในเมท็อด __init__ ใน World ด้วย
self.ship = Ship(self, 100, 100)
ถ้าทดลองรันแล้วทำงานได้เหมือนเดิม อย่าลืม
รูปอธิบายโครงสร้างโปรแกรมและความรับผิดชอบส่วนต่าง ๆ จนถึงจุดนี้
คลาส ModelSprite
- หมายเหตุ: คลาส WorldRenderer เป็นคลาสที่จัดการวาด sprite ทั้งหมดในโค้ดที่เขียนบน Java สำหรับนิสิตที่เรียนปีการศึกษา 2560 ถ้าอ่านแล้วไม่เข้าใจไม่ต้องกังวล ให้อ่านข้ามไปได้เลย
ถ้าเราพยายามอิงโครงสร้างจาก libGDX เราจะพบว่าเราจะต้องสร้าง WorldRenderer อย่างไรก็ตามเนื่องจากเรายังไม่ค่อยมีของบนหน้าจอมาก เราจะยังไม่แยกส่วนดังกล่าวออกในตอนนี้ แต่จะปรับคลาส Sprite เพื่อให้การเชื่อมกับโมเดลของเราสะดวกขึ้น
เราจะสร้างคลาส ModelSprite ที่เป็น subclass ของ Sprite เพื่อรับผิดชอบกิจกรรมต่าง ๆ ดังนี้ (เขียนไว้ด้านบนของ space.py ก่อนคลาส SpaceGameWindow)
class ModelSprite(arcade.Sprite):
def __init__(self, *args, **kwargs):
self.model = kwargs.pop('model', None)
super().__init__(*args, **kwargs)
def sync_with_model(self):
if self.model:
self.set_position(self.model.x, self.model.y)
def draw(self):
self.sync_with_model()
super().draw()
หมายเหตุ เราใช้ความสามารถพิเศษในการส่งและรับ parameter ที่หลากหลายของ python คือการใช้ keyword arguments ในส่วนนี้อาจจะดูยุ่งบ้าง ตอนที่ผมเขียนก็ต้องไปกดหาตัวอย่างจากในเน็ต ดังนั้นถ้ามึนส่วน *args, กับ **kwargs ยังไม่ต้องตกใจตอนนี้
อ่านเพิ่มเติมเกี่ยวกับ *args และ **kwargs ได้ที่ บทความนี้ หรือ jupyter notebook นี้
สังเกตว่าเราแก้เมท็อด draw ใหม่ ให้ sync ก่อนที่จะวาดรูป แน่นอนการทำเช่นนี้ทำให้เราเขียนได้สะดวกขึ้น โดยแลกกับการทำงานที่อาจจะซ้ำซ้อนขึ้นบ้าง
จากนั้นแก้ส่วนสร้าง sprite เป็นดังนี้ (เขียนหลังบรรทัดสร้าง world)
self.ship_sprite = ModelSprite('images/ship.png',model=self.world.ship)
หมายเหตุ สังเกตว่าเราส่ง model ด้วย keyword arguments (model=self.world.ship)
จากนั้นลบบรรทัดที่เคยต้อง set_position ใน update ได้เลย
def update(self, delta):
self.world.update(delta)
# ----- self.ship_sprite.set_position(self.world.ship.x, self.world.ship.y)
รูปอธิบายโครงสร้างโปรแกรมและความรับผิดชอบส่วนต่าง ๆ จนถึงจุดนี้
หมายเหตุเกี่ยวกับ WorldRenderer
- หมายเหตุ: คลาส WorldRenderer เป็นคลาสที่จัดการวาด sprite ทั้งหมดในโค้ดที่เขียนบน Java สำหรับนิสิตที่เรียนปีการศึกษา 2560 ถ้าอ่านแล้วไม่เข้าใจไม่ต้องกังวล ให้อ่านข้ามไปได้เลย
ถ้าต้องการ เราสามารถสร้าง WorldRenderer เพื่อย้ายงานส่วนวาดภาพไปได้ แต่เราจะยังไม่ทำตอนนี้
Python coding convention
แบบสรุปสั้น ๆ
- ชื่อคลาสเป็น CamelCase
- ชื่อตัวแปร เมท็อด และฟังก์ชัน เป็น snake_case
- ใช้ self เป็นพารามิเตอร์แรก แทน object ในการเขียนเมท็อด
อ่านแบบยาวที่ PEP 8 -- Style Guide for Python Code
บังคับทิศทาง
รับการกดปุ่ม
การจัดการกับการกดปุ่มนั้น ระบบของ Arcade จะแจ้งกับ Window ของเราผ่านทางเมท็อดเมท็อดดังด้านล่าง
- on_key_press -- เรียกเมื่อปุ่มถูกกด
- on_key_release -- เรียกเมื่อปุ่มถูกปล่อย
ในทำนองเดียวกัน มีอีกสองเมท็อดที่จะถูกเรียกเมื่อมีการกดเมาส์และขยับเมาส์
- on_mouse_motion
- on_mouse_press
- on_mouse_release
เราจะรับการกดปุ่ม space แล้วปรับทิศทางของยานอวกาศ เราจะเพิ่มเมท็อด on_key_press ในคลาส SpaceGameWindow ใน space.py โดยหลัก ๆ เราจะมอบหน้าที่ให้คลาส World จัดการ
def on_key_press(self, key, key_modifiers):
self.world.on_key_press(key, key_modifiers)
คลาส World ใน models.py จะรับการกดปุ่มและนำไปประมวลผล ก่อนอื่นเราต้อง import โมดูล arcade.key ไว้ก่อน เพื่อให้มีข้อมูลค่าคงที่แทนรหัสปุ่มต่าง ๆ
import arcade.key
ในคลาส World เราจะเพิ่มเมท็อด on_key_press ที่จะตรวจสอบว่ามีการกดปุ่ม space หรือไม่ ถ้ามีการกดให้สั่งให้ ship เปลี่ยนทิศทาง
class World:
# ...
def on_key_press(self, key, key_modifiers):
if key == arcade.key.SPACE:
self.ship.switch_direction()
จากนั้นแก้คลาส Ship ให้เก็บข้อมูลทิศทางและเพิ่มเมท็อด switch_direction
class Ship:
DIR_HORIZONTAL = 0
DIR_VERTICAL = 1
def __init__(self, world, x, y):
self.world = world
self.x = x
self.y = y
self.direction = Ship.DIR_VERTICAL
def switch_direction(self):
if self.direction == Ship.DIR_HORIZONTAL:
self.direction = Ship.DIR_VERTICAL
else:
self.direction = Ship.DIR_HORIZONTAL
def update(self, delta):
if self.direction == Ship.DIR_VERTICAL:
if self.y > self.world.height:
self.y = 0
self.y += 5
else:
if self.x > self.world.width:
self.x = 0
self.x += 5
ทดลองโปรแกรมว่ายานอวกาศเปลี่ยนทิศทางได้ อย่างไรก็ตาม sprite ของเราจะยังมีทิศทางในการหันเป็นอย่างเดิม เราจะแก้ต่อไป
ปรับ sprite ให้หมุน
คลาส Sprite มี attribute angle ที่ระบุมุมในการแสดงผลของรูป sprite เราจะเพิ่ม attribute นี้ใน ship และ sync มุมนี้มาที่ ModelSprite ด้วย ดังโค้ดด้านล่าง
แก้คลาส Ship ใน models.py
class Ship:
# ...
def __init__(self, world, x, y):
# ...
self.angle = 0
def switch_direction(self):
if self.direction == Ship.DIR_HORIZONTAL:
self.direction = Ship.DIR_VERTICAL
self.angle = 0
else:
self.direction = Ship.DIR_HORIZONTAL
self.angle = -90
# ...
จากนั้นให้แก้เมท็อด sync_with_model ใน ModelSprite ใน space.py ให้อ่านค่า angle มากำหนดให้กับ sprite ด้วย
def sync_with_model(self):
if self.model:
self.set_position(self.model.x, self.model.y)
self.angle = self.model.angle
เกี่ยวกับการ sync
- to do
รูปอธิบายโครงสร้างโปรแกรมและความรับผิดชอบส่วนต่าง ๆ จนถึงจุดนี้
อัพโหลดโค้ดขึ้น github, bitbucket, หรือ gitlab
ในส่วนนี้จะให้นิสิตอัพโค้ดที่เขียนมาแล้วขึ้นเว็บที่ให้บริการ git ออนไลน์ มีหลายบริการให้เลือกใช้ ข้อแตกต่างคือ github จะไม่มีบริการ private repository ฟรี ส่วน bitbucket และ gitlab มีบริการฟรีตามเงื่อนไขแตกต่างกันไป
ให้ไปสร้างผู้ใช้ในระบบดังกล่าว จากนั้นกดสร้าง repository (เลือกแบบ public) ระวังอย่ากดให้สร้าง README จะทำให้อัพยุ่งขึ้นมาก
เมื่อสร้างเสร็จ เว็บจะมีคำสั่งให้ใส่เพื่อให้เราสามารถ push ไปยังเว็บเหล่านั้นได้ ให้ดูในหัวข้อที่เขียนถึงกรณีที่มี repository อยู่แล้ว โดยคำสั่งจะมีลักษณะเช่น
git remote add origin XXXXXXXXXXXXXXXXXXXXX git push -u origin master
คำสั่ง git remote add origin เป็นการสั่งให้เพิ่ม repository ในเว็บดังกล่าวเป็นต้นทาง (origin) ส่วนคำสั่ง git push -u origin master เป็นการสั่งให้ push แขนงหลัก (master) ไปยังเซิร์ฟเวอร์
ตัวอย่างข้อความที่เว็บ github แสดงดังด้านล่าง
การอัพโค้ดขึ้นเซิร์ฟเวอร์รอบถัด ๆ ไป
หลังจากที่คุณ git push -u origin master ไปแล้ว การ push โค้ดขึ้น github/gitlab/bitbucket รอบถัด ๆ ไป สามารถสั่ง git push เฉย ๆ ได้เลย ดังด้านล่าง
git push
ยานอวกาศเก็บสมบัติ
เราจะเพิ่มก้อนหินสมบัติบนหน้าจอ ให้ยานอวกาศคอยเก็บ เมื่อเก็บแล้ว ก้อนหินสมบัติจะหายไปปรากฏที่ตำแหน่งใหม่
คลาส Gold
เราจะเพิ่มคลาส Gold ใน models.py เพื่อแทนข้อมูลของแท่งสมบัติ คลาสดังกล่าวยังไม่มีข้อมูลอะไรมาก (มีแค่ x กับ y)
class Gold:
def __init__(self, world, x, y):
self.world = world
self.x = x
self.y = y
จากนั้นให้สร้าง gold ใน World ด้วย
class World:
def __init__(self, width, height):
# ...
self.gold = Gold(self, 400, 400)
sprite ของ Gold
เราจะแสดงรูปแท่งสมบัติบนจอด้วย ModelSprite สามารถวาดรูปเองก็ได้ เราจะสร้างรูปขนาด 40x40 ดังด้านล่าง
สามารถโหลดรูปที่นี่ gold.png เก็บเป็นไฟล์ชื่อ images/gold.png หมายเหตุ ตอน save ให้ระวัง ดูชื่อไฟล์ให้เป็นชื่อตัวเล็กด้วย เช่น gold.png
จากนั้นเพิ่ม attribute gold_sprite ใน SpaceGameWindow (ใน space.py) สังเกตว่าในการวาดรูป เราจะวาด gold_sprite ก่อน ship_sprite เพื่อให้รูปยานอวกาศอยู่ด้านบนในกรณีที่ทับกัน
class SpaceGameWindow(arcade.Window):
def __init__(self, width, height):
# ...
self.gold_sprite = ModelSprite('images/gold.png',model=self.world.gold)
def on_draw(self):
arcade.start_render()
self.gold_sprite.draw()
self.ship_sprite.draw()
# ...
อย่างไรก็ตาม โค้ดดังกล่าวเมื่อรันจะเกิด run-time error เพราะอะไร?
base class ของโมเดล
ปัญหาที่เราพบคือคลาส Gold ไม่มี attribute angle เพื่อใช้งานกับ ModelSprite เราสามารถเพิ่มเข้าไปโดยตรงในคลาส Gold ก็ได้ แต่ถ้าเราต้องการแสดงของอย่างอื่นต่อไป ก็ต้องเพิ่มอีก หรือถ้าจะมี attribute อื่นที่ต้อง sync อีก เราก็ต้องไปไล่แก้ทุกโมเดล สำหรับปัญหานี้วิธีที่สะดวกสุดคือสร้าง parent class (หรือเรียกว่า base class) ขึ้นมาให้มี attribute พื้นฐานให้ครบ
เพิ่มคลาส Model เข้าไปตอนต้นของไฟล์ models.py (หลังคำสั่ง import)
class Model:
def __init__(self, world, x, y, angle):
self.world = world
self.x = x
self.y = y
self.angle = 0
จากนั้นแก้ Ship ให้ inherit มาจาก Model
class Ship(Model):
# ...
def __init__(self, world, x, y):
super().__init__(world, x, y, 0)
self.direction = Ship.DIR_VERTICAL
# ...
รวมทั้งแก้คลาส Gold ด้วย
class Gold(Model):
def __init__(self, world, x, y):
super().__init__(world, x, y, 0)
ทดลองรันอีกทีว่ามีการแสดงรูป gold.png อย่างถูกต้อง
ชน gold
เราจะให้แท่งทองเปลี่ยนที่หลังจากถูกชน
เราจะมาเพิ่มเมท็อด random_location ที่คลาส Gold เสียก่อน เราจะใช้ฟังก์ชัน randint จากโมดูลมาตรฐาน random ดังนั้นต้อง import มาก่อน
from random import randint
สังเกตว่าเรา import ฟังก์ชันมาในชื่อดังกล่าวเลย (โดยการระบุว่าจะ import อะไร ไม่ได้ import ทั้งโมดูล) ด้วยฟังก์ชันดังกล่าวเราสามารถเขียนเมท็อด random_location ได้ดังด้านล่าง
class Gold(Model):
# ...
def random_location(self):
self.x = randint(0, self.world.width - 1)
self.y = randint(0, self.world.height - 1)
ส่วนถัดไปคือโค้ดสำหรับตรวจสอบการชน เนื่องจากเราน่าจะต้องใช้อีกบ่อย ๆ เราจะเขียนเมท็อด hit ไว้ที่ Model เลย เราจะตรวจสอบแบบประมาณอย่างง่าย คือถ้าจุดศูนย์กลางห่างกันไม่เกิน hit_size เราจะถือว่าชน
class Model:
# ...
def hit(self, other, hit_size):
return (abs(self.x - other.x) <= hit_size) and (abs(self.y - other.y) <= hit_size)
จากนั้นในคลาส World ก็ไปเพิ่มโค้ดจัดการตรวจสอบการชนและการสุ่มตำแหน่งใหม่
class World:
# ...
def update(self, delta):
self.ship.update(delta)
if self.ship.hit(self.gold, 15):
self.gold.random_location()
เราใส่ hit_size = 15 เพราะขนาดของแท่งทองคือ 40x40 เราอยากให้ชนตรงกลางสักหน่อย
รูปอธิบายโครงสร้างโปรแกรมและความรับผิดชอบส่วนต่าง ๆ จนถึงจุดนี้
- TODO
แสดงคะแนน
เราจะปรับให้ world เก็บคะแนน (score)
class World:
def __init__(self, width, height):
# ...
self.score = 0
def update(self, delta):
self.ship.update(delta)
if self.ship.hit(self.gold, 10):
self.gold.random_location()
self.score += 1
# ...
จากนั้นใน SpaceGameWindow เราจะอ่านค่ามาแสดงใน on_draw
def on_draw(self):
arcade.start_render()
self.gold_sprite.draw()
self.ship_sprite.draw()
arcade.draw_text(str(self.world.score),
self.width - 30, self.height - 30,
arcade.color.WHITE, 20)
อุกกาบาต
- ดู commit diff ได้ที่ gitlab