Nortis(이전 Notris)는 최신 도구를 사용하여 C로 작성된 자체 제작 PSX 게임입니다. 원래 하드웨어에서 완벽하게 재생할 수 있으며 PSNoobSDK로 구동됩니다.
여기에서 PSX 코드베이스를 확인하세요.
작년에 나는 희귀한 검은색 PlayStation 1을 손에 넣었습니다. 이것은 Net Yaroze라고 불리며 일반 PSX 타이틀은 물론 홈브류 게임도 플레이할 수 있는 특수 콘솔입니다. 이는 취미생활자와 학생들을 게임 산업으로 끌어들이기 위한 특별한 Sony 프로젝트의 일부였습니다.
Yaroze 게임은 매우 제한적이었습니다. Sony는 침실 코더가 상용 개발자와 경쟁하는 것을 원하지 않았기 때문입니다. 다른 Yarozes나 특별 데모 디스크에서만 재생할 수 있습니다. CD-ROM에 접근하지 않고도 시스템 RAM에 완전히 들어맞아야 했습니다. 이러한 제한에도 불구하고 Yaroze는 열정적인 인디 개발자 커뮤니티를 육성했습니다.
그리고 이제 나는 내 자신을 갖게 되었습니다. 그러다가 생각하게 됐어요. PlayStation 게임을 만드는 것이 실제로 어땠나요?
이것은 라이브러리의 오픈 소스 버전을 사용하지만 여전히 원본 하드웨어에서 실행되고 클래식 C로 작성된 간단한 홈브류 PSX 게임을 직접 작성하는 방법에 대한 것입니다.
이 섹션을 건너뛰세요
PSX 게임은 일반적으로 Windows 9X 워크스테이션에서 C로 작성되었습니다. 공식 개발 키트는 일반적인 IBM PC 마더보드에 장착되고 전체 PSX 시스템 칩셋, 비디오 출력 및 추가 RAM(2MB 대신 8MB)을 포함하는 한 쌍의 ISA 확장 카드였습니다. 이는 호스트 시스템에 TTY 및 디버거 출력을 제공했습니다.
파란색 PlayStation에 대해 들어보셨을 것입니다. 이는 개발보다는 QA를 위한 것이며 구운 CD-ROM을 재생할 수 있다는 점을 제외하면 소매점과 동일합니다. 그러나 적어도 한 회사에서는 이를 개발 키트로 변환하기 위해 특수 애드온을 판매했습니다.
디자인은 매우 개발자 친화적이었습니다. Windows 95 PC에서 GDB 중단점을 거치면서 일반 컨트롤러를 사용하여 CRT에서 게임을 플레이할 수 있으며 C SDK 기능에 대한 두꺼운 교과서를 훑어볼 수도 있습니다.
원칙적으로 PSX 개발자는 전적으로 C로 작업할 수 있습니다. SDK는 PSY-Q라는 C 라이브러리 세트로 구성되었으며 실제로는 GCC의 프런트엔드에 불과한 컴파일러 프로그램 ccpsx
포함했습니다. 이는 코드 인라이닝 및 루프 언롤링과 같은 다양한 최적화를 지원했지만 성능이 중요한 섹션은 여전히 수동으로 최적화된 어셈블리를 보장했습니다.
(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와 거의 완전히 다른 언어입니다.
저는 Rust와 같은 언어를 위한 PSX SDK가 존재한다는 것을 알고 있었지만, 90년대에 실행되었던 방식인 '진짜' PSX 프로그래밍의 맛을 경험하고 싶었습니다. 따라서 최신 툴체인과 오픈 소스 라이브러리가 될 것이지만 C는 끝까지 포함됩니다.
게임은 며칠 안에 프로토타입을 제작할 수 있는 2D여야 했습니다. 나는 테트리스 복제품을 선택했습니다. 그것이 내가 원하는 것을 경험할 수 있을 만큼 충분히 복잡할 것이라고 생각했습니다.
첫 번째 단계는 친숙한 기술로 프로토타입을 제작하는 것이었습니다. 이를 통해 기본 설계를 확정한 다음 논리를 단편적으로 C로 변환할 수 있습니다.
웹 개발자로서 프로토타이핑을 위한 가장 확실한 기술은 JavaScript였습니다. JavaScript는 간단하고 간결하며 디버깅하기 쉽고 HTML5 <canvas>
그래픽 API를 자랑합니다. 일이 아주 빨리 이루어졌어
동시에, 더 높은 수준의 JavaScript 기능을 이식하기 어려울 것이라는 점을 경계했습니다. 클래스나 클로저를 사용하는 모든 것은 완전히 다시 작성되어야 하므로 언어의 단순하고 절차적인 하위 집합으로 제한하려고 주의했습니다.
사실 저는 이 프로젝트를 시작하게 된 은밀한 동기가 있었습니다. 그것은 마침내 C를 배우기 위한 핑계였습니다. 그 언어는 제 마음속에 크게 어렴풋이 떠올랐고 저는 그것을 모른다는 것에 대한 열등감이 생기기 시작했습니다.
C는 위협적인 평판을 갖고 있으며 포인터가 매달려 있고 읽기가 잘못 정렬되었으며 segmentation fault
있다는 끔찍한 이야기가 두려웠습니다. 좀 더 정확하게 말하자면, C를 배우려고 했다가 실패하면 결국 내가 실제로 아주 좋은 프로그래머가 아니라는 사실을 알게 될까 봐 걱정되었습니다.
작업을 쉽게 하기 위해 SDL2를 사용하여 입력과 그래픽을 처리하고 데스크탑 환경(MacOS)에 맞게 컴파일할 수 있다고 생각했습니다. 그러면 빌드/디버그 주기가 빨라지고 학습 곡선이 최대한 완만해집니다.
두려움에도 불구하고 나는 C가 엄청나게 재미있다는 것을 알았습니다. 매우 빠르게 '클릭'되었습니다. 매우 간단한 기본 요소(구조체, 문자, 함수)에서 시작하여 이를 추상화 계층으로 구축하여 결국 전체 작업 시스템 위에 앉게 됩니다.
게임을 포팅하는 데는 며칠 밖에 걸리지 않았으며, 저는 첫 번째 진정한 C 프로젝트에 매우 만족했습니다. 그리고 나는 단 하나의 세그폴트도 없었습니다!
SDL을 사용하는 것은 즐거웠지만 메모리를 동적으로 할당해야 하는 몇 가지 측면이 있었습니다. PSX 커널에서 제공하는 malloc
제대로 작동하지 않는 PlayStation에서는 이는 절대 안 되는 일입니다. 그리고 그래픽 파이프라인은 훨씬 더 큰 도약이 될 것입니다...
PlayStation 홈브류의 경우 SDK에 대한 두 가지 주요 선택 사항이 있습니다. 어느 하나:
C++ Psy-Qo 와 같은 몇 가지 다른 옵션이 있으며 메모리 매핑 I/O를 직접 수행하기 위해 SDK를 포기할 수도 있습니다. 하지만 저는 그럴 용기가 없었습니다.
Psy-Q의 가장 큰 문제는 30년이 지난 지금도 여전히 Sony 독점 코드라는 것입니다. 법적으로 이를 사용하여 만든 홈브류는 위험에 처해 있습니다. 이것이 Portal64 디메이크를 망친 이유입니다. Nintendo의 독점 N64 SDK인 libultra
정적으로 연결했습니다.
하지만 솔직히 말해서 제가 PSNoobSDK를 선택한 주된 이유는 문서화가 매우 잘 되어 있고 설정이 간단하기 때문입니다. API는 Psy-Q와 매우 유사합니다. 사실 많은 기능에 대해 Yaroze와 함께 제공되는 인쇄된 참조 자료를 참고할 수 있었습니다.
정품이 아닌 SDK를 사용하는 것이 PSX 순수주의자의 마음을 상하게 한다면 이제 혐오감을 느끼며 읽기를 중단하시기 바랍니다.
나의 첫 번째 작업은 일종의 Hello World였습니다. 색상이 지정된 배경에 두 개의 사각형이 있는 것이었습니다. 간단하게 들리죠?
이 섹션을 건너뛰세요
(*이 중 일부는 단순화되었습니다. 보다 권위 있는 가이드를 보려면 PSNoobSDK 튜토리얼을 읽어보세요.)
우선 PSX VRAM을 16비트 픽셀의 큰 1024 x 512 캔버스로 생각하십시오. 전체적으로 프레임 버퍼와 텍스처가 공유하는 메모리는 1MB입니다. 출력 프레임 버퍼의 해상도를 선택할 수 있습니다(심지어 욕심이 난다면 최대 640x480 픽셀까지). 하지만 해상도가 높을수록 텍스처 수가 줄어듭니다.
대부분의 PSOne 게임(및 일반 게임)에는 듀얼 버퍼 렌더링 개념이 있습니다. 한 프레임이 준비되는 동안 다른 프레임은 화면으로 전송됩니다. 따라서 두 개의 프레임 버퍼를 할당해야 합니다.
(이제 640x480이 실용적이지 않은 이유를 알 수 있습니다. 두 개의 480p 버퍼를 위한 공간이 충분하지 않습니다. 그러나 이 모드는 애니메이션이 많이 필요하지 않은 PSX 시작 로고와 같은 항목에서 사용할 수 있습니다.)
버퍼(표시 및 그리기 환경이라고도 함)는 프레임마다 교체됩니다. 대부분의 PSX 게임은 30fps(북미)를 목표로 하지만 실제 VSync 인터럽트는 60hz에서 발생합니다. 일부 게임은 최대 60fps로 실행됩니다. Tekken 3 및 Kula World(Roll Away)가 떠오릅니다. 하지만 분명히 렌더링 시간을 절반으로 단축해야 합니다. 우리의 처리 능력은 33Mhz밖에 없다는 것을 기억하세요.
하지만 - 그리는 과정은 어떻게 진행되나요? 이는 GPU에 의해 수행되지만 PSX GPU는 최신 그래픽 카드와 매우 다르게 작동합니다. 기본적으로 모든 프레임마다 GPU에는 순서가 지정된 그래픽 '패킷' 또는 명령 목록이 전송됩니다. "여기에 삼각형을 그립니다", "이 텍스처를 로드하여 다음 쿼드를 스킨합니다" 등.
GPU는 3D 변환을 수행하지 않습니다. 이것이 GTE(Geometry Transform Engine) 보조 프로세서의 작업입니다. GPU 명령은 이미 3D 하드웨어로 조작된 순수한 2D 그래픽을 나타냅니다.
즉, 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를 TV로 보내고 GPU 그리기 프레임 3을 가져옵니다.
앞서 언급했듯이 GPU는 완전한 2D 하드웨어이므로 3D 공간의 z 좌표를 알지 못합니다. 폐색을 설명하는 "z-버퍼"가 없습니다. 즉, 어떤 객체가 다른 객체 앞에 있는지를 나타냅니다. 그렇다면 항목이 다른 항목보다 먼저 정렬되는 방법은 무엇입니까?
작동 방식은 순서 테이블이 역방향으로 연결된 그래픽 명령 체인으로 구성된다는 것입니다. 이것들은 페인터의 알고리즘을 구현하기 위해 뒤에서 앞으로 순회됩니다.
정확하게 말하면 주문 테이블은 역방향 연결 목록입니다. 각 항목에는 목록의 이전 항목에 대한 포인터가 있으며 이를 체인에 삽입하여 기본 요소를 추가합니다. 일반적으로 OT는 고정 배열로 초기화되며, 배열의 각 요소는 디스플레이의 '레벨' 또는 레이어를 나타냅니다. 복잡한 장면을 구현하기 위해 OT를 중첩할 수 있습니다.
다음 다이어그램은 이를 설명하는 데 도움이 됩니다(출처).
이 접근 방식은 완벽하지 않으며 각 폴리는 화면 공간에서 단일 'z 인덱스'에만 있을 수 있기 때문에 PSX 형상이 이상한 클리핑을 표시하는 경우가 있지만 대부분의 게임에서는 충분히 잘 작동합니다. 요즘에는 이러한 한계가 PSX의 독특한 매력 중 하나로 여겨지고 있습니다.
이 섹션을 건너뛰세요
우리는 많은 이론에 대해 이야기했습니다. 실제로는 어떤 모습일까요?
이 섹션에서는 모든 코드를 한 줄씩 다루지는 않지만 PSX 그래픽 개념에 대한 맛보기를 제공합니다. 전체 코드를 보려면 hello-psx/main.c
로 이동하세요.
또는 코더가 아닌 경우에는 건너뛰어도 됩니다. 이것은 호기심이 많은 기술자를 위한 것입니다.
가장 먼저 필요한 것은 버퍼를 포함할 구조체입니다. 두 개의 RenderBuffers
포함하는 RenderContext
있고 각 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
DRAW_ENV_0
과 동일한 VRAM을 사용하고 그 반대의 경우도 가능하도록 역 구성으로 디스플레이 및 그리기 환경을 설정해야 합니다.
// 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
의 큰 ole 목록으로 유형이 지정되므로 모양/명령 구조체에 캐스팅한 다음 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 값을 기반으로 한 UV 오프셋을 사용하여 여러 개의 쿼드를 스킨하는 기능이 있습니다.
조각이 들어갈 공간이 필요합니다. 지루한 흰색 직사각형을 사용하면 쉬울 것 같지만 좀 더 느낌이 나는 것을 원했습니다... PlayStation
우리의 사용자 인터페이스가 하나로 합쳐지고 있습니다. 조각은 어떻습니까?
이제 몇 가지 중요한 시각적 디자인이 나옵니다. 이상적으로 각 벽돌은 선명하고 음영 처리된 가장자리로 시각적으로 구별되어야 합니다. 두 개의 삼각형과 하나의 쿼드를 사용하여 이 작업을 수행합니다.
1x 기본 해상도에서는 효과가 덜 명확하지만 여전히 멋지고 두툼해 보입니다.
내 게임의 첫 번째 프로토타입에서 나는 실제로 블록을 중심점에서 90도 뒤집는 완전한 순진한 회전 시스템을 구현했습니다. 이는 블록이 회전하면서 위아래로 움직이면서 '흔들리게' 되기 때문에 실제로는 좋은 접근 방식이 아닌 것으로 나타났습니다.
대신, 회전은 '정확함' 대신 '좋음'으로 하드코딩됩니다. 조각은 4x4 셀의 격자 내에서 정의되며 각 셀은 채워지거나 채워지지 않을 수 있습니다. 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에는 내부 시계가 없습니다.
대신에 많은 게임이 취하는 해결책은 플레이어가 시드를 생성하도록 하는 것입니다. 게임은 '시작하려면 시작을 누르세요'와 같은 텍스트가 포함된 스플래시 또는 타이틀 화면을 표시한 다음 해당 버튼을 누르는 시점에서 시드를 생성하는 타이밍을 가져옵니다.
나는 각 1
비트가 벽돌 행의 '픽셀'이 되는 바이너리 인코딩 int32
s를 선언하여 '그래픽'을 만들었습니다.
내가 원했던 것은 선들이 점차 시야에 녹아드는 것이었습니다. 먼저 호출 횟수를 효과적으로 '추적'하는 함수가 필요했습니다. 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 스플래시는 PSX 부팅 사운드와 마찬가지로 BIOS에서 나오지만 유명한 로고는 실제로 디스크 라이센스 데이터의 일부입니다. 이는 '정품 인증' 역할을 하기 위해 존재합니다. 따라서 게임을 불법 복제하는 사람은 누구나 게시자의 IP는 물론 Sony의 IP도 복사하는 것입니다. 이로 인해 Sony는 소프트웨어 불법 복제를 단속할 수 있는 더 많은 법적 수단을 갖게 되었습니다.
게임 에 로고를 표시하려면 ISO에서 추출한 라이센스 파일을 제공해야 하지만 저작권을 위해 이를 .gitignore
해야 합니다.
< license file = " ${PROJECT_SOURCE_DIR}/license_data.dat " />
좋아요. 이제 준비가 되었습니다.
이 모든 것은 내 검은색 Yaroze PlayStation이라는 충동구매로 시작되었습니다. 아이러니하게도 그것은 불법 복제 방지 하드웨어를 여전히 보유하고 있기 때문에 실제로 내 게임을 플레이하지 않을 것입니다. 나는 내 납땜 기술이 아닌 PSX 역사의 귀중한 조각에 모드칩을 설치하는 것을 좋아하지 않았습니다.
대신, 나는 여전히 괜찮은 드라이브를 가지고 있는 개조된 회색 PlayStation을 추적해야 했습니다. 나는 내 프로젝트의 핵심이 진정한 PlayStation 게임을 작성하는 것이며 이는 진정한 PlayStation을 사용하는 것을 의미한다고 생각했습니다.
또한 적절한 미디어를 찾아야 했습니다. PSX 레이저는 매우 까다로우며 최신 CD-R은 압축 디스크보다 반사율이 훨씬 낮은 경향이 있습니다. 식료품 이야기 CD를 사용한 첫 번째 시도는 시간 낭비였으며 약 2주에 걸쳐 많은 컵받침을 만들었습니다.
이것은 어두운 순간이었습니다. 이렇게까지 했는데 CD 굽기 에 실패했던 걸까요?
몇 주 후에 나는 특별한 JVC Taiyo Yuden 주식을 손에 넣었습니다. 내가 읽을 수 있는 바로는 이 제품은 매우 전문적이며 일반적으로 산업 응용 분야에 사용됩니다. 나는 플래터에 첫 번째 디스크를 태웠고 최악의 상황을 예상했습니다.
이것이 진실의 순간이었습니다.
내 작은 모니터 스피커에서 PlayStation 부팅 시퀀스가 울려 퍼지고 클래식 "PS" 로고가 생생한 640 x 480 해상도로 화면 전체에 튀었습니다. BIOS는 분명히 해당 디스크에서 무엇인가를 발견했지만 이 시점 이후에는 많은 문제가 발생할 수 있습니다. 화면이 검게 변했고 드라이브 오류를 알리는 클릭-클릭-클릭 소리에 귀가 긴장되었습니다.
대신, 어둠 속에서 작은 색깔의 사각형이 하나씩 깜박이기 시작했습니다. 한 줄씩 그들은 NOTRIS
라는 단어를 철자했습니다. 그런 다음 PRESS START TO BEGIN
. 문자가 나에게 손짓했다. 다음에는 무슨 일이 일어날까요?
물론 테트리스 게임이죠. 나는 왜 놀랐는가? C로 자신만의 PlayStation 게임을 작성하는 것은 실제로 매우 간단합니다. 필요한 것은 어떤 실수도 하지 않는 것뿐입니다 . 그것은 특히 낮은 수준의 컴퓨팅입니다. 단단하고 날카로우며 아름답습니다. 최신 컴퓨팅은 가장자리가 더 부드러워졌지만 필수 요소는 변경되지 않았습니다.
컴퓨터를 사랑하는 우리는 우리에게 뭔가 약간 잘못된 점, 즉 합리성에 대한 비합리성, 적대적인 실리콘 상자가 죽었고 굳건하다는 우리 눈과 귀의 모든 증거를 거부하는 방법을 가지고 있어야 합니다. 그리고 교활한 기계에 의한 패션은 그것이 살아 있다는 환상입니다.