Nortis (旧 Notris) は、最新のツールを使用して C で書かれた自作の PSX ゲームです。オリジナルのハードウェアで完全にプレイ可能で、PSNoobSDK を搭載しています。
PSX コードベースはここでご覧ください。
去年、私は珍しい黒いPlayStation 1を手に入れました。これはネットやろうぜと呼ばれるもので、通常のPSXタイトルだけでなく自作ゲームもプレイできる特別なコンソールです。これは、愛好家や学生をゲーム業界に参加させるためのソニーの特別プロジェクトの一環でした。
ソニーは寝室のプログラマーが商業開発者と競合することを望まなかったため、Yaroze ゲームは非常に限定的でした。これらは他の Yarozes または特別なデモ ディスクでのみ再生できました。これらは、CD-ROM にアクセスせずにシステム RAM 内に完全に収まる必要がありました。これらの制限にもかかわらず、Yaroze はインディー開発者の情熱的なコミュニティを育成しました。
そして今、私は自分のものを持っていました。それで私はこう考えました。PlayStation ゲームを書くというのは、実際にはどのような感じだったのでしょうか?
これは、ライブラリのオープンソース バージョンを使用しながらもオリジナルのハードウェアで実行され、クラシック C で書かれたシンプルな自作 PSX ゲームを私自身がどのように書いたかについてです。
このセクションをスキップしてください
PSX ゲームは通常、Windows 9X ワークステーション上で C で書かれていました。公式開発キットは、一般的な IBM PC マザーボードにスロットに差し込まれる 1 対の ISA 拡張カードで、PSX システム チップセット全体、ビデオ出力、および追加の RAM (2MB ではなく 8MB) が含まれていました。これにより、TTY とデバッガーの出力がホスト マシンに提供されました。
青いPlayStationについて聞いたことがあるかもしれません。これらは開発ではなく QA 用であり、書き込まれた CD-ROM を再生できる点を除けば小売店と同じです。ただし、少なくとも 1 社が開発キットに変換するための特別なアドオンを販売しました。
デザインは非常に開発者に優しいものでした。 Windows 95 PC で GDB ブレークポイントをステップ実行しながら、C SDK 関数の分厚い教科書をめくりながら、通常のコントローラを使用して CRT 上でゲームをプレイできます。
原則として、PSX 開発者は完全に C で作業できます。SDK は PSY-Q と呼ばれる C ライブラリのセットで構成され、実際には GCC の単なるフロントエンドであるコンパイラ プログラムccpsx
が含まれていました。これにより、コードのインライン化やループの展開などのさまざまな最適化がサポートされましたが、パフォーマンスが重要なセクションでは依然として手動で最適化されたアセンブリが保証されていました。
(これらの最適化については、SCEE カンファレンスのスライドで読むことができます)。
C++ はccpsx
によってサポートされていましたが、「肥大化した」コードを生成し、コンパイル時間が遅いという評判がありました。実際、C は PSX 開発の共通言語でしたが、一部のプロジェクトでは、ベース エンジンに加えて動的スクリプト言語を使用していました。たとえば、メタルギア ソリッドはレベル スクリプトに TCL を使用しました。そして、ファイナル ファンタジーゲームはさらに進んで、戦闘、フィールド、ミニゲーム システムに独自のバイトコード言語を実装しました。 (これについて詳しくは、こちらをご覧ください)。
(詳細については、https://www.retroreversing.com/official-playStation-devkit をご覧ください)
このセクションをスキップしてください
しかし、私はまったく異なる視点からこれにたどり着きました。つまり、2024 年のソフトウェア エンジニアで、主に Web アプリケーションに取り組んでいたのです。私の職業上の経験は、JavaScript や Haskell などの高級言語のみでした。私は OpenGL の仕事と C++ を少しやったことがありましたが、最新の C++ は C とはほぼ完全に異なる言語です。
Rust のような言語用の PSX SDK が存在することは知っていましたが、90 年代に行われていた「本物の」PSX プログラミングの味を体験したかったのです。つまり、最新のツールチェーンとオープンソース ライブラリになりますが、最後まで C です。
ゲームは数日でプロトタイプを作成できる 2D である必要がありました。私はテトリスのクローンに落ち着きました。それは私がやりたいことを体験するには十分複雑だと考えました。
最初のステップは、使い慣れたテクノロジーでプロトタイプを構築することでした。これにより、基本的な設計を明確にすることができ、その後、ロジックを部分的に C に翻訳することができます。
Web 開発者にとって、プロトタイピングのための最も明白なテクノロジは JavaScript でした。JavaScript はシンプルで簡潔でデバッグが容易で、HTML5 <canvas>
グラフィックス API をサポートしています。物事はとても早くまとまった
同時に、より高度な JavaScript 機能の移植は困難になるのではないかと警戒していました。クラスやクロージャを使用するものはすべて完全に書き直す必要があるため、言語の単純な手続き型サブセットに限定するよう注意しました。
さて、実は私にはこのプロジェクトに取り組む裏の動機がありました。ついに C 言語を学ぶ口実だったのです。C 言語が私の心の中に大きく浮かび上がり、それを知らないことに対して劣等感を抱き始めていました。
C には恐ろしい評判があり、私はぶら下がったポインタ、位置ずれした読み取り、恐ろしいsegmentation fault
などの恐ろしい話を恐れていました。より正確に言えば、C を学ぼうとして失敗したら、結局のところ自分はあまり優れたプログラマーではなかったことがわかるのではないかと心配していました。
作業を簡単にするために、SDL2 を使用して入力とグラフィックを処理し、デスクトップ環境 (MacOS) 用にコンパイルできると考えました。そうすれば、ビルド/デバッグのサイクルが速くなり、学習曲線が可能な限り緩やかになります。
不安にもかかわらず、C は信じられないほど楽しいことがわかりました。すぐに「ピンと来た」のです。非常に単純なプリミティブ (構造体、文字、関数) から始めて、それらを抽象化のレイヤーに構築して、最終的には動作するシステム全体の上に座ることになります。
ゲームの移植には数日しかかかりませんでした。私は初めての本格的な C プロジェクトに非常に満足しました。そして、セグメンテーション違反は一度もありませんでした。
SDL は楽しく作業できましたが、メモリを動的に割り当てる必要がある側面がいくつかありました。 PSX カーネルによって提供されるmalloc
が正しく機能しない PlayStation では、これは不可能です。そして、グラフィックス パイプラインはさらに大きな飛躍となるでしょう...
PlayStation の自作に関しては、SDK には主に 2 つの選択肢があります。どちらか:
C++ Psy-Qoのような他のオプションがいくつかあり、メモリ マップド I/O を自分で行うためだけに SDK を省略することもできますが、私にはその勇気がありませんでした。
Psy-Q の最大の問題は、30 年経った今でも、ソニーの独自コードであることです。法的には、それを使用して構築された自作は危険にさらされます。それが Portal64 のリメイクを沈めた原因です。それは、任天堂独自の N64 SDK であるlibultra
静的にリンクしていました。
しかし正直に言うと、私が PSNoobSDK を選んだ主な理由は、ドキュメントが非常に充実していて、セットアップが簡単だったことです。 API は Psy-Q に非常に似ています。実際、多くの関数については、Yaroze に付属の印刷されたリファレンスを参照するだけで済みます。
私が非正規の SDK を使用していることがあなたの PSX 純粋主義者を傷つけるなら、嫌悪感を抱いて今すぐ読むのをやめてください。
私の最初のタスクは、Hello World のようなもので、色付きの背景に 2 つの正方形がありました。簡単そうに聞こえますよね?
このセクションをスキップしてください
(*これの一部は簡略化されています。より信頼できるガイドについては、PSNoobSDK チュートリアルをお読みください)
まず、PSX VRAM を 16 ビット ピクセルの 1024 x 512 の大きなキャンバスとして考えてください。合計すると、フレームバッファとテクスチャによって 1 MB のメモリが共有されます。出力フレームバッファの解像度は選択できますが、欲を言えば最大 640x480 ピクセルまで選択できますが、解像度が高いほどテクスチャが少なくなります。
ほとんどの PSOne ゲーム (そして...ゲーム全般) にはデュアル バッファリング レンダリングの概念があります。つまり、1 つのフレームが準備されている間に、もう 1 つのフレームが画面に送信されます。したがって、2 つのフレーム バッファを割り当てる必要があります。
(これで、640x480 が実用的でない理由がわかりました。2 つの 480p バッファーに十分なスペースがありません。しかし、このモードは、アニメーションをあまり必要としない PSX 起動ロゴなどで使用できます。)
バッファ (表示環境および描画環境とも呼ばれます) はフレームごとに交換されます。ほとんどの PSX ゲームは 30fps (北米) をターゲットとしていますが、実際の VSync 割り込みは 60hz で発生します。 Tekken 3 や Kula World (Roll Away) など、フル 60 fps で実行できるゲームもありますが、その場合は明らかに半分の時間でレンダリングする必要があります。処理能力が 33 Mhz しかないことに注意してください。
しかし、描画プロセスはどのように行われるのでしょうか?これは 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 の独特の魅力の一部であると考えられています。
このセクションをスキップしてください
理論についてはたくさん話しましたが、実際にはどうなるでしょうか?
このセクションでは、すべてのコードを 1 行ずつ説明するわけではありませんが、PSX グラフィックスの概念を味わうことができます。完全なコードを確認したい場合は、 hello-psx/main.c
にアクセスしてください。
プログラマーではない場合は、読み飛ばしていただいてもかまいません。これは、興味のある技術者向けです。
最初に必要なのは、バッファを含むいくつかの構造体です。 2 つの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
の大きなリストとして型付けされているため、シェイプ/コマンド構造体にキャストし、 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
ユーザーインターフェイスは統合されつつあります。駒はどうですか?
次に、重要なビジュアル デザインをいくつか紹介します。理想的には、各レンガは、シャープで陰影のあるエッジで視覚的に区別できる必要があります。 2 つの三角形と 1 つの四角形を使用してこれを行います。
ネイティブ解像度が 1 倍では、エフェクトはそれほど鮮明ではありませんが、それでも見た目は素晴らしく、分厚く見えます。
私のゲームの最初のプロトタイプでは、実際に中心点でブロックを 90 度反転させる完全な単純な回転システムを実装しました。これは実際には優れたアプローチではないことがわかりました。ブロックが回転するときに上下に移動して「ぐらつく」ことになるからです。
代わりに、回転は「正確」ではなく「適切」になるようにハードコードされています。 Piece は 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 には内部クロックがありません。
代わりに、多くのゲームがとる解決策は、プレイヤーにシードを作成させることです。ゲームでは、「スタートを押して開始」のようなテキストを含むスプラッシュ画面またはタイトル画面が表示され、そのボタンが押されたタイミングからシードが作成されます。
バイナリエンコードされた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 ゲームは 2 段階で起動します。最初に Sony Computer Entertainment 画面が表示され、次に PSX ロゴが表示されます。しかし、 hello-psx
プロジェクトをコンパイルして実行すると、そうではありません。セカンドスクリーンは真っ黒です。何故ですか?
SCE スプラッシュは PSX 起動音と同様に BIOS から発生しますが、有名なロゴは実際にはディスク ライセンス データの一部です。これは「信頼性のシール」のように機能するために存在するため、ゲームを海賊版にしている人は誰でも、出版社の IP だけでなくSony のIP もコピーしていることになります。これにより、ソニーはソフトウェア著作権侵害を取り締まるためのより多くの法的手段を手に入れることができた。
ゲームにロゴを表示したい場合は、ISO から抽出したライセンス ファイルを提供する必要がありますが、著作権のため、 .gitignore
必要があります。
< license file = " ${PROJECT_SOURCE_DIR}/license_data.dat " />
わかった。これで準備は完了です。
ことの始まりは、黒のやろうぜプレイステーションを衝動買いしたことから始まりました。皮肉なことに、著作権侵害対策ハードウェアがまだ残っているため、実際には私のゲームをプレイすることはありません。 PSX の歴史のこのような貴重な部分に Modchip をインストールする気はありませんでした。私のはんだ付けスキルではありませんでした。
代わりに、まだまともなドライブが付いている、改造された灰色の PlayStation を追跡する必要がありました。私は、自分のプロジェクトの目的は真のPlayStation ゲームを書くことであり、それは真のPlayStation を使用することを意味すると考えました。
また、適切なメディアを見つける必要もありました。 PSX レーザーは非常にこだわりがあり、最新の CD-R はプレス ディスクよりも反射率がはるかに低い傾向があります。食料品のストーリー CD を最初に作成しようとしたのは時間の無駄でしたが、約 2 週間かけてたくさんのコースターを作成しました。
これは暗い瞬間でした。ここまでやっても、 CD の書き込みに失敗しただけでしょうか?
数週間後、私は特別なJVC太陽誘電株を手に入れました。私が読んだ限り、これらは非常に専門的であり、通常は産業用途で使用されています。最初のディスクをプラッタに書き込みましたが、最悪の事態を予想していました。
これが真実の瞬間でした。
PlayStation の起動シーケンスが小さなモニター スピーカーから鳴り響き、クラシックな「PS」ロゴが 640 x 480 の鮮やかな解像度で画面全体に飛び散りました。 BIOS は明らかにそのディスク上で何かを見つけましたが、この時点以降は多くのことが失敗する可能性があります。画面が真っ暗になり、ドライブエラーの明らかなカチッ、カチッという音に耳を澄ました。
その代わりに、小さな色の四角が一つずつ暗闇から点滅し始めました。彼らは一行ずつNOTRIS
という単語を綴りました。次に、 PRESS START TO BEGIN
。その文章が私を手招きした。次に何が起こるでしょうか?
もちろんテトリスのゲームです。なぜ私は驚いたのですか? C で独自の PlayStation ゲームを作成するのは実際には非常に簡単です。必要なのは、間違いを犯さないことだけです。それは、特に低レベルのものを自動的に計算することです。硬くて鋭くて美しい。現代のコンピューティングにはソフトなエッジがありますが、本質は変わっていません。
コンピューターを愛する私たちは、自分たちに少し間違った何か、つまり私たちの理性に対する不合理さ、敵対的なシリコンの箱が死んで不屈であるという私たちの目と耳のすべての証拠を否定する方法を持っている必要があります。そして、狡猾な機械によってファッションが生きているかのように錯覚します。