# 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をマスクしてしまうことになります。
```