Volver a Página Principal

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

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 | Siguiente Lección

La idea

La idea de este capítulo es implementar estas pequeñas características:

  • Agregar el título de la ventana con el respectivo cálculo de Frames por Segundo o FPS.
  • Agregar sonido el emulador.
  • Corregir el funcionamiento de los Opcodes FX55 y FX65 acerca del incremento del registro I, ya que la mayoría de los juegos no lo requiere.
  • Mostrar los Frames por Segundo

    Como la mayoría de los emuladores, vamos a mostrar la velocidad del emulador en el título de la ventana y cuando en los emuladores se habla de velocidad, se refieren a los Frames por Segundo o más conocido como FPS. En el cine, las películas en general se proyectan a 24 frames, en los video juegos en cambio, para lograr una completa fluidez en lo que se muestra se intenta siempre mostrar a 60 frames a pesar que con 40 frames ya se ve fluido lo que se ve en pantalla. En nuestro caso, usaremos 60 FPS.

    SDL.Net nos provee 2 propiedades:

    La propiedad Events.TargetFps nos permite setear o obtener los tick por segundos, que setearemos a 60 en el constructor.
    También tenemos la propiedad SdlDotNet.Core.Events.Fps que nos permite setear un framerate o los Frames por Segundo. Esta propiedad en tiempo de ejecución, sobreescribe la anterior por lo que es la que usaremos en definitiva. Por ejemplo si seteamos este valor a 25, entonces el Evento Tick se gatillará 40 veces por Segundo (1000ms / 25fps = 40 veces). Como dijimos, en nuestro caso utilizaremos 60 FPS por lo que esta variable se seteará a 60 en el constructor ya que queremos que el emulador corra a 60 FPS.
    Al setear este valor en 60, no quiere decir que siempre andará a este valor, ya que si el PC es muy lento, el valor de SdlDotNet.Core.Events.Fps debería bajar ya que el framework SDL lo controla. O al contrario, si tenemos un PC muy rápido, puede que ande a más de 60 FPS. Esto nos da la opción de implementar a futuro un "Forzar 60 FPS" que tienen muchos emuladores para que esta velocidad no baje en lo posible ni suba.
    using SdlDotNet.Core; 
    ...
    Events.Fps = 60;
    Events.TargetFps = 60;
    
    Estas mismas propiedades nos permiten saber la información de cuanto es el Framerate (Frames por Segundo) y cuantos Tick hay configurados en un instante dado;
    string valorFPS = SdlDotNet.Core.Events.Fps;
    string valorTick = Events.TargetFps;
    
    Nota: para mostrar en pantalla los FPS, solo utilizaremos lo que nos retorne Events.TargetFps.

    Por último, para escribir en la ventana usamos Video.WindowCaption que debe ser actualizado en cada Tick.
    private void Events_Tick(object sender, TickEventArgs e)
    {
    	...
    	Video.WindowCaption = "Emulador Chip 8 - FPS: " + Events.Fps;
    	...   
    }
    

    Agregar Sonido

    Nos faltaba este importante punto ya que le da todo el "caché" al emulador. En el Chip-8 el sonido que se escucha es de un solo tono. La frecuencia queda a decisión de quien desarrolle el interprete, esto según la página de Cowgod's Chip-8.


    Un poco de contexto: Timers de Sonido y de Delay

    Chip 8 tiene 2 timers, uno para el Sonido conocido como Sound Timer y otro para realizar retardos conocido como Delay Timer.
    El delay timer se activa a 1 siempre que el Registro Delay Timer (DT) no sea 0. Este timer no hace nada más que restar 1 del valor de DT a una taza de 60Hz. Cuando DT llega a 0, este se desactiva. Este timer se implementa pero no se usa en el emulador para algo real, como "alentar" la imagen.

    El sound timer se activa siempre que que el Registro Sound Timer (ST) no sea cero. Este timer también se decrementa a una tasa de 60Hz, sin embargo, siempre que el valor de ST sea mayor a cero, el Chip-8 emitirá un sonido. Cuando ST llega a cero, el Sound Timer se desactiva. Este timer si se usa en la realidad, ya que si el valor es > 0 se ejecuta el sonido.

    Hay 2 formas de implementar el sonido:

  • La primera y más sencilla es utilizar el método Console.Beep( Frecuencia , Duracion ) que permite ejecutar el la bocina de la Bios. La Frecuencia puede ir 37 a 32.767 hercios (a menos Frecuencia más agudo y viceversa) y la duración es en Milisegundos.
  • La segunda es utilizar la librería de SDL .NET con el namespace SdlDotNet.Audio y con esto usando un archivo wav o mp3.

    Por comodidad, es mejor la 1ra. opción ya que estamos emulando un sistema antiguo y con el sonido de la Bios basta. En cambio si estaríamos emulando por ejemplo a la NES, solo nos quedaría la 2da. opción.
    Si se quieren ir por la 2da. opción recomiendo que lean este capítulo de mi tutorial de Desarrollo de Video Juegos ya que está explicado como usar sonidos con SDL.NET. Además si quieren, usen este sonido que es como el típico sonido de "beep".

    Como utilizaremos el método Consola.Beep, primero debemos utilizar el espacio de nombre System.Runtime.InteropServices que nos permite usar la API de Windows por medio del método DllImport que veremos más abajo:
    using System.Runtime.InteropServices;
    
    Luego debemos importar la API de Windows mediante [DllImport("Kernel32.dll")] y luego declaramos el método público, estático y externo Beep (int frecuencia, int duración):
    ...
    [DllImport("Kernel32.dll")]
    public static extern bool Beep(UInt32 frequency, UInt32 duration);
    
    //Constructor
    public Emulador()
    ...
    
    Con lo anterior, ahora en el código fuente podemos usar cuando queramos Console.Beep:
    Console.Beep(350, 50); //Frecuencia del "beep" 350, Duración 50 milisegundos
    
    Ahora debemos ubicar la instrucción que nos avisa que debemos ejecutar un sonido y esta es FX18 AssignToSound() pero por ahora no haremos nada aquí, así que tenerla en mente.
    Como se dijo en el contexto, la ejecución del "beep" en sí es cuando Sound Timer es > 0.
    Hay una dificultad particular de implementar el sonido, ya que el Sound Timer puede estar > 0 en varios Ticks, eso quiere decir que no podemos ejecutar el "beep" en cada tick, sinó solo la primera vez:
    public void EmulaFrame() //representa un frame
    {
        if (soundTimer > 0)
        {
            Console.Beep(350, 50);
        }
    
        //por cada Tick o Frame, se ejecutan 600/60=10 instrucciones
        for (int i = 0; i < operPorFrame; i++)
        {
            EmulaOpcodes();
        }
    }
    
    En el caso anterior, si lo dejamos tal cual, estaría incorrecto ya que si lo ejecutan escucharán unos 34 "beep" por cada acción (un rebote, un golpe, lo que el juego determine) y no 1 "beep". Por ejemplo, si están corriendo PONG y la bola rebota en la pared, se debe escuchar 1 "beep" y en cambio si lo dejamos así se escucharán 34 "beep". Esto debemos corregirlo agregando primero una variable que nos permitirá manejar la ejecución del sonido:
    bool ejecutaSonido = true;
    
    Luego cambiamos el método FX18 AssignToSound():
    void AssignToSound()
    {
    	soundTimer = V[opcode2];
    	ejecutaSonido = true;
    }
    
    Finalmente cambiamos el método EmulaFrame() para que reproduzca el sonido solo la primera vez que SoundTimer sea > 0 y no en cada Tick:
    public void EmulaFrame() //representa un frame
    {
    	if (soundTimer > 0 && ejecutaSonido == true)
    	{
    		Console.Beep(350, 50);
    		ejecutaSonido = false;
    	}	
    	...
    }
    

    Corregir Incremento de I para Opcodes FX55 y FX65

    Como me indicó PacoChan (vean el hilo del foro) muchos juegos no usan el incremento del registro I por lo que sacaremos definitivamente las líneas //I += 1; que estaban al final de los métodos SaveRegisters y LoadRegisters, de esta forma quedarán así:
    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++];
    	}          
    }
    

    Limpiando código antiguo

    Debemos sacar este código ya que no lo usaremos:
    [DllImport("msvcrt.dll")] //Framwork 1.1: para cls (clear screen) en DOS
    public static extern int system(string cmd);
    


    El código fuente de la lección es el siguiente.
    Nota: recordar que si les da un error "type initializer for 'SdlDotNet.Graphics.Video' an exception..." al compilar con un sistema operativo de 64 bits Botón derecho sobre las propiedades del Proyecto -> Properties -> Plataform Target -> x86 ya que por defecto está en Any CPU.
    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;
    
            //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
    		};
    
            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);
            
            //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;              
    
                    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)
            {
                Video.WindowCaption = "Emulador Chip-8  FPS: " + Events.Fps;
                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 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
            {
                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();
                }
            }
    
            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];
                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
    
        }
    }
    

    Con esto tenemos un emulador básico con todas las características mínimas para ejecutar la ROM de PONG.

    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