当前位置: 首页 > news >正文

使用python开发任天堂gameboy模拟器|pyboy开发实践

前言

最近在研究游戏模拟器的工作原理,最后找到一个叫PyBoy的Python开源项目。PyBoy 是一个完全使用 Python 编写的任天堂 Game Boy 模拟器,其核心原理是在现代计算机上,通过软件来模拟 Game Boy 这台物理掌机的所有关键硬件组件,并让它们协同工作以运行原始的游戏ROM(只读存储器)文件。它的设计目标不仅仅是能够玩游戏,更重要的是为AI研究、自动化测试和学习计算机体系结构提供一个高度可访问的Python接口。

先放PyBoy的研究测试效果,如下图:

在这里插入图片描述

Pyboy是个开源的项目我们可以从国内的GitCode网站上找到这个项目,https://gitcode.com/gh_mirrors/py/PyBoy

在这里插入图片描述

查看完pyboy的源码后,我们大致理解PyBoy内部这些核心模块是如何协同工作的,下图展示了其核心的数据流与执行循环。

加载游戏ROM文件
模拟器初始化
CPU,Memory,GPU,Timer, Cartridge
进入主循环
模拟CPU执行一条指令
模拟取指, 解码, 执行
GPU处理图形管线
读取Tile,渲染扫描线
处理输入事件
Timer计数器更新
单帧指令执行完毕?
更新图形界面
SDL2/Headless

下面,我们详细拆解这张图里的各个核心模块和工作原理。

使用python开发任天堂gameboy模拟器|pyboy开发实践

    • 一、核心组件模拟
      • ​(一)中央处理器 (CPU)​​
      • (二)内存管理单元 (Memory Management Unit, MMU)​​
      • ​(二)图形处理单元 (GPU)​​:
      • (三)中断检测与定时器:
    • 二、在pycharm中运行pyboy
      • (一)下载pyboy包
      • (二)导入pyboy包并运行
      • (三)展示运行效果
    • 三、结语

一、核心组件模拟

PyBoy的成功运行,依赖于它精确地模拟了Game Boy的以下几个核心硬件单元。

​(一)中央处理器 (CPU)​​

PyBoy模拟的是Game Boy上使用的Sharp LR35902​处理器。这是一个基于Intel 8080Zilog Z80的8位处理器。模拟器的核心是一个巨大的循环,在这个循环中,PyBoy的CPU模拟模块会从内存中读取指令,然后解码​并执行​它,同时更新程序计数器和各种寄存器。这是模拟器最复杂、最基础的部分。这里主要涉及源码里面core中的cpu.py文件与opcodes.py文件,其中opcodes.py文件实现z80 CPU指令与本地系统CPU(这里是windows)的映射,相当长和复杂,这里就不一一列举代码,只放cpu.py代码。
在这里插入图片描述

核心代码如下:

