Volver a Página Principal

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

Lenguaje: C#
Para: VS 2008 con Sdl.Net 6.1
Por Dark-N: hernaldog@gmail.com
http://darknromhacking.com
Hilo del Foro: http://foro.romhackhispano.org/viewtopic.php?f=4&t=872

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

La idea

La idea de esta parte es terminar con el resto de las instrucciones que se procesan en el "ciclo fetch" ya que en la anterior parte solo vimos 2: CLS-Limpiar pantalla y RET-Retorno de una Subrutina.

Instrucciones Faltantes

3. 1NNN : Salta a una dirección NNN (Jump To Address NNN)

Primero en el switch donde se leen los Opcodes se debe incorporar un nuevo case (1XXX). En esta instrucción se salta directamente a otra dirección y no se regresa de ella, sino que se sigue la ejecución desde allí:
// Ejecutamos las instrucciones a través de los opcodes
switch (opcode1)
{
	// opcodes del tipo 0xxx
	case (0x0):
	{
		// ... capitulo anterior
	}
	// opcodes del tipo 1xxx
	case (0x1):
	{
		// opcode 1NNN: Jump To Address NNN.
		JumpToAddr();
		break;
	}
Ahora, la implementación es la siguiente:
PC = NNN;
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;
}

4. 2NNN : Llama a una dirección NNN (Call Subroutine At Address NNN)

Se debe crear un nuevo case 0x2, donde esta vez saltaremos a una dirección dada y debemos volver luego de ejecutar dicha subrutina, para esto, la única forma de guardar nuestra posición actual (es decir el valor del PC) es utilizando la Pila.
Recordemos lo que dijimos en el capítulo anterior: cada vez que se lea una dirección desde la Pila, primero se decrementa el SP en 1 y luego lee de la Pila.
Y para el caso que se quiera guardar un valor en la Pila, primero se guarda el valor en la Pila y luego se debe aumenta el SP en 1.
// Ejecutamos las instrucciones a travez de los opcodes
switch (opcode1)
{
	// opcodes del tipo 0xxx
	case (0x0):
	{
		// ... capitulo anterior
	}
	// opcodes del tipo 1xxx
	case (0x1):
	{
		// ... 
	}
	// opcodes del tipo 2xxx
	case (0x2):
	{
		// opcode 2NNN: Call Subroutine At Address NNN.
		CallSub();
		break;
	}
La implementación es la siguiente:
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;
}

5. 3XKK : Salta a la Siguiente Instrucción si VX = KK

Nota: VX = V[X].

En este caso lo que se hace es saltar a la siguiente instrucción, es decir, PC se aumenta en 2 o PC += 2, solo si nuestro registro V[X] = opcode KK. Recordar que en nuestro programa opcode2 = X (capítulo anterior).

