1 คะแนน โดย GN⁺ 2023-10-09 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • 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 สำหรับไดเรกทอรี
  • พฤติกรรมที่สังเกตได้มีสามอย่าง
    • แม้จะเข้าไดเรกทอรีด้วย cd timestamp ก็ไม่อัปเดต
    • เมื่อดับเบิลคลิก symbolic link ของไดเรกทอรีใน Finder timestamp จะอัปเดต
    • สามารถอ่านค่านี้ด้วย mdls ได้ด้วยความละเอียดระดับวินาที
  • ด้วยคุณสมบัตินี้ จึงสามารถสร้างปุ่มภายใน Finder ได้
    • สร้าง symbolic link ชื่อ button ในไดเรกทอรี dir ให้ชี้ไปยัง dir
    • ตอนเริ่มต้น อ่านเวลาที่ dir ถูกเปิดล่าสุด
    • polling timestamp ซ้ำ ๆ และเมื่อค่ามีการเปลี่ยนแปลงให้ถือเป็นอินพุต
    • เปิด button โดยไม่เปลี่ยนตำแหน่ง Finder เพื่อเปลี่ยนเวลาที่ dir ถูกเปิดล่าสุด

วาดหน้าจอ 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 ความคิดเห็น

 
GN⁺ 2023-10-09
ความคิดเห็นบน Hacker News
  • น่าทึ่งจนพูดไม่ออก และแม้ผมจะมองตัวเองว่าเป็นคนที่ คิดนอกกรอบ แต่ถ้าก่อนอ่านบทความนี้คงบอกว่าเป็นไปไม่ได้แน่ ๆ
    ชอบมากที่ได้เห็นเขาผลักดันโปรเจกต์ที่ทำขึ้นเพื่อท้าทายตัวเองช่วงสุดสัปดาห์ว่าเป็นไปได้จริงไหม และอ่านบทความได้สนุกมากด้วย

    • ในซอฟต์แวร์ก็เลยไม่เคยพูดว่า เป็นไปไม่ได้ ขอแค่มีเวลาและความคิดสร้างสรรค์ก็พอ
  • Bob รีบคว้าแจ็กเก็ตกับแบ็กแพ็กเพื่อจะตามทีมไปดื่มเบียร์บ่ายวันศุกร์ แล้วไม่ทันสังเกตว่าปล่อย Flappy Bird ที่รันบน MacOS ทิ้งไว้
    ตลอดช่วงสุดสัปดาห์ยาวที่มีวันหยุดธนาคาร อายุการเขียนของ SSD ในโน้ตบุ๊กถูกใช้ไปด้วยความเร็ว 4 เฟรมต่อวินาที และเมื่อ Bob กลับมาในวันอังคารก็พบว่า SSD ไม่ทำงานแล้ว

    • สงสัยว่าคนระดับที่เปิด Flappy Dird ในที่ทำงานจะมีโอกาส รีบขนาดนั้น จริงหรือ
    • ถึงอย่างนั้นก็คาดหวังว่ารันไปเป็นปี ๆ ไดรฟ์ก็ยังไม่น่าตาย
  • ชอบวิธีแบบ “นี่เป็นรูปภาพ ตอนนี้ไม่รู้แล้วว่าจะฝังทวีตยังไง” มากกว่าเสียอีก

    • แบบนี้ทำให้บทความ คงอยู่ได้นาน กว่า เพราะ embed อาจหายหรือพังได้ทุกเมื่อ
    • เห็นด้วย เพราะหลังจากบล็อก Twitter แล้ว ตำแหน่ง embed มักไม่เห็นอะไรเลย หรือบางทีก็เรนเดอร์แค่อีโมจิอยู่กลางพื้นที่ว่าง
  • สงสัยว่า OP จะแก้ปัญหาความเร็วตอนเริ่มของ AppleScript ด้วยการเขียนใหม่เป็น JavaScript ได้ไหม
    มาหลายปีแล้วที่เฟรมเวิร์กพื้นฐานและเครื่องมือบรรทัดคำสั่ง osascript รองรับ AppleScript กับ JavaScript แทบเท่า ๆ กัน และใน JavaScript ก็มีหลายอย่างอยู่ในอ็อบเจ็กต์ $

    • ไม่รู้เลย เดี๋ยวต้องลองดู เจ๋งมากที่ภายใน 5 นาทีได้รับคอมเมนต์เกี่ยวกับ AppleScript ที่มีประโยชน์มากถึงสองอัน
  • ถ้าจะเรียกอีเวนต์ AppleScript แบบเนทีฟจาก Python ก็ลองใช้ py-appscript ได้
    https://github.com/hhas/appscript/tree/master/py-appscript
    เคยใช้แค่ Appscript เวอร์ชัน Ruby แต่ถ้าเวอร์ชัน Python ทำงานแบบเดียวกัน ก็ดูเป็นตัวเลือกที่เหมาะมากจนแทบไม่ต้องใช้ AppleScript เองเลย

    • ไม่ได้คิดด้วยซ้ำว่าจะมีอะไรแบบนี้ ต้องไปดูแน่นอน แต่ก็มีความสนุกอยู่บ้างตรงที่ลูปหลักเป็น AppleScript
  • ถ้าชอบ เกมที่ไปอยู่ในที่ที่ไม่ควรอยู่ แบบนี้ ยังมีโปรเจกต์ที่ไร้เหตุผลยิ่งกว่านี้อีก
    Fontemon: เกมในฟอนต์ https://www.coderelay.io/fontemon.html
    Dungeons & Directories: text adventure ใน file browser https://wheybags.com/dungeons_and_directories/ — อันนี้ผมทำเอง

    • ขอบคุณสำหรับทั้งสองลิงก์ สนุกดีที่เลือกใช้ file system และอีกไม่กี่สัปดาห์พอกลับไปใช้เครื่อง Windows แล้วอยากลองดู
      ได้ลองเล่นเกมฟอนต์นิดหน่อยด้วย และทำ Hexagone[1] เพื่อดูว่าการเขียนฟอนต์ให้ความรู้สึกยังไง แม้ยังไม่แน่ใจว่าจะทำเกมแบบไหน แต่การได้แรงบันดาลใจแบบนี้ก็สนุกดี
      [1] http://eieio.games/nonsense/hexagone-converting-hex-to-rgb-w...
    • ยังมี Defender of the Favicon ด้วย: http://www.p01.org/defender_of_the_favicon/
    • วิธี implement อันที่สองช่างคิดมาก สงสัยว่าเคยพิจารณาทำด้วย FUSE แทนไหม
    • สำหรับสายคลั่ง command line ก็มี rpg-cli ด้วย
      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 อยู่ภายในนั้นอีกที

    • ถ้าไม่เอาการจำลอง MacOS เอาแค่ Game of Life ก็สงสัยว่าอาจทำด้วย ไฟล์ DS_Store ได้ไหม
      ตอนแรกสร้างโฟลเดอร์ตามแพตเทิร์น แล้วคลิกขวาเพื่อจัดเรียง มันก็จะถูกวางเป็นกริด จากนั้นสคริปต์อ่านตำแหน่งโฟลเดอร์จากไฟล์ DS_Store แล้วอัปเดตเป็นตำแหน่งใหม่ของเฟรมถัดไป หรือสร้างและลบโฟลเดอร์เมื่อจำเป็น