#
# License: See LICENSE.md file
# GitHub: https://github.com/Baekalfen/PyBoy
#
import pyboyfrom . import opcodes
from pyboy.utils import INTR_VBLANK, INTR_LCDC, INTR_TIMER, INTR_SERIAL, INTR_HIGHTOLOWFLAGC, FLAGH, FLAGN, FLAGZ = range(4, 8)logger = pyboy.logging.get_logger(__name__)class CPU:def __init__(self, mb):self.A = 0self.F = 0self.B = 0self.C = 0self.D = 0self.E = 0self.HL = 0self.SP = 0self.PC = 0self.interrupts_flag_register = 0self.interrupts_enabled_register = 0self.interrupt_master_enable = Falseself.interrupt_queued = Falseself.mb = mbself.halted = Falseself.stopped = Falseself.is_stuck = Falseself.cycles = 0def save_state(self, f):for n in [self.A, self.F, self.B, self.C, self.D, self.E]:f.write(n & 0xFF)for n in [self.HL, self.SP, self.PC]:f.write_16bit(n)f.write(self.interrupt_master_enable)f.write(self.halted)f.write(self.stopped)f.write(self.interrupts_enabled_register)f.write(self.interrupt_queued)f.write(self.interrupts_flag_register)f.write_64bit(self.cycles)def load_state(self, f, state_version):self.A, self.F, self.B, self.C, self.D, self.E = [f.read() for _ in range(6)]self.HL = f.read_16bit()self.SP = f.read_16bit()self.PC = f.read_16bit()self.interrupt_master_enable = f.read()self.halted = f.read()self.stopped = f.read()if state_version >= 5:# Interrupt register moved from RAM to CPUself.interrupts_enabled_register = f.read()if state_version >= 8:self.interrupt_queued = f.read()self.interrupts_flag_register = f.read()if state_version >= 12:self.cycles = f.read_64bit()logger.debug("State loaded: %s", self.dump_state(""))def dump_state(self, sym_label):opcode_data = [self.mb.getitem(self.mb.cpu.PC + n) for n in range(3)]  # Max 3 length, then we don't need to backtrackopcode = opcode_data[0]opcode_length = opcodes.OPCODE_LENGTHS[opcode]opcode_str = f"Opcode: [{opcodes.CPU_COMMANDS[opcode]}]"if opcode == 0xCB:opcode_str += f" {opcodes.CPU_COMMANDS[opcode_data[1]+0x100]}"else:opcode_str += " " + " ".join(f"{d:02X}" for d in opcode_data[1:opcode_length])return ("\n"f"A: {self.mb.cpu.A:02X}, F: {self.mb.cpu.F:02X}, B: {self.mb.cpu.B:02X}, "f"C: {self.mb.cpu.C:02X}, D: {self.mb.cpu.D:02X}, E: {self.mb.cpu.E:02X}, "f"HL: {self.mb.cpu.HL:04X}, SP: {self.mb.cpu.SP:04X}, PC: {self.mb.cpu.PC:04X} ({sym_label})\n"f"{opcode_str} "f"Interrupts - IME: {self.mb.cpu.interrupt_master_enable}, "f"IE: {self.mb.cpu.interrupts_enabled_register:08b}, "f"IF: {self.mb.cpu.interrupts_flag_register:08b}\n"f"LCD Intr.: {self.mb.lcd._cycles_to_interrupt}, LY:{self.mb.lcd.LY}, LYC:{self.mb.lcd.LYC}\n"f"Timer Intr.: {self.mb.timer._cycles_to_interrupt}\n"f"Sound: PCM12:{self.mb.sound.pcm12():02X}, PCM34:{self.mb.sound.pcm34():02X}\n"f"Sound CH1: \n"f"sound_period: {self.mb.sound.sweepchannel.sound_period}\n"f"length_enable: {self.mb.sound.sweepchannel.length_enable}\n"f"enable: {self.mb.sound.sweepchannel.enable}\n"f"lengthtimer: {self.mb.sound.sweepchannel.lengthtimer}\n"f"envelopetimer: {self.mb.sound.sweepchannel.envelopetimer}\n"f"periodtimer: {self.mb.sound.sweepchannel.periodtimer}\n"f"period: {self.mb.sound.sweepchannel.period}\n"f"waveframe: {self.mb.sound.sweepchannel.waveframe}\n"f"volume: {self.mb.sound.sweepchannel.volume}\n"f"halted:{self.halted}, "f"interrupt_queued:{self.interrupt_queued}, "f"stopped:{self.stopped}\n"f"cycles:{self.cycles}\n")def set_interruptflag(self, flag):self.interrupts_flag_register |= flagdef tick(self, cycles_target):_cycles0 = self.cycles_target = _cycles0 + cycles_targetif self.check_interrupts():self.halted = False# TODO: Cycles it took to handle the interruptif self.halted and self.interrupt_queued:# GBCPUman.pdf page 20# WARNING: The instruction immediately following the HALT instruction is "skipped" when interrupts are# disabled (DI) on the GB,GBP, and SGB.self.halted = Falseself.PC += 1self.PC &= 0xFFFFelif self.halted:self.cycles += cycles_target  # TODO: Number of cycles for a HALT in effect?self.interrupt_queued = Falseself.bail = Falsewhile self.cycles < _target:# TODO: cpu-stuck check for blargg tests?self.fetch_and_execute()if self.bail:  # Possible cycles-target changesbreakdef check_interrupts(self):if self.interrupt_queued:# Interrupt already queued. This happens only when using a debugger.return Falseraised_and_enabled = (self.interrupts_flag_register & 0b11111) & (self.interrupts_enabled_register & 0b11111)if raised_and_enabled:# Clear interrupt flagif self.halted:self.PC += 1  # Escape HALT on returnself.PC &= 0xFFFFif self.interrupt_master_enable:if raised_and_enabled & INTR_VBLANK:self.handle_interrupt(INTR_VBLANK, 0x0040)elif raised_and_enabled & INTR_LCDC:self.handle_interrupt(INTR_LCDC, 0x0048)elif raised_and_enabled & INTR_TIMER:self.handle_interrupt(INTR_TIMER, 0x0050)elif raised_and_enabled & INTR_SERIAL:self.handle_interrupt(INTR_SERIAL, 0x0058)elif raised_and_enabled & INTR_HIGHTOLOW:self.handle_interrupt(INTR_HIGHTOLOW, 0x0060)self.interrupt_queued = Truereturn Trueelse:self.interrupt_queued = Falsereturn Falsedef handle_interrupt(self, flag, addr):self.interrupts_flag_register ^= flag  # Remove flagself.mb.setitem((self.SP - 1) & 0xFFFF, self.PC >> 8)  # Highself.mb.setitem((self.SP - 2) & 0xFFFF, self.PC & 0xFF)  # Lowself.SP -= 2self.SP &= 0xFFFFself.PC = addrself.interrupt_master_enable = Falsedef fetch_and_execute(self):opcode = self.mb.getitem(self.PC)if opcode == 0xCB:  # Extension codeopcode = self.mb.getitem(self.PC + 1)opcode += 0x100  # Internally shifting look-up tablereturn opcodes.execute_opcode(self, opcode)

