Flappy Dird: Flappy Bird ที่สร้างขึ้นใน macOS Finder
(eieio.games)- Flappy Dird เป็นการทดลองสร้าง Flappy Bird โดยใช้รายการไฟล์ใน macOS Finder เป็นหน้าจอ และใช้การเลือกไฟล์กับการเปิดไฟล์เป็นอินพุต
- แนวคิดหลักเริ่มจากการอ่านค่า Date Last Opened ของไดเรกทอรีที่อัปเดตเฉพาะใน Finder ให้เหมือนปุ่ม และเปลี่ยนชื่อไฟล์ symbolic link เพื่อวาดหน้าจอ
- การเรนเดอร์ด้วยการเปลี่ยนชื่อไฟล์แบบง่าย ๆ นั้นช้าและเกิด screen tearing มาก จึงปรับปรุงด้วยวิธี double buffering โดยใช้ AppleScript เปลี่ยนไดเรกทอรีเป้าหมายของหน้าต่าง Finder
- อินพุตแบบดับเบิลคลิกไม่เสถียรเมื่อเกิน 2 FPS เนื่องจากข้อจำกัดของ timestamp ที่ละเอียดระดับวินาที และภายหลังจึงเปลี่ยนไปอ่าน รายการที่เลือก ใน Finder แทน
- โครงสร้างสุดท้ายให้ AppleScript รันลูป ส่วน Python รับผิดชอบสถานะเกมและการเรนเดอร์ ทำงานที่ 4 FPS แต่ยังมีปัญหาอินพุตตกหล่นและข้อจำกัดเฉพาะของ Finder อยู่
ใช้ Finder เป็นหน้าจอเกม
- Flappy Dird เป็นเกมที่ใช้ Finder เป็นฐาน สามารถดูโค้ดและรันเองได้จาก GitHub repository
- เกมมี วิธีใช้งาน, การติดตามคะแนนสูงสุด และโฆษณาแบนเนอร์แบบเลื่อน
- ผู้ใช้เริ่มเกมด้วยการดับเบิลคลิก และทำให้นกกระโดดด้วยการเลือกไฟล์ใดก็ได้ในหน้าต่าง Finder
- ความเร็วในการรันอยู่ที่ประมาณ 4 เฟรมต่อวินาที และยากที่จะทำให้เร็วกว่านี้มาก อีกทั้งบางครั้งอินพุตก็หลุดหาย
ใช้ Date Last Opened เหมือนปุ่ม
- จุดเริ่มต้นคือการที่ Finder มีฟิลด์ Date Last Opened สำหรับไดเรกทอรี
- พฤติกรรมที่สังเกตได้มีสามอย่าง
- แม้จะเข้าไดเรกทอรีด้วย
cdtimestamp ก็ไม่อัปเดต - เมื่อดับเบิลคลิก symbolic link ของไดเรกทอรีใน Finder timestamp จะอัปเดต
- สามารถอ่านค่านี้ด้วย
mdlsได้ด้วยความละเอียดระดับวินาที
- แม้จะเข้าไดเรกทอรีด้วย
- ด้วยคุณสมบัตินี้ จึงสามารถสร้างปุ่มภายใน Finder ได้
- สร้าง symbolic link ชื่อ
buttonในไดเรกทอรีdirให้ชี้ไปยังdir - ตอนเริ่มต้น อ่านเวลาที่
dirถูกเปิดล่าสุด - polling timestamp ซ้ำ ๆ และเมื่อค่ามีการเปลี่ยนแปลงให้ถือเป็นอินพุต
- เปิด
buttonโดยไม่เปลี่ยนตำแหน่ง Finder เพื่อเปลี่ยนเวลาที่dirถูกเปิดล่าสุด
- สร้าง symbolic link ชื่อ
วาดหน้าจอ 15x15 ด้วยชื่อไฟล์อีโมจิ
- แม้จะจัด ASCII art ให้เข้ากับฟอนต์เริ่มต้นของ Finder ได้ยาก แต่ใช้ประโยชน์จากการที่ อีโมจิ ในชื่อไฟล์แสดงผลด้วยความกว้างคงที่
- โปรโตไทป์สร้าง symbolic link ย่อย 15 รายการใน
dirที่ชี้กลับไปยังdirแล้วเปลี่ยนชื่อแต่ละ link เป็นแถวอีโมจิเพื่อประกอบเป็นหน้าจอ - ฟังก์ชันเรนเดอร์รับค่า
bird_y_pos,pipe_locations,frameแล้วสร้าง กริดอีโมจิ 15x15 - ทุกเฟรมจะเปลี่ยนชื่อ symbolic link ทั้งหมดในไดเรกทอรีให้เป็นแต่ละแถวของกริดอีโมจิ
- ยังจำเป็นต้องมีแฮ็กปรับลำดับการเปลี่ยนชื่อเพื่อให้ Finder จัดเรียงตาม
Date Modified - วิธีนี้ใช้งานได้ แต่ช้ามากและเกิด screen tearing อย่างรุนแรง
AppleScript และ double buffering
- ระหว่างหาวิธีรีเฟรชรายการไฟล์ใน Finder เพื่อลด screen tearing ได้ใช้คำสั่ง AppleScript
tell application "Finder" to tell front window to update every item - คำสั่งนี้ช่วยได้บ้าง แต่ยังมี tearing อยู่ และนำไปสู่แนวทางควบคุม Finder โดยตรงด้วย AppleScript ในภายหลัง
- มีตัวเลือกสำหรับ double buffering หลายแบบ
- วิธีสลับเนื้อหาไดเรกทอรีแบบ atomic ด้วย symbolic link ไม่เหมาะ เพราะ Finder ไม่สามารถแสดงเนื้อหาภายในไดเรกทอรีที่เป็น symbolic link ได้
- วิธีใช้ไดเรกทอรีสองตัวที่ชี้หากันสามารถลด tearing ได้ แต่จะเดินเฟรมต่อได้เฉพาะเมื่อผู้ใช้คลิก จึงไม่เหมาะกับเกม
- จำเป็นต้องมีวิธีเปลี่ยนไดเรกทอรีที่ Finder แสดงอยู่ โดยไม่เปลี่ยน “เวลาที่เปิดล่าสุด”
- ทางออกคือใช้
tell application "Finder" to set target of front Finder window to ("PATH" POSIX file)ของ AppleScript เพื่อเปลี่ยนไดเรกทอรีเป้าหมายของหน้าต่าง Finder - เมื่อใช้วิธีนี้ทำ double buffering เกมทำงานได้ลื่นที่ 1 FPS แต่ก็ยังช้าอยู่ดี
เปลี่ยนวิธีอินพุต: จากดับเบิลคลิกเป็นการเลือกไฟล์
- อินพุตแบบดับเบิลคลิกมีข้อจำกัดเมื่อเกิน 2 FPS เพราะความละเอียดของ timestamp อยู่ระดับวินาที
- AppleScript สามารถอ่าน รายการที่เลือก ในหน้าต่าง Finder ได้ จึงเปลี่ยนอินพุตเป็น “เลือกไฟล์ใดก็ได้”
- ลูปเกมใหม่มีโครงสร้างดังนี้
- รอจนกว่าผู้ใช้จะดับเบิลคลิก
- ทุกเฟรม ใช้ AppleScript ตรวจสอบว่ามีไฟล์ถูกเลือกอยู่ในหน้าต่างปัจจุบันหรือไม่
- หากมีการเลือก หรือเวลาที่เปิดล่าสุดเปลี่ยน ให้นกกระโดด
- ใช้ AppleScript เปลี่ยนไดเรกทอรีที่ Finder แสดงอยู่
- รอเวลาที่เหลือเพื่อให้ตรงกับเฟรมเรตเป้าหมาย
- วิธีนี้ไปถึง 2 FPS ได้ระดับหนึ่ง แต่การเรียก AppleScript ใช้เวลาประมาณ 0.2 วินาที ซึ่งเป็นต้นทุนคงที่ที่สูง
- แม้ที่ 2 FPS หากผู้ใช้เลือกไฟล์หลังจากตรวจสอบอินพุตแล้ว ก็อาจพลาดอินพุตได้
ย้าย main loop ไปไว้ใน AppleScript
- จากการ profiling พบว่า AppleScript ที่พิมพ์เลข
1ลง log แล้วจบ ใช้เวลาประมาณ 0.14 วินาที - ภายหลังมีการแก้ไขว่า การวัดนี้ไม่ใช่การวัดเวลาเริ่มต้นของ AppleScript ที่ยุติธรรม และหากเทียบกับ AppleScript ว่าง ๆ อาจใกล้เคียง 0.06 วินาที มากกว่า
- เพื่อไม่ต้องเสียต้นทุนเริ่ม AppleScript ทุกเฟรม จึงย้าย main loop ไปไว้ใน AppleScript และให้ Python รับผิดชอบ game logic กับการเรนเดอร์
- โครงสร้างพื้นฐานเป็นดังนี้
- ใช้
game.py awaitรอการดับเบิลคลิกเริ่มต้นจากผู้ใช้ - ใช้
game.py start-frameบันทึกเวลาเริ่มเฟรม - อ่านจำนวนรายการที่เลือกใน Finder ส่งต่อให้
game.py tickและเรนเดอร์เฟรม - รับชื่อไดเรกทอรีที่
tickเตรียมไว้ แล้วเปลี่ยนเป้าหมายของหน้าต่าง Finder ไปยังไดเรกทอรีนั้น - ใช้
game.py sleepเพื่อให้ตรงกับเฟรมเรตเป้าหมาย และทำซ้ำหากผู้เล่นยังไม่แพ้
- ใช้
- ภายหลัง
start-frameกับsleepสามารถรวมกันได้ และเพิ่มลูปสำหรับเริ่มใหม่หลังตาย
รายละเอียดงานที่จำเป็นเพื่อทำให้เกมสมบูรณ์
- สถานะเกมถูกเก็บในไฟล์
state.jsonโดยอ่านและเขียนทุกเฟรม - ข้อความมีความกว้างของอักขระแคบกว่าอีโมจิ จึงต้องใช้ ระยะห่างที่ hardcode ไว้ จำนวนมากเพื่อจัดแนว
- ข้อความแบนเนอร์เลื่อนด้านบนยุ่งยากเป็นพิเศษ จึงสร้างฟังก์ชัน
read_n_ad_charsเพื่อประมาณว่าควรแสดงกี่ตัวอักษรในครั้งเดียว - แทนที่จะส่ง working directory ให้ AppleScript ใช้การแทนที่ตัวแปร template ในการเรียก Python
first-time-setup - AppleScript กลืนค่า output ของ Python ไป จึง append log ลงไฟล์ แล้วค่อยตรวจด้วย
catภายหลัง - ปรับปรุงความรู้สึกในการเล่นโดยให้แตะในสองเฟรมติดกันแล้วกระโดดสูงขึ้นอีกหนึ่งบรรทัด
ข้อจำกัดของเกม Finder ที่สร้างโดยไม่มีเอนจิน
- โปรโตไทป์มีประมาณ 90 บรรทัด ส่วนโค้ดสุดท้ายมีประมาณ 550 บรรทัด แต่ราวหนึ่งในสามเป็น boilerplate หรือค่าคงที่
- วิธีที่เก็บสถานะเฟรมไว้ในอาร์เรย์ 2D ขนาดเล็กโดยไม่มี game engine ทำให้จดจำภาพรวมของเกมทั้งหมดไว้ในหัวได้ง่าย
- 4 FPS และอินพุตที่จำกัดเป็นข้อจำกัดใหญ่ แต่ก็ดูเป็นไปได้ที่จะสร้างเกมอื่นด้วยวิธีเดียวกัน
- ยังมีไอเดียทำ Tetris ใน Finder ด้วย ซึ่งถูกยกไว้เป็นตัวอย่างที่แม้จะยากแต่ก็อาจเป็นไปได้
1 ความคิดเห็น
ความคิดเห็นบน Hacker News
น่าทึ่งจนพูดไม่ออก และแม้ผมจะมองตัวเองว่าเป็นคนที่ คิดนอกกรอบ แต่ถ้าก่อนอ่านบทความนี้คงบอกว่าเป็นไปไม่ได้แน่ ๆ
ชอบมากที่ได้เห็นเขาผลักดันโปรเจกต์ที่ทำขึ้นเพื่อท้าทายตัวเองช่วงสุดสัปดาห์ว่าเป็นไปได้จริงไหม และอ่านบทความได้สนุกมากด้วย
Bob รีบคว้าแจ็กเก็ตกับแบ็กแพ็กเพื่อจะตามทีมไปดื่มเบียร์บ่ายวันศุกร์ แล้วไม่ทันสังเกตว่าปล่อย Flappy Bird ที่รันบน MacOS ทิ้งไว้
ตลอดช่วงสุดสัปดาห์ยาวที่มีวันหยุดธนาคาร อายุการเขียนของ SSD ในโน้ตบุ๊กถูกใช้ไปด้วยความเร็ว 4 เฟรมต่อวินาที และเมื่อ Bob กลับมาในวันอังคารก็พบว่า SSD ไม่ทำงานแล้ว
ชอบวิธีแบบ “นี่เป็นรูปภาพ ตอนนี้ไม่รู้แล้วว่าจะฝังทวีตยังไง” มากกว่าเสียอีก
สงสัยว่า OP จะแก้ปัญหาความเร็วตอนเริ่มของ AppleScript ด้วยการเขียนใหม่เป็น JavaScript ได้ไหม
มาหลายปีแล้วที่เฟรมเวิร์กพื้นฐานและเครื่องมือบรรทัดคำสั่ง
osascriptรองรับ AppleScript กับ JavaScript แทบเท่า ๆ กัน และใน JavaScript ก็มีหลายอย่างอยู่ในอ็อบเจ็กต์$ถ้าจะเรียกอีเวนต์ AppleScript แบบเนทีฟจาก Python ก็ลองใช้ py-appscript ได้
https://github.com/hhas/appscript/tree/master/py-appscript
เคยใช้แค่ Appscript เวอร์ชัน Ruby แต่ถ้าเวอร์ชัน Python ทำงานแบบเดียวกัน ก็ดูเป็นตัวเลือกที่เหมาะมากจนแทบไม่ต้องใช้ AppleScript เองเลย
ถ้าชอบ เกมที่ไปอยู่ในที่ที่ไม่ควรอยู่ แบบนี้ ยังมีโปรเจกต์ที่ไร้เหตุผลยิ่งกว่านี้อีก
Fontemon: เกมในฟอนต์ https://www.coderelay.io/fontemon.html
Dungeons & Directories: text adventure ใน file browser https://wheybags.com/dungeons_and_directories/ — อันนี้ผมทำเอง
ได้ลองเล่นเกมฟอนต์นิดหน่อยด้วย และทำ Hexagone[1] เพื่อดูว่าการเขียนฟอนต์ให้ความรู้สึกยังไง แม้ยังไม่แน่ใจว่าจะทำเกมแบบไหน แต่การได้แรงบันดาลใจแบบนี้ก็สนุกดี
[1] http://eieio.games/nonsense/hexagone-converting-hex-to-rgb-w...
https://github.com/facundoolano/rpg-cli
ชอบท่าที “ยังทำให้ดีกว่านี้ได้” ที่ไหลอยู่ตลอดทั้งบทความมาก ลองคิดดูว่าถ้านักพัฒนาทุกคนมี mindset แบบนี้จะเป็นยังไง
แน่นอนว่าถ้าทุกคนเป็นแบบนั้น ก็จะทำให้ดีกว่านี้ได้เสมอจนไม่มีอะไรเสร็จสักอย่าง และบางครั้งก็ต้องมีคนบอกว่า “เท่านี้ก็ดีพอแล้ว”
กับโปรเจกต์งานอดิเรกแบบนี้ การคงฝั่ง “ยังทำให้ดีกว่านี้ได้” ไว้จะง่ายกว่า ในงานจริงมักต้องคิดว่าการใช้เวลาเพิ่มอีกหนึ่งวันเพื่อขัดเกลา มีคุณค่ามากกว่างานอื่นทั้งหมดที่เราอยากทำหรือไม่
แต่กับ Flappy Dird ผมอยากทำต่อไปจนกว่าจะพอใจกับโปรเจกต์ หรือจนเชื่อว่าไม่สามารถผลักเกมนี้ไปได้ไกลกว่านี้แล้ว
ตอนเห็นชื่อ ตอนแรกคิดว่าจะเป็นวิธีใช้ AppleScript ขยับไอคอนเหมือนสไปรต์ใน มุมมองไอคอน ของหน้าต่าง Finder
แต่จริง ๆ ทำในมุมมองรายการ และตอนแรกใช้วิธี polling “วันที่เปิดล่าสุด” ของไดเรกทอรี เลยประหลาดใจและเป็นแนวทางที่สร้างสรรค์จริง ๆ
ชอบจิตวิญญาณของบทความนี้ที่เอาเกมเล็ก ๆ ไปวางไว้ในทุกที่ที่เป็นไปได้มาก
ทำให้นึกถึงวิดเจ็ตแถบงาน Fortune Teller fish ที่เคยใช้ได้ใน GNOME สมัยก่อน
อีกเรื่องที่ไม่เกี่ยวกันเลย พอเห็นการเขียนตัวพิมพ์ใหญ่แบบ “MacOS Finder” ตอนแรกนึกว่าเป็นส่วนขยายสำหรับ MacOS 8 หรือ 9 แต่ผลลัพธ์จริงสนุกกว่านั้นมาก
ความท้าทายต่อไปคือรัน Game of Life ใน Finder แล้วจำลอง Mac ที่รัน Finder อยู่ภายในนั้นอีกที
ตอนแรกสร้างโฟลเดอร์ตามแพตเทิร์น แล้วคลิกขวาเพื่อจัดเรียง มันก็จะถูกวางเป็นกริด จากนั้นสคริปต์อ่านตำแหน่งโฟลเดอร์จากไฟล์ DS_Store แล้วอัปเดตเป็นตำแหน่งใหม่ของเฟรมถัดไป หรือสร้างและลบโฟลเดอร์เมื่อจำเป็น