# OAM(スプライト属性メモリ) ゲームボーイのPPUは、最大40個のスプライトを8x8または8x16のピクセルで表示することができます。 ただし、ハードウェアの制限により、1スキャンラインあたり10個のスプライトしか表示できないようになっています。 スプライトのタイルデータはBGタイルと同じフォーマットですが、タイルデータはVRAMの`$8000..8FFF`から取得され、タイル番号(タイルインデックス)は符号なしのインデックス(0~255)となっています。 スプライトについての情報は、OAMという`$FE00..FE9F`のメモリ領域に存在します。 40個のエントリはそれぞれ4バイトで構成されており、以下のような意味を持ちます。
Byte 7 6 5 4 3 2 1 0
0 Y-coordinate - 16
1 X-coordinate - 8
2 Tile index
3 Priority Y-flip X-flip Palette 0 0 0 0
## 0️⃣ 0バイト目 - Y座標 ``` byte0 = スプライトの画面上のY座標 + 16 ``` となっているため、スプライトのY座標(`yCoord`)は `yCoord = byte0 - 16`となります。 なぜ16が加算されているのかは不明です。 ``` Y=0: スプライトは完全に隠れる Y=2: 8x16のスプライトの下2行だけが画面最上部に表示される Y=16: 8x16のスプライトが画面最上部から全部表示される Y=144: 8x16のスプライトは画面最下部に全部表示される Y=152: 8x8のスプライトは画面最下部に全部表示される Y=154: 8x8のスプライトの上6行だけが画面最下部に表示される Y=160: スプライトが完全に隠れる ``` ## 1️⃣ 1バイト目 - X座標 ``` byte1 = スプライトの画面上のX座標 + 8 ``` となっているため、スプライトのX座標(`xCoord`)は `xCoord = byte1 - 8`となります。 これは、スプライトの幅(xのサイズ)が常に8であることを除いて、上記の例と同様に動作します。 スプライトが非表示になるような値(`X=0`,`X>=168`)はスプライトを非表示にしますが、スプライトの描画の優先準備には影響を与えます。そのため、スキャンラインあたりのスプライト数が10個に制限されているせいで、優先順位の低い他のスプライトが取り残される可能性があります。 よってスプライトを非表示にしたい場合は、X座標ではなく、Y座標の値を使って非表示にすることをおすすめします。 ## 2️⃣ 2バイト目 - タイル番号 **8x8の場合** スプライトのサイズが`8x8`(LCDCのbit2=0)なら、このバイトは単純にスプライトのタイル番号(\$00~\$FF)を表しています。 このタイル番号によってVRAMのタイルデータ領域`$8000..8FFF`からタイルデータが選択されます。CGBの場合はVRAMのバンクがバンク0とバンク1の2つありますが、バンクはOAMの3バイト目のbit3で指定します。 **8x16の場合** スプライトのサイズが`8x16`(LCDCのbit2=1)なら、`$8000..8FFF`のメモリ領域は、8x8のタイルの連続として解釈され、2つのタイルごとにスプライトが形成されます。 このモードでは、このバイト(OAMの2バイト目)は、スプライトの上のタイル(y=0~7)のタイル番号を指定します。そしてタイル番号+1が下のタイルのタイル番号となります。 これは、ハードウェアによって強制されます。タイル番号の最下位ビットは無視されます。 つまり、このバイトの値が`NN`とすると、上のタイルは`NN & $FE`,下のタイルは`NN | $01`となります。 ## 3️⃣ 3バイト目 - アトリビュート ``` Bit7 背景/ウィンドウのOBJに対する優先度 (0=OBJが優先、 1=背景/ウィンドウが優先) Bit6 Y反転 (0=反転しない、 1=垂直方向に反転) Bit5 X反転 (0=反転しない、 1=水平方向に反転) Bit4 パレット番号(DMGのみ) (0=OBP0、 1=OBP1) Bit3 VRAMバンク(CGBのみ) (0=バンク0、 1=バンク1) Bit2-0 パレット番号(CGBのみ) (OBP0 から OBP7) ``` ## 🖌 OAMへの書き込み OAMへデータを書き込む際には、まず通常のRAM(通常はWRAM)のバッファにデータを書き込み、次にそのバッファを[DMA転送](./dma.md)を使ってOAMにコピーする方法を取ることをお勧めします。 通常のアクセスでOAM領域に直接データを書き込むことも可能ですが、これはHBlankおよびVBlank期間中にしか機能しないためお勧めしません。 ## 🧿 OBJの優先度 OBJに関しては、**2種類**の優先順位があります。 1つ目は、選択優先度`Selection priority`と呼ばれるもので、あるスキャンライン上に10個以上のOBJがある場合、どのOBJを無視するかを定義するものです。 2つ目は、描画優先度`Drawing priority`と呼ばれるもので、あるOBJが重なったときに、どのOBJを一番上に表示するかを決めるものです。(ゲームボーイは2Dゲーム機なので、Z座標はありません) ### 選択優先度(Selection priority) 各スキャンラインのモード2(OAM Search)期間中、PPUは、LYと各OBJのY座標を比較して、そのスキャンラインに描画する最大10個のOBJを選択します。 PPUはOAMを順番に(\$FE00から\$FE9Fまで)スキャンし、最初の(最大)10個の適切に配置されたOBJを選択します。 よって選択優先度は\$FE00に近いOBJほど高くなります。 また、[X座標](#1バイト目---x座標)で述べましたが、PPUはY座標のみをチェックしてOBJを選択するため、画面外のOBJも1スキャンラインあたり10OBJの制限にカウントされるので、OBJを非表示にしたい場合は、[Y座標](#0バイト目---y座標)を`Y=0`か`Y≥160`にしましょう。(スプライトのサイズが`8x8`なら`Y=0`でなく`Y≤8`でも可) ### 描画優先度(Drawing priority) 2つのオブジェクトの不透明なピクセルが重なった場合、どちらのピクセルが表示されるかは、選択優先度とは別の種類の優先順位(描画優先度)によって決定されます。ただし、この優先順位は、DMGとCGBでは決定方法が異なります。 **DMGモード** X座標が小さいほど描画優先度が高くなります。 X座標が同一の場合は、OAMで最初の方(\$FE00)に位置するオブジェクトが優先されます。 **CGBモード** オブジェクトの優先順位は、OAMにおけるオブジェクトの位置のみで決まります。 つまり、OAMで最初の方(\$FE00)に位置するオブジェクトが優先されます。 ``` Note: 描画優先度と「OBJに対するBGの優先度」(アトリビュート(3バイト目)のbit7)は、お互いに相互作用をしますが、この相互作用はあまり直感的なものではありません。 内部的にはまず、PPUはOBJ間の優先順位を解決してOBJピクセルを選択します。これは、描画優先度が最も高いピクセルです。 「OBJに対するBGの優先度」は、このプロセスでは考慮されません。 OBJの優先順位が解決されて初めて、OBJピクセルはそのOBJの「OBJに対するBGの優先度」がチェックされ、背景の上に描画されるべきかどうかが決定されます。 つまり、「OBJに対するBGの優先度」が有効になっている優先度の高いOBJは、「OBJに対するBGの優先度」が無効になっていても、優先度の低いOBJをマスクしてしまうことになります。 ```