(二)内存管理单元 (Memory Management Unit, MMU)​​

Game Boy 有一个16位地址总线,可寻址64KB的内存空间。这片空间被划分为不同的区域:引导ROM、卡带ROM、工作RAM、显存等。MMU的作用就像是整个系统的“交通枢纽”,负责管理CPU对其他所有组件的访问。当CPU需要读写内存时,MMU会根据地址将请求正确地路由到卡带、RAM或GPU等相应的硬件组件上。这里主要涉及源码里面core中的mb.py文件。
在这里插入图片描述
核心代码如下:

# MemoryManager#def getitem(self, i):if 0x0000 <= i < 0x4000:  # 16kB ROM bank #0if self.bootrom_enabled and (i <= 0xFF or (self.bootrom.cgb and 0x200 <= i < 0x900)):return self.bootrom.getitem(i)else:return self.cartridge.rombanks[self.cartridge.rombank_selected_low, i]elif 0x4000 <= i < 0x8000:  # 16kB switchable ROM bankreturn self.cartridge.rombanks[self.cartridge.rombank_selected, i - 0x4000]elif 0x8000 <= i < 0xA000:  # 8kB Video RAMif not self.cgb or self.lcd.vbk.active_bank == 0:return self.lcd.VRAM0[i - 0x8000]else:return self.lcd.VRAM1[i - 0x8000]elif 0xA000 <= i < 0xC000:  # 8kB switchable RAM bankreturn self.cartridge.getitem(i)elif 0xC000 <= i < 0xE000:  # 8kB Internal RAMbank_offset = 0if self.cgb and 0xD000 <= i:# Find which bank to read from at FF70bank = self.ram.non_io_internal_ram1[0xFF70 - 0xFF4C] & 0b111if bank == 0x0:bank = 0x01bank_offset = (bank - 1) * 0x1000return self.ram.internal_ram0[i - 0xC000 + bank_offset]elif 0xE000 <= i < 0xFE00:  # Echo of 8kB Internal RAM# Redirect to internal RAMreturn self.getitem(i - 0x2000)elif 0xFE00 <= i < 0xFEA0:  # Sprite Attribute Memory (OAM)return self.lcd.OAM[i - 0xFE00]elif 0xFEA0 <= i < 0xFF00:  # Empty but unusable for I/Oreturn self.ram.non_io_internal_ram0[i - 0xFEA0]elif 0xFF00 <= i < 0xFF4C:  # I/O portsif 0xFF01 <= i <= 0xFF02:if self.serial.tick(self.cpu.cycles):self.cpu.set_interruptflag(INTR_SERIAL)if i == 0xFF01:return self.serial.SBelif i == 0xFF02:return self.serial.SCelif 0xFF04 <= i <= 0xFF07:if self.timer.tick(self.cpu.cycles):self.cpu.set_interruptflag(INTR_TIMER)if i == 0xFF04:return self.timer.DIVelif i == 0xFF05:return self.timer.TIMAelif i == 0xFF06:return self.timer.TMAelif i == 0xFF07:return self.timer.TACelif i == 0xFF0F:return self.cpu.interrupts_flag_registerelif 0xFF10 <= i < 0xFF40:self.sound.tick(self.cpu.cycles)return self.sound.get(i - 0xFF10)elif 0xFF40 <= i <= 0xFF4B:if lcd_interrupt := self.lcd.tick(self.cpu.cycles):self.cpu.set_interruptflag(lcd_interrupt)if i == 0xFF40:return self.lcd._LCDC.valueelif i == 0xFF41:return self.lcd._STAT.valueelif i == 0xFF42:return self.lcd.SCYelif i == 0xFF43:return self.lcd.SCXelif i == 0xFF44:return self.lcd.LYelif i == 0xFF45:return self.lcd.LYCelif i == 0xFF46:return 0x00  # DMAelif i == 0xFF47:return self.lcd.BGP.get()elif i == 0xFF48:return self.lcd.OBP0.get()elif i == 0xFF49:return self.lcd.OBP1.get()elif i == 0xFF4A:return self.lcd.WYelif i == 0xFF4B:return self.lcd.WXelse:return self.ram.io_ports[i - 0xFF00]elif 0xFF4C <= i < 0xFF80:  # Empty but unusable for I/O# CGB registersif self.cgb and i == 0xFF4D:return self.key1elif self.cgb and i == 0xFF4F:return self.lcd.vbk.get()elif self.cgb and i == 0xFF68:return self.lcd.bcps.get() | 0x40elif self.cgb and i == 0xFF69:return self.lcd.bcpd.get()elif self.cgb and i == 0xFF6A:return self.lcd.ocps.get() | 0x40elif self.cgb and i == 0xFF6B:return self.lcd.ocpd.get()elif self.cgb and i == 0xFF51:# logger.debug("HDMA1 is not readable")return 0x00  # Not readableelif self.cgb and i == 0xFF52:# logger.debug("HDMA2 is not readable")return 0x00  # Not readableelif self.cgb and i == 0xFF53:# logger.debug("HDMA3 is not readable")return 0x00  # Not readableelif self.cgb and i == 0xFF54:# logger.debug("HDMA4 is not readable")return 0x00  # Not readableelif self.cgb and i == 0xFF55:return self.hdma.hdma5 & 0xFFelif self.cgb and i == 0xFF76:self.sound.tick(self.cpu.cycles)return self.sound.pcm12()elif self.cgb and i == 0xFF77:self.sound.tick(self.cpu.cycles)return self.sound.pcm34()return self.ram.non_io_internal_ram1[i - 0xFF4C]elif 0xFF80 <= i < 0xFFFF:  # Internal RAMreturn self.ram.internal_ram1[i - 0xFF80]elif i == 0xFFFF:  # Interrupt Enable Registerreturn self.cpu.interrupts_enabled_register# else:#     logger.critical("Memory access violation. Tried to read: %0.4x", i)def setitem(self, i, value):if 0x0000 <= i < 0x4000:  # 16kB ROM bank #0# Doesn't change the data. This is for MBC commandsself.cartridge.setitem(i, value)self.cpu.bail = Trueelif 0x4000 <= i < 0x8000:  # 16kB switchable ROM bank# Doesn't change the data. This is for MBC commandsself.cartridge.setitem(i, value)self.cpu.bail = Trueelif 0x8000 <= i < 0xA000:  # 8kB Video RAMif not self.cgb or self.lcd.vbk.active_bank == 0:self.lcd.VRAM0[i - 0x8000] = valueif i < 0x9800:  # Is within tile data -- not tile maps# Mask out the byte of the tileself.lcd.renderer.invalidate_tile(((i & 0xFFF0) - 0x8000) // 16, 0)else:self.lcd.VRAM1[i - 0x8000] = valueif i < 0x9800:  # Is within tile data -- not tile maps# Mask out the byte of the tileself.lcd.renderer.invalidate_tile(((i & 0xFFF0) - 0x8000) // 16, 1)elif 0xA000 <= i < 0xC000:  # 8kB switchable RAM bankself.cartridge.setitem(i, value)elif 0xC000 <= i < 0xE000:  # 8kB Internal RAMbank_offset = 0if self.cgb and 0xD000 <= i:# Find which bank to read from at FF70bank = self.getitem(0xFF70)bank &= 0b111if bank == 0x0:bank = 0x01bank_offset = (bank - 1) * 0x1000self.ram.internal_ram0[i - 0xC000 + bank_offset] = valueelif 0xE000 <= i < 0xFE00:  # Echo of 8kB Internal RAMself.setitem(i - 0x2000, value)  # Redirect to internal RAMelif 0xFE00 <= i < 0xFEA0:  # Sprite Attribute Memory (OAM)self.lcd.OAM[i - 0xFE00] = valueelif 0xFEA0 <= i < 0xFF00:  # Empty but unusable for I/Oself.ram.non_io_internal_ram0[i - 0xFEA0] = valueelif 0xFF00 <= i < 0xFF4C:  # I/O portsif i == 0xFF00:self.ram.io_ports[i - 0xFF00] = self.interaction.pull(value)elif 0xFF01 <= i <= 0xFF02:if self.serial.tick(self.cpu.cycles):self.cpu.set_interruptflag(INTR_SERIAL)if i == 0xFF01:self.serialbuffer[self.serialbuffer_count] = valueself.serialbuffer_count += 1self.serialbuffer_count &= 0x3FFself.serial.set_SB(value)elif i == 0xFF02:self.serial.set_SC(value)elif 0xFF04 <= i <= 0xFF07:if self.timer.tick(self.cpu.cycles):self.cpu.set_interruptflag(INTR_TIMER)if i == 0xFF04:# Pan docs:# “DIV-APU” ... is increased every time DIV’s bit 4 (5 in double-speed mode) goes from 1 to 0 ...# the counter can be made to increase faster by writing to DIV while its relevant bit is set (which# clears DIV, and triggers the falling edge).if self.timer.DIV & (0b1_0000 << self.sound.speed_shift):self.sound.tick(self.cpu.cycles)  # Process outstanding cycles# TODO: Force a falling edge tickself.sound.reset_apu_div()self.timer.reset()elif i == 0xFF05:self.timer.TIMA = valueelif i == 0xFF06:self.timer.TMA = valueelif i == 0xFF07:self.timer.TAC = value & 0b111  # TODO: Move logic to Timer classelif i == 0xFF0F:self.cpu.interrupts_flag_register = valueelif 0xFF10 <= i < 0xFF40:self.sound.tick(self.cpu.cycles)self.sound.set(i - 0xFF10, value)elif 0xFF40 <= i <= 0xFF4B:if lcd_interrupt := self.lcd.tick(self.cpu.cycles):self.cpu.set_interruptflag(lcd_interrupt)if i == 0xFF40:self.lcd.set_lcdc(value)elif i == 0xFF41:self.lcd._STAT.set(value)elif i == 0xFF42:self.lcd.SCY = valueelif i == 0xFF43:self.lcd.SCX = valueelif i == 0xFF44:# LCDC Read-onlyreturnelif i == 0xFF45:self.lcd.LYC = valueelif i == 0xFF46:self.transfer_DMA(value)elif i == 0xFF47:if self.lcd.BGP.set(value):# TODO: Move out of MBself.lcd.renderer.clear_tilecache0()elif i == 0xFF48:if self.lcd.OBP0.set(value):# TODO: Move out of MBself.lcd.renderer.clear_spritecache0()elif i == 0xFF49:if self.lcd.OBP1.set(value):# TODO: Move out of MBself.lcd.renderer.clear_spritecache1()elif i == 0xFF4A:self.lcd.WY = valueelif i == 0xFF4B:self.lcd.WX = valueelse:self.ram.io_ports[i - 0xFF00] = valueself.cpu.bail = Trueelif 0xFF4C <= i < 0xFF80:  # Empty but unusable for I/Oif self.bootrom_enabled and i == 0xFF50 and (value == 0x1 or value == 0x11):logger.debug("Bootrom disabled!")self.bootrom_enabled = Falseself.cpu.bail = True# CGB registerselif self.cgb and i == 0xFF4D:self.key1 = valueself.cpu.bail = Trueelif self.cgb and i == 0xFF4F:self.lcd.vbk.set(value)elif self.cgb and i == 0xFF51:self.hdma.hdma1 = valueelif self.cgb and i == 0xFF52:self.hdma.hdma2 = value  # & 0xF0elif self.cgb and i == 0xFF53:self.hdma.hdma3 = value  # & 0x1Felif self.cgb and i == 0xFF54:self.hdma.hdma4 = value  # & 0xF0elif self.cgb and i == 0xFF55:self.hdma.set_hdma5(value, self)self.cpu.bail = Trueelif self.cgb and i == 0xFF68:self.lcd.bcps.set(value)elif self.cgb and i == 0xFF69:self.lcd.bcpd.set(value)self.lcd.renderer.clear_tilecache0()self.lcd.renderer.clear_tilecache1()elif self.cgb and i == 0xFF6A:self.lcd.ocps.set(value)elif self.cgb and i == 0xFF6B:self.lcd.ocpd.set(value)self.lcd.renderer.clear_spritecache0()self.lcd.renderer.clear_spritecache1()else:self.ram.non_io_internal_ram1[i - 0xFF4C] = valueelif 0xFF80 <= i < 0xFFFF:  # Internal RAMself.ram.internal_ram1[i - 0xFF80] = valueelif i == 0xFFFF:  # Interrupt Enable Registerself.cpu.interrupts_enabled_register = valueself.cpu.bail = True# else:#     logger.critical("Memory access violation. Tried to write: 0x%0.2x to 0x%0.4x", value, i)def transfer_DMA(self, src):# http://problemkaputt.de/pandocs.htm#lcdoamdmatransfers# TODO: Add timing delay of 160µs and disallow access to RAM!dst = 0xFE00offset = src * 0x100for n in range(0xA0):self.setitem(dst + n, self.getitem(n + offset))

