Рубрики
Без рубрики

Как создать свой собственный эмулятор CHIP-8

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

Автор оригинала: FreeCodeCamp Community Member.

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

В настоящее время очень популярное использование эмуляторов – это эмулировать старые видеоигрные системы, такие как Nintendo 64, GameCube и так далее.

Например, с эмулятором Nintendo 64 мы можем запустить Nintendo 64 игры непосредственно на компьютере Windows 10, не требуя от фактической консоли. В нашем случае мы эмулируем CHIP-8 на нашей хост-системе через использование эмулятора, мы будем создавать в этой статье.

Один из самых простых способов научиться делать свои собственные эмуляторы, чтобы начать с эмулятора CHIP-8. Благодаря лишь 4 КБ памяти и 36 инструкций, вы можете быть запущены и работать с вашим собственным эмулятором CHIP-8 менее чем за день. Вы также получите знания, необходимые для перехода к большему, более глубоким эмуляторам.

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

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

Для всей всей статьи мы будем ссылаться на Чип-8 Техническая ссылка Cowgod, который объясняет каждую деталь чипа-8.

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

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

Если вы решите использовать JavaScript, вам нужно будет запустить локальный веб-сервер для тестирования. Я использую Python для этого, который позволяет запустить веб-сервер в текущей папке, запустив python3 -m http.server Отказ

Мы собираемся начать с создания index.html и still.css Файлы, затем перейти к рендереру, клавиатуре, динамике и, наконец, фактический процессор. Наша структура проекта будет выглядеть так:

- roms
- scripts
    chip8.js
    cpu.js
    keyboard.js
    renderer.js
    speaker.js
index.html
style.css

Индекс и стили

Нет ничего сумасшедших в этих двух файлах, они очень простой. index.html Файл просто загружает в стилях, создает элемент холста и загружает Чип8.js файл.




    
        
    
    
        

        
    

still.css Файл еще проще, так как единственное, что стиль, это холст, чтобы облегчить место.

canvas {
    border: 2px solid black;
}

Вам не придется снова прикоснуться к этим двум файлам по всей этой статье, но не стесняйтесь стиль страницы любым способом.

renderer.js.

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

class Renderer {

}

export default Renderer;

Конструктор (масштаб)

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

class Renderer {
    constructor(scale) {

    }
}

export default Renderer;

Нам нужно инициализировать несколько вещей в этом конструкторе. Во-первых, размер дисплея, который для CHIP-8 представляет собой 64×32 пикселей.

this.cols = 64;
this.rows = 32;

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

this.scale = scale;

this.canvas = document.querySelector('canvas');
this.ctx = this.canvas.getContext('2d');

this.canvas.width = this.cols * this.scale;
this.canvas.height = this.rows * this.scale;

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

Последний пункт, который нам нужно добавить в наш конструктор, – это массив, который будет действовать как наш дисплей. Поскольку дисплей CHIP-8 – 64×32 пикселей, размер нашего массива просто 64 * 32 (Cols * Rows), или 2048. В основном мы представляем каждый пиксель, на (1) или выключенном (0), на Чип-8 дисплей с этим массивом.

this.display = new Array(this.cols * this.rows);

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

setpixel (x, y)

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

Говоря о переключении пикселей на или выключении, давайте создадим функцию, которая отвечает за это. Мы назовем функцию setpixel И это займет х и y положение в качестве параметров.

setPixel(x, y) {

}

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

if (x > this.cols) {
    x -= this.cols;
} else if (x < 0) {
    x += this.cols;
}

if (y > this.rows) {
    y -= this.rows;
} else if (y < 0) {
    y += this.rows;
}

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

let pixelLoc = x + (y * this.cols);

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

this.display[pixelLoc] ^= 1;

Все, что эта линия делает переключение значения в Pixelloc (От 0 до 1 или 1 до 0). Значение 1 означает, что пиксель должен быть нарисован, значение 0 означает пиксель должен быть удален. Отсюда мы просто вернем ценность для обозначения того, был ли пиксель удален или нет.

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

return !this.display[pixelLoc];

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

Чисто()

Эта функция полностью очищает наши Дисплей массив путем повторной повторной реализации.

clear() {
    this.display = new Array(this.cols * this.rows);
}

оказывать()

оказывать Функция отвечает за рендурирование пикселей в Дисплей массив на экран. Для этого проекта он будет работать в 60 раз в секунду.

render() {
    // Clears the display every render cycle. Typical for a render loop.
    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);

    // Loop through our display array
    for (let i = 0; i < this.cols * this.rows; i++) {
        // Grabs the x position of the pixel based off of `i`
        let x = (i % this.cols) * this.scale;

        // Grabs the y position of the pixel based off of `i`
        let y = Math.floor(i / this.cols) * this.scale;

        // If the value at this.display[i] == 1, then draw a pixel.
        if (this.display[i]) {
            // Set the pixel color to black
            this.ctx.fillStyle = '#000';

            // Place a pixel at position (x, y) with a width and height of scale
            this.ctx.fillRect(x, y, this.scale, this.scale);
        }
    }
}

TestRender ()

Для целей тестирования давайте создадим функцию, которая нарисует пару пикселей на экране.

testRender() {
    this.setPixel(0, 0);
    this.setPixel(5, 2);
}

Full Renderer.js код

чип8.js.

Теперь, когда у нас есть наш рендер, нам нужно инициализировать его в наших Чип8.js файл.

import Renderer from './renderer.js';

const renderer = new Renderer(10);

Отсюда нужно создать цикл, которая работает, в соответствии с технической ссылкой, 60 Гц или 60 кадрами в секунду. Как и наша функция Render, это не CHIP-8, специфичный и может быть немного изменен для работы практически любым другим проектом.

let loop;

let fps = 60, fpsInterval, startTime, now, then, elapsed;

