ผลต่างระหว่างรุ่นของ "Oop lab/arcade/snake"
Jittat (คุย | มีส่วนร่วม) |
Jittat (คุย | มีส่วนร่วม) |
||
แถว 103: | แถว 103: | ||
self.snake.update(delta) | self.snake.update(delta) | ||
</syntaxhighlight> | </syntaxhighlight> | ||
+ | |||
+ | เราจะใช้คลาส World ใน snake.py ดังนั้นต้องไป import ก่อน โดยเพิ่มบรรทัดต่อไปนี้ในตอนต้นไฟล์ snake | ||
+ | |||
+ | from models import World | ||
'''คลาส ModelSprite''' ใน <tt>snake.py</tt> ใส่ไว้ก่อน <tt>SnakeWindow</tt> | '''คลาส ModelSprite''' ใน <tt>snake.py</tt> ใส่ไว้ก่อน <tt>SnakeWindow</tt> |
รุ่นแก้ไขปัจจุบันเมื่อ 08:56, 15 กันยายน 2560
- หน้านี้เป็นส่วนหนึ่งของ oop lab
จุดวิ่ง
ในส่วนแรกเราจะทำงูขนาด 1 ช่องวิ่งไปมาก่อน
เริ่มด้วยเกมว่าง ๆ
ก่อนเริ่ม อย่าลืมสร้าง git repository ไว้ที่ที่จะทำด้วย โดยสั่ง
git init
เราจะเริ่มโดยสร้างคลาส SnakeWindow ว่าง ๆ ไว้ก่อน ทั้งหมดนี้เขียนในไฟล์ snake.py
import arcade
SCREEN_WIDTH = 600
SCREEN_HEIGHT = 600
class SnakeWindow(arcade.Window):
def __init__(self, width, height):
super().__init__(width, height)
arcade.set_background_color(arcade.color.BLACK)
def main():
window = SnakeWindow(SCREEN_WIDTH, SCREEN_HEIGHT)
arcade.set_window(window)
arcade.run()
if __name__ == '__main__':
main()
ทดลองรัน
ถ้าทดลองรันได้ อย่าลืม git add snake.py แล้วก็
sprite และ snake
เราจะใช้รูปด้านล่างขนาด 16 x 16 แทนตัวงู
ดาวน์โหลดที่ [1] แล้วเซฟในโพลเดอร์ images ในชื่อ block.png
จากนั้นแก้ SnakeWindow ดังนี้
สร้าง arcade.Sprite ใน __init__
def __init__(self, width, height):
# ... ละบรรทัดอื่นไว้
self.snake_sprite = arcade.Sprite('images/block.png')
self.snake_sprite.set_position(300,300)
และสร้างเมท็อด on_draw มาวาด sprite
def on_draw(self):
arcade.start_render()
self.snake_sprite.draw()
ทดลองรัน
ทดลองรัน ถ้าทำงานได้ อย่าลืมเพิ่มไฟล์รูป และ
World, Snake, และ ModelSprite
เราจะแยกโครงสร้างของคลาสในเกมแบบเดียวกับเกม space นั่นคือเราจะมี World เพื่อเก็บข้อมูลของเกมทั้งหมด คลาสงู (Snake) จะอยู่ใน world ส่วนที่แสดงผล ในขั้นแรกนี้เราจะใช้วิธีแบบเดิมคือจะสร้าง ModelSprite เพื่อแสดงงู แต่อีกหน่อยถ้างูยาวขึ้นได้เราจะใช้วิธีอื่น (แต่ ModelSprite ก็ยังจะมีประโยชน์อยู่ในการแสดงผลไม้)
ในส่วนนี้เราจะตัดและแก้จากโค้ด space เลย นิสิตควรจะพิจารณาโค้ดทั้งหมดและพยายามทำความเข้าใจว่าแต่ละส่วนทำงานประสานกันได้อย่างไรก่อนจะทำขั้นถัดไป
ไฟล์ models.py
class Snake:
def __init__(self, world, x, y):
self.world = world
self.x = x
self.y = y
def update(self, delta):
if self.x > self.world.width:
self.x = 0
self.x += 5
class World:
def __init__(self, width, height):
self.width = width
self.height = height
self.snake = Snake(self, width // 2, height // 2)
def update(self, delta):
self.snake.update(delta)
เราจะใช้คลาส World ใน snake.py ดังนั้นต้องไป import ก่อน โดยเพิ่มบรรทัดต่อไปนี้ในตอนต้นไฟล์ snake
from models import World
คลาส ModelSprite ใน snake.py ใส่ไว้ก่อน SnakeWindow
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()
คลาส SnakeWindow ใน snake.py ที่แก้ไขแล้ว
class SnakeWindow(arcade.Window):
def __init__(self, width, height):
super().__init__(width, height)
arcade.set_background_color(arcade.color.BLACK)
self.world = World(SCREEN_WIDTH, SCREEN_HEIGHT)
self.snake_sprite = ModelSprite('images/block.png',
model=self.world.snake)
self.snake_sprite.set_position(300,300)
def update(self, delta):
self.world.update(delta)
def on_draw(self):
arcade.start_render()
self.snake_sprite.draw()
ทดลองรัน
ถ้าพบว่างูวิ่งเร็วมาก แปลว่าทำงานได้ ให้
วิ่งเป็นจังหวะ
เราจะแก้ให้งูค่อย ๆ ขยับเป็นจังหวะ แนวคิดหลัก ๆ คือเราจะสะสมเวลา delta (ซึ่งเป็นเวลาระหว่างการเรียก update แต่ละครั้ง) ไว้จนเกินค่าหนึ่ง แล้วจึงค่อยขยับ เราจะเก็บค่าเวลาที่จะรอไว้ในตัวแปร MOVE_WAIT สังเกตว่าเราจะใช้เป็นตัวใหญ่เพื่อระบุว่าเป็นค่าคงที่ ถ้าอีกหน่อยเราต้องการให้เกมปรับความเร็วได้ เราค่อยแก้ไขส่วนนี้ต่อไป
เราจะประกาศไว้ในคลาส Snake
class Snake:
MOVE_WAIT = 0.2
ตัวแปรที่ประกาศลักษณะนี้จะเป็นตัวแปรที่ติดกับคลาส จะอ้างได้โดยเรียก Snake.MOVE_WAIT หรือจะเรียกผ่านวัตถุของคลาสก็ได้ แต่ถ้ามีการกำหนดค่าจากวัตถุของคลาส ค่านั้นจะไป "แทน" ค่าของคลาส
เราจะเก็บค่าเวลารอใน self.wait_time ซึ่งกำหนดค่าเริ่มต้นเป็น 0 ใน __init__
def __init__(self, world, x, y):
# ละตอนต้นไว้
self.wait_time = 0
เราจะตรวจสอบค่านี้ในเมท็อด update ดังด้านล่าง สังเกตว่าเราแก้ให้งูขยับทีละ 16 จุด (เท่ากับขนาดช่อง)
def update(self, delta):
self.wait_time += delta
if self.wait_time < Snake.MOVE_WAIT:
return
if self.x > self.world.width:
self.x = 0
self.x += 16
self.wait_time = 0
ทดลองรัน
ถ้าทำงานได้ อย่าลืม
บังคับทิศทาง
เราจะเพิ่มค่าคงที่สำหรับจัดการทิศทางไว้ตอนต้นของ models.py
DIR_UP = 1
DIR_RIGHT = 2
DIR_DOWN = 3
DIR_LEFT = 4
DIR_OFFSET = { DIR_UP: (0,1),
DIR_RIGHT: (1,0),
DIR_DOWN: (0,-1),
DIR_LEFT: (-1,0) }
ในคลาส Snake เราจะเพิ่ม attribute direction ไว้เก็บทิศทาง โดยให้เริ่มที่เคลื่อนที่ไปทางขวา
class Snake:
# ... ละส่วนอื่นไว้
def __init__(self, world, x, y):
# ... ละส่วนอื่นไว้
self.direction = DIR_RIGHT
เพื่อให้ไม่ต้องเขียนเลขพิเศษ 16 ในโค้ด เราจะประกาศค่าคงที่ขนาด block ของงูไว้ที่ตอนต้นคลาส Snake ด้วย
class Snake:
BLOCK_SIZE = 16
# ... ละส่วนอื่นไว้
จากนั้นใน update ให้แก้ให้ปรับพิกัด x และ y ตามทิศทาง
def update(self, delta):
self.wait_time += delta
if self.wait_time < Snake.MOVE_WAIT:
return
# เพิ่มโค้ดปรับค่า self.x และ self.y ที่นี่ ให้ใช้ DIR_OFFSET อย่าเขียนโดยใช้ if
self.x ...................................
self.y ...................................
self.wait_time = 0
ให้ทดลองรันว่างูขยับทางขวาหรือไม่ ถ้าได้ให้ลองเปลี่ยนทิศเริ่มต้นของงู (self.direction) ให้เป็นค่าต่าง ๆ เพื่อทดสอบว่าโค้ดที่เขียนถูกต้อง อย่าลืมว่าให้ขยับทีละ Snake.BLOCK_SIZE จุด
ทดลองรัน
ถ้าทำงานได้ อย่าลืม
จัดการกับการกดปุ่ม
ใน SnakeWindow เพิ่มเมทอด on_key_press ดังนี้
# ... ใน SnakeWindow
def on_key_press(self, key, key_modifiers):
self.world.on_key_press(key, key_modifiers)
จากนั้นใน models.py ให้เพิ่มบรรทัดด้านล่างตอนต้น
import arcade.key
เพื่อนำเข้าค่าคงที่ของปุ่มกด แล้วให้เขียนเมทอด on_key_press ใน World ให้ปรับทิศของงูตามปุ่มที่กด
class World:
# ... ละส่วนอื่นไว้
def on_key_press(self, key, key_modifiers):
# เพิ่มโค้ดตรวจสอบปุ่มและเปลี่ยนทิศทางของ self.snake
ในการเปลี่ยนทิศทาง ให้แก้ attr direction ใน self.snake โดยตรงไปก่อนเลย วิธีนี้อาจไม่ใช่วิธีที่ดีที่สุดในทุกกรณี แต่กรณีนี้ทำได้สะดวกและไม่น่ามีปัญหาอะไร
นอกจากนี้ในการเขียน ถ้าเป็นไปได้ ให้พยายามเขียนโดยไม่ใช้ if แต่ใช้ dict เหมือนตัวอย่างทิศทางข้างต้น แต่ถ้าจะ if ก็ไม่เป็นไร สังเกตว่าเมื่อกดเปลี่ยนทิศแล้ว งูจะวิ่งทิศทางดังกล่าวไปตลอด
ทดลองรัน
ถ้าทำงานได้ อย่าลืม
งูที่ยาวขึ้น
ตัวงู
เราจะเพิ่มข้อมูลใน snake ให้เก็บตำแหน่งของตัวงู ส่วน self.x และ self.y ยังเป็นพิกัดของหัวอยู่ ส่วนตัวของงูทั้งหมด (รวมหัวด้วย) จะอยู่ในรายการ self.body เราจะทดลองโดยให้ตัวของงูอยู่กับที่ก่อน เราจะทดลองงูที่ยาวสาม block
def __init__(self, world, x, y):
self.world = world
self.x = x
self.y = y
self.body = [(x,y),
(x-Snake.BLOCK_SIZE, y),
(x-2*Snake.BLOCK_SIZE, y)]
self.length = 3
# .... บรรทัดอื่นละไว้
ลักษณะการเก็บข้อมุลใน body และ x,y จะแสดงดังรูป
เราจะปรับค่าของ body ในส่วนถัด ๆ ไป
SnakeSprite
เราจะสร้างคลาสใหม่เพื่อจัดการวาดรูปงูที่มีหลาย block อย่างไรก็ตาม เราจะไม่ได้สร้างคลาสที่ inherit มาจาก Sprite โดยตรง แต่จะเป็นคลาสที่มี sprite รูปเหลี่ยม และใช้ sprite นั้นวาดรูปตัวงู
ให้เพิ่มคลาสด้านล่าง (คลาส ModelSprite ให้เก็บไว้ก่อน) และเขียนเมท็อด draw ให้เรียบร้อย
class SnakeSprite:
def __init__(self, snake):
self.snake = snake
self.block_sprite = arcade.Sprite('images/block.png')
def draw(self):
for x,y in self.snake.body:
# เพิ่มโค้ดที่วาด self.block_sprite ลงตำแหน่ง (x,y) หลักๆ คือให้ set_position ก่อน แล้วค่อย draw
_________________________________________
_________________________________________
แล้วให้แก้บรรทัดที่สร้าง snake_sprite ใน SnakeWindow ให้เป็นดังนี้
class SnakeWindow(arcade.Window):
# ....
def __init__(self, width, height):
# ... ละบรรทัดอื่นไว้
# แก้บรรทัดที่สร้าง snake_sprite ให้เป็นดังนี้
self.snake_sprite = SnakeSprite(self.world.snake)
ทดลองรัน
ทดลองรัน จะเห็นงูความยาวสามบนจอ (ที่ไม่ขยับ) และ
แก้โค้ด snake ให้ปรับลิสต์ self.body
หลังปรับตำแหน่งของหัวงูใน update จะต้องปรับค่าในลิสต์ self.body ด้วยเพื่อให้แสดงงูได้ถูกต้อง
def update(self, delta):
self.wait_time += delta
if self.wait_time < Snake.MOVE_WAIT:
return
# ส่วนนี้มาจากที่เติมเองด้านบน
self.x .....................................................
self.y .....................................................
self.wait_time = 0
# .... เพิ่มโค้ดปรับ self.body ตรงนี้ โดยเอา (self.x, self.y) ไปไว้หน้าลิสต์ แล้วก็ลบตัวสุดท้ายออก
___________________________________________
___________________________________________
___________________________________________
ทดลองรัน
ทดลองรัน จะเห็นงูความยาวสามบนจอที่บังคับไปมาได้ ถ้าโอเคแล้วให้
กินอาหาร
โมเดล Heart
เราจะเพิมคลาส Heart ใน models.py ที่จะเก็บตำแหน่งของหัวใจ
เราจะสุ่มตัวเลขด้วยฟังก์ชัน randint ในโมดูล random ดังนันที่หัวไฟล์ models.py ให้เพิ่มบรรทัด import ด้านล่างนี้ด้วย
from random import randint
คลาสดังกล่าวจะมีเมทอด random_position เพื่อสุ่มตำแหน่ง เนื่องจากเราต้องการสุ่มให้ตำแหน่งตรงกับ block ของงู เราต้องคูณค่าด้วย Snake.BLOCK_SIZE ด้วย เพิ่มโค้ดของคลาสนี้ก่อน World
class Heart:
def __init__(self, world):
self.world = world
self.x = 0
self.y = 0
def random_position(self):
centerx = self.world.width // 2
centery = self.world.height // 2
self.x = centerx + randint(-15,15) * Snake.BLOCK_SIZE
self.y = centerx + randint(-15,15) * Snake.BLOCK_SIZE
สร้าง self.heart ใน World ด้วย
class World:
def __init__(self, width, height):
# ... ละบรรทัดก่อนหน้า
self.heart = Heart(self)
self.heart.random_position()
เชื่อมกับ sprite
เราจะใช้รูปหัวใจด้านล่างนี้
สามารถโหลดได้ที่ [2] เซพรูปดังกล่าวเป็น images/heart.png
ในการแสดงผล ให้แก้ snake.py ให้สร้าง heart_sprite และวาดรูป sprite ดังกล่าวใน on_draw
class SnakeWindow(arcade.Window):
def __init__(self, width, height):
# ... ละไว้
# เพิ่มโค้ดสร้าง ModelSprite สำหรับ self.heart_sprite ที่นี่ อย่าลืมเชื่อมกับ self.world.heart
__________________________________________________________
__________________________________________________________
อย่าลืมสั่งให้ดวาดรูป sprite ดังกล่าวใน on_draw ด้วย
def on_draw(self):
# ... ละไว้
# เพิ่มโค้ดสั่งให้ self.heart_sprite วาด
__________________________________________________________
__________________________________________________________
ทดลองรัน
ทดลองรันหลาย ๆ รอบ ถ้าเห็นหัวใจย้ายที่ไปมาก็น่าจะใช้ได้
แล้วอย่าลืม...
กินหัวใจ
เราจะให้ snake ตรวจสอบว่ากินหัวใจได้หรือไม่ โดยเพิ่มเมทอด can_eat ในคลาส Snake
class Snake:
# ... ละไว้
def can_eat(self, heart):
# ตรวจสอบพิกัดหัวของงู และพิกัดของ heart คืนค่า True ถ้าตรงกัน
____________________________________________
____________________________________________
จากนั้นเราจะให้ World เป็นคนเรียกตรวจสอบและจัดการให้หัวใจกระโดด
class World:
# ... ละไว้
def update(self, delta):
self.snake.update(delta)
if self.snake.can_eat(self.heart):
self.heart.random_position()
ทดลอง
ถ้าใช้ได้ ให้
บั๊ก
ตอนนี้เนื่องจากเราสุ่มตำแหน่งของหัวโจโดยไม่คำนึงถึงตำแหน่งของงูและตัวของงู เราอาจจะสุ่มหัวใจไปโผล่ตรงกับตัวงูได้
แต่เราจะยังไม่แก้บั๊กนี้ตอนนี้ (ถ้านิสิตสนใจจะแก้ก็ได้ครับ ไม่เป็นไร)
งูยาวขึ้น
เราจะเพิ่มสถานะให้กับ snake ว่าเพิ่งจะได้กินอาหารไป
class Snake:
# .. ละไว้
def __init__(self, world, x, y):
# .. ละไว้
self.has_eaten = False
ในเมทอด update ของ World ที่ตรวจสอบการกิน เราจะปรับสถานะนี้ถ้า snake กินได้
class World:
# .. ละไว้
def update(self, delta):
self.snake.update(delta)
if self.snake.can_eat(self.heart):
self.heart.random_position()
self.snake.has_eaten = True # ... เพิ่มบรรทัดนี้
แก้ update ใน Snake ปรับค่า body ให้ถูกต้อง
งานหลักของเราจะอยู่ที่การปรับเมทอด update ให้ทำงานให้ถูกต้อง อ่านรายละเอียดเกี่ยวกับเมท็อดต่าง ๆ ของลิสต์ได้ที่นี่
class Snake:
# .. ละไว้
def update(self, delta):
# .. ละไว้
# ส่วนนี้มาจากที่เติมเองด้านบน
self.x .........................................................
self.y .........................................................
# ตรวจสอบ self.has_eaten และปรับรายการ self.body และ self.length ให้ถูกต้อง ถ้าไม่ได้มีการกิน ก็ทำงานแบบเดิม
# อย่าลืมจัดการเรื่องปรับสถานะ self.has_eaten หลังการกินด้วย
___________________________________________
___________________________________________
___________________________________________
___________________________________________
___________________________________________
___________________________________________
ทดลอง
ถ้าเล่นได้ อย่าลืม
การแก้ไขอื่น ๆ
เมทอด update ใน Snake ค่อนข้างยาว ควรพยายามแยกออกเป็นฟังก์ชันย่อย
แบบฝึกหัด
ตรวจการกินตัวเอง
ตอนนี้งูยังขยับไปกินตัวเองได้ อย่าลืมเพิ่มโค้ดที่ตรวจจับสถานการณ์ดังกล่าวด้วย
เล่นหลายคน
ด้วยโค้ดที่เรามี การแก้ให้เล่นได้สองคนไม่ใช่เรื่องยากเลย