Volver a Página Principal

Tutorial de creación de un Emulador sencillo de Chip-8 con VS 2008 y C# (Parte 2)

Lenguaje: C#
Para: VS 2008 con Sdl.Net 6.1
Por Dark-N: hernaldog@gmail.com
http://darknromhacking.com
Hilo del Foro: http://foro.romhackhispano.org/viewtopic.php?f=4&t=872
Actualización 13-08-2011: Se agregó la variable rnd de tipo Random.

Lección Anterior | Índice | Siguiente Lección

La idea

La idea en este capítulo es hacer el emulador más sencillo posible, lo realizaremos simplemente en modo Consola, solo utilizando caracteres ASCII para representar los gráficos (como "\", "/", "|" o "*") y no tendrá sonidos ni eventos de Teclado o Joystick.
En la imagen inferior se visualiza el juego PONG emulado en modo Consola, que es lo que haremos en este capítulo y en el siguiente:

Crear Proyecto

Lo primero es crear un proyecto de tipo Consola:

Using

Como se trabajará en modo Consola, se deben agregar solo estos namespaces:
using System;
using System.IO;
NOTA: Si se está trabajando con el Framework 1.1 y no el Framework 2.0 se debe incorporar además el siguiente namespace para poder usar de la bocina de la bios (para simular el sonido) y realizar un clearscren de la ventada de comandos:
using System.Runtime.InteropServices;  //para "clearscreen" y el "beep" de la Bios

Variables Globales que representan la arquitectura de Chip-8

Antes de programar esta parte, se muy necesario que leas acerca de la arquitectura de Chip-8. Un buen sitio es en Wikipedia o Cowgod's Chip-8.
//variables principales        
const int RES_X = 64;  //resolucion de pantalla 64x32
const int RES_Y = 32;        
const int DIR_INICIO = 0x200;
const int TAMANO_MEM = 0xFFF;
const int TAMANO_PILA = 16;
const int CANT_REGISTROS = 16;
int[,] arregloPantalla = new int[RES_X, RES_Y];

//arreglo que representa la memoria
int[] memoria = new int[TAMANO_MEM];

//Arreglo que representa los 16 Registros (V) 
int[] V = new int[CANT_REGISTROS];

//arreglo que representa la Pila
int[] pila = new int[TAMANO_PILA];

//variables que representan registros varios
int instruccion;  //representa una instruccion del Chip-8. Tiene largo 2 byte
int PC;
int I;
int SP;
int KK;

//variables para manejar los opcodes
int opcode1 = 0;
int opcode2 = 0;  //X
int opcode3 = 0;  //Y
int opcode4 = 0;
int NNN = 0;

//variables que representan los 2 timers: Delay Timer y Sound Timer
int delayTimer;
int soundTimer;
Nota: Como no implementaremos el sonido en esta primera parte del tutorial, esta variable que representa la timer de sonido no se utilizará en la lógica principal.

Ojo con las notaciones, en el resto del código utilizaremos mucho la nomenclatura 0xValor indicando que Valor es un valor Hexadecimal y no un Decimal o Entero. Esto quiere decir que 0x200 significa un número 200 hex y no simplemente número 200. Si se convierte 0x200 a decimal usando la calculadora de Windows nos da 0x200 = 512. Es decir 200 hexadecimal equivale a 512 en numero entero o decimal

Ahora voy a explicar acerca del sistema de gráficos del Chip-8 traduciendo lo explicado en Cowgod's Chip-8. La gráfica original del Chip-8 es monocromática (un solo color) con una resolución de 64x32 pixels, aunque algunos intérpretes como el ETI 660 tiene gráficos como 64x48 o 64x64. El Super Chip-48, que es un interprete para la calculadora HP 48, tiene el modo 128x64. Este modo es soportado por la mayoría de de los intérpretes sobre otras plataformas.

(0,0)(63,0)
(0,31)(63,31)





Chip-8 pinta los gráficos en pantalla a través del uso de sprites. Un sprite es un grupo de bytes, en el cual, al representarse de modo binario, arma o conforma la gráfica deseada. Los sprites del Chip-8 pueden ser de hasta 15 bytes, para un posible tamaño de sprite de 8x15.
Los programas o juegos utilizan grupos de sprites para representar caracteres o dígitos (también llamados fonts) que van del 0 al 9 y de la A a la F. Estos sprites son de 5 bytes de largo o lo que es lo mismo 8x5 pixels. Los datos de estos caracteres se almacena el sector de la memoria que es fijo para esta tarea en el Chip-8 (desde 0x000 a 0x1FF). Abajo se muestra un ejemplo del carácter "0" en formato byte y binario. En este último modo, se puede ver dibujado el dígito "0". Estrictamente el "0" se puede dibujar con 4x5 bits ya que se ven solo ceros desde la mitad hacia la derecha:

"0"BinaryHex
****
*  *
*  *
*  *
****
11110000
10010000
10010000
10010000
11110000
0xF0
0x90
0x90
0x90
0xF0

Variables Globales que nos ayudan a representar otros puntos

Las siguientes variables las cree ya que para mi son la mejora forma de simular el manejo de las fuentes que en CHIP-8 abarcan del 0 al 9 y de la A a la F. Esto no quiere decir en absoluto que existan otras formas de manejar estas fuentes. Para los que tienen dudas de como se ven las fuentes en el juego, aquí les pongo un ejemplo:


//variables para el manejo de fuentes (80 bytes, ya que hay 5 bytes x caracter
//y son 16 caracteres o letras (5x16=80). Cada font es de 4x5 bits.
int[] arregloFuentes = {
	0xF0, 0x90, 0x90, 0x90, 0xF0,	// valores para 0
	0x60, 0xE0, 0x60, 0x60, 0xF0,	// valores para 1
	0x60, 0x90, 0x20, 0x40, 0xF0,	// valores para 2
	0xF0, 0x10, 0xF0, 0x10, 0xF0,	// valores para 3
	0x90, 0x90, 0xF0, 0x10, 0x10,	// valores para 4
	0xF0, 0x80, 0x60, 0x10, 0xE0,	// valores para 5
	0xF0, 0x80, 0xF0, 0x90, 0xF0,	// valores para 6
	0xF0, 0x10, 0x10, 0x10, 0x10,	// valores para 7
	0xF0, 0x90, 0xF0, 0x90, 0xF0,	// valores para 8
	0xF0, 0x90, 0xF0, 0x10, 0x10,	// valores para 9
	0x60, 0x90, 0xF0, 0x90, 0x90,	// valores para A
	0xE0, 0x90, 0xE0, 0x90, 0xE0,	// valores para B
	0x70, 0x80, 0x80, 0x80, 0x70,	// valores para C
	0xE0, 0x90, 0x90, 0x90, 0xE0,	// valores para D
	0xF0, 0x80, 0xF0, 0x80, 0xF0,	// valores para E
	0xF0, 0x90, 0xF0, 0x80, 0x80	// valores para F
};
¿Pero como equivalen esos bytes a ciertos dígitos o letras?

Para responder a eso hay que entender lo explicado arriba y que los fonts son de 8x5 y que los dígitos se pueden dibujar con solo 4x5 pixels (5 filas de bits por 4 columnas de bits) y además son de 1 bit por plano, por lo tanto cada uno de los dígitos (0x60, 0x90, etc) representa una fila, de esta forma si tomamos los dígitos del '2' son: 0x60 0x90 0x20 0x40 0xF0, entonces el 0x60 representa la primera fila de las 5 (4x5), el 0x90 la segunda de las 5, y así sucesivamente. Si quieren hacerse una idea más gráfica, les recomiendo bajar mi aplicación que desarrollé el 2006, que está hecha para hacer o ver fuentes de la SNES. Para usarla, se debe setear el modo Bit Plane de 1 bit y luego podemos escribir los bytes 60902040F0 y luego le damos a "VER" y mostraría algo así:



Mas variables a utilizar son:
private bool[] teclasPresionadas = { false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false };

const int TECLA_1	= 0;
const int TECLA_2	= 1;
const int TECLA_3	= 2;
const int TECLA_4	= 3;
const int TECLA_Q	= 4;
const int TECLA_W	= 5;
const int TECLA_E	= 6;
const int TECLA_R	= 7;
const int TECLA_A	= 8;
const int TECLA_S	= 9;
const int TECLA_D	= 10;
const int TECLA_F	= 11;		
const int TECLA_Z	= 12;
const int TECLA_X	= 13;
const int TECLA_C	= 14;
const int TECLA_V	= 15;

// mapeamos las 16 teclas de Chip8
private byte[] MapeoTeclas = 
{	       
	0x01,0x02,0x03,0x0C,
	0x04,0x05,0x06,0x0D,
	0x07,0x08,0x09,0x0E,
	0x0A,0x00,0x0B,0x0F 
};
Si les nace la duda, porqué ese orden para el "Mapeo" de Teclas, según Cowgod's Chip-8, el teclado del Chip-8 tiene un key-pad de 16 teclas hexadecimales, repartidos de la forma:

123C
456D
789E
A0BF







Por último si se está trabajando con el Framework 1.1 y no el Framework 2.0 se deben crear estas variables y declarar las funciones:
[DllImport("Kernel32.dll")] //para beep
public static extern bool Beep(UInt32 frequency, UInt32 duration);
//En Framework 2.0 se puede usar directamente: Console.Beep(350, 50);

[DllImport("msvcrt.dll")] //Framwork 1.1: para cls (clear screen) en DOS
public static extern int system(string cmd);
//En Framework 2.0 se puede usar directamente: Console.Clear();
La primera es para el uso del "Beep" utilizando la bocina de la BIOS. En el Framework 1.1 se usa la DLL del Kernel32 para lograr esto. Como en esta parte del tutorial lo utilizaremos el sonido, se dejará comentado.
La segunda es para realizar un "Clear screen" en la ventana de comandos ya que este método no está presente en la librería. Una vez declarado, se puede usar de la forma system("cls") en cualquier parte del código.
Ambas clases fueron mejoradas en el Framewrok 2.0 y en contemplan los métodos Console.Beep(int frecuencia sonido, int duración) y Console.Clear().

NOTA: por alguna razón que no he logrado averiguar, el uso del system("cls"), me da mejor resultado a nivel visible que Console.Clear(), por lo que lo utilizaré a pesar de que estoy usando el Framework 2.0.


Clase Random

Algunas instrucciones del Chip-8 utilizan la funcionalidad de número aleatorio. El interprete Chip-8 tenía su propio mecanismo para esto, nosotros en C# tenemos la clase Random, por lo que crearemos una variable de este tipo que se utilizará más adelante:
//Variable de tipo Random (para generar números aleatoios) utilizada por ciertas instrucciones
Random rnd = new Random();

Código fuente de esta parte (C# 2.0):
using System;
using System.IO;
using System.Runtime.InteropServices;  //para "clearscreen" y el "beep" de la Bios

namespace Chip8_ConsoleMode
{
    class Emulador
    {

        #region variables emulador

        //variables principales        
        const int RES_X = 64;  //resolucion de pantalla 64x32
        const int RES_Y = 32;        
        const int DIR_INICIO = 0x200;
        const int TAMANO_MEM = 0xFFF;
        const int TAMANO_PILA = 16;
        const int CANT_REGISTROS = 16;
        int[,] arregloPantalla = new int[RES_X, RES_Y];

        //arreglo que representa la memoria
        int[] memoria = new int[TAMANO_MEM];

        //Arreglo que representa los 16 Registros (V) 
        int[] V = new int[CANT_REGISTROS];

        //arreglo que representa la Pila
        int[] pila = new int[TAMANO_PILA];

        //variables que representan registros varios
        int instruccion;  //representa una instruccion del Chip-8. Tiene largo 2 byte
        int PC;
        int I;
        int SP;
        int KK;

        //variables para manejar los opcodes
        int opcode1 = 0;
        int opcode2 = 0;  //X
        int opcode3 = 0;  //Y
        int opcode4 = 0;
        int NNN = 0;

        //variables que representan los 2 timers: Delay Timer y Sound Timer
        int delayTimer;
        int soundTimer;

	//variables para el manejo de fuentes (80 bytes, ya que hay 5 bytes x caracter
	//y son 16 caracteres o letras (5x16=80). Cada font es de 4x5 bits. 
	int[] arregloFuentes = {
			0xF0, 0x90, 0x90, 0x90, 0xF0,	// valores para 0
			0x60, 0xE0, 0x60, 0x60, 0xF0,	// valores para 1
			0x60, 0x90, 0x20, 0x40, 0xF0,	// valores para 2
			0xF0, 0x10, 0xF0, 0x10, 0xF0,	// valores para 3
			0x90, 0x90, 0xF0, 0x10, 0x10,	// valores para 4
			0xF0, 0x80, 0x60, 0x10, 0xE0,	// valores para 5
			0xF0, 0x80, 0xF0, 0x90, 0xF0,	// valores para 6
			0xF0, 0x10, 0x10, 0x10, 0x10,	// valores para 7
			0xF0, 0x90, 0xF0, 0x90, 0xF0,	// valores para 8
			0xF0, 0x90, 0xF0, 0x10, 0x10,	// valores para 9
			0x60, 0x90, 0xF0, 0x90, 0x90,	// valores para A
			0xE0, 0x90, 0xE0, 0x90, 0xE0,	// valores para B
			0x70, 0x80, 0x80, 0x80, 0x70,	// valores para C
			0xE0, 0x90, 0x90, 0x90, 0xE0, 	// valores para D
			0xF0, 0x80, 0xF0, 0x80, 0xF0,	// valores para E
			0xF0, 0x90, 0xF0, 0x80, 0x80	// valores para F
	};

        private bool[] teclasPresionadas = { false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false };

        const int TECLA_1 = 0;
        const int TECLA_2 = 1;
        const int TECLA_3 = 2;
        const int TECLA_4 = 3;
        const int TECLA_Q = 4;
        const int TECLA_W = 5;
        const int TECLA_E = 6;
        const int TECLA_R = 7;
        const int TECLA_A = 8;
        const int TECLA_S = 9;
        const int TECLA_D = 10;
        const int TECLA_F = 11;
        const int TECLA_Z = 12;
        const int TECLA_X = 13;
        const int TECLA_C = 14;
        const int TECLA_V = 15;

        // mapeamos las 16 teclas de Chip8
        private byte[] MapeoTeclas = 
        {	       
            0x01,0x02,0x03,0x0C,
            0x04,0x05,0x06,0x0D,
            0x07,0x08,0x09,0x0E,
            0x0A,0x00,0x0B,0x0F 
        };

        //[DllImport("Kernel32.dll")] //para beep
        //public static extern bool Beep(UInt32 frequency, UInt32 duration);
        //En Framework 2.0 se puede usar directamente: Console.Beep(350, 50);

        [DllImport("msvcrt.dll")] //Framwork 1.1: para cls (clear screen) en DOS
        public static extern int system(string cmd);
        //En Framework 2.0 se puede usar directamente: Console.Clear();

        //Variable de tipo Random (para generar números aleatoios) utilizada por ciertas instrucciones
        Random rnd = new Random();

        #endregion

        
        static void Main(string[] args)
        {
           // ....
        }
    }
}
Lección Anterior | Índice | Siguiente Lección
Volver a Página Principal

blog comments powered by Disqus
2003 - 2018    La Web de Dark-N