function init() {
    fpsInterval = 1000 / fps;
    then = Date.now();
    startTime = then;

    // TESTING CODE. REMOVE WHEN DONE TESTING.
    renderer.testRender();
    renderer.render();
    // END TESTING CODE

    loop = requestAnimationFrame(step);
}

function step() {
    now = Date.now();
    elapsed = now - then;

    if (elapsed > fpsInterval) {
        // Cycle the CPU. We'll come back to this later and fill it out.
    }

    loop = requestAnimationFrame(step);
}

init();

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

keyboard.js.

Ссылка клавиатуры

Техническая ссылка говорит нам, что CHEP-8 использует 16-кверную клавиатуру Hex, которая выложена следующим образом:

1 2 3 C
4 5 6 D
7 8 9 E
A 0 B F

Чтобы сделать эту работу над современными системами, мы должны сопоставить ключ на нашей клавиатуре к каждому из этих клавиш CHEP-8. Мы сделаем это в нашем конструкторе, а также несколько других вещей.

конструктор()

class Keyboard {
    constructor() {
        this.KEYMAP = {
            49: 0x1, // 1
            50: 0x2, // 2
            51: 0x3, // 3
            52: 0xc, // 4
            81: 0x4, // Q
            87: 0x5, // W
            69: 0x6, // E
            82: 0xD, // R
            65: 0x7, // A
            83: 0x8, // S
            68: 0x9, // D
            70: 0xE, // F
            90: 0xA, // Z
            88: 0x0, // X
            67: 0xB, // C
            86: 0xF  // V
        }

        this.keysPressed = [];

        // Some Chip-8 instructions require waiting for the next keypress. We initialize this function elsewhere when needed.
        this.onNextKeyPress = null;

        window.addEventListener('keydown', this.onKeyDown.bind(this), false);
        window.addEventListener('keyup', this.onKeyUp.bind(this), false);
    }
}

export default Keyboard;

В конструкторе мы создали клавиши отображения Keymap, на нашей клавиатуре к клавишам на клавиатуре CHIP-8. Как и что, у нас есть массив для отслеживания нажатых клавиш, нулевая переменная (которую мы поговорим позже), и пару слушателей событий для обработки ввода клавиатуры.

Iskeypressed (ключевой код)

Нам нужен способ проверить, нажата ли определенный ключ. Это просто проверит Клавиатура Массив для указанного чипа-8 ключевой код Отказ

isKeyPressed(keyCode) {
    return this.keysPressed[keyCode];
}

OnkeyDown (событие)

В нашем конструкторе мы добавили КДУЩЬЮ Слушатель событий, который позвонит эту функцию при запуске.

onKeyDown(event) {
    let key = this.KEYMAP[event.which];
    this.keysPressed[key] = true;

    // Make sure onNextKeyPress is initialized and the pressed key is actually mapped to a Chip-8 key
    if (this.onNextKeyPress !== null && key) {
        this.onNextKeyPress(parseInt(key));
        this.onNextKeyPress = null;
    }
}

Все, что мы делаем здесь, добавляют нажатый ключ к нашему Клавиатура Массив и работает onnnextkeyPlepress Если он инициализирован, и был нажат допустимый ключ.

Давайте поговорим об этом, если заявление. Один из инструкций CHEP-8 ( FX0A ) ждет нажатия клавиши перед продолжением выполнения. Мы сделаем Fx0a Инструкция инициализация onnnextkeyPlepress Функция, которая позволит нам подражать этому поведению ждать до следующего ключа. Как только мы пишем эту инструкцию, я объясню это более подробно, как это должно иметь больше смысла, когда вы его видите.

Onkeyup (событие)

У нас также есть слушатель событий для обработки keyup События, и эта функция будет вызвана, когда это событие срабатывает.

onKeyUp(event) {
    let key = this.KEYMAP[event.which];
    this.keysPressed[key] = false;
}

Полная клавиатура

чип8.js.

С созданным класса клавиатуры мы можем вернуться в Чип8.js и подключить клавиатуру.

import Renderer from './renderer.js';
import Keyboard from './keyboard.js'; // NEW

const renderer = new Renderer(10);
const keyboard = new Keyboard(); // NEW

Speaker.js.

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

конструктор

class Speaker {
    constructor() {
        const AudioContext = window.AudioContext || window.webkitAudioContext;

        this.audioCtx = new AudioContext();

        // Create a gain, which will allow us to control the volume
        this.gain = this.audioCtx.createGain();
        this.finish = this.audioCtx.destination;

        // Connect the gain to the audio context
        this.gain.connect(this.finish);
    }
}

export default Speaker;

Все, что мы делаем здесь, создает AudioContext И подключение к нему усиление, чтобы мы могли контролировать громкость. Я не буду добавлять контроль громкости в этом руководстве, но если вы хотите добавить его, вы просто используете следующее:

// Mute the audio
this.gain.setValueAtTime(0, this.audioCtx.currentTime);
// Unmute the audio
this.gain.setValueAtTime(1, this.audioCtx.currentTime);

Играть (частота)

Эта функция делает именно то, что свидетельствует имя: воспроизводит звук на желаемой частоте.

play(frequency) {
    if (this.audioCtx && !this.oscillator) {
        this.oscillator = this.audioCtx.createOscillator();

        // Set the frequency
        this.oscillator.frequency.setValueAtTime(frequency || 440, this.audioCtx.currentTime);

        // Square wave
        this.oscillator.type = 'square';

        // Connect the gain and start the sound
        this.oscillator.connect(this.gain);
        this.oscillator.start();
    }
}

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

останавливаться()

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

stop() {
    if (this.oscillator) {
        this.oscillator.stop();
        this.oscillator.disconnect();
        this.oscillator = null;
    }
}

Все это делает, это останавливать звук, отключить его и устанавливать его на нулю, чтобы он мог быть повторно повторно в Играть () Отказ

