Привет читатель 👋🏽

Вы, должно быть, думаете "only an idiot would try to run a Gameboy emulator in Google Sheets"? И будете правы! Моими любимыми программными проектами всегда были эмуляторы. Однажды я сидел в Google Таблицах и подумал, что это скучно, а знаете, что не скучно? Геймбои! Это не должно быть слишком сложно, в конце концов, я уже написал один на Java.

Мы рассмотрим, что такое эмулятор, основы Gameboy, а затем шаги по его запуску в Google Sheets. Давайте углубимся.

Демо 🎮

Нажмите здесь для листа. Обязательно сделайте копию, иначе ваш лист не будет отображаться. Вы можете сделать это, нажав Файл, затем Создать копию. После копирования вам придется подождать пару секунд, пока активируется скрипт, который откроет новую вкладку на панели инструментов под названием Эмулятор Gameboy. Перейдите на вкладку Эмулятор Gameboy, затем нажмите Показать диалоговое окно. Скрипт должен автоматически переключаться на лист GameboyScreen, если не переключаться на него вручную.

Вас могут встретить с ошибкой о том, что вы не в безопасности или вам нужно разрешение. Нажмите да / продолжить на обоих. А если не уверены, вот исходник.

Вы можете просмотреть код, нажав «Расширения» на панели инструментов и нажав «Скрипт приложений».

Что такое эмулятор 🕹️

Эмулятор можно охарактеризовать как аппаратное или программное обеспечение, которое позволяет компьютерной системе (также известной как ваш ноутбук) запускать программное обеспечение, предназначенное для другой компьютерной системы (также известное как ПЗУ Gameboy).

Общее эмпирическое правило заключается в том, что компьютерная система, эмулирующая другую компьютерную систему, должна быть как минимум в 3–4 раза мощнее исходной, поскольку в процессе эмуляции появляется дополнительный уровень.

Вот как вы можете загружать ПЗУ Gameboy / PSX и запускать их на телефонах / компьютерах или любом устройстве, которое может загрузить соответствующий эмулятор.

Неглубокое погружение в Gameboy 🥽

Gameboy — относительно простая система с модифицированным 8-битным процессором Z80 и 64 КБ адресуемой памяти. Он также имеет несколько сопутствующих подкомпонентов:

  • ЖК
  • Таймеры
  • ВСУ
  • Последовательный порт
  • Кнопки геймпада

Когда компьютерная система запускается, она работает в конечном цикле, называемом циклом выборка-декодирование-выполнение.

ЦП

ЦП функционирует как мозг компьютера и управляет циклами выборки, декодирования и выполнения. Он содержит 8 8-битных регистров A, B, C, D, E, F, H, and L и может объединять их в 4 16-битных регистра AF, BC, DE, and HL вместе с другими 16-битными регистрами SP и PC. Посмотрим, как цикл выглядит внутри ЦП.

const runCycle = () => {
  // Fetch
  const opcode = memory.read8(pc++);
  // Decode
  const instruction = decode(opcode);
  // Execute
  const elapsedCycles = instruction();
  return elapsedCycles;
}

Синхронизация компонентов

После выполнения инструкции прошло определенное количество циклов процессора, и мы должны поддерживать синхронизацию всей системы. Мы делаем это, извлекая каждый компонент в класс и предоставляя функцию update, которая продвигает компонент в зависимости от того, сколько циклов прошло.

let currCycles = 0;
const runFrame = () => {
  while (currCycles < CYCLES_PER_FRAME) {
    const elapsedCycles = cpu.runCycle();
    lcd.update(elapsedCycles);
    apu.update(elapsedCycles);
    timers.update(elapsedCycles);
  }
}

Войдите в Google Таблицы 📊

Это все, что я хочу сказать о Gameboy. Если хотите узнать больше, читайте здесь. Зная, с чего начать, следующим шагом было написание кода для Google Sheets. Google Sheets запускает код с использованием Apps Script, который представляет собой настраиваемую среду выполнения JavaScript на стороне сервера Google, созданную для их веб-приложений.

Погружаясь в документацию, я не вижу особых отличий от обычного JavaScript, поэтому я начал писать! План был прост. Напишите эмулятор с помощью Apps Script, затем отобразите кадры на листе.

