使用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开发实践

一、核心组件模拟

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代码。

核心代码如下:

python 复制代码
#
# License: See LICENSE.md file
# GitHub: https://github.com/Baekalfen/PyBoy
#
import pyboy

from . import opcodes
from pyboy.utils import INTR_VBLANK, INTR_LCDC, INTR_TIMER, INTR_SERIAL, INTR_HIGHTOLOW

FLAGC, FLAGH, FLAGN, FLAGZ = range(4, 8)


logger = pyboy.logging.get_logger(__name__)


class CPU:
    def __init__(self, mb):
        self.A = 0
        self.F = 0
        self.B = 0
        self.C = 0
        self.D = 0
        self.E = 0
        self.HL = 0
        self.SP = 0
        self.PC = 0

        self.interrupts_flag_register = 0
        self.interrupts_enabled_register = 0
        self.interrupt_master_enable = False
        self.interrupt_queued = False

        self.mb = mb

        self.halted = False
        self.stopped = False
        self.is_stuck = False
        self.cycles = 0

    def 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 CPU
            self.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 backtrack

        opcode = 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 |= flag

    def tick(self, cycles_target):
        _cycles0 = self.cycles
        _target = _cycles0 + cycles_target

        if self.check_interrupts():
            self.halted = False
            # TODO: Cycles it took to handle the interrupt

        if 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 = False
            self.PC += 1
            self.PC &= 0xFFFF
        elif self.halted:
            self.cycles += cycles_target  # TODO: Number of cycles for a HALT in effect?
        self.interrupt_queued = False

        self.bail = False
        while self.cycles < _target:
            # TODO: cpu-stuck check for blargg tests?
            self.fetch_and_execute()
            if self.bail:  # Possible cycles-target changes
                break

    def check_interrupts(self):
        if self.interrupt_queued:
            # Interrupt already queued. This happens only when using a debugger.
            return False

        raised_and_enabled = (self.interrupts_flag_register & 0b11111) & (self.interrupts_enabled_register & 0b11111)
        if raised_and_enabled:
            # Clear interrupt flag
            if self.halted:
                self.PC += 1  # Escape HALT on return
                self.PC &= 0xFFFF

            if 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 = True
            return True
        else:
            self.interrupt_queued = False
        return False

    def handle_interrupt(self, flag, addr):
        self.interrupts_flag_register ^= flag  # Remove flag
        self.mb.setitem((self.SP - 1) & 0xFFFF, self.PC >> 8)  # High
        self.mb.setitem((self.SP - 2) & 0xFFFF, self.PC & 0xFF)  # Low
        self.SP -= 2
        self.SP &= 0xFFFF

        self.PC = addr
        self.interrupt_master_enable = False

    def fetch_and_execute(self):
        opcode = self.mb.getitem(self.PC)
        if opcode == 0xCB:  # Extension code
            opcode = self.mb.getitem(self.PC + 1)
            opcode += 0x100  # Internally shifting look-up table

        return 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文件。

核心代码如下:

