ผลต่างระหว่างรุ่นของ "สร้างเกมเดาะตะกร้อด้วย VPython"

จาก Theory Wiki
ไปยังการนำทาง ไปยังการค้นหา
 
(ไม่แสดง 86 รุ่นระหว่างกลางโดยผู้ใช้คนเดียวกัน)
แถว 1: แถว 1:
 +
: ''วิกินี้เป็นส่วนหนึ่งของรายวิชา [[01204223]]''
 +
 
== การเตรียมตัว ==
 
== การเตรียมตัว ==
 
=== ไพทอนและไลบรารีที่เกี่ยวข้อง ===
 
=== ไพทอนและไลบรารีที่เกี่ยวข้อง ===
แถว 12: แถว 14:
  
 
== องค์ประกอบของเกม ==
 
== องค์ประกอบของเกม ==
 +
 +
[[Image:takro-basic.png|center|300px]]
 +
 +
ตัวเกมพื้นฐานประกอบไปด้วย
 +
* '''กำแพงและเพดาน (สีแดง)''' สร้างขึ้นจากคลาส <tt>[http://vpython.org/contents/docs/curve.html vis.curve]</tt> เพื่อใช้แสดงขอบเขตด้านซ้าย ขวา และบน ลูกตะกร้อจะกระดอนกลับเมื่อเคลื่อนที่เลยขอบเขตนี้
 +
* '''ลูกตะกร้อ (สีเหลือง)''' สร้างขึ้นจากคลาส <tt>Ball</tt> ซึ่งสืบสกุลมาจากคลาส <tt>[http://vpython.org/contents/docs/sphere.html vis.sphere]</tt> มีทิศทางการเคลื่อนที่ในสองมิติซึ่งคำนวณจากกฎการเคลื่อนที่
 +
* '''แป้นรับลูกที่ควบคุมด้วยผู้เล่น (สีน้ำเงิน)''' สร้างขึ้นจากคลาส <tt>Player</tt> ซึ่งสืบสกุลมาจากคลาส <tt>[http://vpython.org/contents/docs/cylinder.html vis.cylinder]</tt> มีการเคลื่อนที่ในทิศทางซ้ายและขวาตามความเข้มของแสงที่อ่านได้จากบอร์ดไมโครคอนโทรลเลอร์
  
 
== ระบบพิกัด ==
 
== ระบบพิกัด ==
 +
VPython ใช้ระบบพิกัดในปริภูมิ 3 มิติ อย่างไรก็ตามตำแหน่งต่าง ๆ ของผู้เล่น ลูกตะกร้อ และกำแพง จะวางตัวอยู่ในระนาบ z=0 โดยที่พิกัด (x,y) มีทิศทางและขอบเขตดังภาพ ในที่นี้ <tt>BORDER</tt> เป็นค่าคงที่ที่กำหนดขึ้นมาในโปรแกรมซึ่งสามารถปรับเปลี่ยนขอบเขตได้ตามต้องการ
 +
 +
[[Image:takro-coord.png|center|300px]]
 +
 +
== สร้างสนามและตั้งค่าคงที่เพื่อใช้ในเกม ==
 +
ใช้เท็กซ์เอดิเตอร์สร้างไฟล์ชื่อ <tt>takro.py</tt> ตามโค้ดด้านล่าง
 +
<syntaxhighlight lang="python" line>
 +
import vis
 +
 +
ANIM_RATE = 30
 +
BORDER = 10
 +
scene = vis.display(title='Takro',width=500,height=500)
 +
scene.center = (0,0)
 +
scene.range = BORDER*1.2
 +
 +
border = vis.curve(pos=[
 +
    (-BORDER,-BORDER),
 +
    (-BORDER,+BORDER),
 +
    (+BORDER,+BORDER),
 +
    (+BORDER,-BORDER)
 +
    ],radius=0.1,color=vis.color.red)
 +
 +
while True:
 +
    vis.rate(ANIM_RATE)
 +
</syntaxhighlight>
 +
* '''บรรทัดที่ 1''' อิมพอร์ตโมดูล <tt>vis</tt> ซึ่งเป็นโมดูลหลักของ VPython
 +
* '''บรรทัดที่ 3-4''' กำหนดค่าคงที่เพื่อใช้ในโปรแกรม โดย <tt>ANIM_RATE</tt> ใช้กำหนดอัตราการอัพเดตหน้าจอต่อวินาที และ <tt>BORDER</tt> ใช้กำหนดขอบเขตของสนามตามที่อธิบายไว้ในหัวข้อระบบพิกัดข้างต้น
 +
* '''บรรทัดที่ 5-7''' สร้างหน้าต่างขนาด 500x500 ที่มีหัวข้อ "Takro" ขึ้นมา หน้าต่างนี้ทำหน้าที่เสมือนช่องที่มองเข้าไปในปริภูมิสามมิติของ VPython โดยให้พิกัด (0,0) อยู่ที่จุดศูนย์กลางของหน้าต่าง และช่องหน้าต่างมีขอบเขตครอบคลุมพิกัด (-BORDER,-BORDER) - (+BORDER,+BORDER) ออกไป 20%
 +
* '''บรรทัดที่ 9-14''' สร้างกำแพงและเพดานเพื่อแสดงขอบเขตสนามด้วยเส้นสีแดง เริ่มต้นจากพิกัด (-BORDER,-BORDER) &rarr; (-BORDER,+BORDER) &rarr; (+BORDER,+BORDER) และจบลงที่พิกัด (+BORDER,-BORDER) เส้นแสดงกำแพงแต่ละท่อนมีรัศมี 0.1 หน่วย
 +
* '''บรรทัดที่ 16-17''' เป็นลูปอนันต์เพื่อให้โปรแกรมไม่จบการทำงาน ฟังก์ชัน <tt>vis.rate()</tt> ทำหน้าที่หน่วงเวลาเพื่อให้ลูปทำงานไม่เกินอัตราที่กำหนด (ในที่นี่คือ 30 รอบต่อวินาที ซึ่งทำให้การแสดงผลอยู่ที่ 30 เฟรมต่อวินาทีเช่นกัน) และยังทำหน้าที่ในการประมวลผลอินพุทจากเมาส์และคีย์บอร์ด ซึ่งการกดแป้น Ctrl ค้างไว้แล้วคลิ้กลากเมาส์ไปมาจะทำให้หน้าต่างหมุนเปลี่ยนมุมมอง ในขณะที่การกดแป้น Alt ค้างไว้แล้วลากเมาส์เป็นการซูมเข้าออก
 +
 +
รันสคริปต์ด้วยไพทอน (ที่มากับ Anaconda) ควรเห็นหน้าต่างขนาด 500x500 ปรากฏขึ้นพร้อมกำแพงสีแดงล้อมรอบสามทิศทาง
 +
python takro.py
 +
 +
[[Image:takro-1.png|center|300px]]
 +
 +
== สร้างลูกตะกร้อ ==
 +
 +
=== คลาส <tt>Ball</tt> ===
 +
นิยามคลาส <tt>Ball</tt> เพื่อใช้สร้างเป็นลูกตะกร้อ โดยให้สืบสกุลมาจากคลาสทรงกลม (<tt>[http://vpython.org/contents/docs_vp5/visual/sphere.html vis.sphere]</tt>) ของ VPython ดังโค้ดด้านล่าง
 +
 +
<syntaxhighlight lang="python" line highlight="9-24,31,35">
 +
import vis
 +
 +
ANIM_RATE = 30
 +
BORDER = 10
 +
scene = vis.display(title='Takro',width=500,height=500)
 +
scene.center = (0,0)
 +
scene.range = BORDER*1.2
 +
 +
################################################################
 +
class Ball(vis.sphere):
 +
 +
    SIZE = 0.5
 +
 +
    def __init__(self,color=vis.color.white,vel=(0,0),acc=(0,0)):
 +
        vis.sphere.__init__(self,pos=(0,0),radius=self.SIZE,color=color,
 +
                make_trail=True,retain=10)
 +
        self.vel = vis.vector(vel)
 +
        self.acc = vis.vector(acc)
 +
 +
    def move(self,dt):
 +
        self.vel += self.acc*dt
 +
        self.pos += self.vel*dt
 +
 +
################################################################
 +
border = vis.curve(pos=[
 +
    (-BORDER,-BORDER),
 +
    (-BORDER,+BORDER),
 +
    (+BORDER,+BORDER),
 +
    (+BORDER,-BORDER)
 +
    ],radius=0.1,color=vis.color.red)
 +
ball = Ball(color=vis.color.yellow,vel=(5,10),acc=(0,-5))
 +
 +
while True:
 +
    vis.rate(ANIM_RATE)
 +
    ball.move(1.0/ANIM_RATE)
 +
</syntaxhighlight>
 +
 +
* '''บรรทัดที่ 14-18''' นิยามคอนสตรัคเตอร์ของคลาส โดยให้ไปเรียกใช้คอนสตรัคเตอร์ของคลาส <tt>vis.sphere</tt> เพื่อสร้างทรงกลมตามสีและขนาดที่กำหนดไว้ที่ตำแหน่ง (0,0) อาร์กิวเมนต์ <tt>make_trail</tt> และ <tt>retain</tt> ถูกกำหนดเอาไว้เพื่อให้เห็นวิถีการเคลื่อนที่ในช่วง 10 เฟรมที่ผ่านมา คุณสมบัติ <tt>vel</tt> และ <tt>acc</tt> เก็บค่าเวกเตอร์ความเร็วและความเร่งตามลำดับ เพื่อนำไปใช้ในการคำนวณการเคลื่อนที่ของลูกตะกร้อต่อไป
 +
* '''บรรทัดที่ 20-22''' นิยามเมท็อด <tt>move</tt> เพื่ออัพเดตตำแหน่งและความเร็วของลูกตะกร้อในช่วงเวลา <tt>dt</tt> วินาทีที่ผ่านมา การคำนวณเป็นไปตามกฎการเคลื่อนที่ตามที่ได้อธิบายไว้ในวิกิ [[จำลองการเคลื่อนที่ด้วยคณิตศาสตร์แบบเวกเตอร์ใน VPython]]
 +
* '''บรรทัดที่ 31''' สร้างอ็อบเจ็กต์ขึ้นจากคลาส <tt>Ball</tt> โดยกำหนดให้มีสีเหลือง มีความเร็วต้นตามเวกเตอร์ (5,10) และความเร่ง (0,-5) ซึ่งหมายความว่าเมื่อเริ่มต้นลูกตะกร้อจะเคลื่อนที่ไปทางขวาด้วยความเร็ว 5 หน่วย/วินาที และไปด้านบนด้วยความเร็ว 10 หน่วยต่อวินาที และเสมือนว่าถูกแรงดึงดูดลงด้านล่างด้วยความเร่งในแนวดิ่ง 5 หน่วย/วินาที<sup>2</sup> อ็อบเจ็กต์นี้ถูกอ้างอิงด้วยตัวแปร <tt>ball</tt>
 +
* '''บรรทัดที่ 35''' สั่งให้ลูกตะกร้ออัพเดตตำแหน่งเมื่อผ่านไป 1 ลูป ซึ่งจากการใช้ฟังก์ชัน <tt>vis.rate()</tt> เราทราบได้ว่าเวลาจะผ่านไป 1/ANIM_RATE วินาที จึงต้องส่งค่าเวลานี้ไปให้กับเมท็อด <tt>Ball.move()</tt> เพื่อใช้ในสูตรคำนวณการเคลื่อนที่
 +
 +
ทดลองรันสคริปต์จะเห็นว่าลูกตะกร้อปรากฏขึ้นที่พิกัด (0,0) และมีการเคลื่อนที่ไปในทิศทางบนขวาตามความเร็วต้นและความเร่งที่กำหนดให้ ทดลองเปลี่ยนโค้ดในบรรทัดที่ 31 และสังเกตพฤติกรรมที่เปลี่ยนไปของลูกตะกร้อ อย่างไรก็ตามจะเห็นว่าลูกตะกร้อมีการเคลื่อนที่โดยไม่สนใจกำแพงที่สร้างเอาไว้
 +
 +
[[Image:takro-2.png|center|300px]]
 +
 +
=== ทำให้ตะกร้อกระดอนกำแพง ===
 +
เพิ่มโค้ดตรวจสอบขอบเขตของลูกตะกร้อตามแนวกำแพงและเพดาน เมื่อพบว่าลูกเคลื่อนที่เลยขอบให้ดึงลูกกลับเข้ามาติดกับขอบแล้วเปลี่ยนทิศทางความเร็วตามแนวที่ตกกระทบโดยคงอัตราเร็วไว้เท่าเดิม
 +
 +
<syntaxhighlight lang="python" line start="10" highlight="14-22">
 +
class Ball(vis.sphere):
 +
 +
    SIZE = 0.5
 +
 +
    def __init__(self,color=vis.color.white,vel=(0,0),acc=(0,0)):
 +
        vis.sphere.__init__(self,pos=(0,0),radius=self.SIZE,color=color,
 +
                make_trail=True,retain=10)
 +
        self.vel = vis.vector(vel)
 +
        self.acc = vis.vector(acc)
 +
 +
    def move(self,dt):
 +
        self.vel += self.acc*dt
 +
        self.pos += self.vel*dt
 +
        if self.pos.x+self.SIZE > BORDER:  # hit right wall
 +
            self.pos.x = BORDER-self.SIZE
 +
            self.vel.x = -abs(self.vel.x)
 +
        if self.pos.x-self.SIZE < -BORDER: # hit left wall
 +
            self.pos.x = -BORDER+self.SIZE
 +
            self.vel.x = +abs(self.vel.x)
 +
        if self.pos.y+self.SIZE > BORDER:  # hit ceiling
 +
            self.pos.y = BORDER-self.SIZE
 +
            self.vel.y = -abs(self.vel.y)
 +
</syntaxhighlight>
 +
 +
== สร้างผู้เล่น ==
 +
 +
=== คลาส <tt>Player</tt> ===
 +
นิยามคลาส Player เพื่อใช้สร้างเป็นแป้นรับลูกที่ควบคุมโดยผู้เล่น โดยให้สืบสกุลมาจากคลาสทรงกระบอก (<tt>[http://vpython.org/contents/docs_vp5/visual/cylinder.html vis.cylinder]</tt>) ของ VPython ดังโค้ดด้านล่าง
 +
 +
<syntaxhighlight lang="python" line start="33" highlight="1-12,21,25">
 +
################################################################
 +
class Player(vis.cylinder):
 +
 +
    def __init__(self,controller):
 +
        vis.cylinder.__init__(self,pos=(0,-BORDER),axis=(0,0.2),
 +
                radius=2,color=vis.color.blue)
 +
        self.controller = controller
 +
 +
    def move(self):
 +
        if self.controller == None:
 +
            self.pos.x = ball.pos.x
 +
 +
################################################################
 +
border = vis.curve(pos=[
 +
    (-BORDER,-BORDER),
 +
    (-BORDER,+BORDER),
 +
    (+BORDER,+BORDER),
 +
    (+BORDER,-BORDER)
 +
    ],radius=0.1,color=vis.color.red)
 +
ball = Ball(color=vis.color.yellow,vel=(5,10),acc=(0,-5))
 +
player = Player(None)
 +
 +
while True:
 +
    vis.rate(ANIM_RATE)
 +
    player.move()
 +
    ball.move(1.0/ANIM_RATE)
 +
</syntaxhighlight>
 +
 +
* '''บรรทัดที่ 36-39''' นิยามคอนสตรัคเตอร์ของคลาส โดยใช้คอนสตรัคเตอร์ของ <tt>vis.cylinder</tt> สร้างทรงรูปจานไว้ที่ขอบด้านล่างของหน้าจอ กำหนดให้จานมีสีน้ำเงิน มีความหนา 0.2 หน่วย และมีรัศมี 2 หน่วย นอกจากนั้นคอนสตรัคเตอร์ยังรับพารามิเตอร์ชื่อ controller ซึ่งคาดหวังอ็อบเจ็กต์ของคลาส <tt>PeriBoard</tt> ที่นำมาใช้อ่านค่าแสงจากบอร์ดไมโครคอนโทรลเลอร์ได้
 +
* '''บรรทัดที่ 41-43''' นิยามเมท็อด <tt>move()</tt> ซึ่งถ้าหากไม่ได้มีการกำหนดอ็อบเจ็กต์ <tt>controller</tt> มาให้ตั้งแต่แรกจะทำให้แป้นรับเคลื่อนที่ไปตามตำแหน่งเดียวกันกับลูกตะกร้อ เราจะมาเพิ่มเติมเมท็อดนี้ในภายหลัง
 +
* '''บรรทัดที่ 53''' สร้างอ็อบเจ็กต์ <tt>Player</tt> ขึ้นมาโดยกำหนดอาร์กิวเมนต์เป็น None ให้กับพารามิเตอร์ <tt>controller</tt> เนื่องจากโค้ดยังไม่ได้มีส่วนเชื่อมโยงกับบอร์ด
 +
* '''บรรทัดที่ 58''' เรียกใช้เมท็อด <tt>move()</tt> ของแป้นรับลูกเพื่อให้อัพเดตตำแหน่งของตนเอง
 +
 +
ทดสอบสคริปต์ หน้าจอควรปรากฏแป้นรับลูกรูปจานสีน้ำเงินที่ด้านล่างของจอ โดยมีการเคลื่อนที่ไปตามตำแหน่งของลูกตะกร้อดังภาพ
 +
 +
[[Image:takro-3.png|center|300px]]
 +
 +
อย่างไรก็ตาม เมื่อลูกตะกร้อตกกระทบกับแป้นรับจะทะลุผ่านลงไปโดยไม่เกิดอะไรขึ้น
 +
 +
=== ตรวจสอบการกระทบลูกตะกร้อ ===
 +
เพื่อความสะดวก เราจะเพิ่มเมท็อด <tt>hitPlayer()</tt> เข้าไปในคลาส <tt>Ball</tt> เพื่อตรวจสอบว่าลูกตะกร้อกำลังกระทบแป้นรับลูกอยู่หรือไม่ พร้อมทั้งปรับเปลี่ยนทิศทางความเร็วในแนวดิ่งของลูกตะกร้อหากพบว่ามีการกระทบ
 +
 +
<syntaxhighlight lang="python" line start="9" highlight="24-32">
 +
class Ball(vis.sphere):
 +
 +
    SIZE = 0.5
 +
 +
    def __init__(self,color=vis.color.white,vel=(0,0),acc=(0,0)):
 +
        vis.sphere.__init__(self,pos=(0,0),radius=self.SIZE,color=color,
 +
                make_trail=True,retain=10)
 +
        self.vel = vis.vector(vel)
 +
        self.acc = vis.vector(acc)
 +
 +
    def move(self,dt):
 +
        self.vel += self.acc*dt
 +
        self.pos += self.vel*dt
 +
        if self.pos.x+self.SIZE > BORDER:  # hit right wall
 +
            self.pos.x = BORDER-self.SIZE
 +
            self.vel.x = -abs(self.vel.x)
 +
        if self.pos.x-self.SIZE < -BORDER: # hit left wall
 +
            self.pos.x = -BORDER+self.SIZE
 +
            self.vel.x = +abs(self.vel.x)
 +
        if self.pos.y+self.SIZE > BORDER:  # hit top wall
 +
            self.pos.y = BORDER-self.SIZE
 +
            self.vel.y = -abs(self.vel.y)
 +
 +
    def hitPlayer(self,player):
 +
        if (abs(self.pos.x-player.pos.x) < player.radius) and \
 +
          (self.pos.y < player.pos.y+player.axis.y+self.SIZE) and \
 +
          (self.pos.y > player.pos.y-self.SIZE):
 +
            self.pos.y = player.pos.y+player.axis.y+self.SIZE
 +
            self.vel.y = abs(self.vel.y)
 +
            return True
 +
        else:
 +
            return False
 +
</syntaxhighlight>
 +
 +
จากนั้นจึงนำเมท็อดนี้มาใช้ในลูปหลักดังนี้
 +
<syntaxhighlight lang="python" line start="65" highlight="5">
 +
while True:
 +
    vis.rate(ANIM_RATE)
 +
    ball.move(1.0/ANIM_RATE)
 +
    player.move()
 +
    ball.hitPlayer(player)
 +
</syntaxhighlight>
 +
 +
ทดสอบสคริปต์จะเห็นว่าลูกตะกร้อจะกระดอนกลับขึ้นมาเมื่อกระทบกับแป้นรับลูก ทดลองคอมเม้นต์โค้ด <tt>player.move()</tt> ในบรรทัดที่ 68 เพื่อให้แน่ใจว่าตะกร้อจะไม่กระดอนขึ้นมาหากไม่กระทบกับแป้นรับ
 +
 +
[[Image:takro-4.png|center|300px]]
 +
 +
=== ควบคุมด้วยบอร์ดพ่วง ===
 +
เราจะปรับแก้ไขโค้ดเพื่อให้ผู้เล่นควบคุมแป้นรับลูกได้ด้วยบอร์ดพ่วง โดยเมื่อคว่ำบอร์ดพ่วงลง (แสงน้อย) จะทำให้แป้นรับลูกเคลื่อนที่มาทางซ้าย เมื่อหงายบอร์ดพ่วงขึ้น (แสงมาก) จะทำให้แป้นรับลูกเคลื่อนที่ไปทางขวา เริ่มต้นจากการเพิ่มโค้ดให้เชื่อมต่อกับบอร์ดไมโครคอนโทรลเลอร์ผ่านพอร์ท USB ดังนี้
 +
 +
<syntaxhighlight lang="python" line start="1" highlight="2-3,11-19">
 +
import vis
 +
from practicum import findDevices
 +
from peri import PeriBoard
 +
 +
ANIM_RATE = 30
 +
BORDER = 10
 +
scene = vis.display(title='Takro',width=500,height=500)
 +
scene.center = (0,0)
 +
scene.range = BORDER*1.2
 +
 +
devs = findDevices()
 +
if len(devs) == 0:
 +
    print '*** No MCU board found.'
 +
    board = None
 +
else:
 +
    board = PeriBoard(devs[0])
 +
    print '*** MCU board found'
 +
    print '*** Device manufacturer: %s' % board.getVendorName()
 +
    print '*** Device name: %s' % board.getDeviceName()
 +
</syntaxhighlight>
 +
 +
จากนั้นแก้ไขเมท็อด <tt>Player.move()</tt> ให้นำค่าแสงมาใช้กำหนดตำแหน่งของแป้นรับลูกในกรณีที่พบบอร์ดไมโครคอนโทรลเลอร์ พร้อมทั้งส่งอ็อบเจ็กต์ <tt>board</tt> ไปเป็นอาร์กิวเม้นต์ในระหว่างการสร้างอ็อบเจ็กต์ <tt>player</tt> ขึ้นมา เนื่องจากค่าแสงที่วัดได้อยู่ในช่วง 0 ถึง 1023 บรรทัดที่ 68 จึงนำเอาค่าแสงที่อ่านได้มาปรับให้อยู่ในช่วง -BORDER ถึง +BORDER
 +
<syntaxhighlight lang="python" line start="56" highlight="11-13,23">
 +
class Player(vis.cylinder):
 +
 +
    def __init__(self,controller):
 +
        vis.cylinder.__init__(self,pos=(0,-BORDER),axis=(0,0.2),
 +
                radius=2,color=vis.color.blue)
 +
        self.controller = controller
 +
 +
    def move(self):
 +
        if self.controller == None:
 +
            self.pos.x = ball.pos.x
 +
        else:
 +
            light = self.controller.getLight()
 +
            self.pos.x = light/1023.0*BORDER*2 - BORDER
 +
 +
################################################################
 +
border = vis.curve(pos=[
 +
    (-BORDER,-BORDER),
 +
    (-BORDER,+BORDER),
 +
    (+BORDER,+BORDER),
 +
    (+BORDER,-BORDER)
 +
    ],radius=0.1,color=vis.color.red)
 +
ball = Ball(color=vis.color.yellow,vel=(5,10),acc=(0,-5))
 +
player = Player(board)
 +
</syntaxhighlight>
 +
 +
=== คาลิเบรทตัววัดแสง ===
 +
แม้ว่าผู้เล่นจะสามารถควบคุมตำแหน่งแป้นรับได้แล้วก็ตาม การควบคุมยังทำได้ยากและไม่นิ่มนวลเนื่องจากช่วงแสงในบริเวณที่เล่นไม่ได้ให้ค่าเต็มช่วง 0 ถึง 1023 แต่แกว่งอยู่เพียงบริเวณช่วงเล็ก ๆ ช่วงหนึ่ง เพื่อให้เกมตอบสนองต่อการควบคุมของผู้เล่นได้อย่างถูกต้องในสภาพแสงที่แตกต่างกัน ควรมีการคาลิเบรท (calibrate) ค่าแสงที่วัดได้โดยขอให้ผู้เล่นคว่่ำและหงายบอร์ดก่อนเริ่มต้นเกมเพื่อบันทึกค่าเอาไว้ใช้ต่อไป เราจะเพิ่มโค้ดส่วนนี้ลงไปในจุดที่อ็อบเจ็กต์ <tt>board</tt> ถูกสร้างขึ้นมาแล้วดังนี้
 +
 +
<syntaxhighlight lang="python" line start="11" highlight="10-16">
 +
devs = findDevices()
 +
if len(devs) == 0:
 +
    print '*** No MCU board found.'
 +
    board = None
 +
else:
 +
    board = PeriBoard(devs[0])
 +
    print '*** MCU board found'
 +
    print '*** Device manufacturer: %s' % board.getVendorName()
 +
    print '*** Device name: %s' % board.getDeviceName()
 +
    print 'Calibrating board.'
 +
    raw_input('Face the board down then press ENTER...')
 +
    minLight = board.getLight()
 +
    raw_input('Face the board up then press ENTER...')
 +
    maxLight = board.getLight()
 +
    board.lightRange = (minLight,maxLight)
 +
    print 'Calibration done.  Light range is (%d,%d)' % board.lightRange
 +
</syntaxhighlight>
 +
 +
ค่าแสงต่ำสุดและสูงสุดจะถูกจัดเก็บไว้คู่กันในคุณสมบัติชื่อ <tt>lightRange</tt> ของอ็อบเจ็กต์ <tt>board</tt>
 +
 +
ช่วงค่าที่ได้มาสามารถนำไปปรับแก้ในเมท็อด <tt>Player.move()</tt> ซึ่งแต่เดิมมีการคำนวณตำแหน่งของแป้นจากแสงไว้ดังนี้
 +
 +
<syntaxhighlight lang="python">
 +
self.pos.x = light/1023.0*BORDER*2 - BORDER
 +
</syntaxhighlight>
 +
 +
เราจะแก้ไขบรรทัดนี้ใหม่เพื่อแม็ปค่าแสงตามช่วงที่คาลิเบรทได้ลงไปในช่วงที่เป็นไปได้ของตำแหน่งแป้นรับคือ -BORDER ถึง +BORDER และให้แน่ใจว่าค่าที่ได้มาไม่ตกอยู่นอกช่วงดังกล่าวเพื่อไม่ให้แป้นรับลูกวิ่งออกไปจากกรอบ
 +
 +
<syntaxhighlight lang="python">
 +
minl,maxl = self.controller.lightRange
 +
x = float(light-minl)/(maxl-minl)*BORDER*2-BORDER
 +
if x < -BORDER:
 +
    x = -BORDER
 +
if x > BORDER:
 +
    x = BORDER
 +
self.pos.x = x
 +
</syntaxhighlight>
 +
 +
แม้การคำนวณจะตรงไปตรงมาแต่ก็ดูค่อนข้างเทอะทะ อีกทั้งการคำนวณลักษณะนี้เป็นที่รู้จักกันดีในชื่อ [https://en.wikipedia.org/wiki/Linear_interpolation การประมาณค่าในช่วงแบบเชิงเส้น (linear interpolation)] ทำให้ไลบรารีทางคณิตศาสตร์ส่วนใหญ่เตรียมฟังก์ชันไว้ให้พร้อมใช้งานอยู่แล้ว เราจะนำฟังก์ชัน <tt>[http://docs.scipy.org/doc/numpy/reference/generated/numpy.interp.html interp()]</tt> ของโมดูล numpy มาใช้แทนที่โค้ดข้างบน เริ่มด้วยการอิมพอร์ทฟังก์ชัน <tt>interp()</tt> มาจากโมดูล numpy
 +
 +
<syntaxhighlight lang="python" line start="1" highlight="4">
 +
import vis
 +
from practicum import findDevices
 +
from peri import PeriBoard
 +
from numpy import interp
 +
</syntaxhighlight>
 +
 +
แล้วจึงนำฟังก์ชันมาใช้ในเมท็อด <tt>Player.move()</tt> ดังนี้
 +
 +
<syntaxhighlight lang="python" line start="64" highlight="13-14">
 +
class Player(vis.cylinder):
 +
 +
    def __init__(self,controller):
 +
        vis.cylinder.__init__(self,pos=(0,-BORDER),axis=(0,0.2),
 +
                radius=2,color=vis.color.blue)
 +
        self.controller = controller
 +
 +
    def move(self):
 +
        if self.controller == None:
 +
            self.pos.x = ball.pos.x
 +
        else:
 +
            light = self.controller.getLight()
 +
            x = interp(light,self.controller.lightRange,[-BORDER,BORDER])
 +
            self.pos.x = x
 +
</syntaxhighlight>
 +
 +
กราฟด้านล่างแสดงตัวอย่างความสัมพันธ์ระหว่างค่า <tt>light</tt> กับค่า <tt>x</tt> ที่คำนวณได้จากฟังก์ชัน <tt>interp()</tt> โดยสมมติว่า <tt>self.controller.lightRange</tt> คือ (200,600) และ BORDER=10
 +
 +
[[Image:takro-interp.png|center|500px]]
 +
 +
เห็นได้ว่าฟังก์ชัน <tt>interp()</tt> จะจำกัดค่าที่คืนมาให้ไม่ให้เกินออกไปจากช่วง (-BORDER,+BORDER) ทำให้แป้นรับลูกบอลไม่วิ่งเลยขอบสนามออกไป
 +
 +
{| style="margin: auto;"
 +
|- style="vertical-align: center;"
 +
| [[Image:takro-left.jpg|300px|center|thumb|ตำแหน่งแป้นรับลูกเมื่อแสงน้อย]]
 +
| [[Image:takro-right.png|300px|center|thumb|ตำแหน่งแป้นรับลูกเมื่อแสงมาก]]
 +
|-
 +
|}
 +
 +
=== ลดการส่ายของแป้นรับลูก ===
 +
ในบางสถานการณ์จะพบว่าแป้นรับลูกมีการสั่นไปมา อันเนื่องมาจากมือผู้เล่นสั่นเอง หรือแสงสว่างในบริเวณเป็นแสงไฟแบบฟลูออเรสเซนต์ซึ่งมีการกระพริบตลอดเวลาเป็นปกติ การกระพริบนี้อาจไม่สังเกตเห็นได้ด้วยตามนุษย์เราแต่ตรวจจับได้ด้วยตัววัดแสงบนบอร์ดพ่วง
 +
 +
เราสามารถเกลี่ยตำแหน่งแป้นรับลูกให้เรียบขึ้นได้ด้วยกลไก [http://en.wikipedia.org/wiki/Exponential_smoothing exponential smoothing] ซึ่งมีสูตรดังนี้
 +
 +
:<math>
 +
\begin{align}
 +
s_0& = x_0\\
 +
s_{t}& = \alpha x_t + (1-\alpha)s_{t-1}
 +
\end{align}
 +
</math>
 +
 +
โดยที่
 +
* α คือ smoothing factor มีค่าอยู่ระหว่าง 0 ถึง 1
 +
* ลำดับ <i>x<sub>0</sub></i>,<i>x<sub>1</sub></i>,<i>x<sub>2</sub></i>,... เป็นข้อมูลดิบ
 +
* ลำดับ <i>s<sub>0</sub></i>,<i>s<sub>1</sub></i>,<i>s<sub>2</sub></i>,... เป็นลำดับที่ผ่านการปรับให้เรียบแล้ว
 +
 +
สูตรนี้เป็นการนำเอาค่าก่อนหน้ามาคำนวณแบบถ่วงน้ำหนักร่วมกับค่าที่อ่านได้ในปัจจุบัน โดยหากกำหนดให้ α มีค่ามากทำให้ค่า <i>s<sub>t</sub></i> (ตำแหน่งปัจจุบันของแป้นรับลูก) ขึ้นอยู่กับค่าก่อนหน้าค่อนข้างมาก มีผลทำให้การเปลี่ยนแปลงค่าของ <i>s<sub>t</sub></i> เป็นไปอย่างช้า ๆ และราบเรียบ แต่ก็ทำให้การตอบสนองต่อค่า <i>x<sub>t</sub></i> ที่เข้ามาใหม่ (ค่าความเข้มแสง) ช้าลงเช่นกัน
 +
 +
โค้ดด้านล่างผ่านการปรับแก้ให้ประยุกต์ใช้กลไก exponential smoothing โดยกำหนดให้ α มีค่าเท่ากับ 0.1 นั่นคือให้ความสำคัญกับค่าใหม่ 10% และค่าเก่า 90%
 +
 +
<syntaxhighlight lang="python" line start="64" highlight="14">
 +
class Player(vis.cylinder):
 +
 +
    def __init__(self,controller):
 +
        vis.cylinder.__init__(self,pos=(0,-BORDER),axis=(0,0.2),
 +
                radius=2,color=vis.color.blue)
 +
        self.controller = controller
 +
 +
    def move(self):
 +
        if self.controller == None:
 +
            self.pos.x = ball.pos.x
 +
        else:
 +
            light = self.controller.getLight()
 +
            x = interp(light,self.controller.lightRange,[-BORDER,BORDER])
 +
            self.pos.x = 0.1*x + 0.9*self.pos.x
 +
</syntaxhighlight>
 +
 +
== รายละเอียดอื่น ๆ ==
 +
 +
=== แก้ไขเหตุขัดข้อง USB Error ===
 +
การหยิบจับวงจรไฟฟ้าด้วยมือเปล่านั้นเป็นสิ่งที่ควรหลีกเลี่ยงเพื่อลดความเสี่ยงต่อการถูกไฟฟ้าดูด แม้จะเป็นอุปกรณ์ที่ใช้แรงดันต่ำเช่นบอร์ดไมโครคอนโทรลเลอร์ก็ตาม นอกจากนั้นสัญญาณรบกวนจากร่างกายยังอาจส่งผลไปยังการทำงานของวงจรและการสื่อสารผ่านพอร์ท USB ได้ ปัญหาในการสื่อสารอาจทำให้สคริปต์ค้างและแสดงเหตุขัดข้อง (exception) ดังนี้
 +
 +
usb.core.USBError: [Errno None] error sending control message: Connection timed out
 +
 +
ซึ่งเกิดจากการที่โฮสท์ไม่ได้รับการตอบสนองจากไมโครคอนโทรลเลอร์ภายในเวลาที่กำหนด เราจะดักเหตุขัดข้องนี้เอาไว้เพื่อให้สคริปต์ยังทำงานต่อไปได้ เริ่มต้นด้วยการอิมพอร์ทเหตุขัดข้อง <tt>usb.core.USBError</tt> มาจากโมดูล usb
 +
 +
<syntaxhighlight lang="python" line start="1" highlight="5">
 +
import vis
 +
from practicum import findDevices
 +
from peri import PeriBoard
 +
from numpy import interp
 +
from usb.core import USBError
 +
</syntaxhighlight>
 +
 +
จากนั้นใช้โครงสร้าง <tt>try..except</tt> (ซึ่งทำงานคล้ายคลึงกับโครงสร้าง <tt>try..catch</tt> ในภาษาจาวา) ในจุดที่สคริปต์อ่านค่าแสงจากบอร์ดดังนี้
 +
 +
<syntaxhighlight lang="python" line start="65" highlight="12-17">
 +
class Player(vis.cylinder):
 +
 +
    def __init__(self,controller):
 +
        vis.cylinder.__init__(self,pos=(0,-BORDER),axis=(0,0.2),
 +
                radius=2,color=vis.color.blue)
 +
        self.controller = controller
 +
 +
    def move(self):
 +
        if self.controller == None:
 +
            self.pos.x = ball.pos.x
 +
        else:
 +
            try:
 +
                light = self.controller.getLight()
 +
                x = interp(light,self.controller.lightRange,[-BORDER,BORDER])
 +
                self.pos.x = 0.1*x + 0.9*self.pos.x
 +
            except USBError:
 +
                print "USB error detected"
 +
</syntaxhighlight>
 +
 +
=== ตรวจสอบว่าลูกออกนอกกรอบแล้วจบเกม ===
 +
ที่ผ่านมาเห็นได้ว่าเมื่อผู้เล่นรับลูกพลาดแล้วลูกจะหายไปจากจอภาพตลอดกาล หากซูมภาพออกมาจะเห็นลูกตะกร้อเคลื่อนที่ลงข้างล่างไปเรื่อย ๆ ไม่มีที่สิ้นสุด ในสถานการณ์นี้ผู้เล่นจะทำอะไรไม่ได้อีกเลยนอกจากกดปิดหน้าต่างโปรแกรม เราจึงควรตรวจสอบการออกของลูกและแสดงข้อความ GAME OVER ก่อนจบการทำงานด้วยการปิดหน้าต่าง
 +
 +
เพื่อความสะดวกเราจะเพิ่มเมท็อด <tt>isOut()</tt> ลงในคลาส <tt>Ball</tt> เพื่อใช้ตรวจสอบว่าลูกได้วิ่งเลยขอบล่างของสนามไปแล้วดังนี้
 +
 +
<syntaxhighlight lang="python">
 +
def isOut(self):
 +
    return self.pos.y-self.SIZE < -BORDER
 +
</syntaxhighlight>
 +
 +
จากนั้นจึงเรียกใช้เมท็อดนี้ในลูปหลัก หากพบว่าลูกออกจากสนามไปแล้วให้แสดงป้าย "GAME OVER!" ด้วย <tt>vis.label</tt> หยุดรอ 2 วินาที แล้วจบการทำงาน
 +
 +
<syntaxhighlight lang="python" line start="99" highlight="6-9">
 +
while True:
 +
    vis.rate(ANIM_RATE)
 +
    ball.move(1.0/ANIM_RATE)
 +
    player.move()
 +
    ball.hitPlayer(player)
 +
    if ball.isOut():
 +
        vis.label(height=30,text='GAME OVER!')
 +
        vis.sleep(2)
 +
        vis.exit()
 +
</syntaxhighlight>
 +
 +
[[Image:takro-over.png|center|300px]]
 +
 +
=== แสดงคะแนนให้ผู้เล่น ===
  
== คลาสลูกบอล ==
+
โค้ดที่แก้ไขด้านล่างแสดงป้ายคะแนนให้ผู้เล่นเห็น ป้ายคะแนนสร้างขึ้นจากคลาส <tt>vis.label</tt> ซึ่งอัพเดตข้อความได้โดยการเปลี่ยนคุณสมบัติ <tt>text</tt>
  
== คลาสสำหรับผู้เล่น ==
+
<syntaxhighlight lang="python" line start="87" highlight="9-11,17-19">
 +
border = vis.curve(pos=[
 +
    (-BORDER,-BORDER),
 +
    (-BORDER,+BORDER),
 +
    (+BORDER,+BORDER),
 +
    (+BORDER,-BORDER)
 +
    ],radius=0.1,color=vis.color.red)
 +
ball = Ball(color=vis.color.yellow,vel=(5,10),acc=(0,-5))
 +
player = Player(board)
 +
score = vis.label(pos=(BORDER,BORDER))
 +
score.value = 0
 +
score.text = "Score: %d" % score.value
 +
 +
while True:
 +
    vis.rate(ANIM_RATE)
 +
    ball.move(1.0/ANIM_RATE)
 +
    player.move()
 +
    if ball.hitPlayer(player):
 +
        score.value += 1
 +
        score.text = "Score: %d" % score.value
 +
    if ball.isOut():
 +
        vis.label(text="GAME OVER!",height=30)
 +
        vis.sleep(2)
 +
        vis.exit()
 +
</syntaxhighlight>
  
== จัดเตรียมหน้าจอหลัก ==
+
[[Image:takro-score.png|center|300px]]

รุ่นแก้ไขปัจจุบันเมื่อ 10:26, 19 ตุลาคม 2558

วิกินี้เป็นส่วนหนึ่งของรายวิชา 01204223

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

ไพทอนและไลบรารีที่เกี่ยวข้อง

วิกินี้ใช้ภาษาไพทอนและไลบรารีที่เกี่ยวข้องดังนี้

บอร์ดไมโครคอนโทรลเลอร์และโมดูลไดรเวอร์

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

องค์ประกอบของเกม

Takro-basic.png

ตัวเกมพื้นฐานประกอบไปด้วย

  • กำแพงและเพดาน (สีแดง) สร้างขึ้นจากคลาส vis.curve เพื่อใช้แสดงขอบเขตด้านซ้าย ขวา และบน ลูกตะกร้อจะกระดอนกลับเมื่อเคลื่อนที่เลยขอบเขตนี้
  • ลูกตะกร้อ (สีเหลือง) สร้างขึ้นจากคลาส Ball ซึ่งสืบสกุลมาจากคลาส vis.sphere มีทิศทางการเคลื่อนที่ในสองมิติซึ่งคำนวณจากกฎการเคลื่อนที่
  • แป้นรับลูกที่ควบคุมด้วยผู้เล่น (สีน้ำเงิน) สร้างขึ้นจากคลาส Player ซึ่งสืบสกุลมาจากคลาส vis.cylinder มีการเคลื่อนที่ในทิศทางซ้ายและขวาตามความเข้มของแสงที่อ่านได้จากบอร์ดไมโครคอนโทรลเลอร์

ระบบพิกัด

VPython ใช้ระบบพิกัดในปริภูมิ 3 มิติ อย่างไรก็ตามตำแหน่งต่าง ๆ ของผู้เล่น ลูกตะกร้อ และกำแพง จะวางตัวอยู่ในระนาบ z=0 โดยที่พิกัด (x,y) มีทิศทางและขอบเขตดังภาพ ในที่นี้ BORDER เป็นค่าคงที่ที่กำหนดขึ้นมาในโปรแกรมซึ่งสามารถปรับเปลี่ยนขอบเขตได้ตามต้องการ

Takro-coord.png

สร้างสนามและตั้งค่าคงที่เพื่อใช้ในเกม

ใช้เท็กซ์เอดิเตอร์สร้างไฟล์ชื่อ takro.py ตามโค้ดด้านล่าง

 1 import vis
 2 
 3 ANIM_RATE = 30
 4 BORDER = 10
 5 scene = vis.display(title='Takro',width=500,height=500)
 6 scene.center = (0,0)
 7 scene.range = BORDER*1.2
 8 
 9 border = vis.curve(pos=[
10     (-BORDER,-BORDER),
11     (-BORDER,+BORDER),
12     (+BORDER,+BORDER),
13     (+BORDER,-BORDER)
14     ],radius=0.1,color=vis.color.red)
15 
16 while True:
17     vis.rate(ANIM_RATE)
  • บรรทัดที่ 1 อิมพอร์ตโมดูล vis ซึ่งเป็นโมดูลหลักของ VPython
  • บรรทัดที่ 3-4 กำหนดค่าคงที่เพื่อใช้ในโปรแกรม โดย ANIM_RATE ใช้กำหนดอัตราการอัพเดตหน้าจอต่อวินาที และ BORDER ใช้กำหนดขอบเขตของสนามตามที่อธิบายไว้ในหัวข้อระบบพิกัดข้างต้น
  • บรรทัดที่ 5-7 สร้างหน้าต่างขนาด 500x500 ที่มีหัวข้อ "Takro" ขึ้นมา หน้าต่างนี้ทำหน้าที่เสมือนช่องที่มองเข้าไปในปริภูมิสามมิติของ VPython โดยให้พิกัด (0,0) อยู่ที่จุดศูนย์กลางของหน้าต่าง และช่องหน้าต่างมีขอบเขตครอบคลุมพิกัด (-BORDER,-BORDER) - (+BORDER,+BORDER) ออกไป 20%
  • บรรทัดที่ 9-14 สร้างกำแพงและเพดานเพื่อแสดงขอบเขตสนามด้วยเส้นสีแดง เริ่มต้นจากพิกัด (-BORDER,-BORDER) → (-BORDER,+BORDER) → (+BORDER,+BORDER) และจบลงที่พิกัด (+BORDER,-BORDER) เส้นแสดงกำแพงแต่ละท่อนมีรัศมี 0.1 หน่วย
  • บรรทัดที่ 16-17 เป็นลูปอนันต์เพื่อให้โปรแกรมไม่จบการทำงาน ฟังก์ชัน vis.rate() ทำหน้าที่หน่วงเวลาเพื่อให้ลูปทำงานไม่เกินอัตราที่กำหนด (ในที่นี่คือ 30 รอบต่อวินาที ซึ่งทำให้การแสดงผลอยู่ที่ 30 เฟรมต่อวินาทีเช่นกัน) และยังทำหน้าที่ในการประมวลผลอินพุทจากเมาส์และคีย์บอร์ด ซึ่งการกดแป้น Ctrl ค้างไว้แล้วคลิ้กลากเมาส์ไปมาจะทำให้หน้าต่างหมุนเปลี่ยนมุมมอง ในขณะที่การกดแป้น Alt ค้างไว้แล้วลากเมาส์เป็นการซูมเข้าออก

รันสคริปต์ด้วยไพทอน (ที่มากับ Anaconda) ควรเห็นหน้าต่างขนาด 500x500 ปรากฏขึ้นพร้อมกำแพงสีแดงล้อมรอบสามทิศทาง

python takro.py
Takro-1.png

สร้างลูกตะกร้อ

คลาส Ball

นิยามคลาส Ball เพื่อใช้สร้างเป็นลูกตะกร้อ โดยให้สืบสกุลมาจากคลาสทรงกลม (vis.sphere) ของ VPython ดังโค้ดด้านล่าง

 1 import vis
 2  
 3 ANIM_RATE = 30
 4 BORDER = 10
 5 scene = vis.display(title='Takro',width=500,height=500)
 6 scene.center = (0,0)
 7 scene.range = BORDER*1.2
 8 
 9 ################################################################
10 class Ball(vis.sphere):
11 
12     SIZE = 0.5
13 
14     def __init__(self,color=vis.color.white,vel=(0,0),acc=(0,0)):
15         vis.sphere.__init__(self,pos=(0,0),radius=self.SIZE,color=color,
16                 make_trail=True,retain=10)
17         self.vel = vis.vector(vel)
18         self.acc = vis.vector(acc)
19 
20     def move(self,dt):
21         self.vel += self.acc*dt
22         self.pos += self.vel*dt
23 
24 ################################################################
25 border = vis.curve(pos=[
26     (-BORDER,-BORDER),
27     (-BORDER,+BORDER),
28     (+BORDER,+BORDER),
29     (+BORDER,-BORDER)
30     ],radius=0.1,color=vis.color.red)
31 ball = Ball(color=vis.color.yellow,vel=(5,10),acc=(0,-5))
32 
33 while True:
34     vis.rate(ANIM_RATE)
35     ball.move(1.0/ANIM_RATE)
  • บรรทัดที่ 14-18 นิยามคอนสตรัคเตอร์ของคลาส โดยให้ไปเรียกใช้คอนสตรัคเตอร์ของคลาส vis.sphere เพื่อสร้างทรงกลมตามสีและขนาดที่กำหนดไว้ที่ตำแหน่ง (0,0) อาร์กิวเมนต์ make_trail และ retain ถูกกำหนดเอาไว้เพื่อให้เห็นวิถีการเคลื่อนที่ในช่วง 10 เฟรมที่ผ่านมา คุณสมบัติ vel และ acc เก็บค่าเวกเตอร์ความเร็วและความเร่งตามลำดับ เพื่อนำไปใช้ในการคำนวณการเคลื่อนที่ของลูกตะกร้อต่อไป
  • บรรทัดที่ 20-22 นิยามเมท็อด move เพื่ออัพเดตตำแหน่งและความเร็วของลูกตะกร้อในช่วงเวลา dt วินาทีที่ผ่านมา การคำนวณเป็นไปตามกฎการเคลื่อนที่ตามที่ได้อธิบายไว้ในวิกิ จำลองการเคลื่อนที่ด้วยคณิตศาสตร์แบบเวกเตอร์ใน VPython
  • บรรทัดที่ 31 สร้างอ็อบเจ็กต์ขึ้นจากคลาส Ball โดยกำหนดให้มีสีเหลือง มีความเร็วต้นตามเวกเตอร์ (5,10) และความเร่ง (0,-5) ซึ่งหมายความว่าเมื่อเริ่มต้นลูกตะกร้อจะเคลื่อนที่ไปทางขวาด้วยความเร็ว 5 หน่วย/วินาที และไปด้านบนด้วยความเร็ว 10 หน่วยต่อวินาที และเสมือนว่าถูกแรงดึงดูดลงด้านล่างด้วยความเร่งในแนวดิ่ง 5 หน่วย/วินาที2 อ็อบเจ็กต์นี้ถูกอ้างอิงด้วยตัวแปร ball
  • บรรทัดที่ 35 สั่งให้ลูกตะกร้ออัพเดตตำแหน่งเมื่อผ่านไป 1 ลูป ซึ่งจากการใช้ฟังก์ชัน vis.rate() เราทราบได้ว่าเวลาจะผ่านไป 1/ANIM_RATE วินาที จึงต้องส่งค่าเวลานี้ไปให้กับเมท็อด Ball.move() เพื่อใช้ในสูตรคำนวณการเคลื่อนที่

ทดลองรันสคริปต์จะเห็นว่าลูกตะกร้อปรากฏขึ้นที่พิกัด (0,0) และมีการเคลื่อนที่ไปในทิศทางบนขวาตามความเร็วต้นและความเร่งที่กำหนดให้ ทดลองเปลี่ยนโค้ดในบรรทัดที่ 31 และสังเกตพฤติกรรมที่เปลี่ยนไปของลูกตะกร้อ อย่างไรก็ตามจะเห็นว่าลูกตะกร้อมีการเคลื่อนที่โดยไม่สนใจกำแพงที่สร้างเอาไว้

Takro-2.png

ทำให้ตะกร้อกระดอนกำแพง

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

10 class Ball(vis.sphere):
11 
12     SIZE = 0.5
13 
14     def __init__(self,color=vis.color.white,vel=(0,0),acc=(0,0)):
15         vis.sphere.__init__(self,pos=(0,0),radius=self.SIZE,color=color,
16                 make_trail=True,retain=10)
17         self.vel = vis.vector(vel)
18         self.acc = vis.vector(acc)
19 
20     def move(self,dt):
21         self.vel += self.acc*dt
22         self.pos += self.vel*dt
23         if self.pos.x+self.SIZE > BORDER:  # hit right wall
24             self.pos.x = BORDER-self.SIZE
25             self.vel.x = -abs(self.vel.x)
26         if self.pos.x-self.SIZE < -BORDER: # hit left wall
27             self.pos.x = -BORDER+self.SIZE
28             self.vel.x = +abs(self.vel.x)
29         if self.pos.y+self.SIZE > BORDER:  # hit ceiling
30             self.pos.y = BORDER-self.SIZE
31             self.vel.y = -abs(self.vel.y)

สร้างผู้เล่น

คลาส Player

นิยามคลาส Player เพื่อใช้สร้างเป็นแป้นรับลูกที่ควบคุมโดยผู้เล่น โดยให้สืบสกุลมาจากคลาสทรงกระบอก (vis.cylinder) ของ VPython ดังโค้ดด้านล่าง

33 ################################################################
34 class Player(vis.cylinder):
35 
36     def __init__(self,controller):
37         vis.cylinder.__init__(self,pos=(0,-BORDER),axis=(0,0.2),
38                 radius=2,color=vis.color.blue)
39         self.controller = controller
40 
41     def move(self):
42         if self.controller == None:
43             self.pos.x = ball.pos.x
44 
45 ################################################################
46 border = vis.curve(pos=[
47     (-BORDER,-BORDER),
48     (-BORDER,+BORDER),
49     (+BORDER,+BORDER),
50     (+BORDER,-BORDER)
51     ],radius=0.1,color=vis.color.red)
52 ball = Ball(color=vis.color.yellow,vel=(5,10),acc=(0,-5))
53 player = Player(None)
54  
55 while True:
56     vis.rate(ANIM_RATE)
57     player.move()
58     ball.move(1.0/ANIM_RATE)
  • บรรทัดที่ 36-39 นิยามคอนสตรัคเตอร์ของคลาส โดยใช้คอนสตรัคเตอร์ของ vis.cylinder สร้างทรงรูปจานไว้ที่ขอบด้านล่างของหน้าจอ กำหนดให้จานมีสีน้ำเงิน มีความหนา 0.2 หน่วย และมีรัศมี 2 หน่วย นอกจากนั้นคอนสตรัคเตอร์ยังรับพารามิเตอร์ชื่อ controller ซึ่งคาดหวังอ็อบเจ็กต์ของคลาส PeriBoard ที่นำมาใช้อ่านค่าแสงจากบอร์ดไมโครคอนโทรลเลอร์ได้
  • บรรทัดที่ 41-43 นิยามเมท็อด move() ซึ่งถ้าหากไม่ได้มีการกำหนดอ็อบเจ็กต์ controller มาให้ตั้งแต่แรกจะทำให้แป้นรับเคลื่อนที่ไปตามตำแหน่งเดียวกันกับลูกตะกร้อ เราจะมาเพิ่มเติมเมท็อดนี้ในภายหลัง
  • บรรทัดที่ 53 สร้างอ็อบเจ็กต์ Player ขึ้นมาโดยกำหนดอาร์กิวเมนต์เป็น None ให้กับพารามิเตอร์ controller เนื่องจากโค้ดยังไม่ได้มีส่วนเชื่อมโยงกับบอร์ด
  • บรรทัดที่ 58 เรียกใช้เมท็อด move() ของแป้นรับลูกเพื่อให้อัพเดตตำแหน่งของตนเอง

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

Takro-3.png

อย่างไรก็ตาม เมื่อลูกตะกร้อตกกระทบกับแป้นรับจะทะลุผ่านลงไปโดยไม่เกิดอะไรขึ้น

ตรวจสอบการกระทบลูกตะกร้อ

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

 9 class Ball(vis.sphere):
10 
11     SIZE = 0.5
12 
13     def __init__(self,color=vis.color.white,vel=(0,0),acc=(0,0)):
14         vis.sphere.__init__(self,pos=(0,0),radius=self.SIZE,color=color,
15                 make_trail=True,retain=10)
16         self.vel = vis.vector(vel)
17         self.acc = vis.vector(acc)
18 
19     def move(self,dt):
20         self.vel += self.acc*dt
21         self.pos += self.vel*dt
22         if self.pos.x+self.SIZE > BORDER:  # hit right wall
23             self.pos.x = BORDER-self.SIZE
24             self.vel.x = -abs(self.vel.x)
25         if self.pos.x-self.SIZE < -BORDER: # hit left wall
26             self.pos.x = -BORDER+self.SIZE
27             self.vel.x = +abs(self.vel.x)
28         if self.pos.y+self.SIZE > BORDER:  # hit top wall
29             self.pos.y = BORDER-self.SIZE
30             self.vel.y = -abs(self.vel.y)
31 
32     def hitPlayer(self,player):
33         if (abs(self.pos.x-player.pos.x) < player.radius) and \
34            (self.pos.y < player.pos.y+player.axis.y+self.SIZE) and \
35            (self.pos.y > player.pos.y-self.SIZE):
36             self.pos.y = player.pos.y+player.axis.y+self.SIZE
37             self.vel.y = abs(self.vel.y)
38             return True
39         else:
40             return False

จากนั้นจึงนำเมท็อดนี้มาใช้ในลูปหลักดังนี้

65 while True:
66     vis.rate(ANIM_RATE)
67     ball.move(1.0/ANIM_RATE)
68     player.move()
69     ball.hitPlayer(player)

ทดสอบสคริปต์จะเห็นว่าลูกตะกร้อจะกระดอนกลับขึ้นมาเมื่อกระทบกับแป้นรับลูก ทดลองคอมเม้นต์โค้ด player.move() ในบรรทัดที่ 68 เพื่อให้แน่ใจว่าตะกร้อจะไม่กระดอนขึ้นมาหากไม่กระทบกับแป้นรับ

Takro-4.png

ควบคุมด้วยบอร์ดพ่วง

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

 1 import vis
 2 from practicum import findDevices
 3 from peri import PeriBoard
 4  
 5 ANIM_RATE = 30
 6 BORDER = 10
 7 scene = vis.display(title='Takro',width=500,height=500)
 8 scene.center = (0,0)
 9 scene.range = BORDER*1.2
10 
11 devs = findDevices()
12 if len(devs) == 0:
13     print '*** No MCU board found.'
14     board = None
15 else:
16     board = PeriBoard(devs[0])
17     print '*** MCU board found'
18     print '*** Device manufacturer: %s' % board.getVendorName()
19     print '*** Device name: %s' % board.getDeviceName()

จากนั้นแก้ไขเมท็อด Player.move() ให้นำค่าแสงมาใช้กำหนดตำแหน่งของแป้นรับลูกในกรณีที่พบบอร์ดไมโครคอนโทรลเลอร์ พร้อมทั้งส่งอ็อบเจ็กต์ board ไปเป็นอาร์กิวเม้นต์ในระหว่างการสร้างอ็อบเจ็กต์ player ขึ้นมา เนื่องจากค่าแสงที่วัดได้อยู่ในช่วง 0 ถึง 1023 บรรทัดที่ 68 จึงนำเอาค่าแสงที่อ่านได้มาปรับให้อยู่ในช่วง -BORDER ถึง +BORDER

56 class Player(vis.cylinder):
57 
58     def __init__(self,controller):
59         vis.cylinder.__init__(self,pos=(0,-BORDER),axis=(0,0.2),
60                 radius=2,color=vis.color.blue)
61         self.controller = controller
62 
63     def move(self):
64         if self.controller == None:
65             self.pos.x = ball.pos.x
66         else:
67             light = self.controller.getLight()
68             self.pos.x = light/1023.0*BORDER*2 - BORDER
69 
70 ################################################################
71 border = vis.curve(pos=[
72     (-BORDER,-BORDER),
73     (-BORDER,+BORDER),
74     (+BORDER,+BORDER),
75     (+BORDER,-BORDER)
76     ],radius=0.1,color=vis.color.red)
77 ball = Ball(color=vis.color.yellow,vel=(5,10),acc=(0,-5))
78 player = Player(board)

คาลิเบรทตัววัดแสง

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

11 devs = findDevices()
12 if len(devs) == 0:
13     print '*** No MCU board found.'
14     board = None
15 else:
16     board = PeriBoard(devs[0])
17     print '*** MCU board found'
18     print '*** Device manufacturer: %s' % board.getVendorName()
19     print '*** Device name: %s' % board.getDeviceName()
20     print 'Calibrating board.'
21     raw_input('Face the board down then press ENTER...')
22     minLight = board.getLight()
23     raw_input('Face the board up then press ENTER...')
24     maxLight = board.getLight()
25     board.lightRange = (minLight,maxLight)
26     print 'Calibration done.  Light range is (%d,%d)' % board.lightRange

ค่าแสงต่ำสุดและสูงสุดจะถูกจัดเก็บไว้คู่กันในคุณสมบัติชื่อ lightRange ของอ็อบเจ็กต์ board

ช่วงค่าที่ได้มาสามารถนำไปปรับแก้ในเมท็อด Player.move() ซึ่งแต่เดิมมีการคำนวณตำแหน่งของแป้นจากแสงไว้ดังนี้

self.pos.x = light/1023.0*BORDER*2 - BORDER

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

minl,maxl = self.controller.lightRange
x = float(light-minl)/(maxl-minl)*BORDER*2-BORDER
if x < -BORDER:
    x = -BORDER
if x > BORDER:
    x = BORDER
self.pos.x = x

แม้การคำนวณจะตรงไปตรงมาแต่ก็ดูค่อนข้างเทอะทะ อีกทั้งการคำนวณลักษณะนี้เป็นที่รู้จักกันดีในชื่อ การประมาณค่าในช่วงแบบเชิงเส้น (linear interpolation) ทำให้ไลบรารีทางคณิตศาสตร์ส่วนใหญ่เตรียมฟังก์ชันไว้ให้พร้อมใช้งานอยู่แล้ว เราจะนำฟังก์ชัน interp() ของโมดูล numpy มาใช้แทนที่โค้ดข้างบน เริ่มด้วยการอิมพอร์ทฟังก์ชัน interp() มาจากโมดูล numpy

1 import vis
2 from practicum import findDevices
3 from peri import PeriBoard
4 from numpy import interp

แล้วจึงนำฟังก์ชันมาใช้ในเมท็อด Player.move() ดังนี้

64 class Player(vis.cylinder):
65 
66     def __init__(self,controller):
67         vis.cylinder.__init__(self,pos=(0,-BORDER),axis=(0,0.2),
68                 radius=2,color=vis.color.blue)
69         self.controller = controller
70 
71     def move(self):
72         if self.controller == None:
73             self.pos.x = ball.pos.x
74         else:
75             light = self.controller.getLight()
76             x = interp(light,self.controller.lightRange,[-BORDER,BORDER])
77             self.pos.x = x

กราฟด้านล่างแสดงตัวอย่างความสัมพันธ์ระหว่างค่า light กับค่า x ที่คำนวณได้จากฟังก์ชัน interp() โดยสมมติว่า self.controller.lightRange คือ (200,600) และ BORDER=10

Takro-interp.png

เห็นได้ว่าฟังก์ชัน interp() จะจำกัดค่าที่คืนมาให้ไม่ให้เกินออกไปจากช่วง (-BORDER,+BORDER) ทำให้แป้นรับลูกบอลไม่วิ่งเลยขอบสนามออกไป

ตำแหน่งแป้นรับลูกเมื่อแสงน้อย
ตำแหน่งแป้นรับลูกเมื่อแสงมาก

ลดการส่ายของแป้นรับลูก

ในบางสถานการณ์จะพบว่าแป้นรับลูกมีการสั่นไปมา อันเนื่องมาจากมือผู้เล่นสั่นเอง หรือแสงสว่างในบริเวณเป็นแสงไฟแบบฟลูออเรสเซนต์ซึ่งมีการกระพริบตลอดเวลาเป็นปกติ การกระพริบนี้อาจไม่สังเกตเห็นได้ด้วยตามนุษย์เราแต่ตรวจจับได้ด้วยตัววัดแสงบนบอร์ดพ่วง

เราสามารถเกลี่ยตำแหน่งแป้นรับลูกให้เรียบขึ้นได้ด้วยกลไก exponential smoothing ซึ่งมีสูตรดังนี้

โดยที่

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

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

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

64 class Player(vis.cylinder):
65 
66     def __init__(self,controller):
67         vis.cylinder.__init__(self,pos=(0,-BORDER),axis=(0,0.2),
68                 radius=2,color=vis.color.blue)
69         self.controller = controller
70 
71     def move(self):
72         if self.controller == None:
73             self.pos.x = ball.pos.x
74         else:
75             light = self.controller.getLight()
76             x = interp(light,self.controller.lightRange,[-BORDER,BORDER])
77             self.pos.x = 0.1*x + 0.9*self.pos.x

รายละเอียดอื่น ๆ

แก้ไขเหตุขัดข้อง USB Error

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

usb.core.USBError: [Errno None] error sending control message: Connection timed out

ซึ่งเกิดจากการที่โฮสท์ไม่ได้รับการตอบสนองจากไมโครคอนโทรลเลอร์ภายในเวลาที่กำหนด เราจะดักเหตุขัดข้องนี้เอาไว้เพื่อให้สคริปต์ยังทำงานต่อไปได้ เริ่มต้นด้วยการอิมพอร์ทเหตุขัดข้อง usb.core.USBError มาจากโมดูล usb

1 import vis
2 from practicum import findDevices
3 from peri import PeriBoard
4 from numpy import interp
5 from usb.core import USBError

จากนั้นใช้โครงสร้าง try..except (ซึ่งทำงานคล้ายคลึงกับโครงสร้าง try..catch ในภาษาจาวา) ในจุดที่สคริปต์อ่านค่าแสงจากบอร์ดดังนี้

65 class Player(vis.cylinder):
66 
67     def __init__(self,controller):
68         vis.cylinder.__init__(self,pos=(0,-BORDER),axis=(0,0.2),
69                 radius=2,color=vis.color.blue)
70         self.controller = controller
71 
72     def move(self):
73         if self.controller == None:
74             self.pos.x = ball.pos.x
75         else:
76             try:
77                 light = self.controller.getLight()
78                 x = interp(light,self.controller.lightRange,[-BORDER,BORDER])
79                 self.pos.x = 0.1*x + 0.9*self.pos.x
80             except USBError:
81                 print "USB error detected"

ตรวจสอบว่าลูกออกนอกกรอบแล้วจบเกม

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

เพื่อความสะดวกเราจะเพิ่มเมท็อด isOut() ลงในคลาส Ball เพื่อใช้ตรวจสอบว่าลูกได้วิ่งเลยขอบล่างของสนามไปแล้วดังนี้

def isOut(self):
    return self.pos.y-self.SIZE < -BORDER

จากนั้นจึงเรียกใช้เมท็อดนี้ในลูปหลัก หากพบว่าลูกออกจากสนามไปแล้วให้แสดงป้าย "GAME OVER!" ด้วย vis.label หยุดรอ 2 วินาที แล้วจบการทำงาน

 99 while True:
100     vis.rate(ANIM_RATE)
101     ball.move(1.0/ANIM_RATE)
102     player.move()
103     ball.hitPlayer(player)
104     if ball.isOut():
105         vis.label(height=30,text='GAME OVER!')
106         vis.sleep(2)
107         vis.exit()
Takro-over.png

แสดงคะแนนให้ผู้เล่น

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

 87 border = vis.curve(pos=[
 88     (-BORDER,-BORDER),
 89     (-BORDER,+BORDER),
 90     (+BORDER,+BORDER),
 91     (+BORDER,-BORDER)
 92     ],radius=0.1,color=vis.color.red)
 93 ball = Ball(color=vis.color.yellow,vel=(5,10),acc=(0,-5))
 94 player = Player(board)
 95 score = vis.label(pos=(BORDER,BORDER))
 96 score.value = 0
 97 score.text = "Score: %d" % score.value
 98  
 99 while True:
100     vis.rate(ANIM_RATE)
101     ball.move(1.0/ANIM_RATE)
102     player.move()
103     if ball.hitPlayer(player):
104         score.value += 1
105         score.text = "Score: %d" % score.value
106     if ball.isOut():
107         vis.label(text="GAME OVER!",height=30)
108         vis.sleep(2)
109         vis.exit()
Takro-score.png