switch (opcode1)
{
	case (0x0):
	...
	case (0x1):
	...
	case (0x2):
	...
	// opcodes del tipo 3xxx
	case (0x3):
	{
		// opcode 4XKK: Skip Next Instruction If VX == KK
		SkipIfEql();
		break;
	}
La implementación es la siguiente:
void SkipIfEql()
{
	// Recordar que Opcode2=X
	if (V[opcode2] == KK)
	{
		// Salta a la siguiente instruccion
		PC += 2;
	}
}

6. 4XKK : Salta a la Siguiente Instrucción si VX != KK

Nota: Recordar que VX = V[X]. Lo mismo para un VY = V[Y].

Es igual que el anterior, excepto por que ahora la condición es distinta.
Ahora que ya conocen la mecánica, vamos a ir más rápido:
switch (opcode1)
{	
	...
	// opcodes del tipo 4xxx
	case (0x4):
	{
		// opcode 4XKK: Skip Next Instruction If VX != KK
		SkipIfNotEql();
		break;
	}
}

...

void SkipIfNotEql()
{
	if (V[opcode2] != KK)
	{
		// Salta a la siguiente instruccion
		PC += 2;
	}
}

7. 5XY0 : Salta a la Siguiente Instrucción si VX = VY

Comparamos el valor del registro V para el índice X con el valor del índice Y. Recordar que X = opcode2 e Y = opcode3.
switch (opcode1)
{	
		...
	// opcodes del tipo 5xxx
	case (0x5):
	{
		// opcode 5XY0: Skip Next Instruction If VX == VY
		SkipIfRegEql();
		break;
	}
}

...

void SkipIfRegEql()
{
	if (V[opcode2] == V[opcode3])
	{
		// Salta a la siguiente instruccion
		PC += 2;
	}
}

8. 6XKK : Setea VX = KK

Se setea V[X] con el valor del registro KK. Recordar que X = opcode2.
switch (opcode1)
{	
		...
	// opcodes del tipo 6xxx
	case (0x6):
	{
		// opcode 6XKK: Assign Number KK To Register X.
		AssignNumToReg();
		break;
	}
}

...

void AssignNumToReg()
{
	V[opcode2] = KK;
}
9. 7XKK : Setea VX = VX + KK.

Simple. No amerita mucha explicación.
switch (opcode1)
{	
	...
	// opcodes del tipo 7xxx
	case (0x7):
	{
		// opcode 7XKK: Add Number KK To Register X.
		AddNumToReg();
		break;
	}
}

...

void AddNumToReg()
{
	V[opcode2] += KK;
}

10. 8XY0 : Setea VX = VY

Para los opcodes8XXX tenemos varios tipos. Este es el primero donde seteamos V[X] = V[Y].

switch (opcode1)
{	
	...
	// opcodes del tipo 7xxx
	case (0x7):
	...
	// opcodes del tipo 8xxx
	case (0x8):
	{
		//Tenemos varios tipos
		switch (opcode4)
		{
			// opcode 8XY0: Assign From Register To Register.
			case (0x0):
			{
				AssignRegToReg();
				break;
			}
			...
}

...

void AssignRegToReg()
{
	//VX = VY
	V[opcode2] = V[opcode3];
}

11. 8XY1 : Setea VX = VX OR VY

Para el 2do tipo de opcodes8XXX tenemos este segundo tipo donde se realiza un OR binario entre V[X] y V[Y] y el resultado se asigna a V[X]. Recordar que en C# la instrucción OR se representa como el caracter |.

switch (opcode1)
{	
	...
	// opcodes del tipo 7xxx
	case (0x7):
	...
	// opcodes del tipo 8xxx
	case (0x8):
	{
		//Tenemos varios tipos
		switch (opcode4)
		{
			case (0x0):
			...
			// opcode 8XY1: Bitwise OR Between Registers.
			case (0x1):
			{
				RegisterOR();
				break;
			}
			...
}

...

void RegisterOR()
{
	// OR binario es |, entonces hacemos VX = VX | VY o mas elegante VX |= VY
	V[opcode2] |= V[opcode3];
}

12. 8XY2 : Setea VX = VX AND VY

Para el 2do tipo de opcodes8XXX tenemos este 3er tipo donde se realiza un AND binario entre V[X] y V[Y] y el resultado se asigna a V[X]. Recordar que en C# la instrucción AND se representa como el caracter &.

switch (opcode1)
{	
	...
	// opcodes del tipo 7xxx
	case (0x7):
	...
	// opcodes del tipo 8xxx
	case (0x8):
	{
		//Tenemos varios tipos
		switch (opcode4)
		{
			case (0x0):
			...
			// opcode 8XY1: Bitwise OR Between Registers.
			case (0x1):
			...
			// opcode 8XY2: Bitwise AND Between Registers.
			case (0x2):
			{
				RegisterAND();
				break;
			}
			...
}

...

void RegisterAND()
{
	// OR binario es &, entonces hacemos VX = VX & VY o mas elegante VX &= VY
	V[opcode2] &= V[opcode3];
}

13. 8XY3 : Setea VX = VX XOR VY

Para el 3do tipo de opcodes8XXX tenemos la operación XOR entre V[X] y V[Y] y el resultado se asigna a V[X]. Recordar que en C# la instrucción XOR se representa como el caracter ^.

switch (opcode1)
{	
	...
	// opcodes del tipo 8xxx
	case (0x8):
	{
		//Tenemos varios tipos
		switch (opcode4)
		{
			case (0x0):
			...
			case (0x1):
			...
			case (0x2):
			...
			// opcode 8XY3: Bitwise XOR Between Registers.
			case (0x3):
			{
				RegisterXOR();
				break;
			}
			...
}

...

void RegisterXOR()
{
	// XOR es ^, entonces hacemos VX = VX ^ VY o mas elegante VX ^= VY
	V[opcode2] ^= V[opcode3];
}

14. 8XY4 : Setea VX = VX + VY y setea el Carry en VF

Para el 4do tipo de opcodes8XXX tenemos la operación se suma entre V[X] y V[Y] y el resultado se asigna a V[X].
Por otro lado, si el resultado es mayor a 8 bits (por ejemplo un numero mayor a 255) V[F] se setea a 1, si no, se deja en 0. Solo los 8 bits menores se almacenan dentro de VX. Este valor en VF seteado a 1 es el carry o acarreo. Cuando se suman números binarios se produce un acarreo (carry) cuando la suma excede de uno mientras en la suma de números decimales se produce un acarreo cuando la suma excede de nueve.

La Regla se suma de números binarios es:
  • 0 + 0 = 0
  • 0 + 1 = 1
  • 1 + 0 = 1
  • 1 + 1 = 0, aquí hay acarreo 1 = 10

    Ejemplo:
      111	-> acarreo
       1010
     + 1111
      -----
      11001
    
    Un buen truco en C# para rescatar el acarreo de una operación, es saber primero después de cuantos bits se produce el acarreo, en este caso después de los 8 bits, por lo tanto se tiene:
    ValorAcarreo = operacion a obtener acarreo >> 8

    En nuestro caso:

    ValorAcarreo = VX + VY >> 8
    Y queremos que VF tenga el ValorAcarreo, por lo tanto tenemos:

    V[F] = (V[X] + V[Y]) >> 8

    switch (opcode1)
    {	
    	...
    	// opcodes del tipo 8xxx
    	case (0x8):
    	{
    		//Tenemos varios tipos
    		switch (opcode4)
    		{
    			case (0x0):
    			...
    			case (0x1):
    			...
    			case (0x2):
    			...
    			case (0x3):
    			...
    			// opcode 8XY4: Add Register To Register.
    			case (0x4):
    			{
    				AddRegToReg();
    				break;
    			}
    			...
    }
    
    ...
    
    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];
    }
    

    15. 8XY5 : Setea VX = VX - VY y VF = 1 si VX > VY, sino VF = 0

    En esta operación, primero se tiene que si VX > VY, entonces VF se setea 1, de otra forma, se deja en cero.
    Luego se realiza la resta entre V[X] y V[Y] y el resultado se asigna a V[X].
    La resta (o sustracción) de números binarios es parecida a los números decimales. En una resta de binarios cuando el minuendo es menor que el sustraendo, se produce un préstamo o borrow de 2, mientras que en decimal se produce un préstamo de 10.

    Ejemplo:
    	0 0   -> prestamo
       11010  -> minuendo 
     - 00101  -> sustraendo
       -----
       10101  -> diferencia
    
    Veamos como queda el código:
    switch (opcode1)
    {	
    	...
    	// opcodes del tipo 8xxx
    	case (0x8):
    	{
    		//Tenemos varios tipos
    		switch (opcode4)
    		{
    			...
    			// opcode 8XY5: Sub Register From Register.
    			case (0x5):
    			{
    				SubRegFromReg();
    				break;
    			}
    			...
    }
    
    ...
    
    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];
    }
    

    16. 8XY6 : Setea VF = 1 o 0 según bit menos significativo de VX. Divide VX por 2

    Aquí se realizan 2 operaciones:
  • Si el bit menos significativo del registro VX (en nuestro caso VX = VOpcode2) es 1, entonces VF se setea a 1, de otra forma es 0. Se debe recordar que el bit menos significativo es aquel que está más a la derecha. A nivel de código, una manera más óptima de hacer esto sin usar IF-ELSE, es utilizar operaciones Binarias, esto hace que el emulador sea más eficiente. VF = VX AND 1. Con esto VF valdrá 1 o 0 ya que que solo 1 AND 1 = 1, el resto siempre es cero.
  • Luego VX se divide por 2 (Shift a derecha). Para esto usamos la operación VOpcode 2 >> 1.

    La implementación es la siguiente:
    switch (opcode1)
    {	
    	...
    	// opcodes del tipo 8xxx
    	case (0x8):
    	{
    		//Tenemos varios tipos
    		switch (opcode4)
    		{
    			...
    			// opcode 8XY6: Shift Register Right Once.
    			case (0x6):
    			{
    				ShiftRegRight();
    				break;
    			}
    			...
    }
    
    ...
    
    void ShiftRegRight()
    {
    	//VF = VX AND 1 (VF valdrá 1 o 0) Para este case es más optimo a utilizar
    	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;
    }
    

    17. 8XY7 : Si VY > VX => VF = 1, sino 0. VX = VY - VX

    Operación sencilla, si VY > VX, entonces VF se setea a 1, sino se setea a 0. Además se substrae VX desde VY y el resultado se almacena en VX.

    La implementación es la siguiente:
    switch (opcode1)
    {	
    	...
    	// opcodes del tipo 8xxx
    	case (0x8):
    	{
    		//Tenemos varios tipos
    		switch (opcode4)
    		{
    			...
    			// opcode 8XY7: Sub Register From Register (Reverse Order).
    			case (0x7):
    			{
    				ReverseSubRegs();
    				break;
    			}
    			...
    }
    
    ...
    
    void ReverseSubRegs()
    {
    	if (V[opcode2] <= V[opcode3])
    	{
    		V[0xF] = 0x1;
    	}
    	else
    	{
    		V[0xF] = 0x0;
    	}
    
    	V[opcode2] = V[opcode3] - V[opcode2];
    }
    

    18. 8XYE : Setea VF = 1 o 0 según bit más significativo de VX. Multiplica VX por 2

    Aquí se realizan 2 operaciones:
  • Si el bit más significativo del registro VX (en nuestro caso VX = VOpcode2) es 1, entonces VF se setea a 1, de otra forma es 0. Se debe recordar que el bit menos significativo es aquel que está más a la izquierda. A nivel de código, una manera más óptima de hacer esto sin usar IF-ELSE, es utilizar operaciones Binarias, esto hace que el emulador sea más eficiente. VF = VX AND 10. Con esto VF valdrá 1 o 0 ya que que solo 1 AND 1 = 1, el resto siempre es cero.
  • Luego VX se multiplica por 2 (Shift a izquierda). Para esto usamos la operación VOpcode 2 << 1.

    La implementación es la siguiente:
    switch (opcode1)
    {	
    	...
    	// opcodes del tipo 8xxx
    	case (0x8):
    	{
    		//Tenemos varios tipos
    		switch (opcode4)
    		{
    			...
    			// opcode 8XYE: Shift Register Left Once.
    			case (0xE):
    			{
    				ShiftRegLeft();
    				break;
    			}
    			...
    }
    
    ...
    
    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;
    }
    

    19. 9XY0: Salta a la siguiente instrucción si VX != VY.

    Es muy sencillo, solo debemos saber que para decirle al emulador que ejecute la siguiente instrucción, se aumenta el PC en 2.

    La implementación es la siguiente:
    
    switch (opcode1)
    {	
    	...
    	// opcodes del tipo 9xxx
    	case (0x9):
    	{
    	// opcode 9XY0: Skip Next Instruction If VX != VY
    	SkipIfRegNotEql();
    	break;
    	}
    	...
    }
    ...
    
    
    void SkipIfRegNotEql()
    {			
    	if (V[opcode2] != V[opcode3])
    	{
    		//Aumentamos el PC en 2 para saltar a la siguiente instrucción
    		PC += 2;
    	}
    }
    

    20. ANNN: Setea I = NNNN

    Se setea el Registro de Índice (I) a la dirección NNN, recordar .

    La implementación es la siguiente:
    switch (opcode1)
    {	
    	...
    	// opcodes del tipo AXXX
    	case (0xA):
    	{
    		// opcode ANNN: Set Index Register To Address NNN.
    		AssignIndexAddr();
    		break;
    	}
    	...
    }
    ...
    
    void AssignIndexAddr()
    {
    	// se setea el Registro de Índice (I) a la dirección NNN.
    	I = NNN;
    }
    

    21. BNNN: Salta a la ubicación V[0]+ NNNN

    EL PC se setea al valor NNN más el valor de V[0x0]. Recordar que V tiene un índice que parte en 0x0 y el último es 0xF.

    La implementación es la siguiente:
    switch (opcode1)
    {	
    	...
    	// opcodes del tipo BXXX
    	case (0xB):
    	{
    		// opcode BNNN: Jump To NNN + V0.
    		JumpWithOffset();
    		break;
    	}
    	...
    }
    ...
    
    void JumpWithOffset()
    {
    	PC = NNN + V[0x0];
    }
    

    22. CXKK: Setea VX = un Byte Aleatorio AND KK

    Se genera un número random entre 0(0x0) y 255 (0xFF), el cual se le hace un AND con el valor de KK. El resultado se asigna a VX.

    La implementación es la siguiente:
    switch (opcode1)
    {	
    	...
    	// opcodes del tipo CXXX
    	case (0xC):
    	{
    		// opcode CXKK: Assign Bitwise AND Of Random Number & KK To Register X.
    		RandomANDnum();
    		break;
    	}	
    	...
    }
    ...
    
    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;
    }
    

    23. DXYN: Pinta un sprite en la pantalla

    El interprete lee N bytes desde la memoria, comenzando desde el contenido del registro I. Y se muestra dicho byte en las posiciones VX, VY de la pantalla.
    A los sprites que se pintan se le aplica XOR con lo que está en pantalla. Si esto causa que algún pixel se borre, el registro VF se setea a 1, de otra forma se setea a 0.
    Si el sprite se posiciona afuera de las coordenadas de la pantalla, dicho sprite se le hace aparecer en el lado opuesto de la pantalla.
    El Sprite en Chip-8 es de 8 x N.

    El algoritmo es el siguiente:
    for (lineaY = 0; lineaY < N; lineaY ++){
    
    	data = memoria[I + lineaY]; // esto retorna los bytes para un numero de pixels
    
    	for(pixelX = 0; pixelX < 8; pixelX ++)
    	{
    		if ((data & (0x80 >> pixelX)) != 0)
    		{
    			if (pantalla[ (V[X] + pixelX) % 64, (V[Y] + lineaY) % 32) ] == 1) 
    				V[F]=1  // hay colisión
    			
    			pantalla[ (V[X] + pixelX) % 64, (V[Y] + lineaY) % 32) ] ^= 1;
    		}
    	}
    }
    
    Fijarse que lo que uno hace es setear un arreglo con un 1 si queremos ver o un 0 si no, luego en otro método llamado "ActualizarPantalla();" recorremos el arreglo y pintamos en la pantalla si el valor X,Y de dicho arreglo es 1.

    La implementación es la siguiente:
    switch (opcode1)
    {	
    	...
    	// opcodes del tipo DXXX
    	case (0xD):
    	{
    		// opcode DXYN: Draw Sprite To The Screen.
    		DrawSprite();
    		break;
    	}
    	...
    }
    ...
    
    // Metodo que pinta un sprite en pantalla
    void DrawSprite()
    {
    	// En Chip8 un Sprite es de 8xN
    	int largoSpriteX = 8;
    
    	int dataLeida, tempx, tempy = 0;
    
    	// Se resetea la detección de colisión
    	V[0xF] = 0x0;
    
    	for (int lineaY = 0; lineaY < opcode4; lineaY++)
    	{
    		// Leemos un byte de memoria
    		dataLeida = memoria[I + lineaY];
    
    		for (int pixelX = 0; pixelX < largoSpriteX; pixelX++)
    		{
    			if ((dataLeida & (0x80 >> pixelX)) != 0)
    			{
    				// Chequeamos por alguna colisión (pixel sobrescrito)
    				tempx = (V[opcode2] + pixelX) % 64;
    				tempy = (V[opcode3] + lineaY) % 32;
    
    				//% Resto o modulo de una division, ejemplo 32%64=32, 0%32=0, 1%32=1
    				if (arregloPantalla[tempx, tempy] == 1)  
    					V[0xF] = 1;
    
    				// Dibujamos el el arreglo un 1, luego mas abajo pintamos graficamente el arreglo
    				arregloPantalla[tempx, tempy] ^= 1;  //XOR 0^1=1, 1^0=1, 1^1 =0, 0^0=0
    			}
    		}
    	}
    
    	// Actualiza lo que se ve en pantalla con los valores del arreglo
    	ActualizarPantalla();
    }
    
    
    
    // Actualizar pantalla de acuerdo a los valores del arreglo.
    // En esta implementación se escribe en la consola del sistema
    void ActualizarPantalla()
    {			
    	// Limpiamos la pantalla, también se puede usar Console.Clear();
    	system("cls"); 
    
    	// Pintamos bordes superiores
    	Console.WriteLine("/"+ "".PadRight(RES_X-1, '-') + "\\");
    
    	// Se pinta la pantalla
    	for (int y = 0; y < RES_Y; y++)
    	{		
    		Console.Write("|");	 // Usamos un pipe (|) para los bordes de pantalla
    		for (int x = 0; x < RES_X; x++)
    		{					
    			if (arregloPantalla[x, y] != 0)
    			{
    				Console.Write("*"); // Pintamos un "pixel"
    			}
    			else
    			{
    				Console.Write(" "); // Limpiamos ese "pixel"
    			}
    		}
    		Console.WriteLine("|");
    	}
    
    	// Pintamos bordes inferiores
    	Console.WriteLine("\\"+ "".PadRight(RES_X-1, '-') + "/");
    	Console.WriteLine("");
    }
    

    24. EX9E: Salta a la sgte. instrucción si valor de VX coincide con tecla presionada (tecla presionada)

    Chequea el teclado y si la tecla presionada corresponde al valor de VX (recordar VX = V[opcode2]), el PC se incrementa en 2.
    NOTA: En modo consola no es muy simple implementar las funciones de Keypress (presionar tecla) o KeyRelease (soltar tecla) por lo que cuando veamos el capítulo siguiente el uso de SDL.NET, se hará esta función como corresponde.

    La implementación es la siguiente:
    // 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;
    		}
    ...
    
    void SkipIfKeyDown()
    {
    	if (teclasPresionadas[MapeoTeclas[V[opcode2]]] == true)
    		PC += 2;		
    }
    

    25. EXA1: Salta a la sgte. instrucción si valor de VX no coincide con tecla presionada (soltar tecla)

    Chequea el teclado y si la tecla presionada no corresponde al valor de VX (recordar VX = V[opcode2]), es decir que si se libera la tecla, el PC se incrementa en 2.
    NOTA: En modo consola no es muy simple implementar las funciones de Keypress (presionar tecla) o KeyRelease (soltar tecla) por lo que cuando veamos el capítulo siguiente el uso de SDL.NET, se hará esta función como corresponde.

    La implementación es la siguiente:
    // opcodes del tipo EXXX
    case (0xE):
    {
    	// Tenemos 2 tipos según EXKK
    	switch (KK)
    	{
    		...
    		// opcode EXA1: Skip Next Instruction If Key In VX Is NOT Pressed.
    		case (0xA1):
    		{
    			SkipIfKeyUp();
    			break;
    		}
    ...
    
    void SkipIfKeyUp()
    {
    	if (teclasPresionadas[MapeoTeclas[V[opcode2]]] == false)
    		PC += 2;
    }
    

    26. FX07: Setea Vx = valor del delay timer

    Setea VX (V[opcode2]) = valor de Delay timer.

    La implementación es la siguiente:
    // opcodes del tipo FXXX
    case (0xF):
    {
    	// tenemos varios tipos de Opcodes
    	switch (KK)
    	{
    		// opcode FX07: Assign Delay Timer To Register.
    		case (0x07):
    		{
    			AssignFromDelay();
    			break;
    		}
    ...
    
    void AssignFromDelay()
    {
    	V[opcode2] = delayTimer;
    }
    

    27. FX0A: Espera por una tecla presionada y la almacena en el registro

    La ejecución del programa se detiene hasta que se presiona una tecla, entonces el valor de la tecla se almacena en VX.

    La implementación es la siguiente:
    // opcodes del tipo FXXX
    case (0xF):
    {
    	// tenemos varios tipos de Opcodes
    	switch (KK)
    	{
    		...
    		// opcode FX0A: Wait For Keypress And Store In Register.
    		case (0x0A):
    		{
    			StoreKey();
    			break;
    		}
    ...
    
    void StoreKey()
    {
    	for (int i = 0; i < teclasPresionadas.Length; i++)
    	{
    		if (teclasPresionadas[i] == true)
    		{
    			V[opcode2] = i;
    		}
    	}
    }
    

    28. FX15: Setea Delay Timer = VX

    Setea el Delay Timer igual al valor de VX.

    La implementación es la siguiente:
    // opcodes del tipo FXXX
    case (0xF):
    {
    	// tenemos varios tipos de Opcodes
    	switch (KK)
    	{
    		...
    		// opcode FX15: Assign Register To Delay Timer.
    		case (0x15):
    		{
    			AssignToDelay();
    			break;
    		}
    ...
    
    void AssignToDelay()
    {
    	delayTimer = V[opcode2];
    }
    

    29. FX18: Setea Sound Timer = VX

    Setea el Sound Timer igual al valor de VX.

    La implementación es la siguiente:
    // opcodes del tipo FXXX
    case (0xF):
    {
    	// tenemos varios tipos de Opcodes
    	switch (KK)
    	{
    		...
    		// opcode FX18: Assign Register To Sound Timer.
    		case (0x18):
    		{
    			AssignToSound();
    			break;
    		}
    ...
    
    void AssignToSound()
    {
    	soundTimer = V[opcode2];
    }
    

    30. FX1E: Indice = Indice + VX

    Se setea I = I + VX.

    La implementación es la siguiente:
    // opcodes del tipo FXXX
    case (0xF):
    {
    	// tenemos varios tipos de Opcodes
    	switch (KK)
    	{
    		...
    		// opcode FX1E: Add Register To Index.
    		case (0x1E):
    		{
    			AddRegToIndex();
    			break;
    		}
    ...
    
    void AddRegToIndex()
    {
    	I += V[opcode2];
    }
    

    31. FX29: Setea I = VX * largo Sprite Chip-8

    Como se dijo en el capítulo 2, el largo de los sprites en Chip-8 es 5 bytes, por lo que en esta instrucción podemos hacer que I se setea a VX * largo sprite, o lo que es lo mismo I = V[opcode2] * 5.

    La implementación es la siguiente:
    // opcodes del tipo FXXX
    case (0xF):
    {
    	// tenemos varios tipos de Opcodes
    	switch (KK)
    	{
    		...
    		// opcode FX29: Index Points At CHIP8 Font Char In Register.
    		case (0x29):
    		{
    			IndexAtFontC8();
    			break;
    		}
    ...
    
    void IndexAtFontC8()
    {
    	I = (V[opcode2] * 0x5);
    }
    

    32. FX30: Setea I = VX * largo Sprite Sprite Super Chip-8

    Esta función al ser de Super Chip-8, la dejaremos escrita pero no implementada por ahora.

    La implementación es la siguiente:
    // opcodes del tipo FXXX
    case (0xF):
    {
    	// tenemos varios tipos de Opcodes
    	switch (KK)
    	{
    		...
    	// opcode FX30: Index Points At SCHIP8 Font Char In Register.
    	case (0x30):
    	{
    		IndexAtFontSC8(); //Función de Super Chip-8
    		break;
    	}
    ...
    
    void IndexAtFontSC8()
    {
    	// No implementado aún.
    }
    

    33. FX33: Almacena centenas, decenas y unidades en la memoria[I], memoria[I+1] y memoria[I+2]

    El intérprete toma los valores decimales de VX y coloca las centenas en la ubicación de memoria [I], las decenas en memoria[I+1] y las unidades en memoria[I+2].

    La implementación es la siguiente:
    // opcodes del tipo FXXX
    case (0xF):
    {
    	// tenemos varios tipos de Opcodes
    	switch (KK)
    	{
    		...
    	// opcode FX33: Store BCD Representation Of Register In Memory.
    	case (0x33):
    	{
    		StoreBCD();
    		break;
    	}
    ...
    
    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
    }
    

    34. FX55: se guarda en memoria[I] valor de V0 a VX

    Se copian los valores de los registros V0 a VX dentro de la memoria, a partir de la dirección memoria[I]. I se debe incrementar después de cada asignación.

    La implementación es la siguiente:
    // opcodes del tipo FXXX
    case (0xF):
    {
    	// tenemos varios tipos de Opcodes
    	switch (KK)
    	{
    		...
    	// opcode FX55: Save Registers To Memory.
    	case (0x55):
    	{
    		SaveRegisters();
    		break;
    	}
    ...
    
    void SaveRegisters()
    {
    	for (int i = 0; i <= opcode2; i++)
    	{
    		memoria[I++] = V[i];
    	}
    	I += 1;
    }
    

    35. FX65: se guarda desde V0 a VX según lo que esté en en memoria[I]

    Se leen valores de la memoria desde la ubicación I y se copian a los registros V0 a VX.

    La implementación es la siguiente:
    // opcodes del tipo FXXX
    case (0xF):
    {
    	// tenemos varios tipos de Opcodes
    	switch (KK)
    	{
    		...
    	// opcode FX65: Load Registers From Memory.
    	case (0x65):
    	{
    		LoadRegisters();
    		break;
    	}
    ...
    
    void LoadRegisters()
    {
    	for (int i = 0; i <= opcode2; i++)
    	{
    		V[i] = memoria[I++];
    	}
    	I += 1;
    }
    
    Bueno amigos, con esto finalizamos la explicación de todas las instrucciones del Chip-8. Pendiente para otro capítulo queda las instrucciones del Super Chip-8.



    Código fuente completo de todo lo que llevamos hasta ahora (C# 2.0):
    using System;
    using System.IO;
    using System.Runtime.InteropServices;  //para "clearscreen" y el "beep" de la Bios
    
    namespace Chip8_ConsoleMode
    {
    	class Emulador
    	{
    		#region variables emulador
    
    		//variables principales
    		const int DIR_INICIO		= 0x200;
    		const int TAMANO_MEM		= 0xFFF;
    		const int TAMANO_PILA	   = 16;
    		const int CANT_REGISTROS	= 16;
    
    		//arreglo que representa la memoria
    		int[] memoria = new int[TAMANO_MEM];
    		
    		//Arreglo que representa los 16 Registros (V) 
    		int[] V = new int[CANT_REGISTROS];
    		
    		//arreglo que representa la Pila
    		int[] pila = new int[TAMANO_PILA];
    		
    		//variables que representan registros varios
    		int instruccion;  //representa una instruccion del Chip-8. Tiene largo 2 byte
    		int PC;
    		int I;
    		int SP;	  
    		int KK;
    		
    		//resolucion de pantalla 64x32 (mas alta que larga)
    		const int RES_X	= 64;
    		const int RES_Y	= 32;
    		int[,] arregloPantalla = new int[RES_X, RES_Y];
    		
    		//variables para manejar los opcodes
    		int opcode1 = 0;
    		int opcode2 = 0;  //X
    		int opcode3 = 0;  //Y
    		int opcode4 = 0;
    		int NNN = 0;
    		
    		//variables que representan los 2 timers: Delay Timer y Sound Timer
    		int	delayTimer;
    		int	soundTimer;
    
    		//variables para el manejo de fuentes (80 bytes, ya que hay 5 bytes x caracter 
    		//y son 16 caracteres o letras (5x16=80). Cada font es de 4x5 bits. 
    		int[] arregloFuentes = {
    		   0xF0, 0x90, 0x90, 0x90, 0xF0,	// valores para 0
    		   0x60, 0xE0, 0x60, 0x60, 0xF0,	// valores para 1
    		   0x60, 0x90, 0x20, 0x40, 0xF0,	// valores para 2
    		   0xF0, 0x10, 0xF0, 0x10, 0xF0,	// valores para 3
    		   0x90, 0x90, 0xF0, 0x10, 0x10,	// valores para 4
    		   0xF0, 0x80, 0x60, 0x10, 0xE0,	// valores para 5
    		   0xF0, 0x80, 0xF0, 0x90, 0xF0,	// valores para 6
    		   0xF0, 0x10, 0x10, 0x10, 0x10,	// valores para 7
    		   0xF0, 0x90, 0xF0, 0x90, 0xF0,	// valores para 8
    		   0xF0, 0x90, 0xF0, 0x10, 0x10,	// valores para 9
    		   0x60, 0x90, 0xF0, 0x90, 0x90,	// valores para A
    		   0xE0, 0x90, 0xE0, 0x90, 0xE0,	// valores para B
    		   0x70, 0x80, 0x80, 0x80, 0x70,	// valores para C
    		   0xE0, 0x90, 0x90, 0x90, 0xE0, 	// valores para D
    		   0xF0, 0x80, 0xF0, 0x80, 0xF0,	// valores para E
    		   0xF0, 0x90, 0xF0, 0x80, 0x80		// valores para F
    		};
    
    		private bool[] teclasPresionadas = { false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false };
    		
    		const int TECLA_1	= 0;
    		const int TECLA_2	= 1;
    		const int TECLA_3	= 2;
    		const int TECLA_4	= 3;
    		const int TECLA_Q	= 4;
    		const int TECLA_W	= 5;
    		const int TECLA_E	= 6;
    		const int TECLA_R	= 7;
    		const int TECLA_A	= 8;
    		const int TECLA_S	= 9;
    		const int TECLA_D	= 10;
    		const int TECLA_F	= 11;		
    		const int TECLA_Z	= 12;
    		const int TECLA_X	= 13;
    		const int TECLA_C	= 14;
    		const int TECLA_V	= 15;
    		
    		// mapeamos las 16 teclas de Chip8
    		private byte[] MapeoTeclas = 
    		{		   
    			0x01,0x02,0x03,0x0C,
    			0x04,0x05,0x06,0x0D,
    			0x07,0x08,0x09,0x0E,
    			0x0A,0x00,0x0B,0x0F 
    		};
    
    		//[DllImport("Kernel32.dll")] //para beep
    		//public static extern bool Beep(UInt32 frequency, UInt32 duration);
    		//En Framework 2.0 se puede usar directamente: Console.Beep(350, 50);
    
    		[DllImport("msvcrt.dll")] //Framwork 1.1: para cls (clear screen) en DOS
    		public static extern int system(string cmd);
    		//En Framework 2.0 se puede usar directamente: Console.Clear();
    
    		//Variable de tipo Random (para generar números aleatoios) utilizada por ciertas instrucciones
    		Random rnd = new Random();
    
    		#endregion
    		   
    				
    		static void Main(string[] args)
    		{
    			Emulador emulador = new Emulador();	
    			emulador.Run();	
    		}
    
    		
    		void Run()
    		{			
    			ResetHardware();
    
    			if (CargarJuego() == true)
    			{
    				while (true) //game-loop
    				{					
    					EjecutaFech();
    					ManejaTimers();
    				}
    			}
    		}
    
    
    		void ResetHardware()
    		{
    			// Reseteo de Timers.
    			delayTimer = 0x0;
    			soundTimer = 0x0;
    
    			// Reseteo de Registros generales
    			instruccion = 0x0;
    			PC = DIR_INICIO;
    			SP = 0x0;
    			I = 0x0;
    
    			// Limpiado del Registro V
    			for (int regActual = 0; regActual < CANT_REGISTROS; regActual++)
    			{
    				memoria[regActual] = 0x0;
    			}
    
    			// Limpiado de memoria
    			for (int dir = 0; dir < TAMANO_MEM; dir++)
    			{
    				memoria[dir] = 0x0;
    			}
    
    			// Limpiado de la Pila
    			for (int item = 0; item < TAMANO_PILA; item++)
    			{
    				pila[item] = 0x0;
    			}
    
    			// Carga de Fuentes a Memoria (eran 80 bytes, 5 byte por cada una de las 16 letras)
    			for (int i = 0; i < 80; i++)
    			{
    				memoria[i] = arregloFuentes[i];
    			}
    		}
    
    		void ManejaTimers()
    		{ 
    			if (delayTimer > 0) 
    				delayTimer--;
    			if (soundTimer > 0)
    				soundTimer--;
    		}
    	   
    		bool CargarJuego()
    		{
    			string nombreRom = "PONG";
    			FileStream rom;
    
    			try
    			{
    				rom = new FileStream(@nombreRom, FileMode.Open);
    
    				if (rom.Length == 0)
    				{
    					Console.Write("Error: ROM dañada o vacía");
    					return false;
    				}
    
    				// 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();
    				return true;
    			}
    			catch (Exception ex)
    			{
    				Console.Write("Error general al cargar la ROM. " + ex.Message);
    				return false;
    			}
    		}
    			   
    
    		void EjecutaFech()
    		{
    			#region lectura de instrucciones 
    
    			// leemos cada una de las instrucciones desde la memoria. 
    			// cada instruccion es de 2 bytes
    			instruccion = memoria[PC] << 8 | memoria[PC + 1];
    
    			// 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()
    		{
    			// No se limpia la pantalla directamente, sino el arreglo que la representa
    			// asignandole un valor 0
    			for (int p =0; p < RES_X; p++)
    				for (int q = 0; q < RES_Y; q++)
    					arregloPantalla[p,q] = 0;					
    		}
    
    		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;
    		}
    
    		// Metodo que pinta un sprite en pantalla
    		void DrawSprite()
    		{
    			// En Chip8 un Sprite es de 8xN
    			int largoSpriteX = 8;
    
    			int dataLeida, tempx, tempy = 0;
    
    			// Se resetea la detección de colisión
    			V[0xF] = 0x0;
    
    			for (int lineaY = 0; lineaY < opcode4; lineaY++)
    			{
    				// Leemos un byte de memoria
    				dataLeida = memoria[I + lineaY];
    
    				for (int pixelX = 0; pixelX < largoSpriteX; pixelX++)
    				{
    					if ((dataLeida & (0x80 >> pixelX)) != 0)
    					{
    						// Chequeamos por alguna colisión (pixel sobrescrito)
    						tempx = (V[opcode2] + pixelX) % 64;
    						tempy = (V[opcode3] + lineaY) % 32;
    
    						//% Resto o modulo de una division, ejemplo 32%64=32, 0%32=0, 1%32=1
    						if (arregloPantalla[tempx, tempy] == 1)  
    							V[0xF] = 1;
    
    						// Dibujamos el el arreglo un 1, luego mas abajo pintamos graficamente el arreglo
    						arregloPantalla[tempx, tempy] ^= 1;  //XOR 0^1=1, 1^0=1, 1^1 =0, 0^0=0
    					} 
    				}
    			}			
    
    			// Actualiza lo que se ve en pantalla con los valores del arreglo
    			ActualizarPantalla();
    		}
    
    		void SkipIfKeyDown()
    		{
    			if (teclasPresionadas[MapeoTeclas[V[opcode2]]] == true)
    				PC += 2;		
    		}
    
    		void SkipIfKeyUp()
    		{
    			if (teclasPresionadas[MapeoTeclas[V[opcode2]]] == false)
    				PC += 2;
    		}
    
    		void AssignFromDelay()
    		{
    			V[opcode2] = delayTimer;
    		}
    
    		void StoreKey()
    		{
    			for (int i = 0; i < teclasPresionadas.Length; i++)
    			{
    				if (teclasPresionadas[i] == true)
    				{
    					V[opcode2] = i;
    				}
    			}
    		}
    
    		void AssignToDelay()
    		{
    			delayTimer = V[opcode2];
    		}
    
    		void AssignToSound()
    		{
    			soundTimer = V[opcode2];
    		}
    
    		void AddRegToIndex()
    		{
    			I += V[opcode2];
    		}
    
    		void IndexAtFontC8()
    		{
    			I = (V[opcode2] * 0x5);
    		}
    
    		void IndexAtFontSC8()
    		{
    			// Not Implemanted yer, SCHIP8 Function.
    		}
    
    		void StoreBCD()
    		{
    			int vx = (int) V[opcode2];
    			memoria[I] = vx / 100;			  //centenas
    			memoria[I + 1] = (vx / 10) % 10;	//decenas
    			memoria[I + 2] = vx % 10;		   //unidades
    		}
    
    		void SaveRegisters()
    		{
    			for (int i = 0; i <= opcode2; i++)
    			{
    				memoria[I++] = V[i];
    			}
    			I += 1;
    		}
    
    		void LoadRegisters()
    		{
    			for (int i = 0; i <= opcode2; i++)
    			{
    				V[i] = memoria[I++];
    			}
    			I += 1;
    		}
    
    
    		// Actualizar pantalla de acuerdo a los valores del arreglo.
    		// En esta implementación se escribe en la consola del sistema
    		void ActualizarPantalla()
    		{			
    			// Limpiamos la pantalla, también se puede usar Console.Clear();
    			system("cls"); 
    
    			// Pintamos bordes superiores
    			Console.WriteLine("/"+ "".PadRight(RES_X-1, '-') + "\\");
    
    			// Se pinta la pantalla
    			for (int y = 0; y < RES_Y; y++)
    			{		
    				Console.Write("|");	 // Usamos un pipe (|) para los bordes de pantalla
    				for (int x = 0; x < RES_X; x++)
    				{					
    					if (arregloPantalla[x, y] != 0)
    					{
    						Console.Write("*"); // Pintamos un "pixel"
    					}
    					else
    					{
    						Console.Write(" "); // Limpiamos ese "pixel"
    					}
    				}
    				Console.WriteLine("|");
    			}
    
    			// Pintamos bordes inferiores
    			Console.WriteLine("\\"+ "".PadRight(RES_X-1, '-') + "/");
    			Console.WriteLine("");
    		}
    		#endregion
    	}
    }
    
    Al compilar se debería ver algo así:



    En el siguiente capítulo veremos como transformar esta solución que utiliza la ventana de comandos para mostrar el emulador, a una mejorada que usa una ventana windows con mejores gráficos, manejo de sonido y del teclado.

    Bajar aquí el proyecto .Net para Visual Studio 2008 que contiene los fuentes del capítulo 2, 3 y 4.


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

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