How do I structure an app for a Chip-8 interpreter? (Design Patterns)

Update: I hacked out a simple game loop in src/bin/oxide-cli.rs using canvases. I would still appreciate feedback about integrating this into the app.

Original Post: I’m doing a Chip-8 interpreter as my first Rust projec but I’m struggling with designing the application frontend and how to draw a frame.

For the app structure, I currently have it designed as several screens: a menu, a game (working on it), a debug mode (not yet implemented), and a way to load ROMs (not yet implemented). Is this design pattern of maintaining a screen state and each cycle passing off the job of drawing and handling events to the current screen a valid design pattern and should I continue with it or should I move to something else? Something that I’m not too sure about is: who should manage the emulator state? Should I have the game screen solely manage that state?

For drawing, the screen is a 1-D array of booleans. Should I be using the canvas module to draw each frame? How is this done?

My file tree:
├── app.rs → Hands off drawing and event duties.
├── core.rs → Chip-8 interpreter.
├── lib.rs
├── main.rs → Runs the app.
└── screens
 ├── debug.rs → In the future I want to be able to step through the ROM instructions.
 ├── game.rs → Manages the emulator state and draws the game.
 ├── menu.rs → Main menu.
 ├── mod.rs
 └── widgets
  ├── mod.rs
  └── title.rs → Cute little logo.

There’s many ways to handle app loops. See the examples in the repo for some of those ways Bear in mind if you’re looking at the main branch then you’re viewing code which will be compiled against the not yet released version of Ratatui. See the latest branch or the 0.29 tag instead if you want code that will work with the current release.

At a high level, it sounds like your app has several states. The simplest answer that I like to this is use an enum to represent each state with data on each variant that handles each screen’s state. e.g. something like:

enum Screen {
    Menu(MenuScreen),
    Game(GameScreen),
    Debug(DebugScreen),
}

impl Widget for &Screen {
    fn render(self, area: Rect, buffer: &mut Buffer) {
        match self {
            Self::Menu(menu) => menu.render(area, buffer);
            Self::Game(game) = game.render(area, buffer);
            Self::Debug(debug) => debug.render(area, buffer);
        }
    }
}

struct MenuScreen {
    // various fields that are only relevant to the menu
}

impl Widget for &MenuScreen {
    fn render(self, area: Rect, buffer: &mut Buffer) {
        // ... code to render the menu
    }
}

...

This approach works if your app only ever has one set of state that it’s in. If you have to keep several states, then you might instead have something like:

struct Screens {
    menu: MenuScreen,
    game: Option<GameScreen>,
    debug: Option<DebugScreen>,
}

This assumes you use widgets as your organization. You can also use bare functions that accept Frame if you tend towards a more function based approach than an OOP based approach. Either way works.

The same ideas that apply to rendering really apply to how to handle events etc. Some main level function that dispatches the event to the right screen and methods on the screen type or functions in the module that handle the event to update state variables.

1 Like

I assume that keeping several states allows for pausing a game and continuing from the menu.

Should I make one widget for drawing the game that both the Game and Debug screen use so that the debug additionally displays the instructions with extra debug controls? And maybe different borders or embellishments?

Apologies for the late reply - lots of travel recently.

There are many ways to skin a cat - the simplest is just that show_debug is a bool flag on the game state (whether just the struct that represents the game being played, or the entire app’s state). You can think up many ways to make it more complicated :slight_smile: