Nortis (เดิมชื่อ Notris) เป็นเกม PSX แบบโฮมบรูว์ เขียนด้วยภาษา C โดยใช้เครื่องมือที่ทันสมัย สามารถเล่นได้อย่างสมบูรณ์บนฮาร์ดแวร์ดั้งเดิมและขับเคลื่อนโดย PSNoobSDK
ดูฐานรหัส PSX ที่นี่
ปีที่แล้วฉันได้ครอบครอง PlayStation 1 สีดำหายาก ซึ่งเรียกว่า Net Yaroze และเป็นคอนโซลพิเศษที่สามารถเล่นเกมโฮมบรูว์ได้เช่นเดียวกับเกม PSX ทั่วไป นี่เป็นส่วนหนึ่งของโครงการพิเศษของ Sony เพื่อดึงดูดผู้ชื่นชอบงานอดิเรกและนักเรียนเข้าสู่อุตสาหกรรมเกม
เกม Yaroze มีจำนวนจำกัดมาก เนื่องจาก Sony ไม่ต้องการให้โปรแกรมเขียนโค้ดในห้องนอนแข่งขันกับนักพัฒนาเชิงพาณิชย์ สามารถเล่นได้บน Yarozes อื่นๆ หรือบนดิสก์สาธิตพิเศษเท่านั้น พวกเขาจะต้องพอดีกับ RAM ของระบบทั้งหมดโดยไม่ต้องเข้าถึงซีดีรอม แม้จะมีข้อจำกัดเหล่านี้ Yaroze ก็ได้ส่งเสริมชุมชนนักพัฒนาอินดี้ที่มีความกระตือรือร้น
และตอนนี้ฉันก็มีของตัวเองแล้ว อะไรทำให้ฉันนึกถึง: จริงๆ แล้วการเขียนเกม PlayStation เป็นอย่างไร
นี่เป็นเรื่องเกี่ยวกับวิธีที่ฉันเขียนเกม PSX แบบโฮมบรูว์ง่ายๆ ด้วยตัวเอง โดยใช้ไลบรารีเวอร์ชันโอเพ่นซอร์ส แต่ยังคงทำงานบนฮาร์ดแวร์ดั้งเดิมและเขียนด้วยภาษา C แบบคลาสสิก
ข้ามส่วนนี้
โดยทั่วไปเกม PSX จะเขียนด้วยภาษา C บนเวิร์กสเตชัน Windows 9X devkit อย่างเป็นทางการคือการ์ดส่วนขยาย ISA คู่หนึ่งที่เสียบเข้ากับเมนบอร์ด IBM PC ทั่วไป และบรรจุชิปเซ็ตระบบ PSX ทั้งหมด วิดีโอเอาท์ และ RAM เพิ่มเติม (8mb แทนที่จะเป็น 2mb) สิ่งนี้จัดเตรียมเอาต์พุต TTY และดีบักเกอร์ไปยังเครื่องโฮสต์
คุณอาจเคยได้ยินเกี่ยวกับ PlayStations สีน้ำเงิน สิ่งเหล่านี้มีไว้สำหรับ QA มากกว่าการพัฒนา และเหมือนกับหน่วยขายปลีกยกเว้นว่าสามารถเล่นซีดีรอมที่เบิร์นได้ อย่างไรก็ตาม มีอย่างน้อยหนึ่งบริษัทที่ขายส่วนเสริมพิเศษเพื่อแปลงเป็น devkits:
การออกแบบเป็นมิตรกับนักพัฒนามาก คุณสามารถเล่นเกมของคุณบน CRT ด้วยคอนโทรลเลอร์ปกติในขณะที่ก้าวผ่านจุดพัก GDB บนพีซี Windows 95 ของคุณ โดยอ่านตำราเรียนหนาของฟังก์ชัน C SDK
ตามหลักการแล้ว นักพัฒนา PSX สามารถทำงานได้ทั้งหมดใน C โดย SDK ประกอบด้วยชุดไลบรารี C ที่เรียกว่า PSY-Q และรวมโปรแกรมคอมไพเลอร์ ccpsx
ที่จริงๆ แล้วเป็นเพียงส่วนหน้าของ GCC สิ่งนี้สนับสนุนการเพิ่มประสิทธิภาพหลายอย่าง เช่น การแทรกโค้ดและการคลายลูป แม้ว่าส่วนที่มีความสำคัญด้านประสิทธิภาพยังคงรับประกันการประกอบที่ปรับให้เหมาะสมด้วยมือ
(คุณสามารถอ่านเกี่ยวกับการเพิ่มประสิทธิภาพเหล่านั้นได้ในสไลด์การประชุม SCEE เหล่านี้)
C++ ได้รับการสนับสนุนโดย ccpsx
แต่มีชื่อเสียงในด้านการสร้างโค้ด 'ป่อง' รวมถึงเวลาในการคอมไพล์ที่ช้าลง จริงๆ แล้ว C เป็นภาษากลางของการพัฒนา PSX แต่บางโปรเจ็กต์ก็ใช้ภาษาสคริปต์แบบไดนามิกนอกเหนือจากเอ็นจิ้นพื้นฐาน ตัวอย่างเช่น Metal Gear Solid ใช้ TCL สำหรับการเขียนสคริปต์ระดับ และเกม Final Fantasy ได้พัฒนาไปไกลกว่านั้นและใช้ภาษาไบต์โค้ดของตัวเองสำหรับระบบการต่อสู้ ภาคสนาม และมินิเกม (คุณสามารถเรียนรู้เพิ่มเติมเกี่ยวกับเรื่องนี้ได้ที่นี่)
( สำหรับการอ่านเพิ่มเติมโปรดดูที่ https://www.retroreversing.com/official-playStation-devkit )
ข้ามส่วนนี้
แต่ฉันมาถึงสิ่งนี้จากมุมมองที่แตกต่างออกไปมาก นั่นคือวิศวกรซอฟต์แวร์ในปี 2024 ซึ่งทำงานบนเว็บแอปพลิเคชันเป็นส่วนใหญ่ ประสบการณ์ทางวิชาชีพของฉันเกือบจะเป็นภาษาระดับสูงเช่น JavaScript และ Haskell เท่านั้น; ฉันเคยทำงาน OpenGL และ C++ มาบ้างแล้ว แต่ C++ สมัยใหม่เกือบจะเป็นภาษาที่แตกต่างไปจาก C โดยสิ้นเชิง
ฉันรู้ว่า PSX SDK นั้นมีอยู่ในภาษาอย่าง Rust แต่ฉันอยากสัมผัสกับรสชาติของการเขียนโปรแกรม PSX 'ของจริง' แบบเดียวกับที่เคยทำในทศวรรษ 90 ดังนั้นมันจะเป็น toolchains ที่ทันสมัยและไลบรารีโอเพ่นซอร์ส แต่ใช้ภาษา C ตลอดทาง
เกมดังกล่าวต้องเป็นเกมแบบ 2 มิติที่สามารถสร้างต้นแบบได้ภายในสองสามวัน ฉันตัดสินใจเลือกโคลน Tetris - ฉันคิดว่ามันคงจะซับซ้อน พอ ที่จะสัมผัสประสบการณ์สิ่งที่ฉันต้องการได้
ขั้นตอนแรกคือการสร้างต้นแบบด้วยเทคโนโลยีที่คุ้นเคย สิ่งนี้จะช่วยให้ฉันตอกย้ำการออกแบบพื้นฐานได้ จากนั้นตรรกะก็สามารถแปลทีละน้อยเป็น C ได้
ในฐานะนักพัฒนาเว็บ เทคโนโลยีที่ชัดเจนที่สุดสำหรับการสร้างต้นแบบคือ JavaScript: มันง่าย กระชับ ดีบักง่าย และรองรับ HTML5 <canvas>
กราฟิก API สิ่งต่างๆมารวมกันเร็วมาก
ในเวลาเดียวกัน ฉันก็ระวังว่าคุณลักษณะ JavaScript ระดับสูงกว่านี้จะทำให้พอร์ตยาก สิ่งใดก็ตามที่ใช้คลาสหรือการปิดจะต้องถูกเขียนใหม่ทั้งหมด ดังนั้นฉันจึงระมัดระวังในการจำกัดตัวเองให้อยู่แค่ชุดย่อยของภาษาที่เรียบง่ายตามขั้นตอน
จริงๆ แล้ว ฉันมีเจตนาแอบแฝงในการทำโปรเจ็กต์นี้: มันเป็นข้ออ้างในการเรียนรู้ภาษา C ในที่สุด ภาษาก็ปรากฏอยู่ในใจของฉัน และฉันก็เริ่มพัฒนาปมด้อยโดยที่ฉันไม่รู้
C มีชื่อเสียงที่น่าเกรงขาม และฉันกลัวเรื่องราวสยองขวัญของตัวชี้ห้อย การอ่านที่ไม่ตรงแนว และ segmentation fault
ที่น่าสะพรึงกลัว แม่นยำยิ่งขึ้น: ฉันกังวลว่าหากฉันพยายามเรียน C แต่ล้มเหลว ฉันจะพบว่าจริงๆ แล้วฉันไม่ใช่โปรแกรมเมอร์ที่เก่งนัก
เพื่อให้สิ่งต่าง ๆ ง่ายขึ้น ฉันคิดว่าฉันสามารถใช้ SDL2 เพื่อจัดการอินพุตและกราฟิก และคอมไพล์สำหรับสภาพแวดล้อมเดสก์ท็อปของฉัน (MacOS) นั่นจะทำให้ฉันมีวงจรการสร้าง / ดีบักที่รวดเร็วและทำให้ช่วงการเรียนรู้มีความนุ่มนวลที่สุด
แม้ว่าฉันจะกลัว แต่ฉันพบว่า C สนุกอย่างไม่น่าเชื่อ เร็วมากมัน 'คลิก' สำหรับฉัน คุณเริ่มต้นจากพื้นฐานที่เรียบง่ายมาก - โครงสร้าง, ตัวอักษร, ฟังก์ชัน - และสร้างมันขึ้นมาเป็นเลเยอร์ของนามธรรมเพื่อพบว่าตัวเองนั่งอยู่บนระบบการทำงานทั้งหมดในที่สุด
เกมดังกล่าวใช้เวลาเพียงไม่กี่วันในการย้าย และฉันก็พอใจกับโปรเจ็กต์ True C โปรเจ็กต์แรกของฉันมาก และฉันไม่มี segfault แม้แต่ครั้งเดียว!
SDL รู้สึกยินดีเป็นอย่างยิ่งที่ได้ร่วมงานด้วย แต่มีบางแง่มุมที่ทำให้ฉันต้องจัดสรรหน่วยความจำแบบไดนามิก นี่จะเป็นสิ่งที่ไม่ควรทำบน PlayStation โดยที่ malloc
ที่เคอร์เนล PSX ให้มาทำงานไม่ถูกต้อง และไปป์ไลน์กราฟิกจะเป็นก้าวกระโดดที่ใหญ่กว่า...
เมื่อพูดถึง PlayStation homebrew มีสองตัวเลือกหลักสำหรับ SDK ของคุณ ทั้ง:
มีตัวเลือกอื่นสองสามตัวเช่น C++ Psy-Qo และคุณสามารถละทิ้ง SDK ใด ๆ เพียงเพื่อทำ I / O ที่แมปหน่วยความจำด้วยตัวเอง - แต่ฉันไม่กล้าพอสำหรับสิ่งนั้น
ปัญหาที่ใหญ่ที่สุดของ Psy-Q ก็คือมันยังคงเป็นโค้ดที่เป็นกรรมสิทธิ์ของ Sony แม้จะผ่านไป 30 ปีก็ตาม ตามกฎหมายแล้ว โฮมบรูว์ที่สร้างขึ้นโดยใช้มันมีความเสี่ยง นั่นคือสิ่งที่ทำให้พอร์ทัล 64 จมลง: มันเชื่อมโยงแบบคงที่ libultra
ซึ่งเป็น N64 SDK ที่เป็นกรรมสิทธิ์ของ Nintendo
แต่จริงๆ แล้ว เหตุผลหลักที่ฉันเลือก PSNoobSDK ก็คือว่ามันมีการบันทึกไว้เป็นอย่างดีและติดตั้งง่าย API นั้นคล้ายกับ Psy-Q มาก : จริงๆ แล้วสำหรับฟังก์ชันหลายอย่าง ฉันสามารถอ่านเอกสารอ้างอิงที่มาพร้อมกับ Yaroze ของฉันได้
หากฉันใช้ SDK ที่ไม่ใช่ของแท้ขัดต่อความพิถีพิถันของ PSX ในตัวคุณ อย่าลังเลที่จะเลิกอ่านตอนนี้ด้วยความรังเกียจ
งานแรกของฉันคือสวัสดีชาวโลก: สี่เหลี่ยมสองอันบนพื้นหลังที่มีสี ฟังดูง่ายใช่มั้ย?
ข้ามส่วนนี้
(*บางส่วนเป็นแบบง่าย สำหรับคำแนะนำที่เชื่อถือได้มากกว่านี้ โปรดอ่านบทช่วยสอน PSNoobSDK)
ขั้นแรก ให้คิดว่า PSX VRAM เป็นแคนวาสขนาดใหญ่ 1024 x 512 และพิกเซล 16 บิต โดยรวมแล้วทำให้หน่วยความจำขนาด 1 เมกะไบต์ใช้ร่วมกันโดย framebuffer และ textures เราสามารถเลือกความละเอียดของบัฟเฟอร์เฟรมเอาท์พุตได้ - สูงถึง 640x480 พิกเซลหากเราโลภ - แต่ความละเอียดมากขึ้น = พื้นผิวน้อยลง
เกม PSOne ส่วนใหญ่ (และ... เกมโดยทั่วไป) มีแนวคิดเรื่องการเรนเดอร์แบบบัฟเฟอร์คู่: ในขณะที่เฟรมหนึ่งกำลังเตรียมอยู่ ส่วนอีกเฟรมจะถูกส่งไปยังหน้าจอ ดังนั้นเราจึงจำเป็นต้องจัดสรรบัฟเฟอร์สองเฟรม:
(ตอนนี้คุณจะเห็นแล้วว่าเหตุใด 640x480 จึงใช้งานไม่ได้ - มีพื้นที่ไม่เพียงพอสำหรับบัฟเฟอร์ 480p สองตัว แต่โหมดนี้สามารถใช้ได้กับสิ่งต่างๆ เช่น โลโก้เริ่มต้น PSX ซึ่งไม่ต้องการภาพเคลื่อนไหวมากนัก)
บัฟเฟอร์ (หรือเรียกอีกอย่างว่าสภาพแวดล้อมการแสดงผลและการวาด) จะถูกสลับทุกเฟรม เกม PSX ส่วนใหญ่กำหนดเป้าหมายที่ 30fps (ในอเมริกาเหนือ) แต่การขัดจังหวะ VSync จริงอยู่ที่ 60hz เกมบางเกมสามารถรันได้เต็ม 60 fps - Tekken 3 และ Kula World (Roll Away) อยู่ในใจ - แต่แน่นอนว่าคุณจะต้องเรนเดอร์ในครึ่งเวลา โปรดจำไว้ว่าเรามีพลังการประมวลผลเพียง 33 Mhz
แต่ - กระบวนการวาดทำงานอย่างไร? สิ่งนี้ทำได้โดย GPU แต่ PSX GPU ทำงานแตกต่างอย่างมากจากการ์ดกราฟิกสมัยใหม่ โดยพื้นฐานแล้ว ทุกเฟรมของ GPU จะถูกส่งรายการตามลำดับของ 'แพ็กเก็ต' หรือคำสั่งกราฟิก "วาดรูปสามเหลี่ยมที่นี่" "โหลดพื้นผิวนี้ไปยังรูปสี่เหลี่ยมถัดไป" ฯลฯ
GPU ไม่ทำการแปลงสามมิติ นั่นคืองานของตัวประมวลผลร่วม GTE (Geometry Transform Engine) คำสั่ง GPU แสดงถึงกราฟิก 2D ล้วนๆ ซึ่งควบคุมโดยฮาร์ดแวร์ 3D แล้ว
นั่นหมายความว่าเส้นทางของพิกเซล PSX จะเป็นดังนี้:
ดังนั้นในรหัสเทียม เฟรมลูป PSX (โดยพื้นฐาน) จะเป็นเช่นนี้
FrameBuffer [0, 1]
OrderingTable [0, 1]
id = 1 // flips every frame
loop {
// Game logic
// Construct the next screen by populating the current ordering table
MakeGraphics(OrderingTable[id])
// Wait for last draw to finish; wait for vertical blank
DrawSync()
VSync()
// The other frame has finished drawing in background, so display it
SetDisplay(Framebuffer[!id])
// Start drawing current frame
SetDrawing(Framebuffer[id])
// Send ordering table contents to GPU via DMA
Transfer(OrderingTable[id])
// Flip
id = !id
}
คุณจะเห็นได้ว่าในขณะที่เฟรม 1 อยู่บนหน้าจอ เฟรม 2 ยังคงถูกทาสี และเฟรม 3 อาจยังคงถูก 'สร้าง' โดยตัวโปรแกรมเอง จากนั้นหลังจาก DrawSync / VSync เราก็ส่งเฟรม 2 ไปที่ทีวี และรับเฟรมการวาด GPU 3
ตามที่กล่าวไว้ GPU นั้นเป็นฮาร์ดแวร์ 2 มิติโดยสมบูรณ์ โดยไม่รู้เกี่ยวกับพิกัด z ในพื้นที่ 3 มิติ ไม่มี "z-buffer" ที่จะอธิบายการบดเคี้ยว เช่น วัตถุใดอยู่ข้างหน้าวัตถุอื่น แล้วจะจัดเรียงสิ่งของต่อหน้าผู้อื่นอย่างไร?
วิธีการทำงานคือตารางการเรียงลำดับประกอบด้วยคำสั่งกราฟิกที่เชื่อมโยงแบบย้อนกลับ สิ่งเหล่านี้จะย้อนกลับไปด้านหน้าเพื่อใช้ อัลกอริทึมของจิตรกร
ถ้าให้พูดให้ชัดเจนคือ ตารางการเรียงลำดับจะเป็นรายการที่เชื่อมโยงแบบย้อนกลับ แต่ละรายการมีตัวชี้ไปยังรายการก่อนหน้าในรายการ และเราเพิ่มข้อมูลพื้นฐานโดยการแทรกลงในห่วงโซ่ โดยทั่วไป OT จะถูกเตรียมใช้งานเป็นอาร์เรย์คงที่ โดยแต่ละองค์ประกอบในอาร์เรย์จะแสดงถึง 'ระดับ' หรือเลเยอร์ในจอแสดงผล OT สามารถซ้อนกันเพื่อใช้ฉากที่ซับซ้อนได้
แผนภาพต่อไปนี้ช่วยอธิบาย (ที่มา)
วิธีการนี้ไม่สมบูรณ์แบบ และบางครั้งเรขาคณิต PSX ก็แสดงการตัดภาพแปลกๆ เนื่องจากแต่ละโพลีสามารถอยู่ที่ 'ดัชนี z' เดียวในพื้นที่หน้าจอเท่านั้น แต่จะทำงานได้ดีเพียงพอสำหรับเกมส่วนใหญ่ ปัจจุบันข้อจำกัดดังกล่าวถือเป็นส่วนหนึ่งของเสน่ห์อันโดดเด่นของ PSX
ข้ามส่วนนี้
เราได้พูดคุยกันมากมายเกี่ยวกับทฤษฎี - ในทางปฏิบัติมีลักษณะอย่างไร
ส่วนนี้จะไม่อธิบายโค้ดทั้งหมดทีละบรรทัด แต่น่าจะช่วยให้คุณได้ทดลองแนวคิดกราฟิก PSX หากคุณต้องการดูโค้ดเต็ม ให้ไปที่ hello-psx/main.c
หรือหากคุณไม่ใช่ผู้เขียนโค้ด ก็สามารถข้ามไปได้เลย นี่เป็นเพียงสำหรับนักเทคโนโลยีที่อยากรู้อยากเห็น
สิ่งแรกที่เราต้องการคือโครงสร้างบางอย่างเพื่อเก็บบัฟเฟอร์ของเรา เราจะมี RenderContext
ที่ประกอบด้วย RenderBuffers
สองตัว และ RenderBuffer
แต่ละตัวจะมี:
displayEnv
(ระบุพื้นที่ VRAM ของบัฟเฟอร์การแสดงผลปัจจุบัน)drawEnv
(ระบุพื้นที่ VRAM ของบัฟเฟอร์การดึงปัจจุบัน)orderingTable
(รายการลิงก์ย้อนกลับที่จะมีตัวชี้ไปยังแพ็กเก็ตกราฟิก)primitivesBuffer
(โครงสร้างสำหรับแพ็กเก็ตกราฟิก / คำสั่ง - รวมถึงรูปหลายเหลี่ยมทั้งหมด) #define OT_SIZE 16
#define PACKETS_SIZE 20480
typedef struct {
DISPENV displayEnv ;
DRAWENV drawEnv ;
uint32_t orderingTable [ OT_SIZE ];
uint8_t primitivesBuffer [ PACKETS_SIZE ];
} RenderBuffer ;
typedef struct {
int bufferID ;
uint8_t * p_primitive ; // next primitive
RenderBuffer buffers [ 2 ];
} RenderContext ;
static RenderContext ctx = { 0 };
ทุกเฟรมเราจะกลับค่า bufferID
ซึ่งหมายความว่าเราสามารถทำงานในเฟรมหนึ่งได้อย่างราบรื่นในขณะที่อีกเฟรมหนึ่งกำลังแสดงอยู่ รายละเอียดที่สำคัญคือ p_primitive
จะถูกชี้ไปที่ไบต์ถัดไปอย่างต่อเนื่องใน primitivesBuffer
ปัจจุบัน มี ความจำเป็น ที่จะเพิ่มขึ้นทุกครั้งที่มีการจัดสรรดั้งเดิมและรีเซ็ตเมื่อสิ้นสุดทุกเฟรม
ก่อนอื่นเราต้องตั้งค่าสภาพแวดล้อมการแสดงผลและการวาดในการกำหนดค่าแบบย้อนกลับเพื่อให้ DISP_ENV_1
ใช้ VRAM เดียวกันกับ DRAW_ENV_0
และในทางกลับกัน
// x y width height
SetDefDispEnv ( DISP_ENV_0 , 0 , 0 , 320 , 240 );
SetDefDispEnv ( DISP_ENV_1 , 0 , 240 , 320 , 240 );
SetDefDrawEnv ( DRAW_ENV_0 , 0 , 240 , 320 , 240 );
SetDefDrawEnv ( DRAW_ENV_1 , 0 , 0 , 320 , 240 );
ฉันค่อนข้างจะอึดอัดที่นี่ - แต่จากที่นี่ทุกเฟรมจะเป็นแบบนั้น
while ( 1 ) {
// do game stuff... create graphics for next frame...
// at the end of loop body
// wait for drawing to finish, wait for next vblank interval
DrawSync ( 0 );
VSync ( 0 );
DISPENV * p_dispenv = & ( ctx . buffers [ ctx . bufferID ]. displayEnv );
DRAWENV * p_drawenv = & ( ctx . buffers [ ctx . bufferID ]. drawEnv );
uint32_t * p_ordertable = ctx . buffers [ ctx . bufferID ]. orderingTable ;
// Set display and draw environments
PutDispEnv ( p_dispenv );
PutDrawEnv ( p_drawenv );
// Send ordering table commands to GPU via DMA, starting from the end of the table
DrawOTagEnv ( p_ordertable + OT_SIZE - 1 , p_drawEnv );
// Swap buffers and clear state for next frame
ctx . bufferID ^= 1 ;
ctx . p_primitive = ctx . buffers [ ctx . bufferID ]. primitivesBuffer ;
ClearOTagR ( ctx . buffers [ 0 ]. orderingTable , OT_SIZE );
}
นี่อาจจะต้องรับอะไรมากมาย ไม่ต้องกังวล
หากคุณต้องการเข้าใจสิ่งนี้จริงๆ สิ่งที่ดีที่สุดคือดูที่ hello-psx/main.c
ทุกอย่างมีการแสดงความคิดเห็นในรายละเอียดจำนวนพอสมควร หรืออ่านบทช่วยสอน PSNoobSDK... มันค่อนข้างกระชับและเขียนได้ค่อนข้างชัดเจน
ทีนี้... เราจะวาดของยังไงล่ะ? เราเขียนโครงสร้างลงในบัฟเฟอร์ดั้งเดิมของเรา บัฟเฟอร์นี้พิมพ์เป็นเพียงรายการ chars
ขนาดใหญ่ ดังนั้นเราจึงใส่ลงในรูปร่าง / โครงสร้างคำสั่งของเรา จากนั้นเลื่อนตัวชี้บัฟเฟอร์พื้นฐานโดยใช้ sizeof
:
// Create a tile primitive in the primitive buffer
// We cast p_primitive as a TILE*, so that its char used as the head of the TILE struct
TILE * p_tile = ( TILE * ) p_primitive ;
setTile ( p_tile ); // very very important to call this macro
setXY0 ( p_tile , x , y );
setWH ( p_tile , width , width );
setRGB0 ( p_tile , 252 , 32 , 3 );
// Link into ordering table (z level 2)
int z = 2 ;
addPrim ( ordering_table [ buffer_id ] + z , p_primitive );
// Then advance buffer
ctx . p_primitive += sizeof ( TILE );
เราเพิ่งแทรกสี่เหลี่ยมสีเหลือง! - พยายามเก็บความตื่นเต้นเอาไว้
ข้ามส่วนนี้
ณ จุดนี้ของการเดินทางของฉัน สิ่งที่ฉันมีจริงๆ คือโปรแกรมสาธิต "สวัสดีชาวโลก" พร้อมด้วยกราฟิกพื้นฐานและอินพุตคอนโทรลเลอร์ คุณสามารถเห็นได้จากโค้ดใน hello-psx
ว่าฉันได้บันทึกไว้มากที่สุดเท่าที่จะเป็นไปได้ เพื่อประโยชน์ของตัวเองจริงๆ โปรแกรมการทำงานเป็นก้าวที่ดี แต่ไม่ใช่เกมที่แท้จริง
ถึงเวลา เอาจริงแล้ว
เกมของเราต้องแสดงคะแนน
PSX ไม่ได้ช่วยอะไรคุณมากนักในการแสดงข้อความ มีแบบอักษรดีบัก (แสดงด้านบน) แต่เป็นแบบอักษรพื้นฐานอย่างยิ่ง - สำหรับการพัฒนาและไม่มีอะไรมาก
แต่เราจำเป็นต้องสร้างพื้นผิวแบบอักษรแทน และใช้สิ่งนั้นกับสกินควอด ฉันสร้างแบบอักษร monospace ด้วย https://www.piskelapp.com/ และส่งออกเป็น PNG โปร่งใส:
พื้นผิว PSX จะถูกจัดเก็บในรูปแบบที่เรียกว่า TIM ไฟล์ TIM แต่ละไฟล์ประกอบด้วย:
เนื่องจากตำแหน่ง VRAM ของพื้นผิวถูก 'อบเข้า' ในไฟล์ TIM คุณจึงต้องมีเครื่องมือในการจัดการตำแหน่งพื้นผิวของคุณ ฉันแนะนำhttps://github.com/Lameguy64/TIMeditสำหรับสิ่งนี้
จากนั้นเราก็แค่มีฟังก์ชันสำหรับสกินกลุ่ม quads โดยมีการชดเชย UV ตามค่า ASCII แต่ละค่า
เราต้องการพื้นที่สำหรับวางชิ้นส่วนต่างๆ มันคงจะง่ายที่จะใช้สี่เหลี่ยมสีขาวน่าเบื่อสำหรับสิ่งนี้ แต่ฉันต้องการอะไรที่ให้ความรู้สึกมากกว่า... PlayStation
อินเทอร์เฟซผู้ใช้ของเรากำลังรวมเข้าด้วยกัน แล้วชิ้นส่วนล่ะ?
มาถึงการออกแบบภาพที่สำคัญบางประการแล้ว ตามหลักการแล้ว อิฐแต่ละก้อนควรจะมีขอบที่คมชัดและเป็นเงา เราทำสิ่งนี้ด้วยสามเหลี่ยมสองอันและรูปสี่เหลี่ยม:
ที่ความละเอียดดั้งเดิม 1x เอฟเฟกต์จะชัดเจนน้อยลง แต่ก็ยังดูดีและเป็นก้อน:
ในต้นแบบแรกของเกม ฉันใช้ระบบการหมุนแบบไร้เดียงสาซึ่งจะพลิกบล็อก 90 องศาที่จุดกึ่งกลาง ปรากฎว่านั่นไม่ใช่แนวทางที่ดีจริงๆ เพราะมันทำให้บล็อก 'โยกเยก' โดยเลื่อนขึ้นและลงในขณะที่พวกมันหมุน:
แต่การหมุนจะถูกฮาร์ดโค้ดให้เป็น 'ดี' แทนที่จะเป็น 'แม่นยำ' ชิ้นส่วนถูกกำหนดไว้ภายในตารางเซลล์ขนาด 4x4 และแต่ละเซลล์สามารถเติมหรือไม่เติมก็ได้ มีการหมุน 4 รอบ ดังนั้น: การหมุนสามารถเป็นอาร์เรย์ของตัวเลข 16 บิตสี่ตัวได้ ซึ่งมีลักษณะเช่นนี้:
/**
* Example: T block
*
* As a grid:
*
* .X.. -> 0100
* XXX. -> 1110
* .... -> 0000
* .... -> 0000
*
* binary = 0b0100111000000000
* hexadecimal = 0x4E00
*
*/
typedef int16_t ShapeBits ;
static ShapeBits shapeHexes [ 8 ][ 4 ] = {
{ 0 }, // NONE
{ 0x0F00 , 0x4444 , 0x0F00 , 0x4444 }, // I
{ 0xE200 , 0x44C0 , 0x8E00 , 0xC880 }, // J
{ 0xE800 , 0xC440 , 0x2E00 , 0x88C0 }, // L
{ 0xCC00 , 0xCC00 , 0xCC00 , 0xCC00 }, // O
{ 0x6C00 , 0x8C40 , 0x6C00 , 0x8C40 }, // S
{ 0x0E40 , 0x4C40 , 0x4E00 , 0x4640 }, // T
{ 0x4C80 , 0xC600 , 0x4C80 , 0xC600 }, // Z
};
การแยกค่าของเซลล์เป็นเพียงกรณีของการมาสก์บิตอย่างง่าย:
#define GRID_BIT_OFFSET 0x8000;
int blocks_getShapeBit ( ShapeBits s , int y , int x ) {
int mask = GRID_BIT_OFFSET >> (( y * 4 ) + x );
return s & mask ;
}
สิ่งต่าง ๆ กำลังมารวมกันในขณะนี้ด้วยแรงผลักดัน
เมื่อถึงจุดนี้ ฉันประสบปัญหา: การสุ่ม ชิ้นส่วนต่างๆ จะต้องปรากฏแบบสุ่มเพื่อให้เกมน่าเล่น แต่การสุ่มนั้นทำได้ยากในคอมพิวเตอร์ ในเวอร์ชัน MacOS ของฉัน ฉันสามารถ 'เริ่มต้น' ตัวสร้างตัวเลขสุ่มด้วยนาฬิการะบบได้ แต่ PSX ไม่มีนาฬิกาภายใน
วิธีแก้ปัญหาที่หลายๆ เกมใช้คือการทำให้ผู้เล่นสร้างเมล็ดพันธุ์ขึ้นมา เกมดังกล่าวจะแสดงหน้าจอสแปลชหรือชื่อพร้อมข้อความเช่น "กดเริ่มเพื่อเริ่ม" จากนั้นระบบจะนำจังหวะเวลาจากการกดปุ่มนั้นเพื่อสร้างเมล็ดพันธุ์
ฉันสร้าง 'กราฟิก' โดยการประกาศ int32
ที่เข้ารหัสแบบไบนารีโดยที่แต่ละ 1
บิตจะเป็น 'พิกเซล' ในแถวของอิฐ:
สิ่งที่อยากได้คือให้เส้นค่อยๆ หายไปในการมองเห็น ก่อนอื่นฉันต้องการฟังก์ชั่นที่จะ 'ติดตาม' ได้อย่างมีประสิทธิภาพว่ามีการโทรกี่ครั้ง C ทำให้สิ่งนี้ง่ายขึ้นด้วยคีย์เวิร์ด static
- หากใช้ภายในฟังก์ชัน ที่อยู่หน่วยความจำและเนื้อหาเดียวกันจะถูกนำมาใช้ซ้ำในการเรียกใช้ครั้งถัดไป
จากนั้นภายในฟังก์ชันเดียวกันนี้จะมีการวนซ้ำที่ผ่านค่า x/y ของ 'ตาราง' และตัดสินใจว่ามีขีดเพียงพอที่จะแสดง 'พิกเซล' หรือไม่:
void ui_renderTitleScreen () {
static int32_t titleTimer = 0 ;
titleTimer ++ ;
// For every 2 times (2 frames) this function is called, ticks increases by 1
int32_t ticks = titleTimer / 2 ;
// Dissolve-in the title blocks
for ( int y = 0 ; y < 5 ; y ++ ) {
for ( int x = 0 ; x < 22 ; x ++ ) {
int matrixPosition = ( y * 22 ) + x ;
if ( matrixPosition > ticks ) {
break ; // because this 'pixel' of the display is not to be displayed yet
}
int32_t titleLine = titlePattern [ y ];
int32_t bitMask = titleMask >> x ;
if ( titleLine & bitMask ) { // there is a 'pixel' at this location to show
ui_renderBlock ( /* skip boring details */ );
}
}
}
}
ตอนนี้เรา เกือบจะถึง ที่นั่นแล้ว
เกม PSX แบบคลาสสิกบูตได้ในสองขั้นตอน: ขั้นแรกคือหน้าจอ Sony Computer Entertainment จากนั้นจึงเปิดโลโก้ PSX แต่ถ้าเราคอมไพล์และรันโปรเจ็กต์ hello-psx
มันก็จะไม่เป็นเช่นนั้น หน้าจอที่สองเป็นเพียงสีดำ ทำไมเป็นเช่นนั้น?
SCE Splash มาจาก BIOS เช่นเดียวกับเสียงการบูต PSX แต่โลโก้ที่มีชื่อเสียงนั้นจริงๆ แล้วเป็นส่วนหนึ่งของข้อมูลลิขสิทธิ์ดิสก์ มีไว้เพื่อทำหน้าที่เสมือน 'ตราประทับแห่งความถูกต้อง' ดังนั้นใครก็ตามที่ละเมิดลิขสิทธิ์เกมก็กำลังคัดลอก IP ของ Sony รวมถึงของผู้จัดพิมพ์ด้วย สิ่งนี้ทำให้ Sony มีเครื่องมือทางกฎหมายมากขึ้นในการปราบปรามการละเมิดลิขสิทธิ์ซอฟต์แวร์
หากเราต้องการให้เกม ของเรา แสดงโลโก้ เราจำเป็นต้องจัดเตรียมไฟล์ลิขสิทธิ์ที่แยกมาจาก ISO แต่เพื่อประโยชน์ด้านลิขสิทธิ์ เราจึงต้อง . .gitignore
ไฟล์ดังกล่าว
< license file = " ${PROJECT_SOURCE_DIR}/license_data.dat " />
ตกลง. ตอนนี้ เราพร้อมแล้ว
ทั้งหมดนี้เริ่มต้นด้วยการซื้อแรงกระตุ้น Yaroze PlayStation สีดำของฉัน น่าแปลกที่มันคงไม่เล่นเกมของฉันจริงๆ เพราะมันยังมีฮาร์ดแวร์ป้องกันการละเมิดลิขสิทธิ์อยู่ ฉันไม่คิดว่าจะติดตั้ง modchip บนชิ้นส่วนอันล้ำค่าของประวัติ PSX - ไม่ใช่ด้วยทักษะการบัดกรีของฉัน
แต่ฉันกลับต้องตามหา PlayStation สีเทาที่ได้รับการดัดแปลง ซึ่งยังมีไดรฟ์ที่ดีอยู่ ฉันคิดว่าจุดประสงค์ของโปรเจ็กต์ของฉันคือการเขียนเกม PlayStation อย่างแท้จริง และนั่นหมายถึงการใช้ PlayStation ที่แท้จริง
ฉันยังต้องหาสื่อที่เหมาะสมด้วย เลเซอร์ PSX ค่อนข้างพิถีพิถัน และ CD-R สมัยใหม่มีแนวโน้มที่จะสะท้อนแสงน้อยกว่าดิสก์แบบกดมาก ความพยายามครั้งแรกของฉันกับซีดีเรื่องราวของร้านขายของชำเป็นการเสียเวลา และภายในเวลาประมาณสองสัปดาห์ ฉันได้สร้างที่รองแก้วจำนวนมาก
นี่เป็นช่วงเวลาที่มืดมน ฉันทำมาขนาดนี้แล้ว แต่กลับล้มเหลวใน การเขียนซีดีหรือ เปล่า?
หลังจากผ่านไปหลายสัปดาห์ ฉันก็ได้รับหุ้นพิเศษของ JVC Taiyo Yuden จากสิ่งที่ฉันอ่านมา สิ่งเหล่านี้ค่อนข้างเชี่ยวชาญ และโดยทั่วไปจะใช้ในงานอุตสาหกรรม ฉันเผาแผ่นดิสก์แผ่นแรกลงในจานและคาดว่าจะแย่ที่สุด
นี่คือช่วงเวลาแห่งความจริง:
ลำดับการบูต PlayStation ดังขึ้นจากลำโพงมอนิเตอร์ตัวจิ๋วของฉัน และโลโก้ "PS" แบบคลาสสิกก็กระเด็นไปทั่วหน้าจอด้วยความละเอียดที่มีชีวิตชีวา 640 x 480 BIOS พบ บางสิ่ง บนแผ่นดิสก์นั้นอย่างชัดเจน แต่อาจมีข้อผิดพลาดมากมายหลังจากจุดนี้ หน้าจอกลายเป็นสีดำ และฉันก็ปวดหูเพราะเสียง คลิก-คลิก-คลิก ของข้อผิดพลาดของไดรฟ์
ในทางกลับกัน สี่เหลี่ยมสีเล็กๆ ก็เริ่มกระพริบตาเข้ามาจากความมืด พวกเขาสะกดคำทีละบรรทัด: NOTRIS
จากนั้น: PRESS START TO BEGIN
ข้อความกวักมือเรียกฉัน จะเกิดอะไรขึ้นต่อไป?
แน่นอนว่าเป็นเกม Tetris ทำไมฉันถึงประหลาดใจ? จริงๆ แล้วการเขียนเกม PlayStation ของคุณเองด้วยภาษา C นั้นง่ายมาก สิ่งที่คุณต้องทำก็คือไม่ทำผิดพลาดใดๆ เลย นั่นคือคอมพิวเตอร์สำหรับคุณ โดยเฉพาะอุปกรณ์ระดับต่ำ มันแข็งและคมและสวยงาม คอมพิวเตอร์สมัยใหม่มีขอบที่นุ่มนวลกว่า แต่สิ่งสำคัญยังคงไม่เปลี่ยนแปลง
พวกเราที่รักคอมพิวเตอร์จำเป็นต้องมีบางอย่างผิดปกติกับเรา ความไร้เหตุผลต่อเหตุผลของเรา วิธีที่จะปฏิเสธหลักฐานทั้งหมดที่เห็นด้วยตาและหูของเราว่ากล่องซิลิคอนที่ไม่เป็นมิตรนั้นตายแล้วและไม่ยอมใคร และแฟชั่นด้วยเครื่องจักรอันชาญฉลาดทำให้ภาพลวงตานั้นมีชีวิต