amethyst / bracket-lib

The Roguelike Toolkit (RLTK), implemented for Rust.
MIT License
1.51k stars 107 forks source link

Issues using terminal with another game loop #160

Open ndarilek opened 3 years ago

ndarilek commented 3 years ago

First, thanks for this library! I've enjoyed reading your RLTK tutorial, and while I've built other games, it has inspired me to dip a toe into roguelike development.

I'm trying to use Bracket with Bevy. My first goal is to use either curses or crossterm, then scale up from there. So I don't want to use Bracket's game loop, but want to integrate it with Bevy, and wrap whatever terminal logic it needs in Bevy systems.

I've currently got this code, and it behaves differently in different scenarios. The code:

// use bevy::prelude::*;
use bracket_lib::prelude::*;

// fn render_system(mut terminal: ResMut<BTerm>) {
//     terminal.print(1, 1, "Hello, world.");
// }

fn main() -> BError {
    let mut terminal = BTermBuilder::simple(80, 25)?.build()?;
    terminal.cls();
    terminal.print(5, 5, "Hello, world.\n");
    // App::build()
    //     .add_plugin(bevy::app::ScheduleRunnerPlugin::default())
    //     .add_resource(terminal)
    //     .add_system(render_system.system())
    //     .run();
    Ok(())
}

Am I doing something wrong? Your tutorials run the ECS loop inside Bracket's. Given that I'm inverting the pattern, do I need to add some sort of terminal update loop? I see BTerm::main_loop, but I'm not sure if this is meant to wrap my game loop, or if it contains logic I need to call to update the terminal itself.

Thanks again.

ohmree commented 3 years ago

I'm also trying to use Bracket (the default GL backend) with Bevy and it just opens a non-responsive window.

I think it's because the main_loop code does some backend-specific initialization that doesn't get performed when not calling main_loop, but calling it and having your terminal stored in a resource (when using only bevy_ecs) is also impossible, the error is something about not being able to borrow it (perhaps because main_loop subsumes ownership of the terminal?).

Here's my code, I've been trying to follow the tutorial with bevy (first just the ECS and then the whole library without default features, the app abstraction is very nice to use):

use bevy::{
    app::ScheduleRunnerPlugin, core::CorePlugin, diagnostic::DiagnosticsPlugin, prelude::*,
    type_registry::TypeRegistryPlugin,
};
use bracket_lib::prelude::*;

#[derive(Bundle)]
struct Position {
    x: i32,
    y: i32,
}

#[derive(Bundle)]
struct Renderable {
    glyph: FontCharType,
    fg: RGB,
    bg: RGB,
}

struct LeftMover;

fn setup(mut commands: Commands) {
    commands
        .spawn((
            Position { x: 40, y: 25 },
            Renderable {
                glyph: to_cp437('@'),
                fg: RGB::named(YELLOW),
                bg: RGB::named(BLACK),
            },
        ))
        .spawn_batch((0..10).map(|i| {
            (
                LeftMover,
                Position { x: i * 7, y: 20 },
                Renderable {
                    glyph: to_cp437('☺'),
                    fg: RGB::named(RED),
                    bg: RGB::named(BLACK),
                },
            )
        }));
}

fn render(mut ctx: ResMut<BTerm>, mut query: Query<(&Position, &Renderable)>) {
    ctx.cls();
    for (pos, render) in &mut query.iter() {
        ctx.set(pos.x, pos.y, render.fg, render.bg, render.glyph);
    }
}

fn main() -> BError {
    let context = BTermBuilder::simple80x50().with_title("Rustlike").build()?;

    App::build()
        .add_plugin(TypeRegistryPlugin::default())
        .add_plugin(CorePlugin::default())
        .add_plugin(DiagnosticsPlugin::default())
        .add_plugin(ScheduleRunnerPlugin::default())
        .add_resource(context)
        .add_startup_system(setup.system())
        .add_system(render.system())
        .run();

    Ok(())
}
thebracket commented 3 years ago

That's going to be a tricky one to integrate. I haven't used Bevy yet, but it looks like it also wants to control the main loop. As-is, that's going to be problematic. A minimal bracket-terminal program looks like this:

use bracket_terminal::prelude::*;

struct State {}

impl GameState for State {
    fn tick(&mut self, ctx: &mut BTerm) {
        ctx.print(1, 1, "Hello Bracket World");
    }
}

fn main() -> BError {
    let context = BTermBuilder::simple80x50()
        .with_title("Hello Minimal Bracket World")
        .build()?;
    let gs: State = State {};
    main_loop(context, gs)
}

So main_loop refreshes the GL window every frame, and calls into your GameState's tick function on the way through. In the meantime, it's polling winit for window messages, doing housekeeping and handling IO. So it's doing quite a bit. Unfortunately, Bevy is also trying to do the same thing. You build your app, call the plugins, and it takes over main thread management - so main_loop isn't getting to do its updates. I think I'd have to basically rewrite bracket-terminal as a bevy plugin to get that pattern to work (maybe one day!).

I think you could use the ECS portion of Bevy, and integrate that in the same way as Specs in the RL Tutorial. I know it works with Legion (since 0.3, Legion is pretty easy to use). I'm pretty fully booked right now, but I'll see if I can find time to play with the two soon.