python 复制代码
# MemoryManager
    #
    def getitem(self, i):
        if 0x0000 <= i < 0x4000:  # 16kB ROM bank #0
            if 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 bank
            return self.cartridge.rombanks[self.cartridge.rombank_selected, i - 0x4000]
        elif 0x8000 <= i < 0xA000:  # 8kB Video RAM
            if 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 bank
            return self.cartridge.getitem(i)
        elif 0xC000 <= i < 0xE000:  # 8kB Internal RAM
            bank_offset = 0
            if self.cgb and 0xD000 <= i:
                # Find which bank to read from at FF70
                bank = self.ram.non_io_internal_ram1[0xFF70 - 0xFF4C] & 0b111
                if bank == 0x0:
                    bank = 0x01
                bank_offset = (bank - 1) * 0x1000
            return self.ram.internal_ram0[i - 0xC000 + bank_offset]
        elif 0xE000 <= i < 0xFE00:  # Echo of 8kB Internal RAM
            # Redirect to internal RAM
            return 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/O
            return self.ram.non_io_internal_ram0[i - 0xFEA0]
        elif 0xFF00 <= i < 0xFF4C:  # I/O ports
            if 0xFF01 <= i <= 0xFF02:
                if self.serial.tick(self.cpu.cycles):
                    self.cpu.set_interruptflag(INTR_SERIAL)
                if i == 0xFF01:
                    return self.serial.SB
                elif i == 0xFF02:
                    return self.serial.SC
            elif 0xFF04 <= i <= 0xFF07:
                if self.timer.tick(self.cpu.cycles):
                    self.cpu.set_interruptflag(INTR_TIMER)

                if i == 0xFF04:
                    return self.timer.DIV
                elif i == 0xFF05:
                    return self.timer.TIMA
                elif i == 0xFF06:
                    return self.timer.TMA
                elif i == 0xFF07:
                    return self.timer.TAC
            elif i == 0xFF0F:
                return self.cpu.interrupts_flag_register
            elif 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.value
                elif i == 0xFF41:
                    return self.lcd._STAT.value
                elif i == 0xFF42:
                    return self.lcd.SCY
                elif i == 0xFF43:
                    return self.lcd.SCX
                elif i == 0xFF44:
                    return self.lcd.LY
                elif i == 0xFF45:
                    return self.lcd.LYC
                elif i == 0xFF46:
                    return 0x00  # DMA
                elif 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.WY
                elif i == 0xFF4B:
                    return self.lcd.WX
            else:
                return self.ram.io_ports[i - 0xFF00]
        elif 0xFF4C <= i < 0xFF80:  # Empty but unusable for I/O
            # CGB registers
            if self.cgb and i == 0xFF4D:
                return self.key1
            elif self.cgb and i == 0xFF4F:
                return self.lcd.vbk.get()
            elif self.cgb and i == 0xFF68:
                return self.lcd.bcps.get() | 0x40
            elif self.cgb and i == 0xFF69:
                return self.lcd.bcpd.get()
            elif self.cgb and i == 0xFF6A:
                return self.lcd.ocps.get() | 0x40
            elif 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 readable
            elif self.cgb and i == 0xFF52:
                # logger.debug("HDMA2 is not readable")
                return 0x00  # Not readable
            elif self.cgb and i == 0xFF53:
                # logger.debug("HDMA3 is not readable")
                return 0x00  # Not readable
            elif self.cgb and i == 0xFF54:
                # logger.debug("HDMA4 is not readable")
                return 0x00  # Not readable
            elif self.cgb and i == 0xFF55:
                return self.hdma.hdma5 & 0xFF
            elif 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 RAM
            return self.ram.internal_ram1[i - 0xFF80]
        elif i == 0xFFFF:  # Interrupt Enable Register
            return 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 commands
            self.cartridge.setitem(i, value)
            self.cpu.bail = True
        elif 0x4000 <= i < 0x8000:  # 16kB switchable ROM bank
            # Doesn't change the data. This is for MBC commands
            self.cartridge.setitem(i, value)
            self.cpu.bail = True
        elif 0x8000 <= i < 0xA000:  # 8kB Video RAM
            if not self.cgb or self.lcd.vbk.active_bank == 0:
                self.lcd.VRAM0[i - 0x8000] = value
                if i < 0x9800:  # Is within tile data -- not tile maps
                    # Mask out the byte of the tile
                    self.lcd.renderer.invalidate_tile(((i & 0xFFF0) - 0x8000) // 16, 0)
            else:
                self.lcd.VRAM1[i - 0x8000] = value
                if i < 0x9800:  # Is within tile data -- not tile maps
                    # Mask out the byte of the tile
                    self.lcd.renderer.invalidate_tile(((i & 0xFFF0) - 0x8000) // 16, 1)
        elif 0xA000 <= i < 0xC000:  # 8kB switchable RAM bank
            self.cartridge.setitem(i, value)
        elif 0xC000 <= i < 0xE000:  # 8kB Internal RAM
            bank_offset = 0
            if self.cgb and 0xD000 <= i:
                # Find which bank to read from at FF70
                bank = self.getitem(0xFF70)
                bank &= 0b111
                if bank == 0x0:
                    bank = 0x01
                bank_offset = (bank - 1) * 0x1000
            self.ram.internal_ram0[i - 0xC000 + bank_offset] = value
        elif 0xE000 <= i < 0xFE00:  # Echo of 8kB Internal RAM
            self.setitem(i - 0x2000, value)  # Redirect to internal RAM
        elif 0xFE00 <= i < 0xFEA0:  # Sprite Attribute Memory (OAM)
            self.lcd.OAM[i - 0xFE00] = value
        elif 0xFEA0 <= i < 0xFF00:  # Empty but unusable for I/O
            self.ram.non_io_internal_ram0[i - 0xFEA0] = value
        elif 0xFF00 <= i < 0xFF4C:  # I/O ports
            if 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] = value
                    self.serialbuffer_count += 1
                    self.serialbuffer_count &= 0x3FF
                    self.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 tick
                        self.sound.reset_apu_div()

                    self.timer.reset()
                elif i == 0xFF05:
                    self.timer.TIMA = value
                elif i == 0xFF06:
                    self.timer.TMA = value
                elif i == 0xFF07:
                    self.timer.TAC = value & 0b111  # TODO: Move logic to Timer class
            elif i == 0xFF0F:
                self.cpu.interrupts_flag_register = value
            elif 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 = value
                elif i == 0xFF43:
                    self.lcd.SCX = value
                elif i == 0xFF44:
                    # LCDC Read-only
                    return
                elif i == 0xFF45:
                    self.lcd.LYC = value
                elif i == 0xFF46:
                    self.transfer_DMA(value)
                elif i == 0xFF47:
                    if self.lcd.BGP.set(value):
                        # TODO: Move out of MB
                        self.lcd.renderer.clear_tilecache0()
                elif i == 0xFF48:
                    if self.lcd.OBP0.set(value):
                        # TODO: Move out of MB
                        self.lcd.renderer.clear_spritecache0()
                elif i == 0xFF49:
                    if self.lcd.OBP1.set(value):
                        # TODO: Move out of MB
                        self.lcd.renderer.clear_spritecache1()
                elif i == 0xFF4A:
                    self.lcd.WY = value
                elif i == 0xFF4B:
                    self.lcd.WX = value
            else:
                self.ram.io_ports[i - 0xFF00] = value
            self.cpu.bail = True
        elif 0xFF4C <= i < 0xFF80:  # Empty but unusable for I/O
            if self.bootrom_enabled and i == 0xFF50 and (value == 0x1 or value == 0x11):
                logger.debug("Bootrom disabled!")
                self.bootrom_enabled = False
                self.cpu.bail = True
            # CGB registers
            elif self.cgb and i == 0xFF4D:
                self.key1 = value
                self.cpu.bail = True
            elif self.cgb and i == 0xFF4F:
                self.lcd.vbk.set(value)
            elif self.cgb and i == 0xFF51:
                self.hdma.hdma1 = value
            elif self.cgb and i == 0xFF52:
                self.hdma.hdma2 = value  # & 0xF0
            elif self.cgb and i == 0xFF53:
                self.hdma.hdma3 = value  # & 0x1F
            elif self.cgb and i == 0xFF54:
                self.hdma.hdma4 = value  # & 0xF0
            elif self.cgb and i == 0xFF55:
                self.hdma.set_hdma5(value, self)
                self.cpu.bail = True
            elif 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] = value
        elif 0xFF80 <= i < 0xFFFF:  # Internal RAM
            self.ram.internal_ram1[i - 0xFF80] = value
        elif i == 0xFFFF:  # Interrupt Enable Register
            self.cpu.interrupts_enabled_register = value
            self.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 = 0xFE00
        offset = src * 0x100
        for 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文件。