​(二)图形处理单元 (GPU)​​:

GPU负责渲染图形。Game Boy的屏幕分辨率为160x144 像素。GPU模拟模块会按照固定的时序,从显存中读取背景图、精灵(角色图块)等数据,然后将它们合成为最终的图像帧。PyBoy通过SDL2​PIL​等图形库将最终图像输出到屏幕上,或者是在无界面模式下直接供程序处理。这里主要涉及源码里面core中的lcd.py文件。
在这里插入图片描述


(三)中断检测与定时器:

模拟器还包含了系统定时器的模拟,许多游戏依赖它来计时。同时,PyBoy会捕获键盘或手柄的输入事件,并将其映射为Game Boy的十字键和A、B、选择、开始按钮事件。这里主要涉及源码里面core中的interaction.py文件。

在这里插入图片描述
主要代码如下:

from pyboy.utils import WindowEventP10, P11, P12, P13 = range(4)def reset_bit(x, bit):return x & ~(1 << bit)def set_bit(x, bit):return x | (1 << bit)class Interaction:def __init__(self):self.directional = 0xFself.standard = 0xFdef key_event(self, key):_directional = self.directional_standard = self.standardif key == WindowEvent.PRESS_ARROW_RIGHT:self.directional = reset_bit(self.directional, P10)elif key == WindowEvent.PRESS_ARROW_LEFT:self.directional = reset_bit(self.directional, P11)elif key == WindowEvent.PRESS_ARROW_UP:self.directional = reset_bit(self.directional, P12)elif key == WindowEvent.PRESS_ARROW_DOWN:self.directional = reset_bit(self.directional, P13)elif key == WindowEvent.PRESS_BUTTON_A:self.standard = reset_bit(self.standard, P10)elif key == WindowEvent.PRESS_BUTTON_B:self.standard = reset_bit(self.standard, P11)elif key == WindowEvent.PRESS_BUTTON_SELECT:self.standard = reset_bit(self.standard, P12)elif key == WindowEvent.PRESS_BUTTON_START:self.standard = reset_bit(self.standard, P13)elif key == WindowEvent.RELEASE_ARROW_RIGHT:self.directional = set_bit(self.directional, P10)elif key == WindowEvent.RELEASE_ARROW_LEFT:self.directional = set_bit(self.directional, P11)elif key == WindowEvent.RELEASE_ARROW_UP:self.directional = set_bit(self.directional, P12)elif key == WindowEvent.RELEASE_ARROW_DOWN:self.directional = set_bit(self.directional, P13)elif key == WindowEvent.RELEASE_BUTTON_A:self.standard = set_bit(self.standard, P10)elif key == WindowEvent.RELEASE_BUTTON_B:self.standard = set_bit(self.standard, P11)elif key == WindowEvent.RELEASE_BUTTON_SELECT:self.standard = set_bit(self.standard, P12)elif key == WindowEvent.RELEASE_BUTTON_START:self.standard = set_bit(self.standard, P13)# XOR to find the changed bits, AND it to see if it was high before.# Test for both directional and standard buttons.return ((_directional ^ self.directional) & _directional) or ((_standard ^ self.standard) & _standard)def pull(self, joystickbyte):P14 = (joystickbyte >> 4) & 1P15 = (joystickbyte >> 5) & 1# Bit 7 - Not used (No$GMB)# Bit 6 - Not used (No$GMB)# Bit 5 - P15 out port# Bit 4 - P14 out port# Bit 3 - P13 in port# Bit 2 - P12 in port# Bit 1 - P11 in port# Bit 0 - P10 in port# Guess to make first 4 and last 2 bits true, while keeping selected bitsjoystickByte = 0xFF & (joystickbyte | 0b11001111)if P14 and P15:joystickByte = 0xFelif not P14 and not P15:pass  # FIXME: What happens when both are requested?elif not P14:joystickByte &= self.directionalelif not P15:joystickByte &= self.standardreturn joystickByte | 0b11000000def save_state(self, f):f.write(self.directional)f.write(self.standard)def load_state(self, f, state_version):if state_version >= 7:self.directional = f.read()self.standard = f.read()else:self.directional = 0xFself.standard = 0xF