Full Speaker.js код

чип8.js.

Теперь мы можем подключить динамик до нашего главного Чип8.js файл.

import Renderer from './renderer.js';
import Keyboard from './keyboard.js';
import Speaker from './speaker.js'; // NEW

const renderer = new Renderer(10);
const keyboard = new Keyboard();
const speaker = new Speaker(); // NEW

CPU.JS.

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

Конструктор (рендерер, клавиатура, динамик)

Нам нужно инициализировать несколько специфических переменных CHIP-8 в нашем конструкторе вместе с несколькими другими переменными. Мы будем смотреть на Раздел 2 технической справки, чтобы выяснить спецификации для нашего эмулятора CHIP-8.

Вот спецификации для чипа-8:

  • 4 КБ (4096 байтов) памяти
  • 16 8-битных регистров
  • 16-битный регистр ( it.i ) для хранения адресов памяти
  • Два таймера. Один для задержки, и один для звука.
  • Счетчик программы, который хранит адрес, который в настоящее время выполняется
  • Массив для представления стека

У нас также есть переменная, которая хранит, приостановлен ли эмулятор или нет, и скорость выполнения эмулятора.

class CPU {
    constructor(renderer, keyboard, speaker) {
        this.renderer = renderer;
        this.keyboard = keyboard;
        this.speaker = speaker;

        // 4KB (4096 bytes) of memory
        this.memory = new Uint8Array(4096);

        // 16 8-bit registers
        this.v = new Uint8Array(16);

        // Stores memory addresses. Set this to 0 since we aren't storing anything at initialization.
        this.i = 0;

        // Timers
        this.delayTimer = 0;
        this.soundTimer = 0;

        // Program counter. Stores the currently executing address.
        this.pc = 0x200;

        // Don't initialize this with a size in order to avoid empty results.
        this.stack = new Array();

        // Some instructions require pausing, such as Fx0A.
        this.paused = false;

        this.speed = 10;
    }
}

export default CPU;

loadspritesintomemory ()

Для этой функции мы будем ссылаться на Раздел 2.4 технической справки.

Чип-8 использует 16, 5 байтов, спрайтов. Эти спрайты – это просто шестнадцатеричные цифры 0- F. Вы можете увидеть все спрайты с их двоичными и шестнадцатеричными значениями в разделе 2.4.

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

Ссылка гласит, что эти спрайты хранятся в секции интерпретатора памяти (0x000 до 0x1fff). Давайте пойдем вперед и посмотрим на код для этой функции, чтобы увидеть, как это сделано.

loadSpritesIntoMemory() {
    // Array of hex values for each sprite. Each sprite is 5 bytes.
    // The technical reference provides us with each one of these values.
    const sprites = [
        0xF0, 0x90, 0x90, 0x90, 0xF0, // 0
        0x20, 0x60, 0x20, 0x20, 0x70, // 1
        0xF0, 0x10, 0xF0, 0x80, 0xF0, // 2
        0xF0, 0x10, 0xF0, 0x10, 0xF0, // 3
        0x90, 0x90, 0xF0, 0x10, 0x10, // 4
        0xF0, 0x80, 0xF0, 0x10, 0xF0, // 5
        0xF0, 0x80, 0xF0, 0x90, 0xF0, // 6
        0xF0, 0x10, 0x20, 0x40, 0x40, // 7
        0xF0, 0x90, 0xF0, 0x90, 0xF0, // 8
        0xF0, 0x90, 0xF0, 0x10, 0xF0, // 9
        0xF0, 0x90, 0xF0, 0x90, 0x90, // A
        0xE0, 0x90, 0xE0, 0x90, 0xE0, // B
        0xF0, 0x80, 0x80, 0x80, 0xF0, // C
        0xE0, 0x90, 0x90, 0x90, 0xE0, // D
        0xF0, 0x80, 0xF0, 0x80, 0xF0, // E
        0xF0, 0x80, 0xF0, 0x80, 0x80  // F
    ];

    // According to the technical reference, sprites are stored in the interpreter section of memory starting at hex 0x000
    for (let i = 0; i < sprites.length; i++) {
        this.memory[i] = sprites[i];
    }
}

Все, что мы сделали, была петля через каждый байт в Спрайты Массив и хранится в памяти, начиная с шестигранства 0x000 Отказ

loadprogramintomemory (программа)

Для того, чтобы запустить ROM, мы должны загрузить их в память. Это намного проще, тогда это может звучать. Все, что мы должны сделать, это петлю через содержимое ПЗУ/программы и хранить его в памяти. Техническая ссылка специально рассказывает нам что «большинство программ CHIP-8 начнут на месте 0x200». Поэтому, когда мы загружаем ПЗУ в память, мы начинаем в 0x200 и приращение оттуда.

loadProgramIntoMemory(program) {
    for (let loc = 0; loc < program.length; loc++) {
        this.memory[0x200 + loc] = program[loc];
    }
}

loadrom (romname)

Теперь у нас есть способ загрузить ROM в память, но мы должны сначала схватить ромку из файловой системы, прежде чем она может быть загружена в память. За это работать, вы должны иметь ром. Я включил несколько в Github Repo Для вас скачать и вводить в Римс папка вашего проекта.

JavaScript предоставляет способ сделать HTTP-запрос и получить файл. Я добавил комментарии к коду ниже, чтобы объяснить, что происходит:

loadRom(romName) {
    var request = new XMLHttpRequest;
    var self = this;

    // Handles the response received from sending (request.send()) our request
    request.onload = function() {
        // If the request response has content
        if (request.response) {
            // Store the contents of the response in an 8-bit array
            let program = new Uint8Array(request.response);

            // Load the ROM/program into memory
            self.loadProgramIntoMemory(program);
        }
    }

    // Initialize a GET request to retrieve the ROM from our roms folder
    request.open('GET', 'roms/' + romName);
    request.responseType = 'arraybuffer';

    // Send the GET request
    request.send();
}

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