主要代码如下:

python 复制代码
from pyboy.utils import WindowEvent

P10, 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 = 0xF
        self.standard = 0xF

    def key_event(self, key):
        _directional = self.directional
        _standard = self.standard
        if 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) & 1
        P15 = (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 bits
        joystickByte = 0xFF & (joystickbyte | 0b11001111)
        if P14 and P15:
            joystickByte = 0xF
        elif not P14 and not P15:
            pass  # FIXME: What happens when both are requested?
        elif not P14:
            joystickByte &= self.directional
        elif not P15:
            joystickByte &= self.standard

        return joystickByte | 0b11000000

    def 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 = 0xF
            self.standard = 0xF

二、在pycharm中运行pyboy

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

(一)下载pyboy包


(二)导入pyboy包并运行

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

python 复制代码
from pyboy import PyBoy

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

(三)展示运行效果

如果程序运行正常将得到以下运行结果,特别的是游戏rom来源网络仅做学习研究。

三、结语

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

相关推荐
坚持就完事了4 小时前
正则表达式与Python的re模块
python·正则表达式
Alex艾力的IT数字空间4 小时前
基于PyTorch和CuPy的GPU并行化遗传算法实现
数据结构·人工智能·pytorch·python·深度学习·算法·机器学习
keerduoba4 小时前
EWCCTF2025 Tacticool Bin wp
python
a2006380124 小时前
ply(python版本的flex/bison or Lex/Yacc)
python
wokaoyan19814 小时前
逻辑推演题——谁是骗子
python
九年义务漏网鲨鱼4 小时前
利用AI大模型重构陈旧代码库 (Refactoring Legacy Codebase with AI)
python
滑水滑成滑头4 小时前
**标题:发散创新:智能交通系统的深度探究与实现**摘要:本文将详细
java·人工智能·python
闭着眼睛学算法5 小时前
【双机位A卷】华为OD笔试之【哈希表】双机位A-跳房子I【Py/Java/C++/C/JS/Go六种语言】【欧弟算法】全网注释最详细分类最全的华子OD真题题解
java·c语言·c++·python·算法·华为od·散列表
无限码力5 小时前
华为OD技术面真题 - Python开发 - 2
python·华为od·华为od技术面真题·华为od技术面八股·华为od技术面python八股·华为od面试python真题·华为odpython八股