二、在pycharm中运行pyboy

对PyBoy的源码有了基础的了解后,我们就可以用它来跑一跑实际程序了,第一步是先下载和导入PyBoy的Python包,然后新建一个pyboy对象,并调用pyboy.tick()函数即可。

(一)下载pyboy包

在这里插入图片描述


(二)导入pyboy包并运行

新建main.py文件,并写入以下代码,特别的是还要准备一个游戏rom(使用游戏rom存在法律风险,请注意)并指定rom地址。

from pyboy import PyBoypyboy = PyBoy('roms/bkmy.gb')
try:while pyboy.tick():passpyboy.stop()
except Exception as e:print(e)

在这里插入图片描述


(三)展示运行效果

如果程序运行正常将得到以下运行结果,特别的是游戏rom来源网络仅做学习研究。
在这里插入图片描述

三、结语

学习PyBoy源码与GBC游戏模拟是一次充满挑战与惊喜的探索。深入源码,仿佛揭开游戏模拟的神秘面纱,理解其复杂运行逻辑。用Python操控模拟过程,让我将理论知识用于实践。这不仅提升了编程能力,更让我感受到技术还原经典游戏的魅力,收获颇丰。

http://www.dtcms.com/a/521551.html

相关文章:

  • 平顶山网站建设公司视频软件制作app
  • 手机网站模板网网站开发实训报告总结2021
  • mwf攻防。
  • 购物网站开发 书籍wordpress+去掉阅读
  • CICD实战(9) - 使用Arbess+GitLab实现Python项目自动化部署
  • 贵州两学一做教育网站怎么做点击图片进入网站
  • 阮一峰《TypeScript 教程》学习笔记——类型系统
  • 网站如何做微信支付宝支付湖南网站排名优化公司
  • 房产网站推广做自己移动端网站
  • 江阴网站建设公司vs用户登录注册网站建设代码
  • 锁和原子变量的基础介绍
  • asp网站开发实训报告公司中英文网站建设
  • 状态机之tinyfsm
  • Spring中@Configuration注解的proxyBeanMethods属性详解
  • RSI超买信号与仓位递减结合的ETF止盈策略实现与验证
  • 浙江企业网站建设上海cms建站系统
  • 太原做app网站建设上海网站建设seo公司哪家好
  • windows 安装 nginx
  • 十六、Linux网络配置
  • wordpress多站点支付插件企业网页建设公司咨询电话
  • Linux安装nvm教程(脚本与解压缩两种方式)
  • 无锡网站制作哪家便宜遵义网站建设制作公司
  • 制作网站流程湖南易图科技发展有限公司
  • 1.1、开篇:AI如何重塑网络安全攻防格局?
  • 福州 哈尔滨网站建设 网络服务连云港市网站建设
  • 电商智能客服机器人:客服的服务革新之路
  • 网站建设及推广方案桂林两江四湖象山景区简介
  • (3)Kafka生产者分区策略、ISR、ACK、一致性语义
  • 做盗链网站上国外网站用什么dns
  • 平面设计创意网站建设嵌入式软件开发招聘