Kabosu - Creando cosas

Logo de la página. Gato esférico con colores verdoso.

Emulando Game Boy

Publicado: 2025-08-17

Etiquetas: Game Boy, Godot, Juegos, Proyectos


Buenos días, tardes o noches:

En los últimos meses apenas he escrito por aquí. Desde que volví al trabajo me da bastante pereza sentarme en la misma mesa en la que hago mis horas y usar la misma pantalla para trastear con mi viejo portátil. Al no trabajar en lo que se suele llamar "proyectos personales" no tenía muchas cosas técnicas de las que hablar en este blog. En este tiempo he escrito bastantes artículos que al final no he publicado por diferentes razones: a veces por tratar temas demasiado personales, por contar cosas de mi trabajo, también he escrito alguno sobre economía o política pero al final no me parecieron suficientemente interesantes... Quizá escriba un texto resumiéndolos todos un poco y así me los quito de enmedio.

Al tema. No consigo sentarme en el ordenador el tiempo suficiente como para hacer algo medianamente complejo. En este tiempo sí he hecho pequeñas tareas: He organizado mi colección de fotos, ordenado mi música MP3 y tareas sencillas que puedo hacer en unos minutos sin concentrarme demasiado. Hasta que un día, sin pensarlo demasiado, abrí Godot y me puse a escribir un emulador de Game Boy.

Game Boy

Sin ser un experto, conozco un poco la arquitectura de la Game Boy. Durante más de una década casi todos los juegos que he programado han intentando respetar a rajatabla las limitaciones de esa consola. Durante un tiempo también me puse a aprender su lenguaje ensamblador aunque no llegué a terminar ningún juego con eso. Tengo una categoría en el blog en la que cuentos algunas cosas que hice con ROMs por si a alguien le interesa.

Mi objetivo era modesto: emular la Game Boy lo suficiente para cargar un juego y poder extraer sus gráficos. No necesito meterme en temas de sonido ni conseguir una emulación muy afinada. Con interpretar las instrucciones una a una me basta.

Empezando a emular

No tengo mucha idea de cómo funciona internamente un emulador pero yo empecé a programar sin informarme lo más mínimo como suele ser habitual en mí. Comencé leyendo el contenido de una ROM que tenía por ahí.

func load_from_file(path):
	var file = FileAccess.open(path, FileAccess.READ)
	var size = file.get_length()
	print("Loaded " + path + " - " + str(size) + " bytes")
	rom = file.get_buffer(size)
	
	mem = PackedByteArray(rom)
	mem.resize(0xFFFF + 1)

El espacio de memoria de la Game Boy va de la posición 0x0000 a 0xFFFF siendo la ROM del juego la parte del principio. Creé mi simulación de la memoria a partir de los bytes de la ROM. Esto solo funcionaría con los juegos más pequeños, de hasta 32KB. Para los más grandes no sirve porque habría que emular el cambio de banco pero voy poco a poco.

Una vez leída la ROM del juego estuve comprobando que podía parsear la cabecera. Ya expliqué en su día cómo lo hacía en Python así que no voy a repetirme aquí.

Luego me puseo lo básico del procesador: el puntero de la instrucción actual (PC), el puntero de pila (SP) y el primer registro (A).

var mem  # The whole memory
var rom  # ROM loaded

# Registers
var pc  # Program counter
var sp  # Stack pointer

var A;

func _init():
	var path = "res://puzzle_boy.gb"
	load_from_file(path)
	
	parse_header()
	
	pc = 0x100 # First instruction
	sp = 0xFFFE # Empty stack
	A = 0

Ahora, dentro de un bucle voy cogiendo la posición de memoria mem[pc], la instrucción actual, miro a ver qué hay que hacer con ella y lo hago. Eso me da un nuevo valor para PC, SP o A.

func process_instruction():
	var opcode = mem[pc]
	var inst = decode_opcode(opcode)
	print('PC: ' + hex(pc) + ' Opcode: ' + bin(opcode) + ' instruction: ' + inst)
	execute_instruction(inst)

decode_opcode lo implementé muy cutremente con una serie de ifs:

unc decode_opcode(opcode):
	
	if opcode == 0:
		return 'nop'
	
	if opcode == 195:
		return 'jp imm16'
	
	if opcode == 205:
		return 'call imm16'
	
	if opcode == 240:
		return 'ldh a, [imm8]'

    ...

La función execute_instruction tiene una estructura similar pero modifica el estado de la memoria y los registros.

func execute_instruction(inst):
	if inst == 'nop':
		pc += 1
		return

	elif inst == 'jp imm16':
		var lo = mem[pc + 1]
		var hi = mem[pc + 2]
		print('hi: ' + hex(hi) + ' lo: ' + hex(lo))
		var imm16 = (hi << 8) + lo
		print('jump to ' + hex(imm16))
		pc = imm16
		return

    ...

Se podría mejorar muchísimo pero para una versión inicial no está mal. Durante varias noches me he sentado unos minutos para implementar una o dos instrucciones nuevas. Ejecuto el proyecto, se pone a evaluar instrucciones de la ROM y cuando encuentra una que no he implementado me muestra un error con el código binario. Busco qué instrucción me falta y la añado a mi lista.

Cuando llevaba una docena de instrucciones me di cuenta de que las estaba ejecutando correctamente porque también tenía un emulador de los buenos abierto para comprobar que mis resultados eran iguales pero no entendía qué estaba ejecutando. Por ejemplo, la instrucción ld [r16mem], a copia en la memoria el valor del registro A. Eso lo he implementado pero lo que no he programado las particularidades de la memoria. Dependiendo de en qué posición de memoria estoy escribiendo debería hacer cosas como activar el sonido, mostrar gráficos, apagar la pantalla, etc.

Estos días estoy leyendo un poco por encima este tutorial de ensamblador para Game Boy para así entender un poco más el código. Luego me pongo e implemento alguna instrucción nueva. Luego vuelvo a leer un poco el tutorial. Así en bucle voy aprendiendo.

El proyecto

Este emulador no va a llegar a ningún sitio. No tengo mucha idea del tema y lo estoy haciendo por pura curiosidad. Pero sí que me he dado de una cosa: en mi situación actual me viene muy bien este tipo de proyectos. Algo a lo que puedo dedicar unos pocos minutos y ver una avance. El tiempo que tardo en encontrar una instrucción que no he implementado, investigar qué debería hacer y escribir el código es 5 o 10 minutos. Se adapta muy bien a mis ánimos actuales. Creo que esa es la razón por la que me tomé con tantas ganas el port de Pigeon Ascent. Podía tocar unas pocas líneas de código y ver que el juego se visualizaba mejor que el día de antes. No necesitaba horas para hacer algo. Tardé un mes pero conseguí portar el juego entero empleando unos minutos al día.

Cuando me aburra de este emulador, que pasará pronto seguramente, tengo que buscar algún proyecto que se pueda completar en pasos muy muy pequeños. Al menos ahora ya sé lo que me viene bien.


Artículo siguiente: File over app, ficheros antes que aplicaciones.
Artículo anterior: Mi opinión sobre la Switch 2 tras 2 meses