Как создать Gameboy Advance Emulator (GBA) в браузере с помощью JavaScript

В детстве у меня не было возможности поиграть с настоящим Gameboy, но в эмуляторе Visual Boy Advance, доступном для Windows. Я помню, как часами играл в удивительные игры, такие как Pokemon, WarioLand, Castlevania, More Pokemon и т. Д. Сегодня, как разработчик, я хотел бы поделиться с вами в этой статье, как создать эмулятор GBA в вашем веб-браузере, используя библиотека GBA.js. GBA.js — это эмулятор Game Boy Advance, написанный с нуля для использования технологий HTML5, таких как Canvas и Web Audio. Включает поддержку аудио, Savegames и паузы / возобновления. Он не использует плагинов и предназначен для работы в самых современных веб-браузерах. Он размещен на GitHub и доступен по лицензии BSD с 2 пунктами.

Эта библиотека была написана Джеффри Пфау и мы очень благодарны ему за такой вклад в мир открытого кода.

Что тебе нужно знать

  • Скрипт сам по себе подражать ПЗУ полностью законно, тем не мение распространение дисков (игровых файлов) вообще не разрешено законом. Поэтому, пожалуйста, не используйте это в коммерческих целях или для других плохих вещей, особенно в странах, где это полностью не одобрено (например, в Германии). Намерения этой статьи являются чисто образовательными.
  • Вам нужно предоставить файл HTML либо с http или же https, file Протокол не разрешен, так как вы не сможете импортировать любое ПЗУ для запуска.

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

А. Простота реализации

Если вы не хотите знать, как шаг за шагом реализовать эмулятор GBA (какие файлы добавить и т. Д.), Потому что вы хотите только протестировать его, вы можете просто клонировать репозиторий с помощью git на вашем компьютере:

git clone https://github.com/endrift/gbajs.git

В качестве альтернативы вы можете скачать почтовый клон проекта а затем распакуйте файлы в нужную папку. Тогда не забудьте обслужить папку gbajs, используя некоторые http/https локальный сервер с Node.js, Apache и т. д., потому что, как упоминалось ранее, вы не можете получить доступ к файлу индекса эмулятора с помощью file:// протокол.

Например, мы используем Xampp, что упрощает всю историю http, и мы можем получить доступ к папке gbajs с помощью localhost, и мы сможем использовать эмулятор:

Эмулятор GBA JavaScript - Pokemon

Если вас интересует, как все эти вещи в основном работают и как их реализовать самостоятельно, следуйте следующему пункту.

Б. Пошаговая реализация

Чтобы создать эмулятор в браузере, мы начнем так же, как и с любой другой веб-страницей, создадим некоторую разметку, включая некоторые JS-файлы, и затем откроем ее в браузере:

1. Создание, загрузка и импорт необходимых ресурсов

Создайте базовый HTML-документ с необходимой разметкой, а именно тег Canvas с шириной и высотой исходного экрана Gameboy. Этот файл в нашем случае будет emulator.htmlКроме того, вы, очевидно, захотите использовать основные кнопки действий, доступные на консоли, такие как пауза, регулятор громкости и т. д.


GBA Rocks

App Controls

Select ROM file Upload Savegame

In-game controls

Pause game Reset Download Savegame File

Audio enabled

Change sound level

Чтобы эмулятор работал, вам нужно загрузить около 17 файлов JavaScript, содержащих необходимый код (около 200 КБ без минимизации). Эти файлы можно скачать с официальный репозиторий GBA.js на Github здесь. Когда у вас есть файлы, включите их в свой документ. Вы можете изменить структуру папок по своему усмотрению, это всего лишь пример, в котором используется та же структура исходного проекта, однако рекомендуется сохранить его, так как есть другие файлы JS, которые будут загружаться асинхронно позже, например, в js/video папка должна быть worker.js файл, иначе эмулятор не будет работать:



Вы можете минимизировать файлы, если хотите уменьшить время загрузки своей страницы. Как уже упоминалось, xhr.js файл можно опустить и вместо него добавить метод внутри файла, который позволяет загружать ПЗУ. XMLHttpRequest просто получит файл в формате arraybuffer для последующей обработки с помощью JavaScript, так что вы можете включить его непосредственно из другого файла или с помощью тега script в документе:

