Imran Nazar: Эмуляция GameBoy на JavaScript: Ввод

Перевод шестой статьи про эмуляцию GameBoy на JavaScript. Оригинал здесь. Далее авторский текст.

Это шестая часть серии статей об эмуляции в JavaScript. Десять частей уже доступно, позже будут ещё.


На текущий момент наш эмулятор способен запускать простой тестовый ROM и выводить графику. На данный момент эмулятор не способен распознавать нажатия на кнопки кейпада и сообщать об этом эмулируемой программе. Для того чтобы это сделать мы должны реализовать взаимодействие кейпада с I/O регистрами (регистры ввода/вывода. прим. перев.). В результате мы получим такое:

Рис. 1: Реализация jsGB с вводом

Кейпад

Для пользовательского ввода в GameBoy используется восьмикнопочный кейпад, на котором может быть нажато произвольное число кнопок. На большинстве клавиатур кнопки расположены на сетке из колонок и строк: их можно представить как провода, которые связывает кнопка. Когда одна из колонок активируется, то и строка подсоединённая к этой колонке тоже активируется, благодаря чему появляется возможность распознать активные строки и определить нажатую клавишу.

В GameBoy клавиатура содержит две колонки и четыре строки, благодаря чему всю обработку можно сделать в одном восьмибитном регистре.



Рис. 2: Соединения кейпада

Поскольку все шесть линий привязаны к одному регистру, GameBoy процедура чтения клавиши немного запутана:

  • Записать в JOYP (это регистр ввода вывода. прим. перев.) 0×10 либо 0×20: это активирует 4 или 5 биты соответственно, т.е. одну из колонок;
  • Подождать несколько тактов пока сигнал дойдёт в JOYP;
  • Проверить 4 младших бита в JOYP, для того чтобы какие строки активны для текущей активной колонки.

Реализация кейпада

Написание когда для эмулирования нажатия простое занятие, но дело усложняют два фактора: возможность установить столбец перед тем как будет прочитана строка и коды клавиш в JavaScript. Для согласованной работы двух колонок мы будем использовать два значения, каждое из которых будет содержать пересечение колонки и строк. Также, надо принять во внимание, что для кейпада значения напряжения инвертированы: строка считается неактивной при высоком напряжении и активной при низком. Соответственно, в JOYP находится 1, если клавиша не нажата и 0, если нажата.

JavaScript события keydown и keyup могут быть использованы чтобы определить момент нажатия и отпускания клавиши. Привязать это к обработчику кейпада можно следующим образом:

Key.js: интерфейс объекта

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
KEY = {
    _rows: [0x0F, 0x0F],
    _column: 0,

    reset: function()
    {
        KEY._rows = [0x0F, 0x0F];
    KEY._column = 0;
    },

    rb: function(addr)
    {
        switch(KEY._column)
    {
        case 0x10: return KEY._rows[0];
        case 0x20: return KEY._rows[1];
        default: return 0;
    }
    },

    wb: function(addr, val)
    {
        KEY._column = val & 0x30;
    },

    kdown: function(e)
    {
        // Сбросить соответствующий бит
    },

    kup: function(e)
    {
        // Установить соответствующий бит
    }
};

window.onkeydown = KEY.kdown;
window.onkeyup = KEY.kup;

В дополнение к этому необходимо расширить MMU для обработки регистра ввода/вывода, с дополнением к обработке нулевой страницы.

MMU.js: Интерфейс ввода/вывода кейпада

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
    rb: function(addr)
    {
    switch(addr & 0xF000)
    {
        ...
        case 0xF000:
            switch(addr & 0x0F00)
        {
            ...
            // Нулевая страница
            case 0xF00:
                if(addr >= 0xFF80)
            {
                return MMU._zram[addr & 0x7F];
            }
            else if(addr >= 0xFF40)
            {
                // GPU (64 регистра)
                return GPU.rb(addr);
            }
            else switch(addr & 0x3F)
            {
                case 0x00: return KEY.rb();
                default: return 0;
            }
        }
    }
    }

Теперь, после реализации обработчика кейпада, осталось решить вопросы связанные с обработкой нажатий на кнопки клавиатуры и способностью кейпада различать нажатые кнопки. Это может быть сделано при помощи объекта JavaScript event. Каждое событие, которое проходит через браузер, такое как клик мышки или нажатие клавиши, будет направлено в код (если оно запрошено) вместе с объектом, который описывает событие. В случае нажатия на клавишу, объект события содержит код символа и «key scan» код, которые оба описывают клавишу.

Благодаря испытаниям Peter-Paul Koch было выяснено, что коды клавиш ненадёжны и могут изменяться в зависимости от того какой браузер используется. Единственный случай, когда все браузеры действуют согласованно, это key-scan код используемый для keyup и keydown событий. В любом браузере, нажатие данной кнопки произведёт одно и то же значение. Мы должны обработать восемь клавиш.

Scan code Клавиша на клавиатуре Кнопка на кейпаде
13 Enter Start
32 Пробел Select
37 Стрелка «влево» Влево
38 Стрелка «вверх» Вверх
39 Стрелка «вправо» Вправо
40 Стрелка «вниз» Вниз
88 x B
90 z A

Таблица 1: Key-scan коды используемые jsGB

Как указано выше соответствующие биты должны быть сброшены, когда кнопка нажата и установлены, когда кнопка отпущена. Это может быть реализовано следующим образом:

Key.js: Обработка нажатий клавиш

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
    kdown: function(e)
    {
        switch(e.keyCode)
    {
            case 39: KEY._keys[1] &= 0xE; break;
            case 37: KEY._keys[1] &= 0xD; break;
            case 38: KEY._keys[1] &= 0xB; break;
            case 40: KEY._keys[1] &= 0x7; break;
            case 90: KEY._keys[0] &= 0xE; break;
            case 88: KEY._keys[0] &= 0xD; break;
            case 32: KEY._keys[0] &= 0xB; break;
            case 13: KEY._keys[0] &= 0x7; break;
    }
    },

    kup: function(e)
    {
        switch(e.keyCode)
    {
            case 39: KEY._keys[1] |= 0x1; break;
            case 37: KEY._keys[1] |= 0x2; break;
            case 38: KEY._keys[1] |= 0x4; break;
            case 40: KEY._keys[1] |= 0x8; break;
            case 90: KEY._keys[0] |= 0x1; break;
            case 88: KEY._keys[0] |= 0x2; break;
            case 32: KEY._keys[0] |= 0x4; break;
            case 13: KEY._keys[0] |= 0x8; break;
    }
    }

Тестирование и следующие шаги

Вышеупомянутый Рис. 1 показывает результат наших дополнений к эмулятору запуском простой tic-tac-toe игры. В этом примере начальный экран может быть переключен на титры нажатием кнопки Start, которая соответствует клавише Enter. Следующее нажатие кнопки Start вернёт игровой экран и можно поиграть с компьютером: нажатием кнопки A (которой соответствует клавиша Z) поставить крест или круг.

Сейчас игра проводится вслепую, так как нет индикатора позиции игрока. Игра создаёт этот индикатор используя спрайт: тайл, который может быть помещён графическим чипом над фоном и который может перемещаться независимо. Большинство игр реализует геймплей через использование спрайтов, поэтому их встраивание в эмуляция — важный следующий шаг. В следующий раз я обращу внимание на возможности предоставляемые GameBoy для рендеринга спрайтов и как они могут быть реализованы в JavaScript.

Imran Nazar , Сентябрь 2010.
Статья датирована: 19 сентября 2010