Достаточно просто, поэтому я начал писать код и выполнять простые операции.

// Get sheet meta
const sheet = SpreadsheetApp.getActiveSheet();
const maxCols = sheet.getMaxColumns();
const maxRows = sheet.getMaxRows();
// Resize grid to gameboy dims
resizeSheetToGameboyDims(sheet, maxRows, maxCols);
// Set the size of rows and cols to a gameboy pixel
setSheetRowAndColsToSizeOfPixel(sheet, maxRows, maxCols);
// Clear sheet
clearSheet(sheet);
// Start emulator
startEmulator();

Первый блокпост

Через некоторое время мой код автоматически терпит неудачу без предупреждения. Именно тогда я заметил главный недостаток в моем плане.

Google разрешает только 30 секунд выполнения для пользовательских сценариев! Это означает, что мой эмулятор не мог работать достаточно долго, чтобы быть полезным или отлаживаемым! Все было потеряно, пока я не понял, что можно создавать боковые панели с помощью JavaScript и HTML.

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

Создание боковой панели

Интересной особенностью Apps Scripts HTML является то, что он не может импортировать файлы JavaScript, все должно быть в формате .html. Обходной путь заключается в том, чтобы обернуть весь клиентский JavaScript в файл HTML в тег <script>.

Файлы HTML можно связать с помощью функции импорта внутри скриптлетов <?!= ?>, которая позволяет выполнять код в HTML. Внутри мы запускаем функцию Apps Script на стороне клиента для загрузки HTML из другого файла.

<!-- Outputs the Gameboy file inside the current html file -->
<?!= HtmlService.createHtmlOutputFromFile('gameboy.html')
    .getContent(); ?>

Интеграция Gameboy

Немного написав, я понял, что не так хорош в написании JavaScript, поэтому решил модифицировать существующее ядро Gameboy.

Поскольку логика эмуляции уже есть, все, что нужно сделать, это изменить функцию рисования ядра, чтобы отправить данные кадра на сервер и отрисовать их на листе.

Я начал с определения логики для рисования двумерного массива данных изображения на листе.

const drawGrid = (colors) => {
  try {
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    const sheet = ss.getSheets()[1];
    const range = sheet.getRange("1:144");
    range.setBackgrounds(colors);
    SpreadsheetApp.flush();
  } catch (e) {
    Logger.log(e);
  }
}

Теперь эмулятор на клиенте вызывает функцию рисования с помощью функции google.script.run.[SERVER_FUNCTION_NAME].

google.script.run.drawGrid(frame);

После подключения функций мы получили эмулятор Gameboy, который рисует в гугл-таблицах!

Ограничения 🚫

Это очень медленно

Оказывается, Apps Script API не такой быстрый. Команды сервера работают медленно, а рисование медленнее. Возможно, кто-то из вас мог бы поэкспериментировать и найти решение для ускорения.

Нет возможности ставить команды в очередь

Команды отрисовки, отправленные на сервер, отрисовываются не по порядку, поэтому вы продолжаете видеть кадры, которые выполнялись в прошлом. Чтобы смягчить ситуацию, клиент ставит в очередь команды рисования и отправляет следующую только после завершения предыдущей команды.

Заключительные мысли 💭

Это было интересное, но разочаровывающее путешествие. Я бы хотел, чтобы API Google Sheets имел более высокие ограничения на скрипты. Мне понравилось повторно посещать Gameboy и узнавать о Apps Script, несмотря на то, что он ограничен.

Несмотря на то, что я не получил точного эффекта, которого хотел, я получил его, чтобы нарисовать, так что я думаю, что это что-то значит. В следующий раз обязательно закину крутой проект!

Демо
Исходник

Об авторе 👨🏾‍💻

Я Грегори Гейнс, простой инженер-программист @Google, который пытается писать хорошие статьи. Если вы хотите больше контента, следите за мной в Твиттере по адресу @GregoryAGaines.

Теперь иди создай что-нибудь великое! Если у вас есть какие-либо вопросы, напишите мне в Твиттере (@GregoryAGaines); мы можем поговорить об этом.

Спасибо за прочтение!