Tylian commented 3 years ago

Reviving a dead issue but I figure I'd share some info for people using Google and the likes.

It's possible to integrate Bevy and Bracket by using a custom runner in Bevy rather than the default one. While I'm not going to share all my code, this should at least point people in the right direction.

I ended up making a Bevy plugin that sets up a runner that's driven by Bracket's main loop, and then launches that.

use bracket_lib::prelude::*;
use bevy::prelude::*;

struct BTermState {
    app: App
}

impl GameState for BTermState {
    fn tick(&mut self, ctx: &mut BTerm) {
        // Dispatch systems
        self.app.update();

        // Render screen
        render_draw_buffer(ctx).expect("Couldn't render draw buffer");
    }
}

fn bterm_runner(mut app: App) {
    let context = app.world.remove_resource::<BTerm>()
        .expect("BTerm context doesn't exist in the world, which is required in order to run");

    main_loop(context, BTermState { app })
        .expect("Could not start BTerm main loop");
}

pub struct BTermPlugin;

impl Plugin for BTermPlugin {
    fn build(&self, app: &mut AppBuilder) {
        app.set_runner(bterm_runner);
    }
}

Now, with the above plugin initialized correctly, you can do most of the usual Bevy related things, though you will have to reimplement rendering and such on your own.

use bracket_lib::prelude::*;
use bevy::prelude::*;

use crate::bterm_plugin::BTermPlugin;

fn hello_world() {
    let draw_batch = DrawBatch::new();
    draw_batch.cls();
    draw_batch.print(Point::new(1, 1), "Hello Bevy World");
    draw_batch.submit(0).expect("Could not submit draw batch");
}

fn main() -> BError {
    let context = BTermBuilder::simple80x50()
        .with_title("Hello Bevy Bracket World")
        .build()?;

    App::build()
        .add_plugins(MinimalPlugins)
        .add_plugin(BTermPlugin)
        .insert_resource(context)
        .add_system(hello_world.system())
        .run();

    Ok(())
}

Also keep in mind that this is a multi-threaded environment, so you will have to use a DrawBatch.

Additionally, getting the BTerm context into the world as a resource is not really possible with this approach, my workaround has been editing GameState::tick to insert various newtypes before the systems are dispatched, like so:

// Example code to set up resources, don't copy paste :p
fn tick(&mut self, ctx: &mut BTerm) {
    self.app.world.insert_resource(Fps(ctx.fps));
    self.app.world.insert_resource(InputState {
        key: ctx.key,
        mouse: ctx.mouse_point()
    });
    // ... etc
}
thebracket commented 3 years ago

That's pretty awesome, thank you. I've been considering writing a bracket-bevy crate to offer bracket-terminal as a native Bevy plugin. It would mean creating a native Bevy renderer (wgpu is a pretty good target; lots of boilerplate, but manageable), but that shouldn't be too painful.

sparr commented 10 months ago

I write this comment having just discovered https://github.com/amethyst/bracket-lib/tree/master/bracket-bevy which will probably make this information obsolete, but I'm posting here for posterity and to address the specific concerns in the context of this issue. I should also mention that this is my very first Rust project, so I've probably made some beginner mistakes here.

I hacked around the Resource BTerm borrowing limitation by creating two BTerms and cloning back and forth between them. This allows both bracket-lib's main_loop and bevy's Resource and Query system to interact with it, just not at the same time.

/// Used by bevy App.set_runner().run() to allow bracket-lib to control the game loop
fn bracketlib_runner(mut app: App) {
    let bterm = BTermBuilder::simple80x50().build().unwrap();
    app.insert_resource(BevyBracket {
        bterm: bterm.clone(),
    });
    let gs = BracketGameState { app };
    let _ = main_loop(bterm, gs);
}

/// Resource allowing bevy ecs to interact with bracket-lib functionality
// https://bevyengine.org/learn/book/getting-started/resources/ suggests using a Resource
// to allow the ECS access to globally unique data such as a renderer
#[derive(Resource)]
pub struct BevyBracket {
    pub bterm: BTerm,
}

/// Used by bracket-lib while running its game loop
pub struct BracketGameState {
    /// Makes bevy and bevy ecs functionality available to bracket-lib's main_loop and tick
    pub app: App,
}
impl GameState for BracketGameState {
    /// Called once per frame by bracket-lib's main_loop
    fn tick(&mut self, bterm: &mut BTerm) {
        // Reference lifetime problems arise if trying to put a reference to ctx into BracketLib Resource
        // Workaround is to clone from bterm into the resource, tick, then clone back.
        // bterm is stale between the clones here, and the resource is stale outside of this function,
        // but neither is used while stale so that's ok (for now).
        // TODO: Find a better way to handle this.
        self.app
            .world
            .resource_mut::<BevyBracket>()
            .bterm
            .clone_from(bterm);
        self.app.update();
        bterm.clone_from(&self.app.world.resource_mut::<BevyBracket>().bterm)
    }
}

/// Create the bevy App, set up plugins and systems, run the custom App runner
pub fn run() {
    App::new()
        .add_plugins(MinimalPlugins)
        .add_systems(Startup, add_things)
        .add_systems(Update, draw_things)
        .set_runner(bracketlib_runner)
        .run();
}