Volver a Página Principal

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

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

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

La idea

La idea de este capítulo es realizar los cambios necesario para "convertir" la aplicación en modo consola a modo gráfico utilizando para ello la librería SDL.Net v6.1. Los cambios son menores, por lo que no será una lección larga.
Nota: en este capítulo asumo que antes te has leido al menos hasta el capítulo 2 de mi guía de Desarrollo de VideoJuegos con SDL.Net. Si no lo haz hecho, no entenderás algunos conceptos como Sprite, Surface, Blit, etc.

Nota: A partir de este capítulo incluiré algunas imágenes explicatorias con Windows 7 así que no se espanten.

Adaptando el Proyecto

Lo primero es renombrar la carpeta si la tienes con otro nombre, la dejaremos como "Chip8_vs2008":

Antes

Luego queda:


También adentro se debe cambiar el nombre de la carpeta a solo "Chip8":

Ahora queda:


Por último abrimos el archivo chip8.sln con un editor de texto y se debe cambiar la sección que dice "Chip8_ConsoleMode" a solo "Chip8" (igual que la carpeta, con sus mayúsculas y minúsculas):


Con esto estaríamos ok con el renombrado y si abrimos la solución "chip8.sln" debe abrirse sin problemas. Si no abre, es que te quedó algo mal configurado.
Para dejar todo listo para empezar a depurar, debemos hacer un Rebuild de la solución y este no debe arrojar errores:

Si no lo haces, es posible que al tratar de depurar te arroje el error:


Lo segundo es cambiar el tipo de salida del proyecto de tipo console application a tipo windows application:



Lo tercero es opcional pero válido, y es cambiar de nombre el Proyecto, de "Chip8_ConsoleMode" a "Chip8_SDLMode". Nota: esto solo cambia el nombre que se ve en el Visual Studio pero no el nombre de la carpeta física.



Si ya tienes instalado el SDL.NET 6.1 SDK, lo tercero es agregar la referencia a la DLL de SDl.NET:



Lo mismo con la referencia a System.Drawing que se debe agregar para manejar los colores.

Lo cuarto es agregar los Using al proyecto que son de 2 tipos:

