#bevy #view #game-state #save #model-view-controller

moonshine-view

Generic Model/View framework designed for Bevy

7 releases

new 0.1.6 Nov 30, 2024
0.1.5 Jul 21, 2024
0.1.3 May 20, 2024
0.1.1 Mar 25, 2024

#611 in Game dev

Download history 23/week @ 2024-09-13 31/week @ 2024-09-20 5/week @ 2024-09-27 1/week @ 2024-10-04 142/week @ 2024-11-29

142 downloads per month

MIT license

27KB
407 lines

👁️ Moonshine View

crates.io downloads docs.rs license stars

Generic Model/View framework designed for Bevy and the Moonshine save system.

Overview

The Moonshine Save system is intentionally designed to encourage the user to separate the persistent game state (model) from its aesthetic elements (view). This provides a clear separation of concerns and has various benefits which are explained in detail in the save framework documentation.

An issue with this approach is that it adds additional complexity that the developer has to maintain. Typically, this involves manually de/spawning views associated with saved entities and synchronizing them with the game state via systems.

This crate aims to reduce some of this complexity by providing a more generic and ergonomic solution for synchronizing the game view with the game state.

Usage

Viewables

By definition, a Component is Viewable if a view can be built for it using BuildView.

An Entity is Viewable if it has at least one component which implements BuildView.

use bevy::prelude::*;
use moonshine_core::prelude::*;
use moonshine_view::prelude::*;

#[derive(Component)]
struct Bird;

impl BuildView for Bird {
    fn build(world: &World, object: Object<Self>, view: ViewCommands<Self>) {
        let asset_server = world.resource::<AssetServer>();
        // ...
        for child in object.children() {
            // ...
        }
    }
}

// Remember to register viewable types:
let mut app = App::new();
app.register_viewable::<Bird>();

You may also define a Kind as viewable:

use bevy::prelude::*;
use moonshine_core::prelude::*;
use moonshine_view::prelude::*;

#[derive(Component)]
struct Bird;

#[derive(Component)]
struct Monkey;

struct Creature;

impl Kind for Creature {
    type Filter = Or<(With<Bird>, With<Monkey>)>;
}

impl BuildView for Creature {
    fn build(world: &World, object: Object<Self>, view: ViewCommands<Self>) {
        // All creatures look the same!
    }
}

// Remember to register viewable types:
let mut app = App::new();
app.register_viewable::<Creature>();

This is useful when you want to define the same view for multiple kinds of entities.

In the example above, Creature::build is called for both Monkies and Birds.

Views may be defined polymorphically:

use bevy::prelude::*;
use moonshine_core::prelude::*;
use moonshine_view::prelude::*;

#[derive(Component)]
struct Bird;

#[derive(Component)]
struct Monkey;

struct Creature;

impl Kind for Creature {
    type Filter = Or<(With<Bird>, With<Monkey>)>;
}

impl BuildView<Creature> for Bird {
    fn build(world: &World, object: Object<Creature>, view: ViewCommands<Creature>) {
        // Birds look different, but they're still creatures!
    }
}

impl BuildView<Creature> for Monkey {
    fn build(world: &World, object: Object<Creature>, view: ViewCommands<Creature>) {
        // Monkeys look different, but they're still creatures!
    }
}

// Polymorphic views are registered slightly differently:
let mut app = App::new();
app.add_view::<Creature, Bird>()
    .add_view::<Creature, Monkey>();

This is useful when you want to build a different version of the same view for multiple kinds of entities.

In the example above, Bird::build is called for Birds, while Monkey::build is called for Monkies.

Multiple views may be associated with the same viewable kind:

use bevy::prelude::*;
use moonshine_core::prelude::*;
use moonshine_view::prelude::*;

#[derive(Component)]
struct Bird;

#[derive(Component)]
struct Monkey;

struct Creature;

impl Kind for Creature {
    type Filter = Or<(With<Bird>, With<Monkey>)>;
}

impl BuildView for Creature {
    fn build(world: &World, object: Object<Self>, view: ViewCommands<Self>) {
        // All creatures have the same body!
    }
}

impl BuildView<Creature> for Bird {
    fn build(world: &World, object: Object<Creature>, view: ViewCommands<Creature>) {
        // Birds get wings!
    }
}

impl BuildView<Creature> for Monkey {
    fn build(world: &World, object: Object<Creature>, view: ViewCommands<Creature>) {
        // Monkeys get tails!
    }
}

This is useful when you share some aspects of a view between multiple kinds of entities.

In the example above, Creature::build is called for both Monkies and Birds, while Bird::build is also called for Birds.

[!WARNING] Order of operations is undefined when multiple views are built for the same entity kind.
Prefer to add components/children when building views to avoid ordering issues.

Viewable ⇄ View

When a viewable entity is spawned, a View Entity is spawned with it.

A view entity is an entity with at least one View<T> component. Each View<T> is associated with its model entity via Viewable<T>.

When a Viewable<T> is despawned, or if it is no longer of Kind T, the associated view entity is despawned with it.

Together, Viewable<T> and View<T> form a two-way link between the game state and the game view.

Synchronization

At runtime, it is often required to update the view state based on the viewable state. For example, if an entity's position changes, so should the position of its view.

To solve this, consider using a system which either updates the view based on latest viewable state ("push") or queries the viewable from the view ("pull").

The "push" approach should be preferred because it often leads to less iterations per update cycle.

use bevy::prelude::*;
use moonshine_core::prelude::*;
use moonshine_view::prelude::*;

#[derive(Component)]
struct Bird;

impl BuildView for Bird {
    fn build(world: &World, object: Object<Self>, view: ViewCommands<Self>) {
        // ...
    }
}

// Update view from viewable, if needed (preferred)
fn view_bird_changed(query: Query<(&Bird, &Viewable<Bird>), Changed<Bird>>) {
    for (bird, model) in query.iter() {
        let view = model.view();
        // ...
    }
}

// Query model from view constantly (typically less efficient)
fn view_bird(views: Query<&View<Bird>>, query: Query<&Bird, Changed<Bird>>) {
    for view in views.iter() {
        let viewable = view.viewable();
        if let Ok(bird) = query.get(viewable.entity()) {
            // ...
        }
    }
}

The root view entity is automatically marked with Unload.

This means the entire view entity hierarchy is despawned whenever a new game state is loaded.

Untyped Viewables

Because the view system uses Kind for type safety, there is no access to views of a given viewable entity via a component.

Instead, you may query all views associated with an entity by using the Viewables resource:

use bevy::prelude::*;
use moonshine_view::prelude::*;

fn update_views_generic(viewables: Res<Viewables>) {
    for viewable_entity in viewables.iter() {
        for view_entity in viewables.views(viewable_entity) {
            // ...
        }
    }
}

Examples

See shapes.rs for a complete usage example.

Support

Please post an issue for any bugs, questions, or suggestions.

You may also contact me on the official Bevy Discord server as @Zeenobit.

Dependencies

~19–30MB
~525K SLoC