Volver a Página Principal

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

Lenguaje: C# 2.0
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

La idea

La idea de este capítulo es mostrar como dejar el emulador en una ventana con las opciones Cargar Juego, Pausar y Salir.
Nota: algunas imágenes se encogieron para mejorar la lectura de la página. Haz clic sobre ellas para verlas en tamaño real.

Conceptos

La ventana del emulador tiene 3 componentes:
  • La principal es System.Windows.Forms que en su interior contiene 2 componentes: System.Windows.Forms.MenuStrip que permite el menú superior y que es parte del Framework .Net, y la componente gráfica SdlDotNet SurfaceControl que nos permite dibujar en pantalla los juegos emulados tal como en capítulos anteriores, esta componente es parte de SDL.NET (está contenida en bin\SdlDotNet.dll).

    Agregar un Formulario

    Debemos ir al proyecto y con el 2do. botón agregar un nuevo elemento tipo Windows Form y colocarle el nombre Chip8.cs:



    Notar que Visual Studio genera automáticamente el archivo Chip8.Designer.cs.

    Se debe setear el ancho y alto del formulario en 650 de ancho por 382 de alto que es un tamaño aceptable.

    Agregar la componente para menú superior System.Windows.Forms.MenuStrip

    Abrimos el formulario en modo diseño y vamos al ToolBox (CONTROL + ALT + X) y allí vamos a la sección Menus & Toolbars y ubicamos la componente MenuStrip, la tomamos y arrastramos arriba-izquierda del formulario:



    Escribimos sobre este control las opciones "Archivo" y bajo esta "Cargar Juego", "Reset" y "Salir", luego arriba escribir "Pausa":

    Agregar la componente gráfica SdlDotNet SurfaceControl

    Debemos agregar la componente SdlDotNet SurfaceControl al formulario que agregamos en el paso anterior. Haremos esto mediante código, así que hacemos doble clic al archivo "Chip8.Designer.cs" y una vez abierto abrimos la sección "Windows Form Designer generated code", con esto tenemos el siguiente código:
    /// 
    /// Required designer variable.
    /// 
    private System.ComponentModel.IContainer components = null;
    
    ...
    
    #region Windows Form Designer generated code
    
    /// 
    /// Required method for Designer support - do not modify
    /// the contents of this method with the code editor.
    /// 
    private void InitializeComponent()
    {
        this.components = new System.ComponentModel.Container();
        this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
        this.Text = "Chip8";
    }
    
    #endregion
    
    Aquí debemos incluir la nueva componente con la variable SdlDotNet.Windows.SurfaceControl scDisplay que representará la parte gráfica del emulador y lo ubicamos abajo del control de Menú, es decir en el punto 0,27 y con una resolución de 640 x 320::
    /// 
    /// Required designer variable.
    /// 
    private System.ComponentModel.IContainer components = null;
    
    // Control gráfico dentro de la ventana que permite mostrar el juego
    private SdlDotNet.Windows.SurfaceControl scDisplay;
    
    ...
    
    private void InitializeComponent()
    {
        System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(Chip8));
        this.scDisplay = new SdlDotNet.Windows.SurfaceControl();
        ((System.ComponentModel.ISupportInitialize)(this.scDisplay)).BeginInit();
    	...
    	// seteamos el control gráfico scDisplay para el emulador
    	this.scDisplay.AccessibleDescription = "SdlDotNet SurfaceControl";
    	this.scDisplay.AccessibleName = "SurfaceControl";
    	this.scDisplay.AccessibleRole = System.Windows.Forms.AccessibleRole.Graphic;
    	this.scDisplay.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
    	            | System.Windows.Forms.AnchorStyles.Left)
    	            | System.Windows.Forms.AnchorStyles.Right)));
    	this.scDisplay.Image = ((System.Drawing.Image)(resources.GetObject("scDisplay.Image")));
    	this.scDisplay.InitialImage = null;
    	this.scDisplay.Location = new System.Drawing.Point(0, 27);
    	this.scDisplay.Name = "scDisplay";
    	this.scDisplay.Size = new System.Drawing.Size(640, 320);
    	this.scDisplay.TabIndex = 0;
    	this.scDisplay.TabStop = false;
    	
    	// Agregamos el control gráfico
    	this.Controls.Add(this.scDisplay);
    }
    
    Se debe agregar el recurso "scDisplay.Image" al proyecto, para esto copia esta pequeña imagen y pégala en el archivo Chip8.resx en la sección Images:



    Luego cambia la opción de Persistencia de la imagen por Embedded in .resx de esta forma si queremos hacer un deploy del emulador, por ejemplo en un archivo .exe, esta imagen estará embebida en este y no sea necesario tener la imagen en el mismo directorio:



    Por último seteamos el color negro de fondo para el control scDisplay.



    Para revisar como quedó, si vamos al diseño del emulador se verá la pequeña imagen que cargamos y el fondo negro:

    Cambios en el código

    Debemos aplicar varios cambios al código para que la ventana sea el "manejador" principal que usará la clase "Emulador.cs" para casi todas las operaciones.

  • Lo primero que haremos, será cambiar el código que inicia la aplicación para que parta en modo ventana y no el juego directamente, esto se puede comprobar dándole F5 y se abrirá el emulador del capítulo anterior y no la nueva ventana. Por lo que agregamos a la clase Chip8.cs estos 2 namespaces:
    using SdlDotNet.Graphics;
    using SdlDotNet.Core;
    
    En la clase del formulario Chip8.cs, creamos una nueva instancia de la clase Emulador:
    namespace Chip8
    {
        public partial class Chip8 : Form
        {
            Emulador Emu;
            public Chip8()
            {
                InitializeComponent();
                Emu = new Emulador();
            }
        }
    }
    
  • En Chip8.cs creamos el método UpdateDisplay() que es que va a actualizar el control Surface en el formulario con las gráficas que retorna la clase Emulador.cs:
    // Actualiza control gráfico con lo que entrega el Surface de la clase Emulador.cs
    private void UpdateDisplay()
    {
    	scDisplay.Blit(Emu.SurfaceOut);
    	scDisplay.Update();
    }
    
  • En la clase Emulador.cs comentar el método Event Tick y sacar la inicialización en el constructor:
    //private void Events_Tick(object sender, TickEventArgs e)
    //{
    //    Video.WindowCaption = "Emulador Chip-8  FPS: " + Events.Fps;
    //    EmulaFrame();
    //}
    
    ...
    
    public Emulador()
    {
    	...
    	//Events.Tick += new EventHandler(this.Events_Tick);
    	...
    }
    
    y este Evento lo manejaremos por el formulario, por lo que se debe inicializar con SdlDotNet.Core.Events.Tick += ... en la clase Chip8.cs y crear el método que llama a UpdateDisplay():
    // constructor
    public Chip8()
    {
    	...
    	SdlDotNet.Core.Events.Tick += new EventHandler(Events_Tick);
    }
    ...
    void Events_Tick(object sender, TickEventArgs e)
    {
    	Emu.EmulaFrame();
    	UpdateDisplay();
    }
    
  • En la clase Emulador.cs debemos comentar el método Main que inicia la aplicación:
    //static void Main(string[] args)
    //{
    //	Emulador emulador = new Emulador();	
    //	emulador.Run();	
    //}
    
    Dejaremos que el emulador se inicie desde otra clase, es más elegante y es un punto básico de que se denomina Programación orientada a objetos. Por lo que creamos la clase Programa.cs y adentro escribimos el código que "lanza" el emulador, es decir creamos una instancia de la clase con "new Chip8()":
    using System;
    using System.Windows.Forms;
    
    namespace Chip8
    {
        class Programa
        {
            [STAThread]
            public static void Main()
            {
                Application.EnableVisualStyles();
                Application.SetCompatibleTextRenderingDefault(false);
                Application.Run(new Chip8());
            }
        }
    }
    
  • En la clase Emulador definir nuevas propiedades:
    /// 
    /// Salida de gráficos
    /// 
    private Surface surfaceOut;
    public Surface SurfaceOut
    {
        get { return surfaceOut; }
    }
    
    En el constructor y otras partes del código cambiamos el uso de Video.Screen por nuestra nueva variable de salida surfaceOut, a la vez de incrementar la variable que guarda el largo/alto de un pixel tamanoPixel de 5 a 10:
    
    const int tamanoPixel = 10; // tamaño de pixel, sirve para zoom de todo el emulador
    
    /// 
    /// Constructor de la clase
    /// 
    public Emulador()
    {
        try
        {
            //Video.SetVideoMode(RES_X * tamanoPixel, RES_Y * tamanoPixel);
            //Video.Screen.Fill(Color.Black);
            //Events.Fps = 60;
    
            surfaceOut = Video.CreateRgbSurface(RES_X * tamanoPixel, RES_Y * tamanoPixel);
    
    Cambiamos el método ResetHardware ya que usamos el método ClearScreen():
    void ResetHardware()
    {
    	...
    	//Video.Screen.Fill(Color.Black);
    	//Video.Screen.Update();
    	ClearScreen();
    }
    
    Cambiamos el método ClearScreen que se encarga de dejar la pantalla en negro, cambiando Video.Screen por surfaceOut:
    void ClearScreen()
    {
    	surfaceOut.Fill(Color.Black);
    	surfaceOut.Update();
    	...
    
    Cambiamos el método DrawSprite que se encarga pintar los pixels, cambiando Video.Screen por surfaceOut:
    void DrawSprite()
    {	
    	...
    	if (arregloPantalla[xx % 64, yy % 32] == 1)
    	{
    		arregloPantalla[xx % 64, yy % 32] = 0;
    		surfaceOut.Blit(PixelNegro, new Point((xx % 64) * tamanoPixel, (yy % 32) * tamanoPixel));
    		V[0xF] = 1; //colision activado
    	}
    	else
    	{
    		arregloPantalla[xx % 64, yy % 32] = 1;
    		surfaceOut.Blit(PixelBlanco, new Point((xx % 64) * tamanoPixel, (yy % 32) * tamanoPixel));
    	}
    	...
    	surfaceOut.Update();
    	...
    }
    
  • Agregar el evento de Click cuando se haga clic sobre Cargar Juego. En modo Diseño, se debe seleccionar Cargar Juego y a la derecha, escoger "Eventos" (es un ícono con un rayo de color anaranjado) y en la sección "Click" hacer doble clic sobre el espacio en blanco:



    Con esto se crea automáticamente una sección para poner nuestro código:



    Al hacer clic debemos activar los Eventos. Por ahora sólo haremos esto, más adelante mostraremos una Ventana de Diálogo para escoger el juego.
    
    Emulador.cs
    
    // constructor
    public Emulador()
    {
    	try
    	{
    		...
    		//ResetHardware();
    		//CargarJuego("PONG");
    		...
    
    public void CargarJuego(string nombreRom)  //dejamos público el método
    {
    	..
    }
    
    Chip8.cs
    // Evento clic en opción del menú "Cargar Juego"
    private void cargarJuegoToolStripMenuItem1_Click(object sender, EventArgs e)
    {
    	Emu.ResetHardware();
    	Emu.CargarJuego("PONG");
    	SdlDotNet.Core.Events.Run();
    }
    
  • En Chip8.cs agregamos el Evento de Cerrar Aplicación (usar el ícono del rayo) y en este finalizamos los Eventos de SDL.Net, esto permite que al hacer clic sobre la X que cierre la aplicación o sobre la opción "Salir" del menú superior, no queden pegados los sonidos u otros eventos.
    // Evento gatillado al cerrar la aplicación con la "x" de la ventana
    private void Chip8_FormClosed(object sender, FormClosedEventArgs e)
    {
    	CerrarAplicacion();
    }
    
    // Método que termina los eventos y cierra la aplicación
    void CerrarAplicacion()
    {
    	SdlDotNet.Core.Events.Close();
    	SdlDotNet.Core.Events.QuitApplication();
    	Application.Exit();
    }
    
    // Evento gatillado al presionar Salir
    private void salirToolStripMenuItem_Click(object sender, EventArgs e)
    {
    	CerrarAplicacion();
    }
    
    En Emulador.cs comentamos el método que cierra la aplicación que se controla desde el formulario, lo comentamos en el constructor y en el evento de Teclas Presionadas.
    public Emulador()
    {
    	try
    	{
    		...
    		//Events.Quit += new EventHandler(this.Events_Salir);
    		...
    	}
    	...
    	//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();
    	//}
    	...
    }
    
    En el editor de Visual Studio, si presionamos F5 ya podemos ver el emulador y allí vamos a "Archivo" -> "Cargar Rom" se cargará el emulador con el juego PONG:

    FPS, Pause, Reset, Ventana Carga de ROM y manejo del Teclado

    Para agregar los Frames Por Segundo (FPS) en la clase Chip8.cs se debe incluir en el método Events_Tick, se debe crear la variable de clase TargetFPS y de pasada en el constructor agregar el título del formulario:
    private int TargetFPS;
    
    // constructor
    public Chip8()
    {
    	...
    	this.Text = "Emulador Chip-8";
    	SdlDotNet.Core.Events.Tick += new EventHandler(Events_Tick);
    	TargetFPS = 60;
    	SdlDotNet.Core.Events.Fps = 60;
    }
    ...
    void Events_Tick(object sender, TickEventArgs e)
    {
    	this.Text = "Emulador Chip-8  FPS: " + SdlDotNet.Core.Events.Fps;
    	...
    }
    
    El Pause es muy importante en cualquier emulador. Lo primero es definir una variable de clase booleana que nos diga si el juego está pausado o no, le llamaremos "estaPausado", luego en el Event_Tick se debe validar que no ejecute los frames si está pausado, y finalmente generar el evento "click" de la opción "Pausa" con el ícono de eventos (rayo), con esto se genera el método pausaToolStripMenuItem_Click y adentro setear la variable "estaPausado" que definimos:
    private bool estaPausado;
    ...
    
    // constructor
    public Chip8()
    {
    	estaPausado = false;
    }
    ...
    // Game-Loop
    void Events_Tick(object sender, TickEventArgs e)
    {
        this.Text = "Emulador Chip-8  FPS: " + SdlDotNet.Core.Events.Fps;
        if (estaPausado == false)
        {
            Emu.EmulaFrame();
            UpdateDisplay();
        }
    }
    ...
    // Evento gatillado al presionar Pausa
    private void pausaToolStripMenuItem_Click(object sender, EventArgs e)
    {
        if (estaPausado == false)
            estaPausado = true;
        else
            estaPausado = false;
    }
    
    El Reset también es muy importante, así que imagino que saben que vamos a hacer: generamos el evento "click" de la opción "Reset" con el ícono de eventos (rayo) y adentro llamamos al método Reset de la clase Emulador.cs. No olvidar dejar público este último método.
    Emulador.cs
    public void ResetHardware()  //lo dejamos público
    {
    ...
    }
    
    Chip8.cs
    // Evento gatillado al presionar Reset
    private void resetToolStripMenuItem_Click(object sender, EventArgs e)
    {
    	Reset();
    }
    
    // Resetamos el hardware (registros, memoria) y cargamos de nuevo el juego
    void Reset()
    {
        Emu.ResetHardware();
        Emu.CargarJuego(nombreROM);  // en el siguiente paso definimos nombreROM          
    }
    
    Para hacer la la ventana de diálogo de carga de ROM se debe primero agregar el control System.Windows.Forms.OpenFileDialog al formulario, para esto lo buscamos en el ToolBox (Control + Alt + X) y lo arrastramos al panel inferior de la ventana de diseño.



    Ahora modificamos el método cargarJuegoToolStripMenuItem1_Click de la clase Chip8.cs:
    private string nombreROM = string.Empty;
    ...
    // Evento clic en opción del menú "Cargar Juego"
    private void cargarJuegoToolStripMenuItem1_Click(object sender, EventArgs e)
    {
    	openFileDialog1.FileName = string.Empty;
    
        if (openFileDialog1.ShowDialog() == DialogResult.OK)
        {
            nombreROM = openFileDialog1.FileName;
            Reset();
            SdlDotNet.Core.Events.Run();
        }
    }
    
    Con esto podremos cargar cualquier juego y no sólo la ROM de PONG:



    Para el manejo del Teclado, si nos fijamos al correr el emulador, no funcionan las teclas, basta con presionar Q y la paleta izquierda en el juego PONG no baja, por lo tanto debemos hacer unos ajustes.
    Lo primero es comentar en la clase Emulador.cs los eventos de Teclado. Con esto no tendríamos ningún manejo de evento en esta clase. También comantamos los métodos Tecla_Presionada, Tecla_Liberada las variables de cada tecla, y dejamos púbica la variable teclasPresionadas para ser utilizada desde el formulario:
    //using SdlDotNet.Core; //Eventos
    //using SdlDotNet.Input; //para manejo de teclado
    ...
    public bool[] teclasPresionadas = { false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false };
    ...
    //constructor
    public Emulador()
    {
    	...
    	//Events.KeyboardDown += new EventHandler(this.Tecla_Presionada);
    	//Events.KeyboardUp += new EventHandler(this.Tecla_Liberada);
    	...
    }
    ...
    //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; }
    //}
    
    //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;
    
    Lo segundo es modificar la clase Chip8.cs ya que ahora usamos el delegado KeyEventHandler del Framework .Net ya que el teclado lo manejamos desde el formulario y no la clase KeyboardEventArgs de SDL.Net como lo hacíamos antes:
    // variables para cada tecla
    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;
    
    // constructor
    public Chip8()
    {
    	...
    	this.KeyDown += new KeyEventHandler(this.Tecla_Presionada);
    	this.KeyUp += new KeyEventHandler(this.Tecla_Liberada);
    	...
    }
    
    private void Tecla_Presionada(object sender, KeyEventArgs e)
    {
        if (e.KeyCode == Keys.NumPad1) { Emu.teclasPresionadas[TECLA_1] = true; }
        if (e.KeyCode == Keys.NumPad2) { Emu.teclasPresionadas[TECLA_2] = true; }
        if (e.KeyCode == Keys.NumPad3) { Emu.teclasPresionadas[TECLA_3] = true; }
        if (e.KeyCode == Keys.NumPad4) { Emu.teclasPresionadas[TECLA_4] = true; }
        if (e.KeyCode == Keys.Q) { Emu.teclasPresionadas[TECLA_Q] = true; }
        if (e.KeyCode == Keys.W) { Emu.teclasPresionadas[TECLA_W] = true; }
        if (e.KeyCode == Keys.E) { Emu.teclasPresionadas[TECLA_E] = true; }
        if (e.KeyCode == Keys.R) { Emu.teclasPresionadas[TECLA_R] = true; }
        if (e.KeyCode == Keys.A) { Emu.teclasPresionadas[TECLA_A] = true; }
        if (e.KeyCode == Keys.S) { Emu.teclasPresionadas[TECLA_S] = true; }
        if (e.KeyCode == Keys.D) { Emu.teclasPresionadas[TECLA_D] = true; }
        if (e.KeyCode == Keys.F) { Emu.teclasPresionadas[TECLA_F] = true; }
        if (e.KeyCode == Keys.Z) { Emu.teclasPresionadas[TECLA_Z] = true; }
        if (e.KeyCode == Keys.X) { Emu.teclasPresionadas[TECLA_X] = true; }
        if (e.KeyCode == Keys.C) { Emu.teclasPresionadas[TECLA_C] = true; }
        if (e.KeyCode == Keys.V) { Emu.teclasPresionadas[TECLA_V] = true; }
    }
    
    private void Tecla_Liberada(object sender, KeyEventArgs e)
    {
        if (e.KeyCode == Keys.NumPad1) { Emu.teclasPresionadas[TECLA_1] = false; }
        if (e.KeyCode == Keys.NumPad2) { Emu.teclasPresionadas[TECLA_2] = false; }
        if (e.KeyCode == Keys.NumPad3) { Emu.teclasPresionadas[TECLA_3] = false; }
        if (e.KeyCode == Keys.NumPad4) { Emu.teclasPresionadas[TECLA_4] = false; }
        if (e.KeyCode == Keys.Q) { Emu.teclasPresionadas[TECLA_Q] = false; }
        if (e.KeyCode == Keys.W) { Emu.teclasPresionadas[TECLA_W] = false; }
        if (e.KeyCode == Keys.E) { Emu.teclasPresionadas[TECLA_E] = false; }
        if (e.KeyCode == Keys.R) { Emu.teclasPresionadas[TECLA_R] = false; }
        if (e.KeyCode == Keys.A) { Emu.teclasPresionadas[TECLA_A] = false; }
        if (e.KeyCode == Keys.S) { Emu.teclasPresionadas[TECLA_S] = false; }
        if (e.KeyCode == Keys.D) { Emu.teclasPresionadas[TECLA_D] = false; }
        if (e.KeyCode == Keys.F) { Emu.teclasPresionadas[TECLA_F] = false; }
        if (e.KeyCode == Keys.Z) { Emu.teclasPresionadas[TECLA_Z] = false; }
        if (e.KeyCode == Keys.X) { Emu.teclasPresionadas[TECLA_X] = false; }
        if (e.KeyCode == Keys.C) { Emu.teclasPresionadas[TECLA_C] = false; }
        if (e.KeyCode == Keys.V) { Emu.teclasPresionadas[TECLA_V] = false; }
    }
    

    El código fuente de la lección es el siguiente.
    Nota: se sacaron las líneas que indicamos que deben ser comentadas, esto para dejar el código limpio y ayudar a la lectura de este.

    Programa.cs
    using System;
    using System.Windows.Forms;
    
    namespace Chip8
    {
        class Programa
        {
            [STAThread]
            public static void Main()
            {
                Application.EnableVisualStyles();
                Application.SetCompatibleTextRenderingDefault(false);
                Application.Run(new Chip8());
            }
        }
    }
    

    Emulador.cs
    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.Graphics; //para Surfaces
    using SdlDotNet.Graphics.Sprites;  //para Sprites y textos en pantalla
    
    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;
    
            //variable para manejo del sonido
            bool ejecutaSonido = true;
    
            //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
    		};
    
            public bool[] teclasPresionadas = { false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false };
    		
            // 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);
            
            //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 = 10; // 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
    
            /// 
            /// Salida de gráficos
            /// 
            private Surface surfaceOut;
            public Surface SurfaceOut
            {
                get { return surfaceOut; }
            }
            		
            /// 
            /// Constructor de la clase
            /// 
            public Emulador()
            {
                try
                {
                    surfaceOut = Video.CreateRgbSurface(RES_X * tamanoPixel, RES_Y * tamanoPixel);
                    PixelNegro = Video.CreateRgbSurface(tamanoPixel, tamanoPixel);
                    PixelBlanco = Video.CreateRgbSurface(tamanoPixel, tamanoPixel);
                    PixelNegro.Fill(Color.Black);
                    PixelBlanco.Fill(Color.White);
                }
                catch (Exception ex)
                {
                    MessageBox.Show("Error General: " + ex.Message + "-" + ex.StackTrace);                
                }
            }
    
            public 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];	
    
                ClearScreen();
            }
    
            // Carga la ROM (archivo) a la memoria (arreglo)
            public 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);
                }
    		}
    
            // Emula la ejecución de un Frame
            public void EmulaFrame() //representa un frame
            {
                if (soundTimer > 0 && ejecutaSonido == true)
                {
                    Console.Beep(350, 50);
                    ejecutaSonido = false;                
                }
                
                //por cada Tick o Frame, se ejecutan 600/60=10 instrucciones
                for (int i = 0; i < operPorFrame; i++)
                {
                    EmulaOpcodes();
                }
            }
    
            // Emula la ejecución de un Opcode
            void EmulaOpcodes()
            {
                if (TimeUntilTimerUpdate == 0)
                {
                    if (delayTimer > 0)
                        delayTimer--;
    
                    if (soundTimer > 0) 
                    	soundTimer--;
    
                    TimeUntilTimerUpdate = operPorFrame;
                }
                else
                {
                    TimeUntilTimerUpdate--;
                }
    
                EjecutaOpcodes();
            }
    
            // Lee las instrucciones de la memoria y ejecuta los Opcodes
            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();
                surfaceOut.Fill(Color.Black);
                surfaceOut.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;
                                    surfaceOut.Blit(PixelNegro, new Point((xx % 64) * tamanoPixel, (yy % 32) * tamanoPixel));
                                    V[0xF] = 1; //colision activado
                                }
                                else
                                {
                                    arregloPantalla[xx % 64, yy % 32] = 1;
                                    surfaceOut.Blit(PixelBlanco, new Point((xx % 64) * tamanoPixel, (yy % 32) * tamanoPixel));
                                }
                            }
                        }
                    }
                }
                surfaceOut.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];
                ejecutaSonido = true;
    		}
    
    		void AddRegToIndex()
    		{
    			I += V[opcode2];
    		}
    
    		void IndexAtFontC8()
    		{
    			I = (V[opcode2] * 0x5);
    		}
    
    		void IndexAtFontSC8()
    		{
    			// No implementado aún, función de Super Chip-8
    		}
    
    		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];
    			}            
    		}
    
    		void LoadRegisters()
    		{
    			for (int i = 0; i <= opcode2; i++)
    			{
    				V[i] = memoria[I++];
    			}          
    		}               
            #endregion
        }
    }
    
    Chip8.cs
    using System;
    using System.Windows.Forms;
    
    using SdlDotNet.Graphics;
    using SdlDotNet.Core;
    using SdlDotNet.Input; //para manejo de teclado
    
    namespace Chip8
    {
        public partial class Chip8 : Form
        {
            Emulador Emu;
            private int TargetFPS;
            private bool estaPausado;
            private string nombreROM = string.Empty;
    
            // variables para cada tecla
            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;
    
            // constructor
            public Chip8()
            {
                InitializeComponent();
                
                Emu = new Emulador();
                this.Text = "Emulador Chip-8";
                this.KeyDown += new KeyEventHandler(this.Tecla_Presionada);
                this.KeyUp += new KeyEventHandler(this.Tecla_Liberada);
                SdlDotNet.Core.Events.Tick += new EventHandler(Events_Tick);
                TargetFPS = 60;
                SdlDotNet.Core.Events.Fps = 60;
                estaPausado = false;
            }
    
            // Evento clic en opción del menú "Cargar Juego"
            private void cargarJuegoToolStripMenuItem1_Click(object sender, EventArgs e)
            {
                openFileDialog1.FileName = string.Empty;
    
                if (openFileDialog1.ShowDialog() == DialogResult.OK)
                {
                    nombreROM = openFileDialog1.FileName;
                    Reset();
                    SdlDotNet.Core.Events.Run();
                }
            }
    
            // Resetamos el hardware (registros, memoria) y cargamos de nuevo el juego
            void Reset()
            {
                Emu.ResetHardware();
                Emu.CargarJuego(nombreROM);            
            }
    
            // Game-Loop
            void Events_Tick(object sender, TickEventArgs e)
            {
                this.Text = "Emulador Chip-8  FPS: " + SdlDotNet.Core.Events.Fps;
                if (estaPausado == false)
                {
                    Emu.EmulaFrame();
                    UpdateDisplay();
                }
            }
    
            // Actualiza control gráfico con lo que entrega el Surface de la clase Emulador.cs
            private void UpdateDisplay()
            {
                scDisplay.Blit(Emu.SurfaceOut);
                scDisplay.Update();
            }
    
            // Evento gatillado al cerrar la aplicación con la "x" de la ventana
            private void Chip8_FormClosed(object sender, FormClosedEventArgs e)
            {
                CerrarAplicacion();
            }
    
            // Evento gatillado al presionar Pausa
            private void pausaToolStripMenuItem_Click(object sender, EventArgs e)
            {
                if (estaPausado == false)
                    estaPausado = true;
                else
                    estaPausado = false;
            }
    
            // Evento gatillado al presionar Reset
            private void resetToolStripMenuItem_Click(object sender, EventArgs e)
            {
                Reset();
            }
    
            // Evento gatillado al presionar Salir
            private void salirToolStripMenuItem_Click(object sender, EventArgs e)
            {
                CerrarAplicacion();
            }
    
            // Método que termina los eventos y cierra la aplicación
            void CerrarAplicacion()
            {
                SdlDotNet.Core.Events.Close();
                SdlDotNet.Core.Events.QuitApplication();
                Application.Exit();
            }
    
            private void Tecla_Presionada(object sender, KeyEventArgs e)
            {
                if (e.KeyCode == Keys.NumPad1) { Emu.teclasPresionadas[TECLA_1] = true; }
                if (e.KeyCode == Keys.NumPad2) { Emu.teclasPresionadas[TECLA_2] = true; }
                if (e.KeyCode == Keys.NumPad3) { Emu.teclasPresionadas[TECLA_3] = true; }
                if (e.KeyCode == Keys.NumPad4) { Emu.teclasPresionadas[TECLA_4] = true; }
                if (e.KeyCode == Keys.Q) { Emu.teclasPresionadas[TECLA_Q] = true; }
                if (e.KeyCode == Keys.W) { Emu.teclasPresionadas[TECLA_W] = true; }
                if (e.KeyCode == Keys.E) { Emu.teclasPresionadas[TECLA_E] = true; }
                if (e.KeyCode == Keys.R) { Emu.teclasPresionadas[TECLA_R] = true; }
                if (e.KeyCode == Keys.A) { Emu.teclasPresionadas[TECLA_A] = true; }
                if (e.KeyCode == Keys.S) { Emu.teclasPresionadas[TECLA_S] = true; }
                if (e.KeyCode == Keys.D) { Emu.teclasPresionadas[TECLA_D] = true; }
                if (e.KeyCode == Keys.F) { Emu.teclasPresionadas[TECLA_F] = true; }
                if (e.KeyCode == Keys.Z) { Emu.teclasPresionadas[TECLA_Z] = true; }
                if (e.KeyCode == Keys.X) { Emu.teclasPresionadas[TECLA_X] = true; }
                if (e.KeyCode == Keys.C) { Emu.teclasPresionadas[TECLA_C] = true; }
                if (e.KeyCode == Keys.V) { Emu.teclasPresionadas[TECLA_V] = true; }
            }
    
            private void Tecla_Liberada(object sender, KeyEventArgs e)
            {
                if (e.KeyCode == Keys.NumPad1) { Emu.teclasPresionadas[TECLA_1] = false; }
                if (e.KeyCode == Keys.NumPad2) { Emu.teclasPresionadas[TECLA_2] = false; }
                if (e.KeyCode == Keys.NumPad3) { Emu.teclasPresionadas[TECLA_3] = false; }
                if (e.KeyCode == Keys.NumPad4) { Emu.teclasPresionadas[TECLA_4] = false; }
                if (e.KeyCode == Keys.Q) { Emu.teclasPresionadas[TECLA_Q] = false; }
                if (e.KeyCode == Keys.W) { Emu.teclasPresionadas[TECLA_W] = false; }
                if (e.KeyCode == Keys.E) { Emu.teclasPresionadas[TECLA_E] = false; }
                if (e.KeyCode == Keys.R) { Emu.teclasPresionadas[TECLA_R] = false; }
                if (e.KeyCode == Keys.A) { Emu.teclasPresionadas[TECLA_A] = false; }
                if (e.KeyCode == Keys.S) { Emu.teclasPresionadas[TECLA_S] = false; }
                if (e.KeyCode == Keys.D) { Emu.teclasPresionadas[TECLA_D] = false; }
                if (e.KeyCode == Keys.F) { Emu.teclasPresionadas[TECLA_F] = false; }
                if (e.KeyCode == Keys.Z) { Emu.teclasPresionadas[TECLA_Z] = false; }
                if (e.KeyCode == Keys.X) { Emu.teclasPresionadas[TECLA_X] = false; }
                if (e.KeyCode == Keys.C) { Emu.teclasPresionadas[TECLA_C] = false; }
                if (e.KeyCode == Keys.V) { Emu.teclasPresionadas[TECLA_V] = false; }
            }
        }
    }
    

    Chip8.Designer.cs
    namespace Chip8
    {
        partial class Chip8
        {
            /// 
            /// Required designer variable.
            /// 
            private System.ComponentModel.IContainer components = null;
    
            // Control gráfico dentro de la ventana que permite mostrar el juego
            private SdlDotNet.Windows.SurfaceControl scDisplay;
    
            /// 
            /// Clean up any resources being used.
            /// 
            /// true if managed resources should be disposed; otherwise, false.
            protected override void Dispose(bool disposing)
            {
                if (disposing && (components != null))
                {
                    components.Dispose();
                }
                base.Dispose(disposing);
            }
    
            #region Windows Form Designer generated code
    
            /// 
            /// Required method for Designer support - do not modify
            /// the contents of this method with the code editor.
            /// 
            private void InitializeComponent()
            {
                System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(Chip8));
                this.scDisplay = new SdlDotNet.Windows.SurfaceControl();
                this.menuStrip1 = new System.Windows.Forms.MenuStrip();
                this.cargarJuegoToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
                this.cargarJuegoToolStripMenuItem1 = new System.Windows.Forms.ToolStripMenuItem();
                this.resetToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
                this.salirToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
                this.pausaToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
                this.openFileDialog1 = new System.Windows.Forms.OpenFileDialog();
                ((System.ComponentModel.ISupportInitialize)(this.scDisplay)).BeginInit();
                this.menuStrip1.SuspendLayout();
                this.SuspendLayout();
                // 
                // scDisplay
                // 
                this.scDisplay.AccessibleDescription = "SdlDotNet SurfaceControl";
                this.scDisplay.AccessibleName = "SurfaceControl";
                this.scDisplay.AccessibleRole = System.Windows.Forms.AccessibleRole.Graphic;
                this.scDisplay.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
                            | System.Windows.Forms.AnchorStyles.Left)
                            | System.Windows.Forms.AnchorStyles.Right)));
                this.scDisplay.BackColor = System.Drawing.Color.Black;
                this.scDisplay.Image = ((System.Drawing.Image)(resources.GetObject("scDisplay.Image")));
                this.scDisplay.InitialImage = null;
                this.scDisplay.Location = new System.Drawing.Point(0, 27);
                this.scDisplay.Name = "scDisplay";
                this.scDisplay.Size = new System.Drawing.Size(640, 320);
                this.scDisplay.TabIndex = 0;
                this.scDisplay.TabStop = false;
                // 
                // menuStrip1
                // 
                this.menuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
                this.cargarJuegoToolStripMenuItem,
                this.pausaToolStripMenuItem});
                this.menuStrip1.Location = new System.Drawing.Point(0, 0);
                this.menuStrip1.Name = "menuStrip1";
                this.menuStrip1.Size = new System.Drawing.Size(634, 24);
                this.menuStrip1.TabIndex = 1;
                this.menuStrip1.Text = "menuStrip1";
                // 
                // cargarJuegoToolStripMenuItem
                // 
                this.cargarJuegoToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
                this.cargarJuegoToolStripMenuItem1,
                this.resetToolStripMenuItem,
                this.salirToolStripMenuItem});
                this.cargarJuegoToolStripMenuItem.Name = "cargarJuegoToolStripMenuItem";
                this.cargarJuegoToolStripMenuItem.Size = new System.Drawing.Size(60, 20);
                this.cargarJuegoToolStripMenuItem.Text = "Archivo";
                // 
                // cargarJuegoToolStripMenuItem1
                // 
                this.cargarJuegoToolStripMenuItem1.Name = "cargarJuegoToolStripMenuItem1";
                this.cargarJuegoToolStripMenuItem1.Size = new System.Drawing.Size(152, 22);
                this.cargarJuegoToolStripMenuItem1.Text = "Cargar Juego";
                this.cargarJuegoToolStripMenuItem1.Click += new System.EventHandler(this.cargarJuegoToolStripMenuItem1_Click);
                // 
                // resetToolStripMenuItem
                // 
                this.resetToolStripMenuItem.Name = "resetToolStripMenuItem";
                this.resetToolStripMenuItem.Size = new System.Drawing.Size(152, 22);
                this.resetToolStripMenuItem.Text = "Reset";
                this.resetToolStripMenuItem.Click += new System.EventHandler(this.resetToolStripMenuItem_Click);
                // 
                // salirToolStripMenuItem
                // 
                this.salirToolStripMenuItem.Name = "salirToolStripMenuItem";
                this.salirToolStripMenuItem.Size = new System.Drawing.Size(152, 22);
                this.salirToolStripMenuItem.Text = "Salir";
                this.salirToolStripMenuItem.Click += new System.EventHandler(this.salirToolStripMenuItem_Click);
                // 
                // pausaToolStripMenuItem
                // 
                this.pausaToolStripMenuItem.Name = "pausaToolStripMenuItem";
                this.pausaToolStripMenuItem.Size = new System.Drawing.Size(50, 20);
                this.pausaToolStripMenuItem.Text = "Pausa";
                this.pausaToolStripMenuItem.Click += new System.EventHandler(this.pausaToolStripMenuItem_Click);
                // 
                // openFileDialog1
                // 
                this.openFileDialog1.FileName = "openFileDialog1";
                // 
                // Chip8
                // 
                this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
                this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
                this.ClientSize = new System.Drawing.Size(634, 344);
                this.Controls.Add(this.menuStrip1);
                this.Controls.Add(this.scDisplay);
                this.MainMenuStrip = this.menuStrip1;
                this.Name = "Chip8";
                this.Text = "Chip8";
                this.FormClosed += new System.Windows.Forms.FormClosedEventHandler(this.Chip8_FormClosed);
                ((System.ComponentModel.ISupportInitialize)(this.scDisplay)).EndInit();
                this.menuStrip1.ResumeLayout(false);
                this.menuStrip1.PerformLayout();
                this.ResumeLayout(false);
                this.PerformLayout();
    
            }
    
            #endregion
    
            private System.Windows.Forms.MenuStrip menuStrip1;
            private System.Windows.Forms.ToolStripMenuItem cargarJuegoToolStripMenuItem;
            private System.Windows.Forms.ToolStripMenuItem pausaToolStripMenuItem;
            private System.Windows.Forms.ToolStripMenuItem cargarJuegoToolStripMenuItem1;
            private System.Windows.Forms.ToolStripMenuItem resetToolStripMenuItem;
            private System.Windows.Forms.ToolStripMenuItem salirToolStripMenuItem;
            private System.Windows.Forms.OpenFileDialog openFileDialog1;
        }
    }
    
    Con esto tenemos un emulador con unas simples opciones que nos permiten cargar ROMs (por ahora solo de Chip-8, faltan algunos ajustes para que soporte ROMs de Super Chip-8), puede Resetear, Pausar, tiene sonido y funciona el teclado. Todo esto gracias a que nos basamos en el código fuente de los capítulos anteriores.

    Aquí tenemos imágenes del juego PONG2 Y ANIMAL RACE corriendo en nuestro emulador. Como se ven, aún quedan detalles por arreglar en la emulación, ya que la imagen se ve un poco "desplazada" a la derecha y parpadea mucho, haciendo que no se vean las paletas en el juego PONG2:



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

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

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