Jekyll2023-11-14T05:12:06+04:00http://localhost:4000/feed.xmlflawedMy blog about anything gamedev relatedYury Sinevflawed.blog@gmail.comДелаем Pong для NES (Неделя №1)2020-01-08T23:45:00+04:002020-01-08T23:45:00+04:00http://localhost:4000/gamedev/making-pong-for-nes-week-1-ru<div class="notice--warning">
<h4> Имейте в виду: </h4>
<ul>
<li>Всё что я делаю может быть совершенно неверным</li>
<li>Это пост может содержать нецензурную речь</li>
<li>Это не туториал. Скорее это просто подробное описание разработки этой игры</li>
<li>В посте может быть миллион грамматических ошибок</li>
<li>Не стесняйтесь сообщать мне об ошибках в комментариях или через соцсети/email</li>
</ul>
</div>
<h1 id="почему">Почему?</h1>
<blockquote>
<p>Docendo discimus</p>
</blockquote>
<p>или если вы сегодня не собирались вызывать демонов:</p>
<blockquote>
<p>Уча, мы познаём</p>
</blockquote>
<p>Хотя, учить - это сильное слово, скорее я просто пытаюсь пояснить что я делаю, чаще всего себе же.</p>
<h1 id="почему-nesfamicomденди">Почему NES/Famicom/Денди?</h1>
<p>Я всегда хотел сделать игру для старого железа<sup id="fnref:b41501cc"><a href="#fn:b41501cc" class="footnote">1</a></sup>. Ограничить себя “легковесным” ассемблером, малым количеством ROM/RAM памяти и медленным CPU.
Выжать все соки из этого железа. Быть увереным, что весь мой говнокод будет работать одинаково на всех юнитах.
И для этого, Я выбрал <strong>NES</strong>.</p>
<p>Отложим выжимание сока в сторону и начнём с чего-то простого, например <strong>Pong</strong>.</p>
<h1 id="6502-ассемблер">6502 ассемблер</h1>
<p>Ассемблер не так сложен, как можно было бы подумать, особенно 6502 ассемблер. Разработка под <strong>NES</strong> тоже не выглядит слишком сложной задачей. Конечно, после использования 6502 ассемблера, ты будешь радоваться даже самому простому синтаксическому сахару. И всё же, это не слишком замарочено и точно не так скучно, раздражающе и положительно неудволительно как разработка на 1С.<sup id="fnref:f6d4ec76"><a href="#fn:f6d4ec76" class="footnote">2</a></sup></p>
<p>Если вы ещё не знаете 6502 ассемблер, но всё же хотите понимать что тут происходит, пролистайте эту страницу <a href="http://6502.org/tutorials/6502opcodes.html">описание опкодов</a> или можете найти туторилы в секции <strong>Ссылки</strong>.</p>
<p>Но основная идея такая:</p>
<ul>
<li>Грузим какие-нибудь данные в один из трёх регистров (<code class="highlighter-rouge">LDA</code>, <code class="highlighter-rouge">LDX</code>, <code class="highlighter-rouge">LDY</code>)</li>
<li>Делаем что-нибудь с этими данными (<code class="highlighter-rouge">INX</code> - увеличиваем <code class="highlighter-rouge">X</code> регистор на 1, например)</li>
<li>Сохраняем результат где-нибудь (<code class="highlighter-rouge">STA</code>, <code class="highlighter-rouge">STX</code>, <code class="highlighter-rouge">STY</code>)</li>
<li>И потом ветвления, переносы между регистрами, сабрутины и прочая красота</li>
</ul>
<p>Ну, или просто читайте этот пост ради развлечения, если вы думаете это того стоит.</p>
<h1 id="инструменты">Инструменты</h1>
<p>Я буду использовать <strong>Windows</strong>, <strong>FCEUX</strong> эмулятор, <strong>NESst</strong> и <strong>nesasm3</strong> ассемблер. Также, Я взял <code class="highlighter-rouge">nesdefs.asm</code> и <code class="highlighter-rouge">nesppu.asm</code> у <a href="https://8bitworkshop.com/">8bitworkshop</a><sup id="fnref:f7f95d4a"><a href="#fn:f7f95d4a" class="footnote">3</a></sup> и портировал их на <strong>nesasm</strong><sup id="fnref:b82c9141"><a href="#fn:b82c9141" class="footnote">4</a></sup>. Эти файлы содержат множество полезных констант и макросов.</p>
<h1 id="ines-заголовок">iNES заголовок</h1>
<p>Каждый rom начинается с заголовка, в нашем случае iNES заголовка:</p>
<div class="language-nesasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="p">.</span><span class="kr">inesprg</span> <span class="m">1</span> <span class="c1">; 1x 16KB PRG code</span>
<span class="p">.</span><span class="kr">ineschr</span> <span class="m">1</span> <span class="c1">; 1x 8KB CHR data</span>
<span class="p">.</span><span class="kr">inesmap</span> <span class="m">0</span> <span class="c1">; маппер 0 = NROM, без смены банков</span>
<span class="p">.</span><span class="kr">inesmir</span> <span class="m">1</span> <span class="c1">; отзеркаливание задника</span>
</code></pre></div></div>
<p>В итоге:</p>
<ul>
<li><em>1</em> 16kb PRG ROM - наш код игры/движка</li>
<li><em>1</em> 8kb chr rom - наша графика.</li>
<li>Нет смены банков (32 kb PRG максимум)</li>
<li>вертикальное отзеркаливание задника</li>
</ul>
<p>Для понга этой конфигурации более чем хватит.</p>
<p class="notice--info">Заголовок используется только эмуляторами (и скорее всего флеш катриджами).</p>
<h1 id="переменные">Переменные</h1>
<p>16 битная переменная для позиции левой ракетки выглядит так:</p>
<div class="language-nesasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">paddle1PosLo</span> <span class="p">.</span><span class="kr">rs</span> <span class="m">1</span> <span class="c1">; младший байт позиции левой ракетки</span>
<span class="nf">paddle1PosHi</span> <span class="p">.</span><span class="kr">rs</span> <span class="m">1</span> <span class="c1">; старший байт позиции левой ракетки</span>
</code></pre></div></div>
<p>Формат: <code class="highlighter-rouge">название_переменной</code> <code class="highlighter-rouge">.rs</code> <code class="highlighter-rouge">n байт</code> <br />
<code class="highlighter-rouge">.rs</code> - резервирует n байт памяти для этой переменной.
<br />Мы можем обозначить позицию одной переменной используя <strong>.rs 2</strong> и потом с помощью LOW и HIGH получать младший и старший байт соответственно.</p>
<p class="notice--info">Вообще, сейчас нам младший байт позиции и не нужен, так как мы ничего сейчас двигать не собираемся, но это пригодится в будующем.
<br />16 бит помогут нам добавить перемещению плавности. 16 бит позволяет учитывать сабпиксели в вычислениях, тогда как при 8 битах минимальное изменение переменной позиции/ускорения будет эквивалентно одному пикселю.
<br />Объясню подробнее в следующем посте.</p>
<h1 id="прерывания">Прерывания</h1>
<p>Прерывания - что-то вроде хардварных ивентов. Например, игрок жмёт ресет на консоли, и это вызывает CPU прерывание и счётчик команд перемещается к ресет вектору прерывания.</p>
<p>У NES 3 таких:</p>
<ul>
<li><strong>NMI</strong> ($FFFA-$FFFB) или Non-Maskable Interrupt - вызывается в конце каждого кадра (или в начале v-blank периода)</li>
<li><strong>Reset</strong> ($FFFC-$FFFD) - вызывается каждый раз, когда игрок жмёт кнопку ресет (<em>ага</em>) или при каждом старте программы</li>
<li><strong>IRQ+BRK</strong> ($FFFE-$FFFF) - что-то, в чём я пока не разбирался. Сейчас оно нам не нужно.</li>
</ul>
<p>Мы объявляем векторы прерываний <em>вот так вот</em><sup id="fnref:fbc428f7"><a href="#fn:fbc428f7" class="footnote">5</a></sup>:</p>
<div class="language-nesasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">.</span><span class="kr">macro</span> <span class="nf">NES_VECTORS</span>
<span class="p">.</span><span class="kr">org</span> <span class="mh">$fffa</span> <span class="c1">; начинаем в $fffa</span>
<span class="p">.</span><span class="kt">dw</span> <span class="nf">NMIHandler</span> <span class="c1">; $fffa vblank nmi</span>
<span class="p">.</span><span class="kt">dw</span> <span class="nf">Reset</span> <span class="c1">; $fffc reset</span>
<span class="p">.</span><span class="kt">dw</span> <span class="m">0</span> <span class="c1">; $fffe irq / brk</span>
<span class="p">.</span><span class="kr">ENDM</span>
</code></pre></div></div>
<h1 id="reset-прерывание"><strong>Reset</strong> прерывание</h1>
<h2 id="инициализация">Инициализация</h2>
<p>Сейчас нам нужно временно отключить PPU<sup id="fnref:6157e0dc"><a href="#fn:6157e0dc" class="footnote">6</a></sup>, прерывания и режим десятичных вычислений (NES CPU всё равно этот режим не поддерживает), устанавливаем указатель стека и много чего ещё.</p>
<p class="notice--info">NES CPU (2A03) это урезанный 6502 CPU без режима десятичных вычислений, но имеет специальные привязанные к адрессам памяти регистрами для ввода/вывода, звука, PPU и т.д.</p>
<div class="language-nesasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="nf">NES_INIT</span> <span class="c1">; устанавливаем указатель стека, вырубаем PPU</span>
<span class="k">jsr</span> <span class="nf">WaitSync</span> <span class="c1">; ждём VSYNC</span>
<span class="k">jsr</span> <span class="nf">ClearRAM</span> <span class="c1">; чистим RAM</span>
<span class="k">jsr</span> <span class="nf">WaitSync</span> <span class="c1">; ждём VSYNC (и прогрева PPU)</span>
</code></pre></div></div>
<p class="notice--info"><code class="highlighter-rouge">NES_INIT</code> это макро в <code class="highlighter-rouge">nesdefs.asm</code> и его можно добавить в главный файл (<code class="highlighter-rouge">pong.asm</code> В моём случае) написав <code class="highlighter-rouge">.include "nesdefs.asm"</code> в начале файла.</p>
<p>Вот что <code class="highlighter-rouge">NES_INIT</code> делает:</p>
<div class="language-nesasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="p">.</span><span class="kr">macro</span> <span class="nf">NES_INIT</span>
<span class="k">sei</span> <span class="c1">;отключаем прерывания</span>
<span class="k">cld</span> <span class="c1">;отключаем режим десятичных вычислений</span>
<span class="k">ldx</span> <span class="mh">#$ff</span>
<span class="k">txs</span> <span class="c1">;устанавливаем указатель стека</span>
<span class="k">inx</span> <span class="c1">;увеличиваем X до 0 (FF переполняется в 0)</span>
<span class="k">stx</span> <span class="nf">PPU_MASK</span> <span class="c1">;вырубаем отрисовку</span>
<span class="k">stx</span> <span class="nf">DMC_FREQ</span> <span class="c1">;вырубаем DMC прерывания</span>
<span class="k">stx</span> <span class="nf">PPU_CTRL</span> <span class="c1">;вырубаем NMI прерывания</span>
<span class="k">bit</span> <span class="nf">PPU_STATUS</span> <span class="c1">;обнуляем VBL флаг</span>
<span class="k">bit</span> <span class="nf">APU_CHAN_CTRL</span> <span class="c1">; так и не понял что мы тут делаем</span>
<span class="k">lda</span> <span class="mh">#$40</span>
<span class="k">sta</span> <span class="nf">APU_FRAME</span> <span class="c1">;вырубаем APU Frame прерывание</span>
<span class="k">lda</span> <span class="mh">#$0F</span>
<span class="k">sta</span> <span class="nf">APU_CHAN_CTRL</span> <span class="c1">;вырубаем DMC прерывание, врубаем другие каналы.</span>
<span class="p">.</span><span class="kr">endm</span>
</code></pre></div></div>
<p><strong>VSYNC</strong>. Смотрим, если 7ой бит (самый старший который или же первый если считать слева-направо) <code class="highlighter-rouge">PPU_STATUS</code> ($2002) равен одному.</p>
<div class="language-nesasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">WaitSync:</span>
<span class="k">bit</span> <span class="nf">PPU_STATUS</span> <span class="c1">; тригерит минусовой CPU флаг</span>
<span class="k">bpl</span> <span class="nf">WaitSync</span>
<span class="k">rts</span>
</code></pre></div></div>
<p><strong>Clear RAM</strong>. Необходимый шаг, так как мы не знаем какие данные находятся в RAM когда срабатывает Reset прерывание.
Поэтому мы всегда должны полагать, что RAM при перезагрузке может содержать мусор, а не все 0, как можно было бы подумать.</p>
<div class="language-nesasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">ClearRAM:</span>
<span class="k">lda</span> <span class="m">#0</span> <span class="c1">; A = 0</span>
<span class="k">tax</span> <span class="c1">; X = 0</span>
<span class="p">.</span><span class="nf">clearRAM</span>
<span class="k">sta</span> <span class="mh">$0</span><span class="p">,</span><span class="nf">x</span> <span class="c1">; чистим $0-$ff</span>
<span class="k">cpx</span> <span class="mh">#$fe</span> <span class="c1">; последние 2 байта стека?</span>
<span class="k">bcs</span> <span class="p">.</span><span class="nf">skipStack</span> <span class="c1">; не чистим</span>
<span class="k">sta</span> <span class="mh">$100</span><span class="p">,</span><span class="nf">x</span> <span class="c1">; чистим $100-$1fd</span>
<span class="p">.</span><span class="nf">skipStack</span>
<span class="k">sta</span> <span class="mh">$200</span><span class="p">,</span><span class="nf">x</span> <span class="c1">; чистим $200-$2ff</span>
<span class="k">sta</span> <span class="mh">$300</span><span class="p">,</span><span class="nf">x</span> <span class="c1">; чистим $300-$3ff</span>
<span class="k">sta</span> <span class="mh">$400</span><span class="p">,</span><span class="nf">x</span> <span class="c1">; чистим $400-$4ff</span>
<span class="k">sta</span> <span class="mh">$500</span><span class="p">,</span><span class="nf">x</span> <span class="c1">; чистим $500-$5ff</span>
<span class="k">sta</span> <span class="mh">$600</span><span class="p">,</span><span class="nf">x</span> <span class="c1">; чистим $600-$6ff</span>
<span class="k">sta</span> <span class="mh">$700</span><span class="p">,</span><span class="nf">x</span> <span class="c1">; чистим $700-$7ff</span>
<span class="k">inx</span> <span class="c1">; X = X + 1</span>
<span class="k">bne</span> <span class="p">.</span><span class="nf">clearRAM</span> <span class="c1">; и так 256 раз</span>
<span class="k">rts</span>
</code></pre></div></div>
<p>И ещё один <strong>VSYNC</strong>.</p>
<h2 id="палитра">Палитра</h2>
<p>Теперь мы должны сообщить где используемая палитра будет храниться ($3F00).</p>
<div class="language-nesasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">lda</span> <span class="mh">#$3f</span> <span class="c1">; $3F -> A регистр</span>
<span class="k">ldy</span> <span class="mh">#$00</span> <span class="c1">; $00 -> Y регистр</span>
<span class="k">sta</span> <span class="nf">PPU_ADDR</span> <span class="c1">; сначала записываем старший байт</span>
<span class="k">sty</span> <span class="nf">PPU_ADDR</span> <span class="c1">; и младший $3F00 -> PPU адресс</span>
</code></pre></div></div>
<p>В <strong>NES_INIT</strong> макросе мы отключили NMI и отрисовку, теперь, после инициализации палитры, пришло время включить их.</p>
<div class="language-nesasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">lda</span> <span class="nf">#CTRL_NMI</span>
<span class="k">sta</span> <span class="nf">PPU_CTRL</span> <span class="c1">; включаем NMI</span>
<span class="k">lda</span> <span class="nf">#MASK_COLOR</span>
<span class="k">sta</span> <span class="nf">PPU_MASK</span> <span class="c1">; включаем отрисовку</span>
</code></pre></div></div>
<p>PPU_CTRL биты:
<img src="/assets/images/PPU_CTRL_ru.PNG" alt="ppu_ctrl" /></p>
<p>PPU_MASK биты:
<img src="/assets/images/PPU_MASK_ru.PNG" alt="ppu_mask" /></p>
<p class="notice--info">Вы можете предвартильно посмотреть как будет выглядеть чёрно-белый вариант и цветовый акценты с вашей палитрой и спрайтами в NESst
<img src="/assets/images/nesST_colorEmphasis.png" alt="nesST" /></p>
<p>Добавить палитру можно так:</p>
<div class="language-nesasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">Pallete:</span>
<span class="kr">incbin</span> <span class="s">"palette.pal"</span>
</code></pre></div></div>
<p class="notice--info"><code class="highlighter-rouge">palette.pal</code> это простой бинарник без заголовка и прочего, который содержим эти данные: <br />
$0f,$00,$28,$30,$0f,$01,$21,$31,$0f,$06,$16,$26,$0f,$09,$19,$29,
$0f,$00,$28,$30,$0f,$01,$21,$31,$0f,$06,$16,$26,$0f,$09,$19,$29</p>
<p>или можно добавить эти hex значения простыми текстом:</p>
<div class="language-nesasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">Palette:</span>
<span class="p">.</span><span class="kt">db</span> <span class="mh">$0f</span><span class="p">,</span><span class="mh">$00</span><span class="p">,</span><span class="mh">$28</span><span class="p">,</span><span class="mh">$30</span><span class="p">,</span><span class="mh">$0f</span><span class="p">,</span><span class="mh">$01</span><span class="p">,</span><span class="mh">$21</span><span class="p">,</span><span class="mh">$31</span><span class="p">,</span><span class="mh">$0f</span><span class="p">,</span><span class="mh">$06</span><span class="p">,</span><span class="mh">$16</span><span class="p">,</span><span class="mh">$26</span><span class="p">,</span><span class="mh">$0f</span><span class="p">,</span><span class="mh">$09</span><span class="p">,</span><span class="mh">$19</span><span class="p">,</span><span class="mh">$29</span> <span class="c1">;;задники</span>
<span class="p">.</span><span class="kt">db</span> <span class="mh">$0f</span><span class="p">,</span><span class="mh">$00</span><span class="p">,</span><span class="mh">$28</span><span class="p">,</span><span class="mh">$30</span><span class="p">,</span><span class="mh">$0f</span><span class="p">,</span><span class="mh">$01</span><span class="p">,</span><span class="mh">$21</span><span class="p">,</span><span class="mh">$31</span><span class="p">,</span><span class="mh">$0f</span><span class="p">,</span><span class="mh">$06</span><span class="p">,</span><span class="mh">$16</span><span class="p">,</span><span class="mh">$26</span><span class="p">,</span><span class="mh">$0f</span><span class="p">,</span><span class="mh">$09</span><span class="p">,</span><span class="mh">$19</span><span class="p">,</span><span class="mh">$29</span> <span class="c1">;;спрайты</span>
</code></pre></div></div>
<p>Эти числа соответсвуют цветам из внутренней NES палитры: <sup id="fnref:27e518f9"><a href="#fn:27e518f9" class="footnote">7</a></sup></p>
<p><img src="/assets/images/Savtool-swatches.png" alt="NES palette" /></p>
<p>1 набор из 4 палитр для задников и 1 набор для спрайтов.</p>
<p class="notice--info">В NESst вы можете ткнуть “Palettes” -> “Put to the clipboard” -> “ASM data” чтобы скопировать палитру. Будьте вниматльны, это копирует только 1 набор (16 цветов) за раз.</p>
<p><strong>Загрузка палитр</strong></p>
<div class="language-nesasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">LoadPalettes:</span>
<span class="k">lda</span> <span class="nf">PPU_STATUS</span> <span class="c1">; читаем PPU статус чтобы сбросить #HIGH/#LOW</span>
<span class="k">lda</span> <span class="mh">#$3F</span>
<span class="k">sta</span> <span class="nf">PPU_ADDR</span> <span class="c1">; пишем старший байт $3F00 адресса</span>
<span class="k">lda</span> <span class="mh">#$00</span>
<span class="k">sta</span> <span class="nf">PPU_ADDR</span> <span class="c1">; а теперь младший байт $3F00 адресса</span>
<span class="k">ldx</span> <span class="mh">#$00</span> <span class="c1">; начинаем с нуля</span>
<span class="nf">LoadPalettesLoop:</span>
<span class="k">lda</span> <span class="nf">Palette</span><span class="p">,</span> <span class="nf">x</span> <span class="c1">; грузим данные из адресса (Palette + x)</span>
<span class="k">sta</span> <span class="nf">PPU_DATA</span> <span class="c1">; пишем в PPU_DATA</span>
<span class="k">inx</span> <span class="c1">; x += 1</span>
<span class="k">cpx</span> <span class="mh">#$20</span> <span class="c1">; Сверяем X с шестнадцатеричным $20, десятичным 32 (палитры задников и спрайтов (4*4) * 2)</span>
<span class="k">bne</span> <span class="nf">LoadPalettesLoop</span> <span class="c1">; Если не равно, то уходим в луп LoadPalettesLoop</span>
<span class="c1">; иначе идём дальше</span>
</code></pre></div></div>
<p>Весь этот код просто копирует палитры в $3f00.</p>
<p class="notice--info">Помните, как мы сообщали NES где мы будем хранить палитру? Да, это был $3f00</p>
<h2 id="спрайты">Спрайты</h2>
<p>Вот первая страница (левая сторона) моего chr rom:<br />
<img src="/assets/images/pongTileset1.bmp" alt="128x128 tileset" /></p>
<p><strong>Загрузка спрайтов</strong></p>
<div class="language-nesasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">lda</span> <span class="mh">#$00</span>
<span class="k">sta</span> <span class="nf">OAM_ADDR</span> <span class="c1">; устанавливаем младший байт (00) OAM RAM адресса</span>
<span class="k">lda</span> <span class="mh">#$02</span>
<span class="k">sta</span> <span class="nf">OAM_DMA</span> <span class="c1">; устанавливаем старший байт (02) OAM RAM адресса, начинаем DMA перенос</span>
</code></pre></div></div>
<p>Пишем <code class="highlighter-rouge">$0200</code> в OAM RAM адресс.<br />
<code class="highlighter-rouge">$0200</code>-<code class="highlighter-rouge">$02FF</code> теперь содержит копию OAM (64 записи из 4 байт каждая).</p>
<p>Этот макрос упростит загрузку спрайтов:</p>
<p><strong><em>graphics.asm</em></strong></p>
<div class="language-nesasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">;; \1 спрайты со смещением \2 смещение \3 количество спрайтов</span>
<span class="nf">LoadSprites</span> <span class="p">.</span><span class="kr">macro</span>
<span class="k">ldx</span> <span class="mh">#$00</span>
<span class="nf">LoadSpritesLoop</span><span class="nb">\@</span><span class="o">:</span>
<span class="k">lda</span> <span class="nb">\1</span><span class="p">,</span> <span class="nf">x</span>
<span class="k">sta</span> <span class="nf">OAM_RAM</span><span class="o">+</span><span class="nb">\2</span><span class="p">,</span> <span class="nf">x</span>
<span class="k">inx</span>
<span class="k">cpx</span> <span class="nb">\3</span>
<span class="k">bne</span> <span class="nf">LoadSpritesLoop</span><span class="nb">\@</span>
<span class="p">.</span><span class="kr">endm</span>
</code></pre></div></div>
<p><code class="highlighter-rouge">\@</code> - специальный параметр, который возвращает разные числа для каждого макроса. <br />
<code class="highlighter-rouge">\1-\3</code> - входные параметры.</p>
<p>В C это бы выглядело бы как-то так:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="kt">void</span> <span class="nf">LoadSprites</span><span class="p">(</span><span class="n">byte</span> <span class="n">sprites</span><span class="p">,</span> <span class="n">byte</span> <span class="n">offset</span><span class="p">,</span> <span class="n">byte</span> <span class="n">spritesCount</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="p">};</span>
</code></pre></div></div>
<p>Итак, мы грузим наш метаспрайт (<code class="highlighter-rouge">спрайты</code>) в OAM_RAM со <code class="highlighter-rouge">смещением</code>
пока X не равен длинне метаспрайта (<code class="highlighter-rouge">количество спрайтов</code>)</p>
<p>Вызываем макрос так:</p>
<div class="language-nesasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="nf">LoadSprites</span> <span class="nf">MiddlePadle</span><span class="p">,</span> <span class="mh">#$00</span><span class="p">,</span> <span class="mh">#$10</span>
</code></pre></div></div>
<p class="notice--warning">Кстати, Я только сейчас заметил, что называю метаспрайт ракетки - “MiddlePadle”. Не обращайте особого внимания, Я хотел сделать 3 разных размера ракеток в качестве паур-апов, так что…
<br />Может сделаем</p>
<p>Вот как MiddlePaddle метаспрайт выглядит:</p>
<div class="language-nesasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">MiddlePadle:</span>
<span class="p">.</span><span class="kt">db</span> <span class="m">0</span><span class="p">,</span> <span class="mh">$03</span><span class="p">,</span> <span class="mh">$00</span><span class="p">,</span> <span class="mh">$00</span>
<span class="p">.</span><span class="kt">db</span> <span class="m">8</span><span class="p">,</span> <span class="mh">$04</span><span class="p">,</span> <span class="mh">$00</span><span class="p">,</span> <span class="mh">$00</span>
<span class="p">.</span><span class="kt">db</span> <span class="m">16</span><span class="p">,</span> <span class="mh">$04</span><span class="p">,</span> <span class="mh">$00</span><span class="p">,</span> <span class="mh">$00</span>
<span class="p">.</span><span class="kt">db</span> <span class="m">24</span><span class="p">,</span> <span class="mh">$02</span><span class="p">,</span> <span class="mh">$00</span><span class="p">,</span> <span class="mh">$00</span>
</code></pre></div></div>
<p class="notice--info">Просто напоминание. <br />
Чтобы указать систему счисления, перед числом нужно добавить:<br />
<code class="highlighter-rouge">%</code> - двоичное число<br />
<code class="highlighter-rouge">$</code> - шестнадцатеричное<br />
<code class="highlighter-rouge">no prefix</code> - десятичное.</p>
<p>Давайте рассмотрим первый ряд метаспрайта:</p>
<div class="language-nesasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="c1">;vert sprite attr horiz</span>
<span class="p">.</span><span class="kt">db</span> <span class="m">0</span><span class="p">,</span> <span class="mh">$03</span><span class="p">,</span> <span class="mh">$00</span><span class="p">,</span> <span class="mh">$00</span>
</code></pre></div></div>
<ul>
<li>
<p>Первый байт - это <code class="highlighter-rouge">y</code> координата экрана.</p>
</li>
<li>
<p>Второй байт - индекс спрайта. В нашем pong.chr файле $03 соответсвует этому спрайту.</p>
</li>
</ul>
<p><img src="/assets/images/paddle1.PNG" alt="paddle_graphics" /></p>
<ul>
<li>Третий байт - атрибуты <sup id="fnref:8f138dca"><a href="#fn:8f138dca" class="footnote">8</a></sup></li>
</ul>
<p><img src="/assets/images/attributeBits_ru.PNG" alt="attribute bits" /></p>
<p>Два младших бита - номер палитры. 00 - первый набор палитры, 01 - второй, 10 - третий, 11 - четвёртый. В общем, 0-3 в двоичном представлении. Довольно просто.</p>
<ul>
<li>Четвёртый байт - <code class="highlighter-rouge">x</code> координата экрана.</li>
</ul>
<p class="notice--info">В нашем случае и <code class="highlighter-rouge">x</code> и <code class="highlighter-rouge">y</code> экранные координаты не абсолютные, а относительные. То есть, <code class="highlighter-rouge">0</code> не <code class="highlighter-rouge">y</code> позиция, а смещение <code class="highlighter-rouge">y</code> координаты. <strong>Мы смещаем каждый спрайт на <code class="highlighter-rouge">8</code> (десятичное число), так как каждый спрайт 8x8 пикселей.</strong><sup id="fnref:dd593263"><a href="#fn:dd593263" class="footnote">9</a></sup>
У нас есть две ракетки (левая и правая, да), поэтому использовать смещения будет хорошей идеей, ведь мы сможем использовать один и тот же метаспрайт для обеих ракеток.</p>
<p>Инициализируем переменные. <code class="highlighter-rouge">$80</code> - середина экрана.</p>
<div class="language-nesasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">lda</span> <span class="mh">#$80</span>
<span class="k">sta</span> <span class="nf">paddle1PosHi</span>
<span class="k">lda</span> <span class="mh">#$00</span>
<span class="k">sta</span> <span class="nf">paddle1PosLo</span>
</code></pre></div></div>
<p>Теперь, всё что остаётся - это ждать NMI прерывания.</p>
<div class="language-nesasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">.</span><span class="nf">endless</span>
<span class="k">jmp</span> <span class="p">.</span><span class="nf">endless</span> <span class="c1">; бесконечный цикл</span>
</code></pre></div></div>
<h1 id="nmi"><strong>NMI</strong></h1>
<h2 id="подготовка-ppu">Подготовка PPU</h2>
<p>Следующий шаг - подготовка PPU для рендера следующего кадра.
И так как OAM RAM реализован через динамическую RAM, контент RAM быстро превращает в мусор, поэтому нужно обновлять содержание OAM каждый кадр.<sup id="fnref:8f138dca:1"><a href="#fn:8f138dca" class="footnote">8</a></sup></p>
<p>Также теперь мы может включить отображение спрайтов, записав единицу в 4ый старший бит в PPU_MASK.</p>
<div class="language-nesasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">lda</span> <span class="mh">#$00</span>
<span class="k">sta</span> <span class="nf">OAM_ADDR</span> <span class="c1">; устанавливаем младший байт (00) OAM RAM адресса</span>
<span class="k">lda</span> <span class="mh">#$02</span>
<span class="k">sta</span> <span class="nf">OAM_DMA</span> <span class="c1">; устанавливаем старший байт (02) OAM RAM адресса, начинаем DMA перенорс</span>
<span class="c1">;;</span>
<span class="k">lda</span> <span class="mb">#%10000000</span> <span class="c1">; включаем NMI</span>
<span class="k">sta</span> <span class="nf">PPU_CTRL</span>
<span class="k">lda</span> <span class="mb">#%00010000</span> <span class="c1">; включаем отображение спрайтов</span>
<span class="k">sta</span> <span class="nf">PPU_MASK</span>
<span class="k">lda</span> <span class="mh">#$00</span> <span class="c1">;; сообщаем PPU, что задники у нас пока не скролятся</span>
<span class="k">sta</span> <span class="mh">$2005</span>
<span class="k">sta</span> <span class="mh">$2005</span>
</code></pre></div></div>
<h2 id="обновление-позиции">Обновление позиции</h2>
<p>Теперь нам осталось только обновить позицию ракетки и нарисовать её метаспрайт в этой новой позиции.</p>
<p class="notice--info">Пока мы на самом деле ничего не перемещаем, но этот макрос будет очень полезен позже</p>
<p>Следующий макрос просто добавляет текущую позицию ракетки к смещение метаспрайта.</p>
<p><strong><em>transform.asm</em></strong></p>
<div class="language-nesasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">;; \1 posYHi \2 posXHi \3 offset</span>
<span class="nf">UpdatePos</span> <span class="p">.</span><span class="kr">macro</span>
<span class="k">ldx</span> <span class="mh">#$03</span>
<span class="k">ldy</span> <span class="mh">#$00</span>
<span class="nf">UpdatePosLoop</span><span class="nb">\@</span><span class="o">:</span>
<span class="k">lda</span> <span class="nb">\1</span>
<span class="k">clc</span>
<span class="k">adc</span> <span class="nf">MiddlePadle</span><span class="p">,</span> <span class="nf">y</span> <span class="c1">; добавляем старший байт текущей Y позиции к Y смещению (первый байт каждой строки OAM ROM ракетки)</span>
<span class="k">sta</span> <span class="nf">OAM_RAM</span><span class="o">+</span><span class="nb">\3</span><span class="p">,</span> <span class="nf">y</span> <span class="c1">; храним получившуюся позицию Y в OAM RAM со смещением</span>
<span class="k">lda</span> <span class="nb">\2</span> <span class="c1">; позиции X - константа</span>
<span class="k">sta</span> <span class="nf">OAM_RAM</span><span class="o">+</span><span class="nb">\3</span><span class="p">,</span> <span class="nf">x</span>
<span class="k">inx</span>
<span class="k">inx</span>
<span class="k">inx</span>
<span class="k">inx</span>
<span class="k">iny</span>
<span class="k">iny</span>
<span class="k">iny</span>
<span class="k">iny</span>
<span class="k">cpy</span> <span class="mh">#$10</span> <span class="c1">; можете оставить константой или добавить 4ым аргументом</span>
<span class="k">bne</span> <span class="nf">UpdatePosLoop</span><span class="nb">\@</span>
<span class="p">.</span><span class="kr">endm</span>
</code></pre></div></div>
<p>Вызываем так:</p>
<div class="language-nesasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="nf">UpdatePos</span> <span class="nf">paddle1PosHi</span><span class="p">,</span> <span class="mh">#$0C</span><span class="p">,</span> <span class="mh">#$00</span>
</code></pre></div></div>
<p>Вы навреное заметили 4 <code class="highlighter-rouge">inx</code> и <code class="highlighter-rouge">iny</code> инструкции подряд и подумали, что это выглядит тупо. Ну, в общем-то да, но на самом деле это не так тупо, если подумать об этом.
Вместо 4ёх <code class="highlighter-rouge">inx</code>, вы могли бы написать так:</p>
<div class="language-nesasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="k">txa</span> <span class="c1">; 2 cycles</span>
<span class="k">clc</span> <span class="c1">; 2 cycles</span>
<span class="k">adc</span> <span class="m">4</span> <span class="c1">; 2 cycles</span>
<span class="k">tax</span> <span class="c1">; 2 cycles</span>
</code></pre></div></div>
<p>4 инструкции, 8 циклов. Тоже самое, что написать <code class="highlighter-rouge">inx</code> 4 раза. Видите? Это не тупо, может быть лениво, но не тупо.</p>
<h2 id="метаспрайты">Метаспрайты</h2>
<p>Давайте снова посмотим на OAM ROM нашей ракетки:</p>
<div class="language-nesasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">MiddlePadle:</span>
<span class="p">.</span><span class="kt">db</span> <span class="m">0</span><span class="p">,</span> <span class="mh">$03</span><span class="p">,</span> <span class="mh">$00</span><span class="p">,</span> <span class="mh">$00</span>
<span class="p">.</span><span class="kt">db</span> <span class="m">8</span><span class="p">,</span> <span class="mh">$04</span><span class="p">,</span> <span class="mh">$00</span><span class="p">,</span> <span class="mh">$00</span>
<span class="p">.</span><span class="kt">db</span> <span class="m">16</span><span class="p">,</span> <span class="mh">$04</span><span class="p">,</span> <span class="mh">$00</span><span class="p">,</span> <span class="mh">$00</span>
<span class="p">.</span><span class="kt">db</span> <span class="m">24</span><span class="p">,</span> <span class="mh">$02</span><span class="p">,</span> <span class="mh">$00</span><span class="p">,</span> <span class="mh">$00</span>
</code></pre></div></div>
<p>Каждый спрайт размером 8x8 пикселей. Марио, например, чуть больше этого. Особенно большой Марио.
Чтобы сформировать метаспрайт большого Марио, нужно собрать его из нескольких спрайтов.<br />
Взгляньте на эту гифку:</p>
<p><img src="/assets/images/marioMetasprite.gif" alt="Mario's metasprite" /></p>
<p>И так же с нашей ракеткой. Обратите особое внимание на <code class="highlighter-rouge">x</code> и <code class="highlighter-rouge">y</code> смещения, индекс спрайтов и атрибуты.</p>
<p><img src="/assets/images/pongMetasprite.gif" alt="paddle's metasprite" /></p>
<p>И не забудьте добавить опкод возвращения из прерывания (<code class="highlighter-rouge">RTI</code>) в конце.</p>
<p class="notice--warning"><a href="https://8bitworkshop.com/">8bitworkshop</a> использует SAVE_REGS и RESTORE_REGS в начале и конце NMI соответственно. Честно говоря, я не знаю зачем.
Этот макрос записывает регистры в стек (SAVE) и достаёт регистры из стека (RESTORE). Вы можете найти код макроса в <code class="highlighter-rouge">nesdefs.asm</code></p>
<h1 id="распределение-памяти">Распределение памяти</h1>
<p>Пару слов о распределении памяти нашего понга.</p>
<p>В iNES загловке мы объявили 1 prg rom (2 банка, каждый по 8 kb, в общем 16kb) и 1 chr rom (1 банк, в общем 8 kb).
Вы наверное уже заметили, каждый банк ровно 8 kb.</p>
<p>К PRG можно достучаться через адресса CPU <code class="highlighter-rouge">$8000 - $FFF9</code>.
Взгляните на изображение от <strong>BunnyBoy</strong>:
<img src="/assets/images/cpumemmap.png" alt="cpu memory map" /></p>
<p>Итак, мы располагаем PRG rom в первых 32 kb, зарезервированных под rom картриджа - первый банк в <code class="highlighter-rouge">$8000</code>, второй в <code class="highlighter-rouge">$A000</code>.</p>
<p class="notice--info">Если вам нужно больше, чем 32kb PRG, придётся использовать маппер</p>
<p>CHR rom подключен к PPU и копия chr rom’а находится в диапозоне <code class="highlighter-rouge">$0000-$2000</code>.
<br />И да, это третья банка.</p>
<ul>
<li>В первом банке мы храним <code class="highlighter-rouge">Reset</code> и <code class="highlighter-rouge">NMI</code> код</li>
<li>Во втором банке - палитра и pattern tables (наш MiddlePaddle OAM ROM)</li>
<li>В третьем - наш pong.chr</li>
</ul>
<p>В коде это выглядит так:</p>
<div class="language-nesasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="p">.</span><span class="kr">bank</span> <span class="m">0</span>
<span class="p">.</span><span class="kr">org</span> <span class="mh">$8000</span> <span class="c1">;первые 8 kb prg</span>
<span class="nf">Reset:</span>
<span class="nf">NES_INIT</span> <span class="c1">; устанавливаем указатель стека, вырубаем PPU</span>
<span class="k">jsr</span> <span class="nf">WaitSync</span> <span class="c1">; ждём VSYNC</span>
<span class="k">jsr</span> <span class="nf">ClearRAM</span> <span class="c1">; чистим RAM</span>
<span class="k">jsr</span> <span class="nf">WaitSync</span> <span class="c1">; ждём VSYNC (и прогрева PPU)</span>
<span class="p">...</span>
</code></pre></div></div>
<p>Особо не переживайте, если ничего не понятно. Мы расмотрим маппинг и мапперы позже. Наверное. Может быть.</p>
<h1 id="результаты">Результаты</h1>
<p>И после крови, боли и пота, если луна в правильном цикле и вы хорошо себя вели весь год, вы увидете</p>
<p><strong>Барабанная дробь</strong></p>
<p><img src="/assets/images/pong-1.png" alt="hey, it's pong" /></p>
<p>Куча работы, чтобы отобразить 32x8 метаспрайт на пустом 256x224 экране. Не знаю как вы, а я максимально доволен.</p>
<h1 id="ссылки">Ссылки</h1>
<p>Исходники можно найти тут: <a href="https://github.com/ShyDev/PongForNES">github</a></p>
<p>Ссылки по теме:</p>
<ul>
<li>Благодаря <a href="https://twitter.com/cppchriscpp">@cppchriscpp</a> у нас теперь есть зеркало серии туториалов <a href="http://nerdy-nights.nes.science/">Nerdy Nights</a>. Категорически рекомендую. [eng]</li>
<li><a href="https://8bitworkshop.com/">8bitworkshop</a></li>
<li>Волшебная серия ютуб роликов про 6502 от <a href="https://www.youtube.com/watch?v=LnzuMJLZRdU">Ben Eater</a> [eng]</li>
<li>Куча полезной информации о 6502 <a href="http://6502.org/">6502.org</a> [eng]</li>
<li>Куча полезной информации о NES <a href="http://nesdev.com/">nesdev</a> [eng]</li>
</ul>
<div class="footnotes">
<ol>
<li id="fn:b41501cc">
<p>Всё таки фамикому уже 36 лет. Охренеть <a href="#fnref:b41501cc" class="reversefootnote">↩</a></p>
</li>
<li id="fn:f6d4ec76">
<p>Расслабьтесь, 1С разаботчики, всё хорошо <a href="#fnref:f6d4ec76" class="reversefootnote">↩</a></p>
</li>
<li id="fn:f7f95d4a">
<p>Кстати, отличная онлайн IDE’шка для 6502 ассемблера/cc65 с кучей эмуляторов разных 8-ми битных платформ. Тонна разных полезных тулзов, подсветка синтаксиса и прочее. Гляньте, если у вас нормальное отношение к онлайн IDE <a href="#fnref:f7f95d4a" class="reversefootnote">↩</a></p>
</li>
<li id="fn:b82c9141">
<p>Я портировал только часть кода, который собираюсь использовать в данном проекте <a href="#fnref:b82c9141" class="reversefootnote">↩</a></p>
</li>
<li id="fn:fbc428f7">
<p>извините <a href="#fnref:fbc428f7" class="reversefootnote">↩</a></p>
</li>
<li id="fn:6157e0dc">
<p>PPU or Picture Processing Unit - rtx2080 в мире 8 битных спрайтов, хотя на самом деле ближе к adreno, так как это видеочип <a href="#fnref:6157e0dc" class="reversefootnote">↩</a></p>
</li>
<li id="fn:27e518f9">
<p><a href="https://wiki.nesdev.com/w/index.php/PPU_palettes">PPU palettes (nesdev’s wiki)</a> <a href="#fnref:27e518f9" class="reversefootnote">↩</a></p>
</li>
<li id="fn:8f138dca">
<p><a href="https://wiki.nesdev.com/w/index.php/PPU_OAM">PPU OAM (nesdev’s wiki)</a> <a href="#fnref:8f138dca" class="reversefootnote">↩</a> <a href="#fnref:8f138dca:1" class="reversefootnote">↩<sup>2</sup></a></p>
</li>
<li id="fn:dd593263">
<p>На 8, не сглупите так же как сглупил я. Если вы сместите <code class="highlighter-rouge">x</code> и/или <code class="highlighter-rouge">y</code> только на 1, у вас будет куча проблем, в том числе проблем с приоритетами спрайтов. Я знаю, что это всё звучит очевидно, но я умудрился проглядеть этот момент <a href="#fnref:dd593263" class="reversefootnote">↩</a></p>
</li>
</ol>
</div>Yury Sinevflawed.blog@gmail.comДелаем Pong для NES (Денди). Неделя 1. Показываем спрайты на экране эмулятора