цикл()

Я думаю, что все будет легче понять все, если вы можете увидеть, что происходит каждый раз, когда циклы ЦП. Это функция, которую мы будем звонить в нашу шаг Функция в Чип8.js , который, если вы помните, выполняется примерно в 60 раз в секунду. Мы собираемся взять эту функциональную часть по частям.

На данный момент функции вызываются внутри цикл еще предстоит создать. Мы скоро создадим их.

Первый кусок кода в наших цикл Функция является для цикла, которая обрабатывает выполнение инструкций. Это где наш Скорость Переменная приходит в игру. Чем выше это значение, тем больше инструкции будут выполняться каждый цикл.

cycle() {
    for (let i = 0; i < this.speed; i++) {

    }
}

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

cycle() {
    for (let i = 0; i < this.speed; i++) {
        if (!this.paused) {

        }
    }
}

Если вы посмотрите на Раздел 3.1 , вы можете увидеть все разные инструкции и их оправки. Они выглядят что-то вроде 00e0 или 9xy0 дать несколько примеров. Таким образом, наша задача состоит в том, чтобы схватить этот операционный код из памяти и пройти наряду к другой функции, которая будет справиться с выполнением этой инструкции. Давайте сначала посмотрим на код, а затем я объясню это:

cycle() {
    for (let i = 0; i < this.speed; i++) {
        if (!this.paused) {
            let opcode = (this.memory[this.pc] << 8 | this.memory[this.pc + 1]);
            this.executeInstruction(opcode);
        }
    }
}

Давайте посмотрим на эту строку, в частности: Пусть opcode = (this.mory [this.pc] << 8 | Это. Моржество [this.pc + 1]); Отказ Для тех, которые не очень знакомы с битовыми операциями, это может быть очень пугающим.

Прежде всего, каждая инструкция – это 16 бит (2 байта) длинные ( 3.0 ), но наша память состоит из 8 бит (1 байт) кусков. Это означает, что мы должны объединить две части памяти, чтобы получить полный OPCode. Вот почему у нас есть this.pc и this.pc + 1 в линейке кода выше. Мы просто хватаем обе половины оперативного кода.

Но вы не можете просто объединить два, 1-байтовые значения, чтобы получить 2-байтовое значение. Чтобы правильно сделать это, нам нужно перенести первую память, this.mory [this.pc] 8 бит остались, чтобы сделать его 2 байта долго. В самых базовых условиях это добавит два нуля или точнее, точнее шестнадцатеричное значение 0x00 на правую часть нашего 1-байтового значения, делая его 2 байта.

Например, смещение шестигранников 0x11 8 битов осталось, даст нам шестрец 0x1100 Отказ Оттуда мы побили или ( | ) это со второй частью памяти, this.moryory [this.pc + 1]) Отказ

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

Давайте предположим несколько значений, каждый 1 байт в размере:

this.mory [this.pc] Это. Memory [this.pc + +

Сдвиг ПК 8 бит (1 байт) осталось, чтобы сделать его 2 байта:

ПК

Побитовые или ПК и ПК + 1 :

ПК |. ПК +

или же

0x1000 |.

Наконец, мы хотим обновить наши таймеры, когда работают эмулятор (не приостановлено), воспроизводить звуки и визуализацию спрайтов на экране:

cycle() {
    for (let i = 0; i < this.speed; i++) {
        if (!this.paused) {
            let opcode = (this.memory[this.pc] << 8 | this.memory[this.pc + 1]);
            this.executeInstruction(opcode);
        }
    }

    if (!this.paused) {
        this.updateTimers();
    }

    this.playSound();
    this.renderer.render();
}

Эта функция – это мозг нашего эмулятора в пути. Он обрабатывает выполнение инструкций, обновления таймеров, воспроизведение звука и отображает содержимое на экране.

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

UpdateTimers ()

Давайте перейдем к Раздел 2.5 и настроить логику для таймеров и звука.

Каждый таймер, задержка и звук, уменьшение на 1 со скоростью 60 Гц. Другими словами, каждые 60 кадров наши таймеры будут уменьшаться на 1.

updateTimers() {
    if (this.delayTimer > 0) {
        this.delayTimer -= 1;
    }

    if (this.soundTimer > 0) {
        this.soundTimer -= 1;
    }
}

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

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

Playsound ()

Чтобы подтвердить, до тех пор, пока звуковой таймер больше нуля, мы хотим воспроизвести звук. Мы будем использовать Играть Функция от нашего Спикер Класс, который мы сделали ранее, чтобы сыграть звук с частотой 440.

playSound() {
    if (this.soundTimer > 0) {
        this.speaker.play(440);
    } else {
        this.speaker.stop();
    }
}

ExecuteInstraction (OPCODE)

Для всей всей функции мы будем ссылаться на Раздел 3.0 и 3.1 технической справки.

Это окончательная функция, которую нам нужен для этого файла, и этот длинный. Мы должны выписать логику для всех 36 инструкций CHIP-8. К счастью, большинство из этих инструкций требуют всего несколько строк кода.

Первая информация о том, что все инструкции длится 2 байта. Поэтому каждый раз, когда мы выполняем инструкцию или запустите эту функцию, мы должны увеличить счетчик программы ( it.pc ) на 2, поэтому CPU знает, где находится следующая инструкция.

executeInstruction(opcode) {
    // Increment the program counter to prepare it for the next instruction.
    // Each instruction is 2 bytes long, so increment it by 2.
    this.pc += 2;
}

Давайте посмотрим на эту часть раздела 3.0 сейчас:

In these listings, the following variables are used:

nnn or addr - A 12-bit value, the lowest 12 bits of the instruction
n or nibble - A 4-bit value, the lowest 4 bits of the instruction
x - A 4-bit value, the lower 4 bits of the high byte of the instruction
y - A 4-bit value, the upper 4 bits of the low byte of the instruction
kk or byte - An 8-bit value, the lowest 8 bits of the instruction

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

Эти два значения являются каждым 4 битом (aka. Половину байта или рысик) в размере. х Значение находится в нижних 4 битах высокого байта и y расположен в верхних 4 битах низкого байта.

Например, если у нас есть инструкция 0x5460 высокий байт будет 0x54 и низкий байт будет 0x60 Отказ Нижние 4 бита, или снимают, высокого байта будут 0x4 и верхние 4 бита низкого байта будут 0x6 Отказ Поэтому в этом примере х и y = 0x6 Отказ

Зная все это, давайте напишем код, который будет схватить х и y значения.

executeInstruction(opcode) {
    this.pc += 2;

    // We only need the 2nd nibble, so grab the value of the 2nd nibble
    // and shift it right 8 bits to get rid of everything but that 2nd nibble.
    let x = (opcode & 0x0F00) >> 8;

    // We only need the 3rd nibble, so grab the value of the 3rd nibble
    // and shift it right 4 bits to get rid of everything but that 3rd nibble.
    let y = (opcode & 0x00F0) >> 4;
}

Объяснить это, давайте еще раз предположим, что у нас есть инструкция 0x5460 Отказ Если мы & (побитовой и) эта инструкция с шестнадцатеричным значением 0x0f00 Мы в конечном итоге с 0x0400 Отказ Сдвиньте, что 8 битов вправо, и мы в конечном итоге с 0x04 или 0x4 Отказ То же самое с y Отказ Мы & Инструкция с шестнадцатеричным значением 0x00f0 и получить 0x0060 Отказ Сдвиньте, что 4 бита вправо, и мы в конечном итоге с 0x006 или 0x6 Отказ

Теперь для веселой части, написание логики для всех 36 инструкций. Для каждой инструкции, прежде чем написать код, я настоятельно рекомендую прочитать, что делает эту инструкцию в технической справоме, поскольку вы поймете это намного лучше.

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

switch (opcode & 0xF000) {
    case 0x0000:
        switch (opcode) {
            case 0x00E0:
                break;
            case 0x00EE:
                break;
        }

        break;
    case 0x1000:
        break;
    case 0x2000:
        break;
    case 0x3000:
        break;
    case 0x4000:
        break;
    case 0x5000:
        break;
    case 0x6000:
        break;
    case 0x7000:
        break;
    case 0x8000:
        switch (opcode & 0xF) {
            case 0x0:
                break;
            case 0x1:
                break;
            case 0x2:
                break;
            case 0x3:
                break;
            case 0x4:
                break;
            case 0x5:
                break;
            case 0x6:
                break;
            case 0x7:
                break;
            case 0xE:
                break;
        }

        break;
    case 0x9000:
        break;
    case 0xA000:
        break;
    case 0xB000:
        break;
    case 0xC000:
        break;
    case 0xD000:
        break;
    case 0xE000:
        switch (opcode & 0xFF) {
            case 0x9E:
                break;
            case 0xA1:
                break;
        }

        break;
    case 0xF000:
        switch (opcode & 0xFF) {
            case 0x07:
                break;
            case 0x0A:
                break;
            case 0x15:
                break;
            case 0x18:
                break;
            case 0x1E:
                break;
            case 0x29:
                break;
            case 0x33:
                break;
            case 0x55:
                break;
            case 0x65:
                break;
        }

        break;

    default:
        throw new Error('Unknown opcode ' + opcode);
}

Как вы можете видеть из Переключатель (OPCODE & 0xF000) Мы схватываем верхние 4 бита самого значительного байта оперативного кода. Если вы взглянуте на различные инструкции в техническом справоме, вы заметите, что мы можем сузить разные OPCodes тем самым первым выключением.

0nnn – sys addr

Этот операционный код можно игнорировать.

00E0 – CLS.

Очистить дисплей.

case 0x00E0:
    this.renderer.clear();
    break;

00ee – решить

Поп-последний элемент в стек Массив и храните его в this.pc Отказ Это вернет нас из подпрограммы.

case 0x00EE:
    this.pc = this.stack.pop();
    break;

Технические эталонные состояния этой инструкции также «вычитают 1 от указателя стека». Указатель стека используется для указывает на верхний уровень стека. Но благодаря нашему стек Массив, нам не нужно беспокоиться о том, где верхняя часть стека, поскольку она обрабатывается массивом. Так что для остальных инструкций, если он что-то говорит о указателе стека, вы можете смело игнорировать его.

1nnn – JP Addr

Установите счетчик программы на значение, сохраненное в NNN Отказ

case 0x1000:
    this.pc = (opcode & 0xFFF);
    break;

0xfff хватает ценность NNN Отказ Итак, 0x1426 & 0xfff даст нам 0x426 а потом мы храним это в this.pc Отказ

2nnn – call addr

Для этого техническая ссылка говорит, что мы должны увеличить указатель стека, чтобы он указывал на текущее значение this.pc Отказ Опять же, мы не используем указатель стека в нашем проекте как нашего стек Массив обрабатывает, что для нас. Так вместо того, чтобы увеличивать это, мы просто нажимаем this.pc на стек, который даст нам тот же результат. И просто как с оперативным кодом 1nnn Мы берем ценность NNN и хранить это в this.pc Отказ

case 0x2000:
    this.stack.push(this.pc);
    this.pc = (opcode & 0xFFF);
    break;

3xkk – SE VX, Байт

Это где наш х Значение, которое мы рассчитали выше, вступают в игру.

Эта инструкция сравнивает значение, хранящуюся в х Регистрация ( vx ) значение кк Отказ Обратите внимание, что V означает регистрацию и значение, следующее, в этом случае х , это номер реестра. Если они равны, мы увеличиваем счетчик программы на 2, эффективно пропускав следующую инструкцию.

case 0x3000:
    if (this.v[x] === (opcode & 0xFF)) {
        this.pc += 2;
    }
    break;

opcode & 0xff Часть утверждения IF просто захватывает последний байт OPCode. Это кк часть оперативного кода.

4xkk – SNE VX, Байт

Эта инструкция очень похожа на 3xkk , но вместо этого пропускает следующую инструкцию, если Vx и кк не равны.

case 0x4000:
    if (this.v[x] !== (opcode & 0xFF)) {
        this.pc += 2;
    }
    break;

5xy0 – SE VX, VY

Теперь мы используем обоих х и y Отказ Эта инструкция, как предыдущие два, пропустит следующую инструкцию, если условие будет выполнено. В случае этой инструкции, если Vx равно Vy Мы пропускаем следующую инструкцию.

case 0x5000:
    if (this.v[x] === this.v[y]) {
        this.pc += 2;
    }
    break;

6xkk – ld vx, байт

Эта инструкция установит значение Vx к значению кк Отказ

case 0x6000:
    this.v[x] = (opcode & 0xFF);
    break;

7xkk – добавить vx, байт

Эта инструкция добавляет кк к Vx Отказ

case 0x7000:
    this.v[x] += (opcode & 0xFF);
    break;

8xy0 – ld vx, vy

Прежде чем обсуждать эту инструкцию, я хотел бы объяснить, что происходит с Переключатель (OPCode & 0xf) Отказ Почему выключатель внутри переключателя?

Разумные рассуждения, у нас есть несколько разных инструкций, которые подпадают под Шкаф 0x8000: Отказ Если вы взглянуте на те инструкции в техническом справоме, вы заметите последнюю карту каждого из этих инструкций заканчиваются значением 0-7 или Е Отказ

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

С таком объяснили, давайте перейдем к инструкции. Ничто не сумасшедшее с этим, просто устанавливая ценность Vx равна ценности Vy Отказ

case 0x0:
    this.v[x] = this.v[y];
    break;

8xy1 – или vx, vy

Установить Vx к значению Vx или vy Отказ

case 0x1:
    this.v[x] |= this.v[y];
    break;

8xy2 – и vx, vy

Установить Vx равна ценности VX и VY Отказ

case 0x2:
    this.v[x] &= this.v[y];
    break;

8xy3 – Xor VX, VY

Установить Vx равна ценности Vx xor vy Отказ

case 0x3:
    this.v[x] ^= this.v[y];
    break;

8xy4 – добавить vx, vy

Эти инструкции наборы Vx к Vx + vy Отказ Звучит легко, но для этого есть немного больше. Если мы прочитаем описание этой инструкции, предусмотренного в технической справоме, он говорит:

Если результат превышает 8 битов (то есть,> 255,) VF, установлен на 1, в противном случае 0. только самые низкие 8 битов результата сохраняются и хранятся в VX.

case 0x4:
    let sum = (this.v[x] += this.v[y]);

    this.v[0xF] = 0;

    if (sum > 0xFF) {
        this.v[0xF] = 1;
    }

    this.v[x] = sum;
    break;

Принимая эту строку по линии, мы сначала добавьте this.v [y] к this.v [x] и хранить это значение в переменной сумма Отказ Оттуда мы устанавливаем this.v [0xf] или VF до 0. Мы сделаем это, чтобы избежать необходимости использовать оператор IF-ELFE на следующей строке. Если сумма превышает 255, или Hex 0xff , мы устанавливаем VF до 1. Наконец, мы устанавливаем this.v [x] или Vx к сумме.

Вам может быть интересно, как мы ходим, чтобы «только самые низкие 8 бит результата хранятся и хранятся в VX». Благодаря this.v Быть Uint8Array , любое значение более 8 битов автоматически имеет нижние, крайневые, 8 битов, взятые и сохраненные в массиве. Поэтому нам не нужно ничего особенного с ним.

Позвольте мне предоставить вам пример, чтобы сделать больше смысла этого. Предположим, мы стараемся поставить десятичные 257 в this.v множество. В двоичном количестве это значение 100000001 9-битное значение. Когда мы пытаемся сохранить это 9-битное значение в массиве, это будет только снизить 8 бит. Это означает двоичный 00000001 , который 1 в десятичном периоде будет храниться в this.v Отказ

8xy5 – Sub VX, VY

Эта инструкция вычитает Vy от Vx Отказ Просто как переполнение обрабатывается в предыдущей инструкции, мы должны обрабатывать underflow для этого.

case 0x5:
    this.v[0xF] = 0;

    if (this.v[x] > this.v[y]) {
        this.v[0xF] = 1;
    }

    this.v[x] -= this.v[y];
    break;

Еще раз, так как мы используем Uint8Array , нам не нужно ничего делать, чтобы справиться с подведением, поскольку это позаботится о нас. SO -1 станет 255, -2 становится 254 и так далее.

8xy6 – SR VX {, vy}

case 0x6:
    this.v[0xF] = (this.v[x] & 0x1);

    this.v[x] >>= 1;
    break;

Эта линия this.v [0xf] = (this.v [x] & 0x1); собирается определить наименее значимый бит и установить VF соответственно.

Это намного легче понимать, если вы посмотрите на его двоичное представление. Если Vx , в бинарном, это 1001 , VF будет установлен на 1, так как наименее значимый бит 1. Если Vx это 1000 , VF будет установлен на 0.

8xy7 – subn vx, vy

case 0x7:
    this.v[0xF] = 0;

    if (this.v[y] > this.v[x]) {
        this.v[0xF] = 1;
    }

    this.v[x] = this.v[y] - this.v[x];
    break;

Эта инструкция вычитает Vx от Vy и хранит результат в Vx Отказ Если Vy больше, чем Vx нам нужно хранить 1 в VF , в противном случае мы храним 0.

8xye – shl vx {, vy}

Эта инструкция не только сменит Vx слева 1, но и наборы VF на 0, либо 1 в зависимости от того, если условие выполняется.

case 0xE:
    this.v[0xF] = (this.v[x] & 0x80);
    this.v[x] <<= 1;
    break;

Первая строка кода, this.v [0xf] = (this.v [x] & 0x80); , хватает самый значительный бит Vx и хранение того, что в VF Отказ Чтобы объяснить это, у нас есть 8-битный регистр, Vx И мы хотим получить самые значительные или левые, бит. Для этого нам нужно и Vx с двоичным 10000000 или 0x80 в гексе. Это будет выполнять настройки VF на правильное значение.

После этого мы просто умножаем Vx на 2, сдвигая его осталось 1.

9xy0 – SNE VX, VY

Эта инструкция просто увеличивает счетчик программы на 2, если Vx и Vy не равны.

case 0x9000:
    if (this.v[x] !== this.v[y]) {
        this.pc += 2;
    }
    break;

Ann – Li, Addr

Установите значение регистра Я к NNN Отказ Если код OPCode 0xa740 Тогда (OPCODE & 0XFFF) вернется 0x740 Отказ

case 0xA000:
    this.i = (opcode & 0xFFF);
    break;

BNNN – JP V0, ADDR

Установите счетчик программы ( it.pc ) на NNN Плюс значение регистра 0 ( v0 ).

case 0xB000:
    this.pc = (opcode & 0xFFF) + this.v[0];
    break;

CXKK – RND VX, BYTE

case 0xC000:
    let rand = Math.floor(Math.random() * 0xFF);

    this.v[x] = rand & (opcode & 0xFF);
    break;

Создайте случайное число в диапазоне 0-255, а затем и что с самым низким байтом OPCode. Например, если код OPCode 0xb849 Тогда (OPCODE & 0XFF) вернется 0x49 Отказ

DXYN – DRW VX, VY, Nibble

Это большой! Эта инструкция обрабатывает чертеж и стирание пикселей на экране. Я собираюсь предоставить вам весь код и объяснить его строку.

case 0xD000:
    let width = 8;
    let height = (opcode & 0xF);

    this.v[0xF] = 0;

    for (let row = 0; row < height; row++) {
        let sprite = this.memory[this.i + row];

        for (let col = 0; col < width; col++) {
            // If the bit (sprite) is not 0, render/erase the pixel
            if ((sprite & 0x80) > 0) {
                // If setPixel returns 1, which means a pixel was erased, set VF to 1
                if (this.renderer.setPixel(this.v[x] + col, this.v[y] + row)) {
                    this.v[0xF] = 1;
                }
            }

            // Shift the sprite left 1. This will move the next next col/bit of the sprite into the first position.
            // Ex. 10010000 << 1 will become 0010000
            sprite <<= 1;
        }
    }

    break;

У нас есть ширина Переменная установлена в 8, потому что каждый SPRITE ширина составляет 8 пикселей, поэтому безопасно для жесткого крепления, что значение. Далее мы устанавливаем Высота к значению последнего раскручивания ( n ) OPCode. Если наш операционный код 0xd235 , Высота будет установлен на 5. Оттуда мы устанавливаем VF до 0, которое, если необходимо, будет установлено на 1 позже, если пиксели стерты.

Теперь на петли. Помните, что спрайт выглядит что-то подобное:

11110000
10010000
10010000
10010000
11110000

Наш код собирается строка по ряду (First для loop), то он собирается бит по битам или столбцу по столбцу (второй для loop) через этот спрайт.

Этот кусок кода, let.memory [it.i + ряд]; , захватывает 8-битные памяти или один ряд спрайта, который хранится в Это. + ряд Отказ Технические ссылочные состояния мы начинаем по адресу, хранящему в Я или Это. я В нашем случае, когда мы читаем спрайты из памяти.

В нашей второй для петля, у нас есть Если Заявление, которое хватает левый бит и проверяет, если это больше 0.

Значение 0 указывает на то, что спрайт не имеет пикселя в этом месте, поэтому нам не нужно беспокоиться о рисовании или стирании. Если значение 1, мы переходим к другому, если утверждение, которое проверяет возвращаемое значение setpixel Отказ Давайте посмотрим на значения, переданные в эту функцию.

Наше setpixel Звонок выглядит так: this.renderer.Setpixel (this.v [x] + col, this.v [y] + ряд) Отказ Согласно технической справке, х и y Позиции расположены в Vx и Vy соответственно. Добавьте Col номер для Vx и ряд номер для Vy И вы получите желаемую позицию для рисования/стереть пиксель.

Если setpixel Возвращает 1, мы стереть пиксель и установить VF до 1. Если он возвращается 0, мы ничего не делаем, сохраняя ценность VF равный 0.

Наконец, мы сдвигаем спрайт влево 1 бит. Это позволяет нам пройти каждый бит спрайта.

Например, если Сприт в настоящее время установлено на 10010000 , это станет 0010000 После смещения слева. Оттуда мы можем пройти еще одну итерацию нашего внутреннего для петля, чтобы определить, нарисовать ли пиксель или нет. И продолжение этого процесса, пока мы не достигнем конца или нашего спрайта.

EX9E – SKP VX

Это довольно просто и просто пропускает следующую инструкцию, если ключ хранится в Vx нажимается, увеличивая счетчик программы на 2.

case 0x9E:
    if (this.keyboard.isKeyPressed(this.v[x])) {
        this.pc += 2;
    }
    break;

EXA1 – SKNP VX

Это делает противоположность предыдущей инструкции. Если указанный ключ не нажата, пропустите следующую инструкцию.

case 0xA1:
    if (!this.keyboard.isKeyPressed(this.v[x])) {
        this.pc += 2;
    }
    break;

Fx07 – ld vx, dt

Еще один простой. Мы просто устанавливаем Vx к значению, хранящему в задержка Отказ

case 0x07:
    this.v[x] = this.delayTimer;
    break;

Fx0a – ld vx, k

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

case 0x0A:
    this.paused = true;

    this.keyboard.onNextKeyPress = function(key) {
        this.v[x] = key;
        this.paused = false;
    }.bind(this);
    break;

Сначала мы набор Пауза для правды, чтобы приостановить эмулятор. Тогда, если вы помните из нашего keyboard.js Файл, где мы устанавливаем onnnextkeyPlepress NULL, это где мы их инициализировать. С onnnextkeyPlepress Функция инициализирована, в следующий раз КДУЩЬЮ Событие срабатывает, следующий код в нашем keyboard.js Файл будет запущен:

// keyboard.js
if (this.onNextKeyPress !== null && key) {
    this.onNextKeyPress(parseInt(key));
    this.onNextKeyPress = null;
}

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

FX15 – LD DT, VX

Эта инструкция просто устанавливает значение таймера задержки в значение, хранящееся в регистре Vx Отказ

case 0x15:
    this.delayTimer = this.v[x];
    break;

Fx18 – ld st, vx

Эта инструкция очень похожа на FX15, но устанавливает таймер звука на Vx вместо таймера задержки.

case 0x18:
    this.soundTimer = this.v[x];
    break;

Fx1e – добавить я, vx

Добавить Vx к Я Отказ

case 0x1E:
    this.i += this.v[x];
    break;

Fx29 – ld f, vx – добавить я, vx

Для этого мы настраиваем Я к расположению спрайта на Vx Отказ Это умножено на 5, потому что каждый спрайт длится 5 байтов.

case 0x29:
    this.i = this.v[x] * 5;
    break;

Fx33 – ld b, vx

Эта инструкция собирается захватить сотни, десятки и цифры от регистрации Vx и хранить их в регистрах Я , Я + 1 и Я + 2 соответственно.

case 0x33:
    // Get the hundreds digit and place it in I.
    this.memory[this.i] = parseInt(this.v[x] / 100);

    // Get tens digit and place it in I+1. Gets a value between 0 and 99,
    // then divides by 10 to give us a value between 0 and 9.
    this.memory[this.i + 1] = parseInt((this.v[x] % 100) / 10);

    // Get the value of the ones (last) digit and place it in I+2.
    this.memory[this.i + 2] = parseInt(this.v[x] % 10);
    break;

Fx55 – ld [i], vx

В этой инструкции мы зацикливаемся через регистры V0 через Vx и хранение его значения в памяти, начиная с Я Отказ

case 0x55:
    for (let registerIndex = 0; registerIndex <= x; registerIndex++) {
        this.memory[this.i + registerIndex] = this.v[registerIndex];
    }
    break;

FX65 – LD VX, [I]

Теперь до последней инструкции. Это делает противоположность FX55 Отказ Он читает значения из памяти, начиная с Я и хранит их в регистрах V0 через Vx Отказ

case 0x65:
    for (let registerIndex = 0; registerIndex <= x; registerIndex++) {
        this.v[registerIndex] = this.memory[this.i + registerIndex];
    }
    break;

чип8.js.

С нашим классом CPU создан, давайте закончим наш Чип8.js Файл путем загрузки в ром и велосипеде на нашем ЦП. Нам нужно импортировать cpu.js и инициализировать объект CPU:

import Renderer from './renderer.js';
import Keyboard from './keyboard.js';
import Speaker from './speaker.js';
import CPU from './cpu.js'; // NEW

const renderer = new Renderer(10);
const keyboard = new Keyboard();
const speaker = new Speaker();
const cpu = new CPU(renderer, keyboard, speaker); // NEW

Наше init Функция становится:

function init() {
    fpsInterval = 1000 / fps;
    then = Date.now();
    startTime = then;

    cpu.loadSpritesIntoMemory(); // NEW
    cpu.loadRom('BLITZ'); // NEW
    loop = requestAnimationFrame(step);
}

Когда наш эмулятор инициализируется, мы загрузим спрайты в память и загрузите Блиц ПЗУ. Теперь нам просто нужно верить ЦП:

function step() {
    now = Date.now();
    elapsed = now - then;

    if (elapsed > fpsInterval) {
        cpu.cycle(); // NEW
    }

    loop = requestAnimationFrame(step);
}

С помощью этого мы должны иметь рабочий эмулятор CHIP8.

Заключение

Я начал этот проект некоторое время назад и был очарован этим. Создание эмулятора было всегда чем-то, что заинтересовало меня, но никогда не имело смысла для меня. Это было до тех пор, пока я узнал о чипе-8 и простоте ее по сравнению с более продвинутыми системами.

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

В целом, я надеюсь, вы наслаждались статьей и узнали что-то. Я стремился объяснить все детально, так как можно было максимально простым способом.

Независимо от того, если что-то все еще сбивает с толку, или у вас просто есть вопрос, пожалуйста, не стесняйтесь, дайте мне знать на Twitter или опубликовать проблему на Github Repo Как я бы хотел помочь вам.

Я хотел бы оставить вас с несколькими представлениями о функциях, которые вы можете добавить в свой эмулятор CHIP-8:

  • Аудио управление (отключение отключения, изменение частоты, изменить тип волны (синус, треугольник) и т. Д.)
  • Возможность изменять масштаб рендеринга и скорость эмулятора от UI
  • Пауза и неожиданность
  • Возможность сохранения и загрузки сохранения
  • Выбор ROM