/**
* Loads the ROM from a file using ajax
*
* @param url
* @param callback
*/
function loadRom(url, callback) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.responseType = 'arraybuffer';
xhr.onload = function() {
callback(xhr.response)
};
xhr.send();
}

2. Загрузите файл GBA bios.bin

Затем в том же resources папку, обязательно включите bios.bin файл GBA (доступно в хранилище), этот файл должен находиться в каталоге ресурсов, так как он будет включен с использованием асинхронного запроса позже. Очевидно, вы можете изменить путь, где bios.bin файл, откуда файл должен быть импортирован позже, как только мы добавим необходимые скрипты эмулятора (шаг 3).

Если вы реализуете все в проекте и забыли включить bios.bin файл, вы начнете беспокоиться, почему он не работает, и это понятно почему (возможно, не так много). Нам нужно bios.bin файл GBA для эмуляции ПЗУ, думайте об этом, как будто вы покупаете автомобиль, но у вас нет ключа, чтобы его запустить. BIOS (ключ), имеет определенный набор инструкций, которые сообщают Эмулятору (машине), как начать. Поэтому, если вы не загрузили BIOS, эмулятор не будет работать.

3. Добавить эмулятор необходимых скриптов

Необходимые сценарии эмулятора должны обрабатывать инициализацию, загружать BIOS, изменять громкость, приостанавливать и возобновлять работу эмулятора и т. Д. Большинство из них предназначены для использования, когда пользователь нажимает на одну кнопку (шаг 4), а другие используются. внутренне другими важными функциями:

