Nortis (Notris سابقًا) هي لعبة PSX محلية الصنع، مكتوبة بلغة C باستخدام الأدوات الحديثة. يمكن تشغيلها بالكامل على الأجهزة الأصلية ويتم تشغيلها بواسطة PSNoobSDK.
عرض قاعدة بيانات PSX هنا.
في العام الماضي حصلت على جهاز PlayStation 1 أسود نادر. يُطلق عليه اسم Net Yaroze وهو وحدة تحكم خاصة يمكنها لعب ألعاب البيرة المنزلية بالإضافة إلى ألعاب PSX العادية. لقد كان جزءًا من مشروع خاص لشركة Sony لجذب الهواة والطلاب إلى صناعة الألعاب.
كانت ألعاب Yaroze محدودة للغاية، حيث لم ترغب شركة Sony في أن يتنافس مبرمجو غرف النوم مع المطورين التجاريين. لا يمكن تشغيلها إلا على أقراص Yarozes الأخرى أو على أقراص تجريبية خاصة. كان عليهم أن يتناسبوا بالكامل مع ذاكرة الوصول العشوائي للنظام دون الوصول إلى القرص المضغوط. على الرغم من هذه القيود، عزز Yaroze مجتمعًا متحمسًا من المطورين المستقلين.
والآن كان لدي خاصتي. الأمر الذي جعلني أفكر: كيف كان الأمر في الواقع عند كتابة لعبة PlayStation؟
يتعلق الأمر بكيفية كتابتي لعبة PSX منزلية بسيطة بنفسي، باستخدام إصدار مفتوح المصدر من المكتبات ولكن لا يزال يعمل على الأجهزة الأصلية ومكتوبة بلغة C الكلاسيكية.
تخطي هذا القسم
تمت كتابة ألعاب PSX عادةً بلغة C على محطات عمل Windows 9X. كان طقم التطوير الرسمي عبارة عن زوج من بطاقات التوسعة ISA التي تم إدخالها في اللوحة الأم الشائعة لأجهزة الكمبيوتر الشخصية من نوع IBM وتحتوي على مجموعة شرائح نظام PSX بالكامل ومخرج الفيديو وذاكرة الوصول العشوائي الإضافية (8 ميجابايت بدلاً من 2 ميجابايت). أدى هذا إلى توفير إخراج TTY ومصحح الأخطاء إلى الجهاز المضيف.
ربما سمعت عن أجهزة PlayStation الزرقاء. كانت هذه مخصصة لضمان الجودة وليس للتطوير وهي مماثلة لوحدات البيع بالتجزئة باستثناء أنها يمكنها تشغيل الأقراص المضغوطة المحروقة. ومع ذلك، باعت شركة واحدة على الأقل ملحقًا خاصًا لتحويلها إلى مجموعات تطوير:
كان التصميم صديقًا جدًا للمطورين. يمكنك لعب لعبتك على CRT باستخدام وحدات تحكم عادية أثناء التنقل عبر نقاط توقف GDB على جهاز الكمبيوتر الذي يعمل بنظام التشغيل Windows 95، وتصفح كتاب مدرسي سميك لوظائف C SDK.
من حيث المبدأ، يمكن لمطور PSX العمل بالكامل بلغة C. وتتكون SDK من مجموعة من مكتبات C تسمى PSY-Q، وتضمنت برنامج مترجم ccpsx
الذي كان في الواقع مجرد واجهة أمامية لـGC. وقد دعم هذا مجموعة من التحسينات، مثل تضمين التعليمات البرمجية وفتح الحلقة، على الرغم من أن الأقسام الهامة للأداء لا تزال تتطلب التجميع المحسّن يدويًا.
(يمكنك القراءة عن هذه التحسينات في شرائح مؤتمر 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 "الحقيقية"، بالطريقة التي كانت تتم بها في التسعينيات. لذلك ستكون سلاسل الأدوات الحديثة والمكتبات مفتوحة المصدر، ولكن لغة C على طول الطريق.
يجب أن تكون اللعبة ثنائية الأبعاد ويمكن تصميم نموذج أولي لها في غضون يومين. لقد استقريت على استنساخ Tetris - اعتقدت أن ذلك سيكون معقدًا بما يكفي لتجربة ما أردت.
كانت الخطوة الأولى هي بناء نموذج أولي باستخدام تقنية مألوفة. وهذا من شأنه أن يسمح لي بتثبيت التصميم الأساسي، ومن ثم يمكن ترجمة المنطق بشكل مجزأ إلى لغة C.
كمطور ويب، كانت التكنولوجيا الأكثر وضوحًا للنماذج الأولية هي JavaScript: فهي بسيطة وموجزة وسهلة التصحيح، وهي مزودة بواجهة برمجة التطبيقات الرسومية HTML5 <canvas>
. جاءت الأمور معًا بسرعة كبيرة
في الوقت نفسه، كنت حذرًا من صعوبة نقل المزيد من ميزات JavaScript عالية المستوى. أي شيء يستخدم الفئات أو الإغلاقات سيحتاج إلى إعادة كتابته بالكامل، لذلك كنت حريصًا على تقييد نفسي بمجموعة فرعية إجرائية بسيطة من اللغة.
الآن، كان لدي في الواقع دافع خفي للقيام بهذا المشروع: لقد كان ذريعة لتعلم لغة C أخيرًا. كانت اللغة تلوح في ذهني وبدأت في تطوير عقدة النقص بسبب عدم معرفتي بها.
تتمتع لغة C بسمعة مخيفة وكنت أخشى قصص الرعب المتعلقة بالمؤشرات المتدلية والقراءات المنحرفة وخطأ segmentation fault
المخيف. بتعبير أدق: كنت قلقًا من أنني إذا حاولت تعلم لغة C، وفشلت، فسوف أكتشف أنني لم أكن في الواقع مبرمجًا جيدًا على الإطلاق.
لتسهيل الأمور، اعتقدت أنه يمكنني استخدام SDL2 للتعامل مع المدخلات والرسومات، والتجميع لبيئة سطح المكتب (MacOS). وهذا من شأنه أن يمنحني دورة بناء/تصحيح سريعة ويجعل منحنى التعلم لطيفًا قدر الإمكان.
على الرغم من مخاوفي، وجدت لغة C ممتعة بشكل لا يصدق. بسرعة كبيرة "نقرت" بالنسبة لي. تبدأ من البدائيات البسيطة جدًا - البنيات، والأحرف، والوظائف - ثم تبنيها في طبقات من التجريد لتجد نفسك في النهاية جالسًا على قمة نظام عمل بأكمله.
استغرق إصدار اللعبة بضعة أيام فقط، وكنت راضيًا جدًا عن أول مشروع C حقيقي لي. ولم يكن لدي أي خطأ واحد!
كان من دواعي سروري العمل مع SDL، ولكن كانت هناك بعض الجوانب التي تطلبت مني تخصيص الذاكرة ديناميكيًا. سيكون هذا أمرًا محظورًا على PlayStation، حيث لا يعمل malloc
الذي توفره نواة PSX بشكل صحيح. وسيكون خط أنابيب الرسومات قفزة أكبر …
عندما يتعلق الأمر بـ PlayStation homebrew، هناك خياران رئيسيان لـ SDK الخاص بك. أيضاً:
هناك خياران آخران مثل C++ Psy-Qo ، ويمكنك حتى التخلي عن أي SDK فقط للقيام بالإدخال/الإخراج المعين للذاكرة بنفسك - لكنني لم أكن شجاعًا بما يكفي للقيام بذلك.
أكبر مشكلة في Psy-Q هي أنه لا يزال رمزًا مملوكًا لشركة Sony، حتى بعد مرور 30 عامًا. من الناحية القانونية، فإن أي مشروب بيرة منزلي مصنوع به يكون في خطر. هذا هو ما أدى إلى غرق Portal64: فهو مرتبط بشكل ثابت libultra
، وهو N64 SDK المملوك لشركة Nintendo.
ولكن لنكون صادقين، السبب الرئيسي الذي جعلني أختار PSNoobSDK هو أنه موثق جيدًا وسهل الإعداد. واجهة برمجة التطبيقات (API) تشبه إلى حد كبير Psy-Q: في الواقع، بالنسبة للعديد من الوظائف، كان بإمكاني فقط الرجوع إلى المراجع المطبوعة المرفقة مع Yaroze الخاص بي.
إذا كان استخدامي لمجموعة SDK غير أصلية يسيء إلى أنصار PSX بداخلك، فلا تتردد في التوقف عن القراءة الآن بسبب الاشمئزاز.
كانت مهمتي الأولى عبارة عن عالم مرحب به: مربعان على خلفية ملونة. يبدو بسيطا، أليس كذلك؟
تخطي هذا القسم
(* تم تبسيط بعض هذا. للحصول على دليل أكثر موثوقية، اقرأ البرنامج التعليمي PSNoobSDK)
في البداية، فكر في PSX VRAM كلوحة كبيرة مقاس 1024 × 512 بكسل و16 بت. إجماليًا، يتم مشاركة 1 ميغا بايت من الذاكرة بواسطة الإطارات المؤقتة والأنسجة. يمكننا اختيار دقة المخزن المؤقت لإطارات الإخراج - حتى ما يصل إلى 640 × 480 بكسل إذا كنا جشعين - ولكن دقة أكبر = مواد أقل.
تحتوي معظم ألعاب PSOne (و... الألعاب بشكل عام) على مفهوم العرض المزدوج: أثناء إعداد إطار واحد، يتم إرسال الإطار الآخر إلى الشاشة. لذلك نحن بحاجة إلى تخصيص اثنين من المخازن المؤقتة للإطار:
(الآن يمكنك أن ترى لماذا 640 × 480 غير عملي - لا توجد مساحة كافية لمخزنين مؤقتين بدقة 480 بكسل. ولكن يمكن استخدام هذا الوضع بواسطة أشياء مثل شعار بدء تشغيل PSX، والذي لا يحتاج إلى الكثير من الرسوم المتحركة)
يتم تبديل المخازن المؤقتة (المشار إليها بالتناوب كبيئات العرض والرسم) في كل إطار. تستهدف معظم ألعاب PSX معدل 30 إطارًا في الثانية (في أمريكا الشمالية) ولكن مقاطعة VSync الفعلية تأتي عند 60 هرتز. تتمكن بعض الألعاب من العمل بمعدل 60 إطارًا في الثانية بالكامل - تتبادر إلى ذهني لعبة Tekken 3 وKula World (Roll Away) - ولكن من الواضح أنك تحتاج إلى العرض في نصف الوقت. تذكر أن لدينا فقط 33 ميجا هرتز من قوة المعالجة.
ولكن - كيف تتم عملية الرسم؟ ويتم ذلك عن طريق وحدة معالجة الرسومات، لكن وحدة معالجة الرسومات PSX تعمل بشكل مختلف تمامًا عن بطاقة الرسومات الحديثة. بشكل أساسي، يتم إرسال قائمة مرتبة من "حزم" الرسومات أو الأوامر لكل إطار إلى وحدة معالجة الرسومات. "ارسم مثلثًا هنا"، "قم بتحميل هذا النسيج لتغطية المربع التالي"، وما إلى ذلك.
لا تقوم وحدة معالجة الرسومات بتحويلات ثلاثية الأبعاد؛ هذه هي مهمة المعالج المساعد GTE (محرك التحويل الهندسي). تمثل أوامر وحدة معالجة الرسومات رسومات ثنائية الأبعاد بحتة، ويتم التعامل معها بالفعل بواسطة أجهزة ثلاثية الأبعاد.
وهذا يعني أن مسار بكسل 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.
كما ذكرنا سابقًا، فإن وحدة معالجة الرسومات عبارة عن قطعة ثنائية الأبعاد تمامًا من الأجهزة، ولا تعرف إحداثيات z في الفضاء ثلاثي الأبعاد. لا يوجد "z-buffer" لوصف الانسدادات - أي الكائنات الموجودة أمام الآخرين. فكيف يتم فرز العناصر أمام الآخرين؟
الطريقة التي يعمل بها هي أن جدول الترتيب يشتمل على سلسلة من أوامر الرسومات المرتبطة عكسيًا. يتم اجتيازها من الخلف إلى الأمام لتنفيذ خوارزمية الرسام .
على وجه الدقة، جدول الترتيب عبارة عن قائمة مرتبطة عكسيًا. يحتوي كل عنصر على مؤشر للعنصر السابق في القائمة، ونقوم بإضافة العناصر الأولية عن طريق إدراجها في السلسلة. بشكل عام، تتم تهيئة OTs كمصفوفة ثابتة، حيث يمثل كل عنصر في المصفوفة "مستوى" أو طبقة في الشاشة. يمكن تداخل OTs لتنفيذ المشاهد المعقدة.
الرسم البياني التالي يساعد على شرح ذلك (المصدر)
هذا الأسلوب ليس مثاليًا وفي بعض الأحيان تُظهر هندسة 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 World"، مزود بالرسومات الأساسية ومدخلات وحدة التحكم. يمكنك أن ترى من الكود الموجود في hello-psx
أنني كنت أقوم بتوثيقه قدر الإمكان، وذلك لمصلحتي الخاصة حقًا. وكان برنامج العمل خطوة إيجابية ولكنه ليس لعبة حقيقية.
لقد حان الوقت لنصبح واقعيين .
لعبتنا تحتاج إلى إظهار النتيجة.
لا يوفر لك PSX الكثير في طريقة عرض النص. يوجد خط تصحيح (كما هو موضح أعلاه) ولكنه أساسي للغاية - للتطوير وليس أكثر من ذلك.
بدلاً من ذلك، نحن بحاجة إلى إنشاء نسيج الخط، واستخدام ذلك لتسوية الأسطح الرباعية. لقد قمت بإنشاء خط أحادي المسافة باستخدام https://www.piskelapp.com/ وقمت بتصديره بتنسيق PNG شفاف:
يتم تخزين مواد PSX بتنسيق يسمى TIM. يشتمل كل ملف TIM على:
نظرًا لأن موقع VRAM الخاص بالنسيج "مخبأ في" ملف TIM، فأنت بحاجة إلى أداة لإدارة مواقع النسيج الخاص بك. أوصي بـ https://github.com/Lameguy64/TIMedit لهذا الغرض.
من هناك لدينا فقط وظيفة لسلخ مجموعة من الرباعيات، مع إزاحة الأشعة فوق البنفسجية بناءً على كل قيمة ASCII.
نحن بحاجة إلى مساحة لتناسب القطع. سيكون من السهل استخدام مستطيل أبيض ممل لهذا الغرض، لكنني أردت شيئًا يبدو أكثر... PlayStation
واجهة المستخدم الخاصة بنا تجتمع معًا. ماذا عن القطع؟
الآن يأتي بعض التصميم المرئي المهم. من الناحية المثالية، يجب أن يكون كل لبنة مميزًا بصريًا بحواف حادة ومظللة. نحن نفعل ذلك مع مثلثين ورباعي:
عند الدقة الأصلية 1x، سيكون التأثير أقل وضوحًا، لكنه لا يزال يبدو لطيفًا ومكتنزًا:
في النموذج الأولي للعبتي، قمت بتطبيق نظام دوران ساذج كامل، والذي من شأنه أن يقلب الكتلة بمقدار 90 درجة عند نقطة مركزية. لقد اتضح أن هذا ليس أسلوبًا رائعًا في الواقع، لأنه يتسبب في "تمايل" الكتل، وتتحرك لأعلى ولأسفل أثناء دورانها:
بدلاً من ذلك، تم ترميز عمليات التدوير لتكون "لطيفة" بدلاً من "دقيقة". يتم تعريف القطعة ضمن شبكة من الخلايا 4×4، ويمكن ملء كل خلية أو عدم ملؤها. هناك 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 من BIOS، كما هو الحال مع صوت تشغيل PSX، لكن الشعار الشهير هو في الواقع جزء من بيانات ترخيص القرص. إنها موجودة لتكون بمثابة "ختم الأصالة" - لذا فإن أي شخص يقوم بقرصنة لعبة ما يقوم بنسخ عنوان IP الخاص بشركة Sony وكذلك الناشر. وقد أعطى هذا لشركة سوني المزيد من الأدوات القانونية للقضاء على قرصنة البرامج.
إذا أردنا أن تظهر لعبتنا الشعار، فنحن بحاجة إلى توفير ملف ترخيص مستخرج من ISO، ولكن من أجل حقوق الطبع والنشر يتعين علينا .gitignore
.
< license file = " ${PROJECT_SOURCE_DIR}/license_data.dat " />
تمام. الآن نحن جاهزون.
بدأ كل هذا بشراء جهاز PlayStation Yaroze الأسود الخاص بي. ومن المفارقات أنها لن تلعب لعبتي بالفعل لأنها لا تزال تمتلك أجهزة مكافحة القرصنة. لم أكن أرغب في تثبيت شريحة تعديل على قطعة لا تقدر بثمن من تاريخ PSX - وليس بمهاراتي في اللحام.
وبدلاً من ذلك، اضطررت إلى العثور على جهاز PlayStation رمادي مُعدل، وهو جهاز لا يزال يتمتع بمحرك جيد. اعتقدت أن الهدف من مشروعي هو كتابة لعبة PlayStation حقيقية ، وهذا يعني استخدام PlayStation حقيقي .
كان عليّ أيضًا العثور على الوسائط المناسبة. يعد ليزر PSX صعب الإرضاء تمامًا وتميل الأقراص المضغوطة الحديثة إلى أن تكون أقل انعكاسًا بكثير من الأقراص المضغوطة. كانت محاولاتي الأولى باستخدام الأقراص المضغوطة الخاصة بقصص البقالة مضيعة للوقت، وعلى مدار أسبوعين تقريبًا قمت بإنشاء الكثير من الأكواب.
كانت هذه لحظة مظلمة. هل قطعت كل هذا الطريق، ثم فشلت في حرق القرص المضغوط ؟
وبعد عدة أسابيع حصلت على بعض أسهم JVC Taiyo Yuden الخاصة. مما استطعت قراءته، كانت هذه الأجهزة متخصصة تمامًا، وتستخدم عادةً في التطبيقات الصناعية. لقد قمت بحرق القرص الأول في الطبق وتوقعت الأسوأ.
وكانت هذه لحظة الحقيقة:
انطلق تسلسل تشغيل PlayStation من مكبرات الصوت الصغيرة على الشاشة، كما انتشر شعار "PS" الكلاسيكي عبر الشاشة بدقة نابضة بالحياة تبلغ 640 × 480. من الواضح أن BIOS قد عثر على شيء ما على هذا القرص، ولكن قد يفشل الكثير بعد هذه النقطة. سقطت الشاشة باللون الأسود وأجهدت أذني بسبب النقر فوق النقر فوق خطأ في محرك الأقراص.
بدلاً من ذلك، بدأت المربعات الملونة الصغيرة تومض من الظلام واحدة تلو الأخرى. لقد كتبوا سطرًا سطرًا كلمة: NOTRIS
. ثم: PRESS START TO BEGIN
. النص أشار لي. ماذا سيحدث بعد ذلك؟
لعبة تتريس، بطبيعة الحال. لماذا فوجئت؟ إن كتابة لعبة PlayStation الخاصة بك بلغة C أمر بسيط للغاية: كل ما يتطلبه الأمر هو عدم ارتكاب أي أخطاء على الإطلاق . هذا هو الحوسبة بالنسبة لك، وخاصة الأشياء ذات المستوى المنخفض. إنها صعبة وحادة وجميلة. تتمتع الحوسبة الحديثة بحواف أكثر ليونة ولكن الأساسيات لم تتغير.
أولئك منا الذين يحبون أجهزة الكمبيوتر يحتاجون إلى أن يكون لديهم شيء خاطئ قليلاً فينا، وهو عدم عقلانية عقلانيتنا، وطريقة لإنكار كل الأدلة التي تراها أعيننا وآذاننا على أن صندوق السيليكون المعادي قد مات وأصبح لا ينضب. وتشكل بالآلات الماكرة الوهم الذي تعيشه.