A. Los using de SDL.Net:

  • SdlDotNet.Core: permite el manejo de Eventos como el Tick o Salir.
  • SdlDotNet.Graphics: permite usar Surfaces.
  • SdlDotNet.Graphics.Sprites: para el manejo de Sprites y textos en pantalla
  • SdlDotNet.Input: para el manejo del teclado

  • B. Los using de .Net que utilizarmos para aspectos gráficos son:

  • System.Drawing: permite el manejo de colores.

  • Por lo tanto, los using finales, sumados los anteriores son:

    using System;
    using System.IO; //Librería de .Net para manejar archivos
    using System.Runtime.InteropServices;  //para el "beep" de la Bios
    using System.Drawing;  //Librería de .Net para manejar Colores
    
    using SdlDotNet.Core; //Eventos como Teclado, FPS, etc.
    using SdlDotNet.Graphics; //para Surfaces
    using SdlDotNet.Graphics.Sprites;  //para Sprites y textos en pantalla
    using SdlDotNet.Input; //para manejo de teclado
    

    Cambiando el Namespace

    Esto es opcional, pero que aporta para ordenarnos, y es renombrar el antiguo namespace "Chip8_ConsoleMode" por "Chip8".
    Antes:
    
    namespace Chip8_ConsoleMode
    {
    ...
    }
    
    Ahora:
    
    namespace Chip8
    {
    ...
    }
    

    Nuevas variables de clase

    1. Crearemos 1 variable instruccion que represente a un Opcode cualquiera y será de uso general.
    2. Crearemos 2 variables PixelNegro y PixelBlanco que representarán los pixels, serán de tipo Surface y cada una representará un pixel blanco y otro negro.
    3. Creamos una variable llamada tamanoPixel que contiene el tamaño del Pixel que uno ve en pantalla en 5 unidades o pixels reales, de otra forma el juego se vería muy pequeño en pantalla. Este valor se utilizará en el constructor.
    4. Creamos 2 variables para el manejo del reloj del CPU, una llamada operPorSegundo y otra operPorFrame.
    5. Creamos otra variable para el manejo del reloj del CPU, una llamada TimeUntilTimerUpdate.
    int	opcodes;
    private Surface PixelNegro;
    private Surface PixelBlanco;
    const int tamanoPixel = 5; // tamaño de pixel, sirve para zoom de todo el emulador
    public const int operPorSegundo = 600;
    public const int operPorFrame = operPorSegundo / 60;
    private int TimeUntilTimerUpdate;  //para la simulación de timers
    

    Cambiando el método Run()

    Se debe cambiar el método Run por un nuevo ya que antes Run se utilizaba para simular el game-loop del juego que nos permitía recorrer las instrucciones y pintar en la consola, ahora ese trabajo lo llevará el evento Tick, por lo tanto, en nuestro método Run solo le indicamos que inicie los Eventos en el juego que estarán declarados en el Constructor.
    Antes:
    
    void Run()
    {			
    	ResetHardware();
    	
    	if (CargarJuego() == true)
    	{
    		while (true) //game-loop
    		{                    
    			EjecutaFech();
    			ManejaTimers();
    		}
    	}
    }
    
    Ahora:
    
    // Inicializamos los Eventos
    public void Run()
    {
    	Events.Run();
    }
    

    Agregando Constructor

    Debemos agregar un constructor a la clase Emulador ya que aquí se inicializarán algunas variables globales.
    /// 
    /// Constructor de la clase
    /// 
    public Emulador()
    { 
    
    }
    

    Manejar los errores: bloque Try-Catch

    Mejoraremos de inmediato el Constructor con un simple manejador de Errores, para esto usamos las clausulas Try y Catch de C# y mostrando en pantalla un mensaje de error. Nota: Este tutorial no es un curso de C# por lo que no explicaré en detalle como manejar los errores en C# o cual es el mejor patrón para manejarlos. Por lo tanto, los pasos para hacer esto es:

  • Asegurarnos de tener la referencia a la Dll: System.Windows.Forms.dll
  • Agregar el using respectivo: using System.Windows.Forms;
  • Agregar el código try y el catch monstrando con la función MessageBox.Show("Texto") el error en si con Exception.Message y su traza con Exception.StackTrace:
    /// 
    /// Constructor de la clase
    /// 
    public Emulador()
    {
    	try
    	{
    		...		
    	}
    	catch (Exception ex)
    	{
    	    MessageBox.Show("Error General: " + ex.Message + "-" + ex.StackTrace);
    	    Events.QuitApplication();
    	}
    }
    

    Creando una Aplicación SDL.Net

    Como se explicó en el capítulo 2 de mi guía de Desarrollo de VideoJuegos con SDL.Net, por lo tanto lo primero que debemos tener en el constructor es el manejo de la pantalla con la resolución y el color de fondo del Surface principal del emulador gracias al objeto Video, y los Frames por Segundo que correrá la aplicación gracias al objeto SdlDotNet.Core.Events:
  • Seteo de resolución del Surfase o plano de fondo donde dibujaremos los pixels.
  • Seteo del color de fondo de todo el Surfase, por defecto negro.
  • Seteo de Frames por Segundo por defecto a 60.
  • 
    /// 
    /// Constructor de la clase
    /// 
    public Emulador()
    {
    	try
    	{
    		Video.SetVideoMode(RES_X * tamanoPixel, RES_Y * tamanoPixel);
    		Video.Screen.Fill(Color.Black);
    		Events.Fps = 60;
    		Events.TargetFps = 60;	
    	}
    	catch (Exception ex)
    	{
    		MessageBox.Show("Error General: " + ex.Message + "-" + ex.StackTrace);
    	}
    }
    

    Dibujando un Pixel

    Lo principal que haremos en el emulador "gráfico" es que reemplazaremos los códigos ASCII que utilizabamos para dibujar en modo Consola, por el dibujado de pixels. Por lo tanto, debemos saber como simularemos un pixel utilizando SDL.Net.
    Un Pixel es un "Surface", es decir una superficie de un color dado, al cual se le puede cargar un imagen encima. En nuestro caso, como el emulador es monocromático (solo tiene 2 colores), usaremos un pixel negro que se usa para el fondo y otro blanco para mostrar el juego en sí. Con esto solo necesitamos crear 2 Surfaces, uno para cada pixel.

    La forma de crear una Surface de un color y de ciertas dimensiones es:
    Surface miSurface = new Video.CreateRgbSurface(ancho, alto);
    

    Ejemplo: Creación de un gran pixel de 10 x 10 pixels, de color blanco. Nota: este ejemplo no es parte de nuestro emulador, solo es un ejemplo.
    private Surface PixelBla;
    ...
    PixelBla = Video.CreateRgbSurface(10, 10);
    PixelBla.Fill(Color.White);
    ...
    Video.Screen.Blit(PixelBla, Point destino);
    

    En el emulador el pixel que uno ve en pantalla no puede ser de 1 pixel ya que se vería muy pequeño, haciendo imposible jugar, por lo que la idea es pintar pixels grandes (de 5 x 5 por ejemplo) para que se vea mejor.

    Por lo tanto en nuestro emulador seteamos en el constructor las 2 variables "PixelNegro" y "PixelBlanco" que definimos en la clase:

    /// 
    /// Constructor de la clase
    /// 
    public Emulador()
    {
    	...
    	PixelNegro = Video.CreateRgbSurface(multipPixel, multipPixel);
    	PixelBlanco = Video.CreateRgbSurface(multipPixel, multipPixel);
    	PixelNegro.Fill(Color.Black);
    	PixelBlanco.Fill(Color.White);
    	...  
    }
    

    Reset Harware

    En el modo consola se cargaba con el método ResetHardware en el método Run de la forma:
    void Run()
    {			
    	ResetHardware();
    	if (CargarJuego() == true)
    	{
    	...
    
    Ahora se debe hacer lo mismo en el Constructor. En el método en sí cambian pequeñas cosas, por ejemplo ahora para limpiar la pantalla se usa el método Video.Screen.Fill(Color.Black) para pintar todo el Surface de un color (se pinta en memoria) y enviar el resfresco del Video con Video.Screen.Update() para decirle al Surface que actualice los cambios en pantalla y sean visible para nosotros.
    /// 
    /// Constructor de la clase
    /// 
    public Emulador()
    {
    	...
    	ResetHardware();
    }
    
    void ResetHardware()
    {
        // Reseteamos los Timers
        delayTimer = 0x0;
        soundTimer = 0x0;
    
        // Reseteamos variables
        instruccion = 0x0;
        PC = DIR_INICIO;
        SP = 0x0;
        I = 0x0;
    
        // Limpiamos la memoria
        for (int i = 0; i < TAMANO_MEM; i++)
            memoria[i] = 0x0;
    
    
        // Limpiamos registros
        for (int i = 0; i < CANT_REGISTROS; i++)
            V[i] = 0x0;
    
    
        // Limpiamos el stack
        for (int i = 0; i < TAMANO_PILA; i++)
            pila[i] = 0x0;
    
        // Cargamos las fuentes en la memoria por ej, los marcadores del juego PONG esos "[0-0]", "[0-2]"
        for (int i = 0; i < 80; i++)
            memoria[i] = arregloFuentes[i];	
    
        Video.Screen.Fill(Color.Black);
        Video.Screen.Update();
    }
    

    Carga de la Rom

    En el modo consola se cargaba con el método CargaJuego en el método Run de la forma:
    void Run()
    {			
    	ResetHardware();
    	if (CargarJuego() == true)
    	{
    	...
    
    Ahora se debe cargar la Rom en el Constructor y debemos cambiar la forma de manejar los errores de carga del archivo, la mejora forma que se me ocurrió es con throw new Exception(mensaje) que hace que el handler padre (que está implementado en el constructor y que muestra un MessageBox) lo procese y no lo hacemos aquí mismo, es algo avanzado para quien no ha trabajado con varios niveles de Try-Catch pero leyendo un poco se entiende:
    /// 
    /// Constructor de la clase
    /// 
    public Emulador()
    {
    	...
    	CargarROM("PONG");
    }
    
    void CargarJuego(string nombreRom)
    {            
        FileStream rom;
    
        try
        {
            rom = new FileStream(@nombreRom, FileMode.Open);
    
            if (rom.Length == 0)
            {
                throw new Exception("Error en la Carga de la Rom: ROM dañada o vacía");
            }
    
            // Comenzamos a cargar la rom a la memoria a partir de la dir 0x200
            for (int i = 0; i < rom.Length; i++)
                memoria[DIR_INICIO + i] = (byte)rom.ReadByte();	
    
            rom.Close();                
        }
        catch (Exception ex)
        {
            throw new Exception("Error en la Carga de la Rom: " + ex.Message);
        }
    }
    

    Eventos

    Existen 4 eventos que declararemos en el constructor:

  • El evento Tecla Presionada: Ocurre cuando se presiona una tecla o se mantiene presionada.y no se suelta.
  • El evento Tecla Liberada: Ocurre cuando se libera o suelta una tecla que se mantenía presionada.
  • El evento Salir que se gatilla cuando cerramos la aplicación pinchando la "X" de la ventana en la parte superior derecha.
  • El evento Tick que es gatillado constantemente por la misma aplicación y que nos permitirá ejecutar lo que se conoce como game-loop y por ende, el pintado constante de los gráficos ya que en los juegos cada tick equivale a un frame. Recordemos que por defecto se ejecutan 60 ticks o frames por cada segundo que pasa, o lo que es lo mismo, 1 frame cada 0,0166666 segundos, o también, 1 frame cada 16 milisegundos aproximadamente. El método que hará la emulación se llama EmulaFrame().

  • Por lo tanto, dentro del constructor tenemos:
    public Emulador()
    { 
    	...
    	//declaración de Eventos
    	Events.KeyboardDown += new EventHandler(this.Tecla_Presionada);
    	Events.KeyboardUp += new EventHandler(this.Tecla_Liberada);
    	Events.Quit += new EventHandler(this.Events_Salir);
    	Events.Tick += new EventHandler(this.Events_Tick);
    	...
    }
    ...
    private void Events_Salir(object sender, QuitEventArgs e)
    {
    	Events.QuitApplication();
    }
    
    private void Events_Tick(object sender, TickEventArgs e)
    {
    	EmulaFrame();
    }
    ...
    

    1. Eventos de Teclado

    Los computadores que inicialmente usaban Chip 8 tenían la siguiente configuración de teclado basado en numeros y teclas Hexadecimales:

    123C
    456D
    789E
    A0BF







    Por lo tanto el código del evento de Tecla Presionada y Tecla Liberada quedaría:

    private void Tecla_Presionada(object sender, KeyboardEventArgs e)
    {	
    	//Salimos del juego con tecla Escape
    	if(e.Key == Key.Escape)
    	{
    		Events.QuitApplication();
    	}
    	
    	if(e.Key == Key.One) {teclasPresionadas[TECLA_1] = true;}
    	if(e.Key == Key.Two) {teclasPresionadas[TECLA_2] = true;}
    	if(e.Key == Key.Three) {teclasPresionadas[TECLA_3] = true;}
    	if(e.Key == Key.Four) {teclasPresionadas[TECLA_4] = true;}
    	if(e.Key == Key.Q) {teclasPresionadas[TECLA_Q] = true;}
    	if(e.Key == Key.W) {teclasPresionadas[TECLA_W] = true;}
    	if(e.Key == Key.E) {teclasPresionadas[TECLA_E] = true;}
    	if(e.Key == Key.R) {teclasPresionadas[TECLA_R] = true;}
    	if(e.Key == Key.A) {teclasPresionadas[TECLA_A] = true;}
    	if(e.Key == Key.S) {teclasPresionadas[TECLA_S] = true;}
    	if(e.Key == Key.D) {teclasPresionadas[TECLA_D] = true;}
    	if(e.Key == Key.F) {teclasPresionadas[TECLA_F] = true;}
    	if(e.Key == Key.Z) {teclasPresionadas[TECLA_Z] = true;}
    	if(e.Key == Key.X) {teclasPresionadas[TECLA_X] = true;}
    	if(e.Key == Key.C) {teclasPresionadas[TECLA_C] = true;}
    	if(e.Key == Key.V) {teclasPresionadas[TECLA_V] = true;}
    }
    
    private void Tecla_Liberada(object sender, KeyboardEventArgs e)
    {			
    	if(e.Key == Key.One) {teclasPresionadas[TECLA_1] = false;}
    	if(e.Key == Key.Two) {teclasPresionadas[TECLA_2] = false;}
    	if(e.Key == Key.Three) {teclasPresionadas[TECLA_3] = false;}
    	if(e.Key == Key.Four) {teclasPresionadas[TECLA_4] = false;}
    	if(e.Key == Key.Q) {teclasPresionadas[TECLA_Q] = false;}
    	if(e.Key == Key.W) {teclasPresionadas[TECLA_W] = false;}
    	if(e.Key == Key.E) {teclasPresionadas[TECLA_E] = false;}
    	if(e.Key == Key.R) {teclasPresionadas[TECLA_R] = false;}
    	if(e.Key == Key.A) {teclasPresionadas[TECLA_A] = false;}
    	if(e.Key == Key.S) {teclasPresionadas[TECLA_S] = false;}
    	if(e.Key == Key.D) {teclasPresionadas[TECLA_D] = false;}
    	if(e.Key == Key.F) {teclasPresionadas[TECLA_F] = false;}
    	if(e.Key == Key.Z) {teclasPresionadas[TECLA_Z] = false;}
    	if(e.Key == Key.X) {teclasPresionadas[TECLA_X] = false;}
    	if(e.Key == Key.C) {teclasPresionadas[TECLA_C] = false;}
    	if(e.Key == Key.V) {teclasPresionadas[TECLA_V] = false;}
    }
    

    2. Emulación de Lectura de Cada Instrucción o Game-Loop

    Aquí hacemos pocos cambios respecto al emulador en modo Consola ya que antes teníamos:
    void Run()
    {
        ResetHardware();
    
        if (CargarJuego() == true)
        {
            while (true) //game-loop
            {
                EjecutaFech();            
            }
        }
    }
    
    void EjecutaFech()
    {
    	#region lectura de instrucciones
    	
    	// leemos cada una de las instrucciones desde la memoria. 
    	// cada instruccion es de 2 bytes
    	instruccion = memoria[PC] << 8 | memoria[PC + 1];
    	...
    

    Aquí debemos hacer 4 cosas:

    1. creamos el método EmulaFrame() que se encargará de leer cada opcode de la memoria
    public void EmulaFrame() //representa un frame
    {
        //por cada Tick o Frame, se ejecutan 600/60=10 instrucciones
        for (int i = 0; i < operPorFrame; i++)
        {
            EmulaOpcodes();
        }
    }
    

    2. creamos el método EmulaOpcodes() que maneja los Timers de Delay y de Tiempo y que luego se encarga de llamar al método que procesa los opcodes EjecutaOpcodes():
    void EmulaOpcodes()
    {
    	if (TimeUntilTimerUpdate == 0)
    	{
    		if (delayTimer > 0)
    			delayTimer--;
    		
    		if (soundTimer > 0) 
    			soundTimer--;
    		
    		TimeUntilTimerUpdate = operPorFrame;
    	}
    	else
    	{
    		TimeUntilTimerUpdate--;
    	}
    	
    	EjecutaOpcodes();
    }
    

    3. renombramos el nombre del método EjecutaFech() por uno mas correcto EjecutaOpcodes()

    4. cambiamos un poco el contenido del método que limpia la pantalla ClearScreen().

    Antes:
    void ClearScreen()
    {
        // No se limpia la pantalla directamente, sino el arreglo que la representa
        // asignandole un valor 0
    	for (int p =0; p < RES_X; p++)
    		for (int q = 0; q < RES_Y; q++)
    			arregloPantalla[p,q] = 0;					
    }
    
    Ahora:
    void ClearScreen()
    {
    	Video.Screen.Fill(Color.Black);
    	Video.Screen.Update();
    	
    	for (int p =0; p < RES_X; p++)
    		for (int q = 0; q < RES_Y; q++)
    			arregloPantalla[p,q] = 0;
    }
    

    5. cambiamos el método de realiza los gráficos DrawSprite(). Lo más importante aquí es:

  • el uso de Video.Screen.Blit() para el puntado de objetos sobre el Surface (los objetos se pintan en memoria).
  • usamos Video.Screen.Update() para decirle al Surface que actualice los cambios en pantalla.
  • No se utiliza el método ActualizarPantalla() por lo que debemos eliminarlo.
  • Se deja un "if" inicial para la futura emulación del Super Chip que explicado en forma simple, sería como un Chip-8 con mejoras permitiendo jugar más y mejores Roms.

  • Antes teníamos:
    void DrawSprite()
    {
    	// En Chip8 un Sprite es de 8xN
    	int largoSpriteX = 8;
    
    	int dataLeida, tempx, tempy = 0;
    
    	// Se resetea la detección de colisión
    	V[0xF] = 0x0;
    
    	for (int lineaY = 0; lineaY < opcode4; lineaY++)
    	{
    		// Leemos un byte de memoria
    		dataLeida = memoria[I + lineaY];
    
    		for (int pixelX = 0; pixelX < largoSpriteX; pixelX++)
    		{
    			if ((dataLeida & (0x80 >> pixelX)) != 0)
    			{
    				// Chequeamos por alguna colisión (pixel sobrescrito)
    				tempx = (V[opcode2] + pixelX) % 64;
    				tempy = (V[opcode3] + lineaY) % 32;
    
    				//% Resto o modulo de una division, ejemplo 32%64=32, 0%32=0, 1%32=1
    				if (arregloPantalla[tempx, tempy] == 1)  
    					V[0xF] = 1;
    
    				// Dibujamos el el arreglo un 1, luego mas abajo pintamos graficamente el arreglo
    				arregloPantalla[tempx, tempy] ^= 1;  //XOR 0^1=1, 1^0=1, 1^1 =0, 0^0=0
    			} 
    		}
    	}			
    
    	// Actualiza lo que se ve en pantalla con los valores del arreglo
    	ActualizarPantalla();
    }
    
    Ahora es:
    /// 
    /// Screen is 64x62 pixels
    /// Todos los drawings son hechos en modos XOR. 
    /// Cuando uno o mas pixels son borrados mientras un sprite es pintado, 
    /// el registro VF se setea en 01, sino queda en 00.
    /// 
    void DrawSprite()
    {
        // Reseteamos el registro que detecta colisiones
        V[0xF] = 0x0;
    
        if ((instruccion & 0x000F) == 0) //opcode & 0x000F =opcode4
        {
            // Dibujamos un Sprite de SCHIP8 de tamaño 16x16
            // No implementado aún
        }
        else
        {
            // Bibujamos un Sprite de CHIP8 de tamaño 8xN				
            for (int spriY = 0; spriY < opcode4; spriY++)
            {
                for (int spriX = 0; spriX < 8; spriX++)
                {
                    int x = (memoria[I + spriY] & (0x80 >> spriX));
                    if (x != 0)
                    {
                        // checkeamos por alguna colision
                        int xx = (V[opcode2] + spriX);
                        int yy = (V[opcode3] + spriY);
    
                        if (arregloPantalla[xx % 64, yy % 32] == 1)
                        {
                            arregloPantalla[xx % 64, yy % 32] = 0;
                            Video.Screen.Blit(PixelNegro, new Point((xx % 64) * tamanoPixel, (yy % 32) * tamanoPixel));
                            V[0xF] = 1; //colision activado
                        }
                        else
                        {
                            arregloPantalla[xx % 64, yy % 32] = 1;
                            Video.Screen.Blit(PixelBlanco, new Point((xx % 64) * tamanoPixel, (yy % 32) * tamanoPixel));
                        }
                    }
                }
            }
        }
        Video.Screen.Update();
    }
    

    Finalmente si hacemos un Rebuild, debería compilar ok y ¡podemos ver el resultado con SDL.Net!, además si presionas la tecla q la paleta izquierda debería bajar.


    Código fuente completo de todo lo que llevamos hasta ahora (C# 2.0):

    using System;
    using System.IO; //Librería de .Net para manejar archivos
    using System.Runtime.InteropServices;  //para el "beep" de la Bios
    using System.Drawing;  //Librería de .Net para manejar Colores
    using System.Windows.Forms; //Para enviar mensajes en ventanas de dialogos
    
    using SdlDotNet.Core; //Eventos
    using SdlDotNet.Graphics; //para Surfaces
    using SdlDotNet.Graphics.Sprites;  //para Sprites y textos en pantalla
    using SdlDotNet.Input; //para manejo de teclado
    
    
    namespace Chip8
    {
    	class Emulador
    	{     
    
    		#region variables emulador
    
    		//variables principales
            const int DIR_INICIO        = 0x200;
    		const int TAMANO_MEM        = 0xFFF;
    		const int TAMANO_PILA       = 16;
    		const int CANT_REGISTROS    = 16;
    
            //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;
    		
            //resolucion de pantalla 64x32 (mas alta que larga)
            const int RES_X	= 64;
    		const int RES_Y	= 32;
            int[,] arregloPantalla = new int[RES_X, RES_Y];
    		
            //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();
           
            private Surface PixelNegro;
            private Surface PixelBlanco;
            const int tamanoPixel = 5; // tamaño de pixel, sirve para zoom de todo el emulador
            public const int operPorSegundo = 600;
            public const int operPorFrame = operPorSegundo / 60;
            private int TimeUntilTimerUpdate;  //para la simulación de timers
    
            #endregion
               
            		
            /// 
            /// Constructor de la clase
            /// 
            public Emulador()
            {
                try
                {
                    Video.SetVideoMode(RES_X * tamanoPixel, RES_Y * tamanoPixel);
                    Video.Screen.Fill(Color.Black);
                    Events.Fps = 60;
                    Events.TargetFps = 60;
    
                    PixelNegro = Video.CreateRgbSurface(tamanoPixel, tamanoPixel);
                    PixelBlanco = Video.CreateRgbSurface(tamanoPixel, tamanoPixel);
                    PixelNegro.Fill(Color.Black);
                    PixelBlanco.Fill(Color.White);
    
                    //declaración de Eventos
                    Events.KeyboardDown += new EventHandler(this.Tecla_Presionada);
                    Events.KeyboardUp += new EventHandler(this.Tecla_Liberada);
                    Events.Quit += new EventHandler(this.Events_Salir);
                    Events.Tick += new EventHandler(this.Events_Tick);
    
                    ResetHardware();
                    CargarJuego("PONG");
                }
                catch (Exception ex)
                {
                    MessageBox.Show("Error General: " + ex.Message + "-" + ex.StackTrace);                
                }
            }
    
    		static void Main(string[] args)
    		{
                Emulador emulador = new Emulador();	
    			emulador.Run();	
    		}
    
    
            private void Events_Salir(object sender, QuitEventArgs e)
            {
                Events.QuitApplication();
            }
    
            private void Tecla_Presionada(object sender, KeyboardEventArgs e)
            {
                //Salimos del juego con tecla Escape
                if (e.Key == Key.Escape)
                {
                    Events.QuitApplication();
                }
    
                if (e.Key == Key.One) { teclasPresionadas[TECLA_1] = true; }
                if (e.Key == Key.Two) { teclasPresionadas[TECLA_2] = true; }
                if (e.Key == Key.Three) { teclasPresionadas[TECLA_3] = true; }
                if (e.Key == Key.Four) { teclasPresionadas[TECLA_4] = true; }
                if (e.Key == Key.Q) { teclasPresionadas[TECLA_Q] = true; }
                if (e.Key == Key.W) { teclasPresionadas[TECLA_W] = true; }
                if (e.Key == Key.E) { teclasPresionadas[TECLA_E] = true; }
                if (e.Key == Key.R) { teclasPresionadas[TECLA_R] = true; }
                if (e.Key == Key.A) { teclasPresionadas[TECLA_A] = true; }
                if (e.Key == Key.S) { teclasPresionadas[TECLA_S] = true; }
                if (e.Key == Key.D) { teclasPresionadas[TECLA_D] = true; }
                if (e.Key == Key.F) { teclasPresionadas[TECLA_F] = true; }
                if (e.Key == Key.Z) { teclasPresionadas[TECLA_Z] = true; }
                if (e.Key == Key.X) { teclasPresionadas[TECLA_X] = true; }
                if (e.Key == Key.C) { teclasPresionadas[TECLA_C] = true; }
                if (e.Key == Key.V) { teclasPresionadas[TECLA_V] = true; }
            }
    
            private void Tecla_Liberada(object sender, KeyboardEventArgs e)
            {
                if (e.Key == Key.One) { teclasPresionadas[TECLA_1] = false; }
                if (e.Key == Key.Two) { teclasPresionadas[TECLA_2] = false; }
                if (e.Key == Key.Three) { teclasPresionadas[TECLA_3] = false; }
                if (e.Key == Key.Four) { teclasPresionadas[TECLA_4] = false; }
                if (e.Key == Key.Q) { teclasPresionadas[TECLA_Q] = false; }
                if (e.Key == Key.W) { teclasPresionadas[TECLA_W] = false; }
                if (e.Key == Key.E) { teclasPresionadas[TECLA_E] = false; }
                if (e.Key == Key.R) { teclasPresionadas[TECLA_R] = false; }
                if (e.Key == Key.A) { teclasPresionadas[TECLA_A] = false; }
                if (e.Key == Key.S) { teclasPresionadas[TECLA_S] = false; }
                if (e.Key == Key.D) { teclasPresionadas[TECLA_D] = false; }
                if (e.Key == Key.F) { teclasPresionadas[TECLA_F] = false; }
                if (e.Key == Key.Z) { teclasPresionadas[TECLA_Z] = false; }
                if (e.Key == Key.X) { teclasPresionadas[TECLA_X] = false; }
                if (e.Key == Key.C) { teclasPresionadas[TECLA_C] = false; }
                if (e.Key == Key.V) { teclasPresionadas[TECLA_V] = false; }
            }
    
    
            private void Events_Tick(object sender, TickEventArgs e)
            {
                EmulaFrame();
            }
    
            // Inicializamos los Eventos
            public void Run()
            {
                Events.Run();
            }
    
            void ResetHardware()
            {
                // Reseteamos los Timers
                delayTimer = 0x0;
                soundTimer = 0x0;
    
                // Reseteamos variables
                instruccion = 0x0;
                PC = DIR_INICIO;
                SP = 0x0;
                I = 0x0;
    
                // Limpiamos la memoria
                for (int i = 0; i < TAMANO_MEM; i++)
                    memoria[i] = 0x0;
    
    
                // Limpiamos registros
                for (int i = 0; i < CANT_REGISTROS; i++)
                    V[i] = 0x0;
    
    
                // Limpiamos el stack
                for (int i = 0; i < TAMANO_PILA; i++)
                    pila[i] = 0x0;
    
                // Cargamos las fuentes en la memoria por ej, los marcadores del juego PONG esos "[0-0]", "[0-2]"
                for (int i = 0; i < 80; i++)
                    memoria[i] = arregloFuentes[i];	
    
                Video.Screen.Fill(Color.Black);
                Video.Screen.Update();
            }
    
    
            void ManejaTimers()
            { 
                if (delayTimer > 0) 
                    delayTimer--;
                if (soundTimer > 0)
                    soundTimer--;
            }
    
            void CargarJuego(string nombreRom)
    		{            
                FileStream rom;
    
                try
                {
                    rom = new FileStream(@nombreRom, FileMode.Open);
    
                    if (rom.Length == 0)
                    {
                        throw new Exception("Error en la Carga de la Rom: ROM dañada o vacía");
                    }
    
                    // Comenzamos a cargar la rom a la memoria a partir de la dir 0x200
                    for (int i = 0; i < rom.Length; i++)
                        memoria[DIR_INICIO + i] = (byte)rom.ReadByte();	
    
                    rom.Close();                
                }
                catch (Exception ex)
                {
                    throw new Exception("Error en la Carga de la Rom: " + ex.Message);
                }
    		}
    
            public void EmulaFrame() //representa un frame
            {
                //por cada Tick o Frame, se ejecutan 600/60=10 instrucciones
                for (int i = 0; i < operPorFrame; i++)
                {
                    EmulaOpcodes();
                }
            }
    
            void EmulaOpcodes()
            {
                if (TimeUntilTimerUpdate == 0)
                {
                    if (delayTimer > 0)
                        delayTimer--;
    
                    if (soundTimer > 0) 
                    	soundTimer--;
    
                    TimeUntilTimerUpdate = operPorFrame;
                }
                else
                {
                    TimeUntilTimerUpdate--;
                }
    
                EjecutaOpcodes();
            }
    
    
            void EjecutaOpcodes()
    		{
                #region lectura de instrucciones 
    
                // leemos cada una de las instrucciones desde la memoria. 
                // cada instruccion es de 2 bytes
                instruccion = memoria[PC] << 8 | memoria[PC + 1];
    
                // dejamos incrementado el Program Counter en 2, lista para leer 
                // la siguiente instruccion en el siguiente ciclo.
                PC += 2;
    
                #endregion 
    
                #region extracción de opcodes
    
                //obtengo el valor del registro KK, de largo 1 byte, el más chico de la instrucción
                KK = (instruccion & 0x00FF);
    
    			// cada opcode es de 4 bit
                opcode1 = (instruccion & 0xF000) >> 12; //los 4 bits mayores de la instrucción
                opcode2 = (instruccion & 0x0F00) >> 8;  //X
                opcode3 = (instruccion & 0x00F0) >> 4;  //Y
                opcode4 = (instruccion & 0x000F) >> 0;  //Opcode N = los 4 bits menores de la instrucción
    
                //obtengo el valor del opcode NNN
                NNN = (instruccion & 0x0FFF);
    
                #endregion
    
                #region ejecución de instrucciones
    
                // Ejecutamos las instrucciones a travez de los opcodes
    			switch (opcode1)
    			{
    				// opcodes del tipo 0xxx
    				case (0x0):
    				{
                        switch (instruccion)
    					{
    						// opcode 00E0: Clear Screen.
    						case (0x00E0):
    						{
    							ClearScreen();
    							break;
    						}
    						// opcode 00EE: Return From Subroutine.
    						case (0x00EE):
    						{
    							ReturnFromSub();
    							break;
    						}
    					}
    					break;
    				}
    				// opcodes del tipo 1xxx
    				case (0x1):
    				{
    					// opcode 1NNN: Jump To Address NNN.
    					JumpToAddr();
    					break;
    				}
                    // opcodes del tipo 2xxx
    				case (0x2):
    				{
    					// opcode 2NNN: Call Subroutine At Address NNN.
    					CallSub();
    					break;
    				}
                    // opcodes del tipo 3xxx
    				case (0x3):
    				{
    					// opcode 4XKK: Skip Next Instruction If VX == KK
    					SkipIfEql();
    					break;
    				}
                    // opcodes del tipo 4xxx
    				case (0x4):
    				{
    					// opcode 4XKK: Skip Next Instruction If VX != KK
    					SkipIfNotEql();
    					break;
    				}
                    // opcodes del tipo 5xxx
    				case (0x5):
    				{
    					// opcode 5XY0: Skip Next Instruction If VX == VY
    					SkipIfRegEql();
    					break;
    				}
                    // opcodes del tipo 6xxx
    				case (0x6):
    				{
    					// opcode 6XKK: Assign Number KK To Register X.
    					AssignNumToReg();
    					break;
    				}
                    // opcodes del tipo 7xxx
    				case (0x7):
    				{
    					// opcode 7XKK: Add Number KK To Register X.
    					AddNumToReg();
    					break;
    				}
                    // opcodes del tipo 8xxx
    				case (0x8):
    				{
    					//Tenemos varios tipos
    					switch (opcode4)
    					{
    						// opcode 8XY0: Assign From Register To Register.
    						case (0x0):
    						{
    							AssignRegToReg();
    							break;
    						}
    						// opcode 8XY1: Bitwise OR Between Registers.
    						case (0x1):
    						{
    							RegisterOR();
    							break;
    						}
    						// opcode 8XY2: Bitwise AND Between Registers.
    						case (0x2):
    						{
    							RegisterAND();
    							break;
    						}
    						// opcode 8XY3: Bitwise XOR Between Registers.
    						case (0x3):
    						{
    							RegisterXOR();
    							break;
    						}
    						// opcode 8XY4: Add Register To Register.
    						case (0x4):
    						{
    							AddRegToReg();
    							break;
    						}
    						// opcode 8XY5: Sub Register From Register.
    						case (0x5):
    						{
    							SubRegFromReg();
    							break;
    						}
    						// opcode 8XY6: Shift Register Right Once.
    						case (0x6):
    						{
    							ShiftRegRight();
    							break;
    						}
    						// opcode 8XY7: Sub Register From Register (Reverse Order).
    						case (0x7):
    						{
    							ReverseSubRegs();
    							break;
    						}
    						// opcode 8XYE: Shift Register Left Once.
    						case (0xE):
    						{
    							ShiftRegLeft();
    							break;
    						}
    					}
    					break;
    				}
                    // opcodes del tipo 9xxx
    				case (0x9):
    				{
    					// opcode 9XY0: Skip Next Instruction If VX != VY
    					SkipIfRegNotEql();
    					break;
    				}
    				// opcodes del tipo AXXX
    				case (0xA):
    				{
    					// opcode ANNN: Set Index Register To Address NNN.
    					AssignIndexAddr();
    					break;
    				}
                    // opcodes del tipo BXXX
    				case (0xB):
    				{
    					// opcode BNNN: Jump To NNN + V0.
    					JumpWithOffset();
    					break;
    				}
                    // opcodes del tipo CXXX
    				case (0xC):
    				{
    					// opcode CXKK: Assign Bitwise AND Of Random Number & KK To Register X.
    					RandomANDnum();
    					break;
    				}
                    // opcodes del tipo DXXX
    				case (0xD):
    				{
    					// opcode DXYN: Draw Sprite To The Screen.
    					DrawSprite();
    					break;
    				}
                    // opcodes del tipo EXXX
    				case (0xE):
    				{
    					// Tenemos 2 tipos según EXKK
    					switch (KK)
    					{
    						// opcode EX9E: Skip Next Instruction If Key In VX Is Pressed.
    						case (0x9E):
    						{
    							SkipIfKeyDown();
    							break;
    						}
    						// opcode EXA1: Skip Next Instruction If Key In VX Is NOT Pressed.
    						case (0xA1):
    						{
    							SkipIfKeyUp();
    							break;
    						}
    					}
    					break;
    				}
                    // opcodes del tipo FXXX
    				case (0xF):
    				{
    					// tenemos varios tipos de Opcodes
    					switch (KK)
    					{
    						// opcode FX07: Assign Delay Timer To Register.
    						case (0x07):
    						{
    							AssignFromDelay();
    							break;
    						}
    						// opcode FX0A: Wait For Keypress And Store In Register.
    						case (0x0A):
    						{
    							StoreKey();
    							break;
    						}
    						// opcode FX15: Assign Register To Delay Timer.
    						case (0x15):
    						{
    							AssignToDelay();
    							break;
    						}
    						// opcode FX18: Assign Register To Sound Timer.
    						case (0x18):
    						{
    							AssignToSound();
    							break;
    						}
    						// opcode FX1E: Add Register To Index.
    						case (0x1E):
    						{
    							AddRegToIndex();
    							break;
    						}
    						// opcode FX29: Index Points At CHIP8 Font Char In Register.
    						case (0x29):
    						{
    							IndexAtFontC8();
    							break;
    						}
    						// opcode FX30: Index Points At SCHIP8 Font Char In Register.
    						case (0x30):
    						{
    							IndexAtFontSC8();
    							break;
    						}
    						// opcode FX33: Store BCD Representation Of Register In Memory.
    						case (0x33):
    						{
    							StoreBCD();
    							break;
    						}
    						// opcode FX55: Save Registers To Memory.
    						case (0x55):
    						{
    							SaveRegisters();
    							break;
    						}
    						// opcode FX65: Load Registers From Memory.
    						case (0x65):
    						{
    							LoadRegisters();
    							break;
    						}
    					}
    					break;
    				}
    			}
                
                #endregion
            }
    
            #region implementación de métodos para cada instrucción
    
            void ClearScreen()
            {
                Video.Screen.Fill(Color.Black);
                Video.Screen.Update();
    
                for (int p = 0; p < RES_X; p++)
                    for (int q = 0; q < RES_Y; q++)
                        arregloPantalla[p, q] = 0;
            }
    
    		void ReturnFromSub()
    		{
    			// Se diminuye el SP en 1
    			SP--;
    
    			// Apuntamos el PC (que apunta a la sgte. instrucción a ejecutar) a la
                // posición salvada en la Pila
    			PC = pila[SP];
    		}
    
    		void JumpToAddr()
    		{
    			// salta a la instrucción dada. No se salta directamente, sino que se le indica
                // al PC que vaya a dicha direccion luego de salir del actual ciclo.
                PC = NNN;
    		}
    
    		void CallSub()
    		{
    			// Salva la posicion actual del PC en la Pila, para volver a penas se ejecute la subrutina
    			pila[SP] = PC;
    			SP++;
    
    			// Saltamos a la subrutina indicada en NNN
                PC = NNN;
    		}
    
    		void SkipIfEql()
    		{
    			// Recordar que Opcode2=X
    			if (V[opcode2] == KK)
    			{
    				// Salta a la siguiente instruccion
    				PC += 2;
    			}
    		}
    
    		void SkipIfNotEql()
    		{
    			if (V[opcode2] != KK)
    			{
                    // Salta a la siguiente instruccion
    				PC += 2;
    			}
    		}
    
    		void SkipIfRegEql()
    		{
    			if (V[opcode2] == V[opcode3])
    			{
                    // Salta a la siguiente instruccion
    				PC += 2;
    			}
    		}
    
    		void AssignNumToReg()
    		{
    			V[opcode2] = KK;
    		}
    
    	
    		/**
    		entrega los 8 bits menores (char) de un numero de 16 bits (int)
    
    		@param number el numero de 16 bit
    		@return el numero de 8 bits
    		**/
    		private char getLower(int number)
    		{
    			return (char)(number&0xFF);
    		}
    
    		void AddNumToReg()
    		{
    			V[opcode2] += KK;
    		}
    
    		void AssignRegToReg()
    		{
    			//VX = VY
    			V[opcode2] = V[opcode3];
    		}
    
    		void RegisterOR()
    		{
    			// OR binario es |, entonces hacemos VX = VX | VY o mas elegante VX |= VY
    			V[opcode2] |= V[opcode3];
    		}
    
    		void RegisterAND()
    		{
                // OR binario es &, entonces hacemos VX = VX & VY o mas elegante VX &= VY
    			V[opcode2] &= V[opcode3];
    		}
    
    		void RegisterXOR()
    		{
                // XOR es ^, entonces hacemos VX = VX ^ VY o mas elegante VX ^= VY
    			V[opcode2] ^= V[opcode3];
    		}
    
    		void AddRegToReg()
    		{
    			//Con >> extraemos los 8 bits mayores de la suma, si el resultado supera
                //los 8 bits el >> 8 dara 1 si no 0
    			V[0xF] = (V[opcode2] + V[opcode3]) >> 8;
    
    			//VX = VX + VY
    			V[opcode2] += V[opcode3];
    		}
    
    		void SubRegFromReg()
    		{
    			//seteamos F en 1 si VX > VY
    			if (V[opcode2] >= V[opcode3])
    				V[0xF] = 0x1;
    			else
    				V[0xF] = 0x0;
    
    			//VX = VX - VY
    			V[opcode2] -= V[opcode3];
    		}
    
    		void ShiftRegRight()
    		{
                //VF = VX AND 1 (VF valdrá 1 o 0). Para este case es más optimo
    			V[0xF] = V[opcode2] & 0x1;
                //Manera elegante de escribir un shift a la derecha para dividir por 2: V[opcode2] = V[opcode2] >> 1;
    			V[opcode2] >>= 1;
    		}
    
    		void ReverseSubRegs()
    		{
    			if (V[opcode2] <= V[opcode3])
    			{
    				V[0xF] = 0x1;
    			}
    			else
    			{
    				V[0xF] = 0x0;
    			}
    
                V[opcode2] = V[opcode3] - V[opcode2];
    		}
    
    		void ShiftRegLeft()
    		{
                //VF = VX AND 10 hex
    			V[0xF] = V[opcode2] & 0x10;
                //Manera elegante de escribir un shift a la izquierda para multiplicar por 2: V[opcode2] = V[opcode2] << 1;
    			V[opcode2] <<= 1;
    		}
    
    		void SkipIfRegNotEql()
    		{			
    			if (V[opcode2] != V[opcode3])
    			{
    				//Aumentamos el PC en 2 para saltar a la siguiente instrucción
    				PC += 2;
    			}
    		}
    
    		void AssignIndexAddr()
    		{
    			// se setea el Registro de Índice (I) a la dirección NNN.
                I = NNN;
    		}
    
    		void JumpWithOffset()
    		{
                PC = NNN + V[0x0];
    		}	
    
    		void RandomANDnum()
    		{
                //usamos el variable rnd seteada en la clase. Con el método Next se le puede dar el mínimo (0) y máximo (255)
                int numeroRnd = rnd.Next(0,255);
                V[opcode2] = numeroRnd & KK;
    		}
    
            /// 
            /// Screen is 64x62 pixels
            /// Todos los drawings son hechos en modos XOR. 
            /// Cuando uno o mas pixels son borrados mientras un sprite es pintado, 
            /// el registro VF se setea en 01, sino queda en 00.
            /// 
            void DrawSprite()
            {
                // Reseteamos el registro que detecta colisiones
                V[0xF] = 0x0;
    
                if ((instruccion & 0x000F) == 0) //opcode & 0x000F =opcode4
                {
                    // Dibujamos un Sprite de SCHIP8 de tamaño 16x16
                    // No implementado aún
                }
                else
                {
                    // Bibujamos un Sprite de CHIP8 de tamaño 8xN				
                    for (int spriY = 0; spriY < opcode4; spriY++)
                    {
                        for (int spriX = 0; spriX < 8; spriX++)
                        {
                            int x = (memoria[I + spriY] & (0x80 >> spriX));
                            if (x != 0)
                            {
                                // checkeamos por alguna colision
                                int xx = (V[opcode2] + spriX);
                                int yy = (V[opcode3] + spriY);
    
                                if (arregloPantalla[xx % 64, yy % 32] == 1)
                                {
                                    arregloPantalla[xx % 64, yy % 32] = 0;
                                    Video.Screen.Blit(PixelNegro, new Point((xx % 64) * tamanoPixel, (yy % 32) * tamanoPixel));
                                    V[0xF] = 1; //colision activado
                                }
                                else
                                {
                                    arregloPantalla[xx % 64, yy % 32] = 1;
                                    Video.Screen.Blit(PixelBlanco, new Point((xx % 64) * tamanoPixel, (yy % 32) * tamanoPixel));
                                }
                            }
                        }
                    }
                }
                Video.Screen.Update();
            }
    
    		void SkipIfKeyDown()
    		{
                if (teclasPresionadas[MapeoTeclas[V[opcode2]]] == true)
    				PC += 2;		
    		}
    
    		void SkipIfKeyUp()
    		{
                if (teclasPresionadas[MapeoTeclas[V[opcode2]]] == false)
    				PC += 2;
    		}
    
    		void AssignFromDelay()
    		{
    			V[opcode2] = delayTimer;
    		}
    
    		void StoreKey()
    		{
                for (int i = 0; i < teclasPresionadas.Length; i++)
    			{
                    if (teclasPresionadas[i] == true)
    				{
    					V[opcode2] = i;
    				}
    			}
    		}
    
    		void AssignToDelay()
    		{
    			delayTimer = V[opcode2];
    		}
    
    		void AssignToSound()
    		{
    			soundTimer = V[opcode2];
    		}
    
    		void AddRegToIndex()
    		{
    			I += V[opcode2];
    		}
    
    		void IndexAtFontC8()
    		{
    			I = (V[opcode2] * 0x5);
    		}
    
    		void IndexAtFontSC8()
    		{
    			// Not Implemanted yer, SCHIP8 Function.
    		}
    
    		void StoreBCD()
    		{
    			int vx = (int) V[opcode2];
                memoria[I] = vx / 100;              //centenas
                memoria[I + 1] = (vx / 10) % 10;    //decenas
                memoria[I + 2] = vx % 10;           //unidades
    		}
    
    		void SaveRegisters()
    		{
    			for (int i = 0; i <= opcode2; i++)
    			{
    				memoria[I++] = V[i];
    			}
                //I += 1;
    		}
    
    		void LoadRegisters()
    		{
    			for (int i = 0; i <= opcode2; i++)
    			{
    				V[i] = memoria[I++];
    			}
                //I += 1;
    		}
    
                   
            #endregion
    
        }
    }
    

    Bueno, con esto es suficiente por hoy, ya tenemos un emulador graficamente más decente que en modo consola. Nos vemos en el siguiente capítulo.

    Bajar aquí el proyecto .Net para Visual Studio 2008 que contiene los fuentes de este capítulo.



    Lección Anterior | Índice | Siguiente Lección
    Volver a Página Principal

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