var gba;
var runCommands = [];
// Setup the emulator
try {
gba = new GameBoyAdvance();
gba.keypad.eatInput = true;
gba.setLogger(function (level, error) {
console.error(error);
gba.pause();
var screen = document.getElementById('screen');
if (screen.getAttribute('class') == 'dead') {
console.log('We appear to have crashed multiple times without reseting.');
return;
}
// Show error image in the emulator screen
// The image can be retrieven from the repository
var crash = document.createElement('img');
crash.setAttribute('id', 'crash');
crash.setAttribute('src', 'resources/crash.png');
screen.parentElement.insertBefore(crash, screen);
screen.setAttribute('class', 'dead');
});
} catch (exception) {
gba = null;
}
// Initialize emulator once the browser loads
window.onload = function () {
if (gba && FileReader) {
var canvas = document.getElementById('screen');
gba.setCanvas(canvas);
gba.logLevel = gba.LOG_ERROR;
// Load the BIOS file of GBA (change the path according to yours)
loadRom('resources/bios.bin', function (bios) {
gba.setBios(bios);
});
if (!gba.audio.context) {
// Remove the sound box if sound isn't available
var soundbox = document.getElementById('sound');
soundbox.parentElement.removeChild(soundbox);
}
} else {
var dead = document.getElementById('controls');
dead.parentElement.removeChild(dead);
}
}
function fadeOut(id, nextId, kill) {
var e = document.getElementById(id);
var e2 = document.getElementById(nextId);
if (!e) {
return;
}
var removeSelf = function () {
if (kill) {
e.parentElement.removeChild(e);
} else {
e.setAttribute('class', 'dead');
e.removeEventListener('webkitTransitionEnd', removeSelf);
e.removeEventListener('oTransitionEnd', removeSelf);
e.removeEventListener('transitionend', removeSelf);
}
if (e2) {
e2.setAttribute('class', 'hidden');
setTimeout(function () {
e2.removeAttribute('class');
}, 0);
}
}
e.addEventListener('webkitTransitionEnd', removeSelf, false);
e.addEventListener('oTransitionEnd', removeSelf, false);
e.addEventListener('transitionend', removeSelf, false);
e.setAttribute('class', 'hidden');
}
/**
* Starts the emulator with the given ROM file
*
* @param file
*/
function run(file) {
var dead = document.getElementById('loader');
dead.value = '';
var load = document.getElementById('select');
load.textContent = 'Loading...';
load.removeAttribute('onclick');
var pause = document.getElementById('pause');
pause.textContent = "PAUSE";
gba.loadRomFromFile(file, function (result) {
if (result) {
for (var i = 0; i < runCommands.length; ++i) {
runCommands[i]();
}
runCommands = [];
fadeOut('preload', 'ingame');
fadeOut('instructions', null, true);
gba.runStable();
} else {
load.textContent = 'FAILED';
setTimeout(function () {
load.textContent = 'SELECT';
load.onclick = function () {
document.getElementById('loader').click();
};
}, 3000);
}
});
}
/**
* Resets the emulator
*
*/
function reset() {
gba.pause();
gba.reset();
var load = document.getElementById('select');
load.textContent = 'SELECT';
var crash = document.getElementById('crash');
if (crash) {
var context = gba.targetCanvas.getContext('2d');
context.clearRect(0, 0, 480, 320);
gba.video.drawCallback();
crash.parentElement.removeChild(crash);
var canvas = document.getElementById('screen');
canvas.removeAttribute('class');
} else {
lcdFade(gba.context, gba.targetCanvas.getContext('2d'), gba.video.drawCallback);
}
load.onclick = function () {
document.getElementById('loader').click();
};
fadeOut('ingame', 'preload');
// Clear the ROM
gba.rom = null;
}
/**
* Stores the savefile data in the emulator.
*
* @param file
*/
function uploadSavedataPending(file) {
runCommands.push(function () {
gba.loadSavedataFromFile(file)
});
}
/**
* Toggles the state of the game
*/
function togglePause() {
var e = document.getElementById('pause');
if (gba.paused) {
gba.runStable();
e.textContent = "PAUSE";
} else {
gba.pause();
e.textContent = "UNPAUSE";
}
}
/**
* From a canvas context, creates an LCD animation that fades the content away.
*
* @param context
* @param target
* @param callback
*/
function lcdFade(context, target, callback) {
var i = 0;
var drawInterval = setInterval(function () {
i++;
var pixelData = context.getImageData(0, 0, 240, 160);
for (var y = 0; y < 160; ++y) {
for (var x = 0; x  40) {
clearInterval(drawInterval);
} else {
callback();
}
}, 50);
}
/**
* Set the volume of the emulator.
*
* @param value
*/
function setVolume(value) {
gba.audio.masterVolume = Math.pow(2, value) - 1;
}

4. Добавить сценарии действий

Сценарии действий - это просто прослушиватели событий, прикрепленные к ранее созданным (шаг 1) элементам DOM в качестве кнопки для приостановки, загрузки ПЗУ и т. Д. Они, очевидно, используют некоторые из сценариев предыдущего шага, поэтому их необходимо загружать в контексте, где ГБА существует. Таким образом, вы можете загрузить скрипты из другого файла в вашем документе или просто обернуть их в тег скрипта в вашем документе:

// If clicked, simulate click on the File Select input to load a ROM
document.getElementById("select").addEventListener("click", function(){
document.getElementById("loader").click();
}, false);
// Run the emulator with the loaded ROM
document.getElementById("loader").addEventListener("change", function(){
var ROM = this.files[0];
run(ROM);
}, false);
// If clicked, simulate click on the File Select Input to load the savegame file
document.getElementById("select-savegame-btn").addEventListener("click", function(){
document.getElementById('saveloader').click();
}, false);
// Load the savegame to the emulator
document.getElementById("saveloader").addEventListener("change", function(){
var SAVEGAME = this.files[0];
uploadSavedataPending(SAVEGAME);
}, false);
// Pause/Resume game
document.getElementById("pause").addEventListener("click", function(){
togglePause();
}, false);
// Reset game
document.getElementById("reset-btn").addEventListener("click", function(){
reset();
}, false);
// Download the savegamefile
document.getElementById("download-savegame").addEventListener("click", function(){
gba.downloadSavedata();
}, false);
// Mute/Unmute emulator
document.getElementById("audio-enabled-checkbox").addEventListener("change", function(){
gba.audio.masterEnable = this.checked;
}, false);
// Handle volume level slider
document.getElementById("volume-level-slider").addEventListener("change", function(){
var volumeLevel = this.value;
setVolume(volumeLevel);
}, false);
document.getElementById("volume-level-slider").addEventListener("input", function(){
var volumeLevel = this.value;
setVolume(volumeLevel);
}, false);
// In order to pause/resume the game when the user changes the website tab in the browser
// add the 2 following listeners to the window !
//
// This feature is problematic/tricky to handle, so you can make it better if you need to
window.onblur = function () {
if(gba.hasRom()){
var e = document.getElementById('pause');
if (!gba.paused) {
gba.pause();
e.textContent = "UNPAUSE";
console.log("Window Focused: the game has been paused");
}
}
};
window.onfocus = function () {
if(gba.hasRom()){
var e = document.getElementById('pause');
if (gba.paused) {
gba.runStable();
e.textContent = "PAUSE";
console.log("Window Focused: the game has been resumed");
}
}
}; 

С помощью этого скрипта ваше приложение наконец доступно для использования, и вы можете начать его тестировать. Запустите свой локальный сервер и перейдите к emulator.html подать и протестировать его. Например, как он загружает сохраненную игру, и мы можем продолжать играть в предыдущий игровой матч:

Сохранить игру JavaScript GBA Emulator

Как видите, вторая реализация не добавляет какого-либо стиля, но помогает понять, как работают основы библиотеки и как их легко использовать. Эмулятор сам по себе довольно функционален. Большинство игр запускаются и в них можно играть, возможно, некоторые из них все равно иногда будут зависать или зависать в определенных точках. Кроме того, другие незначительные ошибки в графике или звуке все еще присутствуют, и разработчик работает над исправлением этих ошибок. Список совместимости можно найти здесь. Если вы думаете, что нашли ошибку, сообщить об этом на трекере GitHub.

Последний пример

Следующий файл emulator.html показывает, как должен выглядеть ваш файл, включая все скрипты внутри тегов скрипта:


GBA Rocks

App Controls

Select ROM file Upload Savegame

In-game controls

Pause game Reset Download Savegame File

Audio enabled

Change sound level

var gba; var runCommands = []; // Setup the emulator try { gba = new GameBoyAdvance(); gba.keypad.eatInput = true; gba.setLogger(function (level, error) { console.error(error); gba.pause(); var screen = document.getElementById('screen'); if (screen.getAttribute('class') == 'dead') { console.log('We appear to have crashed multiple times without reseting.'); return; } // Show error image in the emulator screen // The image can be retrieven from the repository var crash = document.createElement('img'); crash.setAttribute('id', 'crash'); crash.setAttribute('src', 'resources/crash.png'); screen.parentElement.insertBefore(crash, screen); screen.setAttribute('class', 'dead'); }); } catch (exception) { gba = null; } // Initialize emulator once the browser loads window.onload = function () { if (gba && FileReader) { var canvas = document.getElementById('screen'); gba.setCanvas(canvas); gba.logLevel = gba.LOG_ERROR; // Load the BIOS file of GBA (change the path according to yours) loadRom('resources/bios.bin', function (bios) { gba.setBios(bios); }); if (!gba.audio.context) { // Remove the sound box if sound isn't available var soundbox = document.getElementById('sound'); soundbox.parentElement.removeChild(soundbox); } } else { var dead = document.getElementById('controls'); dead.parentElement.removeChild(dead); } } function fadeOut(id, nextId, kill) { var e = document.getElementById(id); var e2 = document.getElementById(nextId); if (!e) { return; } var removeSelf = function () { if (kill) { e.parentElement.removeChild(e); } else { e.setAttribute('class', 'dead'); e.removeEventListener('webkitTransitionEnd', removeSelf); e.removeEventListener('oTransitionEnd', removeSelf); e.removeEventListener('transitionend', removeSelf); } if (e2) { e2.setAttribute('class', 'hidden'); setTimeout(function () { e2.removeAttribute('class'); }, 0); } } e.addEventListener('webkitTransitionEnd', removeSelf, false); e.addEventListener('oTransitionEnd', removeSelf, false); e.addEventListener('transitionend', removeSelf, false); e.setAttribute('class', 'hidden'); } /** * Starts the emulator with the given ROM file * * @param file */ function run(file) { var dead = document.getElementById('loader'); dead.value = ''; var load = document.getElementById('select'); load.textContent = 'Loading...'; load.removeAttribute('onclick'); var pause = document.getElementById('pause'); pause.textContent = "PAUSE"; gba.loadRomFromFile(file, function (result) { if (result) { for (var i = 0; i < runCommands.length; ++i) { runCommands[i](); } runCommands = []; fadeOut('preload', 'ingame'); fadeOut('instructions', null, true); gba.runStable(); } else { load.textContent = 'FAILED'; setTimeout(function () { load.textContent = 'SELECT'; load.onclick = function () { document.getElementById('loader').click(); }; }, 3000); } }); } /** * Resets the emulator * */ function reset() { gba.pause(); gba.reset(); var load = document.getElementById('select'); load.textContent = 'SELECT'; var crash = document.getElementById('crash'); if (crash) { var context = gba.targetCanvas.getContext('2d'); context.clearRect(0, 0, 480, 320); gba.video.drawCallback(); crash.parentElement.removeChild(crash); var canvas = document.getElementById('screen'); canvas.removeAttribute('class'); } else { lcdFade(gba.context, gba.targetCanvas.getContext('2d'), gba.video.drawCallback); } load.onclick = function () { document.getElementById('loader').click(); }; fadeOut('ingame', 'preload'); // Clear the ROM gba.rom = null; } /** * Stores the savefile data in the emulator. * * @param file */ function uploadSavedataPending(file) { runCommands.push(function () { gba.loadSavedataFromFile(file) }); } /** * Toggles the state of the game */ function togglePause() { var e = document.getElementById('pause'); if (gba.paused) { gba.runStable(); e.textContent = "PAUSE"; } else { gba.pause(); e.textContent = "UNPAUSE"; } } /** * From a canvas context, creates an LCD animation that fades the content away. * * @param context * @param target * @param callback */ function lcdFade(context, target, callback) { var i = 0; var drawInterval = setInterval(function () { i++; var pixelData = context.getImageData(0, 0, 240, 160); for (var y = 0; y < 160; ++y) { for (var x = 0; x 40) { clearInterval(drawInterval); } else { callback(); } }, 50); } /** * Set the volume of the emulator. * * @param value */ function setVolume(value) { gba.audio.masterVolume = Math.pow(2, value) - 1; } // If clicked, simulate click on the File Select input to load a ROM document.getElementById("select").addEventListener("click", function(){ document.getElementById("loader").click(); }, false); // Run the emulator with the loaded ROM document.getElementById("loader").addEventListener("change", function(){ var ROM = this.files[0]; run(ROM); }, false); // If clicked, simulate click on the File Select Input to load the savegame file document.getElementById("select-savegame-btn").addEventListener("click", function(){ document.getElementById('saveloader').click(); }, false); // Load the savegame to the emulator document.getElementById("saveloader").addEventListener("change", function(){ var SAVEGAME = this.files[0]; uploadSavedataPending(SAVEGAME); }, false); // Pause/Resume game document.getElementById("pause").addEventListener("click", function(){ togglePause(); }, false); // Reset game document.getElementById("reset-btn").addEventListener("click", function(){ reset(); }, false); // Download the savegamefile document.getElementById("download-savegame").addEventListener("click", function(){ gba.downloadSavedata(); }, false); // Mute/Unmute emulator document.getElementById("audio-enabled-checkbox").addEventListener("change", function(){ gba.audio.masterEnable = this.checked; }, false); // Handle volume level slider document.getElementById("volume-level-slider").addEventListener("change", function(){ var volumeLevel = this.value; setVolume(volumeLevel); }, false); document.getElementById("volume-level-slider").addEventListener("input", function(){ var volumeLevel = this.value; setVolume(volumeLevel); }, false); // In order to pause/resume the game when the user changes the website tab in the browser // add the 2 following listeners to the window ! // // This feature is problematic/tricky to handle, so you can make it better if you need to window.onblur = function () { if(gba.hasRom()){ var e = document.getElementById('pause'); if (!gba.paused) { gba.pause(); e.textContent = "UNPAUSE"; console.log("Window Focused: the game has been paused"); } } }; window.onfocus = function () { if(gba.hasRom()){ var e = document.getElementById('pause'); if (gba.paused) { gba.runStable(); e.textContent = "PAUSE"; console.log("Window Focused: the game has been resumed"); } } };

Окончательные рекомендации

  • Проект создан для работы в здравом (недавнем) браузере, поэтому не ожидайте поддержки IE8.
  • Файлы Savegame должны быть загружены до ПЗУ, чтобы убедиться, что они работают правильно.
  • Запретите прокрутку окна, в противном случае процесс рендеринга будет тяжелым, и поэтому игровой процесс будет временно замедлен.
  • Хотя мы рассмотрели наиболее важные моменты эмулятора, возможно, мы что-то забыли, поэтому, пожалуйста, не забудьте посетить официальный репозиторий а также официальная демонстрация для дополнительной информации.

Удачного кодирования вам, милый геймер!

Ссылка на основную публикацию
Adblock
detector